def check_increasing(x, y):
    """Determine whether y is monotonically correlated with x.

    y is found increasing or decreasing with respect to x based on a Spearman
    correlation test.

    Parameters
    ----------
    x : array-like of shape (n_samples,)
            Training data.

    y : array-like of shape (n_samples,)
        Training target.

    Returns
    -------
    increasing_bool : boolean
        Whether the relationship is increasing or decreasing.

    Notes
    -----
    The Spearman correlation coefficient is estimated from the data, and the
    sign of the resulting estimate is used as the result.

    In the event that the 95% confidence interval based on Fisher transform
    spans zero, a warning is raised.

    References
    Fisher transformation. Wikipedia.
    https://en.wikipedia.org/wiki/Fisher_transformation

    Examples
    --------
    >>> from sklearn.isotonic import check_increasing
    >>> x, y = [1, 2, 3, 4, 5], [2, 4, 6, 8, 10]
    >>> check_increasing(x, y)
    np.True_
    >>> y = [10, 8, 6, 4, 2]
    np.False_
    """

    # Calculate Spearman rho estimate and set return accordingly.
    rho, _ = spearmanr(x, y)
    increasing_bool = rho >= 0

    # Run Fisher transform to get the rho CI, but handle rho=+/-1
    if rho not in [-1.0, 1.0] and len(x) > 3:
        F = 0.5 * math.log((1.0 + rho) / (1.0 - rho))
        F_se = 1 / math.sqrt(len(x) - 3)

        # Use a 95% CI, i.e., +/-1.96 S.E.
        # https://en.wikipedia.org/wiki/Fisher_transformation
        rho_0 = math.tanh(F - 1.96 * F_se)
        rho_1 = math.tanh(F + 1.96 * F_se)

        # Warn if the CI spans zero.
        if np.sign(rho_0) != np.sign(rho_1):
            warnings.warn(
                "Confidence interval of the Spearman "
                "correlation coefficient spans zero. "
                "Determination of ``increasing`` may be "
                "suspect."
            )

    return increasing_bool

def isotonic_regression(
    y, *, sample_weight=None, y_min=None, y_max=None, increasing=True
):
    """Solve the isotonic regression model.

    Read more in the :ref:`User Guide <isotonic>`.

        The data.

    sample_weight : array-like of shape (n_samples,), default=None
        Weights on each point of the regression.
        If None, weight is set to 1 (equal weights).

    y_min : float, default=None
        Lower bound on the lowest predicted value (the minimum value may
        still be higher). If not set, defaults to -inf.

    y_max : float, default=None
        Upper bound on the highest predicted value (the maximum may still be
        lower). If not set, defaults to +inf.

    increasing : bool, default=True
        Whether to compute ``y_`` is increasing (if set to True) or decreasing
        (if set to False).

    Returns
    -------
    y_ : ndarray of shape (n_samples,)
        Isotonic fit of y.

    "Active set algorithms for isotonic regression; A unifying framework"
    by Michael J. Best and Nilotpal Chakravarti, section 3.

    Examples
    --------
    >>> from sklearn.isotonic import isotonic_regression
    >>> isotonic_regression([5, 3, 1, 2, 8, 10, 7, 9, 6, 4])
    array([2.75   , 2.75   , 2.75   , 2.75   , 7.33...,
           7.33..., 7.33..., 7.33..., 7.33..., 7.33...])
    """
    y = check_array(y, ensure_2d=False, input_name="y", dtype=[np.float64, np.float32])
    if sp_base_version >= parse_version("1.12.0"):
        res = optimize.isotonic_regression(
            y=y, weights=sample_weight, increasing=increasing
        )
        y = np.asarray(res.x, dtype=y.dtype)
    else:
        # TODO: remove this branch when Scipy 1.12 is the minimum supported version
        # Also remove _inplace_contiguous_isotonic_regression.
        order = np.s_[:] if increasing else np.s_[::-1]
        y = np.array(y[order], dtype=y.dtype)
        sample_weight = _check_sample_weight(sample_weight, y, dtype=y.dtype, copy=True)
        sample_weight = np.ascontiguousarray(sample_weight[order])
        _inplace_contiguous_isotonic_regression(y, sample_weight)
        y = y[order]

    if y_min is not None or y_max is not None:
        # Older versions of np.clip don't accept None as a bound, so use np.inf
        if y_min is None:
            y_min = -np.inf
        if y_max is None:
            y_max = np.inf
        np.clip(y, y_min, y_max, y)
    return y

class IsotonicRegression(RegressorMixin, TransformerMixin, BaseEstimator):
    """Isotonic regression model.

    .. versionadded:: 0.13

    increasing : bool or 'auto', default=True
        Determines whether the predictions should be constrained to increase
        or decrease with `X`. 'auto' will decide based on the Spearman
        correlation estimate's sign.

    out_of_bounds : {'nan', 'clip', 'raise'}, default='nan'
        Handles how `X` values outside of the training domain are handled
        during prediction.

        - 'nan', predictions will be NaN.
        - 'clip', predictions will be set to the value corresponding to
          the nearest train interval endpoint.
        - 'raise', a `ValueError` is raised.

    Attributes
    X_min_ : float
        Minimum value of input array `X_` for left bound.

    X_max_ : float
        Maximum value of input array `X_` for right bound.

    X_thresholds_ : ndarray of shape (n_thresholds,)
        Unique ascending `X` values used to interpolate
        the y = f(X) monotonic function.

        .. versionadded:: 0.24

    y_thresholds_ : ndarray of shape (n_thresholds,)
        De-duplicated `y` values suitable to interpolate the y = f(X)
        monotonic function.

    f_ : function
        The stepwise interpolating function that covers the input domain ``X``.

    increasing_ : bool
        Inferred value for ``increasing``.

    See Also
    --------
    sklearn.linear_model.LinearRegression : Ordinary least squares Linear
        Regression.
    sklearn.ensemble.HistGradientBoostingRegressor : Gradient boosting that
        is a non-parametric model accepting monotonicity constraints.
    isotonic_regression : Function to solve the isotonic regression model.

    Notes
    -----
    Ties are broken using the secondary method from de Leeuw, 1977.

    Isotonic Median Regression: A Linear Programming Approach
    Nilotpal Chakravarti
    Mathematics of Operations Research
    Vol. 14, No. 2 (May, 1989), pp. 303-308

    Isotone Optimization in R : Pool-Adjacent-Violators
    Algorithm (PAVA) and Active Set Methods
    de Leeuw, Hornik, Mair
    Journal of Statistical Software 2009

    Correctness of Kruskal's algorithms for monotone regression with ties
    de Leeuw, Psychometrica, 1977

    Examples
    --------
    >>> from sklearn.datasets import make_regression
    >>> from sklearn.isotonic import IsotonicRegression
    >>> X, y = make_regression(n_samples=10, n_features=1, random_state=41)
    >>> iso_reg = IsotonicRegression().fit(X, y)
    >>> iso_reg.predict([.1, .2])
    array([1.8628..., 3.7256...])
    """

    # T should have been called X
    __metadata_request__predict = {"T": metadata_routing.UNUSED}
    __metadata_request__transform = {"T": metadata_routing.UNUSED}

    _parameter_constraints: dict = {
        "y_min": [Interval(Real, None, None, closed="both"), None],
        "y_max": [Interval(Real, None, None, closed="both"), None],
        "increasing": ["boolean", StrOptions({"auto"})],
        "out_of_bounds": [StrOptions({"nan", "clip", "raise"})],
    }

    def __init__(self, *, y_min=None, y_max=None, increasing=True, out_of_bounds="nan"):
        self.y_min = y_min
        self.y_max = y_max
        self.increasing = increasing
        self.out_of_bounds = out_of_bounds

    def _check_input_data_shape(self, X):
        if not (X.ndim == 1 or (X.ndim == 2 and X.shape[1] == 1)):
            msg = (
                "Isotonic regression input X should be a 1d array or "
                "2d array with 1 feature"
            )
            raise ValueError(msg)

    def _build_f(self, X, y):
        """Build the f_ interp1d function."""

        bounds_error = self.out_of_bounds == "raise"
        if len(y) == 1:
            # single y, constant prediction
            self.f_ = lambda x: y.repeat(x.shape)
        else:
            self.f_ = interpolate.interp1d(
                X, y, kind="linear", bounds_error=bounds_error
            )

    def _build_y(self, X, y, sample_weight, trim_duplicates=True):
        """Build the y_ IsotonicRegression."""
        self._check_input_data_shape(X)
        X = X.reshape(-1)  # use 1d view

        # Determine increasing if auto-determination requested
        if self.increasing == "auto":
            self.increasing_ = check_increasing(X, y)
        else:
            self.increasing_ = self.increasing

        # If sample_weights is passed, removed zero-weight values and clean
        # order
        sample_weight = _check_sample_weight(sample_weight, X, dtype=X.dtype)
        mask = sample_weight > 0
        X, y, sample_weight = X[mask], y[mask], sample_weight[mask]

        order = np.lexsort((y, X))
        X, y, sample_weight = [array[order] for array in [X, y, sample_weight]]
        unique_X, unique_y, unique_sample_weight = _make_unique(X, y, sample_weight)

        X = unique_X
        y = isotonic_regression(
            unique_y,
            sample_weight=unique_sample_weight,
            y_min=self.y_min,
            y_max=self.y_max,
            increasing=self.increasing_,
        )

        # Handle the left and right bounds on X
        self.X_min_, self.X_max_ = np.min(X), np.max(X)

        if trim_duplicates:
            # Remove unnecessary points for faster prediction
            keep_data = np.ones((len(y),), dtype=bool)
            # Aside from the 1st and last point, remove points whose y values
            # are equal to both the point before and the point after it.
            keep_data[1:-1] = np.logical_or(
                np.not_equal(y[1:-1], y[:-2]), np.not_equal(y[1:-1], y[2:])
            )
            return X[keep_data], y[keep_data]
        else:
            # The ability to turn off trim_duplicates is only used to it make
            # easier to unit test that removing duplicates in y does not have
            # any impact the resulting interpolation function (besides
            # prediction speed).
            return X, y

    @_fit_context(prefer_skip_nested_validation=True)
    def fit(self, X, y, sample_weight=None):
        """Fit the model using X, y as training data.

        Parameters
        ----------
        X : array-like of shape (n_samples,) or (n_samples, 1)

            .. versionchanged:: 0.24
               Also accepts 2d array with 1 feature.

        y : array-like of shape (n_samples,)
            Training target.

        sample_weight : array-like of shape (n_samples,), default=None
            Weights. If set to None, all weights will be set to 1 (equal
            weights).

        Returns
        -------
        self : object
            Returns an instance of self.

        Notes
        -----
        X is stored for future use, as :meth:`transform` needs X to interpolate
        new input data.
        """
        check_params = dict(accept_sparse=False, ensure_2d=False)
        X = check_array(
            X, input_name="X", dtype=[np.float64, np.float32], **check_params
        )
        y = check_array(y, input_name="y", dtype=X.dtype, **check_params)
        check_consistent_length(X, y, sample_weight)

        # Transform y by running the isotonic regression algorithm and
        # transform X accordingly.
        X, y = self._build_y(X, y, sample_weight)

        # It is necessary to store the non-redundant part of the training set
        # on the model to make it possible to support model persistence via
        # the pickle module as the object built by scipy.interp1d is not
        # picklable directly.
        self.X_thresholds_, self.y_thresholds_ = X, y

        # Build the interpolation function
        self._build_f(X, y)
        return self

    def _transform(self, T):
        """`_transform` is called by both `transform` and `predict` methods.

        Since `transform` is wrapped to output arrays of specific types (e.g.
        NumPy arrays, pandas DataFrame), we cannot make `predict` call `transform`
        directly.

        The above behaviour could be changed in the future, if we decide to output
        other type of arrays when calling `predict`.
        """
        if hasattr(self, "X_thresholds_"):
            dtype = self.X_thresholds_.dtype
        else:
            dtype = np.float64

        T = check_array(T, dtype=dtype, ensure_2d=False)

        self._check_input_data_shape(T)
        T = T.reshape(-1)  # use 1d view

        if self.out_of_bounds == "clip":
            T = np.clip(T, self.X_min_, self.X_max_)

        res = self.f_(T)

        # on scipy 0.17, interp1d up-casts to float64, so we cast back
        res = res.astype(T.dtype)

        return res

    def transform(self, T):
        """Transform new data by linear interpolation.

        T : array-like of shape (n_samples,) or (n_samples, 1)
            Data to transform.

        Returns
        -------
        y_pred : ndarray of shape (n_samples,)
            The transformed data.
        """
        return self._transform(T)

    def predict(self, T):
        """Predict new data by linear interpolation.

        Returns
        -------
            Transformed data.
        """

    # We implement get_feature_names_out here instead of using
    # `ClassNamePrefixFeaturesOutMixin`` because `input_features` are ignored.
    # `input_features` are ignored because `IsotonicRegression` accepts 1d
    # arrays and the semantics of `feature_names_in_` are not clear for 1d arrays.
    def get_feature_names_out(self, input_features=None):
        """Get output feature names for transformation.

        input_features : array-like of str or None, default=None
            Ignored.

        Returns
        -------
        feature_names_out : ndarray of str objects
            An ndarray with one string i.e. ["isotonicregression0"].
        """
        check_is_fitted(self, "f_")
        class_name = self.__class__.__name__.lower()
        return np.asarray([f"{class_name}0"], dtype=object)

    def __getstate__(self):
        """Pickle-protocol - return state of the estimator."""
        state = super().__getstate__()
        # remove interpolation method
        state.pop("f_", None)
        return state

    def __setstate__(self, state):
        """Pickle-protocol - set state of the estimator.

        We need to rebuild the interpolation function.
        """
        super().__setstate__(state)
        if hasattr(self, "X_thresholds_") and hasattr(self, "y_thresholds_"):
            self._build_f(self.X_thresholds_, self.y_thresholds_)

    def __sklearn_tags__(self):
        tags = super().__sklearn_tags__()
        tags.input_tags.one_d_array = True
        tags.input_tags.two_d_array = False
        return tags

            msg = (
            )

        else:
            )

        else:

        # order

            unique_y,
        )

            )
        else:
            return X, y

            weights).

        Returns
        -------

        Notes
        -----
        """
        )

        directly.

        """
        else:

        return res

        Returns
        -------
        """

        Returns
        -------
        """

            Ignored.

        Returns
        -------
        """

        """

def johnson_lindenstrauss_min_dim(n_samples, *, eps=0.1):
    """Find a 'safe' number of components to randomly project to.

    The distortion introduced by a random projection `p` only changes the
    distance between two points by a factor (1 +- eps) in a euclidean space
    with good probability. The projection `p` is an eps-embedding as defined
    by:

    .. code-block:: text

      (1 - eps) ||u - v||^2 < ||p(u) - p(v)||^2 < (1 + eps) ||u - v||^2

    Where u and v are any rows taken from a dataset of shape (n_samples,
    n_features), eps is in ]0, 1[ and p is a projection by a random Gaussian
    N(0, 1) matrix of shape (n_components, n_features) (or a sparse
    Achlioptas matrix).

    The minimum number of components to guarantee the eps-embedding is
    given by:

      n_components >= 4 log(n_samples) / (eps^2 / 2 - eps^3 / 3)

    Note that the number of dimensions is independent of the original
    number of features but instead depends on the size of the dataset:
    the larger the dataset, the higher is the minimal dimensionality of
    an eps-embedding.

    Read more in the :ref:`User Guide <johnson_lindenstrauss>`.

    n_samples : int or array-like of int
        Number of samples that should be an integer greater than 0. If an array
        is given, it will compute a safe number of components array-wise.

    eps : float or array-like of shape (n_components,), dtype=float, \
            default=0.1
        Maximum distortion rate in the range (0, 1) as defined by the
        Johnson-Lindenstrauss lemma. If an array is given, it will compute a
        safe number of components array-wise.

    Returns
    -------
    n_components : int or ndarray of int
        The minimal number of components to guarantee with good probability
        an eps-embedding with n_samples.

    .. [1] https://en.wikipedia.org/wiki/Johnson%E2%80%93Lindenstrauss_lemma

    .. [2] `Sanjoy Dasgupta and Anupam Gupta, 1999,
           "An elementary proof of the Johnson-Lindenstrauss Lemma."
           <https://citeseerx.ist.psu.edu/doc_view/pid/95cd464d27c25c9c8690b378b894d337cdf021f9>`_

    Examples
    --------
    >>> from sklearn.random_projection import johnson_lindenstrauss_min_dim
    >>> johnson_lindenstrauss_min_dim(1e6, eps=0.5)
    np.int64(663)

    >>> johnson_lindenstrauss_min_dim(1e6, eps=[0.5, 0.1, 0.01])
    array([    663,   11841, 1112658])

    >>> johnson_lindenstrauss_min_dim([1e4, 1e5, 1e6], eps=0.1)
    array([ 7894,  9868, 11841])
    """
    eps = np.asarray(eps)
    n_samples = np.asarray(n_samples)

    if np.any(eps <= 0.0) or np.any(eps >= 1):
        raise ValueError("The JL bound is defined for eps in ]0, 1[, got %r" % eps)

    if np.any(n_samples <= 0):
        raise ValueError(
            "The JL bound is defined for n_samples greater than zero, got %r"
            % n_samples
        )

    denominator = (eps**2 / 2) - (eps**3 / 3)
    return (4 * np.log(n_samples) / denominator).astype(np.int64)

def _check_density(density, n_features):
    """Factorize density check according to Li et al."""
    if density == "auto":
        density = 1 / np.sqrt(n_features)

    elif density <= 0 or density > 1:
        raise ValueError("Expected density in range ]0, 1], got: %r" % density)
    return density

def _check_input_size(n_components, n_features):
    """Factorize argument checking for random matrix generation."""
    if n_components <= 0:
            "n_components must be strictly positive, got %d" % n_components
        )
    if n_features <= 0:
        raise ValueError("n_features must be strictly positive, got %d" % n_features)

def _gaussian_random_matrix(n_components, n_features, random_state=None):
    """Generate a dense Gaussian random matrix.

    The components of the random matrix are drawn from

        N(0, 1.0 / n_components).

    Read more in the :ref:`User Guide <gaussian_random_matrix>`.

    n_components : int,
        Dimensionality of the target projection space.

    n_features : int,
        Dimensionality of the original source space.

    random_state : int, RandomState instance or None, default=None
        Controls the pseudo random number generator used to generate the matrix
        at fit time.
        Pass an int for reproducible output across multiple function calls.
        See :term:`Glossary <random_state>`.

    Returns
    -------
    components : ndarray of shape (n_components, n_features)
        The generated Gaussian random matrix.

    See Also
    --------
    GaussianRandomProjection
    """
    _check_input_size(n_components, n_features)
    rng = check_random_state(random_state)
    components = rng.normal(
        loc=0.0, scale=1.0 / np.sqrt(n_components), size=(n_components, n_features)
    )
    return components

def _sparse_random_matrix(n_components, n_features, density="auto", random_state=None):
    """Generalized Achlioptas random sparse matrix for random projection.

    Setting density to 1 / 3 will yield the original matrix by Dimitris
    Achlioptas while setting a lower value will yield the generalization
    by Ping Li et al.

    If we note :math:`s = 1 / density`, the components of the random matrix are
    drawn from:

      - -sqrt(s) / sqrt(n_components)   with probability 1 / 2s
      -  0                              with probability 1 - 1 / s
      - +sqrt(s) / sqrt(n_components)   with probability 1 / 2s

    Read more in the :ref:`User Guide <sparse_random_matrix>`.

    density : float or 'auto', default='auto'
        Ratio of non-zero component in the random projection matrix in the
        range `(0, 1]`

        If density = 'auto', the value is set to the minimum density
        as recommended by Ping Li et al.: 1 / sqrt(n_features).

        Use density = 1 / 3.0 if you want to reproduce the results from
        Achlioptas, 2001.

    Returns
    -------
    components : {ndarray, sparse matrix} of shape (n_components, n_features)
        The generated Gaussian random matrix. Sparse matrix will be of CSR
        format.

    See Also
    --------
    SparseRandomProjection

    .. [1] Ping Li, T. Hastie and K. W. Church, 2006,
           "Very Sparse Random Projections".
           https://web.stanford.edu/~hastie/Papers/Ping/KDD06_rp.pdf

    .. [2] D. Achlioptas, 2001, "Database-friendly random projections",
           https://cgi.di.uoa.gr/~optas/papers/jl.pdf

    """
    density = _check_density(density, n_features)

    if density == 1:
        # skip index generation if totally dense
        components = rng.binomial(1, 0.5, (n_components, n_features)) * 2 - 1
        return 1 / np.sqrt(n_components) * components

    else:
        # Generate location of non zero elements
        indices = []
        offset = 0
        indptr = [offset]
        for _ in range(n_components):
            # find the indices of the non-zero components for row i
            n_nonzero_i = rng.binomial(n_features, density)
            indices_i = sample_without_replacement(
                n_features, n_nonzero_i, random_state=rng
            )
            indices.append(indices_i)
            offset += n_nonzero_i
            indptr.append(offset)

        indices = np.concatenate(indices)

        # Among non zero components the probability of the sign is 50%/50%
        data = rng.binomial(1, 0.5, size=np.size(indices)) * 2 - 1

        # build the CSR structure by concatenating the rows
        components = sp.csr_matrix(
            (data, indices, indptr), shape=(n_components, n_features)
        )

        return np.sqrt(1 / density) / np.sqrt(n_components) * components

class BaseRandomProjection(
    ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator, metaclass=ABCMeta
):
    """Base class for random projections.

    Warning: This class should not be used directly.
    Use derived classes instead.
    """

        "n_components": [
            Interval(Integral, 1, None, closed="left"),
            StrOptions({"auto"}),
        ],
        "eps": [Interval(Real, 0, None, closed="neither")],
        "compute_inverse_components": ["boolean"],
        "random_state": ["random_state"],
    }

    @abstractmethod
    def __init__(
        self,
        n_components="auto",
        *,
        eps=0.1,
        compute_inverse_components=False,
        random_state=None,
    ):
        self.n_components = n_components
        self.eps = eps
        self.compute_inverse_components = compute_inverse_components
        self.random_state = random_state

    def _make_random_matrix(self, n_components, n_features):
        """Generate the random projection matrix.

        n_components : int,
            Dimensionality of the target projection space.

        n_features : int,
            Dimensionality of the original source space.

        Returns
        -------
        components : {ndarray, sparse matrix} of shape (n_components, n_features)
            The generated random matrix. Sparse matrix will be of CSR format.

        """

    def _compute_inverse_components(self):
        """Compute the pseudo-inverse of the (densified) components."""
        components = self.components_
        if sp.issparse(components):
            components = components.toarray()
        return linalg.pinv(components, check_finite=False)

    def fit(self, X, y=None):
        """Generate a sparse random projection matrix.

        X : {ndarray, sparse matrix} of shape (n_samples, n_features)
            Training set: only the shape is used to find optimal random
            matrix dimensions based on the theory referenced in the
            afore mentioned papers.

        y : Ignored
            Not used, present here for API consistency by convention.

        Returns
        -------
            BaseRandomProjection class instance.
        """
        X = validate_data(
            self, X, accept_sparse=["csr", "csc"], dtype=[np.float64, np.float32]
        )

        n_samples, n_features = X.shape

        if self.n_components == "auto":
            self.n_components_ = johnson_lindenstrauss_min_dim(
                n_samples=n_samples, eps=self.eps
            )

            if self.n_components_ <= 0:
                raise ValueError(
                    "eps=%f and n_samples=%d lead to a target dimension of "
                    "%d which is invalid" % (self.eps, n_samples, self.n_components_)
                )

            elif self.n_components_ > n_features:
                    "%d which is larger than the original space with "
                    "n_features=%d"
                    % (self.eps, n_samples, self.n_components_, n_features)
                )
        else:
            if self.n_components > n_features:
                warnings.warn(
                    "The number of components is higher than the number of"
                    " features: n_features < n_components (%s < %s)."
                    "The dimensionality of the problem will not be reduced."
                    % (n_features, self.n_components),
                    DataDimensionalityWarning,
                )

            self.n_components_ = self.n_components

        # Generate a projection matrix of size [n_components, n_features]
        self.components_ = self._make_random_matrix(
            self.n_components_, n_features
        ).astype(X.dtype, copy=False)

        if self.compute_inverse_components:
            self.inverse_components_ = self._compute_inverse_components()

        # Required by ClassNamePrefixFeaturesOutMixin.get_feature_names_out.
        self._n_features_out = self.n_components

    def inverse_transform(self, X):
        """Project data back to its original space.

        Returns an array X_original whose transform would be X. Note that even
        if X is sparse, X_original is dense: this may use a lot of RAM.

        If `compute_inverse_components` is False, the inverse of the components is
        computed during each call to `inverse_transform` which can be costly.

        X : {array-like, sparse matrix} of shape (n_samples, n_components)
            Data to be transformed back.

        Returns
        -------
        X_original : ndarray of shape (n_samples, n_features)
            Reconstructed data.
        """
        check_is_fitted(self)

        X = check_array(X, dtype=[np.float64, np.float32], accept_sparse=("csr", "csc"))

            return X @ self.inverse_components_.T

        inverse_components = self._compute_inverse_components()
        return X @ inverse_components.T

        tags.transformer_tags.preserves_dtype = ["float64", "float32"]
        tags.input_tags.sparse = True

class GaussianRandomProjection(BaseRandomProjection):
    """Reduce dimensionality through Gaussian random projection.

    The components of the random matrix are drawn from N(0, 1 / n_components).

    n_components : int or 'auto', default='auto'

        n_components can be automatically adjusted according to the
        number of samples in the dataset and the bound given by the
        Johnson-Lindenstrauss lemma. In that case the quality of the
        embedding is controlled by the ``eps`` parameter.

        It should be noted that Johnson-Lindenstrauss lemma can yield
        very conservative estimated of the required number of components
        as it makes no assumption on the structure of the dataset.

    eps : float, default=0.1
        Parameter to control the quality of the embedding according to
        the Johnson-Lindenstrauss lemma when `n_components` is set to
        'auto'. The value should be strictly positive.

        Smaller values lead to better embedding and higher number of
        dimensions (n_components) in the target projection space.

    compute_inverse_components : bool, default=False
        Learn the inverse transform by computing the pseudo-inverse of the
        components during fit. Note that computing the pseudo-inverse does not
        scale well to large matrices.

        Controls the pseudo random number generator used to generate the
        projection matrix at fit time.

    n_components_ : int
        Concrete number of components computed when n_components="auto".

    components_ : ndarray of shape (n_components, n_features)
        Random matrix used for the projection.

    inverse_components_ : ndarray of shape (n_features, n_components)
        Pseudo-inverse of the components, only computed if
        `compute_inverse_components` is True.

        .. versionadded:: 1.1

    n_features_in_ : int
        Number of features seen during :term:`fit`.

    feature_names_in_ : ndarray of shape (`n_features_in_`,)
        Names of features seen during :term:`fit`. Defined only when `X`
        has feature names that are all strings.

        .. versionadded:: 1.0

    See Also
    --------
    SparseRandomProjection : Reduce dimensionality through sparse
        random projection.

    Examples
    --------
    >>> import numpy as np
    >>> from sklearn.random_projection import GaussianRandomProjection
    >>> rng = np.random.RandomState(42)
    >>> X = rng.rand(25, 3000)
    >>> transformer = GaussianRandomProjection(random_state=rng)
    >>> X_new = transformer.fit_transform(X)
    >>> X_new.shape
    (25, 2759)
    """

        self,
        *,
        eps=0.1,
    ):
        super().__init__(
            n_components=n_components,
            eps=eps,
            compute_inverse_components=compute_inverse_components,
            random_state=random_state,
        )

        Returns
        -------
        components : ndarray of shape (n_components, n_features)
            The generated random matrix.
        """
        random_state = check_random_state(self.random_state)
        return _gaussian_random_matrix(
            n_components, n_features, random_state=random_state
        )

    def transform(self, X):
        """Project the data by using matrix product with the random matrix.

            The input data to project into a smaller dimensional space.

        Returns
        -------
        X_new : ndarray of shape (n_samples, n_components)
            Projected array.
        """
            self,
            X,
            accept_sparse=["csr", "csc"],
            reset=False,
            dtype=[np.float64, np.float32],
        )

        return X @ self.components_.T

class SparseRandomProjection(BaseRandomProjection):
    """Reduce dimensionality through sparse random projection.

    Sparse random matrix is an alternative to dense random
    projection matrix that guarantees similar embedding quality while being
    much more memory efficient and allowing faster computation of the
    projected data.

    If we note `s = 1 / density` the components of the random matrix are

      -sqrt(s) / sqrt(n_components)   with probability 1 / 2s
       0                              with probability 1 - 1 / s
      +sqrt(s) / sqrt(n_components)   with probability 1 / 2s

        Ratio in the range (0, 1] of non-zero component in the random
        projection matrix.

        the Johnson-Lindenstrauss lemma when n_components is set to
        'auto'. This value should be strictly positive.

    dense_output : bool, default=False
        If True, ensure that the output of the random projection is a
        dense numpy array even if the input and random projection matrix
        are both sparse. In practice, if the number of components is
        small the number of zero components in the projected data will
        be very small and it will be more CPU and memory efficient to
        use a dense representation.

        If False, the projected data uses a sparse representation if
        the input is sparse.

        components during fit. Note that the pseudo-inverse is always a dense
        array, even if the training data was sparse. This means that it might be
        necessary to call `inverse_transform` on a small batch of samples at a
        time to avoid exhausting the available memory on the host. Moreover,
        computing the pseudo-inverse does not scale well to large matrices.

    components_ : sparse matrix of shape (n_components, n_features)
        Random matrix used for the projection. Sparse matrix will be of CSR
        format.

    density_ : float in range 0.0 - 1.0
        Concrete density computed from when density = "auto".

    See Also
    --------
    GaussianRandomProjection : Reduce dimensionality through Gaussian

    Examples
    --------
    >>> from sklearn.random_projection import SparseRandomProjection
    >>> transformer = SparseRandomProjection(random_state=rng)
    (25, 2759)
    >>> # very few components are non-zero
    >>> np.mean(transformer.components_ != 0)
    np.float64(0.0182...)
    """

        **BaseRandomProjection._parameter_constraints,
        "density": [Interval(Real, 0.0, 1.0, closed="right"), StrOptions({"auto"})],
        "dense_output": ["boolean"],
    }

        self,
        *,
        density="auto",
        eps=0.1,
        dense_output=False,
    ):
            eps=eps,
        )

        self.dense_output = dense_output
        self.density = density

        """Generate the random projection matrix

        n_components : int

        n_features : int

        Returns
        -------
        components : sparse matrix of shape (n_components, n_features)
            The generated random matrix in CSR format.

        """
        self.density_ = _check_density(self.density, n_features)
        return _sparse_random_matrix(
            n_components, n_features, density=self.density_, random_state=random_state
        )

        Returns
        -------
        X_new : {ndarray, sparse matrix} of shape (n_samples, n_components)
            Projected array. It is a sparse matrix only when the input is sparse and
            `dense_output = False`.
        """
            self,
            X,
        )

        return safe_sparse_dot(X, self.components_.T, dense_output=self.dense_output)

        self,
        *,
        eps=0.1,
    ):

        Returns
        -------

        """

        y : Ignored

        Returns
        -------
        """
        )

            )

                )

                )
        else:
                )

        Returns
        -------
        """

        self,
        *,
        eps=0.1,
    ):
            eps=eps,
        )

        Returns
        -------
        """
        )

        Returns
        -------
        """
            self,
            X,
        )

        self,
        *,
        eps=0.1,
    ):
            eps=eps,
        )

        Returns
        -------

        """
        )

        Returns
        -------
        """
            self,
            X,
        )

def clone(estimator, *, safe=True):
    """Construct a new unfitted estimator with the same parameters.

    Clone does a deep copy of the model in an estimator
    without actually copying attached data. It returns a new estimator
    with the same parameters that has not been fitted on any data.

    .. versionchanged:: 1.3
        Delegates to `estimator.__sklearn_clone__` if the method exists.

    estimator : {list, tuple, set} of estimator instance or a single \
            estimator instance
        The estimator or group of estimators to be cloned.
    safe : bool, default=True
        If safe is False, clone will fall back to a deep copy on objects
        that are not estimators. Ignored if `estimator.__sklearn_clone__`
        exists.

    Returns
    -------
    estimator : object
        The deep copy of the input, an estimator if input is an estimator.

    Notes
    -----
    If the estimator's `random_state` parameter is an integer (or if the
    estimator doesn't have a `random_state` parameter), an *exact clone* is
    returned: the clone and the original estimator will give the exact same
    results. Otherwise, *statistical clone* is returned: the clone might
    return different results from the original estimator. More details can be
    found in :ref:`randomness`.

    Examples
    --------
    >>> from sklearn.base import clone
    >>> from sklearn.linear_model import LogisticRegression
    >>> X = [[-1, 0], [0, 1], [0, -1], [1, 0]]
    >>> y = [0, 0, 1, 1]
    >>> classifier = LogisticRegression().fit(X, y)
    >>> cloned_classifier = clone(classifier)
    >>> hasattr(classifier, "classes_")
    True
    >>> hasattr(cloned_classifier, "classes_")
    False
    >>> classifier is cloned_classifier
    False
    """
    if hasattr(estimator, "__sklearn_clone__") and not inspect.isclass(estimator):
        return estimator.__sklearn_clone__()
    return _clone_parametrized(estimator, safe=safe)

def _clone_parametrized(estimator, *, safe=True):
    """Default implementation of clone. See :func:`sklearn.base.clone` for details."""

    estimator_type = type(estimator)
    if estimator_type is dict:
        return {k: clone(v, safe=safe) for k, v in estimator.items()}
    elif estimator_type in (list, tuple, set, frozenset):
        return estimator_type([clone(e, safe=safe) for e in estimator])
    elif not hasattr(estimator, "get_params") or isinstance(estimator, type):
        if not safe:
            return copy.deepcopy(estimator)
        else:
            if isinstance(estimator, type):
                raise TypeError(
                    "Cannot clone object. "
                    "You should provide an instance of "
                    "scikit-learn estimator instead of a class."
                )
            else:
                    "Cannot clone object '%s' (type %s): "
                    "it does not seem to be a scikit-learn "
                    "estimator as it does not implement a "
                    "'get_params' method." % (repr(estimator), type(estimator))
                )

    klass = estimator.__class__
    new_object_params = estimator.get_params(deep=False)
    for name, param in new_object_params.items():
        new_object_params[name] = clone(param, safe=False)

    new_object = klass(**new_object_params)
    try:
        new_object._metadata_request = copy.deepcopy(estimator._metadata_request)
    except AttributeError:
        pass

    params_set = new_object.get_params(deep=False)

    # quick sanity check of the parameters of the clone
    for name in new_object_params:
        param1 = new_object_params[name]
        param2 = params_set[name]
        if param1 is not param2:
            raise RuntimeError(
                "Cannot clone object %s, as the constructor "
                "either does not set or modifies parameter %s" % (estimator, name)
            )

    # _sklearn_output_config is used by `set_output` to configure the output
    # container of an estimator.
    if hasattr(estimator, "_sklearn_output_config"):
        new_object._sklearn_output_config = copy.deepcopy(
            estimator._sklearn_output_config
        )
    return new_object

class BaseEstimator(_HTMLDocumentationLinkMixin, _MetadataRequester):
    """Base class for all estimators in scikit-learn.

    Inheriting from this class provides default implementations of:

    - setting and getting parameters used by `GridSearchCV` and friends;
    - textual and HTML representation displayed in terminals and IDEs;
    - estimator serialization;
    - parameters validation;
    - data validation;
    - feature names validation.

    Read more in the :ref:`User Guide <rolling_your_own_estimator>`.

    Notes
    -----
    All estimators should specify all the parameters that can be set
    at the class level in their ``__init__`` as explicit keyword
    arguments (no ``*args`` or ``**kwargs``).

    Examples
    --------
    >>> from sklearn.base import BaseEstimator
    >>> class MyEstimator(BaseEstimator):
    ...     def __init__(self, *, param=1):
    ...         self.param = param
    ...     def fit(self, X, y=None):
    ...         self.is_fitted_ = True
    ...         return self
    ...     def predict(self, X):
    ...         return np.full(shape=X.shape[0], fill_value=self.param)
    >>> estimator = MyEstimator(param=2)
    >>> estimator.get_params()
    {'param': 2}
    >>> X = np.array([[1, 2], [2, 3], [3, 4]])
    >>> y = np.array([1, 0, 1])
    >>> estimator.fit(X, y).predict(X)
    array([2, 2, 2])
    >>> estimator.set_params(param=3).fit(X, y).predict(X)
    array([3, 3, 3])
    """

    @classmethod
    def _get_param_names(cls):
        """Get parameter names for the estimator"""
        # fetch the constructor or the original constructor before
        # deprecation wrapping if any
        init = getattr(cls.__init__, "deprecated_original", cls.__init__)
        if init is object.__init__:
            # No explicit constructor to introspect
            return []

        # introspect the constructor arguments to find the model parameters
        # to represent
        init_signature = inspect.signature(init)
        # Consider the constructor parameters excluding 'self'
        parameters = [
            p
            for p in init_signature.parameters.values()
            if p.name != "self" and p.kind != p.VAR_KEYWORD
        ]
        for p in parameters:
            if p.kind == p.VAR_POSITIONAL:
                raise RuntimeError(
                    "scikit-learn estimators should always "
                    "specify their parameters in the signature"
                    " of their __init__ (no varargs)."
                    " %s with constructor %s doesn't "
                    " follow this convention." % (cls, init_signature)
                )
        # Extract and sort argument names excluding 'self'
        return sorted([p.name for p in parameters])

    def get_params(self, deep=True):
        """
        Get parameters for this estimator.

        deep : bool, default=True
            If True, will return the parameters for this estimator and
            contained subobjects that are estimators.

        Returns
        -------
        params : dict
            Parameter names mapped to their values.
        """
        out = dict()
        for key in self._get_param_names():
            value = getattr(self, key)
            if deep and hasattr(value, "get_params") and not isinstance(value, type):
                deep_items = value.get_params().items()
                out.update((key + "__" + k, val) for k, val in deep_items)
            out[key] = value
        return out

    def set_params(self, **params):
        """Set the parameters of this estimator.

        The method works on simple estimators as well as on nested objects
        (such as :class:`~sklearn.pipeline.Pipeline`). The latter have
        parameters of the form ``<component>__<parameter>`` so that it's
        possible to update each component of a nested object.

        **params : dict
            Estimator parameters.

        Returns
        -------
        self : estimator instance
            Estimator instance.
        """
        if not params:
            # Simple optimization to gain speed (inspect is slow)
            return self
        valid_params = self.get_params(deep=True)

        nested_params = defaultdict(dict)  # grouped by prefix
        for key, value in params.items():
            key, delim, sub_key = key.partition("__")
            if key not in valid_params:
                local_valid_params = self._get_param_names()
                    f"Invalid parameter {key!r} for estimator {self}. "
                    f"Valid parameters are: {local_valid_params!r}."
                )

            if delim:
                nested_params[key][sub_key] = value
            else:
                setattr(self, key, value)
                valid_params[key] = value

        for key, sub_params in nested_params.items():
            valid_params[key].set_params(**sub_params)

    def __sklearn_clone__(self):
        return _clone_parametrized(self)

    def __repr__(self, N_CHAR_MAX=700):
        # N_CHAR_MAX is the (approximate) maximum number of non-blank
        # characters to render. We pass it as an optional parameter to ease
        # the tests.

        from .utils._pprint import _EstimatorPrettyPrinter

        N_MAX_ELEMENTS_TO_SHOW = 30  # number of elements to show in sequences

        # use ellipsis for sequences with a lot of elements
        pp = _EstimatorPrettyPrinter(
            compact=True,
            indent=1,
            indent_at_name=True,
            n_max_elements_to_show=N_MAX_ELEMENTS_TO_SHOW,
        )

        repr_ = pp.pformat(self)

        # Use bruteforce ellipsis when there are a lot of non-blank characters
        n_nonblank = len("".join(repr_.split()))
        if n_nonblank > N_CHAR_MAX:
            lim = N_CHAR_MAX // 2  # apprx number of chars to keep on both ends
            regex = r"^(\s*\S){%d}" % lim
            # The regex '^(\s*\S){%d}' % n
            # matches from the start of the string until the nth non-blank
            # character:
            # - ^ matches the start of string
            # - (pattern){n} matches n repetitions of pattern
            # - \s*\S matches a non-blank char following zero or more blanks
            left_lim = re.match(regex, repr_).end()
            right_lim = re.match(regex, repr_[::-1]).end()

            if "\n" in repr_[left_lim:-right_lim]:
                # The left side and right side aren't on the same line.
                # To avoid weird cuts, e.g.:
                # categoric...ore',
                # we need to start the right side with an appropriate newline
                # character so that it renders properly as:
                # categoric...
                # handle_unknown='ignore',
                # so we add [^\n]*\n which matches until the next \n
                regex += r"[^\n]*\n"
                right_lim = re.match(regex, repr_[::-1]).end()

            ellipsis = "..."
            if left_lim + len(ellipsis) < len(repr_) - right_lim:
                # Only add ellipsis if it results in a shorter repr
                repr_ = repr_[:left_lim] + "..." + repr_[-right_lim:]

        return repr_

        if getattr(self, "__slots__", None):
            raise TypeError(
                "You cannot use `__slots__` in objects inheriting from "
                "`sklearn.base.BaseEstimator`."
            )

        try:
            state = super().__getstate__()
            if state is None:
                # For Python 3.11+, empty instance (no `__slots__`,
                # and `__dict__`) will return a state equal to `None`.
                state = self.__dict__.copy()
        except AttributeError:
            # Python < 3.11
            state = self.__dict__.copy()

        if type(self).__module__.startswith("sklearn."):
            return dict(state.items(), _sklearn_version=__version__)
        else:
            return state

            pickle_version = state.pop("_sklearn_version", "pre-0.18")
            if pickle_version != __version__:
                    InconsistentVersionWarning(
                        estimator_name=self.__class__.__name__,
                        current_sklearn_version=__version__,
                        original_sklearn_version=pickle_version,
                    ),
                )
        try:
            super().__setstate__(state)
            self.__dict__.update(state)

        return Tags(
            estimator_type=None,
            target_tags=TargetTags(required=False),
            transformer_tags=None,
            regressor_tags=None,
            classifier_tags=None,
        )

    def _validate_params(self):
        """Validate types and values of constructor parameters

        The expected type and values must be defined in the `_parameter_constraints`
        class attribute, which is a dictionary `param_name: list of constraints`. See
        the docstring of `validate_parameter_constraints` for a description of the
        accepted constraints.
        """
        validate_parameter_constraints(
            self._parameter_constraints,
            self.get_params(deep=False),
            caller_name=self.__class__.__name__,
        )

    @property
    def _repr_html_(self):
        """HTML representation of estimator.

        This is redundant with the logic of `_repr_mimebundle_`. The latter
        should be favored in the long term, `_repr_html_` is only
        implemented for consumers who do not interpret `_repr_mimbundle_`.
        """
        if get_config()["display"] != "diagram":
            raise AttributeError(
                "_repr_html_ is only defined when the "
                "'display' configuration option is set to "
                "'diagram'"
            )
        return self._repr_html_inner

    def _repr_html_inner(self):
        """This function is returned by the @property `_repr_html_` to make
        `hasattr(estimator, "_repr_html_") return `True` or `False` depending
        on `get_config()["display"]`.
        """
        return estimator_html_repr(self)

    def _repr_mimebundle_(self, **kwargs):
        """Mime bundle used by jupyter kernels to display estimator"""
        output = {"text/plain": repr(self)}
        if get_config()["display"] == "diagram":
            output["text/html"] = estimator_html_repr(self)
        return output

class ClassifierMixin:
    """Mixin class for all classifiers in scikit-learn.

    This mixin defines the following functionality:

    - set estimator type to `"classifier"` through the `estimator_type` tag;
    - `score` method that default to :func:`~sklearn.metrics.accuracy_score`.
    - enforce that `fit` requires `y` to be passed through the `requires_y` tag,
      which is done by setting the classifier type tag.

    Examples
    --------
    >>> from sklearn.base import BaseEstimator, ClassifierMixin
    >>> # Mixin classes should always be on the left-hand side for a correct MRO
    >>> class MyEstimator(ClassifierMixin, BaseEstimator):
    >>> estimator = MyEstimator(param=1)
    array([1, 1, 1])
    >>> estimator.score(X, y)
    0.66...
    """

    # TODO(1.8): Remove this attribute
    _estimator_type = "classifier"

        tags.estimator_type = "classifier"
        tags.classifier_tags = ClassifierTags()
        tags.target_tags.required = True

    def score(self, X, y, sample_weight=None):
        """
        Return :ref:`accuracy <accuracy_score>` on provided data and labels.

        In multi-label classification, this is the subset accuracy
        which is a harsh metric since you require for each sample that
        each label set be correctly predicted.

        X : array-like of shape (n_samples, n_features)
            Test samples.

        y : array-like of shape (n_samples,) or (n_samples, n_outputs)
            True labels for `X`.

            Sample weights.

        Returns
        -------
        score : float
            Mean accuracy of ``self.predict(X)`` w.r.t. `y`.
        """
        from .metrics import accuracy_score

        return accuracy_score(y, self.predict(X), sample_weight=sample_weight)

class RegressorMixin:
    """Mixin class for all regression estimators in scikit-learn.

    - set estimator type to `"regressor"` through the `estimator_type` tag;
    - `score` method that default to :func:`~sklearn.metrics.r2_score`.
      which is done by setting the regressor type tag.

    Examples
    --------
    >>> from sklearn.base import BaseEstimator, RegressorMixin
    >>> class MyEstimator(RegressorMixin, BaseEstimator):
    >>> estimator = MyEstimator(param=0)
    >>> y = np.array([-1, 0, 1])
    array([0, 0, 0])
    0.0
    """

    _estimator_type = "regressor"

        tags.estimator_type = "regressor"
        tags.regressor_tags = RegressorTags()

        """Return :ref:`coefficient of determination <r2_score>` on test data.

        The coefficient of determination, :math:`R^2`, is defined as
        :math:`(1 - \\frac{u}{v})`, where :math:`u` is the residual
        sum of squares ``((y_true - y_pred)** 2).sum()`` and :math:`v`
        is the total sum of squares ``((y_true - y_true.mean()) ** 2).sum()``.
        The best possible score is 1.0 and it can be negative (because the
        model can be arbitrarily worse). A constant model that always predicts
        the expected value of `y`, disregarding the input features, would get
        a :math:`R^2` score of 0.0.

            Test samples. For some estimators this may be a precomputed
            kernel matrix or a list of generic objects instead with shape
            ``(n_samples, n_samples_fitted)``, where ``n_samples_fitted``
            is the number of samples used in the fitting for the estimator.

            True values for `X`.

        Returns
        -------
            :math:`R^2` of ``self.predict(X)`` w.r.t. `y`.

        Notes
        -----
        The :math:`R^2` score used when calling ``score`` on a regressor uses
        ``multioutput='uniform_average'`` from version 0.23 to keep consistent
        with default value of :func:`~sklearn.metrics.r2_score`.
        This influences the ``score`` method of all the multioutput
        regressors (except for
        :class:`~sklearn.multioutput.MultiOutputRegressor`).
        """

        from .metrics import r2_score

        y_pred = self.predict(X)
        return r2_score(y, y_pred, sample_weight=sample_weight)

class ClusterMixin:
    """Mixin class for all cluster estimators in scikit-learn.

    - set estimator type to `"clusterer"` through the `estimator_type` tag;
    - `fit_predict` method returning the cluster labels associated to each sample.

    Examples
    --------
    >>> from sklearn.base import BaseEstimator, ClusterMixin
    >>> class MyClusterer(ClusterMixin, BaseEstimator):
    ...         self.labels_ = np.ones(shape=(len(X),), dtype=np.int64)
    >>> X = [[1, 2], [2, 3], [3, 4]]
    >>> MyClusterer().fit_predict(X)
    """

    _estimator_type = "clusterer"

        tags.estimator_type = "clusterer"
        if tags.transformer_tags is not None:
            tags.transformer_tags.preserves_dtype = []

    def fit_predict(self, X, y=None, **kwargs):
        """
        Perform clustering on `X` and returns cluster labels.

            Input data.

        y : Ignored
            Not used, present for API consistency by convention.

        **kwargs : dict
            Arguments to be passed to ``fit``.

            .. versionadded:: 1.4

        Returns
        -------
        labels : ndarray of shape (n_samples,), dtype=np.int64
            Cluster labels.
        """
        # non-optimized default implementation; override when a better
        # method is possible for a given clustering algorithm
        self.fit(X, **kwargs)
        return self.labels_

class BiclusterMixin:
    """Mixin class for all bicluster estimators in scikit-learn.

    - `biclusters_` property that returns the row and column indicators;
    - `get_indices` method that returns the row and column indices of a bicluster;
    - `get_shape` method that returns the shape of a bicluster;
    - `get_submatrix` method that returns the submatrix corresponding to a bicluster.

    Examples
    --------
    >>> from sklearn.base import BaseEstimator, BiclusterMixin
    >>> class DummyBiClustering(BiclusterMixin, BaseEstimator):
    ...         self.rows_ = np.ones(shape=(1, X.shape[0]), dtype=bool)
    ...         self.columns_ = np.ones(shape=(1, X.shape[1]), dtype=bool)
    >>> X = np.array([[1, 1], [2, 1], [1, 0],
    ...               [4, 7], [3, 5], [3, 6]])
    >>> bicluster = DummyBiClustering().fit(X)
    >>> hasattr(bicluster, "biclusters_")
    True
    >>> bicluster.get_indices(0)
    (array([0, 1, 2, 3, 4, 5]), array([0, 1]))
    """

    @property
    def biclusters_(self):
        """Convenient way to get row and column indicators together.

        Returns the ``rows_`` and ``columns_`` members.
        """
        return self.rows_, self.columns_

    def get_indices(self, i):
        """Row and column indices of the `i`'th bicluster.

        Only works if ``rows_`` and ``columns_`` attributes exist.

        i : int
            The index of the cluster.

        Returns
        -------
        row_ind : ndarray, dtype=np.intp
            Indices of rows in the dataset that belong to the bicluster.
        col_ind : ndarray, dtype=np.intp
            Indices of columns in the dataset that belong to the bicluster.
        """
        rows = self.rows_[i]
        columns = self.columns_[i]
        return np.nonzero(rows)[0], np.nonzero(columns)[0]

    def get_shape(self, i):
        """Shape of the `i`'th bicluster.

        i : int

        Returns
        -------
        n_rows : int
            Number of rows in the bicluster.

        n_cols : int
            Number of columns in the bicluster.
        """
        indices = self.get_indices(i)
        return tuple(len(i) for i in indices)

    def get_submatrix(self, i, data):
        """Return the submatrix corresponding to bicluster `i`.

        i : int
        data : array-like of shape (n_samples, n_features)
            The data.

        Returns
        -------
        submatrix : ndarray of shape (n_rows, n_cols)
            The submatrix corresponding to bicluster `i`.

        Notes
        -----
        Works with sparse matrices. Only works if ``rows_`` and
        ``columns_`` attributes exist.
        """

        data = check_array(data, accept_sparse="csr")
        row_ind, col_ind = self.get_indices(i)
        return data[row_ind[:, np.newaxis], col_ind]

class TransformerMixin(_SetOutputMixin):
    """Mixin class for all transformers in scikit-learn.

    - a `fit_transform` method that delegates to `fit` and `transform`;
    - a `set_output` method to output `X` as a specific container type.

    If :term:`get_feature_names_out` is defined, then :class:`BaseEstimator` will
    automatically wrap `transform` and `fit_transform` to follow the `set_output`
    API. See the :ref:`developer_api_set_output` for details.

    :class:`OneToOneFeatureMixin` and
    :class:`ClassNamePrefixFeaturesOutMixin` are helpful mixins for
    defining :term:`get_feature_names_out`.

    Examples
    --------
    >>> from sklearn.base import BaseEstimator, TransformerMixin
    >>> class MyTransformer(TransformerMixin, BaseEstimator):
    ...     def transform(self, X):
    ...         return np.full(shape=len(X), fill_value=self.param)
    >>> transformer = MyTransformer()
    >>> transformer.fit_transform(X)
    """

        tags.transformer_tags = TransformerTags()

    def fit_transform(self, X, y=None, **fit_params):
        """
        Fit to data, then transform it.

        Fits transformer to `X` and `y` with optional parameters `fit_params`
        and returns a transformed version of `X`.

            Input samples.

        y :  array-like of shape (n_samples,) or (n_samples, n_outputs), \
                default=None
            Target values (None for unsupervised transformations).

        **fit_params : dict
            Additional fit parameters.

        Returns
        -------
        X_new : ndarray array of shape (n_samples, n_features_new)
            Transformed array.
        """

        # we do not route parameters here, since consumers don't route. But
        # since it's possible for a `transform` method to also consume
        # metadata, we check if that's the case, and we raise a warning telling
        # users that they should implement a custom `fit_transform` method
        # to forward metadata to `transform` as well.
        #
        # For that, we calculate routing and check if anything would be routed
        # to `transform` if we were to route them.
        if _routing_enabled():
            transform_params = self.get_metadata_routing().consumes(
                method="transform", params=fit_params.keys()
            )
            if transform_params:
                    (
                        f"This object ({self.__class__.__name__}) has a `transform`"
                        " method which consumes metadata, but `fit_transform` does not"
                        " forward metadata to `transform`. Please implement a custom"
                        " `fit_transform` method to forward metadata to `transform` as"
                        " well. Alternatively, you can explicitly do"
                        " `set_transform_request`and set all values to `False` to"
                        " disable metadata routed to `transform`, if that's an option."
                    ),
                    UserWarning,
                )

        if y is None:
            # fit method of arity 1 (unsupervised transformation)
            return self.fit(X, **fit_params).transform(X)
        else:
            # fit method of arity 2 (supervised transformation)
            return self.fit(X, y, **fit_params).transform(X)

class OneToOneFeatureMixin:
    """Provides `get_feature_names_out` for simple transformers.

    This mixin assumes there's a 1-to-1 correspondence between input features
    and output features, such as :class:`~sklearn.preprocessing.StandardScaler`.

    Examples
    --------
    >>> from sklearn.base import OneToOneFeatureMixin, BaseEstimator
    >>> class MyEstimator(OneToOneFeatureMixin, BaseEstimator):
    ...         self.n_features_in_ = X.shape[1]
    >>> X = np.array([[1, 2], [3, 4]])
    >>> MyEstimator().fit(X).get_feature_names_out()
    array(['x0', 'x1'], dtype=object)
    """

            Input features.

            - If `input_features` is `None`, then `feature_names_in_` is
              used as feature names in. If `feature_names_in_` is not defined,
              then the following input feature names are generated:
              `["x0", "x1", ..., "x(n_features_in_ - 1)"]`.
            - If `input_features` is an array-like, then `input_features` must
              match `feature_names_in_` if `feature_names_in_` is defined.

        Returns
        -------
            Same as input features.
        """
        # Note that passing attributes="n_features_in_" forces check_is_fitted
        # to check if the attribute is present. Otherwise it will pass on
        # stateless estimators (requires_fit=False)
        check_is_fitted(self, attributes="n_features_in_")
        return _check_feature_names_in(self, input_features)

class ClassNamePrefixFeaturesOutMixin:
    """Mixin class for transformers that generate their own names by prefixing.

    This mixin is useful when the transformer needs to generate its own feature
    names out, such as :class:`~sklearn.decomposition.PCA`. For example, if
    :class:`~sklearn.decomposition.PCA` outputs 3 features, then the generated feature
    names out are: `["pca0", "pca1", "pca2"]`.

    This mixin assumes that a `_n_features_out` attribute is defined when the
    transformer is fitted. `_n_features_out` is the number of output features
    that the transformer will return in `transform` of `fit_transform`.

    Examples
    --------
    >>> from sklearn.base import ClassNamePrefixFeaturesOutMixin, BaseEstimator
    >>> class MyEstimator(ClassNamePrefixFeaturesOutMixin, BaseEstimator):
    ...         self._n_features_out = X.shape[1]
    array(['myestimator0', 'myestimator1'], dtype=object)
    """

        The feature names out will prefixed by the lowercased class name. For
        example, if the transformer outputs 3 features, then the feature names
        out are: `["class_name0", "class_name1", "class_name2"]`.

            Only used to validate feature names with the names seen in `fit`.

        Returns
        -------
            Transformed feature names.
        """
        check_is_fitted(self, "_n_features_out")
        return _generate_get_feature_names_out(
            self, self._n_features_out, input_features=input_features
        )

class DensityMixin:
    """Mixin class for all density estimators in scikit-learn.

    - sets estimator type to `"density_estimator"` through the `estimator_type` tag;
    - `score` method that default that do no-op.

    Examples
    --------
    >>> from sklearn.base import DensityMixin
    >>> class MyEstimator(DensityMixin):
    >>> estimator = MyEstimator()
    >>> hasattr(estimator, "score")
    True
    """

    _estimator_type = "DensityEstimator"

        tags.estimator_type = "density_estimator"

    def score(self, X, y=None):
        """Return the score of the model on the data `X`.

        y : Ignored

        Returns
        -------
        """
        pass

class OutlierMixin:
    """Mixin class for all outlier detection estimators in scikit-learn.

    - set estimator type to `"outlier_detector"` through the `estimator_type` tag;
    - `fit_predict` method that default to `fit` and `predict`.

    Examples
    --------
    >>> from sklearn.base import BaseEstimator, OutlierMixin
    >>> class MyEstimator(OutlierMixin):
    ...         return np.ones(shape=len(X))
    >>> estimator.fit_predict(X)
    array([1., 1., 1.])
    """

    _estimator_type = "outlier_detector"

        tags.estimator_type = "outlier_detector"

        """Perform fit on X and returns labels for X.

        Returns -1 for outliers and 1 for inliers.

        X : {array-like, sparse matrix} of shape (n_samples, n_features)
            The input samples.

        y : Ignored

        Returns
        -------
        y : ndarray of shape (n_samples,)
            1 for inliers, -1 for outliers.
        """
        # since it's possible for a `predict` method to also consume
        # users that they should implement a custom `fit_predict` method
        # to forward metadata to `predict` as well.
        #
        # to `predict` if we were to route them.
                method="predict", params=kwargs.keys()
            )
                    (
                        f"This object ({self.__class__.__name__}) has a `predict` "
                        "method which consumes metadata, but `fit_predict` does not "
                        "forward metadata to `predict`. Please implement a custom "
                        "`fit_predict` method to forward metadata to `predict` as well."
                        "Alternatively, you can explicitly do `set_predict_request`"
                        "and set all values to `False` to disable metadata routed to "
                        "`predict`, if that's an option."
                    ),
                )

        # override for transductive outlier detectors like LocalOulierFactor
        return self.fit(X, **kwargs).predict(X)

class MetaEstimatorMixin:
    """Mixin class for all meta estimators in scikit-learn.

    This mixin is empty, and only exists to indicate that the estimator is a
    meta-estimator.

    .. versionchanged:: 1.6
        The `_required_parameters` is now removed and is unnecessary since tests are
        refactored and don't use this anymore.

    Examples
    --------
    >>> from sklearn.base import MetaEstimatorMixin
    >>> from sklearn.datasets import load_iris
    >>> class MyEstimator(MetaEstimatorMixin):
    ...     def __init__(self, *, estimator=None):
    ...         self.estimator = estimator
    ...         if self.estimator is None:
    ...             self.estimator_ = LogisticRegression()
    ...         else:
    ...             self.estimator_ = self.estimator
    >>> X, y = load_iris(return_X_y=True)
    >>> estimator = MyEstimator().fit(X, y)
    >>> estimator.estimator_
    LogisticRegression()
    """

class MultiOutputMixin:
    """Mixin to mark estimators that support multioutput."""

        tags.target_tags.multi_output = True

class _UnstableArchMixin:
    """Mark estimators that are non-determinstic on 32bit or PowerPC"""

        tags.non_deterministic = _IS_32BIT or platform.machine().startswith(
            ("ppc", "powerpc")
        )

def is_classifier(estimator):
    """Return True if the given estimator is (probably) a classifier.

        Estimator object to test.

    Returns
    -------
    out : bool
        True if estimator is a classifier and False otherwise.

    Examples
    --------
    >>> from sklearn.base import is_classifier
    >>> from sklearn.cluster import KMeans
    >>> from sklearn.svm import SVC, SVR
    >>> classifier = SVC()
    >>> regressor = SVR()
    >>> kmeans = KMeans()
    >>> is_classifier(classifier)
    True
    >>> is_classifier(regressor)
    False
    >>> is_classifier(kmeans)
    False
    """
    # TODO(1.8): Remove this check
    if isinstance(estimator, type):
        warnings.warn(
            f"passing a class to {print(inspect.stack()[0][3])} is deprecated and "
            "will be removed in 1.8. Use an instance of the class instead.",
            FutureWarning,
        )
        return getattr(estimator, "_estimator_type", None) == "classifier"

    return get_tags(estimator).estimator_type == "classifier"

def is_regressor(estimator):
    """Return True if the given estimator is (probably) a regressor.

    estimator : estimator instance

    Returns
    -------
    out : bool
        True if estimator is a regressor and False otherwise.

    Examples
    --------
    >>> from sklearn.base import is_regressor
    >>> is_regressor(classifier)
    False
    >>> is_regressor(regressor)
    True
    >>> is_regressor(kmeans)
    False
    """
        )
        return getattr(estimator, "_estimator_type", None) == "regressor"

    return get_tags(estimator).estimator_type == "regressor"

def is_clusterer(estimator):
    """Return True if the given estimator is (probably) a clusterer.

    .. versionadded:: 1.6

    Returns
    -------
    out : bool
        True if estimator is a clusterer and False otherwise.

    Examples
    --------
    >>> from sklearn.base import is_clusterer
    >>> is_clusterer(classifier)
    False
    >>> is_clusterer(regressor)
    False
    >>> is_clusterer(kmeans)
    True
    """
        )
        return getattr(estimator, "_estimator_type", None) == "clusterer"

    return get_tags(estimator).estimator_type == "clusterer"

def is_outlier_detector(estimator):
    """Return True if the given estimator is (probably) an outlier detector.

    Returns
    -------
    out : bool
        True if estimator is an outlier detector and False otherwise.
    """
        )
        return getattr(estimator, "_estimator_type", None) == "outlier_detector"

    return get_tags(estimator).estimator_type == "outlier_detector"

def _fit_context(*, prefer_skip_nested_validation):
    """Decorator to run the fit methods of estimators within context managers.

    prefer_skip_nested_validation : bool
        If True, the validation of parameters of inner estimators or functions
        called during fit will be skipped.

        This is useful to avoid validating many times the parameters passed by the
        user from the public facing API. It's also useful to avoid validating
        parameters that we pass internally to inner functions that are guaranteed to
        be valid by the test suite.

        It should be set to True for most estimators, except for those that receive
        non-validated objects as parameters, such as meta-estimators that are given
        estimator objects.

    Returns
    -------
    decorated_fit : method
        The decorated fit method.
    """

    def decorator(fit_method):
        @functools.wraps(fit_method)
        def wrapper(estimator, *args, **kwargs):
            global_skip_validation = get_config()["skip_parameter_validation"]

            # we don't want to validate again for each call to partial_fit
            partial_fit_and_fitted = (
                fit_method.__name__ == "partial_fit" and _is_fitted(estimator)
            )

            if not global_skip_validation and not partial_fit_and_fitted:
                estimator._validate_params()

            with config_context(
                skip_parameter_validation=(
                    prefer_skip_nested_validation or global_skip_validation
                )
            ):
                return fit_method(estimator, *args, **kwargs)

        return wrapper

    return decorator

            return []

            p
        ]
                )

        """

        Returns
        -------
        """
        return out

        Returns
        -------
        """

                )

            if delim:
            else:

            indent=1,
        )

            )

        try:

        else:

                    ),
                )
        try:

        )

        """
        )

        """
            )

        """

        """

        Returns
        -------
        """

        Returns
        -------

        Notes
        -----
        """

        """

        y : Ignored

        Returns
        -------
        """

        """

        i : int

        Returns
        -------
        """

        i : int

        Returns
        -------

        """

        i : int
            The data.

        Returns
        -------

        Notes
        -----
        """

        """

        Returns
        -------
        """

        #
            )
                    (
                    ),
                )

        else:

        Returns
        -------
        """

        Returns
        -------
        """
        )

        y : Ignored

        Returns
        -------
        """
        pass

        y : Ignored

        Returns
        -------
        """
        #
            )
                    (
                    ),
                )

        )

            )

                )
            ):

            )

                )
            ):

class PolynomialCountSketch(
    ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator
):
    """Polynomial kernel approximation via Tensor Sketch.

    Implements Tensor Sketch, which approximates the feature map
    of the polynomial kernel::

        K(X, Y) = (gamma * <X, Y> + coef0)^degree

    by efficiently computing a Count Sketch of the outer product of a
    vector with itself using Fast Fourier Transforms (FFT). Read more in the
    :ref:`User Guide <polynomial_kernel_approx>`.

    .. versionadded:: 0.24

    gamma : float, default=1.0
        Parameter of the polynomial kernel whose feature map
        will be approximated.

    degree : int, default=2
        Degree of the polynomial kernel whose feature map

    coef0 : int, default=0
        Constant term of the polynomial kernel whose feature map

    n_components : int, default=100
        Dimensionality of the output feature space. Usually, `n_components`
        should be greater than the number of features in input samples in
        order to achieve good performance. The optimal score / run time
        balance is typically achieved around `n_components` = 10 * `n_features`,
        but this depends on the specific dataset being used.

    random_state : int, RandomState instance, default=None
        Determines random number generation for indexHash and bitHash
        initialization. Pass an int for reproducible results across multiple
        function calls. See :term:`Glossary <random_state>`.

    indexHash_ : ndarray of shape (degree, n_features), dtype=int64
        Array of indexes in range [0, n_components) used to represent
        the 2-wise independent hash functions for Count Sketch computation.

    bitHash_ : ndarray of shape (degree, n_features), dtype=float32
        Array with random entries in {+1, -1}, used to represent

    See Also
    --------
    AdditiveChi2Sampler : Approximate feature map for additive chi2 kernel.
    Nystroem : Approximate a kernel map using a subset of the training data.
    RBFSampler : Approximate a RBF kernel feature map using random Fourier
        features.
    SkewedChi2Sampler : Approximate feature map for "skewed chi-squared" kernel.
    sklearn.metrics.pairwise.kernel_metrics : List of built-in kernels.

    Examples
    --------
    >>> from sklearn.kernel_approximation import PolynomialCountSketch
    >>> from sklearn.linear_model import SGDClassifier
    >>> X = [[0, 0], [1, 1], [1, 0], [0, 1]]
    >>> ps = PolynomialCountSketch(degree=3, random_state=1)
    >>> X_features = ps.fit_transform(X)
    >>> clf = SGDClassifier(max_iter=10, tol=1e-3)
    >>> clf.fit(X_features, y)
    SGDClassifier(max_iter=10)
    >>> clf.score(X_features, y)
    1.0

    For a more detailed example of usage, see
    :ref:`sphx_glr_auto_examples_kernel_approximation_plot_scalable_poly_kernels.py`
    """

        "gamma": [Interval(Real, 0, None, closed="left")],
        "degree": [Interval(Integral, 1, None, closed="left")],
        "coef0": [Interval(Real, None, None, closed="neither")],
        "n_components": [Interval(Integral, 1, None, closed="left")],
    }

        self, *, gamma=1.0, degree=2, coef0=0, n_components=100, random_state=None
    ):
        self.gamma = gamma
        self.degree = degree
        self.coef0 = coef0

        """Fit the model with X.

        Initializes the internal variables. The method needs no information
        about the distribution of data, so we only care about n_features in X.

            Training data, where `n_samples` is the number of samples
            and `n_features` is the number of features.

        y : array-like of shape (n_samples,) or (n_samples, n_outputs), \

        Returns
        -------
            Returns the instance itself.
        """
        X = validate_data(self, X, accept_sparse="csc")

        n_features = X.shape[1]
        if self.coef0 != 0:
            n_features += 1

        self.indexHash_ = random_state.randint(
            0, high=self.n_components, size=(self.degree, n_features)
        )

        self.bitHash_ = random_state.choice(a=[-1, 1], size=(self.degree, n_features))

        """Generate the feature map approximation for X.

        X : {array-like}, shape (n_samples, n_features)
            New data, where `n_samples` is the number of samples

        Returns
        -------
        X_new : array-like, shape (n_samples, n_components)
        """

        X = validate_data(self, X, accept_sparse="csc", reset=False)

        X_gamma = np.sqrt(self.gamma) * X

        if sp.issparse(X_gamma) and self.coef0 != 0:
            X_gamma = sp.hstack(
                [X_gamma, np.sqrt(self.coef0) * np.ones((X_gamma.shape[0], 1))],
                format="csc",
            )

        elif not sp.issparse(X_gamma) and self.coef0 != 0:
            X_gamma = np.hstack(
                [X_gamma, np.sqrt(self.coef0) * np.ones((X_gamma.shape[0], 1))]
            )

        if X_gamma.shape[1] != self.indexHash_.shape[1]:
            raise ValueError(
                "Number of features of test samples does not"
                " match that of training samples."
            )

        count_sketches = np.zeros((X_gamma.shape[0], self.degree, self.n_components))

        if sp.issparse(X_gamma):
            for j in range(X_gamma.shape[1]):
                for d in range(self.degree):
                    iHashIndex = self.indexHash_[d, j]
                    iHashBit = self.bitHash_[d, j]
                    count_sketches[:, d, iHashIndex] += (
                        (iHashBit * X_gamma[:, [j]]).toarray().ravel()
                    )

        else:
                    count_sketches[:, d, iHashIndex] += iHashBit * X_gamma[:, j]

        # For each same, compute a count sketch of phi(x) using the polynomial
        # multiplication (via FFT) of p count sketches of x.
        count_sketches_fft = fft(count_sketches, axis=2, overwrite_x=True)
        count_sketches_fft_prod = np.prod(count_sketches_fft, axis=1)
        data_sketch = np.real(ifft(count_sketches_fft_prod, overwrite_x=True))

        return data_sketch

class RBFSampler(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """Approximate a RBF kernel feature map using random Fourier features.

    It implements a variant of Random Kitchen Sinks.[1]

    Read more in the :ref:`User Guide <rbf_kernel_approx>`.

    gamma : 'scale' or float, default=1.0
        Parameter of RBF kernel: exp(-gamma * x^2).
        If ``gamma='scale'`` is passed then it uses
        1 / (n_features * X.var()) as value of gamma.

        .. versionadded:: 1.2
           The option `"scale"` was added in 1.2.

        Number of Monte Carlo samples per original feature.
        Equals the dimensionality of the computed feature space.

        Pseudo-random number generator to control the generation of the random
        weights and random offset when fitting the training data.

    random_offset_ : ndarray of shape (n_components,), dtype={np.float64, np.float32}
        Random offset used to compute the projection in the `n_components`
        dimensions of the feature space.

    random_weights_ : ndarray of shape (n_features, n_components),\
        dtype={np.float64, np.float32}
        Random projection directions drawn from the Fourier transform
        of the RBF kernel.

    See Also
    --------
    PolynomialCountSketch : Polynomial kernel approximation via Tensor Sketch.
    SkewedChi2Sampler : Approximate feature map for
        "skewed chi-squared" kernel.

    Notes
    -----
    See "Random Features for Large-Scale Kernel Machines" by A. Rahimi and
    Benjamin Recht.

    [1] "Weighted Sums of Random Kitchen Sinks: Replacing
    minimization with randomization in learning" by A. Rahimi and
    (https://people.eecs.berkeley.edu/~brecht/papers/08.rah.rec.nips.pdf)

    Examples
    --------
    >>> from sklearn.kernel_approximation import RBFSampler
    >>> rbf_feature = RBFSampler(gamma=1, random_state=1)
    >>> X_features = rbf_feature.fit_transform(X)
    >>> clf = SGDClassifier(max_iter=5, tol=1e-3)
    SGDClassifier(max_iter=5)
    1.0
    """

        "gamma": [
            StrOptions({"scale"}),
            Interval(Real, 0.0, None, closed="left"),
        ],
    }

    def __init__(self, *, gamma=1.0, n_components=100, random_state=None):

        Samples random projection according to n_features.

        X : {array-like, sparse matrix}, shape (n_samples, n_features)

        y : array-like, shape (n_samples,) or (n_samples, n_outputs), \

        Returns
        -------
        """
        X = validate_data(self, X, accept_sparse="csr")
        sparse = sp.issparse(X)
        if self.gamma == "scale":
            # var = E[X^2] - E[X]^2 if sparse
            X_var = (X.multiply(X)).mean() - (X.mean()) ** 2 if sparse else X.var()
            self._gamma = 1.0 / (n_features * X_var) if X_var != 0 else 1.0
        else:
            self._gamma = self.gamma
        self.random_weights_ = (2.0 * self._gamma) ** 0.5 * random_state.normal(
            size=(n_features, self.n_components)
        )

        self.random_offset_ = random_state.uniform(0, 2 * np.pi, size=self.n_components)

        if X.dtype == np.float32:
            # Setting the data type of the fitted attribute will ensure the
            # output data type during `transform`.
            self.random_weights_ = self.random_weights_.astype(X.dtype, copy=False)
            self.random_offset_ = self.random_offset_.astype(X.dtype, copy=False)

        """Apply the approximate feature map to X.

        Returns
        -------
        """

        X = validate_data(self, X, accept_sparse="csr", reset=False)
        projection = safe_sparse_dot(X, self.random_weights_)
        projection += self.random_offset_
        np.cos(projection, projection)
        projection *= (2.0 / self.n_components) ** 0.5
        return projection

class SkewedChi2Sampler(
):
    """Approximate feature map for "skewed chi-squared" kernel.

    Read more in the :ref:`User Guide <skewed_chi_kernel_approx>`.

    skewedness : float, default=1.0
        "skewedness" parameter of the kernel. Needs to be cross-validated.

    random_weights_ : ndarray of shape (n_features, n_components)
        Weight array, sampled from a secant hyperbolic distribution, which will
        be used to linearly transform the log of the data.

    random_offset_ : ndarray of shape (n_features, n_components)
        Bias term, which will be added to the data. It is uniformly distributed
        between 0 and 2*pi.

    See Also
    --------
        features.
    sklearn.metrics.pairwise.chi2_kernel : The exact chi squared kernel.

    See "Random Fourier Approximations for Skewed Multiplicative Histogram
    Kernels" by Fuxin Li, Catalin Ionescu and Cristian Sminchisescu.

    Examples
    --------
    >>> from sklearn.kernel_approximation import SkewedChi2Sampler
    >>> chi2_feature = SkewedChi2Sampler(skewedness=.01,
    ...                                  n_components=10,
    ...                                  random_state=0)
    >>> X_features = chi2_feature.fit_transform(X, y)
    1.0
    """

        "skewedness": [Interval(Real, None, None, closed="neither")],
    }

    def __init__(self, *, skewedness=1.0, n_components=100, random_state=None):
        self.skewedness = skewedness

        X : array-like, shape (n_samples, n_features)

        Returns
        -------
        """
        X = validate_data(self, X)
        uniform = random_state.uniform(size=(n_features, self.n_components))
        # transform by inverse CDF of sech
        self.random_weights_ = 1.0 / np.pi * np.log(np.tan(np.pi / 2.0 * uniform))

            and `n_features` is the number of features. All values of X must be
            strictly greater than "-skewedness".

        Returns
        -------
        """
            self, X, copy=True, dtype=[np.float64, np.float32], reset=False
        )
        if (X <= -self.skewedness).any():
            raise ValueError("X may not contain entries smaller than -skewedness.")

        X += self.skewedness
        np.log(X, X)
        projection *= np.sqrt(2.0) / np.sqrt(self.n_components)

class AdditiveChi2Sampler(TransformerMixin, BaseEstimator):
    """Approximate feature map for additive chi2 kernel.

    Uses sampling the fourier transform of the kernel characteristic
    at regular intervals.

    Since the kernel that is to be approximated is additive, the components of
    the input vectors can be treated separately.  Each entry in the original
    space is transformed into 2*sample_steps-1 features, where sample_steps is
    a parameter of the method. Typical values of sample_steps include 1, 2 and
    3.

    Optimal choices for the sampling interval for certain data ranges can be
    computed (see the reference). The default values should be reasonable.

    Read more in the :ref:`User Guide <additive_chi_kernel_approx>`.

    sample_steps : int, default=2
        Gives the number of (complex) sampling points.

    sample_interval : float, default=None
        Sampling interval. Must be specified when sample_steps not in {1,2,3}.

    See Also
    --------
    SkewedChi2Sampler : A Fourier-approximation to a non-additive variant of
        the chi squared kernel.

    sklearn.metrics.pairwise.additive_chi2_kernel : The exact additive chi
        squared kernel.

    Notes
    -----
    This estimator approximates a slightly different version of the additive
    chi squared kernel then ``metric.additive_chi2`` computes.

    This estimator is stateless and does not need to be fitted. However, we
    recommend to call :meth:`fit_transform` instead of :meth:`transform`, as
    parameter validation is only performed in :meth:`fit`.

    See `"Efficient additive kernels via explicit feature maps"
    <http://www.robots.ox.ac.uk/~vedaldi/assets/pubs/vedaldi11efficient.pdf>`_
    A. Vedaldi and A. Zisserman, Pattern Analysis and Machine Intelligence,
    2011

    Examples
    --------
    >>> from sklearn.datasets import load_digits
    >>> from sklearn.kernel_approximation import AdditiveChi2Sampler
    >>> X, y = load_digits(return_X_y=True)
    >>> chi2sampler = AdditiveChi2Sampler(sample_steps=2)
    >>> X_transformed = chi2sampler.fit_transform(X, y)
    >>> clf = SGDClassifier(max_iter=5, random_state=0, tol=1e-3)
    >>> clf.fit(X_transformed, y)
    SGDClassifier(max_iter=5, random_state=0)
    >>> clf.score(X_transformed, y)
    0.9499...
    """

        "sample_steps": [Interval(Integral, 1, None, closed="left")],
        "sample_interval": [Interval(Real, 0, None, closed="left"), None],
    }

    def __init__(self, *, sample_steps=2, sample_interval=None):
        self.sample_steps = sample_steps
        self.sample_interval = sample_interval

        """Only validates estimator's parameters.

        This method allows to: (i) validate the estimator's parameters and
        (ii) be consistent with the scikit-learn transformer API.

        Returns
        -------
            Returns the transformer.
        """
        X = validate_data(self, X, accept_sparse="csr", ensure_non_negative=True)

        if self.sample_interval is None and self.sample_steps not in (1, 2, 3):
                "If sample_steps is not in [1, 2, 3],"
                " you need to provide sample_interval"
            )

        """Apply approximate feature map to X.

        Returns
        -------
        X_new : {ndarray, sparse matrix}, \
               shape = (n_samples, n_features * (2*sample_steps - 1))
            Whether the return value is an array or sparse matrix depends on
            the type of the input X.
        """
            self, X, accept_sparse="csr", reset=False, ensure_non_negative=True
        )

        if self.sample_interval is None:
            # See figure 2 c) of "Efficient additive kernels via explicit feature maps"
            # <http://www.robots.ox.ac.uk/~vedaldi/assets/pubs/vedaldi11efficient.pdf>
            # A. Vedaldi and A. Zisserman, Pattern Analysis and Machine Intelligence,
            # 2011
            if self.sample_steps == 1:
                sample_interval = 0.8
            elif self.sample_steps == 2:
                sample_interval = 0.5
            elif self.sample_steps == 3:
                sample_interval = 0.4
            else:
                    "If sample_steps is not in [1, 2, 3],"
                    " you need to provide sample_interval"
                )
        else:
            sample_interval = self.sample_interval

        # zeroth component
        # 1/cosh = sech
        # cosh(0) = 1.0
        transf = self._transform_sparse if sparse else self._transform_dense
        return transf(X, self.sample_steps, sample_interval)

            Only used to validate feature names with the names seen in :meth:`fit`.

        Returns
        -------
        """
        # to check if the attribute is present. Otherwise it will pass on this
        # stateless estimator (requires_fit=False)
        input_features = _check_feature_names_in(
            self, input_features, generate_names=True
        )
        est_name = self.__class__.__name__.lower()

        names_list = [f"{est_name}_{name}_sqrt" for name in input_features]

        for j in range(1, self.sample_steps):
            cos_names = [f"{est_name}_{name}_cos{j}" for name in input_features]
            sin_names = [f"{est_name}_{name}_sin{j}" for name in input_features]
            names_list.extend(cos_names + sin_names)

        return np.asarray(names_list, dtype=object)

    @staticmethod
    def _transform_dense(X, sample_steps, sample_interval):
        non_zero = X != 0.0
        X_nz = X[non_zero]

        X_step = np.zeros_like(X)
        X_step[non_zero] = np.sqrt(X_nz * sample_interval)

        X_new = [X_step]

        log_step_nz = sample_interval * np.log(X_nz)
        step_nz = 2 * X_nz * sample_interval

        for j in range(1, sample_steps):
            factor_nz = np.sqrt(step_nz / np.cosh(np.pi * j * sample_interval))

            X_step = np.zeros_like(X)
            X_step[non_zero] = factor_nz * np.cos(j * log_step_nz)
            X_new.append(X_step)

            X_step[non_zero] = factor_nz * np.sin(j * log_step_nz)

        return np.hstack(X_new)

    def _transform_sparse(X, sample_steps, sample_interval):
        indices = X.indices.copy()
        indptr = X.indptr.copy()

        data_step = np.sqrt(X.data * sample_interval)
        X_step = sp.csr_matrix(
            (data_step, indices, indptr), shape=X.shape, dtype=X.dtype, copy=False
        )

        log_step_nz = sample_interval * np.log(X.data)
        step_nz = 2 * X.data * sample_interval

            data_step = factor_nz * np.cos(j * log_step_nz)
            X_step = sp.csr_matrix(
                (data_step, indices, indptr), shape=X.shape, dtype=X.dtype, copy=False
            )

            data_step = factor_nz * np.sin(j * log_step_nz)
            )

        return sp.hstack(X_new)

        tags.requires_fit = False
        tags.input_tags.positive_only = True

class Nystroem(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """Approximate a kernel map using a subset of the training data.

    Constructs an approximate feature map for an arbitrary kernel
    using a subset of the data as basis.

    Read more in the :ref:`User Guide <nystroem_kernel_approx>`.

    kernel : str or callable, default='rbf'
        Kernel map to be approximated. A callable should accept two arguments
        and the keyword arguments passed to this object as `kernel_params`, and
        should return a floating point number.

    gamma : float, default=None
        Gamma parameter for the RBF, laplacian, polynomial, exponential chi2
        and sigmoid kernels. Interpretation of the default value is left to
        the kernel; see the documentation for sklearn.metrics.pairwise.
        Ignored by other kernels.

    coef0 : float, default=None
        Zero coefficient for polynomial and sigmoid kernels.

    degree : float, default=None
        Degree of the polynomial kernel. Ignored by other kernels.

    kernel_params : dict, default=None
        Additional parameters (keyword arguments) for kernel function passed
        as callable object.

        Number of features to construct.
        How many data points will be used to construct the mapping.

        Pseudo-random number generator to control the uniform sampling without
        replacement of `n_components` of the training data to construct the
        basis kernel.

    n_jobs : int, default=None
        The number of jobs to use for the computation. This works by breaking
        down the kernel matrix into `n_jobs` even slices and computing them in
        parallel.

        ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
        ``-1`` means using all processors. See :term:`Glossary <n_jobs>`
        for more details.

        Subset of training points used to construct the feature map.

    component_indices_ : ndarray of shape (n_components)
        Indices of ``components_`` in the training set.

    normalization_ : ndarray of shape (n_components, n_components)
        Normalization matrix needed for embedding.
        Square root of the kernel matrix on ``components_``.

    See Also
    --------
        features.

    * Williams, C.K.I. and Seeger, M.
      "Using the Nystroem method to speed up kernel machines",
      Advances in neural information processing systems 2001

    * T. Yang, Y. Li, M. Mahdavi, R. Jin and Z. Zhou
      "Nystroem Method vs Random Fourier Features: A Theoretical and Empirical
      Comparison",
      Advances in Neural Information Processing Systems 2012

    Examples
    --------
    >>> from sklearn import datasets, svm
    >>> from sklearn.kernel_approximation import Nystroem
    >>> X, y = datasets.load_digits(n_class=9, return_X_y=True)
    >>> data = X / 16.
    >>> clf = svm.LinearSVC()
    >>> feature_map_nystroem = Nystroem(gamma=.2,
    ...                                 random_state=1,
    ...                                 n_components=300)
    >>> data_transformed = feature_map_nystroem.fit_transform(data)
    >>> clf.fit(data_transformed, y)
    LinearSVC()
    >>> clf.score(data_transformed, y)
    0.9987...
    """

        "kernel": [
            StrOptions(set(PAIRWISE_KERNEL_FUNCTIONS.keys()) | {"precomputed"}),
            callable,
        ],
        "gamma": [Interval(Real, 0, None, closed="left"), None],
        "coef0": [Interval(Real, None, None, closed="neither"), None],
        "degree": [Interval(Real, 1, None, closed="left"), None],
        "kernel_params": [dict, None],
        "n_jobs": [Integral, None],
    }

        self,
        kernel="rbf",
        *,
        gamma=None,
        coef0=None,
        degree=None,
        kernel_params=None,
        n_components=100,
        n_jobs=None,
    ):
        self.kernel = kernel
        self.kernel_params = kernel_params
        self.n_jobs = n_jobs

        """Fit estimator to data.

        Samples a subset of training points, computes kernel
        on these and computes normalization matrix.

        Returns
        -------
        """
        rnd = check_random_state(self.random_state)
        n_samples = X.shape[0]

        # get basis vectors
        if self.n_components > n_samples:
            # XXX should we just bail?
            n_components = n_samples
                "n_components > n_samples. This is not possible.\n"
                "n_components was set to n_samples, which results"
                " in inefficient evaluation of the full kernel."
            )

        else:
            n_components = self.n_components
        n_components = min(n_samples, n_components)
        inds = rnd.permutation(n_samples)
        basis_inds = inds[:n_components]
        basis = X[basis_inds]

        basis_kernel = pairwise_kernels(
            basis,
            metric=self.kernel,
            filter_params=True,
            n_jobs=self.n_jobs,
            **self._get_kernel_params(),
        )

        # sqrt of kernel matrix on basis vectors
        U, S, V = svd(basis_kernel)
        S = np.maximum(S, 1e-12)
        self.normalization_ = np.dot(U / np.sqrt(S), V)
        self.components_ = basis
        self.component_indices_ = basis_inds
        self._n_features_out = n_components

        """Apply feature map to X.

        Computes an approximate feature map using the kernel
        between some training points and X.

        Returns
        -------
        X_transformed : ndarray of shape (n_samples, n_components)
        """

        kernel_params = self._get_kernel_params()
        embedded = pairwise_kernels(
            X,
            self.components_,
            **kernel_params,
        )
        return np.dot(embedded, self.normalization_.T)

    def _get_kernel_params(self):
        params = self.kernel_params
        if params is None:
            params = {}
        if not callable(self.kernel) and self.kernel != "precomputed":
            for param in KERNEL_PARAMS[self.kernel]:
                if getattr(self, param) is not None:
                    params[param] = getattr(self, param)
        else:
            if (
                self.gamma is not None
                or self.coef0 is not None
                or self.degree is not None
            ):
                    "Don't pass gamma, coef0 or degree to "
                    "Nystroem if using a callable "
                    "or precomputed kernel"
                )

        return params

    ):

        Returns
        -------
        """

        )

        Returns
        -------
        """

            )

            )

            )

                    )

        else:

        Returns
        -------
        """
        else:
        )

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """
        )

        Returns
        -------
        """

            )

        Returns
        -------
        """
        )

            # 2011
            else:
                )
        else:

        Returns
        -------
        """
        )

        )

            )

            )

        self,
        *,
    ):

        Returns
        -------
        """

            )

        else:

            basis,
        )

        Returns
        -------
        """

            X,
        )

            params = {}
        else:
            if (
            ):
                )

def raccoon_face_or_skip():
    # SciPy >= 1.10 requires network to access to get data
    if scipy_datasets_require_network:
        run_network_tests = environ.get("SKLEARN_SKIP_NETWORK_TESTS", "1") == "0"
        if not run_network_tests:
            raise SkipTest("test is enabled when SKLEARN_SKIP_NETWORK_TESTS=0")

        try:
            import pooch  # noqa: F401
        except ImportError:
            raise SkipTest("test requires pooch to be installed")

        from scipy.datasets import face
    else:
        from scipy.misc import face

    return face(gray=True)

def _fetch_fixture(f):
    """Fetch dataset (download if missing and requested by environment)."""
    download_if_missing = environ.get("SKLEARN_SKIP_NETWORK_TESTS", "1") == "0"

    @wraps(f)
    def wrapped(*args, **kwargs):
        kwargs["download_if_missing"] = download_if_missing
        try:
            return f(*args, **kwargs)
        except OSError as e:
            if str(e) != "Data not found and `download_if_missing` is False":
                raise
            pytest.skip("test is enabled when SKLEARN_SKIP_NETWORK_TESTS=0")

    return pytest.fixture(lambda: wrapped)

def pytest_collection_modifyitems(config, items):
    """Called after collect is completed.

    config : pytest config
    items : list of collected items
    """
    run_network_tests = environ.get("SKLEARN_SKIP_NETWORK_TESTS", "1") == "0"
    skip_network = pytest.mark.skip(
        reason="test is enabled when SKLEARN_SKIP_NETWORK_TESTS=0"
    )

    # download datasets during collection to avoid thread unsafe behavior
    # when running pytest in parallel with pytest-xdist
    dataset_features_set = set(dataset_fetchers)
    datasets_to_download = set()

    for item in items:
        if isinstance(item, DoctestItem) and "fetch_" in item.name:
            fetcher_function_name = item.name.split(".")[-1]
            dataset_fetchers_key = f"{fetcher_function_name}_fxt"
            dataset_to_fetch = set([dataset_fetchers_key]) & dataset_features_set
        elif not hasattr(item, "fixturenames"):
            continue
        else:
            item_fixtures = set(item.fixturenames)
            dataset_to_fetch = item_fixtures & dataset_features_set

        if not dataset_to_fetch:
            continue

        if run_network_tests:
            datasets_to_download |= dataset_to_fetch
        else:
            # network tests are skipped
            item.add_marker(skip_network)

    # Only download datasets on the first worker spawned by pytest-xdist
    # to avoid thread unsafe behavior. If pytest-xdist is not used, we still
    # download before tests run.
    worker_id = environ.get("PYTEST_XDIST_WORKER", "gw0")
    if worker_id == "gw0" and run_network_tests:
        for name in datasets_to_download:
            with suppress(SkipTest):
                dataset_fetchers[name]()

        # Known failure on with GradientBoostingClassifier on ARM64
        if (
            item.name.endswith("GradientBoostingClassifier")
            and platform.machine() == "aarch64"
        ):
            marker = pytest.mark.xfail(
                reason=(
                    "know failure. See "
                    "https://github.com/scikit-learn/scikit-learn/issues/17797"
                )
            )
            item.add_marker(marker)

    skip_doctests = False
    try:
        import matplotlib  # noqa: F401
    except ImportError:
        skip_doctests = True
        reason = "matplotlib is required to run the doctests"

    if _IS_32BIT:
        reason = "doctest are only run when the default numpy int is 64 bits."
    elif sys.platform.startswith("win32"):
        reason = (
            "doctests are not run for Windows because numpy arrays "
            "repr is inconsistent across platforms."
        )

    if np_base_version < parse_version("2"):
        # TODO: configure numpy to output scalar arrays as regular Python scalars
        # once possible to improve readability of the tests docstrings.
        # https://numpy.org/neps/nep-0051-scalar-representation.html#implementation
        reason = "Due to NEP 51 numpy scalar repr has changed in numpy 2"

    if sp_version < parse_version("1.14"):
        reason = "Scipy sparse matrix repr has changed in scipy 1.14"

    # Normally doctest has the entire module's scope. Here we set globs to an empty dict
    # to remove the module's scope:
    # https://docs.python.org/3/library/doctest.html#what-s-the-execution-context
        if isinstance(item, DoctestItem):
            item.dtest.globs = {}

    if skip_doctests:
        skip_marker = pytest.mark.skip(reason=reason)

        for item in items:
            if isinstance(item, DoctestItem):
                # work-around an internal error with pytest if adding a skip
                # mark to a doctest in a contextmanager, see
                # https://github.com/pytest-dev/pytest/issues/8796 for more
                # details.
                if item.name != "sklearn._config.config_context":
                    item.add_marker(skip_marker)
    try:
        import PIL  # noqa: F401

        pillow_installed = True
        pillow_installed = False

    if not pillow_installed:
        skip_marker = pytest.mark.skip(reason="pillow (or PIL) not installed!")
            if item.name in [
                "sklearn.feature_extraction.image.PatchExtractor",
                "sklearn.feature_extraction.image.extract_patches_2d",
            ]:
                item.add_marker(skip_marker)

def pyplot():
    """Setup and teardown fixture for matplotlib.

    This fixture checks if we can import matplotlib. If not, the tests will be
    skipped. Otherwise, we close the figures before and after running the
    functions.

    Returns
    -------
    pyplot : module
        The ``matplotlib.pyplot`` module.
    """
    pyplot = pytest.importorskip("matplotlib.pyplot")
    pyplot.close("all")
    yield pyplot

def pytest_generate_tests(metafunc):
    """Parametrization of global_random_seed fixture

    based on the SKLEARN_TESTS_GLOBAL_RANDOM_SEED environment variable.

    The goal of this fixture is to prevent tests that use it to be sensitive
    to a specific seed value while still being deterministic by default.

    See the documentation for the SKLEARN_TESTS_GLOBAL_RANDOM_SEED
    variable for instructions on how to use this fixture.

    https://scikit-learn.org/dev/computing/parallelism.html#sklearn-tests-global-random-seed

    """
    # When using pytest-xdist this function is called in the xdist workers.
    # We rely on SKLEARN_TESTS_GLOBAL_RANDOM_SEED environment variable which is
    # set in before running pytest and is available in xdist workers since they
    # are subprocesses.
    RANDOM_SEED_RANGE = list(range(100))  # All seeds in [0, 99] should be valid.
    random_seed_var = environ.get("SKLEARN_TESTS_GLOBAL_RANDOM_SEED")

    default_random_seeds = [42]

    if random_seed_var is None:
        random_seeds = default_random_seeds
    elif random_seed_var == "all":
        random_seeds = RANDOM_SEED_RANGE
    else:
        if "-" in random_seed_var:
            start, stop = random_seed_var.split("-")
            random_seeds = list(range(int(start), int(stop) + 1))
        else:
            random_seeds = [int(random_seed_var)]

        if min(random_seeds) < 0 or max(random_seeds) > 99:
                "The value(s) of the environment variable "
                "SKLEARN_TESTS_GLOBAL_RANDOM_SEED must be in the range [0, 99] "
                f"(or 'all'), got: {random_seed_var}"
            )

    if "global_random_seed" in metafunc.fixturenames:
        metafunc.parametrize("global_random_seed", random_seeds)

def pytest_configure(config):
    # Use matplotlib agg backend during the tests including doctests
    try:
        import matplotlib

        matplotlib.use("agg")
        pass

    allowed_parallelism = joblib.cpu_count(only_physical_cores=True)
    xdist_worker_count = environ.get("PYTEST_XDIST_WORKER_COUNT")
    if xdist_worker_count is not None:
        # Set the number of OpenMP and BLAS threads based on the number of workers
        # xdist is using to prevent oversubscription.
        allowed_parallelism = max(allowed_parallelism // int(xdist_worker_count), 1)
    threadpool_limits(allowed_parallelism)

    if environ.get("SKLEARN_WARNINGS_AS_ERRORS", "0") != "0":
        # This seems like the only way to programmatically change the config
        # filterwarnings. This was suggested in
        # https://github.com/pytest-dev/pytest/issues/3311#issuecomment-373177592
        for line in get_pytest_filterwarning_lines():
            config.addinivalue_line("filterwarnings", line)

    faulthandler_timeout = int(environ.get("SKLEARN_FAULTHANDLER_TIMEOUT", "0"))
    if faulthandler_timeout > 0:
        faulthandler.enable()
        faulthandler.dump_traceback_later(faulthandler_timeout, exit=True)

def hide_available_pandas(monkeypatch):
    """Pretend pandas was not installed."""
    import_orig = builtins.__import__

    def mocked_import(name, *args, **kwargs):
        if name == "pandas":
            raise ImportError()
        return import_orig(name, *args, **kwargs)

    monkeypatch.setattr(builtins, "__import__", mocked_import)

def print_changed_only_false():
    """Set `print_changed_only` to False for the duration of the test."""
    set_config(print_changed_only=False)
    yield
    set_config(print_changed_only=True)  # reset to default

        try:
                raise

class DummyClassifier(MultiOutputMixin, ClassifierMixin, BaseEstimator):
    """DummyClassifier makes predictions that ignore the input features.

    This classifier serves as a simple baseline to compare against other more
    complex classifiers.

    The specific behavior of the baseline is selected with the `strategy`
    parameter.

    All strategies make predictions that ignore the input feature values passed
    as the `X` argument to `fit` and `predict`. The predictions, however,
    typically depend on values observed in the `y` parameter passed to `fit`.

    Note that the "stratified" and "uniform" strategies lead to
    non-deterministic predictions that can be rendered deterministic by setting
    the `random_state` parameter if needed. The other strategies are naturally
    deterministic and, once fit, always return the same constant prediction
    for any value of `X`.

    Read more in the :ref:`User Guide <dummy_estimators>`.

    strategy : {"most_frequent", "prior", "stratified", "uniform", \
            "constant"}, default="prior"
        Strategy to use to generate predictions.

        * "most_frequent": the `predict` method always returns the most
          frequent class label in the observed `y` argument passed to `fit`.
          The `predict_proba` method returns the matching one-hot encoded
          vector.
        * "prior": the `predict` method always returns the most frequent
          class label in the observed `y` argument passed to `fit` (like
          "most_frequent"). ``predict_proba`` always returns the empirical
          class distribution of `y` also known as the empirical class prior
          distribution.
        * "stratified": the `predict_proba` method randomly samples one-hot
          vectors from a multinomial distribution parametrized by the empirical
          class prior probabilities.
          The `predict` method returns the class label which got probability
          one in the one-hot vector of `predict_proba`.
          Each sampled row of both methods is therefore independent and
          identically distributed.
        * "uniform": generates predictions uniformly at random from the list
          of unique classes observed in `y`, i.e. each class has equal
          probability.
        * "constant": always predicts a constant label that is provided by
          the user. This is useful for metrics that evaluate a non-majority
          class.

          .. versionchanged:: 0.24
             The default value of `strategy` has changed to "prior" in version
             0.24.

        Controls the randomness to generate the predictions when
        ``strategy='stratified'`` or ``strategy='uniform'``.

    constant : int or str or array-like of shape (n_outputs,), default=None
        The explicit constant as predicted by the "constant" strategy. This
        parameter is useful only for the "constant" strategy.

    classes_ : ndarray of shape (n_classes,) or list of such arrays
        Unique class labels observed in `y`. For multi-output classification
        problems, this attribute is a list of arrays as each output has an
        independent set of possible classes.

    n_classes_ : int or list of int
        Number of label for each output.

    class_prior_ : ndarray of shape (n_classes,) or list of such arrays
        Frequency of each class observed in `y`. For multioutput classification
        problems, this is computed independently for each output.

        Names of features seen during :term:`fit`. Defined only when `X` has
        feature names that are all strings.

    n_outputs_ : int
        Number of outputs.

    sparse_output_ : bool
        True if the array returned from predict is to be in sparse CSC format.
        Is automatically set to True if the input `y` is passed in sparse
        format.

    See Also
    --------
    DummyRegressor : Regressor that makes predictions using simple rules.

    Examples
    --------
    >>> from sklearn.dummy import DummyClassifier
    >>> X = np.array([-1, 1, 1, 1])
    >>> y = np.array([0, 1, 1, 1])
    >>> dummy_clf = DummyClassifier(strategy="most_frequent")
    >>> dummy_clf.fit(X, y)
    DummyClassifier(strategy='most_frequent')
    >>> dummy_clf.predict(X)
    array([1, 1, 1, 1])
    >>> dummy_clf.score(X, y)
    0.75
    """

        "strategy": [
            StrOptions({"most_frequent", "prior", "stratified", "uniform", "constant"})
        ],
        "constant": [Integral, str, "array-like", None],
    }

    def __init__(self, *, strategy="prior", random_state=None, constant=None):
        self.strategy = strategy
        self.constant = constant

        """Fit the baseline classifier.

            Target values.

        Returns
        -------
        """
        validate_data(self, X, skip_check_array=True)

        self._strategy = self.strategy

        if self._strategy == "uniform" and sp.issparse(y):
            y = y.toarray()
                (
                    "A local copy of the target data has been converted "
                    "to a numpy array. Predicting on sparse target data "
                    "with the uniform strategy would not save memory "
                    "and would be slower."
                ),
                UserWarning,
            )

        self.sparse_output_ = sp.issparse(y)

        if not self.sparse_output_:
            y = np.asarray(y)
            y = np.atleast_1d(y)

        if y.ndim == 1:
            y = np.reshape(y, (-1, 1))

        self.n_outputs_ = y.shape[1]

        check_consistent_length(X, y)

        if sample_weight is not None:
            sample_weight = _check_sample_weight(sample_weight, X)

        if self._strategy == "constant":
            if self.constant is None:
                    "Constant target value has to be specified "
                    "when the constant strategy is used."
                )
            else:
                constant = np.reshape(np.atleast_1d(self.constant), (-1, 1))
                if constant.shape[0] != self.n_outputs_:
                    raise ValueError(
                        "Constant target value should have shape (%d, 1)."
                        % self.n_outputs_
                    )

        (self.classes_, self.n_classes_, self.class_prior_) = class_distribution(
            y, sample_weight
        )

            for k in range(self.n_outputs_):
                if not any(constant[k][0] == c for c in self.classes_[k]):
                    # Checking in case of constant strategy if the constant
                    # provided by the user is in y.
                    err_msg = (
                        "The constant target value must be present in "
                        "the training data. You provided constant={}. "
                        "Possible values are: {}.".format(
                            self.constant, self.classes_[k].tolist()
                        )
                    )
                    raise ValueError(err_msg)

        if self.n_outputs_ == 1:
            self.n_classes_ = self.n_classes_[0]
            self.classes_ = self.classes_[0]
            self.class_prior_ = self.class_prior_[0]

    def predict(self, X):
        """Perform classification on test vectors X.

            Test data.

        Returns
        -------
            Predicted target values for X.
        """

        # numpy random_state expects Python int and not long as size argument
        # under Windows
        n_samples = _num_samples(X)
        rs = check_random_state(self.random_state)

        n_classes_ = self.n_classes_
        classes_ = self.classes_
        class_prior_ = self.class_prior_
        constant = self.constant
            # Get same type even for self.n_outputs_ == 1
            n_classes_ = [n_classes_]
            classes_ = [classes_]
            class_prior_ = [class_prior_]
            constant = [constant]
        # Compute probability only once
        if self._strategy == "stratified":
            proba = self.predict_proba(X)
            if self.n_outputs_ == 1:
                proba = [proba]

        if self.sparse_output_:
            class_prob = None
            if self._strategy in ("most_frequent", "prior"):
                classes_ = [np.array([cp.argmax()]) for cp in class_prior_]

            elif self._strategy == "stratified":
                class_prob = class_prior_

            elif self._strategy == "uniform":
                    "Sparse target prediction is not "
                    "supported with the uniform strategy"
                )

            elif self._strategy == "constant":
                classes_ = [np.array([c]) for c in constant]

            y = _random_choice_csc(n_samples, classes_, class_prob, self.random_state)
        else:
                y = np.tile(
                    [
                        classes_[k][class_prior_[k].argmax()]
                        for k in range(self.n_outputs_)
                    ],
                    [n_samples, 1],
                )

                y = np.vstack(
                    [
                        classes_[k][proba[k].argmax(axis=1)]
                    ]
                ).T

                ret = [
                    classes_[k][rs.randint(n_classes_[k], size=n_samples)]
                    for k in range(self.n_outputs_)
                ]
                y = np.vstack(ret).T

                y = np.tile(self.constant, (n_samples, 1))

                y = np.ravel(y)

        return y

    def predict_proba(self, X):
        """
        Return probability estimates for the test vectors X.

            Test data.

        Returns
        -------
        P : ndarray of shape (n_samples, n_classes) or list of such arrays
            Returns the probability of the sample for each class in
            the model, where classes are ordered arithmetically, for each
            output.
        """

        P = []
        for k in range(self.n_outputs_):
            if self._strategy == "most_frequent":
                ind = class_prior_[k].argmax()
                out = np.zeros((n_samples, n_classes_[k]), dtype=np.float64)
                out[:, ind] = 1.0
            elif self._strategy == "prior":
                out = np.ones((n_samples, 1)) * class_prior_[k]

                out = rs.multinomial(1, class_prior_[k], size=n_samples)
                out = out.astype(np.float64)

                out = np.ones((n_samples, n_classes_[k]), dtype=np.float64)
                out /= n_classes_[k]

                ind = np.where(classes_[k] == constant[k])

            P.append(out)

            P = P[0]

        return P

    def predict_log_proba(self, X):
        """
        Return log probability estimates for the test vectors X.

        X : {array-like, object with finite length or shape}

        Returns
        -------
            Returns the log probability of the sample for each class in
            the model, where classes are ordered arithmetically for each
            output.
        """
        proba = self.predict_proba(X)
            return np.log(proba)
        else:
            return [np.log(p) for p in proba]

        tags.classifier_tags.poor_score = True
        tags.no_validation = True

        """Return the mean accuracy on the given test data and labels.

        X : None or array-like of shape (n_samples, n_features)
            Test samples. Passing None as test samples gives the same result
            as passing real test samples, since DummyClassifier
            operates independently of the sampled observations.

            True labels for X.

        Returns
        -------
            Mean accuracy of self.predict(X) w.r.t. y.
        """
        if X is None:
            X = np.zeros(shape=(len(y), 1))
        return super().score(X, y, sample_weight)

class DummyRegressor(MultiOutputMixin, RegressorMixin, BaseEstimator):
    """Regressor that makes predictions using simple rules.

    This regressor is useful as a simple baseline to compare with other
    (real) regressors. Do not use it for real problems.

    strategy : {"mean", "median", "quantile", "constant"}, default="mean"

        * "mean": always predicts the mean of the training set
        * "median": always predicts the median of the training set
        * "quantile": always predicts a specified quantile of the training set,
          provided with the quantile parameter.
        * "constant": always predicts a constant value that is provided by
          the user.

    constant : int or float or array-like of shape (n_outputs,), default=None

    quantile : float in [0.0, 1.0], default=None
        The quantile to predict using the "quantile" strategy. A quantile of
        0.5 corresponds to the median, while 0.0 to the minimum and 1.0 to the
        maximum.

    constant_ : ndarray of shape (1, n_outputs)
        Mean or median or quantile of the training targets or constant value
        given by the user.

    See Also
    --------
    DummyClassifier: Classifier that makes predictions using simple rules.

    Examples
    --------
    >>> from sklearn.dummy import DummyRegressor
    >>> X = np.array([1.0, 2.0, 3.0, 4.0])
    >>> y = np.array([2.0, 3.0, 5.0, 10.0])
    >>> dummy_regr = DummyRegressor(strategy="mean")
    >>> dummy_regr.fit(X, y)
    DummyRegressor()
    >>> dummy_regr.predict(X)
    array([5., 5., 5., 5.])
    >>> dummy_regr.score(X, y)
    0.0
    """

        "strategy": [StrOptions({"mean", "median", "quantile", "constant"})],
        "quantile": [Interval(Real, 0.0, 1.0, closed="both"), None],
        "constant": [
            Interval(Real, None, None, closed="neither"),
            "array-like",
            None,
        ],
    }

    def __init__(self, *, strategy="mean", constant=None, quantile=None):
        self.quantile = quantile

        """Fit the baseline regressor.

        Returns
        -------
            Fitted estimator.
        """

        y = check_array(y, ensure_2d=False, input_name="y")
        if len(y) == 0:
            raise ValueError("y must not be empty.")

        if self.strategy == "mean":
            self.constant_ = np.average(y, axis=0, weights=sample_weight)

        elif self.strategy == "median":
            if sample_weight is None:
                self.constant_ = np.median(y, axis=0)
            else:
                self.constant_ = [
                    _weighted_percentile(y[:, k], sample_weight, percentile_rank=50.0)
                ]

        elif self.strategy == "quantile":
            if self.quantile is None:
                    "When using `strategy='quantile', you have to specify the desired "
                    "quantile in the range [0, 1]."
                )
            percentile_rank = self.quantile * 100.0
                self.constant_ = np.percentile(y, axis=0, q=percentile_rank)
            else:
                    _weighted_percentile(
                        y[:, k], sample_weight, percentile_rank=percentile_rank
                    )
                ]

        elif self.strategy == "constant":
                )

            self.constant_ = check_array(
                self.constant,
                accept_sparse=["csr", "csc", "coo"],
                ensure_2d=False,
                ensure_min_samples=0,
            )

            if self.n_outputs_ != 1 and self.constant_.shape[0] != y.shape[1]:
                    "Constant target value should have shape (%d, 1)." % y.shape[1]
                )

        self.constant_ = np.reshape(self.constant_, (1, -1))

    def predict(self, X, return_std=False):

            Test data.

        return_std : bool, default=False
            Whether to return the standard deviation of posterior prediction.
            All zeros in this case.

            .. versionadded:: 0.20

        Returns
        -------

        y_std : array-like of shape (n_samples,) or (n_samples, n_outputs)
            Standard deviation of predictive distribution of query points.
        """

        y = np.full(
            (n_samples, self.n_outputs_),
            self.constant_,
            dtype=np.array(self.constant_).dtype,
        )
        y_std = np.zeros((n_samples, self.n_outputs_))

            y = np.ravel(y)
            y_std = np.ravel(y_std)

        return (y, y_std) if return_std else y

        tags.regressor_tags.poor_score = True

        """Return the coefficient of determination R^2 of the prediction.

        The coefficient R^2 is defined as `(1 - u/v)`, where `u` is the
        residual sum of squares `((y_true - y_pred) ** 2).sum()` and `v` is the
        total sum of squares `((y_true - y_true.mean()) ** 2).sum()`. The best
        possible score is 1.0 and it can be negative (because the model can be
        arbitrarily worse). A constant model that always predicts the expected
        value of y, disregarding the input features, would get a R^2 score of
        0.0.

            as passing real test samples, since `DummyRegressor`

            True values for X.

        Returns
        -------
            R^2 of `self.predict(X)` w.r.t. y.
        """

        Returns
        -------
        """

                (
                ),
            )

                )
            else:
                    )

        )

                    err_msg = (
                        )
                    )

            Test data.

        Returns
        -------
        """

                )

        else:
                    [
                    ],
                )

                    [
                    ]
                ).T

                ret = [
                ]

        return y

        """

            Test data.

        Returns
        -------
            output.
        """

        P = []

            P = P[0]

        return P

        """

        Returns
        -------
            output.
        """
        else:

        Returns
        -------
        """

        Returns
        -------
        """

            else:
                ]

                )
            else:
                    )
                ]

                )

            )

                )

            Test data.

        Returns
        -------

        """

        )

        0.0.

        Returns
        -------
        """

def _raise_or_warn_if_not_fitted(estimator):
    """A context manager to make sure a NotFittedError is raised, if a sub-estimator
    raises the error.

    Otherwise, we raise a warning if the pipeline is not fitted, with the deprecation.

    TODO(1.8): remove this context manager and replace with check_is_fitted.
    """
    try:
        yield
    except NotFittedError as exc:
        raise NotFittedError("Pipeline is not fitted yet.") from exc

    # we only get here if the above didn't raise
    try:
        check_is_fitted(estimator)
    except NotFittedError:
            "This Pipeline instance is not fitted yet. Call 'fit' with "
            "appropriate arguments before using other methods such as transform, "
            "predict, etc. This will raise an error in 1.8 instead of the current "
            "warning.",
        )

def _final_estimator_has(attr):
    """Check that final_estimator has `attr`.

    Used together with `available_if` in `Pipeline`."""

    def check(self):
        # raise original `AttributeError` if `attr` does not exist
        getattr(self._final_estimator, attr)
        return True

    return check

def _cached_transform(
    sub_pipeline, *, cache, param_name, param_value, transform_params
):
    """Transform a parameter value using a sub-pipeline and cache the result.

    sub_pipeline : Pipeline
        The sub-pipeline to be used for transformation.
    cache : dict
        The cache dictionary to store the transformed values.
    param_name : str
        The name of the parameter to be transformed.
    param_value : object
        The value of the parameter to be transformed.
    transform_params : dict
        The metadata to be used for transformation. This passed to the
        `transform` method of the sub-pipeline.

    Returns
    -------
    transformed_value : object
        The transformed value of the parameter.
    """
    if param_name not in cache:
        # If the parameter is a tuple, transform each element of the
        # tuple. This is needed to support the pattern present in
        # `lightgbm` and `xgboost` where users can pass multiple
        # validation sets.
        if isinstance(param_value, tuple):
            cache[param_name] = tuple(
                sub_pipeline.transform(element, **transform_params)
                for element in param_value
            )
        else:
            cache[param_name] = sub_pipeline.transform(param_value, **transform_params)

    return cache[param_name]

class Pipeline(_BaseComposition):
    """
    A sequence of data transformers with an optional final predictor.

    `Pipeline` allows you to sequentially apply a list of transformers to
    preprocess the data and, if desired, conclude the sequence with a final
    :term:`predictor` for predictive modeling.

    Intermediate steps of the pipeline must be transformers, that is, they
    must implement `fit` and `transform` methods.
    The final :term:`estimator` only needs to implement `fit`.
    The transformers in the pipeline can be cached using ``memory`` argument.

    The purpose of the pipeline is to assemble several steps that can be
    cross-validated together while setting different parameters. For this, it
    enables setting parameters of the various steps using their names and the
    parameter name separated by a `'__'`, as in the example below. A step's
    estimator may be replaced entirely by setting the parameter with its name
    to another estimator, or a transformer removed by setting it to
    `'passthrough'` or `None`.

    For an example use case of `Pipeline` combined with
    :class:`~sklearn.model_selection.GridSearchCV`, refer to
    :ref:`sphx_glr_auto_examples_compose_plot_compare_reduction.py`. The
    example :ref:`sphx_glr_auto_examples_compose_plot_digits_pipe.py` shows how
    to grid search on a pipeline using `'__'` as a separator in the parameter names.

    Read more in the :ref:`User Guide <pipeline>`.

    .. versionadded:: 0.5

    steps : list of tuples
        List of (name of step, estimator) tuples that are to be chained in
        sequential order. To be compatible with the scikit-learn API, all steps
        must define `fit`. All non-last steps must also define `transform`. See
        :ref:`Combining Estimators <combining_estimators>` for more details.

    transform_input : list of str, default=None
        The names of the :term:`metadata` parameters that should be transformed by the
        pipeline before passing it to the step consuming it.

        This enables transforming some input arguments to ``fit`` (other than ``X``)
        to be transformed by the steps of the pipeline up to the step which requires
        them. Requirement is defined via :ref:`metadata routing <metadata_routing>`.
        For instance, this can be used to pass a validation set through the pipeline.

        You can only set this if metadata routing is enabled, which you
        can enable using ``sklearn.set_config(enable_metadata_routing=True)``.

        .. versionadded:: 1.6

    memory : str or object with the joblib.Memory interface, default=None
        Used to cache the fitted transformers of the pipeline. The last step
        will never be cached, even if it is a transformer. By default, no
        caching is performed. If a string is given, it is the path to the
        caching directory. Enabling caching triggers a clone of the transformers
        before fitting. Therefore, the transformer instance given to the
        pipeline cannot be inspected directly. Use the attribute ``named_steps``
        or ``steps`` to inspect estimators within the pipeline. Caching the
        transformers is advantageous when fitting is time consuming. See
        :ref:`sphx_glr_auto_examples_neighbors_plot_caching_nearest_neighbors.py`
        for an example on how to enable caching.

    verbose : bool, default=False
        If True, the time elapsed while fitting each step will be printed as it
        is completed.

    named_steps : :class:`~sklearn.utils.Bunch`
        Dictionary-like object, with the following attributes.
        Read-only attribute to access any step parameter by user given name.
        Keys are step names and values are steps parameters.

    classes_ : ndarray of shape (n_classes,)
        The classes labels. Only exist if the last step of the pipeline is a
        classifier.

        Number of features seen during :term:`fit`. Only defined if the
        underlying first estimator in `steps` exposes such an attribute
        when fit.

        Names of features seen during :term:`fit`. Only defined if the
        underlying estimator exposes such an attribute when fit.

    See Also
    --------
    make_pipeline : Convenience function for simplified pipeline construction.

    Examples
    --------
    >>> from sklearn.svm import SVC
    >>> from sklearn.preprocessing import StandardScaler
    >>> from sklearn.datasets import make_classification
    >>> from sklearn.model_selection import train_test_split
    >>> from sklearn.pipeline import Pipeline
    >>> X, y = make_classification(random_state=0)
    >>> X_train, X_test, y_train, y_test = train_test_split(X, y,
    ...                                                     random_state=0)
    >>> pipe = Pipeline([('scaler', StandardScaler()), ('svc', SVC())])
    >>> # The pipeline can be used as any other estimator
    >>> # and avoids leaking the test set into the train set
    >>> pipe.fit(X_train, y_train).score(X_test, y_test)
    0.88
    >>> # An estimator's parameter can be set using '__' syntax
    >>> pipe.set_params(svc__C=10).fit(X_train, y_train).score(X_test, y_test)
    0.76
    """

    # BaseEstimator interface
        "steps": [list, Hidden(tuple)],
        "transform_input": [list, None],
        "memory": [None, str, HasMethods(["cache"])],
        "verbose": ["boolean"],
    }

    def __init__(self, steps, *, transform_input=None, memory=None, verbose=False):
        self.steps = steps
        self.transform_input = transform_input
        self.memory = memory
        self.verbose = verbose

    def set_output(self, *, transform=None):
        """Set the output container when `"transform"` and `"fit_transform"` are called.

        Calling `set_output` will set the output of all estimators in `steps`.

        transform : {"default", "pandas", "polars"}, default=None
            Configure output of `transform` and `fit_transform`.

            - `"default"`: Default output format of a transformer
            - `"pandas"`: DataFrame output
            - `"polars"`: Polars output
            - `None`: Transform configuration is unchanged

                `"polars"` option was added.

        Returns
        -------
        """
        for _, _, step in self._iter():
            _safe_set_output(step, transform=transform)

        """Get parameters for this estimator.

        Returns the parameters given in the constructor as well as the
        estimators contained within the `steps` of the `Pipeline`.

        Returns
        -------
        params : mapping of string to any
        """
        return self._get_params("steps", deep=deep)

    def set_params(self, **kwargs):

        Valid parameter keys can be listed with ``get_params()``. Note that
        you can directly set the parameters of the estimators contained in
        `steps`.

            Parameters of this estimator or parameters of estimators contained
            in `steps`. Parameters of the steps may be set using its name and
            the parameter name separated by a '__'.

        Returns
        -------
            Pipeline class instance.
        """
        self._set_params("steps", **kwargs)

    def _validate_steps(self):
        names, estimators = zip(*self.steps)

        # validate names
        self._validate_names(names)

        # validate estimators
        transformers = estimators[:-1]
        estimator = estimators[-1]

        for t in transformers:
            if t is None or t == "passthrough":
                continue
            if not (hasattr(t, "fit") or hasattr(t, "fit_transform")) or not hasattr(
                t, "transform"
            ):
                    "All intermediate steps should be "
                    "transformers and implement fit and transform "
                    "or be the string 'passthrough' "
                    "'%s' (type %s) doesn't" % (t, type(t))
                )

        # We allow last estimator to be None as an identity transformation
        if (
            estimator is not None
            and estimator != "passthrough"
            and not hasattr(estimator, "fit")
        ):
                "Last step of Pipeline should implement fit "
                "or be the string 'passthrough'. "
                "'%s' (type %s) doesn't" % (estimator, type(estimator))
            )

    def _iter(self, with_final=True, filter_passthrough=True):
        """
        Generate (idx, (name, trans)) tuples from self.steps

        When filter_passthrough is True, 'passthrough' and None transformers
        are filtered out.
        """
        stop = len(self.steps)
        if not with_final:
            stop -= 1

        for idx, (name, trans) in enumerate(islice(self.steps, 0, stop)):
            if not filter_passthrough:
                yield idx, name, trans
            elif trans is not None and trans != "passthrough":

    def __len__(self):
        """
        Returns the length of the Pipeline
        """
        return len(self.steps)

    def __getitem__(self, ind):
        """Returns a sub-pipeline or a single estimator in the pipeline

        Indexing with an integer will return an estimator; using a slice
        returns another Pipeline instance which copies a slice of this
        Pipeline. This copy is shallow: modifying (or fitting) estimators in
        the sub-pipeline will affect the larger pipeline and vice-versa.
        However, replacing a value in `step` will not affect a copy.

        See
        :ref:`sphx_glr_auto_examples_feature_selection_plot_feature_selection_pipeline.py`
        for an example of how to use slicing to inspect part of a pipeline.
        """
        if isinstance(ind, slice):
            if ind.step not in (1, None):
                raise ValueError("Pipeline slicing only supports a step of 1")
            return self.__class__(
                self.steps[ind], memory=self.memory, verbose=self.verbose
            )
        try:
            name, est = self.steps[ind]
        except TypeError:
            # Not an int, try get step by name
            return self.named_steps[ind]
        return est

    # TODO(1.8): Remove this property
    @property
    def _estimator_type(self):
        """Return the estimator type of the last step in the pipeline."""

        if not self.steps:
            return None

        return self.steps[-1][1]._estimator_type

    @property
    def named_steps(self):
        """Access the steps by name.

        Read-only attribute to access any step by given name.
        Keys are steps names and values are the steps objects."""
        # Use Bunch object to improve autocomplete
        return Bunch(**dict(self.steps))

    @property
    def _final_estimator(self):
        try:
            estimator = self.steps[-1][1]
            return "passthrough" if estimator is None else estimator
        except (ValueError, AttributeError, TypeError):
            # This condition happens when a call to a method is first calling
            # `_available_if` and `fit` did not validate `steps` yet. We
            # return `None` and an `InvalidParameterError` will be raised
            # right after.

    def _log_message(self, step_idx):
        if not self.verbose:
        name, _ = self.steps[step_idx]

        return "(step %d of %d) Processing %s" % (step_idx + 1, len(self.steps), name)

    def _check_method_params(self, method, props, **kwargs):
            routed_params = process_routing(self, method, **props, **kwargs)
            return routed_params
        else:
            fit_params_steps = Bunch(
                **{
                    name: Bunch(**{method: {} for method in METHODS})
                    for name, step in self.steps
                    if step is not None
                }
            )
            for pname, pval in props.items():
                if "__" not in pname:
                        "Pipeline.fit does not accept the {} parameter. "
                        "You can pass parameters to specific steps of your "
                        "pipeline using the stepname__parameter format, e.g. "
                        "`Pipeline.fit(X, y, logisticregression__sample_weight"
                        "=sample_weight)`.".format(pname)
                    )
                step, param = pname.split("__", 1)
                fit_params_steps[step]["fit"][param] = pval
                # without metadata routing, fit_transform and fit_predict
                # get all the same params and pass it to the last fit.
                fit_params_steps[step]["fit_transform"][param] = pval
                fit_params_steps[step]["fit_predict"][param] = pval
            return fit_params_steps

    def _get_metadata_for_step(self, *, step_idx, step_params, all_params):
        """Get params (metadata) for step `name`.

        This transforms the metadata up to this step if required, which is
        indicated by the `transform_input` parameter.

        If a param in `step_params` is included in the `transform_input` list,
        it will be transformed.

        step_idx : int
            Index of the step in the pipeline.

        step_params : dict
            Parameters specific to the step. These are routed parameters, e.g.
            `routed_params[name]`. If a parameter name here is included in the
            `pipeline.transform_input`, then it will be transformed. Note that
            these parameters are *after* routing, so the aliases are already
            resolved.

        all_params : dict
            All parameters passed by the user. Here this is used to call
            `transform` on the slice of the pipeline itself.

        Returns
        -------
        dict
            Parameters to be passed to the step. The ones which should be
            transformed are transformed.
        """
        if (
            self.transform_input is None
            or not all_params
            or not step_params
            or step_idx == 0
        ):
            # we only need to process step_params if transform_input is set
            # and metadata is given by the user.
            return step_params

        sub_pipeline = self[:step_idx]
        sub_metadata_routing = get_routing_for_object(sub_pipeline)
        # here we get the metadata required by sub_pipeline.transform
        transform_params = {
            key: value
            for key, value in all_params.items()
            if key
            in sub_metadata_routing.consumes(
                method="transform", params=all_params.keys()
            )
        }
        transformed_params = dict()  # this is to be returned
        transformed_cache = dict()  # used to transform each param once
        # `step_params` is the output of `process_routing`, so it has a dict for each
        # method (e.g. fit, transform, predict), which are the args to be passed to
        # those methods. We need to transform the parameters which are in the
        # `transform_input`, before returning these dicts.
        for method, method_params in step_params.items():
            transformed_params[method] = Bunch()
            for param_name, param_value in method_params.items():
                # An example of `(param_name, param_value)` is
                # `('sample_weight', array([0.5, 0.5, ...]))`
                if param_name in self.transform_input:
                    # This parameter now needs to be transformed by the sub_pipeline, to
                    # this step. We cache these computations to avoid repeating them.
                    transformed_params[method][param_name] = _cached_transform(
                        sub_pipeline,
                        cache=transformed_cache,
                        param_name=param_name,
                        param_value=param_value,
                        transform_params=transform_params,
                    )
                else:
                    transformed_params[method][param_name] = param_value
        return transformed_params

    # Estimator interface

    def _fit(self, X, y=None, routed_params=None, raw_params=None):
        """Fit the pipeline except the last step.

        routed_params is the output of `process_routing`
        raw_params is the parameters passed by the user, used when `transform_input`
            is set by the user, to transform metadata using a sub-pipeline.
        """
        # shallow copy of steps - this should really be steps_
        self.steps = list(self.steps)
        self._validate_steps()
        # Setup the memory
        memory = check_memory(self.memory)

        fit_transform_one_cached = memory.cache(_fit_transform_one)

        for step_idx, name, transformer in self._iter(
            with_final=False, filter_passthrough=False
        ):
            if transformer is None or transformer == "passthrough":
                with _print_elapsed_time("Pipeline", self._log_message(step_idx)):
                    continue

            if hasattr(memory, "location") and memory.location is None:
                # we do not clone when caching is disabled to
                # preserve backward compatibility
                cloned_transformer = transformer
            else:
                cloned_transformer = clone(transformer)
            # Fit or load from cache the current transformer
            step_params = self._get_metadata_for_step(
                step_idx=step_idx,
                step_params=routed_params[name],
                all_params=raw_params,
            )

            X, fitted_transformer = fit_transform_one_cached(
                cloned_transformer,
                X,
                y,
                weight=None,
                message_clsname="Pipeline",
                message=self._log_message(step_idx),
                params=step_params,
            )
            # Replace the transformer of the step with the fitted
            # transformer. This is necessary when loading the transformer
            # from the cache.
            self.steps[step_idx] = (name, fitted_transformer)
        return X

    @_fit_context(
        # estimators in Pipeline.steps are not validated yet
        prefer_skip_nested_validation=False
    )
    def fit(self, X, y=None, **params):
        """Fit the model.

        Fit all the transformers one after the other and sequentially transform the
        data. Finally, fit the transformed data using the final estimator.

        X : iterable
            Training data. Must fulfill input requirements of first step of the
            pipeline.

        y : iterable, default=None
            Training targets. Must fulfill label requirements for all steps of
            the pipeline.

        **params : dict of str -> object
            - If `enable_metadata_routing=False` (default): Parameters passed to the
              ``fit`` method of each step, where each parameter name is prefixed such
              that parameter ``p`` for step ``s`` has key ``s__p``.

            - If `enable_metadata_routing=True`: Parameters requested and accepted by
              steps. Each step must have requested certain metadata for these parameters
              to be forwarded to them.

            .. versionchanged:: 1.4
                Parameters are now passed to the ``transform`` method of the
                intermediate steps as well, if requested, and if
                `enable_metadata_routing=True` is set via
                :func:`~sklearn.set_config`.

            See :ref:`Metadata Routing User Guide <metadata_routing>` for more
            details.

        Returns
        -------
            Pipeline with fitted steps.
        """
        if not _routing_enabled() and self.transform_input is not None:
                "The `transform_input` parameter can only be set if metadata "
                "routing is enabled. You can enable metadata routing using "
                "`sklearn.set_config(enable_metadata_routing=True)`."
            )

        routed_params = self._check_method_params(method="fit", props=params)
        Xt = self._fit(X, y, routed_params, raw_params=params)
        with _print_elapsed_time("Pipeline", self._log_message(len(self.steps) - 1)):
            if self._final_estimator != "passthrough":
                last_step_params = self._get_metadata_for_step(
                    step_idx=len(self) - 1,
                    step_params=routed_params[self.steps[-1][0]],
                    all_params=params,
                )
                self._final_estimator.fit(Xt, y, **last_step_params["fit"])

    def _can_fit_transform(self):
        return (
            self._final_estimator == "passthrough"
            or hasattr(self._final_estimator, "transform")
            or hasattr(self._final_estimator, "fit_transform")
        )

    @available_if(_can_fit_transform)
    )
    def fit_transform(self, X, y=None, **params):
        """Fit the model and transform with the final estimator.

        Fit all the transformers one after the other and sequentially transform
        the data. Only valid if the final estimator either implements
        `fit_transform` or `fit` and `transform`.

            pipeline.

                `enable_metadata_routing=True`.

            details.

        Returns
        -------
        Xt : ndarray of shape (n_samples, n_transformed_features)
            Transformed samples.
        """
        routed_params = self._check_method_params(method="fit_transform", props=params)
        Xt = self._fit(X, y, routed_params)

        last_step = self._final_estimator
            if last_step == "passthrough":
                return Xt
            last_step_params = self._get_metadata_for_step(
                step_idx=len(self) - 1,
                step_params=routed_params[self.steps[-1][0]],
                all_params=params,
            )
            if hasattr(last_step, "fit_transform"):
                return last_step.fit_transform(
                    Xt, y, **last_step_params["fit_transform"]
                )
            else:
                return last_step.fit(Xt, y, **last_step_params["fit"]).transform(
                    Xt, **last_step_params["transform"]
                )

    @available_if(_final_estimator_has("predict"))
    def predict(self, X, **params):
        """Transform the data, and apply `predict` with the final estimator.

        Call `transform` of each transformer in the pipeline. The transformed
        data are finally passed to the final estimator that calls `predict`
        method. Only valid if the final estimator implements `predict`.

            Data to predict on. Must fulfill input requirements of first step
            of the pipeline.

            - If `enable_metadata_routing=False` (default): Parameters to the
              ``predict`` called at the end of all transformations in the pipeline.

            details.

            Note that while this may be used to return uncertainties from some
            models with ``return_std`` or ``return_cov``, uncertainties that are
            generated by the transformations in the pipeline are not propagated
            to the final estimator.

        Returns
        -------
        y_pred : ndarray
            Result of calling `predict` on the final estimator.
        """
        # TODO(1.8): Remove the context manager and use check_is_fitted(self)
        with _raise_or_warn_if_not_fitted(self):
            Xt = X

            if not _routing_enabled():
                for _, name, transform in self._iter(with_final=False):
                    Xt = transform.transform(Xt)
                return self.steps[-1][1].predict(Xt, **params)

            # metadata routing enabled
            routed_params = process_routing(self, "predict", **params)
            for _, name, transform in self._iter(with_final=False):
                Xt = transform.transform(Xt, **routed_params[name].transform)
            return self.steps[-1][1].predict(
                Xt, **routed_params[self.steps[-1][0]].predict
            )

    @available_if(_final_estimator_has("fit_predict"))
    )
    def fit_predict(self, X, y=None, **params):
        """Transform the data, and apply `fit_predict` with the final estimator.

        Call `fit_transform` of each transformer in the pipeline. The
        transformed data are finally passed to the final estimator that calls
        `fit_predict` method. Only valid if the final estimator implements
        `fit_predict`.

            Training data. Must fulfill input requirements of first step of

            Training targets. Must fulfill label requirements for all steps

            details.

        Returns
        -------
            Result of calling `fit_predict` on the final estimator.
        """
        routed_params = self._check_method_params(method="fit_predict", props=params)

        params_last_step = routed_params[self.steps[-1][0]]
            y_pred = self.steps[-1][1].fit_predict(
                Xt, y, **params_last_step.get("fit_predict", {})
            )
        return y_pred

    @available_if(_final_estimator_has("predict_proba"))
    def predict_proba(self, X, **params):
        """Transform the data, and apply `predict_proba` with the final estimator.

        data are finally passed to the final estimator that calls
        `predict_proba` method. Only valid if the final estimator implements
        `predict_proba`.

              `predict_proba` called at the end of all transformations in the pipeline.

            details.

        Returns
        -------
        y_proba : ndarray of shape (n_samples, n_classes)
            Result of calling `predict_proba` on the final estimator.
        """
            Xt = X

                return self.steps[-1][1].predict_proba(Xt, **params)

            routed_params = process_routing(self, "predict_proba", **params)
            return self.steps[-1][1].predict_proba(
                Xt, **routed_params[self.steps[-1][0]].predict_proba
            )

    @available_if(_final_estimator_has("decision_function"))
    def decision_function(self, X, **params):
        """Transform the data, and apply `decision_function` with the final estimator.

        `decision_function` method. Only valid if the final estimator
        implements `decision_function`.

        **params : dict of string -> object
            Parameters requested and accepted by steps. Each step must have
            requested certain metadata for these parameters to be forwarded to
            them.

                Only available if `enable_metadata_routing=True`. See
                :ref:`Metadata Routing User Guide <metadata_routing>` for more
                details.

        Returns
        -------
        y_score : ndarray of shape (n_samples, n_classes)
            Result of calling `decision_function` on the final estimator.
        """
            _raise_for_params(params, self, "decision_function")

            # not branching here since params is only available if
            # enable_metadata_routing=True
            routed_params = process_routing(self, "decision_function", **params)

            Xt = X
                Xt = transform.transform(
                    Xt, **routed_params.get(name, {}).get("transform", {})
                )
            return self.steps[-1][1].decision_function(
                Xt,
                **routed_params.get(self.steps[-1][0], {}).get("decision_function", {}),
            )

    @available_if(_final_estimator_has("score_samples"))
    def score_samples(self, X):
        """Transform the data, and apply `score_samples` with the final estimator.

        `score_samples` method. Only valid if the final estimator implements
        `score_samples`.

        Returns
        -------
        y_score : ndarray of shape (n_samples,)
            Result of calling `score_samples` on the final estimator.
        """
            Xt = X
            for _, _, transformer in self._iter(with_final=False):
                Xt = transformer.transform(Xt)
            return self.steps[-1][1].score_samples(Xt)

    @available_if(_final_estimator_has("predict_log_proba"))
    def predict_log_proba(self, X, **params):
        """Transform the data, and apply `predict_log_proba` with the final estimator.

        `predict_log_proba` method. Only valid if the final estimator
        implements `predict_log_proba`.

              `predict_log_proba` called at the end of all transformations in the
              pipeline.

            details.

        Returns
        -------
        y_log_proba : ndarray of shape (n_samples, n_classes)
            Result of calling `predict_log_proba` on the final estimator.
        """
            Xt = X

                return self.steps[-1][1].predict_log_proba(Xt, **params)

            routed_params = process_routing(self, "predict_log_proba", **params)
            return self.steps[-1][1].predict_log_proba(
                Xt, **routed_params[self.steps[-1][0]].predict_log_proba
            )

    def _can_transform(self):
        return self._final_estimator == "passthrough" or hasattr(
            self._final_estimator, "transform"
        )

    @available_if(_can_transform)
    def transform(self, X, **params):
        """Transform the data, and apply `transform` with the final estimator.

        `transform` method. Only valid if the final estimator
        implements `transform`.

        This also works where final estimator is `None` in which case all prior
        transformations are applied.

            Data to transform. Must fulfill input requirements of first step

            them.

                details.

        Returns
        -------
        """
            _raise_for_params(params, self, "transform")

            routed_params = process_routing(self, "transform", **params)
            Xt = X
            for _, name, transform in self._iter():
            return Xt

    def _can_inverse_transform(self):
        return all(hasattr(t, "inverse_transform") for _, _, t in self._iter())

    @available_if(_can_inverse_transform)
    def inverse_transform(self, X, **params):
        """Apply `inverse_transform` for each step in a reverse order.

        All estimators in the pipeline must support `inverse_transform`.

        X : array-like of shape (n_samples, n_transformed_features)
            Data samples, where ``n_samples`` is the number of samples and
            ``n_features`` is the number of features. Must fulfill
            input requirements of last step of pipeline's
            ``inverse_transform`` method.

            them.

                details.

        Returns
        -------
        Xt : ndarray of shape (n_samples, n_features)
            Inverse transformed data, that is, data in the original feature
            space.
        """
            _raise_for_params(params, self, "inverse_transform")

            # we don't have to branch here, since params is only non-empty if
            # enable_metadata_routing=True.
            routed_params = process_routing(self, "inverse_transform", **params)
            reverse_iter = reversed(list(self._iter()))
            for _, name, transform in reverse_iter:
                X = transform.inverse_transform(
                    X, **routed_params[name].inverse_transform
                )
            return X

    @available_if(_final_estimator_has("score"))
    def score(self, X, y=None, sample_weight=None, **params):
        """Transform the data, and apply `score` with the final estimator.

        `score` method. Only valid if the final estimator implements `score`.

            Targets used for scoring. Must fulfill label requirements for all
            steps of the pipeline.

        sample_weight : array-like, default=None
            If not None, this argument is passed as ``sample_weight`` keyword
            argument to the ``score`` method of the final estimator.

            them.

                details.

        Returns
        -------
            Result of calling `score` on the final estimator.
        """
            Xt = X
                score_params = {}
                if sample_weight is not None:
                    score_params["sample_weight"] = sample_weight
                return self.steps[-1][1].score(Xt, y, **score_params)

            # metadata routing is enabled.
            routed_params = process_routing(
                self, "score", sample_weight=sample_weight, **params
            )

            Xt = X
            return self.steps[-1][1].score(
                Xt, y, **routed_params[self.steps[-1][0]].score
            )

    @property
    def classes_(self):
        """The classes labels. Only exist if the last step is a classifier."""
        return self.steps[-1][1].classes_

            return tags

        try:
            if self.steps[0][1] is not None and self.steps[0][1] != "passthrough":
                tags.input_tags.pairwise = get_tags(
                    self.steps[0][1]
                ).input_tags.pairwise
            # WARNING: the sparse tag can be incorrect.
            # Some Pipelines accepting sparse data are wrongly tagged sparse=False.
            # For example Pipeline([PCA(), estimator]) accepts sparse data
            # even if the estimator doesn't as PCA outputs a dense array.
            tags.input_tags.sparse = all(
                get_tags(step).input_tags.sparse
                for name, step in self.steps
                if step is not None and step != "passthrough"
            )
            # This happens when the `steps` is not a list of (name, estimator)
            # tuples and `fit` is not called yet to validate the steps.
            pass

        try:
            if self.steps[-1][1] is not None and self.steps[-1][1] != "passthrough":
                last_step_tags = get_tags(self.steps[-1][1])
                tags.estimator_type = last_step_tags.estimator_type
                tags.target_tags.multi_output = last_step_tags.target_tags.multi_output
                tags.classifier_tags = deepcopy(last_step_tags.classifier_tags)
                tags.regressor_tags = deepcopy(last_step_tags.regressor_tags)
                tags.transformer_tags = deepcopy(last_step_tags.transformer_tags)
            pass

        Transform input features using the pipeline.

        Returns
        -------
        """
        feature_names_out = input_features
        for _, name, transform in self._iter():
            if not hasattr(transform, "get_feature_names_out"):
                raise AttributeError(
                    "Estimator {} does not provide get_feature_names_out. "
                    "Did you mean to call pipeline[:-1].get_feature_names_out"
                    "()?".format(name)
                )
            feature_names_out = transform.get_feature_names_out(feature_names_out)
        return feature_names_out

    @property
    def n_features_in_(self):
        """Number of features seen during first step `fit` method."""
        # delegate to first step (which will call check_is_fitted)
        return self.steps[0][1].n_features_in_

    @property
    def feature_names_in_(self):
        """Names of features seen during first step `fit` method."""
        return self.steps[0][1].feature_names_in_

    def __sklearn_is_fitted__(self):
        """Indicate whether pipeline has been fit.

        This is done by checking whether the last non-`passthrough` step of the
        pipeline is fitted.

        An empty pipeline is considered fitted.
        """

        # First find the last step that is not 'passthrough'
        last_step = None
        for _, estimator in reversed(self.steps):
            if estimator != "passthrough":
                last_step = estimator
                break

        if last_step is None:
            # All steps are 'passthrough', so the pipeline is considered fitted
            return True

        try:
            # check if the last step of the pipeline is fitted
            # we only check the last step since if the last step is fit, it
            # means the previous steps should also be fit. This is faster than
            # checking if every step of the pipeline is fit.
            check_is_fitted(last_step)
        except NotFittedError:
            return False

    def _sk_visual_block_(self):
        _, estimators = zip(*self.steps)

        def _get_name(name, est):
            if est is None or est == "passthrough":
                return f"{name}: passthrough"
            # Is an estimator
            return f"{name}: {est.__class__.__name__}"

        names = [_get_name(name, est) for name, est in self.steps]
        name_details = [str(est) for est in estimators]
        return _VisualBlock(
            "serial",
            estimators,
            names=names,
            name_details=name_details,
            dash_wrapped=False,
        )

    def get_metadata_routing(self):
        """Get metadata routing of this object.

        Please check :ref:`User Guide <metadata_routing>` on how the routing
        mechanism works.

        Returns
        -------
        routing : MetadataRouter
            A :class:`~sklearn.utils.metadata_routing.MetadataRouter` encapsulating
            routing information.
        """
        router = MetadataRouter(owner=self.__class__.__name__)

        # first we add all steps except the last one
        for _, name, trans in self._iter(with_final=False, filter_passthrough=True):
            method_mapping = MethodMapping()
            # fit, fit_predict, and fit_transform call fit_transform if it
            # exists, or else fit and transform
            if hasattr(trans, "fit_transform"):
                (
                    method_mapping.add(caller="fit", callee="fit_transform")
                    .add(caller="fit_transform", callee="fit_transform")
                    .add(caller="fit_predict", callee="fit_transform")
                )
            else:
                (
                    method_mapping.add(caller="fit", callee="fit")
                    .add(caller="fit", callee="transform")
                    .add(caller="fit_transform", callee="fit")
                    .add(caller="fit_transform", callee="transform")
                    .add(caller="fit_predict", callee="fit")
                    .add(caller="fit_predict", callee="transform")
                )

            (
                method_mapping.add(caller="predict", callee="transform")
                .add(caller="predict", callee="transform")
                .add(caller="predict_proba", callee="transform")
                .add(caller="decision_function", callee="transform")
                .add(caller="predict_log_proba", callee="transform")
                .add(caller="transform", callee="transform")
                .add(caller="inverse_transform", callee="inverse_transform")
                .add(caller="score", callee="transform")
            )

            router.add(method_mapping=method_mapping, **{name: trans})

        final_name, final_est = self.steps[-1]
        if final_est is None or final_est == "passthrough":
            return router

        # then we add the last step
        method_mapping = MethodMapping()
        if hasattr(final_est, "fit_transform"):
            method_mapping.add(caller="fit_transform", callee="fit_transform")
        else:
            method_mapping.add(caller="fit", callee="fit").add(
                caller="fit", callee="transform"
            )
        (
            method_mapping.add(caller="fit", callee="fit")
            .add(caller="predict", callee="predict")
            .add(caller="fit_predict", callee="fit_predict")
            .add(caller="predict_proba", callee="predict_proba")
            .add(caller="decision_function", callee="decision_function")
            .add(caller="predict_log_proba", callee="predict_log_proba")
            .add(caller="transform", callee="transform")
            .add(caller="inverse_transform", callee="inverse_transform")
            .add(caller="score", callee="score")
        )

        router.add(method_mapping=method_mapping, **{final_name: final_est})
        return router

def _name_estimators(estimators):
    """Generate names for estimators."""

    names = [
        estimator if isinstance(estimator, str) else type(estimator).__name__.lower()
        for estimator in estimators
    ]
    namecount = defaultdict(int)
    for est, name in zip(estimators, names):
        namecount[name] += 1

    for k, v in list(namecount.items()):
        if v == 1:
            del namecount[k]

    for i in reversed(range(len(estimators))):
        name = names[i]
        if name in namecount:
            names[i] += "-%d" % namecount[name]
            namecount[name] -= 1

    return list(zip(names, estimators))

def make_pipeline(*steps, memory=None, transform_input=None, verbose=False):
    """Construct a :class:`Pipeline` from the given estimators.

    This is a shorthand for the :class:`Pipeline` constructor; it does not
    require, and does not permit, naming the estimators. Instead, their names
    will be set to the lowercase of their types automatically.

    *steps : list of Estimator objects
        List of the scikit-learn estimators that are chained together.

        transformers is advantageous when fitting is time consuming.

        This can be used to pass a validation set through the pipeline for instance.

    Returns
    -------
    p : Pipeline
        Returns a scikit-learn :class:`Pipeline` object.

    See Also
    --------
    Pipeline : Class for creating a pipeline of transforms with a final
        estimator.

    Examples
    --------
    >>> from sklearn.naive_bayes import GaussianNB
    >>> from sklearn.pipeline import make_pipeline
    >>> make_pipeline(StandardScaler(), GaussianNB(priors=None))
    Pipeline(steps=[('standardscaler', StandardScaler()),
                    ('gaussiannb', GaussianNB())])
    """
    return Pipeline(
        _name_estimators(steps),
        transform_input=transform_input,
        memory=memory,
        verbose=verbose,
    )

def _transform_one(transformer, X, y, weight, params):
    """Call transform and apply weight to output.

    transformer : estimator
        Estimator to be used for transformation.

    X : {array-like, sparse matrix} of shape (n_samples, n_features)
        Input data to be transformed.

    y : ndarray of shape (n_samples,)
        Ignored.

    weight : float
        Weight to be applied to the output of the transformation.

    params : dict
        Parameters to be passed to the transformer's ``transform`` method.

        This should be of the form ``process_routing()["step_name"]``.
    """
    res = transformer.transform(X, **params.transform)
    # if we have a weight for this transformer, multiply output
    if weight is None:
        return res
    return res * weight

def _fit_transform_one(
    transformer, X, y, weight, message_clsname="", message=None, params=None
):
    """
    Fits ``transformer`` to ``X`` and ``y``. The transformed result is returned
    with the fitted transformer. If ``weight`` is not ``None``, the result will
    be multiplied by ``weight``.

    ``params`` needs to be of the form ``process_routing()["step_name"]``.
    """
    params = params or {}
    with _print_elapsed_time(message_clsname, message):
        if hasattr(transformer, "fit_transform"):
            res = transformer.fit_transform(X, y, **params.get("fit_transform", {}))
        else:
            res = transformer.fit(X, y, **params.get("fit", {})).transform(
                X, **params.get("transform", {})
            )

        return res, transformer
    return res * weight, transformer

def _fit_one(transformer, X, y, weight, message_clsname="", message=None, params=None):
    """
    Fits ``transformer`` to ``X`` and ``y``.
    """
        return transformer.fit(X, y, **params["fit"])

class FeatureUnion(TransformerMixin, _BaseComposition):
    """Concatenates results of multiple transformer objects.

    This estimator applies a list of transformer objects in parallel to the
    input data, then concatenates the results. This is useful to combine
    several feature extraction mechanisms into a single transformer.

    Parameters of the transformers may be set using its name and the parameter
    name separated by a '__'. A transformer may be replaced entirely by
    setting the parameter with its name to another transformer, removed by
    setting to 'drop' or disabled by setting to 'passthrough' (features are
    passed without transformation).

    Read more in the :ref:`User Guide <feature_union>`.

    transformer_list : list of (str, transformer) tuples
        List of transformer objects to be applied to the data. The first
        half of each tuple is the name of the transformer. The transformer can
        be 'drop' for it to be ignored or can be 'passthrough' for features to
        be passed unchanged.

           Added the option `"passthrough"`.

        .. versionchanged:: 0.22
           Deprecated `None` as a transformer in favor of 'drop'.

        Number of jobs to run in parallel.

        .. versionchanged:: v0.20
           `n_jobs` default changed from 1 to None

    transformer_weights : dict, default=None
        Multiplicative weights for features per transformer.
        Keys are transformer names, values the weights.
        Raises ValueError if key not present in ``transformer_list``.

        If True, the time elapsed while fitting each transformer will be
        printed as it is completed.

    verbose_feature_names_out : bool, default=True
        If True, :meth:`get_feature_names_out` will prefix all feature names
        with the name of the transformer that generated that feature.
        If False, :meth:`get_feature_names_out` will not prefix any feature
        names and will error if feature names are not unique.

        .. versionadded:: 1.5

    named_transformers : :class:`~sklearn.utils.Bunch`
        Read-only attribute to access any transformer parameter by user
        given name. Keys are transformer names and values are
        transformer parameters.

        underlying first transformer in `transformer_list` exposes such an
        attribute when fit.

        Names of features seen during :term:`fit`. Defined only when
        `X` has feature names that are all strings.

        .. versionadded:: 1.3

    See Also
    --------
    make_union : Convenience function for simplified feature union
        construction.

    Examples
    --------
    >>> from sklearn.pipeline import FeatureUnion
    >>> from sklearn.decomposition import PCA, TruncatedSVD
    >>> union = FeatureUnion([("pca", PCA(n_components=1)),
    ...                       ("svd", TruncatedSVD(n_components=2))])
    >>> X = [[0., 1., 3], [2., 2., 5]]
    >>> union.fit_transform(X)
    array([[-1.5       ,  3.0..., -0.8...],
           [ 1.5       ,  5.7...,  0.4...]])
    >>> union.set_params(svd__n_components=1).fit_transform(X)
    array([[-1.5       ,  3.0...],
           [ 1.5       ,  5.7...]])

    :ref:`sphx_glr_auto_examples_compose_plot_feature_union.py`.
    """

        self,
        transformer_list,
        *,
        transformer_weights=None,
        verbose=False,
        verbose_feature_names_out=True,
    ):
        self.transformer_list = transformer_list
        self.transformer_weights = transformer_weights
        self.verbose_feature_names_out = verbose_feature_names_out

        `set_output` will set the output of all estimators in `transformer_list`.

        Returns
        -------
        """
        super().set_output(transform=transform)
        for _, step, _ in self._iter():

    @property
    def named_transformers(self):
        return Bunch(**dict(self.transformer_list))

        estimators contained within the `transformer_list` of the
        `FeatureUnion`.

        Returns
        -------
        """
        return self._get_params("transformer_list", deep=deep)

        `transformer_list`.

            in `transform_list`. Parameters of the transformers may be set
            using its name and the parameter name separated by a '__'.

        Returns
        -------
            FeatureUnion class instance.
        """
        self._set_params("transformer_list", **kwargs)

    def _validate_transformers(self):
        names, transformers = zip(*self.transformer_list)

            if t in ("drop", "passthrough"):
                continue
            ):
                    "All estimators should implement fit and "
                    "transform. '%s' (type %s) doesn't" % (t, type(t))
                )

    def _validate_transformer_weights(self):
        if not self.transformer_weights:
            return

        transformer_names = set(name for name, _ in self.transformer_list)
        for name in self.transformer_weights:
            if name not in transformer_names:
                    f'Attempting to weight transformer "{name}", '
                    "but it is not present in transformer_list."
                )

    def _iter(self):
        """
        Generate (name, trans, weight) tuples excluding None and
        'drop' transformers.
        """

        get_weight = (self.transformer_weights or {}).get

        for name, trans in self.transformer_list:
            if trans == "drop":
                continue
            if trans == "passthrough":
                trans = FunctionTransformer(feature_names_out="one-to-one")
            yield (name, trans, get_weight(name))

        Returns
        -------
        """
        # List of tuples (name, feature_names_out)
        transformer_with_feature_names_out = []
        for name, trans, _ in self._iter():
            if not hasattr(trans, "get_feature_names_out"):
                    "Transformer %s (type %s) does not provide get_feature_names_out."
                    % (str(name), type(trans).__name__)
                )
            feature_names_out = trans.get_feature_names_out(input_features)
            transformer_with_feature_names_out.append((name, feature_names_out))

        return self._add_prefix_for_feature_names_out(
            transformer_with_feature_names_out
        )

    def _add_prefix_for_feature_names_out(self, transformer_with_feature_names_out):
        """Add prefix for feature names out that includes the transformer names.

        transformer_with_feature_names_out : list of tuples of (str, array-like of str)
            The tuple consistent of the transformer's name and its feature names out.

        Returns
        -------
        feature_names_out : ndarray of shape (n_features,), dtype=str
        """
        if self.verbose_feature_names_out:
            # Prefix the feature names out with the transformers name
            names = list(
                chain.from_iterable(
                    (f"{name}__{i}" for i in feature_names_out)
                    for name, feature_names_out in transformer_with_feature_names_out
                )
            )
            return np.asarray(names, dtype=object)

        # verbose_feature_names_out is False
        # Check that names are all unique without a prefix
        feature_names_count = Counter(
            chain.from_iterable(s for _, s in transformer_with_feature_names_out)
        )
        top_6_overlap = [
            name for name, count in feature_names_count.most_common(6) if count > 1
        ]
        top_6_overlap.sort()
        if top_6_overlap:
            if len(top_6_overlap) == 6:
                # There are more than 5 overlapping names, we only show the 5
                # of the feature names
                names_repr = str(top_6_overlap[:5])[:-1] + ", ...]"
            else:
                names_repr = str(top_6_overlap)
                f"Output feature names: {names_repr} are not unique. Please set "
                "verbose_feature_names_out=True to add prefixes to feature names"
            )

        return np.concatenate(
            [name for _, name in transformer_with_feature_names_out],
        )

    def fit(self, X, y=None, **fit_params):
        """Fit all transformers using X.

        X : iterable or array-like, depending on transformers
            Input data, used to fit transformers.

        y : array-like of shape (n_samples, n_outputs), default=None
            Targets for supervised learning.

        **fit_params : dict, default=None
            - If `enable_metadata_routing=False` (default):
              Parameters directly passed to the `fit` methods of the
              sub-transformers.

            - If `enable_metadata_routing=True`:
              Parameters safely routed to the `fit` methods of the
              sub-transformers. See :ref:`Metadata Routing User Guide
              <metadata_routing>` for more details.

            .. versionchanged:: 1.5
                `**fit_params` can be routed via metadata routing API.

        Returns
        -------
        """
            routed_params = process_routing(self, "fit", **fit_params)
        else:
            # TODO(SLEP6): remove when metadata routing cannot be disabled.
            routed_params = Bunch()
            for name, _ in self.transformer_list:
                routed_params[name] = Bunch(fit={})
                routed_params[name].fit = fit_params

        transformers = self._parallel_func(X, y, _fit_one, routed_params)

        if not transformers:
            # All transformers are None

        self._update_transformer_list(transformers)

        """Fit all transformers, transform the data and concatenate results.

            Input data to be transformed.

        **params : dict, default=None

                `**params` can now be routed via metadata routing API.

        Returns
        -------
        X_t : array-like or sparse matrix of \
                shape (n_samples, sum_n_components)
            The `hstack` of results of transformers. `sum_n_components` is the
            sum of `n_components` (output dimension) over transformers.
        """
            routed_params = process_routing(self, "fit_transform", **params)
        else:
            for name, obj in self.transformer_list:
                if hasattr(obj, "fit_transform"):
                    routed_params[name] = Bunch(fit_transform={})
                    routed_params[name].fit_transform = params
                else:
                    routed_params[name] = Bunch(fit={})
                    routed_params[name] = Bunch(transform={})
                    routed_params[name].fit = params

        results = self._parallel_func(X, y, _fit_transform_one, routed_params)
        if not results:
            return np.zeros((X.shape[0], 0))

        Xs, transformers = zip(*results)

        return self._hstack(Xs)

    def _log_message(self, name, idx, total):
        return "(step %d of %d) Processing %s" % (idx, total, name)

    def _parallel_func(self, X, y, func, routed_params):
        """Runs func in parallel on X and y"""
        self.transformer_list = list(self.transformer_list)
        self._validate_transformers()
        self._validate_transformer_weights()
        transformers = list(self._iter())

        return Parallel(n_jobs=self.n_jobs)(
            delayed(func)(
                transformer,
                X,
                y,
                weight,
                message_clsname="FeatureUnion",
                message=self._log_message(name, idx, len(transformers)),
                params=routed_params[name],
            )
            for idx, (name, transformer, weight) in enumerate(transformers, 1)
        )

        """Transform X separately by each transformer, concatenate results.

            Parameters routed to the `transform` method of the sub-transformers via the
            metadata routing API. See :ref:`Metadata Routing User Guide
            <metadata_routing>` for more details.

            .. versionadded:: 1.5

        Returns
        -------
        X_t : array-like or sparse matrix of shape (n_samples, sum_n_components)
        """
        _raise_for_params(params, self, "transform")

        else:
                routed_params[name] = Bunch(transform={})

        Xs = Parallel(n_jobs=self.n_jobs)(
            delayed(_transform_one)(trans, X, None, weight, params=routed_params[name])
            for name, trans, weight in self._iter()
        )
        if not Xs:

    def _hstack(self, Xs):
        adapter = _get_container_adapter("transform", self)
        if adapter and all(adapter.is_supported_container(X) for X in Xs):
            return adapter.hstack(Xs)

        if any(sparse.issparse(f) for f in Xs):
            Xs = sparse.hstack(Xs).tocsr()
        else:
            Xs = np.hstack(Xs)
        return Xs

    def _update_transformer_list(self, transformers):
        transformers = iter(transformers)
        self.transformer_list[:] = [
            (name, old if old == "drop" else next(transformers))
            for name, old in self.transformer_list
        ]

    @property
        """Number of features seen during :term:`fit`."""

        # X is passed to all transformers so we just delegate to the first one
        return self.transformer_list[0][1].n_features_in_

    @property
        """Names of features seen during :term:`fit`."""
        # X is passed to all transformers -- delegate to the first one
        return self.transformer_list[0][1].feature_names_in_

        # Delegate whether feature union was fitted
        for _, transformer, _ in self._iter():
            check_is_fitted(transformer)

        return _VisualBlock("parallel", transformers, names=names)

    def __getitem__(self, name):
        """Return transformer with name."""
        if not isinstance(name, str):
            raise KeyError("Only string keys are supported")
        return self.named_transformers[name]

        Returns
        -------
        """

        for name, transformer in self.transformer_list:
            router.add(
                **{name: transformer},
                method_mapping=MethodMapping()
                .add(caller="fit", callee="fit")
                .add(caller="fit_transform", callee="fit_transform")
                .add(caller="fit_transform", callee="fit")
                .add(caller="fit_transform", callee="transform")
                .add(caller="transform", callee="transform"),
            )

        try:
                get_tags(trans).input_tags.sparse
                for name, trans in self.transformer_list
                if trans not in {"passthrough", "drop"}
            )
        except Exception:
            # If `transformer_list` does not comply with our API (list of tuples)
            # then it will fail. In this case, we assume that `sparse` is False
            # but the parameter validation will raise an error during `fit`.
            pass  # pragma: no cover

def make_union(
    *transformers, n_jobs=None, verbose=False, verbose_feature_names_out=True
):
    """Construct a :class:`FeatureUnion` from the given transformers.

    This is a shorthand for the :class:`FeatureUnion` constructor; it does not
    require, and does not permit, naming the transformers. Instead, they will
    be given names automatically based on their types. It also does not allow
    weighting.

    *transformers : list of estimators
        One or more estimators.

           `n_jobs` default changed from 1 to None.

        If True, the feature names generated by `get_feature_names_out` will
        include prefixes derived from the transformer names.

    Returns
    -------
    f : FeatureUnion
        A :class:`FeatureUnion` object for concatenating the results of multiple
        transformer objects.

    See Also
    --------
    FeatureUnion : Class for concatenating the results of multiple transformer
        objects.

    Examples
    --------
    >>> from sklearn.pipeline import make_union
    >>> make_union(PCA(), TruncatedSVD())
     FeatureUnion(transformer_list=[('pca', PCA()),
                                   ('truncatedsvd', TruncatedSVD())])
    """
    return FeatureUnion(
        _name_estimators(transformers),
        n_jobs=n_jobs,
        verbose_feature_names_out=verbose_feature_names_out,
    )

        Returns
        -------
        """

        Returns
        -------
        """

        `steps`.

        Returns
        -------
        """

                continue
            ):
                )

        if (
        ):
            )

        """

        """
            stop -= 1

        """
        """

        See
        """
            )
        try:
        return est

        try:

        else:
                **{
                }
            )
                    )

            resolved.

        Returns
        -------
        dict
        """
        if (
        ):

            key: value
            if key
            )
        }
                    )
                else:

        """

        ):
                    continue

            else:
            )

                X,
                y,
            )
        return X

            pipeline.

            details.

        Returns
        -------
        """
            )

                )

        return (
        )

            pipeline.

            details.

        Returns
        -------
        """

                return Xt
            )
                )
            else:
                )

            details.

        Returns
        -------
        """
            Xt = X

            )

            details.

        Returns
        -------
        """

            )

            details.

        Returns
        -------
        """
            Xt = X

            )

            them.

                details.

        Returns
        -------
        """

            Xt = X
                )
                Xt,
            )

        Returns
        -------
        """
            Xt = X

              pipeline.

            details.

        Returns
        -------
        """
            Xt = X

            )

        )

            them.

                details.

        Returns
        -------
        """

            Xt = X
            return Xt

            them.

                details.

        Returns
        -------
            space.
        """

                )
            return X

            them.

                details.

        Returns
        -------
        """
            Xt = X

            )

            Xt = X
            )

        try:
            )
            pass

        try:
            pass

        Returns
        -------
        """
                )

        """

                break

        try:

            "serial",
        )

        Returns
        -------
        """

                (
                )
            else:
                (
                )

            (
            )

        else:
            )
        (
        )

        self,
        *,
    ):

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

                continue
            ):
                )

            return

                )

        """
        """

                continue

        Returns
        -------
        """
                )

        )

        Returns
        -------
        """
                )
            )

        )
        ]
            else:
            )

        )

        Returns
        -------
        """
        else:

        Returns
        -------
        """
        else:
                else:

                X,
                y,
                weight,
            )
        )

        Returns
        -------
        """

        else:

        )
        if not Xs:

        else:
        return Xs

        ]

        Returns
        -------
        """

            )

        try:
            )

class KernelRidge(MultiOutputMixin, RegressorMixin, BaseEstimator):
    """Kernel ridge regression.

    Kernel ridge regression (KRR) combines ridge regression (linear least
    squares with l2-norm regularization) with the kernel trick. It thus
    learns a linear function in the space induced by the respective kernel and
    the data. For non-linear kernels, this corresponds to a non-linear
    function in the original space.

    The form of the model learned by KRR is identical to support vector
    regression (SVR). However, different loss functions are used: KRR uses
    squared error loss while support vector regression uses epsilon-insensitive
    loss, both combined with l2 regularization. In contrast to SVR, fitting a
    KRR model can be done in closed-form and is typically faster for
    medium-sized datasets. On the other hand, the learned model is non-sparse
    and thus slower than SVR, which learns a sparse model for epsilon > 0, at
    prediction-time.

    This estimator has built-in support for multi-variate regression
    (i.e., when y is a 2d-array of shape [n_samples, n_targets]).

    Read more in the :ref:`User Guide <kernel_ridge>`.

    alpha : float or array-like of shape (n_targets,), default=1.0
        Regularization strength; must be a positive float. Regularization
        improves the conditioning of the problem and reduces the variance of
        the estimates. Larger values specify stronger regularization.
        Alpha corresponds to ``1 / (2C)`` in other linear models such as
        :class:`~sklearn.linear_model.LogisticRegression` or
        :class:`~sklearn.svm.LinearSVC`. If an array is passed, penalties are
        assumed to be specific to the targets. Hence they must correspond in
        number. See :ref:`ridge_regression` for formula.

    kernel : str or callable, default="linear"
        Kernel mapping used internally. This parameter is directly passed to
        :class:`~sklearn.metrics.pairwise.pairwise_kernels`.
        If `kernel` is a string, it must be one of the metrics
        in `pairwise.PAIRWISE_KERNEL_FUNCTIONS` or "precomputed".
        If `kernel` is "precomputed", X is assumed to be a kernel matrix.
        Alternatively, if `kernel` is a callable function, it is called on
        each pair of instances (rows) and the resulting value recorded. The
        callable should take two rows from X as input and return the
        corresponding kernel value as a single number. This means that
        callables from :mod:`sklearn.metrics.pairwise` are not allowed, as
        they operate on matrices, not single samples. Use the string
        identifying the kernel instead.

    degree : float, default=3

    coef0 : float, default=1

    dual_coef_ : ndarray of shape (n_samples,) or (n_samples, n_targets)
        Representation of weight vector(s) in kernel space

    X_fit_ : {ndarray, sparse matrix} of shape (n_samples, n_features)
        Training data, which is also required for prediction. If
        kernel == "precomputed" this is instead the precomputed
        training matrix, of shape (n_samples, n_samples).

    See Also
    --------
    sklearn.gaussian_process.GaussianProcessRegressor : Gaussian
        Process regressor providing automatic kernel hyperparameters
        tuning and predictions uncertainty.
    sklearn.linear_model.Ridge : Linear ridge regression.
    sklearn.linear_model.RidgeCV : Ridge regression with built-in
        cross-validation.
    sklearn.svm.SVR : Support Vector Regression accepting a large variety
        of kernels.

    * Kevin P. Murphy
      "Machine Learning: A Probabilistic Perspective", The MIT Press
      chapter 14.4.3, pp. 492-493

    Examples
    --------
    >>> from sklearn.kernel_ridge import KernelRidge
    >>> n_samples, n_features = 10, 5
    >>> rng = np.random.RandomState(0)
    >>> y = rng.randn(n_samples)
    >>> X = rng.randn(n_samples, n_features)
    >>> krr = KernelRidge(alpha=1.0)
    >>> krr.fit(X, y)
    KernelRidge(alpha=1.0)
    """

        "alpha": [Interval(Real, 0, None, closed="left"), "array-like"],
            callable,
        ],
        "degree": [Interval(Real, 0, None, closed="left")],
    }

        self,
        alpha=1,
        *,
        kernel="linear",
        degree=3,
        coef0=1,
    ):
        self.alpha = alpha

    def _get_kernel(self, X, Y=None):
        if callable(self.kernel):
            params = self.kernel_params or {}
        else:
            params = {"gamma": self.gamma, "degree": self.degree, "coef0": self.coef0}
        return pairwise_kernels(X, Y, metric=self.kernel, filter_params=True, **params)

        tags.input_tags.pairwise = self.kernel == "precomputed"

        """Fit Kernel Ridge regression model.

            Training data. If kernel == "precomputed" this is instead
            a precomputed kernel matrix, of shape (n_samples, n_samples).

        y : array-like of shape (n_samples,) or (n_samples, n_targets)

        sample_weight : float or array-like of shape (n_samples,), default=None
            Individual weights for each sample, ignored if None is passed.

        Returns
        -------
        """
        # Convert data
        X, y = validate_data(
            self, X, y, accept_sparse=("csr", "csc"), multi_output=True, y_numeric=True
        )
        if sample_weight is not None and not isinstance(sample_weight, float):

        K = self._get_kernel(X)
        alpha = np.atleast_1d(self.alpha)

        ravel = False
        if len(y.shape) == 1:
            y = y.reshape(-1, 1)
            ravel = True

        copy = self.kernel == "precomputed"
        self.dual_coef_ = _solve_cholesky_kernel(K, y, alpha, sample_weight, copy)
        if ravel:
            self.dual_coef_ = self.dual_coef_.ravel()

        self.X_fit_ = X

        """Predict using the kernel ridge model.

            Samples. If kernel == "precomputed" this is instead a
            precomputed kernel matrix, shape = [n_samples,
            n_samples_fitted], where n_samples_fitted is the number of
            samples used in the fitting for this estimator.

        Returns
        -------
        C : ndarray of shape (n_samples,) or (n_samples, n_targets)
            Returns predicted values.
        """
        X = validate_data(self, X, accept_sparse=("csr", "csc"), reset=False)
        K = self._get_kernel(X, self.X_fit_)
        return np.dot(K, self.dual_coef_)

        self,
        alpha=1,
        *,
        degree=3,
        coef0=1,
    ):

        else:

        Returns
        -------
        """
        )

        if ravel:

        Returns
        -------
        """

def _fit_binary(estimator, X, y, fit_params, classes=None):
    """Fit a single binary estimator."""
    unique_y = np.unique(y)
    if len(unique_y) == 1:
        if classes is not None:
            if y[0] == -1:
                c = 0
            else:
                c = y[0]
                "Label %s is present in all training examples." % str(classes[c])
            )
        estimator = _ConstantPredictor().fit(X, unique_y)
    else:
        estimator = clone(estimator)
        estimator.fit(X, y, **fit_params)
    return estimator

def _partial_fit_binary(estimator, X, y, partial_fit_params):
    """Partially fit a single binary estimator."""
    estimator.partial_fit(X, y, classes=np.array((0, 1)), **partial_fit_params)

def _predict_binary(estimator, X):
    """Make predictions using a single binary estimator."""
    if is_regressor(estimator):
        return estimator.predict(X)
    try:
        score = np.ravel(estimator.decision_function(X))
    except (AttributeError, NotImplementedError):
        # probabilities of the positive class
        score = estimator.predict_proba(X)[:, 1]
    return score

def _threshold_for_binary_predict(estimator):
    """Threshold for predictions from binary estimator."""
    if hasattr(estimator, "decision_function") and is_classifier(estimator):
        return 0.0
    else:
        # predict_proba threshold
        return 0.5

class _ConstantPredictor(BaseEstimator):
    """Helper predictor to be used when only one class is present."""

    def fit(self, X, y):
        check_params = dict(
            ensure_all_finite=False, dtype=None, ensure_2d=False, accept_sparse=True
        )
        validate_data(
            self, X, y, reset=True, validate_separately=(check_params, check_params)
        )
        self.y_ = y

            self,
            X,
            ensure_all_finite=False,
            dtype=None,
            accept_sparse=True,
            ensure_2d=False,
        )

        return np.repeat(self.y_, _num_samples(X))

    def decision_function(self, X):
            self,
            X,
        )

            self,
            X,
        )
        y_ = self.y_.astype(np.float64)
        return np.repeat([np.hstack([1 - y_, y_])], _num_samples(X), axis=0)

def _estimators_has(attr):
    """Check if self.estimator or self.estimators_[0] has attr.

    If `self.estimators_[0]` has the attr, then its safe to assume that other
    estimators have it too. We raise the original `AttributeError` if `attr`
    does not exist. This function is used together with `available_if`.
    """

        if hasattr(self, "estimators_"):
            getattr(self.estimators_[0], attr)
        else:
            getattr(self.estimator, attr)

class OneVsRestClassifier(
    MultiOutputMixin,
    ClassifierMixin,
    MetaEstimatorMixin,
    BaseEstimator,
):
    """One-vs-the-rest (OvR) multiclass strategy.

    Also known as one-vs-all, this strategy consists in fitting one classifier
    per class. For each classifier, the class is fitted against all the other
    classes. In addition to its computational efficiency (only `n_classes`
    classifiers are needed), one advantage of this approach is its
    interpretability. Since each class is represented by one and one classifier
    only, it is possible to gain knowledge about the class by inspecting its
    corresponding classifier. This is the most commonly used strategy for
    multiclass classification and is a fair default choice.

    OneVsRestClassifier can also be used for multilabel classification. To use
    this feature, provide an indicator matrix for the target `y` when calling
    `.fit`. In other words, the target labels should be formatted as a 2D
    binary (0/1) matrix, where [i, j] == 1 indicates the presence of label j
    in sample i. This estimator uses the binary relevance method to perform
    multilabel classification, which involves training one binary classifier
    independently for each label.

    Read more in the :ref:`User Guide <ovr_classification>`.

    estimator : estimator object
        A regressor or a classifier that implements :term:`fit`.
        When a classifier is passed, :term:`decision_function` will be used
        in priority and it will fallback to :term:`predict_proba` if it is not
        available.
        When a regressor is passed, :term:`predict` is used.

        The number of jobs to use for the computation: the `n_classes`
        one-vs-rest problems are computed in parallel.

        .. versionchanged:: 0.20

    verbose : int, default=0
        The verbosity level, if non zero, progress messages are printed.
        Below 50, the output is sent to stderr. Otherwise, the output is sent
        to stdout. The frequency of the messages increases with the verbosity
        level, reporting all iterations at 10. See :class:`joblib.Parallel` for
        more details.

    estimators_ : list of `n_classes` estimators
        Estimators used for predictions.

    classes_ : array, shape = [`n_classes`]
        Class labels.

    n_classes_ : int
        Number of classes.

    label_binarizer_ : LabelBinarizer object
        Object used to transform multiclass labels to binary labels and
        vice-versa.

    multilabel_ : boolean
        Whether a OneVsRestClassifier is a multilabel classifier.

    See Also
    --------
    OneVsOneClassifier : One-vs-one multiclass strategy.
    OutputCodeClassifier : (Error-Correcting) Output-Code multiclass strategy.
    sklearn.multioutput.MultiOutputClassifier : Alternate way of extending an
        estimator for multilabel classification.
    sklearn.preprocessing.MultiLabelBinarizer : Transform iterable of iterables
        to binary indicator matrix.

    Examples
    --------
    >>> from sklearn.multiclass import OneVsRestClassifier
    >>> X = np.array([
    ...     [10, 10],
    ...     [8, 10],
    ...     [-5, 5.5],
    ...     [-5.4, 5.5],
    ...     [-20, -20],
    ...     [-15, -20]
    ... ])
    >>> y = np.array([0, 0, 1, 1, 2, 2])
    >>> clf = OneVsRestClassifier(SVC()).fit(X, y)
    >>> clf.predict([[-19, -20], [9, 9], [-5, 5]])
    array([2, 0, 1])
    """

    _parameter_constraints = {
        "estimator": [HasMethods(["fit"])],
        "verbose": ["verbose"],
    }

    def __init__(self, estimator, *, n_jobs=None, verbose=0):
        self.estimator = estimator

        # OneVsRestClassifier.estimator is not validated yet
    )
    def fit(self, X, y, **fit_params):
        """Fit underlying estimators.

            Data.

        y : {array-like, sparse matrix} of shape (n_samples,) or (n_samples, n_classes)
            Multi-class targets. An indicator matrix turns on multilabel
            classification.

            Parameters passed to the ``estimator.fit`` method of each
            sub-estimator.

                details.

        Returns
        -------
            Instance of fitted estimator.
        """
        _raise_for_params(fit_params, self, "fit")

        routed_params = process_routing(
            self,
            "fit",
            **fit_params,
        )
        # A sparse LabelBinarizer, with sparse_output=True, has been shown to
        # outperform or match a dense label binarizer in all cases and has also
        # resulted in less or equal memory consumption in the fit_ovr function
        # overall.
        self.label_binarizer_ = LabelBinarizer(sparse_output=True)
        Y = self.label_binarizer_.fit_transform(y)
        Y = Y.tocsc()
        self.classes_ = self.label_binarizer_.classes_
        columns = (col.toarray().ravel() for col in Y.T)
        # In cases where individual estimators are very fast to train setting
        # n_jobs > 1 in can results in slower performance due to the overhead
        # of spawning threads.  See joblib issue #112.
        self.estimators_ = Parallel(n_jobs=self.n_jobs, verbose=self.verbose)(
            delayed(_fit_binary)(
                self.estimator,
                X,
                column,
                fit_params=routed_params.estimator.fit,
                classes=[
                    "not %s" % self.label_binarizer_.classes_[i],
                    self.label_binarizer_.classes_[i],
                ],
            )
            for i, column in enumerate(columns)
        )

        if hasattr(self.estimators_[0], "n_features_in_"):
            self.n_features_in_ = self.estimators_[0].n_features_in_
        if hasattr(self.estimators_[0], "feature_names_in_"):
            self.feature_names_in_ = self.estimators_[0].feature_names_in_

    @available_if(_estimators_has("partial_fit"))
    )
    def partial_fit(self, X, y, classes=None, **partial_fit_params):
        """Partially fit underlying estimators.

        Should be used when memory is inefficient to train all data.
        Chunks of data can be passed in several iterations.

            Data.

        classes : array, shape (n_classes, )
            Classes across all calls to partial_fit.
            Can be obtained via `np.unique(y_all)`, where y_all is the
            target vector of the entire dataset.
            This argument is only required in the first call of partial_fit
            and can be omitted in the subsequent calls.

        **partial_fit_params : dict
            Parameters passed to the ``estimator.partial_fit`` method of each

                details.

        Returns
        -------
            Instance of partially fitted estimator.
        """
        _raise_for_params(partial_fit_params, self, "partial_fit")

            self,
            "partial_fit",
            **partial_fit_params,
        )

        if _check_partial_fit_first_call(self, classes):
            self.estimators_ = [clone(self.estimator) for _ in range(self.n_classes_)]

            # A sparse LabelBinarizer, with sparse_output=True, has been
            # shown to outperform or match a dense label binarizer in all
            # cases and has also resulted in less or equal memory consumption
            # in the fit_ovr function overall.
            self.label_binarizer_ = LabelBinarizer(sparse_output=True)
            self.label_binarizer_.fit(self.classes_)

        if len(np.setdiff1d(y, self.classes_)):
                (
                    "Mini-batch contains {0} while classes " + "must be subset of {1}"
                ).format(np.unique(y), self.classes_)
            )

        Y = self.label_binarizer_.transform(y)

        self.estimators_ = Parallel(n_jobs=self.n_jobs)(
            delayed(_partial_fit_binary)(
                estimator,
                X,
                column,
                partial_fit_params=routed_params.estimator.partial_fit,
            )
            for estimator, column in zip(self.estimators_, columns)
        )

        """Predict multi-class targets using underlying estimators.

            Data.

        Returns
        -------
            Predicted multi-class targets.
        """

        if self.label_binarizer_.y_type_ == "multiclass":
            maxima = np.empty(n_samples, dtype=float)
            maxima.fill(-np.inf)
            argmaxima = np.zeros(n_samples, dtype=int)
            for i, e in enumerate(self.estimators_):
                pred = _predict_binary(e, X)
                np.maximum(maxima, pred, out=maxima)
                argmaxima[maxima == pred] = i
            return self.classes_[argmaxima]
        else:
            thresh = _threshold_for_binary_predict(self.estimators_[0])
            indices = array.array("i")
            indptr = array.array("i", [0])
            for e in self.estimators_:
                indices.extend(np.where(_predict_binary(e, X) > thresh)[0])
                indptr.append(len(indices))
            data = np.ones(len(indices), dtype=int)
            indicator = sp.csc_matrix(
                (data, indices, indptr), shape=(n_samples, len(self.estimators_))
            )
            return self.label_binarizer_.inverse_transform(indicator)

    @available_if(_estimators_has("predict_proba"))
        """Probability estimates.

        The returned estimates for all classes are ordered by label of classes.

        Note that in the multilabel case, each sample can have any number of
        labels. This returns the marginal probability that the given sample has
        the label in question. For example, it is entirely consistent that two
        labels both have a 90% probability of applying to a given sample.

        In the single label multiclass case, the rows of the returned matrix
        sum to 1.

        Returns
        -------
        T : array-like of shape (n_samples, n_classes)
            Returns the probability of the sample for each class in the model,
            where classes are ordered as they are in `self.classes_`.
        """
        # Y[i, j] gives the probability that sample i has the label j.
        # In the multi-label case, these are not disjoint.
        Y = np.array([e.predict_proba(X)[:, 1] for e in self.estimators_]).T

        if len(self.estimators_) == 1:
            # Only one estimator, but we still want to return probabilities
            # for two classes.
            Y = np.concatenate(((1 - Y), Y), axis=1)

        if not self.multilabel_:
            # Then, probabilities should be normalized to 1.
            Y /= np.sum(Y, axis=1)[:, np.newaxis]
        return Y

    @available_if(_estimators_has("decision_function"))
        """Decision function for the OneVsRestClassifier.

        Return the distance of each sample from the decision boundary for each
        class. This can only be used with estimators which implement the
        `decision_function` method.

        Returns
        -------
        T : array-like of shape (n_samples, n_classes) or (n_samples,) for \
            binary classification.

            .. versionchanged:: 0.19
                output shape changed to ``(n_samples,)`` to conform to
                scikit-learn conventions for binary classification.
        """
            return self.estimators_[0].decision_function(X)
        return np.array(
            [est.decision_function(X).ravel() for est in self.estimators_]
        ).T

    @property
    def multilabel_(self):
        """Whether this is a multilabel classifier."""
        return self.label_binarizer_.y_type_.startswith("multilabel")

    @property
    def n_classes_(self):
        """Number of classes."""
        return len(self.classes_)

        """Indicate if wrapped estimator is using a precomputed Gram matrix"""
        tags.input_tags.pairwise = get_tags(self.estimator).input_tags.pairwise
        tags.input_tags.sparse = get_tags(self.estimator).input_tags.sparse

        .. versionadded:: 1.4

        Returns
        -------
        """

        router = (
            MetadataRouter(owner=self.__class__.__name__)
            .add_self_request(self)
            .add(
                estimator=self.estimator,
                .add(caller="partial_fit", callee="partial_fit"),
            )
        )

def _fit_ovo_binary(estimator, X, y, i, j, fit_params):
    """Fit a single binary estimator (one-vs-one)."""
    cond = np.logical_or(y == i, y == j)
    y = y[cond]
    y_binary = np.empty(y.shape, int)
    y_binary[y == i] = 0
    y_binary[y == j] = 1
    indcond = np.arange(_num_samples(X))[cond]

    fit_params_subset = _check_method_params(X, params=fit_params, indices=indcond)
    return (
        _fit_binary(
            estimator,
            _safe_split(estimator, X, None, indices=indcond)[0],
            y_binary,
            fit_params=fit_params_subset,
            classes=[i, j],
        ),
        indcond,
    )

def _partial_fit_ovo_binary(estimator, X, y, i, j, partial_fit_params):
    """Partially fit a single binary estimator(one-vs-one)."""

    y = y[cond]
    if len(y) != 0:
        y_binary = np.zeros_like(y)
        y_binary[y == j] = 1
        partial_fit_params_subset = _check_method_params(
            X, params=partial_fit_params, indices=cond
        )
        return _partial_fit_binary(
            estimator, X[cond], y_binary, partial_fit_params=partial_fit_params_subset
        )

class OneVsOneClassifier(MetaEstimatorMixin, ClassifierMixin, BaseEstimator):
    """One-vs-one multiclass strategy.

    This strategy consists in fitting one classifier per class pair.
    At prediction time, the class which received the most votes is selected.
    Since it requires to fit `n_classes * (n_classes - 1) / 2` classifiers,
    this method is usually slower than one-vs-the-rest, due to its
    O(n_classes^2) complexity. However, this method may be advantageous for
    algorithms such as kernel algorithms which don't scale well with
    `n_samples`. This is because each individual learning problem only involves
    a small subset of the data whereas, with one-vs-the-rest, the complete
    dataset is used `n_classes` times.

    Read more in the :ref:`User Guide <ovo_classification>`.

        The number of jobs to use for the computation: the `n_classes * (
        n_classes - 1) / 2` OVO problems are computed in parallel.

    estimators_ : list of ``n_classes * (n_classes - 1) / 2`` estimators

    classes_ : numpy array of shape [n_classes]
        Array containing labels.

    pairwise_indices_ : list, length = ``len(estimators_)``, or ``None``
        Indices of samples used when training the estimators.
        ``None`` when ``estimator``'s `pairwise` tag is False.

    See Also
    --------
    OneVsRestClassifier : One-vs-all multiclass strategy.

    Examples
    --------
    >>> from sklearn.multiclass import OneVsOneClassifier
    >>> from sklearn.svm import LinearSVC
    >>> X_train, X_test, y_train, y_test = train_test_split(
    ...     X, y, test_size=0.33, shuffle=True, random_state=0)
    >>> clf = OneVsOneClassifier(
    ...     LinearSVC(random_state=0)).fit(X_train, y_train)
    >>> clf.predict(X_test[:10])
    array([2, 1, 0, 2, 0, 2, 0, 1, 1, 1])
    """

    }

    def __init__(self, estimator, *, n_jobs=None):

        # OneVsOneClassifier.estimator is not validated yet
    )

            Data.

            Multi-class targets.

                details.

        Returns
        -------
            The fitted underlying estimator.
        """

            self,
            "fit",
        )

        # We need to validate the data because we do a safe_indexing later.
            self, X, y, accept_sparse=["csr", "csc"], ensure_all_finite=False
        )
        check_classification_targets(y)

        self.classes_ = np.unique(y)
        if len(self.classes_) == 1:
                "OneVsOneClassifier can not be fit when only one class is present."
            )
        n_classes = self.classes_.shape[0]
        estimators_indices = list(
            zip(
                *(
                    Parallel(n_jobs=self.n_jobs)(
                        delayed(_fit_ovo_binary)(
                            self.estimator,
                            X,
                            y,
                            self.classes_[i],
                            self.classes_[j],
                            fit_params=routed_params.estimator.fit,
                        )
                        for i in range(n_classes)
                        for j in range(i + 1, n_classes)
                    )
                )
            )
        )

        self.estimators_ = estimators_indices[0]

        pairwise = self.__sklearn_tags__().input_tags.pairwise
        self.pairwise_indices_ = estimators_indices[1] if pairwise else None

    )

        Should be used when memory is inefficient to train all data. Chunks
        of data can be passed in several iteration, where the first call
        should have an array of all target variables.

        X : {array-like, sparse matrix) of shape (n_samples, n_features)
            Data.

                details.

        Returns
        -------
            The partially fitted underlying estimator.
        """

            self,
        )

        first_call = _check_partial_fit_first_call(self, classes)
        if first_call:
            self.estimators_ = [
                clone(self.estimator)
                for _ in range(self.n_classes_ * (self.n_classes_ - 1) // 2)
            ]

                "Mini-batch contains {0} while it must be subset of {1}".format(
                    np.unique(y), self.classes_
                )
            )

            self,
            X,
            y,
            reset=first_call,
        )
        combinations = itertools.combinations(range(self.n_classes_), 2)
            delayed(_partial_fit_ovo_binary)(
                X,
                y,
                self.classes_[i],
                self.classes_[j],
            )
            for estimator, (i, j) in zip(self.estimators_, (combinations))
        )

        self.pairwise_indices_ = None

        """Estimate the best class label for each sample in X.

        This is implemented as ``argmax(decision_function(X), axis=1)`` which
        will return the label of the class with most votes by estimators
        predicting the outcome of a decision for each possible class pair.

            Data.

        Returns
        -------
        y : numpy array of shape [n_samples]
        """
        Y = self.decision_function(X)
        if self.n_classes_ == 2:
            return self.classes_[(Y > thresh).astype(int)]
        return self.classes_[Y.argmax(axis=1)]

        """Decision function for the OneVsOneClassifier.

        The decision values for the samples are computed by adding the
        normalized sum of pair-wise classification confidence levels to the
        votes in order to disambiguate between the decision values when the
        votes for all the classes are equal leading to a tie.

        Returns
        -------
        Y : array-like of shape (n_samples, n_classes) or (n_samples,)

        """
            self,
            X,
        )

        indices = self.pairwise_indices_
        if indices is None:
            Xs = [X] * len(self.estimators_)
        else:
            Xs = [X[:, idx] for idx in indices]

        predictions = np.vstack(
            [est.predict(Xi) for est, Xi in zip(self.estimators_, Xs)]
        ).T
        confidences = np.vstack(
            [_predict_binary(est, Xi) for est, Xi in zip(self.estimators_, Xs)]
        ).T
        Y = _ovr_decision_function(predictions, confidences, len(self.classes_))
            return Y[:, 1]
        return Y

    @property

        Returns
        -------
        """

        router = (
            .add(
            )
        )

class OutputCodeClassifier(MetaEstimatorMixin, ClassifierMixin, BaseEstimator):
    """(Error-Correcting) Output-Code multiclass strategy.

    Output-code based strategies consist in representing each class with a
    binary code (an array of 0s and 1s). At fitting time, one binary
    classifier per bit in the code book is fitted.  At prediction time, the
    classifiers are used to project new points in the class space and the class
    closest to the points is chosen. The main advantage of these strategies is
    that the number of classifiers used can be controlled by the user, either
    for compressing the model (0 < `code_size` < 1) or for making the model more
    robust to errors (`code_size` > 1). See the documentation for more details.

    Read more in the :ref:`User Guide <ecoc>`.

        An estimator object implementing :term:`fit` and one of
        :term:`decision_function` or :term:`predict_proba`.

    code_size : float, default=1.5
        Percentage of the number of classes to be used to create the code book.
        A number between 0 and 1 will require fewer classifiers than
        one-vs-the-rest. A number greater than 1 will require more classifiers
        than one-vs-the-rest.

        The generator used to initialize the codebook.

        The number of jobs to use for the computation: the multiclass problems
        are computed in parallel.

    estimators_ : list of `int(n_classes * code_size)` estimators

    code_book_ : ndarray of shape (n_classes, `len(estimators_)`)
        Binary array containing the code of each class.

    See Also
    --------

    .. [1] "Solving multiclass learning problems via error-correcting output
       codes",
       Dietterich T., Bakiri G.,
       Journal of Artificial Intelligence Research 2,
       1995.

    .. [2] "The error coding method and PICTs",
       James G., Hastie T.,
       Journal of Computational and Graphical statistics 7,
       1998.

    .. [3] "The Elements of Statistical Learning",
       Hastie T., Tibshirani R., Friedman J., page 606 (second-edition)
       2008.

    Examples
    --------
    >>> from sklearn.multiclass import OutputCodeClassifier
    >>> from sklearn.ensemble import RandomForestClassifier
    >>> X, y = make_classification(n_samples=100, n_features=4,
    ...                            n_informative=2, n_redundant=0,
    ...                            random_state=0, shuffle=False)
    >>> clf = OutputCodeClassifier(
    ...     estimator=RandomForestClassifier(random_state=0),
    ...     random_state=0).fit(X, y)
    >>> clf.predict([[0, 0, 0, 0]])
    array([1])
    """

        "estimator": [
            HasMethods(["fit", "decision_function"]),
            HasMethods(["fit", "predict_proba"]),
        ],
        "code_size": [Interval(Real, 0.0, None, closed="neither")],
    }

    def __init__(self, estimator, *, code_size=1.5, random_state=None, n_jobs=None):
        self.code_size = code_size

        # OutputCodeClassifier.estimator is not validated yet
    )

            Data.

                details.

        Returns
        -------
            Returns a fitted instance of self.
        """

            self,
            "fit",
        )

        y = validate_data(self, X="no_validation", y=y)

        if n_classes == 0:
                "OutputCodeClassifier can not be fit when no class is present."
            )
        n_estimators = int(n_classes * self.code_size)

        # FIXME: there are more elaborate methods than generating the codebook
        # randomly.
        self.code_book_ = random_state.uniform(size=(n_classes, n_estimators))
        self.code_book_[self.code_book_ > 0.5] = 1.0

        if hasattr(self.estimator, "decision_function"):
            self.code_book_[self.code_book_ != 1] = -1.0
        else:
            self.code_book_[self.code_book_ != 1] = 0.0

        classes_index = {c: i for i, c in enumerate(self.classes_)}

        Y = np.array(
            [self.code_book_[classes_index[y[i]]] for i in range(_num_samples(y))],
            dtype=int,
        )

                self.estimator, X, Y[:, i], fit_params=routed_params.estimator.fit
            )
            for i in range(Y.shape[1])
        )

            Data.

        Returns
        -------
        """
        # ArgKmin only accepts C-contiguous array. The aggregated predictions need to be
        # transposed. We therefore create a F-contiguous array to avoid a copy and have
        # a C-contiguous array after the transpose operation.
            [_predict_binary(e, X) for e in self.estimators_],
            order="F",
            dtype=np.float64,
        ).T
        pred = pairwise_distances_argmin(Y, self.code_book_, metric="euclidean")
        return self.classes_[pred]

        Returns
        -------
        """

        router = MetadataRouter(owner=self.__class__.__name__).add(
            estimator=self.estimator,
            method_mapping=MethodMapping().add(caller="fit", callee="fit"),
        )

        )
        )
        self.y_ = y

            self,
            X,
        )

            self,
            X,
        )

            self,
            X,
        )

        else:

            Data.

                details.

        Returns
        -------
        """

            self,
            "fit",
        )
        # overall.
                X,
                column,
                classes=[
                ],
            )
        )

            Data.

                details.

        Returns
        -------
        """

            self,
        )

                (
            )

                X,
                column,
            )
        )

            Data.

        Returns
        -------
        """

        else:
            )

        sum to 1.

        Returns
        -------
        """

        return Y

        Returns
        -------

        """
        ).T

        Returns
        -------
        """

        router = (
            .add(
            )
        )

            Data.

                details.

        Returns
        -------
        """

            self,
            "fit",
        )

        )

            )
            zip(
                *(
                            X,
                            y,
                        )
                    )
                )
            )
        )

            Data.

                details.

        Returns
        -------
        """

            self,
        )

            ]

                )
            )

            self,
            X,
            y,
        )
                X,
                y,
            )
        )

            Data.

        Returns
        -------
        """

        Returns
        -------

        """
            self,
            X,
        )

        else:

        ).T
        ).T
        return Y

            Data.

                details.

        Returns
        -------
        """

            self,
            "fit",
        )

            )

        else:

        )

            )
        )

            Data.

        Returns
        -------
        """
        ).T

        Returns
        -------
        """

        )

class _BaseNB(ClassifierMixin, BaseEstimator, metaclass=ABCMeta):
    """Abstract base class for naive Bayes estimators"""

    def _joint_log_likelihood(self, X):
        """Compute the unnormalized posterior log probability of X

        I.e. ``log P(c) + log P(x|c)`` for all rows x of X, as an array-like of
        shape (n_samples, n_classes).

        Public methods predict, predict_proba, predict_log_proba, and
        predict_joint_log_proba pass the input through _check_X before handing it
        over to _joint_log_likelihood. The term "joint log likelihood" is used
        interchangibly with "joint log probability".
        """

    def _check_X(self, X):
        """To be overridden in subclasses with the actual checks.

        Only used in predict* methods.
        """

    def predict_joint_log_proba(self, X):
        """Return joint log probability estimates for the test vector X.

        For each row x of X and class y, the joint log probability is given by
        ``log P(x, y) = log P(y) + log P(x|y),``
        where ``log P(y)`` is the class prior probability and ``log P(x|y)`` is
        the class-conditional probability.

        Returns
        -------
        C : ndarray of shape (n_samples, n_classes)
            Returns the joint log-probability of the samples for each class in
            the model. The columns correspond to the classes in sorted
            order, as they appear in the attribute :term:`classes_`.
        """
        X = self._check_X(X)
        return self._joint_log_likelihood(X)

        """
        Perform classification on an array of test vectors X.

        Returns
        -------
        C : ndarray of shape (n_samples,)
        """
        jll = self._joint_log_likelihood(X)
        return self.classes_[np.argmax(jll, axis=1)]

        """
        Return log-probability estimates for the test vector X.

        Returns
        -------
        C : array-like of shape (n_samples, n_classes)
            Returns the log-probability of the samples for each class in
        """
        # normalize by P(x) = P(f_1, ..., f_n)
        log_prob_x = logsumexp(jll, axis=1)
        return jll - np.atleast_2d(log_prob_x).T

        """
        Return probability estimates for the test vector X.

        Returns
        -------
            Returns the probability of the samples for each class in
        """
        return np.exp(self.predict_log_proba(X))

class GaussianNB(_BaseNB):
    """
    Gaussian Naive Bayes (GaussianNB).

    Can perform online updates to model parameters via :meth:`partial_fit`.
    For details on algorithm used to update feature means and variance online,
    see `Stanford CS tech report STAN-CS-79-773 by Chan, Golub, and LeVeque
    <http://i.stanford.edu/pub/cstr/reports/cs/tr/79/773/CS-TR-79-773.pdf>`_.

    Read more in the :ref:`User Guide <gaussian_naive_bayes>`.

    priors : array-like of shape (n_classes,), default=None
        Prior probabilities of the classes. If specified, the priors are not
        adjusted according to the data.

    var_smoothing : float, default=1e-9
        Portion of the largest variance of all features that is added to
        variances for calculation stability.

        .. versionadded:: 0.20

    class_count_ : ndarray of shape (n_classes,)
        number of training samples observed in each class.

    class_prior_ : ndarray of shape (n_classes,)
        probability of each class.

        class labels known to the classifier.

    epsilon_ : float
        absolute additive value to variances.

    var_ : ndarray of shape (n_classes, n_features)
        Variance of each feature per class.

    theta_ : ndarray of shape (n_classes, n_features)
        mean of each feature per class.

    See Also
    --------
    BernoulliNB : Naive Bayes classifier for multivariate Bernoulli models.
    CategoricalNB : Naive Bayes classifier for categorical features.
    ComplementNB : Complement Naive Bayes classifier.
    MultinomialNB : Naive Bayes classifier for multinomial models.

    Examples
    --------
    >>> X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
    >>> Y = np.array([1, 1, 1, 2, 2, 2])
    >>> clf = GaussianNB()
    >>> clf.fit(X, Y)
    GaussianNB()
    >>> print(clf.predict([[-0.8, -1]]))
    [1]
    >>> clf_pf = GaussianNB()
    >>> clf_pf.partial_fit(X, Y, np.unique(Y))
    >>> print(clf_pf.predict([[-0.8, -1]]))
    [1]
    """

        "priors": ["array-like", None],
        "var_smoothing": [Interval(Real, 0, None, closed="left")],
    }

    def __init__(self, *, priors=None, var_smoothing=1e-9):
        self.priors = priors
        self.var_smoothing = var_smoothing

        """Fit Gaussian Naive Bayes according to X, y.

            Training vectors, where `n_samples` is the number of samples

            Weights applied to individual samples (1. for unweighted).

            .. versionadded:: 0.17
               Gaussian Naive Bayes supports fitting with *sample_weight*.

        Returns
        -------
        """
        y = validate_data(self, y=y)
        return self._partial_fit(
            X, y, np.unique(y), _refit=True, sample_weight=sample_weight
        )

        """Validate X, used only in predict* methods."""
        return validate_data(self, X, reset=False)

    def _update_mean_variance(n_past, mu, var, X, sample_weight=None):
        """Compute online update of Gaussian mean and variance.

        Given starting sample count, mean, and variance, a new set of
        points X, and optionally sample weights, return the updated mean and
        variance. (NB - each dimension (column) in X is treated as independent
        -- you get variance, not covariance).

        Can take scalar mean and variance, or vector mean and variance to
        simultaneously update a number of independent Gaussians.

        See Stanford CS tech report STAN-CS-79-773 by Chan, Golub, and LeVeque:

        http://i.stanford.edu/pub/cstr/reports/cs/tr/79/773/CS-TR-79-773.pdf

        n_past : int
            Number of samples represented in old mean and variance. If sample
            weights were given, this should contain the sum of sample
            weights represented in old mean and variance.

        mu : array-like of shape (number of Gaussians,)
            Means for Gaussians in original set.

        var : array-like of shape (number of Gaussians,)
            Variances for Gaussians in original set.

        Returns
        -------
        total_mu : array-like of shape (number of Gaussians,)
            Updated mean for each Gaussian over the combined set.

        total_var : array-like of shape (number of Gaussians,)
            Updated variance for each Gaussian over the combined set.
        """
        if X.shape[0] == 0:
            return mu, var

        # Compute (potentially weighted) mean and variance of new datapoints
            n_new = float(sample_weight.sum())
            if np.isclose(n_new, 0.0):
                return mu, var
            new_mu = np.average(X, axis=0, weights=sample_weight)
            new_var = np.average((X - new_mu) ** 2, axis=0, weights=sample_weight)
        else:
            n_new = X.shape[0]
            new_var = np.var(X, axis=0)
            new_mu = np.mean(X, axis=0)

        if n_past == 0:
            return new_mu, new_var

        n_total = float(n_past + n_new)

        # Combine mean of old and new data, taking into consideration
        # (weighted) number of observations
        total_mu = (n_new * new_mu + n_past * mu) / n_total

        # Combine variance of old and new data, taking into consideration
        # (weighted) number of observations. This is achieved by combining
        # the sum-of-squared-differences (ssd)
        old_ssd = n_past * var
        new_ssd = n_new * new_var
        total_ssd = old_ssd + new_ssd + (n_new * n_past / n_total) * (mu - new_mu) ** 2
        total_var = total_ssd / n_total

        return total_mu, total_var

    def partial_fit(self, X, y, classes=None, sample_weight=None):
        """Incremental fit on a batch of samples.

        This method is expected to be called several times consecutively
        on different chunks of a dataset so as to implement out-of-core
        or online learning.

        This is especially useful when the whole dataset is too big to fit in
        memory at once.

        This method has some performance and numerical stability overhead,
        hence it is better to call partial_fit on chunks of data that are
        as large as possible (as long as fitting in the memory budget) to
        hide the overhead.

            Training vectors, where `n_samples` is the number of samples and
            `n_features` is the number of features.

        classes : array-like of shape (n_classes,), default=None
            List of all the classes that can possibly appear in the y vector.

            Must be provided at the first call to partial_fit, can be omitted
            in subsequent calls.

        Returns
        -------
        """
            X, y, classes, _refit=False, sample_weight=sample_weight
        )

    def _partial_fit(self, X, y, classes=None, _refit=False, sample_weight=None):
        """Actual implementation of Gaussian NB fitting.

        _refit : bool, default=False
            If true, act as though this were the first time we called
            _partial_fit (ie, throw away any past fitting and start over).

        Returns
        -------
        """
        if _refit:
            self.classes_ = None

        X, y = validate_data(self, X, y, reset=first_call)

        # If the ratio of data variance between dimensions is too small, it
        # will cause numerical errors. To address this, we artificially
        # boost the variance by epsilon, a small fraction of the standard
        # deviation of the largest dimension.
        self.epsilon_ = self.var_smoothing * np.var(X, axis=0).max()

            # This is the first call to partial_fit:
            # initialize various cumulative counters
            n_features = X.shape[1]
            n_classes = len(self.classes_)
            self.theta_ = np.zeros((n_classes, n_features))
            self.var_ = np.zeros((n_classes, n_features))

            self.class_count_ = np.zeros(n_classes, dtype=np.float64)

            # Initialise the class prior
            # Take into account the priors
            if self.priors is not None:
                priors = np.asarray(self.priors)
                # Check that the provided prior matches the number of classes
                if len(priors) != n_classes:
                    raise ValueError("Number of priors must match number of classes.")
                # Check that the sum is 1
                if not np.isclose(priors.sum(), 1.0):
                    raise ValueError("The sum of the priors should be 1.")
                # Check that the priors are non-negative
                if (priors < 0).any():
                    raise ValueError("Priors must be non-negative.")
                self.class_prior_ = priors
            else:
                # Initialize the priors to zeros for each class
                self.class_prior_ = np.zeros(len(self.classes_), dtype=np.float64)
        else:
            if X.shape[1] != self.theta_.shape[1]:
                msg = "Number of features %d does not match previous data %d."
                raise ValueError(msg % (X.shape[1], self.theta_.shape[1]))
            # Put epsilon back in each time
            self.var_[:, :] -= self.epsilon_

        classes = self.classes_

        unique_y = np.unique(y)
        unique_y_in_classes = np.isin(unique_y, classes)

        if not np.all(unique_y_in_classes):
                "The target label(s) %s in y do not exist in the initial classes %s"
                % (unique_y[~unique_y_in_classes], classes)
            )

        for y_i in unique_y:
            i = classes.searchsorted(y_i)
            X_i = X[y == y_i, :]

            if sample_weight is not None:
                sw_i = sample_weight[y == y_i]
                N_i = sw_i.sum()
            else:
                sw_i = None
                N_i = X_i.shape[0]

            new_theta, new_sigma = self._update_mean_variance(
                self.class_count_[i], self.theta_[i, :], self.var_[i, :], X_i, sw_i
            )

            self.theta_[i, :] = new_theta
            self.var_[i, :] = new_sigma
            self.class_count_[i] += N_i

        self.var_[:, :] += self.epsilon_

        # Update if only no priors is provided
        if self.priors is None:
            # Empirical prior, with sample_weight taken into account
            self.class_prior_ = self.class_count_ / self.class_count_.sum()

        joint_log_likelihood = []
        for i in range(np.size(self.classes_)):
            jointi = np.log(self.class_prior_[i])
            n_ij = -0.5 * np.sum(np.log(2.0 * np.pi * self.var_[i, :]))
            n_ij -= 0.5 * np.sum(((X - self.theta_[i, :]) ** 2) / (self.var_[i, :]), 1)
            joint_log_likelihood.append(jointi + n_ij)

        joint_log_likelihood = np.array(joint_log_likelihood).T
        return joint_log_likelihood

class _BaseDiscreteNB(_BaseNB):
    """Abstract base class for naive Bayes on discrete/categorical data

    Any estimator based on this class should provide:

    __init__
    _joint_log_likelihood(X) as per _BaseNB
    _update_feature_log_prob(alpha)
    _count(X, Y)
    """

        "fit_prior": ["boolean"],
        "class_prior": ["array-like", None],
        "force_alpha": ["boolean"],
    }

    def __init__(self, alpha=1.0, fit_prior=True, class_prior=None, force_alpha=True):
        self.fit_prior = fit_prior
        self.class_prior = class_prior
        self.force_alpha = force_alpha

    def _count(self, X, Y):
        """Update counts that are used to calculate probabilities.

        The counts make up a sufficient statistic extracted from the data.
        Accordingly, this method is called each time `fit` or `partial_fit`
        update the model. `class_count_` and `feature_count_` must be updated
        here along with any model specific counts.

        Y : ndarray of shape (n_samples, n_classes)
            Binarized class labels.
        """

    def _update_feature_log_prob(self, alpha):
        """Update feature log probabilities based on counts.

        This method is called each time `fit` or `partial_fit` update the
        model.

        alpha : float
            smoothing parameter. See :meth:`_check_alpha`.
        """

        return validate_data(self, X, accept_sparse="csr", reset=False)

    def _check_X_y(self, X, y, reset=True):
        """Validate X and y in fit methods."""
        return validate_data(self, X, y, accept_sparse="csr", reset=reset)

    def _update_class_log_prior(self, class_prior=None):
        """Update class log priors.

        The class log priors are based on `class_prior`, class count or the
        number of classes. This method is called each time `fit` or
        `partial_fit` update the model.
        """
        n_classes = len(self.classes_)
        if class_prior is not None:
            if len(class_prior) != n_classes:
                raise ValueError("Number of priors must match number of classes.")
            self.class_log_prior_ = np.log(class_prior)
        elif self.fit_prior:
            with warnings.catch_warnings():
                # silence the warning when count is 0 because class was not yet
                # observed
                warnings.simplefilter("ignore", RuntimeWarning)
                log_class_count = np.log(self.class_count_)

            # empirical prior, with sample_weight taken into account
            self.class_log_prior_ = log_class_count - np.log(self.class_count_.sum())
        else:
            self.class_log_prior_ = np.full(n_classes, -np.log(n_classes))

    def _check_alpha(self):
        alpha = (
            np.asarray(self.alpha) if not isinstance(self.alpha, Real) else self.alpha
        )
        alpha_min = np.min(alpha)
        if isinstance(alpha, np.ndarray):
            if not alpha.shape[0] == self.n_features_in_:
                    "When alpha is an array, it should contains `n_features`. "
                    f"Got {alpha.shape[0]} elements instead of {self.n_features_in_}."
                )
            # check that all alpha are positive
            if alpha_min < 0:
                raise ValueError("All values in alpha must be greater than 0.")
        alpha_lower_bound = 1e-10
        if alpha_min < alpha_lower_bound and not self.force_alpha:
                "alpha too small will result in numeric errors, setting alpha ="
                f" {alpha_lower_bound:.1e}. Use `force_alpha=True` to keep alpha"
                " unchanged."
            )
            return np.maximum(alpha, alpha_lower_bound)
        return alpha

        This method has some performance overhead hence it is better to call
        partial_fit on chunks of data that are as large as possible
        (as long as fitting in the memory budget) to hide the overhead.

        Returns
        -------
        """
        first_call = not hasattr(self, "classes_")

        X, y = self._check_X_y(X, y, reset=first_call)
        _, n_features = X.shape

            n_classes = len(classes)
            self._init_counters(n_classes, n_features)

        Y = label_binarize(y, classes=self.classes_)
        if Y.shape[1] == 1:
            if len(self.classes_) == 2:
                Y = np.concatenate((1 - Y, Y), axis=1)
            else:  # degenerate case: just one class
                Y = np.ones_like(Y)

        if X.shape[0] != Y.shape[0]:
            msg = "X.shape[0]=%d and y.shape[0]=%d are incompatible."
            raise ValueError(msg % (X.shape[0], y.shape[0]))

        # label_binarize() returns arrays with dtype=np.int64.
        # We convert it to np.float64 to support sample_weight consistently
        Y = Y.astype(np.float64, copy=False)
            sample_weight = np.atleast_2d(sample_weight)
            Y *= sample_weight.T

        class_prior = self.class_prior

        # Count raw events from data before updating the class log prior
        # and feature log probas
        self._count(X, Y)

        # XXX: OPTIM: we could introduce a public finalization method to
        # be called by the user explicitly just once after several consecutive
        # calls to partial_fit and prior any call to predict[_[log_]proba]
        # to avoid computing the smooth log probas at each call to partial fit
        alpha = self._check_alpha()
        self._update_feature_log_prob(alpha)
        self._update_class_log_prior(class_prior=class_prior)

        """Fit Naive Bayes classifier according to X, y.

        Returns
        -------
        """
        X, y = self._check_X_y(X, y)

        labelbin = LabelBinarizer()
        Y = labelbin.fit_transform(y)
        self.classes_ = labelbin.classes_

        # LabelBinarizer().fit_transform() returns arrays with dtype=np.int64.
        # We convert it to np.float64 to support sample_weight consistently;
        # this means we also don't have to cast X to floating point
            Y = Y.astype(np.float64, copy=False)

        n_classes = Y.shape[1]
        self._init_counters(n_classes, n_features)

    def _init_counters(self, n_classes, n_features):
        self.class_count_ = np.zeros(n_classes, dtype=np.float64)
        self.feature_count_ = np.zeros((n_classes, n_features), dtype=np.float64)

class MultinomialNB(_BaseDiscreteNB):
    """
    Naive Bayes classifier for multinomial models.

    The multinomial Naive Bayes classifier is suitable for classification with
    discrete features (e.g., word counts for text classification). The
    multinomial distribution normally requires integer feature counts. However,
    in practice, fractional counts such as tf-idf may also work.

    Read more in the :ref:`User Guide <multinomial_naive_bayes>`.

    alpha : float or array-like of shape (n_features,), default=1.0
        Additive (Laplace/Lidstone) smoothing parameter
        (set alpha=0 and force_alpha=True, for no smoothing).

    force_alpha : bool, default=True
        If False and alpha is less than 1e-10, it will set alpha to
        1e-10. If True, alpha will remain unchanged. This may cause
        numerical errors if alpha is too close to 0.

        .. versionchanged:: 1.4
           The default value of `force_alpha` changed to `True`.

    fit_prior : bool, default=True
        Whether to learn class prior probabilities or not.
        If false, a uniform prior will be used.

    class_prior : array-like of shape (n_classes,), default=None

        Number of samples encountered for each class during fitting. This
        value is weighted by the sample weight when provided.

    class_log_prior_ : ndarray of shape (n_classes,)
        Smoothed empirical log probability for each class.

        Class labels known to the classifier

    feature_count_ : ndarray of shape (n_classes, n_features)
        Number of samples encountered for each (class, feature)
        during fitting. This value is weighted by the sample weight when
        provided.

    feature_log_prob_ : ndarray of shape (n_classes, n_features)
        Empirical log probability of features
        given a class, ``P(x_i|y)``.

    See Also
    --------
    GaussianNB : Gaussian Naive Bayes.

    C.D. Manning, P. Raghavan and H. Schuetze (2008). Introduction to
    Information Retrieval. Cambridge University Press, pp. 234-265.
    https://nlp.stanford.edu/IR-book/html/htmledition/naive-bayes-text-classification-1.html

    Examples
    --------
    >>> rng = np.random.RandomState(1)
    >>> X = rng.randint(5, size=(6, 100))
    >>> y = np.array([1, 2, 3, 4, 5, 6])
    >>> from sklearn.naive_bayes import MultinomialNB
    >>> clf = MultinomialNB()
    >>> clf.fit(X, y)
    MultinomialNB()
    >>> print(clf.predict(X[2:3]))
    [3]
    """

        self, *, alpha=1.0, force_alpha=True, fit_prior=True, class_prior=None
    ):
            alpha=alpha,
            fit_prior=fit_prior,
            class_prior=class_prior,
            force_alpha=force_alpha,
        )

        """Count and smooth feature occurrences."""
        check_non_negative(X, "MultinomialNB (input X)")
        self.feature_count_ += safe_sparse_dot(Y.T, X)
        self.class_count_ += Y.sum(axis=0)

        """Apply smoothing to raw counts and recompute log probabilities"""
        smoothed_fc = self.feature_count_ + alpha
        smoothed_cc = smoothed_fc.sum(axis=1)

        self.feature_log_prob_ = np.log(smoothed_fc) - np.log(
            smoothed_cc.reshape(-1, 1)
        )

        """Calculate the posterior log probability of the samples X"""
        return safe_sparse_dot(X, self.feature_log_prob_.T) + self.class_log_prior_

class ComplementNB(_BaseDiscreteNB):
    """The Complement Naive Bayes classifier described in Rennie et al. (2003).

    The Complement Naive Bayes classifier was designed to correct the "severe
    assumptions" made by the standard Multinomial Naive Bayes classifier. It is
    particularly suited for imbalanced data sets.

    Read more in the :ref:`User Guide <complement_naive_bayes>`.

    .. versionadded:: 0.20

        Only used in edge case with a single class in the training set.

        Prior probabilities of the classes. Not used.

    norm : bool, default=False
        Whether or not a second normalization of the weights is performed. The
        default behavior mirrors the implementations found in Mahout and Weka,
        which do not follow the full algorithm described in Table 9 of the
        paper.

        Smoothed empirical log probability for each class. Only used in edge
        case with a single class in the training set.

    feature_all_ : ndarray of shape (n_features,)
        Number of samples encountered for each feature during fitting. This

        Number of samples encountered for each (class, feature) during fitting.
        This value is weighted by the sample weight when provided.

        Empirical weights for class complements.

    See Also
    --------

    Rennie, J. D., Shih, L., Teevan, J., & Karger, D. R. (2003).
    Tackling the poor assumptions of naive bayes text classifiers. In ICML
    (Vol. 3, pp. 616-623).
    https://people.csail.mit.edu/jrennie/papers/icml03-nb.pdf

    Examples
    --------
    >>> from sklearn.naive_bayes import ComplementNB
    >>> clf = ComplementNB()
    ComplementNB()
    [3]
    """

        **_BaseDiscreteNB._parameter_constraints,
        "norm": ["boolean"],
    }

        self,
        *,
        alpha=1.0,
        force_alpha=True,
        fit_prior=True,
        class_prior=None,
        norm=False,
    ):
        )
        self.norm = norm

        """Count feature occurrences."""
        check_non_negative(X, "ComplementNB (input X)")
        self.feature_all_ = self.feature_count_.sum(axis=0)

        """Apply smoothing to raw counts and compute the weights."""
        comp_count = self.feature_all_ + alpha - self.feature_count_
        logged = np.log(comp_count / comp_count.sum(axis=1, keepdims=True))
        # _BaseNB.predict uses argmax, but ComplementNB operates with argmin.
        if self.norm:
            summed = logged.sum(axis=1, keepdims=True)
            feature_log_prob = logged / summed
        else:
            feature_log_prob = -logged
        self.feature_log_prob_ = feature_log_prob

        """Calculate the class scores for the samples in X."""
        jll = safe_sparse_dot(X, self.feature_log_prob_.T)
            jll += self.class_log_prior_
        return jll

class BernoulliNB(_BaseDiscreteNB):
    """Naive Bayes classifier for multivariate Bernoulli models.

    Like MultinomialNB, this classifier is suitable for discrete data. The
    difference is that while MultinomialNB works with occurrence counts,
    BernoulliNB is designed for binary/boolean features.

    Read more in the :ref:`User Guide <bernoulli_naive_bayes>`.

    binarize : float or None, default=0.0
        Threshold for binarizing (mapping to booleans) of sample features.
        If None, input is presumed to already consist of binary vectors.

        Log probability of each class (smoothed).

        provided.

        Empirical log probability of features given a class, P(x_i|y).

    See Also
    --------
    ComplementNB : The Complement Naive Bayes classifier
        described in Rennie et al. (2003).
    GaussianNB : Gaussian Naive Bayes (GaussianNB).

    https://nlp.stanford.edu/IR-book/html/htmledition/the-bernoulli-model-1.html

    A. McCallum and K. Nigam (1998). A comparison of event models for naive
    Bayes text classification. Proc. AAAI/ICML-98 Workshop on Learning for
    Text Categorization, pp. 41-48.

    V. Metsis, I. Androutsopoulos and G. Paliouras (2006). Spam filtering with
    naive Bayes -- Which naive Bayes? 3rd Conf. on Email and Anti-Spam (CEAS).

    Examples
    --------
    >>> Y = np.array([1, 2, 3, 4, 4, 5])
    >>> from sklearn.naive_bayes import BernoulliNB
    >>> clf = BernoulliNB()
    BernoulliNB()
    [3]
    """

        "binarize": [None, Interval(Real, 0, None, closed="left")],
    }

        self,
        *,
        binarize=0.0,
    ):
        )
        self.binarize = binarize

        X = super()._check_X(X)
        if self.binarize is not None:
            X = binarize(X, threshold=self.binarize)
        return X

        X, y = super()._check_X_y(X, y, reset=reset)
        return X, y

        smoothed_cc = self.class_count_ + alpha * 2

        )

        n_features = self.feature_log_prob_.shape[1]
        n_features_X = X.shape[1]

        if n_features_X != n_features:
                "Expected input with %d features, got %d instead"
                % (n_features, n_features_X)
            )

        neg_prob = np.log(1 - np.exp(self.feature_log_prob_))
        # Compute  neg_prob · (1 - X).T  as  ∑neg_prob - X · neg_prob
        jll = safe_sparse_dot(X, (self.feature_log_prob_ - neg_prob).T)
        jll += self.class_log_prior_ + neg_prob.sum(axis=1)

        return jll

class CategoricalNB(_BaseDiscreteNB):
    """Naive Bayes classifier for categorical features.

    The categorical Naive Bayes classifier is suitable for classification with
    discrete features that are categorically distributed. The categories of
    each feature are drawn from a categorical distribution.

    Read more in the :ref:`User Guide <categorical_naive_bayes>`.

    alpha : float, default=1.0

    min_categories : int or array-like of shape (n_features,), default=None
        Minimum number of categories per feature.

        - integer: Sets the minimum number of categories per feature to
          `n_categories` for each features.
        - array-like: shape (n_features,) where `n_categories[i]` holds the
          minimum number of categories for the ith column of the input.
        - None (default): Determines the number of categories automatically
          from the training data.

    category_count_ : list of arrays of shape (n_features,)
        Holds arrays of shape (n_classes, n_categories of respective feature)
        for each feature. Each array provides the number of samples
        encountered for each class and category of the specific feature.

    feature_log_prob_ : list of arrays of shape (n_features,)
        for each feature. Each array provides the empirical log probability
        of categories given the respective feature and class, ``P(x_i|y)``.

    n_categories_ : ndarray of shape (n_features,), dtype=np.int64
        Number of categories for each feature. This value is
        inferred from the data or set by the minimum number of categories.

    See Also
    --------

    Examples
    --------
    >>> from sklearn.naive_bayes import CategoricalNB
    >>> clf = CategoricalNB()
    CategoricalNB()
    [3]
    """

        "min_categories": [
            None,
        ],
        "alpha": [Interval(Real, 0, None, closed="left")],
    }

        self,
        *,
        min_categories=None,
    ):
        )
        self.min_categories = min_categories

            `n_features` is the number of features. Here, each feature of X is
            assumed to be from a different categorical distribution.
            It is further assumed that all categories of each feature are
            represented by the numbers 0, ..., n - 1, where n refers to the
            total number of categories for the given feature. This can, for
            instance, be achieved with the help of OrdinalEncoder.

        Returns
        -------
        """
        return super().fit(X, y, sample_weight=sample_weight)

        Returns
        -------
        """
        return super().partial_fit(X, y, classes, sample_weight=sample_weight)

        tags.input_tags.sparse = False

            self,
            X,
            dtype="int",
            accept_sparse=False,
            ensure_all_finite=True,
        )
        check_non_negative(X, "CategoricalNB (input X)")
        return X

            self,
            X,
            y,
            reset=reset,
        )
        return X, y

        self.category_count_ = [np.zeros((n_classes, 0)) for _ in range(n_features)]

    def _validate_n_categories(X, min_categories):
        # rely on max for n_categories categories are encoded between 0...n-1
        n_categories_X = X.max(axis=0) + 1
        min_categories_ = np.array(min_categories)
        if min_categories is not None:
            if not np.issubdtype(min_categories_.dtype, np.signedinteger):
                    "'min_categories' should have integral type. Got "
                    f"{min_categories_.dtype} instead."
                )
            n_categories_ = np.maximum(n_categories_X, min_categories_, dtype=np.int64)
            if n_categories_.shape != n_categories_X.shape:
                    f"'min_categories' should have shape ({X.shape[1]},"
                    ") when an array-like is provided. Got"
                    f" {min_categories_.shape} instead."
                )
            return n_categories_
        else:
            return n_categories_X

        def _update_cat_count_dims(cat_count, highest_feature):
            diff = highest_feature + 1 - cat_count.shape[1]
            if diff > 0:
                # we append a column full of zeros for each new category
                return np.pad(cat_count, [(0, 0), (0, diff)], "constant")
            return cat_count

        def _update_cat_count(X_feature, Y, cat_count, n_classes):
            for j in range(n_classes):
                mask = Y[:, j].astype(bool)
                if Y.dtype.type == np.int64:
                    weights = None
                else:
                    weights = Y[mask, j]
                counts = np.bincount(X_feature[mask], weights=weights)
                indices = np.nonzero(counts)[0]
                cat_count[j, indices] += counts[indices]

        self.n_categories_ = self._validate_n_categories(X, self.min_categories)
        for i in range(self.n_features_in_):
            X_feature = X[:, i]
            self.category_count_[i] = _update_cat_count_dims(
                self.category_count_[i], self.n_categories_[i] - 1
            )
            _update_cat_count(
                X_feature, Y, self.category_count_[i], self.class_count_.shape[0]
            )

        feature_log_prob = []
            smoothed_cat_count = self.category_count_[i] + alpha
            smoothed_class_count = smoothed_cat_count.sum(axis=1)
            feature_log_prob.append(
                np.log(smoothed_cat_count) - np.log(smoothed_class_count.reshape(-1, 1))
            )

        _check_n_features(self, X, reset=False)
        jll = np.zeros((X.shape[0], self.class_count_.shape[0]))
            indices = X[:, i]
            jll += self.feature_log_prob_[i][:, indices].T
        total_ll = jll + self.class_log_prior_
        return total_ll

        """

        """

        Returns
        -------
        """

        """

        Returns
        -------
        """

        """

        Returns
        -------
        """

        """

        Returns
        -------
        """

        Returns
        -------
        """
        )

        Returns
        -------

        """

        else:

        Returns
        -------
        """
        )

        Returns
        -------
        """
        if _refit:

            else:
        else:

            )

            else:
                sw_i = None

            )

        """

        model.

        """

        """
                # observed

        else:

        alpha = (
        )
                )
            )

        Returns
        -------
        """

        Returns
        -------
        """

    ):
        )

        )

        self,
        *,
    ):
        )

        else:

        return jll

        self,
        *,
    ):
        )

        return X

        return X, y

        )

            )

        return jll

        self,
        *,
    ):
        )

        Returns
        -------
        """

        Returns
        -------
        """

            self,
            X,
        )
        return X

            self,
            X,
            y,
        )
        return X, y

                )
                )
        else:

            if diff > 0:

                else:

            )
            )

            )

            if diff > 0:

                else:

def _get_threadlocal_config():
    """Get a threadlocal **mutable** configuration. If the configuration
    does not exist, copy the default global configuration."""
    if not hasattr(_threadlocal, "global_config"):
        _threadlocal.global_config = _global_config.copy()
    return _threadlocal.global_config

def get_config():
    """Retrieve current values for configuration set by :func:`set_config`.

    Returns
    -------
    config : dict
        Keys are parameter names that can be passed to :func:`set_config`.

    See Also
    --------
    config_context : Context manager for global scikit-learn configuration.
    set_config : Set global scikit-learn configuration.

    Examples
    --------
    >>> import sklearn
    >>> config = sklearn.get_config()
    >>> config.keys()
    dict_keys([...])
    """
    # Return a copy of the threadlocal configuration so that users will
    # not be able to modify the configuration with the returned dict.
    return _get_threadlocal_config().copy()

def set_config(
    assume_finite=None,
    working_memory=None,
    print_changed_only=None,
    display=None,
    pairwise_dist_chunk_size=None,
    enable_cython_pairwise_dist=None,
    array_api_dispatch=None,
    transform_output=None,
    enable_metadata_routing=None,
    skip_parameter_validation=None,
):
    """Set global scikit-learn configuration.

    .. versionadded:: 0.19

    assume_finite : bool, default=None
        If True, validation for finiteness will be skipped,
        saving time, but leading to potential crashes. If
        False, validation for finiteness will be performed,
        avoiding error.  Global default: False.

        .. versionadded:: 0.19

    working_memory : int, default=None
        If set, scikit-learn will attempt to limit the size of temporary arrays
        to this number of MiB (per job when parallelised), often saving both
        computation time and memory on expensive operations that can be
        performed in chunks. Global default: 1024.

    print_changed_only : bool, default=None
        If True, only the parameters that were set to non-default
        values will be printed when printing an estimator. For example,
        ``print(SVC())`` while True will only print 'SVC()' while the default
        behaviour would be to print 'SVC(C=1.0, cache_size=200, ...)' with
        all the non-changed parameters.

        .. versionadded:: 0.21

    display : {'text', 'diagram'}, default=None
        If 'diagram', estimators will be displayed as a diagram in a Jupyter
        lab or notebook context. If 'text', estimators will be displayed as
        text. Default is 'diagram'.

        .. versionadded:: 0.23

    pairwise_dist_chunk_size : int, default=None
        The number of row vectors per chunk for the accelerated pairwise-
        distances reduction backend. Default is 256 (suitable for most of
        modern laptops' caches and architectures).

        Intended for easier benchmarking and testing of scikit-learn internals.
        End users are not expected to benefit from customizing this configuration
        setting.

    enable_cython_pairwise_dist : bool, default=None
        Use the accelerated pairwise-distances reduction backend when
        possible. Global default: True.

        setting.

    array_api_dispatch : bool, default=None
        Use Array API dispatching when inputs follow the Array API standard.
        Default is False.

        See the :ref:`User Guide <array_api>` for more details.

    transform_output : str, default=None
        Configure output of `transform` and `fit_transform`.

        See :ref:`sphx_glr_auto_examples_miscellaneous_plot_set_output.py`
        for an example on how to use the API.

        - `"default"`: Default output format of a transformer
        - `"pandas"`: DataFrame output
        - `"polars"`: Polars output
        - `None`: Transform configuration is unchanged

            `"polars"` option was added.

    enable_metadata_routing : bool, default=None
        Enable metadata routing. By default this feature is disabled.

        Refer to :ref:`metadata routing user guide <metadata_routing>` for more
        details.

        - `True`: Metadata routing is enabled
        - `False`: Metadata routing is disabled, use the old syntax.
        - `None`: Configuration is unchanged

    skip_parameter_validation : bool, default=None
        If `True`, disable the validation of the hyper-parameters' types and values in
        the fit method of estimators and for arguments passed to public helper
        functions. It can save time in some situations but can lead to low level
        crashes and exceptions with confusing error messages.

        Note that for data parameters, such as `X` and `y`, only type validation is
        skipped but validation with `check_array` will continue to run.

    See Also
    --------
    get_config : Retrieve current values of the global configuration.

    Examples
    --------
    >>> from sklearn import set_config
    >>> set_config(display='diagram')  # doctest: +SKIP
    """
    local_config = _get_threadlocal_config()

    if assume_finite is not None:
        local_config["assume_finite"] = assume_finite
    if working_memory is not None:
        local_config["working_memory"] = working_memory
    if print_changed_only is not None:
        local_config["print_changed_only"] = print_changed_only
    if display is not None:
        local_config["display"] = display
    if pairwise_dist_chunk_size is not None:
        local_config["pairwise_dist_chunk_size"] = pairwise_dist_chunk_size
    if enable_cython_pairwise_dist is not None:
        local_config["enable_cython_pairwise_dist"] = enable_cython_pairwise_dist
    if array_api_dispatch is not None:
        from .utils._array_api import _check_array_api_dispatch

        _check_array_api_dispatch(array_api_dispatch)
        local_config["array_api_dispatch"] = array_api_dispatch
    if transform_output is not None:
        local_config["transform_output"] = transform_output
    if enable_metadata_routing is not None:
        local_config["enable_metadata_routing"] = enable_metadata_routing
    if skip_parameter_validation is not None:
        local_config["skip_parameter_validation"] = skip_parameter_validation

def config_context(
    *,
):
    """Context manager for global scikit-learn configuration.

        avoiding error. If None, the existing value won't change.
        The default value is False.

        performed in chunks. If None, the existing value won't change.
        The default value is 1024.

        ``print(SVC())`` while True will only print 'SVC()', but would print
        'SVC(C=1.0, cache_size=200, ...)' with all the non-changed parameters
        when False. If None, the existing value won't change.
        The default value is True.

        .. versionchanged:: 0.23
           Default changed from False to True.

        text. If None, the existing value won't change.
        The default value is 'diagram'.

        setting.

        setting.

        details.

    Yields
    ------
    None.

    See Also
    --------

    Notes
    -----
    All settings, not just those presently modified, will be returned to
    their previous values when the context manager is exited.

    Examples
    --------
    >>> from sklearn.utils.validation import assert_all_finite
    >>> with sklearn.config_context(assume_finite=True):
    ...     assert_all_finite([float('nan')])
    ...     with sklearn.config_context(assume_finite=False):
    ...         assert_all_finite([float('nan')])
    Traceback (most recent call last):
    ...
    ValueError: Input contains NaN...
    """
    old_config = get_config()
    set_config(
        assume_finite=assume_finite,
        working_memory=working_memory,
        print_changed_only=print_changed_only,
        display=display,
        pairwise_dist_chunk_size=pairwise_dist_chunk_size,
        enable_cython_pairwise_dist=enable_cython_pairwise_dist,
        array_api_dispatch=array_api_dispatch,
        transform_output=transform_output,
        enable_metadata_routing=enable_metadata_routing,
        skip_parameter_validation=skip_parameter_validation,
    )

    try:
        yield
    finally:
        set_config(**old_config)

def _cov(X, shrinkage=None, covariance_estimator=None):
    """Estimate covariance matrix (using optional covariance_estimator).
    X : array-like of shape (n_samples, n_features)
        Input data.

    shrinkage : {'empirical', 'auto'} or float, default=None
        Shrinkage parameter, possible values:
          - None or 'empirical': no shrinkage (default).
          - 'auto': automatic shrinkage using the Ledoit-Wolf lemma.
          - float between 0 and 1: fixed shrinkage parameter.

        Shrinkage parameter is ignored if  `covariance_estimator`
        is not None.

    covariance_estimator : estimator, default=None
        If not None, `covariance_estimator` is used to estimate
        the covariance matrices instead of relying on the empirical
        covariance estimator (with potential shrinkage).
        The object should have a fit method and a ``covariance_`` attribute
        like the estimators in :mod:`sklearn.covariance``.
        if None the shrinkage parameter drives the estimate.

    Returns
    -------
    s : ndarray of shape (n_features, n_features)
        Estimated covariance matrix.
    """
    if covariance_estimator is None:
        shrinkage = "empirical" if shrinkage is None else shrinkage
        if isinstance(shrinkage, str):
            if shrinkage == "auto":
                sc = StandardScaler()  # standardize features
                X = sc.fit_transform(X)
                s = ledoit_wolf(X)[0]
                # rescale
                s = sc.scale_[:, np.newaxis] * s * sc.scale_[np.newaxis, :]
            elif shrinkage == "empirical":
                s = empirical_covariance(X)
        elif isinstance(shrinkage, Real):
            s = shrunk_covariance(empirical_covariance(X), shrinkage)
    else:
        if shrinkage is not None and shrinkage != 0:
                "covariance_estimator and shrinkage parameters "
                "are not None. Only one of the two can be set."
            )
        covariance_estimator.fit(X)
        if not hasattr(covariance_estimator, "covariance_"):
                "%s does not have a covariance_ attribute"
                % covariance_estimator.__class__.__name__
            )
        s = covariance_estimator.covariance_
    return s

def _class_means(X, y):
    """Compute class means.

    y : array-like of shape (n_samples,) or (n_samples, n_targets)
        Target values.

    Returns
    -------
    means : array-like of shape (n_classes, n_features)
        Class means.
    """
    xp, is_array_api_compliant = get_namespace(X)
    classes, y = xp.unique_inverse(y)
    means = xp.zeros((classes.shape[0], X.shape[1]), device=device(X), dtype=X.dtype)

    if is_array_api_compliant:
        for i in range(classes.shape[0]):
            means[i, :] = xp.mean(X[y == i], axis=0)
    else:
        # TODO: Explore the choice of using bincount + add.at as it seems sub optimal
        # from a performance-wise
        cnt = np.bincount(y)
        np.add.at(means, y, X)
        means /= cnt[:, None]
    return means

def _class_cov(X, y, priors, shrinkage=None, covariance_estimator=None):
    """Compute weighted within-class covariance matrix.

    The per-class covariance are weighted by the class priors.

    priors : array-like of shape (n_classes,)
        Class priors.

    shrinkage : 'auto' or float, default=None
          - None: no shrinkage (default).

        Shrinkage parameter is ignored if `covariance_estimator` is not None.

        the covariance matrices instead of relying the empirical
        like the estimators in sklearn.covariance.
        If None, the shrinkage parameter drives the estimate.

    Returns
    -------
    cov : array-like of shape (n_features, n_features)
        Weighted within-class covariance matrix
    """
    classes = np.unique(y)
    cov = np.zeros(shape=(X.shape[1], X.shape[1]))
    for idx, group in enumerate(classes):
        Xg = X[y == group, :]
        cov += priors[idx] * np.atleast_2d(_cov(Xg, shrinkage, covariance_estimator))
    return cov

class DiscriminantAnalysisPredictionMixin:
    """Mixin class for QuadraticDiscriminantAnalysis and NearestCentroid."""

        """Apply decision function to an array of samples.

            Array of samples (test vectors).

        Returns
        -------
        y_scores : ndarray of shape (n_samples,) or (n_samples, n_classes)
            Decision function values related to each class, per sample.
            In the two-class case, the shape is `(n_samples,)`, giving the
            log likelihood ratio of the positive class.
        """
        y_scores = self._decision_function(X)
        if len(self.classes_) == 2:
            return y_scores[:, 1] - y_scores[:, 0]
        return y_scores

        """Perform classification on an array of vectors `X`.

        Returns the class label for each sample.

            Input vectors, where `n_samples` is the number of samples and

        Returns
        -------
            Class label for each sample.
        """
        scores = self._decision_function(X)
        return self.classes_.take(scores.argmax(axis=1))

        """Estimate class probabilities.

        Returns
        -------
            Probability estimate of the sample for each class in the
            model, where classes are ordered as they are in `self.classes_`.
        """

        """Estimate log class probabilities.

        Returns
        -------
            Estimated log probabilities.
        """
        log_likelihood = scores - scores.max(axis=1)[:, np.newaxis]
        return log_likelihood - np.log(
            np.exp(log_likelihood).sum(axis=1)[:, np.newaxis]
        )

class LinearDiscriminantAnalysis(
    ClassNamePrefixFeaturesOutMixin,
    LinearClassifierMixin,
    TransformerMixin,
):
    """Linear Discriminant Analysis.

    A classifier with a linear decision boundary, generated by fitting class
    conditional densities to the data and using Bayes' rule.

    The model fits a Gaussian density to each class, assuming that all classes
    share the same covariance matrix.

    The fitted model can also be used to reduce the dimensionality of the input
    by projecting it to the most discriminative directions, using the
    `transform` method.

    .. versionadded:: 0.17

    For a comparison between
    :class:`~sklearn.discriminant_analysis.LinearDiscriminantAnalysis`
    and :class:`~sklearn.discriminant_analysis.QuadraticDiscriminantAnalysis`, see
    :ref:`sphx_glr_auto_examples_classification_plot_lda_qda.py`.

    Read more in the :ref:`User Guide <lda_qda>`.

    solver : {'svd', 'lsqr', 'eigen'}, default='svd'
        Solver to use, possible values:
          - 'svd': Singular value decomposition (default).
            Does not compute the covariance matrix, therefore this solver is
            recommended for data with a large number of features.
          - 'lsqr': Least squares solution.
            Can be combined with shrinkage or custom covariance estimator.
          - 'eigen': Eigenvalue decomposition.

        .. versionchanged:: 1.2
            `solver="svd"` now has experimental Array API support. See the
            :ref:`Array API User Guide <array_api>` for more details.

        This should be left to None if `covariance_estimator` is used.
        Note that shrinkage works only with 'lsqr' and 'eigen' solvers.

        For a usage example, see
        :ref:`sphx_glr_auto_examples_classification_plot_lda.py`.

        The class prior probabilities. By default, the class proportions are
        inferred from the training data.

    n_components : int, default=None
        Number of components (<= min(n_classes - 1, n_features)) for
        dimensionality reduction. If None, will be set to
        min(n_classes - 1, n_features). This parameter only affects the
        `transform` method.

        :ref:`sphx_glr_auto_examples_decomposition_plot_pca_vs_lda.py`.

    store_covariance : bool, default=False
        If True, explicitly compute the weighted within-class covariance
        matrix when solver is 'svd'. The matrix is always computed
        and stored for the other solvers.

        .. versionadded:: 0.17

    tol : float, default=1.0e-4
        Absolute threshold for a singular value of X to be considered
        significant, used to estimate the rank of X. Dimensions whose
        singular values are non-significant are discarded. Only used if
        solver is 'svd'.

    covariance_estimator : covariance estimator, default=None
        like the estimators in :mod:`sklearn.covariance`.

        This should be left to None if `shrinkage` is used.
        Note that `covariance_estimator` works only with 'lsqr' and 'eigen'
        solvers.

    coef_ : ndarray of shape (n_features,) or (n_classes, n_features)
        Weight vector(s).

    intercept_ : ndarray of shape (n_classes,)
        Intercept term.

    covariance_ : array-like of shape (n_features, n_features)
        Weighted within-class covariance matrix. It corresponds to
        `sum_k prior_k * C_k` where `C_k` is the covariance matrix of the
        samples in class `k`. The `C_k` are estimated using the (potentially
        shrunk) biased estimator of covariance. If solver is 'svd', only
        exists when `store_covariance` is True.

    explained_variance_ratio_ : ndarray of shape (n_components,)
        Percentage of variance explained by each of the selected components.
        If ``n_components`` is not set then all components are stored and the
        sum of explained variances is equal to 1.0. Only available when eigen
        or svd solver is used.

    means_ : array-like of shape (n_classes, n_features)
        Class-wise means.

    priors_ : array-like of shape (n_classes,)
        Class priors (sum to 1).

    scalings_ : array-like of shape (rank, n_classes - 1)
        Scaling of the features in the space spanned by the class centroids.
        Only available for 'svd' and 'eigen' solvers.

    xbar_ : array-like of shape (n_features,)
        Overall mean. Only present if solver is 'svd'.

    classes_ : array-like of shape (n_classes,)
        Unique class labels.

    See Also
    --------
    QuadraticDiscriminantAnalysis : Quadratic Discriminant Analysis.

    Examples
    --------
    >>> from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
    >>> y = np.array([1, 1, 1, 2, 2, 2])
    >>> clf = LinearDiscriminantAnalysis()
    LinearDiscriminantAnalysis()
    [1]
    """

        "solver": [StrOptions({"svd", "lsqr", "eigen"})],
        "shrinkage": [StrOptions({"auto"}), Interval(Real, 0, 1, closed="both"), None],
        "n_components": [Interval(Integral, 1, None, closed="left"), None],
        "store_covariance": ["boolean"],
        "tol": [Interval(Real, 0, None, closed="left")],
        "covariance_estimator": [HasMethods("fit"), None],
    }

        self,
        solver="svd",
        shrinkage=None,
        priors=None,
        n_components=None,
        store_covariance=False,
        tol=1e-4,
        covariance_estimator=None,
    ):
        self.solver = solver
        self.shrinkage = shrinkage
        self.store_covariance = store_covariance  # used only in svd solver
        self.tol = tol  # used only in svd solver
        self.covariance_estimator = covariance_estimator

    def _solve_lstsq(self, X, y, shrinkage, covariance_estimator):
        """Least squares solver.

        The least squares solver computes a straightforward solution of the
        optimal decision rule based directly on the discriminant functions. It
        can only be used for classification (with any covariance estimator),
        because
        estimation of eigenvectors is not performed. Therefore, dimensionality
        reduction with the transform is not supported.

        y : array-like of shape (n_samples,) or (n_samples, n_classes)

        shrinkage : 'auto', float or None
            Shrinkage parameter, possible values:
              - None: no shrinkage.
              - 'auto': automatic shrinkage using the Ledoit-Wolf lemma.
              - float between 0 and 1: fixed shrinkage parameter.

            Shrinkage parameter is ignored if  `covariance_estimator` i
            not None

        covariance_estimator : estimator, default=None
            If not None, `covariance_estimator` is used to estimate
            the covariance matrices instead of relying the empirical
            covariance estimator (with potential shrinkage).
            The object should have a fit method and a ``covariance_`` attribute
            like the estimators in sklearn.covariance.
            if None the shrinkage parameter drives the estimate.

            .. versionadded:: 0.24

        Notes
        -----
        This solver is based on [1]_, section 2.6.2, pp. 39-41.

        References
        .. [1] R. O. Duda, P. E. Hart, D. G. Stork. Pattern Classification
           (Second Edition). John Wiley & Sons, Inc., New York, 2001. ISBN
           0-471-05669-3.
        """
        self.means_ = _class_means(X, y)
        self.covariance_ = _class_cov(
            X, y, self.priors_, shrinkage, covariance_estimator
        )
        self.coef_ = linalg.lstsq(self.covariance_, self.means_.T)[0].T
        self.intercept_ = -0.5 * np.diag(np.dot(self.means_, self.coef_.T)) + np.log(
            self.priors_
        )

    def _solve_eigen(self, X, y, shrinkage, covariance_estimator):
        """Eigenvalue solver.

        The eigenvalue solver computes the optimal solution of the Rayleigh
        coefficient (basically the ratio of between class scatter to within
        class scatter). This solver supports both classification and
        dimensionality reduction (with any covariance estimator).

              - float between 0 and 1: fixed shrinkage constant.

            not None

        Notes
        -----
        This solver is based on [1]_, section 3.8.3, pp. 121-124.

        """
        )

        Sw = self.covariance_  # within scatter
        St = _cov(X, shrinkage, covariance_estimator)  # total scatter
        Sb = St - Sw  # between scatter

        evals, evecs = linalg.eigh(Sb, Sw)
        self.explained_variance_ratio_ = np.sort(evals / np.sum(evals))[::-1][
            : self._max_components
        ]
        evecs = evecs[:, np.argsort(evals)[::-1]]  # sort eigenvectors

        self.scalings_ = evecs
        self.coef_ = np.dot(self.means_, evecs).dot(evecs.T)
        )

    def _solve_svd(self, X, y):
        """SVD solver.

        """
        xp, is_array_api_compliant = get_namespace(X)

        if is_array_api_compliant:
            svd = xp.linalg.svd
        else:
            svd = scipy.linalg.svd

        if self.store_covariance:
            self.covariance_ = _class_cov(X, y, self.priors_)

        Xc = []
        for idx, group in enumerate(self.classes_):
            Xg = X[y == group]
            Xc.append(Xg - self.means_[idx, :])

        self.xbar_ = self.priors_ @ self.means_

        Xc = xp.concat(Xc, axis=0)

        # 1) within (univariate) scaling by with classes std-dev
        std = xp.std(Xc, axis=0)
        # avoid division by zero in normalization
        std[std == 0] = 1.0
        fac = xp.asarray(1.0 / (n_samples - n_classes), dtype=X.dtype, device=device(X))

        # 2) Within variance scaling
        X = xp.sqrt(fac) * (Xc / std)
        # SVD of centered (within)scaled data
        U, S, Vt = svd(X, full_matrices=False)

        rank = xp.sum(xp.astype(S > self.tol, xp.int32))
        # Scaling of within covariance is: V' 1/S
        scalings = (Vt[:rank, :] / std).T / S[:rank]
        fac = 1.0 if n_classes == 1 else 1.0 / (n_classes - 1)

        # 3) Between variance scaling
        # Scale weighted centers
        X = (
            (xp.sqrt((n_samples * self.priors_) * fac)) * (self.means_ - self.xbar_).T
        ).T @ scalings
        # Centers are living in a space with n_classes-1 dim (maximum)
        # Use SVD to find projection in the space spanned by the
        # (n_classes) centers
        _, S, Vt = svd(X, full_matrices=False)

        if self._max_components == 0:
            self.explained_variance_ratio_ = xp.empty((0,), dtype=S.dtype)
        else:
            self.explained_variance_ratio_ = (S**2 / xp.sum(S**2))[
                : self._max_components
            ]

        rank = xp.sum(xp.astype(S > self.tol * S[0], xp.int32))
        self.scalings_ = scalings @ Vt.T[:, :rank]
        coef = (self.means_ - self.xbar_) @ self.scalings_
        self.intercept_ = -0.5 * xp.sum(coef**2, axis=1) + xp.log(self.priors_)
        self.coef_ = coef @ self.scalings_.T
        self.intercept_ -= self.xbar_ @ self.coef_.T

        # LinearDiscriminantAnalysis.covariance_estimator is not validated yet
    )
        """Fit the Linear Discriminant Analysis model.

        .. versionchanged:: 0.19
            `store_covariance` and `tol` has been moved to main constructor.

        Returns
        -------
        """
        xp, _ = get_namespace(X)

            self, X, y, ensure_min_samples=2, dtype=[xp.float64, xp.float32]
        )
        self.classes_ = unique_labels(y)
        n_samples, _ = X.shape

        if n_samples == n_classes:
                "The number of samples must be more than the number of classes."
            )

        if self.priors is None:  # estimate priors from sample
            _, cnts = xp.unique_counts(y)  # non-negative ints
            self.priors_ = xp.astype(cnts, X.dtype) / float(y.shape[0])
        else:
            self.priors_ = xp.asarray(self.priors, dtype=X.dtype)

        if xp.any(self.priors_ < 0):
            raise ValueError("priors must be non-negative")

        if xp.abs(xp.sum(self.priors_) - 1.0) > 1e-5:
            warnings.warn("The priors do not sum to 1. Renormalizing", UserWarning)
            self.priors_ = self.priors_ / self.priors_.sum()

        # Maximum number of components no matter what n_components is
        # specified:
        max_components = min(n_classes - 1, X.shape[1])

        if self.n_components is None:
            self._max_components = max_components
        else:
            if self.n_components > max_components:
                    "n_components cannot be larger than min(n_features, n_classes - 1)."
                )
            self._max_components = self.n_components

        if self.solver == "svd":
            if self.shrinkage is not None:
                raise NotImplementedError("shrinkage not supported with 'svd' solver.")
            if self.covariance_estimator is not None:
                    "covariance estimator "
                    "is not supported "
                    "with svd solver. Try another solver"
                )
            self._solve_svd(X, y)
        elif self.solver == "lsqr":
            self._solve_lstsq(
                X,
                y,
                shrinkage=self.shrinkage,
                covariance_estimator=self.covariance_estimator,
            )
        elif self.solver == "eigen":
            self._solve_eigen(
                X,
                y,
            )
        if size(self.classes_) == 2:  # treat binary case as a special case
            coef_ = xp.asarray(self.coef_[1, :] - self.coef_[0, :], dtype=X.dtype)
            self.coef_ = xp.reshape(coef_, (1, -1))
            intercept_ = xp.asarray(
                self.intercept_[1] - self.intercept_[0], dtype=X.dtype
            )
            self.intercept_ = xp.reshape(intercept_, (1,))
        self._n_features_out = self._max_components

        """Project data to maximize class separation.

        Returns
        -------
        X_new : ndarray of shape (n_samples, n_components) or \
            (n_samples, min(rank, n_components))
            Transformed data. In the case of the 'svd' solver, the shape
            is (n_samples, min(rank, n_components)).
        """
        if self.solver == "lsqr":
            raise NotImplementedError(
                "transform not implemented for 'lsqr' solver (use 'svd' or 'eigen')."
            )
        X = validate_data(self, X, reset=False)

            X_new = (X - self.xbar_) @ self.scalings_
            X_new = X @ self.scalings_

        return X_new[:, : self._max_components]

        """Estimate probability.

        Returns
        -------
            Estimated probabilities.
        """
        decision = self.decision_function(X)
        if size(self.classes_) == 2:
            proba = _expit(decision, xp)
            return xp.stack([1 - proba, proba], axis=1)
        else:
            return softmax(decision)

        """Estimate log probability.

        Returns
        -------
        """
        prediction = self.predict_proba(X)

        info = xp.finfo(prediction.dtype)
        if hasattr(info, "smallest_normal"):
            smallest_normal = info.smallest_normal
        else:
            # smallest_normal was introduced in NumPy 1.22
            smallest_normal = info.tiny

        prediction[prediction == 0.0] += smallest_normal
        return xp.log(prediction)

        The decision function is equal (up to a constant factor) to the
        log-posterior of the model, i.e. `log p(y = k | x)`. In a binary
        classification setting this instead corresponds to the difference
        `log p(y = 1 | x) - log p(y = 0 | x)`. See :ref:`lda_qda_math`.

        Returns
        -------
        """
        # Only override for the doc
        return super().decision_function(X)

        tags.array_api_support = True

class QuadraticDiscriminantAnalysis(
    DiscriminantAnalysisPredictionMixin, ClassifierMixin, BaseEstimator
):
    """Quadratic Discriminant Analysis.

    A classifier with a quadratic decision boundary, generated
    by fitting class conditional densities to the data
    and using Bayes' rule.

    The model fits a Gaussian density to each class.

    :class:`~sklearn.discriminant_analysis.QuadraticDiscriminantAnalysis`
    and :class:`~sklearn.discriminant_analysis.LinearDiscriminantAnalysis`, see

        Class priors. By default, the class proportions are inferred from the
        training data.

    reg_param : float, default=0.0
        Regularizes the per-class covariance estimates by transforming S2 as
        ``S2 = (1 - reg_param) * S2 + reg_param * np.eye(n_features)``,
        where S2 corresponds to the `scaling_` attribute of a given class.

        If True, the class covariance matrices are explicitly computed and
        stored in the `self.covariance_` attribute.

        Absolute threshold for the covariance matrix to be considered rank
        deficient after applying some regularization (see `reg_param`) to each
        `Sk` where `Sk` represents covariance matrix for k-th class. This
        parameter does not affect the predictions. It controls when a warning
        is raised if the covariance matrix is not full rank.

    covariance_ : list of len n_classes of ndarray \
            of shape (n_features, n_features)
        For each class, gives the covariance matrix estimated using the
        samples of that class. The estimations are unbiased. Only present if
        `store_covariance` is True.

    rotations_ : list of len n_classes of ndarray of shape (n_features, n_k)
        For each class k an array of shape (n_features, n_k), where
        ``n_k = min(n_features, number of elements in class k)``
        It is the rotation of the Gaussian distribution, i.e. its
        principal axis. It corresponds to `V`, the matrix of eigenvectors
        coming from the SVD of `Xk = U S Vt` where `Xk` is the centered
        matrix of samples from class k.

    scalings_ : list of len n_classes of ndarray of shape (n_k,)
        For each class, contains the scaling of
        the Gaussian distributions along its principal axes, i.e. the
        variance in the rotated coordinate system. It corresponds to `S^2 /
        (n_samples - 1)`, where `S` is the diagonal matrix of singular values
        from the SVD of `Xk`, where `Xk` is the centered matrix of samples
        from class k.

    See Also
    --------
    LinearDiscriminantAnalysis : Linear Discriminant Analysis.

    Examples
    --------
    >>> from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
    >>> clf = QuadraticDiscriminantAnalysis()
    QuadraticDiscriminantAnalysis()
    [1]
    """

        "reg_param": [Interval(Real, 0, 1, closed="both")],
    }

        self, *, priors=None, reg_param=0.0, store_covariance=False, tol=1.0e-4
    ):
        self.reg_param = reg_param
        self.store_covariance = store_covariance
        self.tol = tol

        """Fit the model according to the given training data and parameters.

            ``store_covariances`` has been moved to main constructor as
            ``store_covariance``.

            ``tol`` has been moved to main constructor.

            Training vector, where `n_samples` is the number of samples and

            Target values (integers).

        Returns
        -------
        """
        X, y = validate_data(self, X, y)
        self.classes_, y = np.unique(y, return_inverse=True)
        if n_classes < 2:
                "The number of classes has to be greater than one; got %d class"
                % (n_classes)
            )
            self.priors_ = np.bincount(y) / float(n_samples)
        else:
            self.priors_ = np.array(self.priors)

        cov = None
        store_covariance = self.store_covariance
        if store_covariance:
            cov = []
        means = []
        scalings = []
        rotations = []
        for ind in range(n_classes):
            Xg = X[y == ind, :]
            meang = Xg.mean(0)
            means.append(meang)
            if len(Xg) == 1:
                    "y has only 1 sample in class %s, covariance is ill defined."
                    % str(self.classes_[ind])
                )
            Xgc = Xg - meang
            # Xgc = U * S * V.T
            _, S, Vt = np.linalg.svd(Xgc, full_matrices=False)
            S2 = (S**2) / (len(Xg) - 1)
            S2 = ((1 - self.reg_param) * S2) + self.reg_param
            rank = np.sum(S2 > self.tol)
            if rank < n_features:
                    f"The covariance matrix of class {ind} is not full rank. "
                    "Increasing the value of parameter `reg_param` might help"
                    " reducing the collinearity.",
                    linalg.LinAlgWarning,
                )
            if self.store_covariance or store_covariance:
                # cov = V * (S^2 / (n-1)) * V.T
                cov.append(np.dot(S2 * Vt.T, Vt))
            scalings.append(S2)
            rotations.append(Vt.T)
        if self.store_covariance or store_covariance:
            self.covariance_ = cov
        self.means_ = np.asarray(means)
        self.scalings_ = scalings
        self.rotations_ = rotations

    def _decision_function(self, X):
        # return log posterior, see eq (4.12) p. 110 of the ESL.

        norm2 = []
        for i in range(len(self.classes_)):
            R = self.rotations_[i]
            S = self.scalings_[i]
            Xm = X - self.means_[i]
            X2 = np.dot(Xm, R * (S ** (-0.5)))
            norm2.append(np.sum(X2**2, axis=1))
        norm2 = np.array(norm2).T  # shape = [len(X), n_classes]
        u = np.asarray([np.sum(np.log(s)) for s in self.scalings_])
        return -0.5 * (norm2 + u) + np.log(self.priors_)

        Returns
        -------
        C : ndarray of shape (n_samples,) or (n_samples, n_classes)
        """

        """Perform classification on an array of test vectors X.

        The predicted class C for each sample in X is returned.

            Vector to be scored, where `n_samples` is the number of samples and

        Returns
        -------
        """
        return super().predict(X)

        """Return posterior probabilities of classification.

            Array of samples/test vectors.

        Returns
        -------
            Posterior probabilities of classification per class.
        """
        # compute the likelihood of the underlying gaussian models
        # up to a multiplicative constant.
        return super().predict_proba(X)

        """Return log of posterior probabilities of classification.

        Returns
        -------
            Posterior log-probabilities of classification per class.
        """
        # XXX : can do better to avoid precision overflows
        return super().predict_log_proba(X)

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """
        )

        self,
        tol=1e-4,
    ):

        because

            not None

        Notes
        -----

        """
        )
        )

            not None

        Notes
        -----

        """
        )

        ]

        )

        """

        else:

        Xc = []

        X = (

        else:
            ]

        Returns
        -------
        """

        )

            )

        else:

        else:
                )

                )
                X,
                y,
            )
                X,
                y,
            )
            )

        Returns
        -------
        """
            )

        Returns
        -------
        """
        else:

        Returns
        -------
        """

        else:

        Returns
        -------
        """

    ):

        Returns
        -------
        """
            )
        else:

        cov = None
            cov = []
        means = []
                )
                )

        norm2 = []

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

def _fit_estimator(estimator, X, y, sample_weight=None, **fit_params):
    estimator = clone(estimator)
    if sample_weight is not None:
        estimator.fit(X, y, sample_weight=sample_weight, **fit_params)
    else:

def _partial_fit_estimator(
    estimator, X, y, classes=None, partial_fit_params=None, first_time=True
):
    partial_fit_params = {} if partial_fit_params is None else partial_fit_params
    if first_time:

    if classes is not None:
        estimator.partial_fit(X, y, classes=classes, **partial_fit_params)
    else:
        estimator.partial_fit(X, y, **partial_fit_params)

def _available_if_estimator_has(attr):
    """Return a function to check if the sub-estimator(s) has(have) `attr`.

    Helper for Chain implementations.
    """

    def _check(self):
            return all(hasattr(est, attr) for est in self.estimators_)

        if hasattr(self.estimator, attr):

        return False

    return available_if(_check)

class _MultiOutputEstimator(MetaEstimatorMixin, BaseEstimator, metaclass=ABCMeta):
        "estimator": [HasMethods(["fit", "predict"])],
    }

    @_available_if_estimator_has("partial_fit")
        # MultiOutput*.estimator is not validated yet
    )
    def partial_fit(self, X, y, classes=None, sample_weight=None, **partial_fit_params):
        """Incrementally fit a separate model for each class output.

            The input data.

        y : {array-like, sparse matrix} of shape (n_samples, n_outputs)
            Multi-output targets.

        classes : list of ndarray of shape (n_outputs,), default=None
            Each array is unique classes for one output in str/int.
            Can be obtained via
            ``[np.unique(y[:, i]) for i in range(y.shape[1])]``, where `y`
            is the target matrix of the entire dataset.
            This argument is required for the first call to partial_fit
            Note that `y` doesn't need to contain all labels in `classes`.

            Sample weights. If `None`, then samples are equally weighted.
            Only supported if the underlying regressor supports sample
            weights.

        **partial_fit_params : dict of str -> object

            Only available if `enable_metadata_routing=True`. See the
            :ref:`User Guide <metadata_routing>`.

            .. versionadded:: 1.3

        Returns
        -------
            Returns a fitted instance.
        """

        first_time = not hasattr(self, "estimators_")

        y = validate_data(self, X="no_validation", y=y, multi_output=True)

                "y must have at least two dimensions for "
                "multi-output regression but has only one."
            )

                partial_fit_params["sample_weight"] = sample_weight
                self,
                "partial_fit",
                **partial_fit_params,
            )
        else:
            if sample_weight is not None and not has_fit_parameter(
                self.estimator, "sample_weight"
            ):
                    "Underlying estimator does not support sample weights."
                )

                routed_params = Bunch(
                    estimator=Bunch(partial_fit=Bunch(sample_weight=sample_weight))
                )
            else:
                routed_params = Bunch(estimator=Bunch(partial_fit=Bunch()))

            delayed(_partial_fit_estimator)(
                self.estimators_[i] if not first_time else self.estimator,
                X,
                y[:, i],
                classes[i] if classes is not None else None,
                first_time=first_time,
            )
            for i in range(y.shape[1])
        )

        if first_time and hasattr(self.estimators_[0], "n_features_in_"):
        if first_time and hasattr(self.estimators_[0], "feature_names_in_"):

    )
    def fit(self, X, y, sample_weight=None, **fit_params):
        """Fit the model to data, separately for each output variable.

            Multi-output targets. An indicator matrix turns on multilabel
            estimation.

            weights.

        **fit_params : dict of string -> object
            Parameters passed to the ``estimator.fit`` method of each step.

            .. versionadded:: 0.23

        Returns
        -------
        """
        if not hasattr(self.estimator, "fit"):
            raise ValueError("The base estimator should implement a fit method")

        if is_classifier(self):
            check_classification_targets(y)

            )

                fit_params["sample_weight"] = sample_weight
                self,
                "fit",
                **fit_params,
            )
        else:
            ):
                )

            fit_params_validated = _check_method_params(X, params=fit_params)
            routed_params = Bunch(estimator=Bunch(fit=fit_params_validated))
                routed_params.estimator.fit["sample_weight"] = sample_weight

            delayed(_fit_estimator)(
                self.estimator, X, y[:, i], **routed_params.estimator.fit
            )
        )

        """Predict multi-output variable using model for each target variable.

        Returns
        -------
            Multi-output targets predicted across multiple predictors.
            Note: Separate models are generated for each predictor.
        """
        if not hasattr(self.estimators_[0], "predict"):
            raise ValueError("The base estimator should implement a predict method")

        y = Parallel(n_jobs=self.n_jobs)(
            delayed(e.predict)(X) for e in self.estimators_
        )

        return np.asarray(y).T

        tags.target_tags.single_output = False

        Returns
        -------
        """
            method_mapping=MethodMapping()
            .add(caller="partial_fit", callee="partial_fit")
            .add(caller="fit", callee="fit"),
        )

class MultiOutputRegressor(RegressorMixin, _MultiOutputEstimator):
    """Multi target regression.

    This strategy consists of fitting one regressor per target. This is a
    simple strategy for extending regressors that do not natively support
    multi-target regression.

    .. versionadded:: 0.18

        An estimator object implementing :term:`fit` and :term:`predict`.

    n_jobs : int or None, optional (default=None)
        The number of jobs to run in parallel.
        :meth:`fit`, :meth:`predict` and :meth:`partial_fit` (if supported
        by the passed estimator) will be parallelized for each target.

        When individual estimators are fast to train or predict,
        using ``n_jobs > 1`` can result in slower performance due
        to the parallelism overhead.

        ``None`` means `1` unless in a :obj:`joblib.parallel_backend` context.
        ``-1`` means using all available processes / threads.
        See :term:`Glossary <n_jobs>` for more details.

            `n_jobs` default changed from `1` to `None`.

    estimators_ : list of ``n_output`` estimators

        underlying `estimator` exposes such an attribute when fit.

        underlying estimators expose such an attribute when fit.

    See Also
    --------
    RegressorChain : A multi-label model that arranges regressions into a
        chain.
    MultiOutputClassifier : Classifies each output independently rather than
        chaining.

    Examples
    --------
    >>> from sklearn.datasets import load_linnerud
    >>> from sklearn.multioutput import MultiOutputRegressor
    >>> from sklearn.linear_model import Ridge
    >>> X, y = load_linnerud(return_X_y=True)
    >>> regr = MultiOutputRegressor(Ridge(random_state=123)).fit(X, y)
    >>> regr.predict(X[[0]])
    array([[176..., 35..., 57...]])
    """

        super().__init__(estimator, n_jobs=n_jobs)

    def partial_fit(self, X, y, sample_weight=None, **partial_fit_params):
        """Incrementally fit the model to data, for each output variable.

            weights.

        Returns
        -------
        """
        super().partial_fit(X, y, sample_weight=sample_weight, **partial_fit_params)

class MultiOutputClassifier(ClassifierMixin, _MultiOutputEstimator):
    """Multi target classification.

    This strategy consists of fitting one classifier per target. This is a
    simple strategy for extending classifiers that do not natively support
    multi-target classification.

        A :term:`predict_proba` method will be exposed only if `estimator` implements
        it.

    See Also
    --------
    ClassifierChain : A multi-label model that arranges binary classifiers
        into a chain.
    MultiOutputRegressor : Fits one regressor per target variable.

    Examples
    --------
    >>> from sklearn.datasets import make_multilabel_classification
    >>> from sklearn.multioutput import MultiOutputClassifier
    >>> X, y = make_multilabel_classification(n_classes=3, random_state=0)
    >>> clf = MultiOutputClassifier(LogisticRegression()).fit(X, y)
    >>> clf.predict(X[-2:])
    array([[1, 1, 1],
           [1, 0, 1]])
    """

    def fit(self, X, Y, sample_weight=None, **fit_params):
        """Fit the model to data matrix X and targets Y.

        Y : array-like of shape (n_samples, n_classes)
            The target values.

            Only supported if the underlying classifier supports sample
            weights.

        Returns
        -------
        """
        super().fit(X, Y, sample_weight=sample_weight, **fit_params)
        self.classes_ = [estimator.classes_ for estimator in self.estimators_]

    def _check_predict_proba(self):
            # raise an AttributeError if `predict_proba` does not exist for
            # each estimator
            [getattr(est, "predict_proba") for est in self.estimators_]
        # raise an AttributeError if `predict_proba` does not exist for the
        # unfitted estimator
        getattr(self.estimator, "predict_proba")

    @available_if(_check_predict_proba)
        """Return prediction probabilities for each class of each output.

        This method will raise a ``ValueError`` if any of the
        estimators do not have ``predict_proba``.

        Returns
        -------
        p : array of shape (n_samples, n_classes), or a list of n_outputs \
                such arrays if n_outputs > 1.
            The class probabilities of the input samples. The order of the
            classes corresponds to that in the attribute :term:`classes_`.

                This function now returns a list of arrays where the length of
                the list is ``n_outputs``, and each array is (``n_samples``,
                ``n_classes``) for that particular output.
        """
        results = [estimator.predict_proba(X) for estimator in self.estimators_]
        return results

    def score(self, X, y):

        y : array-like of shape (n_samples, n_outputs)

        Returns
        -------
        scores : float
            Mean accuracy of predicted target versus true target.
        """
        n_outputs_ = len(self.estimators_)
                "multi target classification but has only one"
            )
        if y.shape[1] != n_outputs_:
                "The number of outputs of Y for fit {0} and"
                " score {1} should be same".format(n_outputs_, y.shape[1])
            )
        return np.mean(np.all(y == y_pred, axis=1))

        # FIXME
        tags._skip_test = True

def _available_if_base_estimator_has(attr):
    """Return a function to check if `base_estimator` or `estimators_` has `attr`.

    """

        return hasattr(self._get_estimator(), attr) or all(
            hasattr(est, attr) for est in self.estimators_
        )

class _BaseChain(BaseEstimator, metaclass=ABCMeta):
        "base_estimator": [
            HasMethods(["fit", "predict"]),
            StrOptions({"deprecated"}),
        ],
            Hidden(None),
        ],
        "order": ["array-like", StrOptions({"random"}), None],
        "cv": ["cv_object", StrOptions({"prefit"})],
    }

    # TODO(1.9): Remove base_estimator
        self,
        estimator=None,
        *,
        order=None,
        cv=None,
        base_estimator="deprecated",
    ):
        self.base_estimator = base_estimator
        self.order = order
        self.cv = cv

    # TODO(1.8): This is a temporary getter method to validate input wrt deprecation.
    # It was only included to avoid relying on the presence of self.estimator_
    def _get_estimator(self):
        """Get and validate estimator."""

        if self.estimator is not None and (self.base_estimator != "deprecated"):
                "Both `estimator` and `base_estimator` are provided. You should only"
                " pass `estimator`. `base_estimator` as a parameter is deprecated in"
                " version 1.7, and will be removed in version 1.9."
            )

        if self.base_estimator != "deprecated":
            warning_msg = (
                "`base_estimator` as an argument was deprecated in 1.7 and will be"
                " removed in 1.9. Use `estimator` instead."
            )
            warnings.warn(warning_msg, FutureWarning)
            return self.base_estimator
        else:
            return self.estimator

    def _log_message(self, *, estimator_idx, n_estimators, processing_msg):
        return f"({estimator_idx} of {n_estimators}) {processing_msg}"

    def _get_predictions(self, X, *, output_method):
        """Get predictions for each model in the chain."""
        X = validate_data(self, X, accept_sparse=True, reset=False)
        Y_output_chain = np.zeros((X.shape[0], len(self.estimators_)))
        Y_feature_chain = np.zeros((X.shape[0], len(self.estimators_)))

        # `RegressorChain` does not have a `chain_method_` parameter so we
        # default to "predict"
        chain_method = getattr(self, "chain_method_", "predict")
        hstack = sp.hstack if sp.issparse(X) else np.hstack
        for chain_idx, estimator in enumerate(self.estimators_):
            previous_predictions = Y_feature_chain[:, :chain_idx]
            # if `X` is a scipy sparse dok_array, we convert it to a sparse
            # coo_array format before hstacking, it's faster; see
            # https://github.com/scipy/scipy/issues/20060#issuecomment-1937007039:
            if sp.issparse(X) and not sp.isspmatrix(X) and X.format == "dok":
                X = sp.coo_array(X)
            X_aug = hstack((X, previous_predictions))

            feature_predictions, _ = _get_response_values(
                X_aug,
                response_method=chain_method,
            )
            Y_feature_chain[:, chain_idx] = feature_predictions

            output_predictions, _ = _get_response_values(
                X_aug,
                response_method=output_method,
            )
            Y_output_chain[:, chain_idx] = output_predictions

        inv_order = np.empty_like(self.order_)
        inv_order[self.order_] = np.arange(len(self.order_))
        Y_output = Y_output_chain[:, inv_order]

        return Y_output

    def fit(self, X, Y, **fit_params):

            Parameters passed to the `fit` method of each step.

        Returns
        -------
        """
        X, Y = validate_data(self, X, Y, multi_output=True, accept_sparse=True)

        self.order_ = self.order
        if isinstance(self.order_, tuple):
            self.order_ = np.array(self.order_)

        if self.order_ is None:
            self.order_ = np.array(range(Y.shape[1]))
        elif isinstance(self.order_, str):
            if self.order_ == "random":
                self.order_ = random_state.permutation(Y.shape[1])
        elif sorted(self.order_) != list(range(Y.shape[1])):
            raise ValueError("invalid order")

        self.estimators_ = [clone(self._get_estimator()) for _ in range(Y.shape[1])]

        if self.cv is None:
            Y_pred_chain = Y[:, self.order_]
            if sp.issparse(X):
                X_aug = sp.hstack((X, Y_pred_chain), format="lil")
                X_aug = X_aug.tocsr()
            else:
                X_aug = np.hstack((X, Y_pred_chain))

        elif sp.issparse(X):
            # TODO: remove this condition check when the minimum supported scipy version
            # doesn't support sparse matrices anymore
            if not sp.isspmatrix(X):
                # if `X` is a scipy sparse dok_array, we convert it to a sparse
                # coo_array format before hstacking, it's faster; see
                # https://github.com/scipy/scipy/issues/20060#issuecomment-1937007039:
                if X.format == "dok":
                    X = sp.coo_array(X)
                # in case that `X` is a sparse array we create `Y_pred_chain` as a
                # sparse array format:
                Y_pred_chain = sp.coo_array((X.shape[0], Y.shape[1]))
            else:
                Y_pred_chain = sp.coo_matrix((X.shape[0], Y.shape[1]))
            X_aug = sp.hstack((X, Y_pred_chain), format="lil")

        else:
            Y_pred_chain = np.zeros((X.shape[0], Y.shape[1]))
            X_aug = np.hstack((X, Y_pred_chain))

        del Y_pred_chain

        else:
            routed_params = Bunch(estimator=Bunch(fit=fit_params))

        if hasattr(self, "chain_method"):
            chain_method = _check_response_method(
                self._get_estimator(),
                self.chain_method,
            ).__name__
            self.chain_method_ = chain_method
        else:
            # `RegressorChain` does not have a `chain_method` parameter
            chain_method = "predict"

            message = self._log_message(
                estimator_idx=chain_idx + 1,
                n_estimators=len(self.estimators_),
                processing_msg=f"Processing order {self.order_[chain_idx]}",
            )
            y = Y[:, self.order_[chain_idx]]
            with _print_elapsed_time("Chain", message):
                estimator.fit(
                    X_aug[:, : (X.shape[1] + chain_idx)],
                    y,
                    **routed_params.estimator.fit,
                )

            if self.cv is not None and chain_idx < len(self.estimators_) - 1:
                col_idx = X.shape[1] + chain_idx
                cv_result = cross_val_predict(
                    self._get_estimator(),
                    X_aug[:, :col_idx],
                    y=y,
                    cv=self.cv,
                    method=chain_method,
                )
                # `predict_proba` output is 2D, we use only output for classes[-1]
                if cv_result.ndim > 1:
                    cv_result = cv_result[:, 1]
                if sp.issparse(X_aug):
                    X_aug[:, col_idx] = np.expand_dims(cv_result, 1)
                else:
                    X_aug[:, col_idx] = cv_result

        """Predict on the data matrix X using the ClassifierChain model.

        Returns
        -------
        Y_pred : array-like of shape (n_samples, n_classes)
            The predicted values.
        """
        return self._get_predictions(X, output_method="predict")

        tags.input_tags.sparse = get_tags(self._get_estimator()).input_tags.sparse

class ClassifierChain(MetaEstimatorMixin, ClassifierMixin, _BaseChain):
    """A multi-label model that arranges binary classifiers into a chain.

    Each model makes a prediction in the order specified by the chain using
    all of the available features provided to the model plus the predictions
    of models that are earlier in the chain.

    For an example of how to use ``ClassifierChain`` and benefit from its
    ensemble, see
    :ref:`ClassifierChain on a yeast dataset
    <sphx_glr_auto_examples_multioutput_plot_classifier_chain_yeast.py>` example.

    Read more in the :ref:`User Guide <classifierchain>`.

    estimator : estimator
        The base estimator from which the classifier chain is built.

    order : array-like of shape (n_outputs,) or 'random', default=None
        If `None`, the order will be determined by the order of columns in
        the label matrix Y.::

            order = [0, 1, 2, ..., Y.shape[1] - 1]

        The order of the chain can be explicitly set by providing a list of
        integers. For example, for a chain of length 5.::

            order = [1, 3, 2, 4, 0]

        means that the first model in the chain will make predictions for
        column 1 in the Y matrix, the second model will make predictions
        for column 3, etc.

        If order is `random` a random ordering will be used.

    cv : int, cross-validation generator or an iterable, default=None
        Determines whether to use cross validated predictions or true
        labels for the results of previous estimators in the chain.
        Possible inputs for cv are:

        - None, to use true labels when fitting,
        - integer, to specify the number of folds in a (Stratified)KFold,
        - :term:`CV splitter`,
        - An iterable yielding (train, test) splits as arrays of indices.

    chain_method : {'predict', 'predict_proba', 'predict_log_proba', \
            'decision_function'} or list of such str's, default='predict'

        Prediction method to be used by estimators in the chain for
        the 'prediction' features of previous estimators in the chain.

        - if `str`, name of the method;
        - if a list of `str`, provides the method names in order of
          preference. The method used corresponds to the first method in
          the list that is implemented by `base_estimator`.

    random_state : int, RandomState instance or None, optional (default=None)
        If ``order='random'``, determines random number generation for the
        chain order.
        In addition, it controls the random seed given at each `base_estimator`
        at each chaining iteration. Thus, it is only used when `base_estimator`
        exposes a `random_state`.

        If True, chain progress is output as each model is completed.

    base_estimator : estimator, default="deprecated"
        Use `estimator` instead.

        .. deprecated:: 1.7
            `base_estimator` is deprecated and will be removed in 1.9.
            Use `estimator` instead.

    classes_ : list
        A list of arrays of length ``len(estimators_)`` containing the
        class labels for each estimator in the chain.

    estimators_ : list
        A list of clones of base_estimator.

    order_ : list
        The order of labels in the classifier chain.

    chain_method_ : str
        Prediction method used by estimators in the chain for the prediction
        features.

        underlying `base_estimator` exposes such an attribute when fit.

    See Also
    --------
    RegressorChain : Equivalent for regression.
        chaining.

    Jesse Read, Bernhard Pfahringer, Geoff Holmes, Eibe Frank, "Classifier
    Chains for Multi-label Classification", 2009.

    Examples
    --------
    >>> from sklearn.multioutput import ClassifierChain
    >>> X, Y = make_multilabel_classification(
    ...    n_samples=12, n_classes=3, random_state=0
    ... )
    >>> X_train, X_test, Y_train, Y_test = train_test_split(
    ...    X, Y, random_state=0
    ... )
    >>> base_lr = LogisticRegression(solver='lbfgs', random_state=0)
    >>> chain = ClassifierChain(base_lr, order='random', random_state=0)
    >>> chain.fit(X_train, Y_train).predict(X_test)
    array([[1., 1., 0.],
           [1., 0., 0.],
           [0., 1., 0.]])
    >>> chain.predict_proba(X_test)
    array([[0.8387..., 0.9431..., 0.4576...],
           [0.8878..., 0.3684..., 0.2640...],
           [0.0321..., 0.9935..., 0.0626...]])
    """

        **_BaseChain._parameter_constraints,
        "chain_method": [
            list,
            tuple,
            StrOptions(
                {"predict", "predict_proba", "predict_log_proba", "decision_function"}
            ),
        ],
    }

    # TODO(1.9): Remove base_estimator from __init__
        self,
        *,
        cv=None,
        chain_method="predict",
    ):
            order=order,
            cv=cv,
            verbose=verbose,
            base_estimator=base_estimator,
        )
        self.chain_method = chain_method

        # ClassifierChain.base_estimator is not validated yet
    )

        Returns
        -------
            Class instance.
        """

        super().fit(X, Y, **fit_params)

    @_available_if_base_estimator_has("predict_proba")
        """Predict probability estimates.

        Returns
        -------
        Y_prob : array-like of shape (n_samples, n_classes)
            The predicted probabilities.
        """
        return self._get_predictions(X, output_method="predict_proba")

        """Predict logarithm of probability estimates.

        Returns
        -------
        Y_log_prob : array-like of shape (n_samples, n_classes)
            The predicted logarithm of the probabilities.
        """
        return np.log(self.predict_proba(X))

    @_available_if_base_estimator_has("decision_function")
        """Evaluate the decision_function of the models in the chain.

        Returns
        -------
        Y_decision : array-like of shape (n_samples, n_classes)
            Returns the decision function of the sample for each model
            in the chain.
        """
        return self._get_predictions(X, output_method="decision_function")

        Returns
        -------
        """

            estimator=self._get_estimator(),
        )

        # FIXME

class RegressorChain(MetaEstimatorMixin, RegressorMixin, _BaseChain):
    """A multi-label model that arranges regressions into a chain.

    Read more in the :ref:`User Guide <regressorchain>`.

        The base estimator from which the regressor chain is built.

        If order is 'random' a random ordering will be used.

    See Also
    --------
    ClassifierChain : Equivalent for classification.
    MultiOutputRegressor : Learns each output independently rather than
        chaining.

    Examples
    --------
    >>> from sklearn.multioutput import RegressorChain
    >>> logreg = LogisticRegression(solver='lbfgs')
    >>> X, Y = [[1, 0], [0, 1], [1, 1]], [[0, 2], [1, 1], [2, 0]]
    >>> chain = RegressorChain(logreg, order=[0, 1]).fit(X, Y)
    >>> chain.predict(X)
    array([[0., 2.],
           [1., 1.],
           [2., 0.]])
    """

        # RegressorChain.base_estimator is not validated yet
    )

            Parameters passed to the `fit` method at each step
            of the regressor chain.

        Returns
        -------
        """

        Returns
        -------
        """

        )

            weights.

        Returns
        -------
        """

            )

                self,
            )
        else:
            ):
                )

                )
            else:

                X,
                y[:, i],
            )
        )

            weights.

        Returns
        -------
        """

            )

                self,
                "fit",
            )
        else:
            ):
                )

            )
        )

        Returns
        -------
        """

        )

        Returns
        -------
        """
        )

            weights.

        Returns
        -------
        """

            weights.

        Returns
        -------
        """

        Returns
        -------

        """

        Returns
        -------
        """
            )
            )

        # FIXME

        )

        self,
        *,
        cv=None,
    ):

            )

            )
        else:

                X_aug,
            )

                X_aug,
            )

        Returns
        -------
        """

            else:

            else:

        else:

        else:

        else:

            )
                    y,
                )

                    y=y,
                )
                else:

        Returns
        -------
        """

        self,
        *,
        cv=None,
    ):
            cv=cv,
        )

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        )

        # FIXME

        Returns
        -------
        """

class UnsetMetadataPassedError(ValueError):
    """Exception class to raise if a metadata is passed which is not explicitly \
        requested (metadata=True) or not requested (metadata=False).

    .. versionadded:: 1.3

    message : str
        The message

    unrequested_params : dict
        A dictionary of parameters and their values which are provided but not
        requested.

    routed_params : dict
        A dictionary of routed parameters.
    """

    def __init__(self, *, message, unrequested_params, routed_params):
        super().__init__(message)
        self.unrequested_params = unrequested_params
        self.routed_params = routed_params

class NotFittedError(ValueError, AttributeError):
    """Exception class to raise if estimator is used before fitting.

    This class inherits from both ValueError and AttributeError to help with
    exception handling and backward compatibility.

    Examples
    --------
    >>> from sklearn.exceptions import NotFittedError
    >>> try:
    ...     LinearSVC().predict([[1, 2], [2, 3], [3, 4]])
    ... except NotFittedError as e:
    ...     print(repr(e))
    NotFittedError("This LinearSVC instance is not fitted yet. Call 'fit' with
    appropriate arguments before using this estimator."...)

    .. versionchanged:: 0.18
       Moved from sklearn.utils.validation.
    """

class ConvergenceWarning(UserWarning):
    """Custom warning to capture convergence problems

       Moved from sklearn.utils.
    """

class DataConversionWarning(UserWarning):
    """Warning used to notify implicit data conversions happening in the code.

    This warning occurs when some input data needs to be converted or
    interpreted in a way that may not match the user's expectations.

    For example, this warning may occur when the user
        - passes an integer array to a function which expects float input and
          will convert the input
        - requests a non-copying operation, but a copy is required to meet the
          implementation's data-type expectations;
        - passes an input whose shape can be interpreted ambiguously.

    """

class DataDimensionalityWarning(UserWarning):
    """Custom warning to notify potential issues with data dimensionality.

    For example, in random projection, this warning is raised when the
    number of components, which quantifies the dimensionality of the target
    projection space, is higher than the number of features, which quantifies
    the dimensionality of the original source space, to imply that the
    dimensionality of the problem will not be reduced.

    """

class EfficiencyWarning(UserWarning):
    """Warning used to notify the user of inefficient computation.

    This warning notifies the user that the efficiency may not be optimal due
    to some reason which may be included as a part of the warning message.
    This may be subclassed into a more specific Warning class.

    """

class FitFailedWarning(RuntimeWarning):
    """Warning class used if there is an error while fitting the estimator.

    This Warning is used in meta estimators GridSearchCV and RandomizedSearchCV
    and the cross-validation helper function cross_val_score to warn when there
    is an error while fitting the estimator.

       Moved from sklearn.cross_validation.
    """

class SkipTestWarning(UserWarning):
    """Warning class used to notify the user of a test that was skipped.

    For example, one of the estimator checks requires a pandas import.
    If the pandas package cannot be imported, the test will be skipped rather
    than register as a failure.
    """

class UndefinedMetricWarning(UserWarning):
    """Warning used when the metric is invalid

       Moved from sklearn.base.
    """

class PositiveSpectrumWarning(UserWarning):
    """Warning raised when the eigenvalues of a PSD matrix have issues

    This warning is typically raised by ``_check_psd_eigenvalues`` when the
    eigenvalues of a positive semidefinite (PSD) matrix such as a gram matrix
    (kernel) present significant negative eigenvalues, or bad conditioning i.e.
    very small non-zero eigenvalues compared to the largest eigenvalue.

    .. versionadded:: 0.22
    """

class InconsistentVersionWarning(UserWarning):
    """Warning raised when an estimator is unpickled with an inconsistent version.

    estimator_name : str
        Estimator name.

    current_sklearn_version : str
        Current scikit-learn version.

    original_sklearn_version : str
        Original scikit-learn version.
    """

        self, *, estimator_name, current_sklearn_version, original_sklearn_version
    ):
        self.estimator_name = estimator_name
        self.current_sklearn_version = current_sklearn_version
        self.original_sklearn_version = original_sklearn_version

    def __str__(self):
        return (
            f"Trying to unpickle estimator {self.estimator_name} from version"
            f" {self.original_sklearn_version} when "
            f"using version {self.current_sklearn_version}. This might lead to breaking"
            " code or "
            "invalid results. Use at your own risk. "
            "For more info please refer to:\n"
            "https://scikit-learn.org/stable/model_persistence.html"
            "#security-maintainability-limitations"
        )

class EstimatorCheckFailedWarning(UserWarning):
    """Warning raised when an estimator check from the common tests fails.

        Estimator instance for which the test failed.

    check_name : str
        Name of the check that failed.

    exception : Exception
        Exception raised by the failed check.

    status : str
        Status of the check.

    expected_to_fail : bool
        Whether the check was expected to fail.

    expected_to_fail_reason : str
        Reason for the expected failure.
    """

        self,
        *,
        estimator,
        check_name: str,
        exception: Exception,
        status: str,
        expected_to_fail: bool,
        expected_to_fail_reason: str,
    ):
        self.check_name = check_name
        self.exception = exception
        self.status = status
        self.expected_to_fail = expected_to_fail
        self.expected_to_fail_reason = expected_to_fail_reason

    def __repr__(self):
        expected_to_fail_str = (
            f"Expected to fail: {self.expected_to_fail_reason}"
            if self.expected_to_fail
            else "Not expected to fail"
        )
        return (
            f"Test {self.check_name} failed for estimator {self.estimator!r}.\n"
            f"Expected to fail reason: {expected_to_fail_str}\n"
            f"Exception: {self.exception}"
        )

        return self.__repr__()

    ):

        return (
            " code or "
        )

        self,
        *,
    ):

        )
        return (
        )

class CalibratedClassifierCV(ClassifierMixin, MetaEstimatorMixin, BaseEstimator):
    """Probability calibration with isotonic regression or logistic regression.

    This class uses cross-validation to both estimate the parameters of a
    classifier and subsequently calibrate a classifier. With default
    `ensemble=True`, for each cv split it
    fits a copy of the base estimator to the training subset, and calibrates it
    using the testing subset. For prediction, predicted probabilities are
    averaged across these individual calibrated classifiers. When
    `ensemble=False`, cross-validation is used to obtain unbiased predictions,
    via :func:`~sklearn.model_selection.cross_val_predict`, which are then
    used for calibration. For prediction, the base estimator, trained using all
    the data, is used. This is the prediction method implemented when
    `probabilities=True` for :class:`~sklearn.svm.SVC` and :class:`~sklearn.svm.NuSVC`
    estimators (see :ref:`User Guide <scores_probabilities>` for details).

    Already fitted classifiers can be calibrated by wrapping the model in a
    :class:`~sklearn.frozen.FrozenEstimator`. In this case all provided
    data is used for calibration. The user has to take care manually that data
    for model fitting and calibration are disjoint.

    The calibration is based on the :term:`decision_function` method of the
    `estimator` if it exists, else on :term:`predict_proba`.

    Read more in the :ref:`User Guide <calibration>`.
    In order to learn more on the CalibratedClassifierCV class, see the
    following calibration examples:
    :ref:`sphx_glr_auto_examples_calibration_plot_calibration.py`,
    :ref:`sphx_glr_auto_examples_calibration_plot_calibration_curve.py`, and
    :ref:`sphx_glr_auto_examples_calibration_plot_calibration_multiclass.py`.

    estimator : estimator instance, default=None
        The classifier whose output need to be calibrated to provide more
        accurate `predict_proba` outputs. The default classifier is
        a :class:`~sklearn.svm.LinearSVC`.

    method : {'sigmoid', 'isotonic'}, default='sigmoid'
        The method to use for calibration. Can be 'sigmoid' which
        corresponds to Platt's method (i.e. a logistic regression model) or
        'isotonic' which is a non-parametric approach. It is not advised to
        use isotonic calibration with too few calibration samples
        ``(<<1000)`` since it tends to overfit.

    cv : int, cross-validation generator, or iterable, default=None
        Determines the cross-validation splitting strategy.

        - None, to use the default 5-fold cross-validation,
        - integer, to specify the number of folds.

        For integer/None inputs, if ``y`` is binary or multiclass,
        :class:`~sklearn.model_selection.StratifiedKFold` is used. If ``y`` is
        neither binary nor multiclass, :class:`~sklearn.model_selection.KFold`
        is used.

        Refer to the :ref:`User Guide <cross_validation>` for the various
        cross-validation strategies that can be used here.

            ``cv`` default value if None changed from 3-fold to 5-fold.

        .. versionchanged:: 1.6
            `"prefit"` is deprecated. Use :class:`~sklearn.frozen.FrozenEstimator`
            instead.

        ``-1`` means using all processors.

        Base estimator clones are fitted in parallel across cross-validation
        iterations. Therefore parallelism happens only when `cv != "prefit"`.

    ensemble : bool, or "auto", default="auto"
        Determines how the calibrator is fitted.

        "auto" will use `False` if the `estimator` is a
        :class:`~sklearn.frozen.FrozenEstimator`, and `True` otherwise.

        If `True`, the `estimator` is fitted using training data, and
        calibrated using testing data, for each `cv` fold. The final estimator
        is an ensemble of `n_cv` fitted classifier and calibrator pairs, where
        `n_cv` is the number of cross-validation folds. The output is the
        average predicted probabilities of all pairs.

        If `False`, `cv` is used to compute unbiased predictions, via
        :func:`~sklearn.model_selection.cross_val_predict`, which are then
        used for calibration. At prediction time, the classifier used is the
        `estimator` trained on all the data.
        Note that this method is also internally implemented  in
        :mod:`sklearn.svm` estimators with the `probabilities=True` parameter.

            `"auto"` option is added and is the default.

        The class labels.

    calibrated_classifiers_ : list (len() equal to cv or 1 if `ensemble=False`)
        The list of classifier and calibrator pairs.

        - When `ensemble=True`, `n_cv` fitted `estimator` and calibrator pairs.
          `n_cv` is the number of cross-validation folds.
        - When `ensemble=False`, the `estimator`, fitted on all the data, and fitted
          calibrator.

        .. versionchanged:: 0.24
            Single calibrated classifier case when `ensemble=False`.

    See Also
    --------
    calibration_curve : Compute true and predicted probabilities
        for a calibration curve.

    .. [1] Obtaining calibrated probability estimates from decision trees
           and naive Bayesian classifiers, B. Zadrozny & C. Elkan, ICML 2001

    .. [2] Transforming Classifier Scores into Accurate Multiclass
           Probability Estimates, B. Zadrozny & C. Elkan, (KDD 2002)

    .. [3] Probabilistic Outputs for Support Vector Machines and Comparisons to
           Regularized Likelihood Methods, J. Platt, (1999)

    .. [4] Predicting Good Probabilities with Supervised Learning,
           A. Niculescu-Mizil & R. Caruana, ICML 2005

    Examples
    --------
    >>> from sklearn.calibration import CalibratedClassifierCV
    >>> X, y = make_classification(n_samples=100, n_features=2,
    ...                            n_redundant=0, random_state=42)
    >>> base_clf = GaussianNB()
    >>> calibrated_clf = CalibratedClassifierCV(base_clf, cv=3)
    >>> calibrated_clf.fit(X, y)
    CalibratedClassifierCV(...)
    >>> len(calibrated_clf.calibrated_classifiers_)
    3
    >>> calibrated_clf.predict_proba(X)[:5, :]
    array([[0.110..., 0.889...],
           [0.072..., 0.927...],
           [0.928..., 0.071...],
           [0.071..., 0.928...]])
    >>> X_train, X_calib, y_train, y_calib = train_test_split(
    ...        X, y, random_state=42
    ... )
    >>> base_clf.fit(X_train, y_train)
    >>> from sklearn.frozen import FrozenEstimator
    >>> calibrated_clf = CalibratedClassifierCV(FrozenEstimator(base_clf))
    >>> calibrated_clf.fit(X_calib, y_calib)
    1
    >>> calibrated_clf.predict_proba([[-0.5, 0.5]])
    array([[0.936..., 0.063...]])
    """

            None,
        ],
        "method": [StrOptions({"isotonic", "sigmoid"})],
        "cv": ["cv_object", Hidden(StrOptions({"prefit"}))],
        "ensemble": ["boolean", StrOptions({"auto"})],
    }

        self,
        *,
        method="sigmoid",
        cv=None,
        ensemble="auto",
    ):
        self.method = method
        self.ensemble = ensemble

        """Resolve which estimator to return (default is LinearSVC)"""
        if self.estimator is None:
            # we want all classifiers that don't expose a random_state
            # to be deterministic (and we don't want to expose this one).
            estimator = LinearSVC(random_state=0)
            if _routing_enabled():
                estimator.set_fit_request(sample_weight=True)
        else:
            estimator = self.estimator

        return estimator

        # CalibratedClassifierCV.estimator is not validated yet
    )
        """Fit the calibrated model.

            Sample weights. If None, then samples are equally weighted.

            Parameters to pass to the `fit` method of the underlying
            classifier.

        Returns
        -------
        """
        X, y = indexable(X, y)
        estimator = self._get_estimator()

        _ensemble = self.ensemble
        if _ensemble == "auto":
            _ensemble = not isinstance(estimator, FrozenEstimator)

        self.calibrated_classifiers_ = []
        if self.cv == "prefit":
            # TODO(1.8): Remove this code branch and cv='prefit'
                "The `cv='prefit'` option is deprecated in 1.6 and will be removed in"
                " 1.8. You can use CalibratedClassifierCV(FrozenEstimator(estimator))"
                " instead.",
                category=FutureWarning,
            )
            # `classes_` should be consistent with that of estimator
            check_is_fitted(self.estimator, attributes=["classes_"])
            self.classes_ = self.estimator.classes_

            predictions, _ = _get_response_values(
                X,
                response_method=["decision_function", "predict_proba"],
            )
            if predictions.ndim == 1:
                # Reshape binary output from `(n_samples,)` to `(n_samples, 1)`
                predictions = predictions.reshape(-1, 1)

                # Check that the sample_weight dtype is consistent with the predictions
                # to avoid unintentional upcasts.
                sample_weight = _check_sample_weight(
                    sample_weight, predictions, dtype=predictions.dtype
                )

            calibrated_classifier = _fit_calibrator(
                predictions,
                y,
                self.classes_,
                self.method,
                sample_weight,
            )
            self.calibrated_classifiers_.append(calibrated_classifier)
        else:
            # Set `classes_` using all `y`
            label_encoder_ = LabelEncoder().fit(y)
            self.classes_ = label_encoder_.classes_

                routed_params = process_routing(
                    self,
                    "fit",
                    sample_weight=sample_weight,
                    **fit_params,
                )
            else:
                # sample_weight checks
                fit_parameters = signature(estimator.fit).parameters
                supports_sw = "sample_weight" in fit_parameters
                if sample_weight is not None and not supports_sw:
                    estimator_name = type(estimator).__name__
                    warnings.warn(
                        f"Since {estimator_name} does not appear to accept"
                        " sample_weight, sample weights will only be used for the"
                        " calibration itself. This can be caused by a limitation of"
                        " the current scikit-learn API. See the following issue for"
                        " more details:"
                        " https://github.com/scikit-learn/scikit-learn/issues/21134."
                        " Be warned that the result of the calibration is likely to be"
                        " incorrect."
                    )
                routed_params = Bunch()
                routed_params.splitter = Bunch(split={})  # no routing for splitter
                routed_params.estimator = Bunch(fit=fit_params)
                if sample_weight is not None and supports_sw:
                    routed_params.estimator.fit["sample_weight"] = sample_weight

            # Check that each cross-validation fold can have at least one
            # example per class
            if isinstance(self.cv, int):
                n_folds = self.cv
            elif hasattr(self.cv, "n_splits"):
                n_folds = self.cv.n_splits
            else:
                n_folds = None
            if n_folds and np.any(np.unique(y, return_counts=True)[1] < n_folds):
                    f"Requesting {n_folds}-fold "
                    "cross-validation but provided less than "
                    f"{n_folds} examples for at least one class."
                )
            if isinstance(self.cv, LeaveOneOut):
                    "LeaveOneOut cross-validation does not allow"
                    "all classes to be present in test splits. "
                    "Please use a cross-validation generator that allows "
                    "all classes to appear in every test and train split."
                )
            cv = check_cv(self.cv, y, classifier=True)

            if _ensemble:
                parallel = Parallel(n_jobs=self.n_jobs)
                self.calibrated_classifiers_ = parallel(
                    delayed(_fit_classifier_calibrator_pair)(
                        clone(estimator),
                        X,
                        y,
                        train=train,
                        test=test,
                        method=self.method,
                        classes=self.classes_,
                        sample_weight=sample_weight,
                        fit_params=routed_params.estimator.fit,
                    )
                    for train, test in cv.split(X, y, **routed_params.splitter.split)
                )
            else:
                this_estimator = clone(estimator)
                method_name = _check_response_method(
                    this_estimator,
                    ["decision_function", "predict_proba"],
                ).__name__
                predictions = cross_val_predict(
                    estimator=this_estimator,
                    X=X,
                    y=y,
                    cv=cv,
                    method=method_name,
                    n_jobs=self.n_jobs,
                    params=routed_params.estimator.fit,
                )
                if len(self.classes_) == 2:
                    # Ensure shape (n_samples, 1) in the binary case
                    if method_name == "predict_proba":
                        # Select the probability column of the positive class
                        predictions = _process_predict_proba(
                            y_pred=predictions,
                            target_type="binary",
                            classes=self.classes_,
                            pos_label=self.classes_[1],
                        )
                    predictions = predictions.reshape(-1, 1)

                    # Check that the sample_weight dtype is consistent with the
                    # predictions to avoid unintentional upcasts.
                    sample_weight = _check_sample_weight(
                        sample_weight, predictions, dtype=predictions.dtype
                    )

                this_estimator.fit(X, y, **routed_params.estimator.fit)
                # Note: Here we don't pass on fit_params because the supported
                # calibrators don't support fit_params anyway
                calibrated_classifier = _fit_calibrator(
                    predictions,
                    y,
                    self.classes_,
                    self.method,
                    sample_weight,
                )
                self.calibrated_classifiers_.append(calibrated_classifier)

        first_clf = self.calibrated_classifiers_[0].estimator
        if hasattr(first_clf, "n_features_in_"):
            self.n_features_in_ = first_clf.n_features_in_
        if hasattr(first_clf, "feature_names_in_"):
            self.feature_names_in_ = first_clf.feature_names_in_

        """Calibrated probabilities of classification.

        This function returns calibrated probabilities of classification
        according to each class on an array of test vectors X.

            The samples, as accepted by `estimator.predict_proba`.

        Returns
        -------
            The predicted probas.
        """
        # Compute the arithmetic mean of the predictions of the calibrated
        # classifiers
        mean_proba = np.zeros((_num_samples(X), len(self.classes_)))
        for calibrated_classifier in self.calibrated_classifiers_:
            proba = calibrated_classifier.predict_proba(X)
            mean_proba += proba

        mean_proba /= len(self.calibrated_classifiers_)

        return mean_proba

        """Predict the target of new samples.

        The predicted class is the class that has the highest probability,
        and can thus be different from the prediction of the uncalibrated classifier.

            The samples, as accepted by `estimator.predict`.

        Returns
        -------
            The predicted class.
        """
        return self.classes_[np.argmax(self.predict_proba(X), axis=1)]

        Returns
        -------
        """
        router = (
            .add(
                estimator=self._get_estimator(),
                method_mapping=MethodMapping().add(caller="fit", callee="fit"),
            )
            .add(
                splitter=self.cv,
                method_mapping=MethodMapping().add(caller="fit", callee="split"),
            )
        )

def _fit_classifier_calibrator_pair(
    estimator,
    X,
    y,
    train,
    test,
    method,
    classes,
    sample_weight=None,
    fit_params=None,
):
    """Fit a classifier/calibration pair on a given train/test split.

    Fit the classifier on the train set, compute its predictions on the test
    set and use the predictions as input to fit the calibrator along with the
    test labels.

        Cloned base estimator.

    X : array-like, shape (n_samples, n_features)
        Sample data.

    y : array-like, shape (n_samples,)
        Targets.

    train : ndarray, shape (n_train_indices,)
        Indices of the training subset.

    test : ndarray, shape (n_test_indices,)
        Indices of the testing subset.

    method : {'sigmoid', 'isotonic'}
        Method to use for calibration.

    classes : ndarray, shape (n_classes,)
        The target classes.

    sample_weight : array-like, default=None
        Sample weights for `X`.

    fit_params : dict, default=None
        Parameters to pass to the `fit` method of the underlying

    Returns
    -------
    calibrated_classifier : _CalibratedClassifier instance
    """
    fit_params_train = _check_method_params(X, params=fit_params, indices=train)
    X_train, y_train = _safe_indexing(X, train), _safe_indexing(y, train)
    X_test, y_test = _safe_indexing(X, test), _safe_indexing(y, test)

    estimator.fit(X_train, y_train, **fit_params_train)

    predictions, _ = _get_response_values(
        X_test,
        response_method=["decision_function", "predict_proba"],
    )
    if predictions.ndim == 1:
        # Reshape binary output from `(n_samples,)` to `(n_samples, 1)`
        predictions = predictions.reshape(-1, 1)

        # Check that the sample_weight dtype is consistent with the predictions
        # to avoid unintentional upcasts.
        sample_weight = _check_sample_weight(sample_weight, X, dtype=predictions.dtype)
        sw_test = _safe_indexing(sample_weight, test)
    else:
        sw_test = None
    calibrated_classifier = _fit_calibrator(
        estimator, predictions, y_test, classes, method, sample_weight=sw_test
    )
    return calibrated_classifier

def _fit_calibrator(clf, predictions, y, classes, method, sample_weight=None):
    """Fit calibrator(s) and return a `_CalibratedClassifier`
    instance.

    `n_classes` (i.e. `len(clf.classes_)`) calibrators are fitted.
    However, if `n_classes` equals 2, one calibrator is fitted.

    clf : estimator instance
        Fitted classifier.

    predictions : array-like, shape (n_samples, n_classes) or (n_samples, 1) \
                    when binary.
        Raw predictions returned by the un-calibrated base classifier.

        The targets.

        All the prediction classes.

        The method to use for calibration.

    sample_weight : ndarray, shape (n_samples,), default=None
        Sample weights. If None, then samples are equally weighted.

    Returns
    -------
    pipeline : _CalibratedClassifier instance
    """
    Y = label_binarize(y, classes=classes)
    label_encoder = LabelEncoder().fit(classes)
    pos_class_indices = label_encoder.transform(clf.classes_)
    calibrators = []
    for class_idx, this_pred in zip(pos_class_indices, predictions.T):
        if method == "isotonic":
            calibrator = IsotonicRegression(out_of_bounds="clip")
        else:  # "sigmoid"
            calibrator = _SigmoidCalibration()
        calibrator.fit(this_pred, Y[:, class_idx], sample_weight)
        calibrators.append(calibrator)

    pipeline = _CalibratedClassifier(clf, calibrators, method=method, classes=classes)
    return pipeline

class _CalibratedClassifier:
    """Pipeline-like chaining a fitted classifier and its fitted calibrators.

    calibrators : list of fitted estimator instances
        List of fitted calibrators (either 'IsotonicRegression' or
        '_SigmoidCalibration'). The number of calibrators equals the number of
        classes. However, if there are 2 classes, the list contains only one
        fitted calibrator.

    classes : array-like of shape (n_classes,)

        corresponds to Platt's method or 'isotonic' which is a
        non-parametric approach based on isotonic regression.
    """

    def __init__(self, estimator, calibrators, *, classes, method="sigmoid"):
        self.calibrators = calibrators
        self.classes = classes

        """Calculate calibrated probabilities.

        Calculates classification calibrated probabilities
        for each class, in a one-vs-all manner, for `X`.

        X : ndarray of shape (n_samples, n_features)
            The sample data.

        Returns
        -------
        proba : array, shape (n_samples, n_classes)
            The predicted probabilities. Can be exact zeros.
        """
        predictions, _ = _get_response_values(
            self.estimator,
            X,
            response_method=["decision_function", "predict_proba"],
        )
        if predictions.ndim == 1:
            # Reshape binary output from `(n_samples,)` to `(n_samples, 1)`
            predictions = predictions.reshape(-1, 1)

        n_classes = len(self.classes)

        label_encoder = LabelEncoder().fit(self.classes)
        pos_class_indices = label_encoder.transform(self.estimator.classes_)

        proba = np.zeros((_num_samples(X), n_classes))
        for class_idx, this_pred, calibrator in zip(
            pos_class_indices, predictions.T, self.calibrators
        ):
            if n_classes == 2:
                # When binary, `predictions` consists only of predictions for
                # clf.classes_[1] but `pos_class_indices` = 0
                class_idx += 1
            proba[:, class_idx] = calibrator.predict(this_pred)

        # Normalize the probabilities
        if n_classes == 2:
            proba[:, 0] = 1.0 - proba[:, 1]
        else:
            denominator = np.sum(proba, axis=1)[:, np.newaxis]
            # In the edge case where for each class calibrator returns a null
            # probability for a given sample, use the uniform distribution
            # instead.
            uniform_proba = np.full_like(proba, 1 / n_classes)
            proba = np.divide(
                proba, denominator, out=uniform_proba, where=denominator != 0
            )

        # Deal with cases where the predicted probability minimally exceeds 1.0
        proba[(1.0 < proba) & (proba <= 1.0 + 1e-5)] = 1.0

        return proba

def _sigmoid_calibration(
    predictions, y, sample_weight=None, max_abs_prediction_threshold=30
):
    """Probability Calibration with sigmoid method (Platt 2000)

    predictions : ndarray of shape (n_samples,)
        The decision function or predict proba for the samples.

    Returns
    -------
    a : float
        The slope.

    b : float
        The intercept.

    Platt, "Probabilistic Outputs for Support Vector Machines"
    """
    predictions = column_or_1d(predictions)
    y = column_or_1d(y)

    F = predictions  # F follows Platt's notations

    scale_constant = 1.0
    max_prediction = np.max(np.abs(F))

    # If the predictions have large values we scale them in order to bring
    # them within a suitable range. This has no effect on the final
    # (prediction) result because linear models like Logisitic Regression
    # without a penalty are invariant to multiplying the features by a
    # constant.
    if max_prediction >= max_abs_prediction_threshold:
        scale_constant = max_prediction
        # We rescale the features in a copy: inplace rescaling could confuse
        # the caller and make the code harder to reason about.
        F = F / scale_constant

    # Bayesian priors (see Platt end of section 2.2):
    # It corresponds to the number of samples, taking into account the
    # `sample_weight`.
    mask_negative_samples = y <= 0
        prior0 = (sample_weight[mask_negative_samples]).sum()
        prior1 = (sample_weight[~mask_negative_samples]).sum()
    else:
        prior0 = float(np.sum(mask_negative_samples))
        prior1 = y.shape[0] - prior0
    T = np.zeros_like(y, dtype=predictions.dtype)
    T[y > 0] = (prior1 + 1.0) / (prior1 + 2.0)
    T[y <= 0] = 1.0 / (prior0 + 2.0)

    bin_loss = HalfBinomialLoss()

    def loss_grad(AB):
        # .astype below is needed to ensure y_true and raw_prediction have the
        # same dtype. With result = np.float64(0) * np.array([1, 2], dtype=np.float32)
        # - in Numpy 2, result.dtype is float64
        # - in Numpy<2, result.dtype is float32
        raw_prediction = -(AB[0] * F + AB[1]).astype(dtype=predictions.dtype)
        l, g = bin_loss.loss_gradient(
            y_true=T,
            raw_prediction=raw_prediction,
            sample_weight=sample_weight,
        )
        loss = l.sum()
        # TODO: Remove casting to np.float64 when minimum supported SciPy is 1.11.2
        # With SciPy >= 1.11.2, the LBFGS implementation will cast to float64
        # https://github.com/scipy/scipy/pull/18825.
        # Here we cast to float64 to support SciPy < 1.11.2
        grad = np.asarray([-g @ F, -g.sum()], dtype=np.float64)
        return loss, grad

    AB0 = np.array([0.0, log((prior0 + 1.0) / (prior1 + 1.0))])

    opt_result = minimize(
        loss_grad,
        AB0,
        method="L-BFGS-B",
        jac=True,
        options={
            "gtol": 1e-6,
            "ftol": 64 * np.finfo(float).eps,
        },
    )
    AB_ = opt_result.x

    # The tuned multiplicative parameter is converted back to the original
    # input feature scale. The offset parameter does not need rescaling since
    # we did not rescale the outcome variable.
    return AB_[0] / scale_constant, AB_[1]

class _SigmoidCalibration(RegressorMixin, BaseEstimator):
    """Sigmoid regression model.

    a_ : float
        The slope.

    b_ : float
    """

        X : array-like of shape (n_samples,)

        Returns
        -------
        """
        X = column_or_1d(X)
        y = column_or_1d(y)

        self.a_, self.b_ = _sigmoid_calibration(X, y, sample_weight)

        T : array-like of shape (n_samples,)
            Data to predict from.

        Returns
        -------
        T_ : ndarray of shape (n_samples,)
            The predicted data.
        """
        T = column_or_1d(T)
        return expit(-(self.a_ * T + self.b_))

def calibration_curve(
    y_true,
    y_prob,
    *,
    pos_label=None,
    n_bins=5,
    strategy="uniform",
):
    """Compute true and predicted probabilities for a calibration curve.

    The method assumes the inputs come from a binary classifier, and
    discretize the [0, 1] interval into bins.

    Calibration curves may also be referred to as reliability diagrams.

    y_true : array-like of shape (n_samples,)
        True targets.

    y_prob : array-like of shape (n_samples,)
        Probabilities of the positive class.

    pos_label : int, float, bool or str, default=None
        The label of the positive class.

    n_bins : int, default=5
        Number of bins to discretize the [0, 1] interval. A bigger number
        requires more data. Bins with no samples (i.e. without
        corresponding values in `y_prob`) will not be returned, thus the
        returned arrays may have less than `n_bins` values.

    strategy : {'uniform', 'quantile'}, default='uniform'
        Strategy used to define the widths of the bins.

        uniform
            The bins have identical widths.
        quantile
            The bins have the same number of samples and depend on `y_prob`.

    Returns
    -------
    prob_true : ndarray of shape (n_bins,) or smaller
        The proportion of samples whose class is the positive class, in each
        bin (fraction of positives).

    prob_pred : ndarray of shape (n_bins,) or smaller
        The mean predicted probability in each bin.

    Alexandru Niculescu-Mizil and Rich Caruana (2005) Predicting Good
    Probabilities With Supervised Learning, in Proceedings of the 22nd
    International Conference on Machine Learning (ICML).
    See section 4 (Qualitative Analysis of Predictions).

    Examples
    --------
    >>> from sklearn.calibration import calibration_curve
    >>> y_true = np.array([0, 0, 0, 0, 1, 1, 1, 1, 1])
    >>> y_pred = np.array([0.1, 0.2, 0.3, 0.4, 0.65, 0.7, 0.8, 0.9,  1.])
    >>> prob_true, prob_pred = calibration_curve(y_true, y_pred, n_bins=3)
    >>> prob_true
    array([0. , 0.5, 1. ])
    >>> prob_pred
    array([0.2  , 0.525, 0.85 ])
    """
    y_true = column_or_1d(y_true)
    y_prob = column_or_1d(y_prob)
    check_consistent_length(y_true, y_prob)
    pos_label = _check_pos_label_consistency(pos_label, y_true)

    if y_prob.min() < 0 or y_prob.max() > 1:
        raise ValueError("y_prob has values outside [0, 1].")

    labels = np.unique(y_true)
    if len(labels) > 2:
            f"Only binary classification is supported. Provided labels {labels}."
        )
    y_true = y_true == pos_label

    if strategy == "quantile":  # Determine bin edges by distribution of data
        quantiles = np.linspace(0, 1, n_bins + 1)
        bins = np.percentile(y_prob, quantiles * 100)
    elif strategy == "uniform":
        bins = np.linspace(0.0, 1.0, n_bins + 1)
    else:
            "Invalid entry to 'strategy' input. Strategy "
            "must be either 'quantile' or 'uniform'."
        )

    binids = np.searchsorted(bins[1:-1], y_prob)

    bin_sums = np.bincount(binids, weights=y_prob, minlength=len(bins))
    bin_true = np.bincount(binids, weights=y_true, minlength=len(bins))
    bin_total = np.bincount(binids, minlength=len(bins))

    nonzero = bin_total != 0
    prob_true = bin_true[nonzero] / bin_total[nonzero]
    prob_pred = bin_sums[nonzero] / bin_total[nonzero]

    return prob_true, prob_pred

class CalibrationDisplay(_BinaryClassifierCurveDisplayMixin):
    """Calibration curve (also known as reliability diagram) visualization.

    It is recommended to use
    :func:`~sklearn.calibration.CalibrationDisplay.from_estimator` or
    :func:`~sklearn.calibration.CalibrationDisplay.from_predictions`
    to create a `CalibrationDisplay`. All parameters are stored as attributes.

    Read more about calibration in the :ref:`User Guide <calibration>` and
    more about the scikit-learn visualization API in :ref:`visualizations`.

    For an example on how to use the visualization, see
    :ref:`sphx_glr_auto_examples_calibration_plot_calibration_curve.py`.

    .. versionadded:: 1.0

    prob_true : ndarray of shape (n_bins,)
        The proportion of samples whose class is the positive class (fraction
        of positives), in each bin.

    prob_pred : ndarray of shape (n_bins,)

    y_prob : ndarray of shape (n_samples,)
        Probability estimates for the positive class, for each sample.

    estimator_name : str, default=None
        Name of estimator. If None, the estimator name is not shown.

        The positive class when computing the calibration curve.
        By default, `pos_label` is set to `estimators.classes_[1]` when using
        `from_estimator` and set to 1 when using `from_predictions`.

    line_ : matplotlib Artist
        Calibration curve.

    ax_ : matplotlib Axes
        Axes with calibration curve.

    figure_ : matplotlib Figure
        Figure containing the curve.

    See Also
    --------
    calibration_curve : Compute true and predicted probabilities for a
        calibration curve.
    CalibrationDisplay.from_predictions : Plot calibration curve using true
        and predicted labels.
    CalibrationDisplay.from_estimator : Plot calibration curve using an
        estimator and data.

    Examples
    --------
    >>> from sklearn.calibration import calibration_curve, CalibrationDisplay
    ...     X, y, random_state=0)
    >>> clf = LogisticRegression(random_state=0)
    >>> clf.fit(X_train, y_train)
    LogisticRegression(random_state=0)
    >>> y_prob = clf.predict_proba(X_test)[:, 1]
    >>> prob_true, prob_pred = calibration_curve(y_test, y_prob, n_bins=10)
    >>> disp = CalibrationDisplay(prob_true, prob_pred, y_prob)
    >>> disp.plot()
    <...>
    """

        self, prob_true, prob_pred, y_prob, *, estimator_name=None, pos_label=None
    ):
        self.prob_true = prob_true
        self.prob_pred = prob_pred
        self.y_prob = y_prob
        self.pos_label = pos_label

    def plot(self, *, ax=None, name=None, ref_line=True, **kwargs):
        """Plot visualization.

        Extra keyword arguments will be passed to
        :func:`matplotlib.pyplot.plot`.

        ax : Matplotlib Axes, default=None
            Axes object to plot on. If `None`, a new figure and axes is
            created.

        name : str, default=None
            Name for labeling curve. If `None`, use `estimator_name` if
            not `None`, otherwise no labeling is shown.

        ref_line : bool, default=True
            If `True`, plots a reference line representing a perfectly
            calibrated classifier.

            Keyword arguments to be passed to :func:`matplotlib.pyplot.plot`.

        Returns
        -------
        display : :class:`~sklearn.calibration.CalibrationDisplay`
            Object that stores computed values.
        """
        self.ax_, self.figure_, name = self._validate_plot_params(ax=ax, name=name)

        info_pos_label = (
            f"(Positive class: {self.pos_label})" if self.pos_label is not None else ""
        )

        default_line_kwargs = {"marker": "s", "linestyle": "-"}
        if name is not None:
            default_line_kwargs["label"] = name
        line_kwargs = _validate_style_kwargs(default_line_kwargs, kwargs)

        ref_line_label = "Perfectly calibrated"
        existing_ref_line = ref_line_label in self.ax_.get_legend_handles_labels()[1]
        if ref_line and not existing_ref_line:
            self.ax_.plot([0, 1], [0, 1], "k:", label=ref_line_label)
        self.line_ = self.ax_.plot(self.prob_pred, self.prob_true, **line_kwargs)[0]

        # We always have to show the legend for at least the reference line
        self.ax_.legend(loc="lower right")

        xlabel = f"Mean predicted probability {info_pos_label}"
        ylabel = f"Fraction of positives {info_pos_label}"
        self.ax_.set(xlabel=xlabel, ylabel=ylabel)

    def from_estimator(
        cls,
        X,
        y,
        *,
        n_bins=5,
        strategy="uniform",
        pos_label=None,
        name=None,
        ax=None,
        ref_line=True,
        **kwargs,
    ):
        """Plot calibration curve using a binary classifier and data.

        A calibration curve, also known as a reliability diagram, uses inputs
        from a binary classifier and plots the average predicted probability
        for each bin against the fraction of positive classes, on the
        y-axis.

        Read more about calibration in the :ref:`User Guide <calibration>` and
        more about the scikit-learn visualization API in :ref:`visualizations`.

        estimator : estimator instance
            Fitted classifier or a fitted :class:`~sklearn.pipeline.Pipeline`
            in which the last estimator is a classifier. The classifier must
            have a :term:`predict_proba` method.

            Input values.

            Binary target values.

        n_bins : int, default=5
            Number of bins to discretize the [0, 1] interval into when
            calculating the calibration curve. A bigger number requires more
            data.

        strategy : {'uniform', 'quantile'}, default='uniform'
            Strategy used to define the widths of the bins.

            - `'uniform'`: The bins have identical widths.
            - `'quantile'`: The bins have the same number of samples and depend
              on predicted probabilities.

        pos_label : int, float, bool or str, default=None
            The positive class when computing the calibration curve.
            By default, `estimators.classes_[1]` is considered as the
            positive class.

            .. versionadded:: 1.1

            Name for labeling curve. If `None`, the name of the estimator is
            used.

        ax : matplotlib axes, default=None
            created.

        Returns
        -------
        display : :class:`~sklearn.calibration.CalibrationDisplay`.

        See Also
        --------
        CalibrationDisplay.from_predictions : Plot calibration curve using true
            and predicted labels.

        Examples
        --------
        >>> import matplotlib.pyplot as plt
        >>> from sklearn.datasets import make_classification
        >>> from sklearn.model_selection import train_test_split
        >>> from sklearn.linear_model import LogisticRegression
        >>> from sklearn.calibration import CalibrationDisplay
        >>> X, y = make_classification(random_state=0)
        >>> X_train, X_test, y_train, y_test = train_test_split(
        ...     X, y, random_state=0)
        >>> clf = LogisticRegression(random_state=0)
        >>> clf.fit(X_train, y_train)
        LogisticRegression(random_state=0)
        >>> disp = CalibrationDisplay.from_estimator(clf, X_test, y_test)
        >>> plt.show()
        """
        y_prob, pos_label, name = cls._validate_and_get_response_values(
            X,
            y,
            response_method="predict_proba",
            pos_label=pos_label,
            name=name,
        )

        return cls.from_predictions(
            y,
            y_prob,
            n_bins=n_bins,
            strategy=strategy,
            ref_line=ref_line,
            ax=ax,
            **kwargs,
        )

    def from_predictions(
        cls,
        y_true,
        y_prob,
        *,
        n_bins=5,
        ax=None,
        **kwargs,
    ):
        """Plot calibration curve using true labels and predicted probabilities.

        Calibration curve, also known as reliability diagram, uses inputs
        y-axis.

        y_true : array-like of shape (n_samples,)
            True labels.

        y_prob : array-like of shape (n_samples,)
            The predicted probabilities of the positive class.

            data.

            By default `pos_label` is set to 1.

            Name for labeling curve.

            created.

        Returns
        -------

        See Also
        --------
        CalibrationDisplay.from_estimator : Plot calibration curve using an
            estimator and data.

        Examples
        --------
        >>> y_prob = clf.predict_proba(X_test)[:, 1]
        >>> disp = CalibrationDisplay.from_predictions(y_test, y_prob)
        """
        pos_label_validated, name = cls._validate_from_predictions_params(
            y_true, y_prob, sample_weight=None, pos_label=pos_label, name=name
        )

        prob_true, prob_pred = calibration_curve(
            y_true, y_prob, n_bins=n_bins, strategy=strategy, pos_label=pos_label
        )

        disp = cls(
            prob_true=prob_true,
            prob_pred=prob_pred,
            y_prob=y_prob,
            estimator_name=name,
            pos_label=pos_label_validated,
        )
        return disp.plot(ax=ax, ref_line=ref_line, **kwargs)

        self,
        *,
        cv=None,
    ):

        else:

        Returns
        -------
        """

            )

                X,
            )

                )

                y,
            )
        else:

                    self,
                    "fit",
                )
            else:
                    )

            else:
                )
                )

                        X,
                        y,
                    )
                )
            else:
                    X=X,
                    y=y,
                    cv=cv,
                )
                        )

                    )

                    y,
                )

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """
        router = (
            .add(
            )
            .add(
            )
        )

        Returns
        -------
        """
            X,
        )

        ):

        else:
            # instead.
            )

            y_true=T,
        )

        Returns
        -------
        """

        Returns
        -------
        """

    ):

            created.

        Returns
        -------
        """

        )

        cls,
        X,
        y,
        *,
        n_bins=5,
        ax=None,
        **kwargs,
    ):

        y-axis.

            data.

            used.

            created.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        """
            X,
            y,
        )

            y,
            y_prob,
            ax=ax,
            **kwargs,
        )

        cls,
        y_true,
        y_prob,
        *,
        n_bins=5,
        ax=None,
        **kwargs,
    ):

        y-axis.

            data.

            created.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        """
        )

        )

        disp = cls(
        )

def log_likelihood(emp_cov, precision):
    """Compute the sample mean of the log_likelihood under a covariance model.

    Computes the empirical expected log-likelihood, allowing for universal
    comparison (beyond this software package), and accounts for normalization
    terms and scaling.

    emp_cov : ndarray of shape (n_features, n_features)
        Maximum Likelihood Estimator of covariance.

    precision : ndarray of shape (n_features, n_features)
        The precision matrix of the covariance model to be tested.

    Returns
    -------
    log_likelihood_ : float
        Sample mean of the log-likelihood.
    """
    p = precision.shape[0]
    log_likelihood_ = -np.sum(emp_cov * precision) + fast_logdet(precision)
    log_likelihood_ -= p * np.log(2 * np.pi)
    log_likelihood_ /= 2.0
    return log_likelihood_

def empirical_covariance(X, *, assume_centered=False):
    """Compute the Maximum likelihood covariance estimator.

    X : ndarray of shape (n_samples, n_features)
        Data from which to compute the covariance estimate.

    assume_centered : bool, default=False
        If `True`, data will not be centered before computation.
        Useful when working with data whose mean is almost, but not exactly
        zero.
        If `False`, data will be centered before computation.

    Returns
    -------
    covariance : ndarray of shape (n_features, n_features)
        Empirical covariance (Maximum Likelihood Estimator).

    Examples
    --------
    >>> from sklearn.covariance import empirical_covariance
    >>> X = [[1,1,1],[1,1,1],[1,1,1],
    ...      [0,0,0],[0,0,0],[0,0,0]]
    >>> empirical_covariance(X)
    array([[0.25, 0.25, 0.25],
           [0.25, 0.25, 0.25],
           [0.25, 0.25, 0.25]])
    """
    X = check_array(X, ensure_2d=False, ensure_all_finite=False)

    if X.ndim == 1:
        X = np.reshape(X, (1, -1))

    if X.shape[0] == 1:
            "Only one sample available. You may want to reshape your data array"
        )

    if assume_centered:
        covariance = np.dot(X.T, X) / X.shape[0]
    else:
        covariance = np.cov(X.T, bias=1)

    if covariance.ndim == 0:
        covariance = np.array([[covariance]])
    return covariance

class EmpiricalCovariance(BaseEstimator):
    """Maximum likelihood covariance estimator.

    Read more in the :ref:`User Guide <covariance>`.

    store_precision : bool, default=True
        Specifies if the estimated precision is stored.

        If True, data are not centered before computation.
        zero.
        If False (default), data are centered before computation.

    location_ : ndarray of shape (n_features,)
        Estimated location, i.e. the estimated mean.

    covariance_ : ndarray of shape (n_features, n_features)
        Estimated covariance matrix

    precision_ : ndarray of shape (n_features, n_features)
        Estimated pseudo-inverse matrix.
        (stored only if store_precision is True)

    See Also
    --------
    EllipticEnvelope : An object for detecting outliers in
        a Gaussian distributed dataset.
    GraphicalLasso : Sparse inverse covariance estimation
        with an l1-penalized estimator.
    LedoitWolf : LedoitWolf Estimator.
    MinCovDet : Minimum Covariance Determinant
        (robust estimator of covariance).
    OAS : Oracle Approximating Shrinkage Estimator.
    ShrunkCovariance : Covariance estimator with shrinkage.

    Examples
    --------
    >>> from sklearn.covariance import EmpiricalCovariance
    >>> from sklearn.datasets import make_gaussian_quantiles
    >>> real_cov = np.array([[.8, .3],
    ...                      [.3, .4]])
    >>> X = rng.multivariate_normal(mean=[0, 0],
    ...                             cov=real_cov,
    ...                             size=500)
    >>> cov = EmpiricalCovariance().fit(X)
    >>> cov.covariance_
    array([[0.7569..., 0.2818...],
           [0.2818..., 0.3928...]])
    >>> cov.location_
    array([0.0622..., 0.0193...])
    """

    # X_test should have been called X
    __metadata_request__score = {"X_test": metadata_routing.UNUSED}

        "store_precision": ["boolean"],
        "assume_centered": ["boolean"],
    }

    def __init__(self, *, store_precision=True, assume_centered=False):
        self.store_precision = store_precision
        self.assume_centered = assume_centered

    def _set_covariance(self, covariance):
        """Saves the covariance and precision estimates

        Storage is done accordingly to `self.store_precision`.
        Precision stored only if invertible.

        covariance : array-like of shape (n_features, n_features)
            Estimated covariance matrix to be stored, and from which precision
            is computed.
        """
        covariance = check_array(covariance)
        # set covariance
        self.covariance_ = covariance
        # set precision
        if self.store_precision:
            self.precision_ = linalg.pinvh(covariance, check_finite=False)
        else:
            self.precision_ = None

    def get_precision(self):
        """Getter for the precision matrix.

        Returns
        -------
        precision_ : array-like of shape (n_features, n_features)
            The precision matrix associated to the current covariance object.
        """
            precision = self.precision_
        else:
            precision = linalg.pinvh(self.covariance_, check_finite=False)
        return precision

        """Fit the maximum likelihood covariance estimator to X.

          Training data, where `n_samples` is the number of samples and
          `n_features` is the number of features.

        y : Ignored

        Returns
        -------
        """
        if self.assume_centered:
            self.location_ = np.zeros(X.shape[1])
        else:
            self.location_ = X.mean(0)
        covariance = empirical_covariance(X, assume_centered=self.assume_centered)
        self._set_covariance(covariance)

    def score(self, X_test, y=None):
        """Compute the log-likelihood of `X_test` under the estimated Gaussian model.

        The Gaussian model is defined by its mean and covariance matrix which are
        represented respectively by `self.location_` and `self.covariance_`.

        X_test : array-like of shape (n_samples, n_features)
            Test data of which we compute the likelihood, where `n_samples` is
            the number of samples and `n_features` is the number of features.
            `X_test` is assumed to be drawn from the same distribution than
            the data used in fit (including centering).

        y : Ignored

        Returns
        -------
        res : float
            The log-likelihood of `X_test` with `self.location_` and `self.covariance_`
            as estimators of the Gaussian model mean and covariance matrix respectively.
        """
        X_test = validate_data(self, X_test, reset=False)
        # compute empirical covariance of the test set
        test_cov = empirical_covariance(X_test - self.location_, assume_centered=True)
        # compute log likelihood
        res = log_likelihood(test_cov, self.get_precision())

        return res

    def error_norm(self, comp_cov, norm="frobenius", scaling=True, squared=True):
        """Compute the Mean Squared Error between two covariance estimators.

        comp_cov : array-like of shape (n_features, n_features)
            The covariance to compare with.

        norm : {"frobenius", "spectral"}, default="frobenius"
            The type of norm used to compute the error. Available error types:
            - 'frobenius' (default): sqrt(tr(A^t.A))
            - 'spectral': sqrt(max(eigenvalues(A^t.A))
            where A is the error ``(comp_cov - self.covariance_)``.

        scaling : bool, default=True
            If True (default), the squared error norm is divided by n_features.
            If False, the squared error norm is not rescaled.

        squared : bool, default=True
            Whether to compute the squared error norm or the error norm.
            If True (default), the squared error norm is returned.
            If False, the error norm is returned.

        Returns
        -------
        result : float
            The Mean Squared Error (in the sense of the Frobenius norm) between
            `self` and `comp_cov` covariance estimators.
        """
        # compute the error
        error = comp_cov - self.covariance_
        # compute the error norm
        if norm == "frobenius":
            squared_norm = np.sum(error**2)
        elif norm == "spectral":
            squared_norm = np.amax(linalg.svdvals(np.dot(error.T, error)))
        else:
                "Only spectral and frobenius norms are implemented"
            )
        # optionally scale the error norm
        if scaling:
            squared_norm = squared_norm / error.shape[0]
        # finally get either the squared norm or the norm
        if squared:
            result = squared_norm
        else:
            result = np.sqrt(squared_norm)

        return result

    def mahalanobis(self, X):
        """Compute the squared Mahalanobis distances of given observations.

            The observations, the Mahalanobis distances of the which we
            compute. Observations are assumed to be drawn from the same
            distribution than the data used in fit.

        Returns
        -------
        dist : ndarray of shape (n_samples,)
            Squared Mahalanobis distances of the observations.
        """

        precision = self.get_precision()
        with config_context(assume_finite=True):
            # compute mahalanobis distances
            dist = pairwise_distances(
                X, self.location_[np.newaxis, :], metric="mahalanobis", VI=precision
            )

        return np.reshape(dist, (len(X),)) ** 2

        """
        else:

        Returns
        -------
        """
        else:

        y : Ignored

        Returns
        -------
        """
        else:

        y : Ignored

        Returns
        -------
        res : float
        """

        return res

        Returns
        -------
        """
        else:
            )
        else:

        Returns
        -------
        """

            )

def c_step(
    X,
    n_support,
    remaining_iterations=30,
    initial_estimates=None,
    verbose=False,
    cov_computation_method=empirical_covariance,
    random_state=None,
):
    """C_step procedure described in [Rouseeuw1984]_ aiming at computing MCD.

        Data set in which we look for the n_support observations whose
        scatter matrix has minimum determinant.

    n_support : int
        Number of observations to compute the robust estimates of location
        and covariance from. This parameter must be greater than
        `n_samples / 2`.

    remaining_iterations : int, default=30
        Number of iterations to perform.
        According to [Rouseeuw1999]_, two iterations are sufficient to get
        close to the minimum, and we never need more than 30 to reach
        convergence.

    initial_estimates : tuple of shape (2,), default=None
        Initial estimates of location and shape from which to run the c_step
        procedure:
        - initial_estimates[0]: an initial location estimate
        - initial_estimates[1]: an initial covariance estimate

        Verbose mode.

    cov_computation_method : callable, \
            default=:func:`sklearn.covariance.empirical_covariance`
        The function which will be used to compute the covariance.
        Must return array of shape (n_features, n_features).

        Determines the pseudo random number generator for shuffling the data.
        Pass an int for reproducible results across multiple function calls.

    Returns
    -------
    location : ndarray of shape (n_features,)
        Robust location estimates.

        Robust covariance estimates.

    support : ndarray of shape (n_samples,)
        A mask for the `n_support` observations whose scatter matrix has
        minimum determinant.

    .. [Rouseeuw1999] A Fast Algorithm for the Minimum Covariance Determinant
        Estimator, 1999, American Statistical Association and the American
        Society for Quality, TECHNOMETRICS
    """
    X = np.asarray(X)
    random_state = check_random_state(random_state)
    return _c_step(
        X,
        n_support,
        remaining_iterations=remaining_iterations,
        initial_estimates=initial_estimates,
        cov_computation_method=cov_computation_method,
        random_state=random_state,
    )

def _c_step(
    X,
    random_state,
):
    n_samples, n_features = X.shape
    dist = np.inf

    # Initialisation
    if initial_estimates is None:
        # compute initial robust estimates from a random subset
        support_indices = random_state.permutation(n_samples)[:n_support]
    else:
        # get initial robust estimates from the function parameters
        location = initial_estimates[0]
        covariance = initial_estimates[1]
        # run a special iteration for that case (to get an initial support_indices)
        precision = linalg.pinvh(covariance)
        X_centered = X - location
        dist = (np.dot(X_centered, precision) * X_centered).sum(1)
        # compute new estimates
        support_indices = np.argpartition(dist, n_support - 1)[:n_support]

    X_support = X[support_indices]
    location = X_support.mean(0)
    covariance = cov_computation_method(X_support)

    # Iterative procedure for Minimum Covariance Determinant computation
    det = fast_logdet(covariance)
    # If the data already has singular covariance, calculate the precision,
    # as the loop below will not be entered.
    if np.isinf(det):

    previous_det = np.inf
    while det < previous_det and remaining_iterations > 0 and not np.isinf(det):
        # save old estimates values
        previous_location = location
        previous_covariance = covariance
        previous_det = det
        previous_support_indices = support_indices
        # compute a new support_indices from the full data set mahalanobis distances
        dist = (np.dot(X_centered, precision) * X_centered).sum(axis=1)
        X_support = X[support_indices]
        location = X_support.mean(axis=0)
        covariance = cov_computation_method(X_support)
        det = fast_logdet(covariance)
        # update remaining iterations for early stopping
        remaining_iterations -= 1

    previous_dist = dist
    dist = (np.dot(X - location, precision) * (X - location)).sum(axis=1)
    # Check if best fit already found (det => 0, logdet => -inf)
        results = location, covariance, det, support_indices, dist
    # Check convergence
    if np.allclose(det, previous_det):
        # c_step procedure converged
        if verbose:
            print(
                "Optimal couple (location, covariance) found before"
                " ending iterations (%d left)" % (remaining_iterations)
            )
    elif det > previous_det:
        # determinant has increased (should not happen)
            "Determinant has increased; this should not happen: "
            "log(det) > log(previous_det) (%.15f > %.15f). "
            "You may want to try with a higher value of "
            "support_fraction (current value: %.3f)."
            % (det, previous_det, n_support / n_samples),
            RuntimeWarning,
        )
        results = (
            previous_location,
            previous_covariance,
            previous_det,
            previous_support_indices,
            previous_dist,
        )

    # Check early stopping
    if remaining_iterations == 0:
            print("Maximum number of iterations reached")

    location, covariance, det, support_indices, dist = results
    # Convert from list of indices to boolean mask.
    support = np.bincount(support_indices, minlength=n_samples).astype(bool)
    return location, covariance, det, support, dist

def select_candidates(
    X,
    n_trials,
    select=1,
    n_iter=30,
):
    """Finds the best pure subset of observations to compute MCD from it.

    The purpose of this function is to find the best sets of n_support
    observations with respect to a minimization of their covariance
    matrix determinant. Equivalently, it removes n_samples-n_support
    observations to construct what we call a pure data set (i.e. not
    containing outliers). The list of the observations of the pure
    data set is referred to as the `support`.

    Starting from a random support, the pure data set is found by the
    c_step procedure introduced by Rousseeuw and Van Driessen in
    [RV]_.

        Data (sub)set in which we look for the n_support purest observations.

        The number of samples the pure data set must contain.
        This parameter must be in the range `[(n + p + 1)/2] < n_support < n`.

    n_trials : int or tuple of shape (2,)
        Number of different initial sets of observations from which to
        run the algorithm. This parameter should be a strictly positive
        integer.
        Instead of giving a number of trials to perform, one can provide a
        list of initial estimates that will be used to iteratively run
        c_step procedures. In this case:
        - n_trials[0]: array-like, shape (n_trials, n_features)
          is the list of `n_trials` initial location estimates
        - n_trials[1]: array-like, shape (n_trials, n_features, n_features)
          is the list of `n_trials` initial covariances estimates

    select : int, default=1
        Number of best candidates results to return. This parameter must be
        a strictly positive integer.

    n_iter : int, default=30
        Maximum number of iterations for the c_step procedure.
        (2 is enough to be close to the final solution. "Never" exceeds 20).
        This parameter must be a strictly positive integer.

        Control the output verbosity.

        Must return an array of shape (n_features, n_features).

    See Also
    ---------
    c_step

    Returns
    -------
    best_locations : ndarray of shape (select, n_features)
        The `select` location estimates computed from the `select` best
        supports found in the data set (`X`).

    best_covariances : ndarray of shape (select, n_features, n_features)
        The `select` covariance estimates computed from the `select`
        best supports found in the data set (`X`).

    best_supports : ndarray of shape (select, n_samples)
        The `select` best supports found in the data set (`X`).

    .. [RV] A Fast Algorithm for the Minimum Covariance Determinant
    """

    if isinstance(n_trials, Integral):
        run_from_estimates = False
    elif isinstance(n_trials, tuple):
        run_from_estimates = True
        estimates_list = n_trials
        n_trials = estimates_list[0].shape[0]
    else:
        raise TypeError(
            "Invalid 'n_trials' parameter, expected tuple or  integer, got %s (%s)"
            % (n_trials, type(n_trials))
        )

    # compute `n_trials` location and shape estimates candidates in the subset
    all_estimates = []
    if not run_from_estimates:
        # perform `n_trials` computations from random initial supports
        for j in range(n_trials):
            all_estimates.append(
                _c_step(
                    X,
                    n_support,
                    remaining_iterations=n_iter,
                    verbose=verbose,
                    cov_computation_method=cov_computation_method,
                    random_state=random_state,
                )
            )
    else:
        # perform computations from every given initial estimates
            initial_estimates = (estimates_list[0][j], estimates_list[1][j])
                _c_step(
                    X,
                    initial_estimates=initial_estimates,
                )
            )
    all_locs_sub, all_covs_sub, all_dets_sub, all_supports_sub, all_ds_sub = zip(
        *all_estimates
    )
    # find the `n_best` best results among the `n_trials` ones
    index_best = np.argsort(all_dets_sub)[:select]
    best_locations = np.asarray(all_locs_sub)[index_best]
    best_covariances = np.asarray(all_covs_sub)[index_best]
    best_supports = np.asarray(all_supports_sub)[index_best]
    best_ds = np.asarray(all_ds_sub)[index_best]

    return best_locations, best_covariances, best_supports, best_ds

def fast_mcd(
    X,
    support_fraction=None,
):
    """Estimate the Minimum Covariance Determinant matrix.

    Read more in the :ref:`User Guide <robust_covariance>`.

        The data matrix, with p features and n samples.

    support_fraction : float, default=None
        The proportion of points to be included in the support of the raw
        MCD estimate. Default is `None`, which implies that the minimum
        value of `support_fraction` will be used within the algorithm:
        `(n_samples + n_features + 1) / 2 * n_samples`. This parameter must be
        in the range (0, 1).

    Returns
    -------
        Robust location of the data.

        Robust covariance of the features.

    support : ndarray of shape (n_samples,), dtype=bool
        A mask of the observations that have been used to compute
        the robust location and covariance estimates of the data set.

    Notes
    -----
    The FastMCD algorithm has been introduced by Rousseuw and Van Driessen
    in "A Fast Algorithm for the Minimum Covariance Determinant Estimator,
    1999, American Statistical Association and the American Society
    for Quality, TECHNOMETRICS".
    The principle is to compute robust estimates and random subsets before
    pooling them into a larger subsets, and finally into the full data set.
    Depending on the size of the initial sample, we have one, two or three
    such computation levels.

    Note that only raw estimates are returned. If one is interested in
    the correction and reweighting steps described in [RouseeuwVan]_,
    see the MinCovDet object.

    .. [RouseeuwVan] A Fast Algorithm for the Minimum Covariance
        Determinant Estimator, 1999, American Statistical Association
        and the American Society for Quality, TECHNOMETRICS

    .. [Butler1993] R. W. Butler, P. L. Davies and M. Jhun,
        Asymptotics For The Minimum Covariance Determinant Estimator,
        The Annals of Statistics, 1993, Vol. 21, No. 3, 1385-1400
    """

    X = check_array(X, ensure_min_samples=2, estimator="fast_mcd")

    # minimum breakdown value
    if support_fraction is None:
        n_support = min(int(np.ceil(0.5 * (n_samples + n_features + 1))), n_samples)
    else:
        n_support = int(support_fraction * n_samples)

    # 1-dimensional case quick computation
    # (Rousseeuw, P. J. and Leroy, A. M. (2005) References, in Robust
    #  Regression and Outlier Detection, John Wiley & Sons, chapter 4)
    if n_features == 1:
        if n_support < n_samples:
            # find the sample shortest halves
            X_sorted = np.sort(np.ravel(X))
            diff = X_sorted[n_support:] - X_sorted[: (n_samples - n_support)]
            halves_start = np.where(diff == np.min(diff))[0]
            # take the middle points' mean to get the robust location estimate
            location = (
                0.5
                * (X_sorted[n_support + halves_start] + X_sorted[halves_start]).mean()
            )
            support = np.zeros(n_samples, dtype=bool)
            X_centered = X - location
            support[np.argsort(np.abs(X_centered), 0)[:n_support]] = True
            covariance = np.asarray([[np.var(X[support])]])
            location = np.array([location])
            # get precision matrix in an optimized way
            precision = linalg.pinvh(covariance)
            dist = (np.dot(X_centered, precision) * (X_centered)).sum(axis=1)
        else:
            support = np.ones(n_samples, dtype=bool)
            covariance = np.asarray([[np.var(X)]])
            location = np.asarray([np.mean(X)])
    # Starting FastMCD algorithm for p-dimensional case
    if (n_samples > 500) and (n_features > 1):
        # 1. Find candidate supports on subsets
        # a. split the set in subsets of size ~ 300
        n_subsets = n_samples // 300
        n_samples_subsets = n_samples // n_subsets
        samples_shuffle = random_state.permutation(n_samples)
        h_subset = int(np.ceil(n_samples_subsets * (n_support / float(n_samples))))
        # b. perform a total of 500 trials
        n_trials_tot = 500
        # c. select 10 best (location, covariance) for each subset
        n_best_sub = 10
        n_trials = max(10, n_trials_tot // n_subsets)
        n_best_tot = n_subsets * n_best_sub
        all_best_locations = np.zeros((n_best_tot, n_features))
        try:
            all_best_covariances = np.zeros((n_best_tot, n_features, n_features))
        except MemoryError:
            # The above is too big. Let's try with something much small
            # (and less optimal)
            n_best_tot = 10
            n_best_sub = 2
        for i in range(n_subsets):
            low_bound = i * n_samples_subsets
            high_bound = low_bound + n_samples_subsets
            current_subset = X[samples_shuffle[low_bound:high_bound]]
            best_locations_sub, best_covariances_sub, _, _ = select_candidates(
                current_subset,
                h_subset,
                n_trials,
                select=n_best_sub,
                n_iter=2,
                cov_computation_method=cov_computation_method,
                random_state=random_state,
            )
            subset_slice = np.arange(i * n_best_sub, (i + 1) * n_best_sub)
            all_best_locations[subset_slice] = best_locations_sub
            all_best_covariances[subset_slice] = best_covariances_sub
        # 2. Pool the candidate supports into a merged set
        # (possibly the full dataset)
        n_samples_merged = min(1500, n_samples)
        h_merged = int(np.ceil(n_samples_merged * (n_support / float(n_samples))))
        if n_samples > 1500:
            n_best_merged = 10
        else:
            n_best_merged = 1
        # find the best couples (location, covariance) on the merged set
        selection = random_state.permutation(n_samples)[:n_samples_merged]
        locations_merged, covariances_merged, supports_merged, d = select_candidates(
            X[selection],
            h_merged,
            n_trials=(all_best_locations, all_best_covariances),
            select=n_best_merged,
            cov_computation_method=cov_computation_method,
        )
        # 3. Finally get the overall best (locations, covariance) couple
        if n_samples < 1500:
            # directly get the best couple (location, covariance)
            location = locations_merged[0]
            covariance = covariances_merged[0]
            dist = np.zeros(n_samples)
            support[selection] = supports_merged[0]
            dist[selection] = d[0]
        else:
            # select the best couple on the full dataset
            locations_full, covariances_full, supports_full, d = select_candidates(
                X,
                n_support,
                n_trials=(locations_merged, covariances_merged),
                select=1,
            )
            location = locations_full[0]
            covariance = covariances_full[0]
            support = supports_full[0]
            dist = d[0]
    elif n_features > 1:
        # 1. Find the 10 best couples (location, covariance)
        # considering two iterations
        n_trials = 30
        n_best = 10
        locations_best, covariances_best, _, _ = select_candidates(
            X,
            n_support,
            n_trials=n_trials,
            select=n_best,
            n_iter=2,
        )
        # 2. Select the best couple on the full dataset amongst the 10
        locations_full, covariances_full, supports_full, d = select_candidates(
            X,
            n_trials=(locations_best, covariances_best),
            select=1,
        )
        location = locations_full[0]
        covariance = covariances_full[0]
        support = supports_full[0]
        dist = d[0]

    return location, covariance, support, dist

class MinCovDet(EmpiricalCovariance):
    """Minimum Covariance Determinant (MCD): robust estimator of covariance.

    The Minimum Covariance Determinant covariance estimator is to be applied
    on Gaussian-distributed data, but could still be relevant on data
    drawn from a unimodal, symmetric distribution. It is not meant to be used
    with multi-modal data (the algorithm used to fit a MinCovDet object is
    likely to fail in such a case).
    One should consider projection pursuit methods to deal with multi-modal
    datasets.

        Specify if the estimated precision is stored.

        If True, the support of the robust location and the covariance
        estimates is computed, and a covariance estimate is recomputed from
        it, without centering the data.
        Useful to work with data whose mean is significantly equal to
        zero but is not exactly zero.
        If False, the robust location and covariance are directly computed
        with the FastMCD algorithm without additional treatment.

        MCD estimate. Default is None, which implies that the minimum
        value of support_fraction will be used within the algorithm:
        `(n_samples + n_features + 1) / 2 * n_samples`. The parameter must be
        in the range (0, 1].

    raw_location_ : ndarray of shape (n_features,)
        The raw robust estimated location before correction and re-weighting.

    raw_covariance_ : ndarray of shape (n_features, n_features)
        The raw robust estimated covariance before correction and re-weighting.

    raw_support_ : ndarray of shape (n_samples,)
        the raw robust estimates of location and shape, before correction
        and re-weighting.

        Estimated robust location.

        Estimated robust covariance matrix.

        Estimated pseudo inverse matrix.

    support_ : ndarray of shape (n_samples,)
        the robust estimates of location and shape.

    dist_ : ndarray of shape (n_samples,)
        Mahalanobis distances of the training set (on which :meth:`fit` is
        called) observations.

    See Also
    --------
    EmpiricalCovariance : Maximum likelihood covariance estimator.
    GraphicalLassoCV : Sparse inverse covariance with cross-validated
        choice of the l1 penalty.

    .. [Rouseeuw1984] P. J. Rousseeuw. Least median of squares regression.
        J. Am Stat Ass, 79:871, 1984.
    .. [Rousseeuw] A Fast Algorithm for the Minimum Covariance Determinant
    .. [ButlerDavies] R. W. Butler, P. L. Davies and M. Jhun,

    Examples
    --------
    >>> from sklearn.covariance import MinCovDet
    ...                                   cov=real_cov,
    ...                                   size=500)
    >>> cov = MinCovDet(random_state=0).fit(X)
    array([[0.7411..., 0.2535...],
           [0.2535..., 0.3053...]])
    array([0.0813... , 0.0427...])
    """

        **EmpiricalCovariance._parameter_constraints,
        "support_fraction": [Interval(Real, 0, 1, closed="right"), None],
    }
    _nonrobust_covariance = staticmethod(empirical_covariance)

        self,
        *,
        store_precision=True,
        assume_centered=False,
        support_fraction=None,
    ):
        self.support_fraction = support_fraction

        """Fit a Minimum Covariance Determinant with the FastMCD algorithm.

        y : Ignored

        Returns
        -------
        """
        X = validate_data(self, X, ensure_min_samples=2, estimator="MinCovDet")
        # check that the empirical covariance is full rank
        if (linalg.svdvals(np.dot(X.T, X)) > 1e-8).sum() != n_features:
                "The covariance matrix associated to your dataset is not full rank"
            )
        # compute and store raw estimates
        raw_location, raw_covariance, raw_support, raw_dist = fast_mcd(
            X,
            support_fraction=self.support_fraction,
            cov_computation_method=self._nonrobust_covariance,
        )
            raw_location = np.zeros(n_features)
            raw_covariance = self._nonrobust_covariance(
                X[raw_support], assume_centered=True
            )
            precision = linalg.pinvh(raw_covariance)
            raw_dist = np.sum(np.dot(X, precision) * X, 1)
        self.raw_location_ = raw_location
        self.raw_covariance_ = raw_covariance
        self.raw_support_ = raw_support
        self.location_ = raw_location
        self.support_ = raw_support
        self.dist_ = raw_dist
        # obtain consistency at normal models
        self.correct_covariance(X)
        # re-weight estimator
        self.reweight_covariance(X)

    def correct_covariance(self, data):
        """Apply a correction to raw Minimum Covariance Determinant estimates.

        Correction using the empirical correction factor suggested
        by Rousseeuw and Van Driessen in [RVD]_.

            The data matrix, with p features and n samples.
            The data set must be the one which was used to compute
            the raw estimates.

        Returns
        -------
        covariance_corrected : ndarray of shape (n_features, n_features)
            Corrected robust covariance estimate.

        .. [RVD] A Fast Algorithm for the Minimum Covariance
            Determinant Estimator, 1999, American Statistical Association
            and the American Society for Quality, TECHNOMETRICS
        """

        # Check that the covariance of the support data is not equal to 0.
        # Otherwise self.dist_ = 0 and thus correction = 0.
        n_samples = len(self.dist_)
        n_support = np.sum(self.support_)
        if n_support < n_samples and np.allclose(self.raw_covariance_, 0):
                "The covariance matrix of the support data "
                "is equal to 0, try to increase support_fraction"
            )
        correction = np.median(self.dist_) / chi2(data.shape[1]).isf(0.5)
        covariance_corrected = self.raw_covariance_ * correction
        self.dist_ /= correction
        return covariance_corrected

    def reweight_covariance(self, data):
        """Re-weight raw Minimum Covariance Determinant estimates.

        Re-weight observations using Rousseeuw's method (equivalent to
        deleting outlying observations from the data set before
        computing location and covariance estimates) described
        in [RVDriessen]_.

        Returns
        -------
        location_reweighted : ndarray of shape (n_features,)
            Re-weighted robust location estimate.

        covariance_reweighted : ndarray of shape (n_features, n_features)
            Re-weighted robust covariance estimate.

        support_reweighted : ndarray of shape (n_samples,), dtype=bool
            A mask of the observations that have been used to compute
            the re-weighted robust location and covariance estimates.

        .. [RVDriessen] A Fast Algorithm for the Minimum Covariance
        """
        n_samples, n_features = data.shape
        mask = self.dist_ < chi2(n_features).isf(0.025)
            location_reweighted = np.zeros(n_features)
        else:
            location_reweighted = data[mask].mean(0)
        covariance_reweighted = self._nonrobust_covariance(
            data[mask], assume_centered=self.assume_centered
        )
        support_reweighted = np.zeros(n_samples, dtype=bool)
        support_reweighted[mask] = True
        self._set_covariance(covariance_reweighted)
        self.location_ = location_reweighted
        self.support_ = support_reweighted
        X_centered = data - self.location_
        self.dist_ = np.sum(np.dot(X_centered, self.get_precision()) * X_centered, 1)
        return location_reweighted, covariance_reweighted, support_reweighted

        self,
        *,
    ):

        y : Ignored

        Returns
        -------
        """
            )
            X,
        )
            )

        Returns
        -------

        """

            )

        Returns
        -------

        """
        else:
        )

def _objective(mle, precision_, alpha):
    """Evaluation of the graphical-lasso objective function

    the objective function is made of a shifted scaled version of the
    normalized log-likelihood (i.e. its empirical mean over the samples) and a
    penalisation term to promote sparsity
    """
    p = precision_.shape[0]
    cost = -2.0 * log_likelihood(mle, precision_) + p * np.log(2 * np.pi)
    cost += alpha * (np.abs(precision_).sum() - np.abs(np.diag(precision_)).sum())
    return cost

def _dual_gap(emp_cov, precision_, alpha):
    """Expression of the dual gap convergence criterion

    The specific definition is given in Duchi "Projected Subgradient Methods
    for Learning Sparse Gaussians".
    """
    gap = np.sum(emp_cov * precision_)
    gap -= precision_.shape[0]
    gap += alpha * (np.abs(precision_).sum() - np.abs(np.diag(precision_)).sum())
    return gap

def _graphical_lasso(
    emp_cov,
    alpha,
    *,
    cov_init=None,
    mode="cd",
    tol=1e-4,
    enet_tol=1e-4,
    max_iter=100,
    eps=np.finfo(np.float64).eps,
):
    _, n_features = emp_cov.shape
    if alpha == 0:
        # Early return without regularization
        precision_ = linalg.inv(emp_cov)
        cost = -2.0 * log_likelihood(emp_cov, precision_)
        cost += n_features * np.log(2 * np.pi)
        d_gap = np.sum(emp_cov * precision_) - n_features
        return emp_cov, precision_, (cost, d_gap), 0

    if cov_init is None:
        covariance_ = emp_cov.copy()
    else:
        covariance_ = cov_init.copy()
    # As a trivial regularization (Tikhonov like), we scale down the
    # off-diagonal coefficients of our starting point: This is needed, as
    # in the cross-validation the cov_init can easily be
    # ill-conditioned, and the CV loop blows. Beside, this takes
    # conservative stand-point on the initial conditions, and it tends to
    # make the convergence go faster.
    covariance_ *= 0.95
    diagonal = emp_cov.flat[:: n_features + 1]
    covariance_.flat[:: n_features + 1] = diagonal
    precision_ = linalg.pinvh(covariance_)

    indices = np.arange(n_features)
    i = 0  # initialize the counter to be robust to `max_iter=0`
    costs = list()
    # The different l1 regression solver have different numerical errors
    if mode == "cd":
        errors = dict(over="raise", invalid="ignore")
    else:
        errors = dict(invalid="raise")
    try:
        # be robust to the max_iter=0 edge case, see:
        # https://github.com/scikit-learn/scikit-learn/issues/4134
        d_gap = np.inf
        # set a sub_covariance buffer
        sub_covariance = np.copy(covariance_[1:, 1:], order="C")
        for i in range(max_iter):
            for idx in range(n_features):
                # To keep the contiguous matrix `sub_covariance` equal to
                # covariance_[indices != idx].T[indices != idx]
                # we only need to update 1 column and 1 line when idx changes
                if idx > 0:
                    di = idx - 1
                    sub_covariance[di] = covariance_[di][indices != idx]
                    sub_covariance[:, di] = covariance_[:, di][indices != idx]
                else:
                    sub_covariance[:] = covariance_[1:, 1:]
                row = emp_cov[idx, indices != idx]
                with np.errstate(**errors):
                    if mode == "cd":
                        # Use coordinate descent
                        coefs = -(
                            precision_[indices != idx, idx]
                            / (precision_[idx, idx] + 1000 * eps)
                        )
                        coefs, _, _, _ = cd_fast.enet_coordinate_descent_gram(
                            coefs,
                            alpha,
                            0,
                            sub_covariance,
                            row,
                            row,
                            max_iter,
                            enet_tol,
                            check_random_state(None),
                            False,
                        )
                    else:  # mode == "lars"
                        _, _, coefs = lars_path_gram(
                            Xy=row,
                            Gram=sub_covariance,
                            n_samples=row.size,
                            alpha_min=alpha / (n_features - 1),
                            copy_Gram=True,
                            eps=eps,
                            method="lars",
                            return_path=False,
                        )
                # Update the precision matrix
                precision_[idx, idx] = 1.0 / (
                    covariance_[idx, idx]
                    - np.dot(covariance_[indices != idx, idx], coefs)
                )
                precision_[indices != idx, idx] = -precision_[idx, idx] * coefs
                precision_[idx, indices != idx] = -precision_[idx, idx] * coefs
                coefs = np.dot(sub_covariance, coefs)
                covariance_[idx, indices != idx] = coefs
                covariance_[indices != idx, idx] = coefs
            if not np.isfinite(precision_.sum()):
                raise FloatingPointError(
                    "The system is too ill-conditioned for this solver"
                )
            d_gap = _dual_gap(emp_cov, precision_, alpha)
            cost = _objective(emp_cov, precision_, alpha)
            if verbose:
                print(
                    "[graphical_lasso] Iteration % 3i, cost % 3.2e, dual gap %.3e"
                    % (i, cost, d_gap)
                )
            costs.append((cost, d_gap))
            if np.abs(d_gap) < tol:
                break
            if not np.isfinite(cost) and i > 0:
                    "Non SPD result: the system is too ill-conditioned for this solver"
                )
        else:
                "graphical_lasso: did not converge after %i iteration: dual gap: %.3e"
                % (max_iter, d_gap),
                ConvergenceWarning,
            )
    except FloatingPointError as e:
        e.args = (e.args[0] + ". The system is too ill-conditioned for this solver",)
        raise e

    return covariance_, precision_, costs, i + 1

def alpha_max(emp_cov):
    """Find the maximum alpha for which there are some non-zeros off-diagonal.

        The sample covariance matrix.

    Notes
    -----
    This results from the bound for the all the Lasso that are solved
    in GraphicalLasso: each time, the row of cov corresponds to Xy. As the
    bound for alpha is given by `max(abs(Xy))`, the result follows.
    """
    A = np.copy(emp_cov)
    A.flat[:: A.shape[0] + 1] = 0
    return np.max(np.abs(A))

def graphical_lasso(
    emp_cov,
    alpha,
    *,
    tol=1e-4,
    return_costs=False,
    return_n_iter=False,
):
    """L1-penalized covariance estimator.

    Read more in the :ref:`User Guide <sparse_inverse_covariance>`.

    .. versionchanged:: v0.20
        graph_lasso has been renamed to graphical_lasso

    emp_cov : array-like of shape (n_features, n_features)
        Empirical covariance from which to compute the covariance estimate.

    alpha : float
        The regularization parameter: the higher alpha, the more
        regularization, the sparser the inverse covariance.
        Range is (0, inf].

    mode : {'cd', 'lars'}, default='cd'
        The Lasso solver to use: coordinate descent or LARS. Use LARS for
        very sparse underlying graphs, where p > n. Elsewhere prefer cd
        which is more numerically stable.

    tol : float, default=1e-4
        The tolerance to declare convergence: if the dual gap goes below
        this value, iterations are stopped. Range is (0, inf].

    enet_tol : float, default=1e-4
        The tolerance for the elastic net solver used to calculate the descent
        direction. This parameter controls the accuracy of the search direction
        for a given column update, not of the overall parameter estimate. Only
        used for mode='cd'. Range is (0, inf].

    max_iter : int, default=100
        The maximum number of iterations.

        If verbose is True, the objective function and dual gap are
        printed at each iteration.

    return_costs : bool, default=False
        If return_costs is True, the objective function and dual gap
        at each iteration are returned.

    eps : float, default=eps
        The machine-precision regularization in the computation of the
        Cholesky diagonal factors. Increase this for very ill-conditioned
        systems. Default is `np.finfo(np.float64).eps`.

    return_n_iter : bool, default=False
        Whether or not to return the number of iterations.

    Returns
    -------
        The estimated covariance matrix.

        The estimated (sparse) precision matrix.

    costs : list of (objective, dual_gap) pairs
        The list of values of the objective function and the dual gap at
        each iteration. Returned only if return_costs is True.

    n_iter : int
        Number of iterations. Returned only if `return_n_iter` is set to True.

    See Also
    --------
    GraphicalLassoCV : Sparse inverse covariance with
        cross-validated choice of the l1 penalty.

    Notes
    -----
    The algorithm employed to solve this problem is the GLasso algorithm,
    from the Friedman 2008 Biostatistics paper. It is the same algorithm
    as in the R `glasso` package.

    One possible difference with the `glasso` R package is that the
    diagonal coefficients are not penalized.

    Examples
    --------
    >>> from sklearn.datasets import make_sparse_spd_matrix
    >>> from sklearn.covariance import empirical_covariance, graphical_lasso
    >>> true_cov = make_sparse_spd_matrix(n_dim=3,random_state=42)
    >>> X = rng.multivariate_normal(mean=np.zeros(3), cov=true_cov, size=3)
    >>> emp_cov = empirical_covariance(X, assume_centered=True)
    >>> emp_cov, _ = graphical_lasso(emp_cov, alpha=0.05)
    >>> emp_cov
    array([[ 1.68...,  0.21..., -0.20...],
           [ 0.21...,  0.22..., -0.08...],
           [-0.20..., -0.08...,  0.23...]])
    """
    model = GraphicalLasso(
        alpha=alpha,
        mode=mode,
        covariance="precomputed",
        tol=tol,
        enet_tol=enet_tol,
        max_iter=max_iter,
        eps=eps,
        assume_centered=True,
    ).fit(emp_cov)

    output = [model.covariance_, model.precision_]
    if return_costs:
        output.append(model.costs_)
    if return_n_iter:
        output.append(model.n_iter_)
    return tuple(output)

class BaseGraphicalLasso(EmpiricalCovariance):
        "tol": [Interval(Real, 0, None, closed="right")],
        "enet_tol": [Interval(Real, 0, None, closed="right")],
        "max_iter": [Interval(Integral, 0, None, closed="left")],
        "mode": [StrOptions({"cd", "lars"})],
        "eps": [Interval(Real, 0, None, closed="both")],
    }
    _parameter_constraints.pop("store_precision")

        self,
        tol=1e-4,
        enet_tol=1e-4,
        max_iter=100,
        mode="cd",
        eps=np.finfo(np.float64).eps,
    ):
        super().__init__(assume_centered=assume_centered)
        self.enet_tol = enet_tol
        self.max_iter = max_iter
        self.mode = mode

class GraphicalLasso(BaseGraphicalLasso):
    """Sparse inverse covariance estimation with an l1-penalized estimator.

    For a usage example see
    :ref:`sphx_glr_auto_examples_applications_plot_stock_market.py`.

        GraphLasso has been renamed to GraphicalLasso

    alpha : float, default=0.01

    covariance : "precomputed", default=None
        If covariance is "precomputed", the input data in `fit` is assumed
        to be the covariance matrix. If `None`, the empirical covariance
        is estimated from the data `X`.

        plotted at each iteration.

        zero.
        If False, data are centered before computation.

    n_iter_ : int
        Number of iterations run.

    costs_ : list of (objective, dual_gap) pairs

    See Also
    --------
    graphical_lasso : L1-penalized covariance estimator.

    Examples
    --------
    >>> from sklearn.covariance import GraphicalLasso
    >>> true_cov = np.array([[0.8, 0.0, 0.2, 0.0],
    ...                      [0.0, 0.4, 0.0, 0.0],
    ...                      [0.2, 0.0, 0.3, 0.1],
    ...                      [0.0, 0.0, 0.1, 0.7]])
    >>> np.random.seed(0)
    >>> X = np.random.multivariate_normal(mean=[0, 0, 0, 0],
    ...                                   cov=true_cov,
    ...                                   size=200)
    >>> cov = GraphicalLasso().fit(X)
    >>> np.around(cov.covariance_, decimals=3)
    array([[0.816, 0.049, 0.218, 0.019],
           [0.049, 0.364, 0.017, 0.034],
           [0.218, 0.017, 0.322, 0.093],
           [0.019, 0.034, 0.093, 0.69 ]])
    >>> np.around(cov.location_, decimals=3)
    array([0.073, 0.04 , 0.038, 0.143])
    """

        **BaseGraphicalLasso._parameter_constraints,
        "alpha": [Interval(Real, 0, None, closed="both")],
        "covariance": [StrOptions({"precomputed"}), None],
    }

        self,
        alpha=0.01,
        *,
        covariance=None,
        tol=1e-4,
    ):
            tol=tol,
            enet_tol=enet_tol,
            max_iter=max_iter,
            mode=mode,
            eps=eps,
            assume_centered=assume_centered,
        )
        self.covariance = covariance

        """Fit the GraphicalLasso model to X.

            Data from which to compute the covariance estimate.

        y : Ignored

        Returns
        -------
        """
        # Covariance does not make sense for a single feature
        X = validate_data(self, X, ensure_min_features=2, ensure_min_samples=2)

        if self.covariance == "precomputed":
            emp_cov = X.copy()
        else:
            emp_cov = empirical_covariance(X, assume_centered=self.assume_centered)
            if self.assume_centered:
                self.location_ = np.zeros(X.shape[1])
            else:
                self.location_ = X.mean(0)

        self.covariance_, self.precision_, self.costs_, self.n_iter_ = _graphical_lasso(
            emp_cov,
            alpha=self.alpha,
            cov_init=None,
            mode=self.mode,
            tol=self.tol,
            enet_tol=self.enet_tol,
            max_iter=self.max_iter,
            verbose=self.verbose,
            eps=self.eps,
        )

def graphical_lasso_path(
    X,
    alphas,
    X_test=None,
    tol=1e-4,
):
    """l1-penalized covariance estimator along a path of decreasing alphas

    alphas : array-like of shape (n_alphas,)
        The list of regularization parameters, decreasing order.

    cov_init : array of shape (n_features, n_features), default=None
        The initial guess for the covariance.

    X_test : array of shape (n_test_samples, n_features), default=None
        Optional test matrix to measure generalisation error.

        this value, iterations are stopped. The tolerance must be a positive
        number.

        used for mode='cd'. The tolerance must be a positive number.

        The maximum number of iterations. This parameter should be a strictly
        positive integer.

    verbose : int or bool, default=False
        The higher the verbosity flag, the more information is printed
        during the fitting.

    Returns
    -------
    covariances_ : list of shape (n_alphas,) of ndarray of shape \
            (n_features, n_features)
        The estimated covariance matrices.

    precisions_ : list of shape (n_alphas,) of ndarray of shape \
        The estimated (sparse) precision matrices.

    scores_ : list of shape (n_alphas,), dtype=float
        The generalisation error (log-likelihood) on the test data.
        Returned only if test data is passed.
    """
    inner_verbose = max(0, verbose - 1)
    emp_cov = empirical_covariance(X)
    else:
        covariance_ = cov_init
    covariances_ = list()
    precisions_ = list()
    scores_ = list()
    if X_test is not None:
        test_emp_cov = empirical_covariance(X_test)

    for alpha in alphas:
        try:
            # Capture the errors, and move on
            covariance_, precision_, _, _ = _graphical_lasso(
                emp_cov,
                alpha=alpha,
                cov_init=covariance_,
                mode=mode,
                tol=tol,
                enet_tol=enet_tol,
                max_iter=max_iter,
                verbose=inner_verbose,
                eps=eps,
            )
            covariances_.append(covariance_)
            precisions_.append(precision_)
            if X_test is not None:
                this_score = log_likelihood(test_emp_cov, precision_)
        except FloatingPointError:
            this_score = -np.inf
            covariances_.append(np.nan)
            precisions_.append(np.nan)
        if X_test is not None:
            if not np.isfinite(this_score):
                this_score = -np.inf
            scores_.append(this_score)
        if verbose == 1:
            sys.stderr.write(".")
        elif verbose > 1:
                print(
                    "[graphical_lasso_path] alpha: %.2e, score: %.2e"
                    % (alpha, this_score)
                )
            else:
                print("[graphical_lasso_path] alpha: %.2e" % alpha)
        return covariances_, precisions_, scores_
    return covariances_, precisions_

class GraphicalLassoCV(BaseGraphicalLasso):
    """Sparse inverse covariance w/ cross-validated choice of the l1 penalty.

    See glossary entry for :term:`cross-validation estimator`.

        GraphLassoCV has been renamed to GraphicalLassoCV

    alphas : int or array-like of shape (n_alphas,), dtype=float, default=4
        If an integer is given, it fixes the number of points on the
        grids of alpha to be used. If a list is given, it gives the
        grid to be used. See the notes in the class docstring for
        more details. Range is [1, inf) for an integer.
        Range is (0, inf] for an array-like of floats.

    n_refinements : int, default=4
        The number of times the grid is refined. Not used if explicit
        values of alphas are passed. Range is [1, inf).

    cv : int, cross-validation generator or iterable, default=None

        For integer/None inputs :class:`~sklearn.model_selection.KFold` is used.

        Refer :ref:`User Guide <cross_validation>` for the various

        Maximum number of iterations.

        very sparse underlying graphs, where number of features is greater
        than number of samples. Elsewhere prefer cd which is more numerically
        stable.

        If verbose is True, the objective function and duality gap are

        zero.

        Estimated precision matrix (inverse covariance).

    alpha_ : float
        Penalization parameter selected.

    cv_results_ : dict of ndarrays
        A dict with keys:

        alphas : ndarray of shape (n_alphas,)
            All penalization parameters explored.

        split(k)_test_score : ndarray of shape (n_alphas,)
            Log-likelihood score on left-out data across (k)th fold.

            .. versionadded:: 1.0

        mean_test_score : ndarray of shape (n_alphas,)
            Mean of scores over the folds.

        std_test_score : ndarray of shape (n_alphas,)
            Standard deviation of scores over the folds.

        Number of iterations run for the optimal alpha.

    See Also
    --------

    Notes
    -----
    The search for the optimal penalization parameter (`alpha`) is done on an
    iteratively refined grid: first the cross-validated scores on a grid are
    computed, then a new refined grid is centered around the maximum, and so
    on.

    One of the challenges which is faced here is that the solvers can
    fail to converge to a well-conditioned estimate. The corresponding
    values of `alpha` then come out as missing values, but the optimum may
    be close to these missing values.

    In `fit`, once the best parameter `alpha` is found through
    cross-validation, the model is fit again using the entire training set.

    Examples
    --------
    >>> from sklearn.covariance import GraphicalLassoCV
    >>> cov = GraphicalLassoCV().fit(X)
    array([[0.816, 0.051, 0.22 , 0.017],
           [0.051, 0.364, 0.018, 0.036],
           [0.22 , 0.018, 0.322, 0.094],
           [0.017, 0.036, 0.094, 0.69 ]])
    """

        "alphas": [Interval(Integral, 0, None, closed="left"), "array-like"],
        "n_refinements": [Interval(Integral, 1, None, closed="left")],
        "cv": ["cv_object"],
    }

        self,
        *,
        alphas=4,
        n_refinements=4,
        cv=None,
        tol=1e-4,
    ):
            tol=tol,
            eps=eps,
        )
        self.alphas = alphas
        self.n_refinements = n_refinements

        """Fit the GraphicalLasso covariance model to X.

        y : Ignored

            Parameters to be passed to the CV splitter and the
            cross_val_score function.

                Only available if `enable_metadata_routing=True`,
                which can be set by using
                ``sklearn.set_config(enable_metadata_routing=True)``.
                See :ref:`Metadata Routing User Guide <metadata_routing>` for
                more details.

        Returns
        -------
        """
        _raise_for_params(params, self, "fit")

        X = validate_data(self, X, ensure_min_features=2)
        else:
        emp_cov = empirical_covariance(X, assume_centered=self.assume_centered)

        cv = check_cv(self.cv, y, classifier=False)

        # List of (alpha, scores, covs)
        path = list()
        n_alphas = self.alphas
        inner_verbose = max(0, self.verbose - 1)

        if _is_arraylike_not_scalar(n_alphas):
            for alpha in self.alphas:
                check_scalar(
                    alpha,
                    "alpha",
                    Real,
                    min_val=0,
                    max_val=np.inf,
                    include_boundaries="right",
                )
            alphas = self.alphas
            n_refinements = 1
        else:
            n_refinements = self.n_refinements
            alpha_1 = alpha_max(emp_cov)
            alpha_0 = 1e-2 * alpha_1
            alphas = np.logspace(np.log10(alpha_0), np.log10(alpha_1), n_alphas)[::-1]

            routed_params = process_routing(self, "fit", **params)
        else:
            routed_params = Bunch(splitter=Bunch(split={}))

        t0 = time.time()
        for i in range(n_refinements):
                # No need to see the convergence warnings on this grid:
                # they will always be points that will not converge
                # during the cross-validation
                warnings.simplefilter("ignore", ConvergenceWarning)
                # Compute the cross-validated loss on the current grid

                # NOTE: Warm-restarting graphical_lasso_path has been tried,
                # and this did not allow to gain anything
                # (same execution time with or without).
                this_path = Parallel(n_jobs=self.n_jobs, verbose=self.verbose)(
                    delayed(graphical_lasso_path)(
                        X[train],
                        alphas=alphas,
                        X_test=X[test],
                        mode=self.mode,
                        tol=self.tol,
                        enet_tol=self.enet_tol,
                        max_iter=int(0.1 * self.max_iter),
                        verbose=inner_verbose,
                        eps=self.eps,
                    )
                )

            # Little danse to transform the list in what we need
            covs, _, scores = zip(*this_path)
            covs = zip(*covs)
            scores = zip(*scores)
            path.extend(zip(alphas, scores, covs))
            path = sorted(path, key=operator.itemgetter(0), reverse=True)

            # Find the maximum (avoid using built in 'max' function to
            # have a fully-reproducible selection of the smallest alpha
            # in case of equality)
            best_score = -np.inf
            last_finite_idx = 0
            for index, (alpha, scores, _) in enumerate(path):
                this_score = np.mean(scores)
                if this_score >= 0.1 / np.finfo(np.float64).eps:
                    this_score = np.nan
                if np.isfinite(this_score):
                    last_finite_idx = index
                if this_score >= best_score:
                    best_score = this_score
                    best_index = index

            # Refine the grid
            if best_index == 0:
                # We do not need to go back: we have chosen
                # the highest value of alpha for which there are
                # non-zero coefficients
                alpha_1 = path[0][0]
                alpha_0 = path[1][0]
            elif best_index == last_finite_idx and not best_index == len(path) - 1:
                # We have non-converged models on the upper bound of the
                # grid, we need to refine the grid there
                alpha_1 = path[best_index][0]
                alpha_0 = path[best_index + 1][0]
            elif best_index == len(path) - 1:
                alpha_0 = 0.01 * path[best_index][0]
            else:
                alpha_1 = path[best_index - 1][0]

            if not _is_arraylike_not_scalar(n_alphas):
                alphas = np.logspace(np.log10(alpha_1), np.log10(alpha_0), n_alphas + 2)
                alphas = alphas[1:-1]

            if self.verbose and n_refinements > 1:
                print(
                    "[GraphicalLassoCV] Done refinement % 2i out of %i: % 3is"
                    % (i + 1, n_refinements, time.time() - t0)
                )

        path = list(zip(*path))
        grid_scores = list(path[1])
        alphas = list(path[0])
        # Finally, compute the score with alpha = 0
        alphas.append(0)
        grid_scores.append(
            cross_val_score(
                EmpiricalCovariance(),
                X,
                cv=cv,
                n_jobs=self.n_jobs,
                params=params,
            )
        )
        grid_scores = np.array(grid_scores)

        self.cv_results_ = {"alphas": np.array(alphas)}

        for i in range(grid_scores.shape[1]):
            self.cv_results_[f"split{i}_test_score"] = grid_scores[:, i]

        self.cv_results_["mean_test_score"] = np.mean(grid_scores, axis=1)
        self.cv_results_["std_test_score"] = np.std(grid_scores, axis=1)

        best_alpha = alphas[best_index]
        self.alpha_ = best_alpha

        # Finally fit the model with the selected alpha
            emp_cov,
            alpha=best_alpha,
            verbose=inner_verbose,
        )

        Returns
        -------
        """
            splitter=check_cv(self.cv),
            method_mapping=MethodMapping().add(callee="split", caller="fit"),
        )

        self,
        tol=1e-4,
    ):

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
            eps=eps,
        )

        y : Ignored

        Returns
        -------
        """

        else:
            else:

            emp_cov,
        )

        self,
        *,
        alphas=4,
        cv=None,
        tol=1e-4,
    ):
            tol=tol,
            eps=eps,
        )

        y : Ignored

        Returns
        -------
        """

        else:

                    alpha,
                    "alpha",
                    Real,
                )
        else:

        else:

                        X[train],
                    )
                )

            else:

                print(
                )

                X,
                cv=cv,
            )
        )

            emp_cov,
        )

        Returns
        -------
        """
        )

def _ledoit_wolf(X, *, assume_centered, block_size):
    """Estimate the shrunk Ledoit-Wolf covariance matrix."""
    # for only one feature, the result is the same whatever the shrinkage
    if len(X.shape) == 2 and X.shape[1] == 1:
        if not assume_centered:
            X = X - X.mean()
        return np.atleast_2d((X**2).mean()), 0.0
    n_features = X.shape[1]

    # get Ledoit-Wolf shrinkage
    shrinkage = ledoit_wolf_shrinkage(
        X, assume_centered=assume_centered, block_size=block_size
    )
    emp_cov = empirical_covariance(X, assume_centered=assume_centered)
    mu = np.sum(np.trace(emp_cov)) / n_features
    shrunk_cov = (1.0 - shrinkage) * emp_cov
    shrunk_cov.flat[:: n_features + 1] += shrinkage * mu

    return shrunk_cov, shrinkage

def _oas(X, *, assume_centered=False):
    """Estimate covariance with the Oracle Approximating Shrinkage algorithm.

    The formulation is based on [1]_.
    [1] "Shrinkage algorithms for MMSE covariance estimation.",
        Chen, Y., Wiesel, A., Eldar, Y. C., & Hero, A. O.
        IEEE Transactions on Signal Processing, 58(10), 5016-5029, 2010.
        https://arxiv.org/pdf/0907.4698.pdf
    """
        # for only one feature, the result is the same whatever the shrinkage

    # The shrinkage is defined as:
    # shrinkage = min(
    # trace(S @ S.T) + trace(S)**2) / ((n + 1) (trace(S @ S.T) - trace(S)**2 / p), 1
    # )
    # where n and p are n_samples and n_features, respectively (cf. Eq. 23 in [1]).
    # The factor 2 / p is omitted since it does not impact the value of the estimator
    # for large p.

    # Instead of computing trace(S)**2, we can compute the average of the squared
    # elements of S that is equal to trace(S)**2 / p**2.
    # See the definition of the Frobenius norm:
    # https://en.wikipedia.org/wiki/Matrix_norm#Frobenius_norm
    alpha = np.mean(emp_cov**2)
    mu = np.trace(emp_cov) / n_features
    mu_squared = mu**2

    # The factor 1 / p**2 will cancel out since it is in both the numerator and
    # denominator
    num = alpha + mu_squared
    den = (n_samples + 1) * (alpha - mu_squared / n_features)
    shrinkage = 1.0 if den == 0 else min(num / den, 1.0)

    # The shrunk covariance is defined as:
    # (1 - shrinkage) * S + shrinkage * F (cf. Eq. 4 in [1])
    # where S is the empirical covariance and F is the shrinkage target defined as
    # F = trace(S) / n_features * np.identity(n_features) (cf. Eq. 3 in [1])

def shrunk_covariance(emp_cov, shrinkage=0.1):
    """Calculate covariance matrices shrunk on the diagonal.

    Read more in the :ref:`User Guide <shrunk_covariance>`.

    emp_cov : array-like of shape (..., n_features, n_features)
        Covariance matrices to be shrunk, at least 2D ndarray.

    shrinkage : float, default=0.1
        Coefficient in the convex combination used for the computation
        of the shrunk estimate. Range is [0, 1].

    Returns
    -------
    shrunk_cov : ndarray of shape (..., n_features, n_features)
        Shrunk covariance matrices.

    Notes
    -----
    The regularized (shrunk) covariance is given by::

        (1 - shrinkage) * cov + shrinkage * mu * np.identity(n_features)

    where `mu = trace(cov) / n_features`.

    Examples
    --------
    >>> from sklearn.covariance import empirical_covariance, shrunk_covariance
    >>> real_cov = np.array([[.8, .3], [.3, .4]])
    >>> X = rng.multivariate_normal(mean=[0, 0], cov=real_cov, size=500)
    >>> shrunk_covariance(empirical_covariance(X))
    array([[0.73..., 0.25...],
           [0.25..., 0.41...]])
    """
    emp_cov = check_array(emp_cov, allow_nd=True)
    n_features = emp_cov.shape[-1]

    mu = np.trace(emp_cov, axis1=-2, axis2=-1) / n_features
    mu = np.expand_dims(mu, axis=tuple(range(mu.ndim, emp_cov.ndim)))
    shrunk_cov += shrinkage * mu * np.eye(n_features)

    return shrunk_cov

class ShrunkCovariance(EmpiricalCovariance):
    """Covariance estimator with shrinkage.

        If True, data will not be centered before computation.
        zero.
        If False, data will be centered before computation.

    See Also
    --------

    Notes
    -----
    The regularized covariance is given by:

    (1 - shrinkage) * cov + shrinkage * mu * np.identity(n_features)

    where mu = trace(cov) / n_features

    Examples
    --------
    >>> from sklearn.covariance import ShrunkCovariance
    >>> cov = ShrunkCovariance().fit(X)
    array([[0.7387..., 0.2536...],
           [0.2536..., 0.4110...]])
    """

        "shrinkage": [Interval(Real, 0, 1, closed="both")],
    }

    def __init__(self, *, store_precision=True, assume_centered=False, shrinkage=0.1):
            store_precision=store_precision, assume_centered=assume_centered
        )

        """Fit the shrunk covariance model to X.

        y : Ignored

        Returns
        -------
        """
        # Not calling the parent object to fit, to avoid a potential
        # matrix inversion when setting the precision
        else:
        covariance = shrunk_covariance(covariance, self.shrinkage)

def ledoit_wolf_shrinkage(X, assume_centered=False, block_size=1000):
    """Estimate the shrunk Ledoit-Wolf covariance matrix.

        Data from which to compute the Ledoit-Wolf shrunk covariance shrinkage.

    block_size : int, default=1000
        Size of blocks into which the covariance matrix will be split.

    Returns
    -------
    shrinkage : float
        of the shrunk estimate.

    Notes
    -----
    The regularized (shrunk) covariance is:

    Examples
    --------
    >>> from sklearn.covariance import ledoit_wolf_shrinkage
    >>> real_cov = np.array([[.4, .2], [.2, .8]])
    >>> X = rng.multivariate_normal(mean=[0, 0], cov=real_cov, size=50)
    >>> shrinkage_coefficient = ledoit_wolf_shrinkage(X)
    >>> shrinkage_coefficient
    np.float64(0.23...)
    """
    X = check_array(X)
        return 0.0

        )

    # optionally center data
    if not assume_centered:
        X = X - X.mean(0)

    # A non-blocked version of the computation is present in the tests
    # in tests/test_covariance.py

    # number of blocks to split the covariance matrix into
    n_splits = int(n_features / block_size)
    X2 = X**2
    emp_cov_trace = np.sum(X2, axis=0) / n_samples
    mu = np.sum(emp_cov_trace) / n_features
    beta_ = 0.0  # sum of the coefficients of <X2.T, X2>
    delta_ = 0.0  # sum of the *squared* coefficients of <X.T, X>
    # starting block computation
    for i in range(n_splits):
        for j in range(n_splits):
            rows = slice(block_size * i, block_size * (i + 1))
            cols = slice(block_size * j, block_size * (j + 1))
            beta_ += np.sum(np.dot(X2.T[rows], X2[:, cols]))
            delta_ += np.sum(np.dot(X.T[rows], X[:, cols]) ** 2)
        rows = slice(block_size * i, block_size * (i + 1))
        beta_ += np.sum(np.dot(X2.T[rows], X2[:, block_size * n_splits :]))
        delta_ += np.sum(np.dot(X.T[rows], X[:, block_size * n_splits :]) ** 2)
    for j in range(n_splits):
        cols = slice(block_size * j, block_size * (j + 1))
        beta_ += np.sum(np.dot(X2.T[block_size * n_splits :], X2[:, cols]))
        delta_ += np.sum(np.dot(X.T[block_size * n_splits :], X[:, cols]) ** 2)
    delta_ += np.sum(
        np.dot(X.T[block_size * n_splits :], X[:, block_size * n_splits :]) ** 2
    )
    delta_ /= n_samples**2
    beta_ += np.sum(
        np.dot(X2.T[block_size * n_splits :], X2[:, block_size * n_splits :])
    )
    # use delta_ to compute beta
    beta = 1.0 / (n_features * n_samples) * (beta_ / n_samples - delta_)
    # delta is the sum of the squared coefficients of (<X.T,X> - mu*Id) / p
    delta = delta_ - 2.0 * mu * emp_cov_trace.sum() + n_features * mu**2
    delta /= n_features
    # get final beta as the min between beta and delta
    # We do this to prevent shrinking more than "1", which would invert
    # the value of covariances
    beta = min(beta, delta)
    # finally get shrinkage
    shrinkage = 0 if beta == 0 else beta / delta
    return shrinkage

def ledoit_wolf(X, *, assume_centered=False, block_size=1000):

        This is purely a memory optimization and does not affect results.

    Returns
    -------
    shrunk_cov : ndarray of shape (n_features, n_features)
        Shrunk covariance.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.covariance import empirical_covariance, ledoit_wolf
    >>> covariance, shrinkage = ledoit_wolf(X)
    >>> covariance
    array([[0.44..., 0.16...],
           [0.16..., 0.80...]])
    >>> shrinkage
    """
    estimator = LedoitWolf(
        assume_centered=assume_centered,
        block_size=block_size,
        store_precision=False,
    ).fit(X)

    return estimator.covariance_, estimator.shrinkage_

class LedoitWolf(EmpiricalCovariance):
    """LedoitWolf Estimator.

    Ledoit-Wolf is a particular form of shrinkage, where the shrinkage
    coefficient is computed using O. Ledoit and M. Wolf's formula as
    described in "A Well-Conditioned Estimator for Large-Dimensional
    Covariance Matrices", Ledoit and Wolf, Journal of Multivariate
    Analysis, Volume 88, Issue 2, February 2004, pages 365-411.

        zero.
        If False (default), data will be centered before computation.

        Size of blocks into which the covariance matrix will be split
        during its Ledoit-Wolf estimation. This is purely a memory
        optimization and does not affect results.

    shrinkage_ : float

    See Also
    --------

    Notes
    -----
    The regularised covariance is:

    and shrinkage is given by the Ledoit and Wolf formula (see References)

    "A Well-Conditioned Estimator for Large-Dimensional Covariance Matrices",
    Ledoit and Wolf, Journal of Multivariate Analysis, Volume 88, Issue 2,
    February 2004, pages 365-411.

    Examples
    --------
    >>> from sklearn.covariance import LedoitWolf
    >>> real_cov = np.array([[.4, .2],
    ...                      [.2, .8]])
    >>> X = np.random.multivariate_normal(mean=[0, 0],
    ...                                   size=50)
    >>> cov = LedoitWolf().fit(X)
    array([[0.4406..., 0.1616...],
           [0.1616..., 0.8022...]])
    array([ 0.0595... , -0.0075...])

    See also :ref:`sphx_glr_auto_examples_covariance_plot_covariance_estimation.py`
    and :ref:`sphx_glr_auto_examples_covariance_plot_lw_vs_oas.py`
    for more detailed examples.
    """

        "block_size": [Interval(Integral, 1, None, closed="left")],
    }

    def __init__(self, *, store_precision=True, assume_centered=False, block_size=1000):
        )
        self.block_size = block_size

        """Fit the Ledoit-Wolf shrunk covariance model to X.

        y : Ignored

        Returns
        -------
        """
        # Not calling the parent object to fit, to avoid computing the
        # covariance matrix (and potentially the precision)
        else:
        covariance, shrinkage = _ledoit_wolf(
            X - self.location_, assume_centered=True, block_size=self.block_size
        )
        self.shrinkage_ = shrinkage

def oas(X, *, assume_centered=False):
    """Estimate covariance with the Oracle Approximating Shrinkage.

      If True, data will not be centered before computation.
      Useful to work with data whose mean is significantly equal to
      zero but is not exactly zero.
      If False, data will be centered before computation.

    Returns
    -------
    shrunk_cov : array-like of shape (n_features, n_features)

    Notes
    -----

    (1 - shrinkage) * cov + shrinkage * mu * np.identity(n_features),

    where mu = trace(cov) / n_features and shrinkage is given by the OAS formula
    (see [1]_).

    The shrinkage formulation implemented here differs from Eq. 23 in [1]_. In
    the original article, formula (23) states that 2/p (p being the number of
    features) is multiplied by Trace(cov*cov) in both the numerator and
    denominator, but this operation is omitted because for a large p, the value
    of 2/p is so small that it doesn't affect the value of the estimator.

    .. [1] :arxiv:`"Shrinkage algorithms for MMSE covariance estimation.",
           Chen, Y., Wiesel, A., Eldar, Y. C., & Hero, A. O.
           IEEE Transactions on Signal Processing, 58(10), 5016-5029, 2010.
           <0907.4698>`

    Examples
    --------
    >>> from sklearn.covariance import oas
    >>> real_cov = [[.8, .3], [.3, .4]]
    >>> shrunk_cov, shrinkage = oas(X)
    >>> shrunk_cov
    array([[0.7533..., 0.2763...],
           [0.2763..., 0.3964...]])
    np.float64(0.0195...)
    """
    estimator = OAS(
    ).fit(X)

class OAS(EmpiricalCovariance):
    """Oracle Approximating Shrinkage Estimator.

        zero.

      coefficient in the convex combination used for the computation
      of the shrunk estimate. Range is [0, 1].

    See Also
    --------

    Notes
    -----

    Examples
    --------
    >>> from sklearn.covariance import OAS
    >>> oas = OAS().fit(X)
    >>> oas.covariance_
    >>> oas.precision_
    array([[ 1.7833..., -1.2431... ],
           [-1.2431...,  3.3889...]])
    >>> oas.shrinkage_

    """

        """Fit the Oracle Approximating Shrinkage covariance model to X.

        y : Ignored

        Returns
        -------
        """
        else:

        covariance, shrinkage = _oas(X - self.location_, assume_centered=True)

        )

        y : Ignored

        Returns
        -------
        """
        else:

        )

        y : Ignored

        Returns
        -------
        """
        else:
        )

        y : Ignored

        Returns
        -------
        """
        else:

class EllipticEnvelope(OutlierMixin, MinCovDet):
    """An object for detecting outliers in a Gaussian distributed dataset.

    Read more in the :ref:`User Guide <outlier_detection>`.

        If True, the support of robust location and covariance estimates
        is computed, and a covariance estimate is recomputed from it,
        without centering the data.

        MCD estimate. If None, the minimum value of support_fraction will
        be used within the algorithm: `(n_samples + n_features + 1) / 2 * n_samples`.
        Range is (0, 1).

    contamination : float, default=0.1
        The amount of contamination of the data set, i.e. the proportion
        of outliers in the data set. Range is (0, 0.5].

        Determines the pseudo random number generator for shuffling
        the data. Pass an int for reproducible results across multiple function
        calls. See :term:`Glossary <random_state>`.

        A mask of the observations that have been used to compute the
        robust estimates of location and shape.

    offset_ : float
        Offset used to define the decision function from the raw scores.
        We have the relation: ``decision_function = score_samples - offset_``.
        The offset depends on the contamination parameter and is defined in
        such a way we obtain the expected number of outliers (samples with
        decision function < 0) in training.

    See Also
    --------

    Notes
    -----
    Outlier detection from covariance estimation may break or not
    perform well in high-dimensional settings. In particular, one will
    always take care to work with ``n_samples > n_features ** 2``.

    .. [1] Rousseeuw, P.J., Van Driessen, K. "A fast algorithm for the
       minimum covariance determinant estimator" Technometrics 41(3), 212
       (1999)

    Examples
    --------
    >>> from sklearn.covariance import EllipticEnvelope
    >>> true_cov = np.array([[.8, .3],
    >>> X = np.random.RandomState(0).multivariate_normal(mean=[0, 0],
    ...                                                  cov=true_cov,
    ...                                                  size=500)
    >>> cov = EllipticEnvelope(random_state=0).fit(X)
    >>> # predict returns 1 for an inlier and -1 for an outlier
    >>> cov.predict([[0, 0],
    ...              [3, 3]])
    array([ 1, -1])
    """

        **MinCovDet._parameter_constraints,
        "contamination": [Interval(Real, 0, 0.5, closed="right")],
    }

        self,
        *,
        contamination=0.1,
    ):
            store_precision=store_precision,
            support_fraction=support_fraction,
        )
        self.contamination = contamination

        """Fit the EllipticEnvelope model.

        y : Ignored

        Returns
        -------
        """
        super().fit(X)
        self.offset_ = np.percentile(-self.dist_, 100.0 * self.contamination)

        """Compute the decision function of the given observations.

            The data matrix.

        Returns
        -------
        decision : ndarray of shape (n_samples,)
            Decision function of the samples.
            It is equal to the shifted Mahalanobis distances.
            The threshold for being an outlier is 0, which ensures a
            compatibility with other outlier detection algorithms.
        """
        negative_mahal_dist = self.score_samples(X)
        return negative_mahal_dist - self.offset_

        """Compute the negative Mahalanobis distances.

        Returns
        -------
        negative_mahal_distances : array-like of shape (n_samples,)
            Opposite of the Mahalanobis distances.
        """
        return -self.mahalanobis(X)

        """
        Predict labels (1 inlier, -1 outlier) of X according to fitted model.

        Returns
        -------
        is_inlier : ndarray of shape (n_samples,)
            Returns -1 for anomalies/outliers and +1 for inliers.
        """
        values = self.decision_function(X)
        is_inlier = np.full(values.shape[0], -1, dtype=int)
        is_inlier[values >= 0] = 1

        return is_inlier

        Returns
        -------
        """

        self,
        *,
    ):
        )

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        """

        Returns
        -------
        """

        Returns
        -------
        """

def test_mcd(global_random_seed):
    # Tests the FastMCD algorithm implementation
    # Small data set
    # test without outliers (random independent normal data)
    launch_mcd_on_dataset(100, 5, 0, 0.02, 0.1, 75, global_random_seed)
    # test with a contaminated data set (medium contamination)
    launch_mcd_on_dataset(100, 5, 20, 0.3, 0.3, 65, global_random_seed)
    # test with a contaminated data set (strong contamination)
    launch_mcd_on_dataset(100, 5, 40, 0.1, 0.1, 50, global_random_seed)

    # Medium data set
    launch_mcd_on_dataset(1000, 5, 450, 0.1, 0.1, 540, global_random_seed)

    # Large data set
    launch_mcd_on_dataset(1700, 5, 800, 0.1, 0.1, 870, global_random_seed)

    # 1D data set
    launch_mcd_on_dataset(500, 1, 100, 0.02, 0.02, 350, global_random_seed)

    # n_samples == n_features
    launch_mcd_on_dataset(20, 20, 0, 0.1, 0.1, 15, global_random_seed)

def test_fast_mcd_on_invalid_input():
    X = np.arange(100)
    msg = "Expected 2D array, got 1D array instead"
    with pytest.raises(ValueError, match=msg):
        fast_mcd(X)

def test_mcd_class_on_invalid_input():
    mcd = MinCovDet()
        mcd.fit(X)

def launch_mcd_on_dataset(
    n_samples, n_features, n_outliers, tol_loc, tol_cov, tol_support, seed
):
    rand_gen = np.random.RandomState(seed)
    data = rand_gen.randn(n_samples, n_features)
    # add some outliers
    outliers_index = rand_gen.permutation(n_samples)[:n_outliers]
    outliers_offset = 10.0 * (rand_gen.randint(2, size=(n_outliers, n_features)) - 0.5)
    data[outliers_index] += outliers_offset
    inliers_mask = np.ones(n_samples).astype(bool)
    inliers_mask[outliers_index] = False

    pure_data = data[inliers_mask]
    # compute MCD by fitting an object
    mcd_fit = MinCovDet(random_state=seed).fit(data)
    T = mcd_fit.location_
    S = mcd_fit.covariance_
    H = mcd_fit.support_
    # compare with the estimates learnt from the inliers
    error_location = np.mean((pure_data.mean(0) - T) ** 2)
    assert error_location < tol_loc
    error_cov = np.mean((empirical_covariance(pure_data) - S) ** 2)
    assert error_cov < tol_cov
    assert np.sum(H) >= tol_support
    assert_array_almost_equal(mcd_fit.mahalanobis(data), mcd_fit.dist_)

def test_mcd_issue1127():
    # Check that the code does not break with X.shape = (3, 1)
    # (i.e. n_support = n_samples)
    rnd = np.random.RandomState(0)
    X = rnd.normal(size=(3, 1))
    mcd.fit(X)

def test_mcd_issue3367(global_random_seed):
    # Check that MCD completes when the covariance matrix is singular
    # i.e. one of the rows and columns are all zeros
    rand_gen = np.random.RandomState(global_random_seed)

    # Think of these as the values for X and Y -> 10 values between -5 and 5
    data_values = np.linspace(-5, 5, 10).tolist()
    # Get the cartesian product of all possible coordinate pairs from above set
    data = np.array(list(itertools.product(data_values, data_values)))

    # Add a third column that's all zeros to make our data a set of point
    # within a plane, which means that the covariance matrix will be singular
    data = np.hstack((data, np.zeros((data.shape[0], 1))))

    # The below line of code should raise an exception if the covariance matrix
    # is singular. As a further test, since we have points in XYZ, the
    # principle components (Eigenvectors) of these directly relate to the
    # geometry of the points. Since it's a plane, we should be able to test
    # that the Eigenvector that corresponds to the smallest Eigenvalue is the
    # plane normal, specifically [0, 0, 1], since everything is in the XY plane
    # (as I've set it up above). To do this one would start by:
    #
    #     evals, evecs = np.linalg.eigh(mcd_fit.covariance_)
    #     normal = evecs[:, np.argmin(evals)]
    #
    # After which we need to assert that our `normal` is equal to [0, 0, 1].
    # Do note that there is floating point error associated with this, so it's
    # best to subtract the two and then compare some small tolerance (e.g.
    # 1e-12).
    MinCovDet(random_state=rand_gen).fit(data)

def test_mcd_support_covariance_is_zero():
    # Check that MCD returns a ValueError with informative message when the
    # covariance of the support data is equal to 0.
    X_1 = np.array([0.5, 0.1, 0.1, 0.1, 0.957, 0.1, 0.1, 0.1, 0.4285, 0.1])
    X_1 = X_1.reshape(-1, 1)
    X_2 = np.array([0.5, 0.3, 0.3, 0.3, 0.957, 0.3, 0.3, 0.3, 0.4285, 0.3])
    X_2 = X_2.reshape(-1, 1)
    msg = (
        "The covariance matrix of the support data is equal to 0, try to "
        "increase support_fraction"
    )
    for X in [X_1, X_2]:
        with pytest.raises(ValueError, match=msg):
            MinCovDet().fit(X)

def test_mcd_increasing_det_warning(global_random_seed):
    # Check that a warning is raised if we observe increasing determinants
    # during the c_step. In theory the sequence of determinants should be
    # decreasing. Increasing determinants are likely due to ill-conditioned
    # covariance matrices that result in poor precision matrices.

    X = [
        [5.1, 3.5, 1.4, 0.2],
        [4.9, 3.0, 1.4, 0.2],
        [4.7, 3.2, 1.3, 0.2],
        [4.6, 3.1, 1.5, 0.2],
        [5.0, 3.6, 1.4, 0.2],
        [4.6, 3.4, 1.4, 0.3],
        [5.0, 3.4, 1.5, 0.2],
        [4.4, 2.9, 1.4, 0.2],
        [4.9, 3.1, 1.5, 0.1],
        [5.4, 3.7, 1.5, 0.2],
        [4.8, 3.4, 1.6, 0.2],
        [4.8, 3.0, 1.4, 0.1],
        [4.3, 3.0, 1.1, 0.1],
        [5.1, 3.5, 1.4, 0.3],
        [5.7, 3.8, 1.7, 0.3],
        [5.4, 3.4, 1.7, 0.2],
        [4.6, 3.6, 1.0, 0.2],
        [5.0, 3.0, 1.6, 0.2],
        [5.2, 3.5, 1.5, 0.2],
    ]

    mcd = MinCovDet(support_fraction=0.5, random_state=global_random_seed)
    warn_msg = "Determinant has increased"
    with pytest.warns(RuntimeWarning, match=warn_msg):

def test_covariance():
    # Tests Covariance module on a simple dataset.
    # test covariance fit from data
    cov = EmpiricalCovariance()
    cov.fit(X)
    assert_array_almost_equal(emp_cov, cov.covariance_, 4)
    assert_almost_equal(cov.error_norm(emp_cov), 0)
    assert_almost_equal(cov.error_norm(emp_cov, norm="spectral"), 0)
    assert_almost_equal(cov.error_norm(emp_cov, norm="frobenius"), 0)
    assert_almost_equal(cov.error_norm(emp_cov, scaling=False), 0)
    assert_almost_equal(cov.error_norm(emp_cov, squared=False), 0)
    with pytest.raises(NotImplementedError):
        cov.error_norm(emp_cov, norm="foo")
    # Mahalanobis distances computation test
    mahal_dist = cov.mahalanobis(X)
    assert np.amin(mahal_dist) > 0

    # test with n_features = 1
    X_1d = X[:, 0].reshape((-1, 1))
    cov.fit(X_1d)
    assert_array_almost_equal(empirical_covariance(X_1d), cov.covariance_, 4)
    assert_almost_equal(cov.error_norm(empirical_covariance(X_1d)), 0)
    assert_almost_equal(cov.error_norm(empirical_covariance(X_1d), norm="spectral"), 0)

    # test with one sample
    # Create X with 1 sample and 5 features
    X_1sample = np.arange(5).reshape(1, 5)
    warn_msg = "Only one sample available. You may want to reshape your data array"
    with pytest.warns(UserWarning, match=warn_msg):
        cov.fit(X_1sample)

    assert_array_almost_equal(cov.covariance_, np.zeros(shape=(5, 5), dtype=np.float64))

    # test integer type
    X_integer = np.asarray([[0, 1], [1, 0]])
    result = np.asarray([[0.25, -0.25], [-0.25, 0.25]])
    assert_array_almost_equal(empirical_covariance(X_integer), result)

    # test centered case
    cov = EmpiricalCovariance(assume_centered=True)
    assert_array_equal(cov.location_, np.zeros(X.shape[1]))

def test_shrunk_covariance_func(n_matrices):
    """Check `shrunk_covariance` function."""

    n_features = 2
    cov = np.ones((n_features, n_features))
    cov_target = np.array([[1, 0.5], [0.5, 1]])

    if n_matrices > 1:
        cov = np.repeat(cov[np.newaxis, ...], n_matrices, axis=0)
        cov_target = np.repeat(cov_target[np.newaxis, ...], n_matrices, axis=0)

    cov_shrunk = shrunk_covariance(cov, 0.5)
    assert_allclose(cov_shrunk, cov_target)

def test_shrunk_covariance():
    """Check consistency between `ShrunkCovariance` and `shrunk_covariance`."""

    # Tests ShrunkCovariance module on a simple dataset.
    # compare shrunk covariance obtained from data and from MLE estimate
    cov = ShrunkCovariance(shrinkage=0.5)
    assert_array_almost_equal(
        shrunk_covariance(empirical_covariance(X), shrinkage=0.5), cov.covariance_, 4
    )

    # same test with shrinkage not provided
    cov = ShrunkCovariance()
        shrunk_covariance(empirical_covariance(X)), cov.covariance_, 4
    )

    # same test with shrinkage = 0 (<==> empirical_covariance)
    cov = ShrunkCovariance(shrinkage=0.0)
    assert_array_almost_equal(empirical_covariance(X), cov.covariance_, 4)

    cov = ShrunkCovariance(shrinkage=0.3)

    # test shrinkage coeff on a simple data set (without saving precision)
    cov = ShrunkCovariance(shrinkage=0.5, store_precision=False)
    assert cov.precision_ is None

def test_ledoit_wolf():
    # Tests LedoitWolf module on a simple dataset.
    # test shrinkage coeff on a simple data set
    X_centered = X - X.mean(axis=0)
    lw = LedoitWolf(assume_centered=True)
    lw.fit(X_centered)
    shrinkage_ = lw.shrinkage_

    score_ = lw.score(X_centered)
    assert_almost_equal(
        ledoit_wolf_shrinkage(X_centered, assume_centered=True), shrinkage_
    )
        ledoit_wolf_shrinkage(X_centered, assume_centered=True, block_size=6),
        shrinkage_,
    )
    lw_cov_from_mle, lw_shrinkage_from_mle = ledoit_wolf(
        X_centered, assume_centered=True
    )
    assert_array_almost_equal(lw_cov_from_mle, lw.covariance_, 4)
    assert_almost_equal(lw_shrinkage_from_mle, lw.shrinkage_)
    # compare estimates given by LW and ShrunkCovariance
    scov = ShrunkCovariance(shrinkage=lw.shrinkage_, assume_centered=True)
    scov.fit(X_centered)
    assert_array_almost_equal(scov.covariance_, lw.covariance_, 4)

    lw.fit(X_1d)
    lw_cov_from_mle, lw_shrinkage_from_mle = ledoit_wolf(X_1d, assume_centered=True)
    assert_array_almost_equal((X_1d**2).sum() / n_samples, lw.covariance_, 4)

    lw = LedoitWolf(store_precision=False, assume_centered=True)
    assert_almost_equal(lw.score(X_centered), score_, 4)
    assert lw.precision_ is None

    # Same tests without assuming centered data
    lw = LedoitWolf()
    lw.fit(X)
    assert_almost_equal(lw.shrinkage_, shrinkage_, 4)
    assert_almost_equal(lw.shrinkage_, ledoit_wolf_shrinkage(X))
    assert_almost_equal(lw.shrinkage_, ledoit_wolf(X)[1])
        lw.shrinkage_, _ledoit_wolf(X=X, assume_centered=False, block_size=10000)[1]
    )
    assert_almost_equal(lw.score(X), score_, 4)
    lw_cov_from_mle, lw_shrinkage_from_mle = ledoit_wolf(X)
    scov = ShrunkCovariance(shrinkage=lw.shrinkage_)
    scov.fit(X)

    assert_allclose(
        X_1d.var(ddof=0),
        _ledoit_wolf(X=X_1d, assume_centered=False, block_size=10000)[0],
    )
    lw_cov_from_mle, lw_shrinkage_from_mle = ledoit_wolf(X_1d)
    assert_array_almost_equal(empirical_covariance(X_1d), lw.covariance_, 4)

    # warning should be raised when using only 1 sample

        lw.fit(X_1sample)

    assert_array_almost_equal(lw.covariance_, np.zeros(shape=(5, 5), dtype=np.float64))

    lw = LedoitWolf(store_precision=False)
    lw.fit(X)

def _naive_ledoit_wolf_shrinkage(X):
    # A simple implementation of the formulas from Ledoit & Wolf

    # The computation below achieves the following computations of the
    # "O. Ledoit and M. Wolf, A Well-Conditioned Estimator for
    # Large-Dimensional Covariance Matrices"
    # beta and delta are given in the beginning of section 3.2
    emp_cov = empirical_covariance(X, assume_centered=False)
    delta_ = emp_cov.copy()
    delta_.flat[:: n_features + 1] -= mu
    delta = (delta_**2).sum() / n_features
    X2 = X**2
    beta_ = (
        1.0
        / (n_features * n_samples)
        * np.sum(np.dot(X2.T, X2) / n_samples - emp_cov**2)
    )

    beta = min(beta_, delta)
    shrinkage = beta / delta

def test_ledoit_wolf_small():
    # Compare our blocked implementation to the naive implementation
    X_small = X[:, :4]
    lw.fit(X_small)

    assert_almost_equal(shrinkage_, _naive_ledoit_wolf_shrinkage(X_small))

def test_ledoit_wolf_large():
    # test that ledoit_wolf doesn't error on data that is wider than block_size
    rng = np.random.RandomState(0)
    # use a number of features that is larger than the block-size
    X = rng.normal(size=(10, 20))
    lw = LedoitWolf(block_size=10).fit(X)
    # check that covariance is about diagonal (random normal noise)
    assert_almost_equal(lw.covariance_, np.eye(20), 0)
    cov = lw.covariance_

    # check that the result is consistent with not splitting data into blocks.
    lw = LedoitWolf(block_size=25).fit(X)
    assert_almost_equal(lw.covariance_, cov)

def test_ledoit_wolf_empty_array(ledoit_wolf_fitting_function):
    """Check that we validate X and raise proper error with 0-sample array."""
    X_empty = np.zeros((0, 2))
    with pytest.raises(ValueError, match="Found array with 0 sample"):
        ledoit_wolf_fitting_function(X_empty)

def test_oas():
    # Tests OAS module on a simple dataset.
    oa = OAS(assume_centered=True)
    oa.fit(X_centered)
    shrinkage_ = oa.shrinkage_
    score_ = oa.score(X_centered)
    oa_cov_from_mle, oa_shrinkage_from_mle = oas(X_centered, assume_centered=True)
    assert_array_almost_equal(oa_cov_from_mle, oa.covariance_, 4)
    assert_almost_equal(oa_shrinkage_from_mle, oa.shrinkage_)
    # compare estimates given by OAS and ShrunkCovariance
    scov = ShrunkCovariance(shrinkage=oa.shrinkage_, assume_centered=True)
    assert_array_almost_equal(scov.covariance_, oa.covariance_, 4)

    X_1d = X[:, 0:1]
    oa.fit(X_1d)
    oa_cov_from_mle, oa_shrinkage_from_mle = oas(X_1d, assume_centered=True)
    assert_array_almost_equal((X_1d**2).sum() / n_samples, oa.covariance_, 4)

    oa = OAS(store_precision=False, assume_centered=True)
    assert_almost_equal(oa.score(X_centered), score_, 4)
    assert oa.precision_ is None

    # Same tests without assuming centered data--------------------------------
    oa = OAS()
    oa.fit(X)
    assert_almost_equal(oa.shrinkage_, shrinkage_, 4)
    assert_almost_equal(oa.score(X), score_, 4)
    oa_cov_from_mle, oa_shrinkage_from_mle = oas(X)
    scov = ShrunkCovariance(shrinkage=oa.shrinkage_)

    oa = OAS()
    oa_cov_from_mle, oa_shrinkage_from_mle = oas(X_1d)
    assert_array_almost_equal(empirical_covariance(X_1d), oa.covariance_, 4)

    oa = OAS()
        oa.fit(X_1sample)

    assert_array_almost_equal(oa.covariance_, np.zeros(shape=(5, 5), dtype=np.float64))

    oa = OAS(store_precision=False)
    oa.fit(X)

    # test function _oas without assuming centered data
    X_1f = X[:, 0:1]
    oa = OAS()
    oa.fit(X_1f)
    _oa_cov_from_mle, _oa_shrinkage_from_mle = _oas(X_1f)
    assert_array_almost_equal(_oa_cov_from_mle, oa.covariance_, 4)
    assert_almost_equal(_oa_shrinkage_from_mle, oa.shrinkage_)
    assert_array_almost_equal((X_1f**2).sum() / n_samples, oa.covariance_, 4)

def test_EmpiricalCovariance_validates_mahalanobis():
    """Checks that EmpiricalCovariance validates data with mahalanobis."""
    cov = EmpiricalCovariance().fit(X)

    msg = f"X has 2 features, but \\w+ is expecting {X.shape[1]} features as input"
        cov.mahalanobis(X[:, :2])

def test_elliptic_envelope(global_random_seed):
    rnd = np.random.RandomState(global_random_seed)
    X = rnd.randn(100, 10)
    clf = EllipticEnvelope(contamination=0.1)
    with pytest.raises(NotFittedError):
        clf.predict(X)
        clf.decision_function(X)
    clf.fit(X)
    y_pred = clf.predict(X)
    scores = clf.score_samples(X)
    decisions = clf.decision_function(X)

    assert_array_almost_equal(scores, -clf.mahalanobis(X))
    assert_array_almost_equal(clf.mahalanobis(X), clf.dist_)
        clf.score(X, np.ones(100)), (100 - y_pred[y_pred == -1].size) / 100.0
    )
    assert sum(y_pred == -1) == sum(decisions < 0)

def test_score_samples():
    X_train = [[1, 1], [1, 2], [2, 1]]
    clf1 = EllipticEnvelope(contamination=0.2).fit(X_train)
    clf2 = EllipticEnvelope().fit(X_train)
    assert_array_equal(
        clf1.score_samples([[2.0, 2.0]]),
        clf1.decision_function([[2.0, 2.0]]) + clf1.offset_,
    )
        clf2.score_samples([[2.0, 2.0]]),
        clf2.decision_function([[2.0, 2.0]]) + clf2.offset_,
    )
        clf1.score_samples([[2.0, 2.0]]), clf2.score_samples([[2.0, 2.0]])
    )

def test_graphical_lassos(random_state=1):
    """Test the graphical lasso solvers.

    This checks is unstable for some random seeds where the covariance found with "cd"
    and "lars" solvers are different (4 cases / 100 tries).
    """
    # Sample data from a sparse multivariate normal
    dim = 20
    n_samples = 100
    prec = make_sparse_spd_matrix(dim, alpha=0.95, random_state=random_state)
    cov = linalg.inv(prec)
    X = random_state.multivariate_normal(np.zeros(dim), cov, size=n_samples)

    for alpha in (0.0, 0.1, 0.25):
        covs = dict()
        icovs = dict()
        for method in ("cd", "lars"):
            cov_, icov_, costs = graphical_lasso(
                emp_cov, return_costs=True, alpha=alpha, mode=method
            )
            covs[method] = cov_
            icovs[method] = icov_
            costs, dual_gap = np.array(costs).T
            # Check that the costs always decrease (doesn't hold if alpha == 0)
            if not alpha == 0:
                # use 1e-12 since the cost can be exactly 0
                assert_array_less(np.diff(costs), 1e-12)
        # Check that the 2 approaches give similar results
        assert_allclose(covs["cd"], covs["lars"], atol=5e-4)
        assert_allclose(icovs["cd"], icovs["lars"], atol=5e-4)

    # Smoke test the estimator
    model = GraphicalLasso(alpha=0.25).fit(X)
    model.score(X)
    assert_array_almost_equal(model.covariance_, covs["cd"], decimal=4)
    assert_array_almost_equal(model.covariance_, covs["lars"], decimal=4)

    # For a centered matrix, assume_centered could be chosen True or False
    # Check that this returns indeed the same result for centered data
    Z = X - X.mean(0)
    precs = list()
    for assume_centered in (False, True):
        prec_ = GraphicalLasso(assume_centered=assume_centered).fit(Z).precision_
        precs.append(prec_)
    assert_array_almost_equal(precs[0], precs[1])

def test_graphical_lasso_when_alpha_equals_0():
    """Test graphical_lasso's early return condition when alpha=0."""
    X = np.random.randn(100, 10)
    emp_cov = empirical_covariance(X, assume_centered=True)

    model = GraphicalLasso(alpha=0, covariance="precomputed").fit(emp_cov)
    assert_allclose(model.precision_, np.linalg.inv(emp_cov))

    _, precision = graphical_lasso(emp_cov, alpha=0)
    assert_allclose(precision, np.linalg.inv(emp_cov))

def test_graphical_lasso_n_iter(mode):
    X, _ = datasets.make_classification(n_samples=5_000, n_features=20, random_state=0)

    _, _, n_iter = graphical_lasso(
        emp_cov, 0.2, mode=mode, max_iter=2, return_n_iter=True
    )
    assert n_iter == 2

def test_graphical_lasso_iris():
    # Hard-coded solution from R glasso package for alpha=1.0
    # (need to set penalize.diagonal to FALSE)
    cov_R = np.array(
        [
            [0.68112222, 0.0000000, 0.265820, 0.02464314],
            [0.00000000, 0.1887129, 0.000000, 0.00000000],
            [0.26582000, 0.0000000, 3.095503, 0.28697200],
            [0.02464314, 0.0000000, 0.286972, 0.57713289],
        ]
    )
    icov_R = np.array(
        [
            [1.5190747, 0.000000, -0.1304475, 0.0000000],
            [0.0000000, 5.299055, 0.0000000, 0.0000000],
            [-0.1304475, 0.000000, 0.3498624, -0.1683946],
            [0.0000000, 0.000000, -0.1683946, 1.8164353],
        ]
    )
    X = datasets.load_iris().data
    for method in ("cd", "lars"):
        cov, icov = graphical_lasso(emp_cov, alpha=1.0, return_costs=False, mode=method)
        assert_array_almost_equal(cov, cov_R)
        assert_array_almost_equal(icov, icov_R)

def test_graph_lasso_2D():
    # Hard-coded solution from Python skggm package
    # obtained by calling `quic(emp_cov, lam=.1, tol=1e-8)`
    cov_skggm = np.array([[3.09550269, 1.186972], [1.186972, 0.57713289]])

    icov_skggm = np.array([[1.52836773, -3.14334831], [-3.14334831, 8.19753385]])
    X = datasets.load_iris().data[:, 2:]
        cov, icov = graphical_lasso(emp_cov, alpha=0.1, return_costs=False, mode=method)
        assert_array_almost_equal(cov, cov_skggm)
        assert_array_almost_equal(icov, icov_skggm)

def test_graphical_lasso_iris_singular():
    # Small subset of rows to test the rank-deficient case
    # Need to choose samples such that none of the variances are zero
    indices = np.arange(10, 13)

    # Hard-coded solution from R glasso package for alpha=0.01
        [
            [0.08, 0.056666662595, 0.00229729713223, 0.00153153142149],
            [0.056666662595, 0.082222222222, 0.00333333333333, 0.00222222222222],
            [0.002297297132, 0.003333333333, 0.00666666666667, 0.00009009009009],
            [0.001531531421, 0.002222222222, 0.00009009009009, 0.00222222222222],
        ]
    )
        [
            [24.42244057, -16.831679593, 0.0, 0.0],
            [-16.83168201, 24.351841681, -6.206896552, -12.5],
            [0.0, -6.206896171, 153.103448276, 0.0],
            [0.0, -12.499999143, 0.0, 462.5],
        ]
    )
    X = datasets.load_iris().data[indices, :]
        cov, icov = graphical_lasso(
            emp_cov, alpha=0.01, return_costs=False, mode=method
        )
        assert_array_almost_equal(cov, cov_R, decimal=5)
        assert_array_almost_equal(icov, icov_R, decimal=5)

def test_graphical_lasso_cv(random_state=1):
    dim = 5
    n_samples = 6
    prec = make_sparse_spd_matrix(dim, alpha=0.96, random_state=random_state)
    # Capture stdout, to smoke test the verbose mode
    orig_stdout = sys.stdout
    try:
        sys.stdout = StringIO()
        # We need verbose very high so that Parallel prints on stdout
        GraphicalLassoCV(verbose=100, alphas=5, tol=1e-1).fit(X)
    finally:
        sys.stdout = orig_stdout

def test_graphical_lasso_cv_alphas_iterable(alphas_container_type):
    """Check that we can pass an array-like to `alphas`.

    Non-regression test for:
    https://github.com/scikit-learn/scikit-learn/issues/22489
    """
    true_cov = np.array(
        [
            [0.8, 0.0, 0.2, 0.0],
            [0.0, 0.4, 0.0, 0.0],
            [0.2, 0.0, 0.3, 0.1],
            [0.0, 0.0, 0.1, 0.7],
        ]
    )
    X = rng.multivariate_normal(mean=[0, 0, 0, 0], cov=true_cov, size=200)
    alphas = _convert_container([0.02, 0.03], alphas_container_type)
    GraphicalLassoCV(alphas=alphas, tol=1e-1, n_jobs=1).fit(X)

def test_graphical_lasso_cv_alphas_invalid_array(alphas, err_type, err_msg):
    """Check that if an array-like containing a value
    outside of (0, inf] is passed to `alphas`, a ValueError is raised.
    Check if a string is passed, a TypeError is raised.
    """
        [
        ]
    )

    with pytest.raises(err_type, match=err_msg):
        GraphicalLassoCV(alphas=alphas, tol=1e-1, n_jobs=1).fit(X)

def test_graphical_lasso_cv_scores():
    splits = 4
    n_alphas = 5
    n_refinements = 3
        [
        ]
    )
    cov = GraphicalLassoCV(cv=splits, alphas=n_alphas, n_refinements=n_refinements).fit(
        X
    )

    _assert_graphical_lasso_cv_scores(
        cov=cov,
        n_splits=splits,
        n_refinements=n_refinements,
        n_alphas=n_alphas,
    )

def test_graphical_lasso_cv_scores_with_routing(global_random_seed):
    """Check that `GraphicalLassoCV` internally dispatches metadata to
    the splitter.
    """
    splits = 5
        [
        ]
    )
    rng = np.random.RandomState(global_random_seed)
    X = rng.multivariate_normal(mean=[0, 0, 0, 0], cov=true_cov, size=300)
    n_samples = X.shape[0]
    groups = rng.randint(0, 5, n_samples)
    params = {"groups": groups}
    cv = GroupKFold(n_splits=splits)
    cv.set_split_request(groups=True)

    cov = GraphicalLassoCV(cv=cv, alphas=n_alphas, n_refinements=n_refinements).fit(
        X, **params
    )

        cov=cov,
    )

def _assert_graphical_lasso_cv_scores(cov, n_splits, n_refinements, n_alphas):
    cv_results = cov.cv_results_
    # alpha and one for each split

    total_alphas = n_refinements * n_alphas + 1
    keys = ["alphas"]
    split_keys = [f"split{i}_test_score" for i in range(n_splits)]
    for key in keys + split_keys:
        assert key in cv_results
        assert len(cv_results[key]) == total_alphas

    cv_scores = np.asarray([cov.cv_results_[key] for key in split_keys])
    expected_mean = cv_scores.mean(axis=0)
    expected_std = cv_scores.std(axis=0)

    assert_allclose(cov.cv_results_["mean_test_score"], expected_mean)
    assert_allclose(cov.cv_results_["std_test_score"], expected_std)

class BaseWeightBoosting(BaseEnsemble, metaclass=ABCMeta):
    """Base class for AdaBoost estimators.

    Warning: This class should not be used directly. Use derived classes
    instead.
    """

        "estimator": [HasMethods(["fit", "predict"]), None],
        "n_estimators": [Interval(Integral, 1, None, closed="left")],
        "learning_rate": [Interval(Real, 0, None, closed="neither")],
    }

        self,
        *,
        n_estimators=50,
        estimator_params=tuple(),
        learning_rate=1.0,
    ):
            estimator=estimator,
            n_estimators=n_estimators,
            estimator_params=estimator_params,
        )

        self.learning_rate = learning_rate

        # Only called to validate X in non-fit methods, therefore reset=False
        return validate_data(
            self,
            X,
            ensure_2d=True,
            allow_nd=True,
        )

        # AdaBoost*.estimator is not validated yet
    )
        """Build a boosted classifier/regressor from the training set (X, y).

            The training input samples. Sparse matrix can be CSC, CSR, COO,
            DOK, or LIL. COO, DOK, and LIL are converted to CSR.

            Sample weights. If None, the sample weights are initialized to
            1 / n_samples.

        Returns
        -------
        """
        _raise_for_unsupported_routing(self, "fit", sample_weight=sample_weight)
            self,
            X,
            y,
            y_numeric=is_regressor(self),
        )

        sample_weight = _check_sample_weight(
            sample_weight, X, dtype=np.float64, copy=True, ensure_non_negative=True
        )
        sample_weight /= sample_weight.sum()

        # Check parameters
        self._validate_estimator()

        # Clear any previous fit results
        self.estimators_ = []
        self.estimator_weights_ = np.zeros(self.n_estimators, dtype=np.float64)
        self.estimator_errors_ = np.ones(self.n_estimators, dtype=np.float64)

        # Initialization of the random number instance that will be used to
        # generate a seed at each iteration
        epsilon = np.finfo(sample_weight.dtype).eps

        zero_weight_mask = sample_weight == 0.0
        for iboost in range(self.n_estimators):
            # avoid extremely small sample weight, for details see issue #20320
            sample_weight = np.clip(sample_weight, a_min=epsilon, a_max=None)
            # do not clip sample weights that were exactly zero originally
            sample_weight[zero_weight_mask] = 0.0

            # Boosting step
            sample_weight, estimator_weight, estimator_error = self._boost(
                iboost, X, y, sample_weight, random_state
            )

            # Early termination
                break
            self.estimator_weights_[iboost] = estimator_weight
            self.estimator_errors_[iboost] = estimator_error

            # Stop if error is zero
            if estimator_error == 0:
                break

            sample_weight_sum = np.sum(sample_weight)

            if not np.isfinite(sample_weight_sum):
                    (
                        "Sample weights have reached infinite values,"
                        f" at iteration {iboost}, causing overflow. "
                        "Iterations stopped. Try lowering the learning rate."
                    ),
                    stacklevel=2,
                )
                break

            # Stop if the sum of sample weights has become non-positive
            if sample_weight_sum <= 0:
                break

            if iboost < self.n_estimators - 1:
                # Normalize
                sample_weight /= sample_weight_sum

    def _boost(self, iboost, X, y, sample_weight, random_state):
        """Implement a single boost.

        Warning: This method needs to be overridden by subclasses.

        iboost : int
            The index of the current boost iteration.

            The target values (class labels).

        sample_weight : array-like of shape (n_samples,)
            The current sample weights.

        random_state : RandomState
            The current random number generator

        Returns
        -------
        sample_weight : array-like of shape (n_samples,) or None
            The reweighted sample weights.
            If None then boosting has terminated early.

        estimator_weight : float
            The weight for the current boost.

        error : float
            The classification error for the current boost.
        """
        pass

    def staged_score(self, X, y, sample_weight=None):
        """Return staged scores for X, y.

        This generator method yields the ensemble score after each iteration of
        boosting and therefore allows monitoring, such as to determine the
        score on a test set after each boost.

            Labels for X.

        Yields
        ------
        z : float
        """

        for y_pred in self.staged_predict(X):
            if is_classifier(self):
                yield accuracy_score(y, y_pred, sample_weight=sample_weight)
            else:
                yield r2_score(y, y_pred, sample_weight=sample_weight)

    @property
    def feature_importances_(self):
        """The impurity-based feature importances.

        The higher, the more important the feature.
        The importance of a feature is computed as the (normalized)
        total reduction of the criterion brought by that feature.  It is also
        known as the Gini importance.

        Warning: impurity-based feature importances can be misleading for
        high cardinality features (many unique values). See
        :func:`sklearn.inspection.permutation_importance` as an alternative.

        Returns
        -------
        feature_importances_ : ndarray of shape (n_features,)
            The feature importances.
        """
        if self.estimators_ is None or len(self.estimators_) == 0:
                "Estimator not fitted, call `fit` before `feature_importances_`."
            )

        try:
            norm = self.estimator_weights_.sum()
            return (
                sum(
                    weight * clf.feature_importances_
                    for weight, clf in zip(self.estimator_weights_, self.estimators_)
                )
                / norm
            )

        except AttributeError as e:
                "Unable to compute feature importances "
                "since estimator does not have a "
                "feature_importances_ attribute"
            ) from e

def _samme_proba(estimator, n_classes, X):
    """Calculate algorithm 4, step 2, equation c) of Zhu et al [1].

    .. [1] J. Zhu, H. Zou, S. Rosset, T. Hastie, "Multi-class AdaBoost", 2009.

    """
    proba = estimator.predict_proba(X)

    # Displace zero probabilities so the log is defined.
    # Also fix negative elements which may occur with
    # negative sample weights.
    np.clip(proba, np.finfo(proba.dtype).eps, None, out=proba)
    log_proba = np.log(proba)

    return (n_classes - 1) * (
        log_proba - (1.0 / n_classes) * log_proba.sum(axis=1)[:, np.newaxis]
    )

class AdaBoostClassifier(
    _RoutingNotSupportedMixin, ClassifierMixin, BaseWeightBoosting
):
    """An AdaBoost classifier.

    An AdaBoost [1]_ classifier is a meta-estimator that begins by fitting a
    classifier on the original dataset and then fits additional copies of the
    classifier on the same dataset but where the weights of incorrectly
    classified instances are adjusted such that subsequent classifiers focus
    more on difficult cases.

    This class implements the algorithm based on [2]_.

    Read more in the :ref:`User Guide <adaboost>`.

    .. versionadded:: 0.14

    estimator : object, default=None
        The base estimator from which the boosted ensemble is built.
        Support for sample weighting is required, as well as proper
        ``classes_`` and ``n_classes_`` attributes. If ``None``, then
        the base estimator is :class:`~sklearn.tree.DecisionTreeClassifier`
        initialized with `max_depth=1`.

           `base_estimator` was renamed to `estimator`.

    n_estimators : int, default=50
        The maximum number of estimators at which boosting is terminated.
        In case of perfect fit, the learning procedure is stopped early.
        Values must be in the range `[1, inf)`.

    learning_rate : float, default=1.0
        Weight applied to each classifier at each boosting iteration. A higher
        learning rate increases the contribution of each classifier. There is
        a trade-off between the `learning_rate` and `n_estimators` parameters.
        Values must be in the range `(0.0, inf)`.

    algorithm : {'SAMME'}, default='SAMME'
        Use the SAMME discrete boosting algorithm.

        .. deprecated:: 1.6
            `algorithm` is deprecated and will be removed in version 1.8. This
            estimator only implements the 'SAMME' algorithm.

        Controls the random seed given at each `estimator` at each
        boosting iteration.
        Thus, it is only used when `estimator` exposes a `random_state`.

    estimator_ : estimator
        The base estimator from which the ensemble is grown.

           `base_estimator_` was renamed to `estimator_`.

    estimators_ : list of classifiers
        The collection of fitted sub-estimators.

        The classes labels.

        The number of classes.

    estimator_weights_ : ndarray of floats
        Weights for each estimator in the boosted ensemble.

    estimator_errors_ : ndarray of floats
        Classification error for each estimator in the boosted
        ensemble.

    feature_importances_ : ndarray of shape (n_features,)
        The impurity-based feature importances if supported by the
        ``estimator`` (when based on decision trees).

    See Also
    --------
    AdaBoostRegressor : An AdaBoost regressor that begins by fitting a
        regressor on the original dataset and then fits additional copies of
        the regressor on the same dataset but where the weights of instances
        are adjusted according to the error of the current prediction.

    GradientBoostingClassifier : GB builds an additive model in a forward
        stage-wise fashion. Regression trees are fit on the negative gradient
        of the binomial or multinomial deviance loss function. Binary
        classification is a special case where only a single regression tree is
        induced.

    sklearn.tree.DecisionTreeClassifier : A non-parametric supervised learning
        method used for classification.
        Creates a model that predicts the value of a target variable by
        learning simple decision rules inferred from the data features.

    .. [1] Y. Freund, R. Schapire, "A Decision-Theoretic Generalization of
           on-Line Learning and an Application to Boosting", 1995.

    .. [2] :doi:`J. Zhu, H. Zou, S. Rosset, T. Hastie, "Multi-class adaboost."
           Statistics and its Interface 2.3 (2009): 349-360.
           <10.4310/SII.2009.v2.n3.a8>`

    Examples
    --------
    >>> from sklearn.ensemble import AdaBoostClassifier
    >>> X, y = make_classification(n_samples=1000, n_features=4,
    >>> clf = AdaBoostClassifier(n_estimators=100, random_state=0)
    AdaBoostClassifier(n_estimators=100, random_state=0)
    >>> clf.score(X, y)
    0.96...

    For a detailed example of using AdaBoost to fit a sequence of DecisionTrees
    as weaklearners, please refer to
    :ref:`sphx_glr_auto_examples_ensemble_plot_adaboost_multiclass.py`.

    For a detailed example of using AdaBoost to fit a non-linearly separable
    classification dataset composed of two Gaussian quantiles clusters, please
    refer to :ref:`sphx_glr_auto_examples_ensemble_plot_adaboost_twoclass.py`.
    """

    # TODO(1.8): remove "algorithm" entry
        **BaseWeightBoosting._parameter_constraints,
        "algorithm": [StrOptions({"SAMME"}), Hidden(StrOptions({"deprecated"}))],
    }

        self,
        *,
        algorithm="deprecated",
    ):
            learning_rate=learning_rate,
        )

        self.algorithm = algorithm

    def _validate_estimator(self):
        """Check the estimator and set the estimator_ attribute."""
        super()._validate_estimator(default=DecisionTreeClassifier(max_depth=1))

        if self.algorithm != "deprecated":
                "The parameter 'algorithm' is deprecated in 1.6 and has no effect. "
                "It will be removed in version 1.8.",
                FutureWarning,
            )

        if not has_fit_parameter(self.estimator_, "sample_weight"):
                f"{self.estimator.__class__.__name__} doesn't support sample_weight."
            )

        Perform a single boost according to the discrete SAMME algorithm and return the
        updated sample weights.

            The training input samples.

        random_state : RandomState instance
            The RandomState instance used if the base estimator accepts a
            `random_state` attribute.

        Returns
        -------

        estimator_error : float
        """
        estimator = self._make_estimator(random_state=random_state)

        estimator.fit(X, y, sample_weight=sample_weight)

        y_predict = estimator.predict(X)

        if iboost == 0:
            self.classes_ = getattr(estimator, "classes_", None)
            self.n_classes_ = len(self.classes_)

        # Instances incorrectly classified
        incorrect = y_predict != y

        # Error fraction
        estimator_error = np.mean(np.average(incorrect, weights=sample_weight, axis=0))

        # Stop if classification is perfect
        if estimator_error <= 0:
            return sample_weight, 1.0, 0.0

        n_classes = self.n_classes_

        # Stop if the error is at least as bad as random guessing
        if estimator_error >= 1.0 - (1.0 / n_classes):
            self.estimators_.pop(-1)
            if len(self.estimators_) == 0:
                    "BaseClassifier in AdaBoostClassifier "
                    "ensemble is worse than random, ensemble "
                    "can not be fit."
                )
            return None, None, None

        # Boost weight using multi-class AdaBoost SAMME alg
        estimator_weight = self.learning_rate * (
            np.log((1.0 - estimator_error) / estimator_error) + np.log(n_classes - 1.0)
        )

        # Only boost the weights if it will fit again
        if not iboost == self.n_estimators - 1:
            # Only boost positive weights
            sample_weight = np.exp(
                np.log(sample_weight)
                + estimator_weight * incorrect * (sample_weight > 0)
            )

        return sample_weight, estimator_weight, estimator_error

        """Predict classes for X.

        The predicted class of an input sample is computed as the weighted mean
        prediction of the classifiers in the ensemble.

        Returns
        -------
            The predicted classes.
        """
        pred = self.decision_function(X)

            return self.classes_.take(pred > 0, axis=0)

        return self.classes_.take(np.argmax(pred, axis=1), axis=0)

    def staged_predict(self, X):
        """Return staged predictions for X.

        This generator method yields the ensemble prediction after each
        iteration of boosting and therefore allows monitoring, such as to
        determine the prediction on a test set after each boost.

            The input samples. Sparse matrix can be CSC, CSR, COO,

        Yields
        ------
        y : generator of ndarray of shape (n_samples,)
        """

            for pred in self.staged_decision_function(X):
                yield np.array(classes.take(pred > 0, axis=0))

        else:
                yield np.array(classes.take(np.argmax(pred, axis=1), axis=0))

        """Compute the decision function of ``X``.

        Returns
        -------
        score : ndarray of shape of (n_samples, k)
            The decision function of the input samples. The order of
            outputs is the same as that of the :term:`classes_` attribute.
            Binary classification is a special cases with ``k == 1``,
            otherwise ``k==n_classes``. For binary classification,
            values closer to -1 or 1 mean more like the first or second
            class in ``classes_``, respectively.
        """

        classes = self.classes_[:, np.newaxis]

        if n_classes == 1:
            return np.zeros_like(X, shape=(X.shape[0], 1))

        pred = sum(
            np.where(
                (estimator.predict(X) == classes).T,
                w,
                -1 / (n_classes - 1) * w,
            )
            for estimator, w in zip(self.estimators_, self.estimator_weights_)
        )

        pred /= self.estimator_weights_.sum()
            pred[:, 0] *= -1
            return pred.sum(axis=1)
        return pred

    def staged_decision_function(self, X):
        """Compute decision function of ``X`` for each boosting iteration.

        This method allows monitoring (i.e. determine error on testing set)
        after each boosting iteration.

        Yields
        ------
        score : generator of ndarray of shape (n_samples, k)
            outputs is the same of that of the :term:`classes_` attribute.
        """

        pred = None
        norm = 0.0

        for weight, estimator in zip(self.estimator_weights_, self.estimators_):
            norm += weight

            current_pred = np.where(
                weight,
                -1 / (n_classes - 1) * weight,
            )

            if pred is None:
                pred = current_pred
            else:
                pred += current_pred

                tmp_pred = np.copy(pred)
                tmp_pred[:, 0] *= -1
                yield (tmp_pred / norm).sum(axis=1)
            else:
                yield pred / norm

    def _compute_proba_from_decision(decision, n_classes):
        """Compute probabilities from the decision function.

        This is based eq. (15) of [1] where:
            p(y=c|X) = exp((1 / K-1) f_c(X)) / sum_k(exp((1 / K-1) f_k(X)))
                     = softmax((1 / K-1) * f(X))

        .. [1] J. Zhu, H. Zou, S. Rosset, T. Hastie, "Multi-class AdaBoost",
               2009.
        """
            decision = np.vstack([-decision, decision]).T / 2
        else:
            decision /= n_classes - 1
        return softmax(decision, copy=False)

        """Predict class probabilities for X.

        The predicted class probabilities of an input sample is computed as
        the weighted mean predicted class probabilities of the classifiers
        in the ensemble.

        Returns
        -------
        p : ndarray of shape (n_samples, n_classes)
            The class probabilities of the input samples. The order of
        """

            return np.ones((_num_samples(X), 1))

        return self._compute_proba_from_decision(decision, n_classes)

    def staged_predict_proba(self, X):

        This generator method yields the ensemble predicted class probabilities
        after each iteration of boosting and therefore allows monitoring, such
        as to determine the predicted class probabilities on a test set after
        each boost.

        Yields
        ------
        p : generator of ndarray of shape (n_samples,)
        """

        for decision in self.staged_decision_function(X):
            yield self._compute_proba_from_decision(decision, n_classes)

        """Predict class log-probabilities for X.

        The predicted class log-probabilities of an input sample is computed as
        the weighted mean predicted class log-probabilities of the classifiers

        Returns
        -------
        """

class AdaBoostRegressor(_RoutingNotSupportedMixin, RegressorMixin, BaseWeightBoosting):
    """An AdaBoost regressor.

    An AdaBoost [1] regressor is a meta-estimator that begins by fitting a
    regressor on the original dataset and then fits additional copies of the
    regressor on the same dataset but where the weights of instances are
    adjusted according to the error of the current prediction. As such,
    subsequent regressors focus more on difficult cases.

    This class implements the algorithm known as AdaBoost.R2 [2].

        If ``None``, then the base estimator is
        :class:`~sklearn.tree.DecisionTreeRegressor` initialized with
        `max_depth=3`.

        Weight applied to each regressor at each boosting iteration. A higher
        learning rate increases the contribution of each regressor. There is

    loss : {'linear', 'square', 'exponential'}, default='linear'
        The loss function to use when updating the weights after each

        In addition, it controls the bootstrap of the weights used to train the
        `estimator` at each boosting iteration.

    estimators_ : list of regressors

        Regression error for each estimator in the boosted ensemble.

    See Also
    --------
    AdaBoostClassifier : An AdaBoost classifier.
    GradientBoostingRegressor : Gradient Boosting Classification Tree.
    sklearn.tree.DecisionTreeRegressor : A decision tree regressor.

    .. [2] H. Drucker, "Improving Regressors using Boosting Techniques", 1997.

    Examples
    --------
    >>> from sklearn.ensemble import AdaBoostRegressor
    >>> X, y = make_regression(n_features=4, n_informative=2,
    ...                        random_state=0, shuffle=False)
    >>> regr = AdaBoostRegressor(random_state=0, n_estimators=100)
    >>> regr.fit(X, y)
    AdaBoostRegressor(n_estimators=100, random_state=0)
    >>> regr.predict([[0, 0, 0, 0]])
    array([4.7972...])
    >>> regr.score(X, y)
    0.9771...

    For a detailed example of utilizing :class:`~sklearn.ensemble.AdaBoostRegressor`
    to fit a sequence of decision trees as weak learners, please refer to
    :ref:`sphx_glr_auto_examples_ensemble_plot_adaboost_regression.py`.
    """

        "loss": [StrOptions({"linear", "square", "exponential"})],
    }

        self,
        *,
        loss="linear",
    ):
        )

        self.loss = loss

        super()._validate_estimator(default=DecisionTreeRegressor(max_depth=3))

        """Implement a single boost for regression

        Perform a single boost according to the AdaBoost.R2 algorithm and
        return the updated sample weights.

            The target values (class labels in classification, real numbers in
            regression).

            Controls also the bootstrap of the weights used to train the weak
            learner.

        Returns
        -------

            The regression error for the current boost.
        """

        # Weighted sampling of the training set with replacement
        bootstrap_idx = random_state.choice(
            np.arange(_num_samples(X)),
            size=_num_samples(X),
            replace=True,
            p=sample_weight,
        )

        # Fit on the bootstrapped sample and obtain a prediction
        # for all samples in the training set
        X_ = _safe_indexing(X, bootstrap_idx)
        y_ = _safe_indexing(y, bootstrap_idx)
        estimator.fit(X_, y_)

        error_vect = np.abs(y_predict - y)
        sample_mask = sample_weight > 0
        masked_sample_weight = sample_weight[sample_mask]
        masked_error_vector = error_vect[sample_mask]

        error_max = masked_error_vector.max()
        if error_max != 0:
            masked_error_vector /= error_max

        if self.loss == "square":
            masked_error_vector **= 2
        elif self.loss == "exponential":
            masked_error_vector = 1.0 - np.exp(-masked_error_vector)

        # Calculate the average loss
        estimator_error = (masked_sample_weight * masked_error_vector).sum()

            # Stop if fit is perfect

        elif estimator_error >= 0.5:
            # Discard current estimator only if it isn't the only one
            if len(self.estimators_) > 1:
                self.estimators_.pop(-1)

        beta = estimator_error / (1.0 - estimator_error)

        # Boost weight using AdaBoost.R2 alg
        estimator_weight = self.learning_rate * np.log(1.0 / beta)

            sample_weight[sample_mask] *= np.power(
                beta, (1.0 - masked_error_vector) * self.learning_rate
            )

    def _get_median_predict(self, X, limit):
        # Evaluate predictions of all estimators
        predictions = np.array([est.predict(X) for est in self.estimators_[:limit]]).T

        # Sort the predictions
        sorted_idx = np.argsort(predictions, axis=1)

        # Find index of median prediction for each sample
        weight_cdf = stable_cumsum(self.estimator_weights_[sorted_idx], axis=1)
        median_or_above = weight_cdf >= 0.5 * weight_cdf[:, -1][:, np.newaxis]
        median_idx = median_or_above.argmax(axis=1)

        median_estimators = sorted_idx[np.arange(_num_samples(X)), median_idx]

        # Return median predictions
        return predictions[np.arange(_num_samples(X)), median_estimators]

        """Predict regression value for X.

        The predicted regression value of an input sample is computed
        as the weighted median prediction of the regressors in the ensemble.

        Returns
        -------
            The predicted regression values.
        """

        return self._get_median_predict(X, len(self.estimators_))

        Yields
        ------
        """

        for i, _ in enumerate(self.estimators_, 1):
            yield self._get_median_predict(X, limit=i)

        self,
        *,
    ):
        )

            self,
            X,
        )

        Returns
        -------
        """
            self,
            X,
            y,
        )

        )

            )

                break

                break

                    (
                    ),
                )
                break

                break

        Returns
        -------

        """
        pass

        Yields
        ------
        z : float
        """

            else:

        Returns
        -------
        """
            )

        try:
            return (
                sum(
                )
                / norm
            )

            ) from e

        self,
        *,
    ):
        )

            )

            )

        Returns
        -------

        """

                )

        )

            )

        Returns
        -------
        """

        Yields
        ------
        """

        else:

        Returns
        -------
        """

        pred = sum(
            np.where(
                w,
            )
        )

        Yields
        ------
        """

        pred = None
        norm = 0.0

                weight,
            )

            else:

            else:

               2009.
        """
        else:

        Returns
        -------
        """

        Yields
        ------
        """

        Returns
        -------
        """

        self,
        *,
    ):
        )

            learner.

        Returns
        -------

        """

        )

            )

        Returns
        -------
        """

        Yields
        ------
        """

def _fit_single_estimator(
    estimator, X, y, fit_params, message_clsname=None, message=None
):
    """Private function used to fit an estimator within a job."""
    # TODO(SLEP6): remove if-condition for unrouted sample_weight when metadata
    # routing can't be disabled.
    if not _routing_enabled() and "sample_weight" in fit_params:
        try:
            with _print_elapsed_time(message_clsname, message):
                estimator.fit(X, y, sample_weight=fit_params["sample_weight"])
        except TypeError as exc:
            if "unexpected keyword argument 'sample_weight'" in str(exc):
                    "Underlying estimator {} does not support sample weights.".format(
                        estimator.__class__.__name__
                    )
                ) from exc
            raise
    else:
        with _print_elapsed_time(message_clsname, message):
            estimator.fit(X, y, **fit_params)

def _set_random_states(estimator, random_state=None):
    """Set fixed random_state parameters for an estimator.

    Finds all parameters ending ``random_state`` and sets them to integers
    derived from ``random_state``.

    estimator : estimator supporting get/set_params
        Estimator with potential randomness managed by random_state
        parameters.

        integers. Pass an int for reproducible output across multiple function
        calls.

    Notes
    -----
    This does not necessarily set *all* ``random_state`` attributes that
    control an estimator's randomness, only those accessible through
    ``estimator.get_params()``.  ``random_state``s not controlled include
    those belonging to:

        * cross-validation splitters
        * ``scipy.stats`` rvs
    """
    to_set = {}
    for key in sorted(estimator.get_params(deep=True)):
        if key == "random_state" or key.endswith("__random_state"):
            to_set[key] = random_state.randint(np.iinfo(np.int32).max)

    if to_set:
        estimator.set_params(**to_set)

class BaseEnsemble(MetaEstimatorMixin, BaseEstimator, metaclass=ABCMeta):
    """Base class for all ensemble classes.

    instead.

        The base estimator from which the ensemble is built.

    n_estimators : int, default=10
        The number of estimators in the ensemble.

    estimator_params : list of str, default=tuple()
        The list of attributes to use as parameters when instantiating a
        new base estimator. If none are given, default parameters are used.

    estimators_ : list of estimators
        The collection of fitted base estimators.
    """

        self,
        *,
        n_estimators=10,
    ):
        # Set parameters
        self.n_estimators = n_estimators
        self.estimator_params = estimator_params

        # Don't instantiate estimators now! Parameters of estimator might
        # still change. Eg., when grid-searching with the nested object syntax.
        # self.estimators_ needs to be filled by the derived classes in fit.

    def _validate_estimator(self, default=None):
        """Check the base estimator.

        Sets the `estimator_` attributes.
        """
        if self.estimator is not None:
            self.estimator_ = self.estimator
        else:
            self.estimator_ = default

    def _make_estimator(self, append=True, random_state=None):
        """Make and configure a copy of the `estimator_` attribute.

        Warning: This method should be used to properly instantiate new
        sub-estimators.
        """
        estimator = clone(self.estimator_)
        estimator.set_params(**{p: getattr(self, p) for p in self.estimator_params})

        if random_state is not None:
            _set_random_states(estimator, random_state)

        if append:
            self.estimators_.append(estimator)

        """Return the number of estimators in the ensemble."""
        return len(self.estimators_)

    def __getitem__(self, index):
        """Return the index'th estimator in the ensemble."""
        return self.estimators_[index]

    def __iter__(self):
        """Return iterator over estimators in the ensemble."""
        return iter(self.estimators_)

def _partition_estimators(n_estimators, n_jobs):
    """Private function used to partition estimators between jobs."""
    # Compute the number of jobs
    n_jobs = min(effective_n_jobs(n_jobs), n_estimators)

    # Partition estimators between jobs
    n_estimators_per_job = np.full(n_jobs, n_estimators // n_jobs, dtype=int)
    n_estimators_per_job[: n_estimators % n_jobs] += 1
    starts = np.cumsum(n_estimators_per_job)

    return n_jobs, n_estimators_per_job.tolist(), [0] + starts.tolist()

class _BaseHeterogeneousEnsemble(
    MetaEstimatorMixin, _BaseComposition, metaclass=ABCMeta
):
    """Base class for heterogeneous ensemble of learners.

    estimators : list of (str, estimator) tuples
        The ensemble of estimators to use in the ensemble. Each element of the
        list is defined as a tuple of string (i.e. name of the estimator) and
        an estimator instance. An estimator can be set to `'drop'` using
        `set_params`.

        The elements of the estimators parameter, having been fitted on the
        training data. If an estimator has been set to `'drop'`, it will not
        appear in `estimators_`.
    """

    @property
    def named_estimators(self):
        """Dictionary to access any fitted sub-estimators by name.

        Returns
        -------
        :class:`~sklearn.utils.Bunch`
        """
        return Bunch(**dict(self.estimators))

    def __init__(self, estimators):
        self.estimators = estimators

    def _validate_estimators(self):
        if len(self.estimators) == 0 or not all(
            isinstance(item, (tuple, list)) and isinstance(item[0], str)
            for item in self.estimators
        ):
                "Invalid 'estimators' attribute, 'estimators' should be a "
                "non-empty list of (string, estimator) tuples."
            )
        names, estimators = zip(*self.estimators)
        # defined by MetaEstimatorMixin

        has_estimator = any(est != "drop" for est in estimators)
        if not has_estimator:
                "All estimators are dropped. At least one is required "
                "to be an estimator."
            )

        is_estimator_type = is_classifier if is_classifier(self) else is_regressor

        for est in estimators:
            if est != "drop" and not is_estimator_type(est):
                    "The estimator {} should be a {}.".format(
                        est.__class__.__name__, is_estimator_type.__name__[3:]
                    )
                )

        return names, estimators

        """
        Set the parameters of an estimator from the ensemble.

        Valid parameter keys can be listed with `get_params()`. Note that you
        can directly set the parameters of the estimators contained in
        `estimators`.

        **params : keyword arguments
            Specific parameters using e.g.
            `set_params(parameter_name=new_value)`. In addition, to setting the
            parameters of the estimator, the individual estimator of the
            estimators can also be set, or can be removed by setting them to
            'drop'.

        Returns
        -------
        """
        super()._set_params("estimators", **params)

        """
        Get the parameters of an estimator from the ensemble.

        estimators contained within the `estimators` parameter.

            Setting it to True gets the various estimators and the parameters
            of the estimators as well.

        Returns
        -------
            Parameter and estimator names mapped to their values or parameter
            names mapped to their values.
        """
        return super()._get_params("estimators", deep=deep)

        try:
            tags.input_tags.allow_nan = all(
                get_tags(est[1]).input_tags.allow_nan if est[1] != "drop" else True
                for est in self.estimators
            )
                get_tags(est[1]).input_tags.sparse if est[1] != "drop" else True
            )
            # If `estimators` does not comply with our API (list of tuples) then it will
            # fail. In this case, we assume that `allow_nan` and `sparse` are False but
            # the parameter validation will raise an error during `fit`.

        self,
        *,
    ):

        """
        else:

        """

        if append:

        Returns
        -------
        """

        ):
            )

            )

                    )
                )

        """

            'drop'.

        Returns
        -------
        """

        """

        Returns
        -------
        """

        try:
            )
            )

def _generate_indices(random_state, bootstrap, n_population, n_samples):
    """Draw randomly sampled indices."""
    # Draw sample indices
    if bootstrap:
        indices = random_state.randint(0, n_population, n_samples)
    else:
        indices = sample_without_replacement(
            n_population, n_samples, random_state=random_state
        )

    return indices

def _generate_bagging_indices(
    bootstrap_features,
    bootstrap_samples,
    n_features,
    n_samples,
    max_features,
    max_samples,
):
    """Randomly draw feature and sample indices."""
    # Get valid random state

    # Draw indices
    feature_indices = _generate_indices(
        random_state, bootstrap_features, n_features, max_features
    )
    sample_indices = _generate_indices(
        random_state, bootstrap_samples, n_samples, max_samples
    )

    return feature_indices, sample_indices

def _parallel_build_estimators(
    n_estimators,
    ensemble,
    X,
    y,
    seeds,
    total_n_estimators,
    verbose,
    check_input,
    fit_params,
):
    """Private function used to build a batch of estimators within a job."""
    # Retrieve settings
    max_features = ensemble._max_features
    max_samples = ensemble._max_samples
    bootstrap = ensemble.bootstrap
    bootstrap_features = ensemble.bootstrap_features
    has_check_input = has_fit_parameter(ensemble.estimator_, "check_input")
    requires_feature_indexing = bootstrap_features or max_features != n_features

    # Build estimators
    estimators = []
    estimators_features = []

    # TODO: (slep6) remove if condition for unrouted sample_weight when metadata
    support_sample_weight = has_fit_parameter(ensemble.estimator_, "sample_weight")
    if not _routing_enabled() and (
        not support_sample_weight and fit_params.get("sample_weight") is not None
    ):
            "The base estimator doesn't support sample weight, but sample_weight is "
            "passed to the fit method."
        )

    for i in range(n_estimators):
        if verbose > 1:
            print(
                "Building estimator %d of %d for this parallel run (total %d)..."
                % (i + 1, n_estimators, total_n_estimators)
            )

        random_state = seeds[i]
        estimator = ensemble._make_estimator(append=False, random_state=random_state)

        if has_check_input:
            estimator_fit = partial(estimator.fit, check_input=check_input)
        else:
            estimator_fit = estimator.fit

        # Draw random feature, sample indices
        features, indices = _generate_bagging_indices(
            random_state,
            bootstrap_features,
            bootstrap,
            n_features,
            n_samples,
            max_features,
            max_samples,
        )

        fit_params_ = fit_params.copy()

        # TODO(SLEP6): remove if condition for unrouted sample_weight when metadata
        # routing can't be disabled.
        # 1. If routing is enabled, we will check if the routing supports sample
        # weight and use it if it does.
        # 2. If routing is not enabled, we will check if the base
        # estimator supports sample_weight and use it if it does.

        # Note: Row sampling can be achieved either through setting sample_weight or
        # by indexing. The former is more efficient. Therefore, use this method
        # if possible, otherwise use indexing.
            request_or_router = get_routing_for_object(ensemble.estimator_)
            consumes_sample_weight = request_or_router.consumes(
                "fit", ("sample_weight",)
            )
        else:
            consumes_sample_weight = support_sample_weight
        if consumes_sample_weight:
            # Draw sub samples, using sample weights, and then fit
            curr_sample_weight = _check_sample_weight(
                fit_params_.pop("sample_weight", None), X
            ).copy()

            if bootstrap:
                sample_counts = np.bincount(indices, minlength=n_samples)
                curr_sample_weight *= sample_counts
            else:
                not_indices_mask = ~indices_to_mask(indices, n_samples)
                curr_sample_weight[not_indices_mask] = 0

            fit_params_["sample_weight"] = curr_sample_weight
            X_ = X[:, features] if requires_feature_indexing else X
            estimator_fit(X_, y, **fit_params_)
        else:
            # cannot use sample_weight, so use indexing
            y_ = _safe_indexing(y, indices)
            X_ = _safe_indexing(X, indices)
            fit_params_ = _check_method_params(X, params=fit_params_, indices=indices)
            if requires_feature_indexing:
                X_ = X_[:, features]
            estimator_fit(X_, y_, **fit_params_)

        estimators.append(estimator)
        estimators_features.append(features)

    return estimators, estimators_features

def _parallel_predict_proba(
    estimators,
    estimators_features,
    X,
    n_classes,
    predict_params=None,
    predict_proba_params=None,
):
    """Private function used to compute (proba-)predictions within a job."""
    proba = np.zeros((n_samples, n_classes))

    for estimator, features in zip(estimators, estimators_features):
        if hasattr(estimator, "predict_proba"):
            proba_estimator = estimator.predict_proba(
                X[:, features], **(predict_params or {})
            )

            if n_classes == len(estimator.classes_):
                proba += proba_estimator

            else:
                proba[:, estimator.classes_] += proba_estimator[
                    :, range(len(estimator.classes_))
                ]

        else:
            # Resort to voting
            predictions = estimator.predict(
                X[:, features], **(predict_proba_params or {})
            )

            for i in range(n_samples):
                proba[i, predictions[i]] += 1

    return proba

def _parallel_predict_log_proba(estimators, estimators_features, X, n_classes, params):
    """Private function used to compute log probabilities within a job."""
    log_proba = np.empty((n_samples, n_classes))
    log_proba.fill(-np.inf)
    all_classes = np.arange(n_classes, dtype=int)

        log_proba_estimator = estimator.predict_log_proba(X[:, features], **params)

        if n_classes == len(estimator.classes_):
            log_proba = np.logaddexp(log_proba, log_proba_estimator)

        else:
            log_proba[:, estimator.classes_] = np.logaddexp(
                log_proba[:, estimator.classes_],
                log_proba_estimator[:, range(len(estimator.classes_))],
            )

            missing = np.setdiff1d(all_classes, estimator.classes_)
            log_proba[:, missing] = np.logaddexp(log_proba[:, missing], -np.inf)

    return log_proba

def _parallel_decision_function(estimators, estimators_features, X, params):
    """Private function used to compute decisions within a job."""
    return sum(
        estimator.decision_function(X[:, features], **params)
        for estimator, features in zip(estimators, estimators_features)
    )

def _parallel_predict_regression(estimators, estimators_features, X, params):
    """Private function used to compute predictions within a job."""
        estimator.predict(X[:, features], **params)
    )

class BaseBagging(BaseEnsemble, metaclass=ABCMeta):
    """Base class for Bagging meta-estimator.

    instead.
    """

        "max_samples": [
            Interval(RealNotInt, 0, 1, closed="right"),
        ],
        "max_features": [
        ],
        "bootstrap": ["boolean"],
        "bootstrap_features": ["boolean"],
        "oob_score": ["boolean"],
        "warm_start": ["boolean"],
        "n_jobs": [None, Integral],
    }

        self,
        *,
        max_samples=1.0,
        max_features=1.0,
        bootstrap=True,
        bootstrap_features=False,
        oob_score=False,
        warm_start=False,
        verbose=0,
    ):
        )
        self.max_samples = max_samples
        self.max_features = max_features
        self.bootstrap = bootstrap
        self.bootstrap_features = bootstrap_features
        self.oob_score = oob_score
        self.warm_start = warm_start

        # BaseBagging.estimator is not validated yet
    )
        """Build a Bagging ensemble of estimators from the training set (X, y).

            The training input samples. Sparse matrices are accepted only if
            they are supported by the base estimator.

            Note that this is supported only if the base estimator supports
            sample weighting.
            Parameters to pass to the underlying estimators.

        Returns
        -------
        """

        # Convert data (X is required to be 2d and indexable)
            self,
            X,
            y,
            multi_output=True,
        )

        return self._fit(
            X,
            y,
            max_samples=self.max_samples,
        )

    def _parallel_args(self):
        return {}

    def _fit(
        self,
        X,
        y,
        max_samples=None,
        max_depth=None,
        check_input=True,
        sample_weight=None,
        **fit_params,
    ):
        """Build a Bagging ensemble of estimators from the training
           set (X, y).

        max_samples : int or float, default=None
            Argument to use instead of self.max_samples.

        max_depth : int, default=None
            Override value used when constructing base estimator. Only
            supported if the base estimator has a max_depth parameter.

        check_input : bool, default=True
            Override value used when fitting base estimator. Only supported
            if the base estimator has a check_input parameter for fit function.
            If the meta-estimator already checks the input, set this value to
            False to prevent redundant input validation.

            Parameters to pass to the :term:`fit` method of the underlying
            estimator.

        Returns
        -------
        """

        # Remap output
        self._n_samples = n_samples
        y = self._validate_y(y)

        self._validate_estimator(self._get_estimator())

            fit_params["sample_weight"] = sample_weight

        else:
            routed_params.estimator = Bunch(fit=fit_params)
            if "sample_weight" in fit_params:
                routed_params.estimator.fit["sample_weight"] = fit_params[
                    "sample_weight"
                ]

        if max_depth is not None:
            self.estimator_.max_depth = max_depth

        # Validate max_samples
        if max_samples is None:
            max_samples = self.max_samples
        elif not isinstance(max_samples, numbers.Integral):
            max_samples = int(max_samples * X.shape[0])

        if max_samples > X.shape[0]:
            raise ValueError("max_samples must be <= n_samples")

        # Store validated integer row sampling value
        self._max_samples = max_samples

        # Validate max_features
        if isinstance(self.max_features, numbers.Integral):
            max_features = self.max_features
        elif isinstance(self.max_features, float):
            max_features = int(self.max_features * self.n_features_in_)

        if max_features > self.n_features_in_:
            raise ValueError("max_features must be <= n_features")

        max_features = max(1, int(max_features))

        # Store validated integer feature sampling value
        self._max_features = max_features

        # Other checks
        if not self.bootstrap and self.oob_score:
            raise ValueError("Out of bag estimation only available if bootstrap=True")

        if self.warm_start and self.oob_score:
            raise ValueError("Out of bag estimate only available if warm_start=False")

        if hasattr(self, "oob_score_") and self.warm_start:
            del self.oob_score_

        if not self.warm_start or not hasattr(self, "estimators_"):
            # Free allocated memory, if any
            self.estimators_ = []
            self.estimators_features_ = []

        n_more_estimators = self.n_estimators - len(self.estimators_)

        if n_more_estimators < 0:
                "n_estimators=%d must be larger or equal to "
                "len(estimators_)=%d when warm_start==True"
                % (self.n_estimators, len(self.estimators_))
            )

        elif n_more_estimators == 0:
            warn(
                "Warm-start fitting without increasing n_estimators does not "
                "fit new trees."
            )

        # Parallel loop
        n_jobs, n_estimators, starts = _partition_estimators(
            n_more_estimators, self.n_jobs
        )
        total_n_estimators = sum(n_estimators)

        # Advance random state to state after training
        # the first n_estimators
        if self.warm_start and len(self.estimators_) > 0:
            random_state.randint(MAX_INT, size=len(self.estimators_))

        seeds = random_state.randint(MAX_INT, size=n_more_estimators)
        self._seeds = seeds

        all_results = Parallel(
            n_jobs=n_jobs, verbose=self.verbose, **self._parallel_args()
        )(
            delayed(_parallel_build_estimators)(
                n_estimators[i],
                self,
                X,
                y,
                seeds[starts[i] : starts[i + 1]],
                total_n_estimators,
                verbose=self.verbose,
                check_input=check_input,
            )
            for i in range(n_jobs)
        )

        # Reduce
        self.estimators_ += list(
            itertools.chain.from_iterable(t[0] for t in all_results)
        )
        self.estimators_features_ += list(
            itertools.chain.from_iterable(t[1] for t in all_results)
        )

        if self.oob_score:
            self._set_oob_score(X, y)

    def _set_oob_score(self, X, y):
        """Calculate out of bag predictions and score."""

    def _validate_y(self, y):
        if len(y.shape) == 1 or y.shape[1] == 1:
            return column_or_1d(y, warn=True)
        return y

    def _get_estimators_indices(self):
        # Get drawn indices along both sample and feature axes
        for seed in self._seeds:
            # Operations accessing random_state must be performed identically
            # to those in `_parallel_build_estimators()`
            feature_indices, sample_indices = _generate_bagging_indices(
                seed,
                self.bootstrap_features,
                self.bootstrap,
                self.n_features_in_,
                self._n_samples,
                self._max_features,
                self._max_samples,
            )

            yield feature_indices, sample_indices

    @property
    def estimators_samples_(self):
        """
        The subset of drawn samples for each base estimator.

        Returns a dynamically generated list of indices identifying
        the samples used for fitting each member of the ensemble, i.e.,
        the in-bag samples.

        Note: the list is re-created at each call to the property in order
        to reduce the object memory footprint by not storing the sampling
        data. Thus fetching the property may be slower than expected.
        """
        return [sample_indices for _, sample_indices in self._get_estimators_indices()]

        Returns
        -------
        """

        method_mapping.add(caller="fit", callee="fit").add(
            caller="decision_function", callee="decision_function"
        )

        # the router needs to be built depending on whether the sub-estimator has a
        # `predict_proba` method (as BaggingClassifier decides dynamically at runtime):
        if hasattr(self._get_estimator(), "predict_proba"):
            (
                method_mapping.add(caller="predict", callee="predict_proba").add(
                    caller="predict_proba", callee="predict_proba"
                )
            )

        else:
            (
                method_mapping.add(caller="predict", callee="predict").add(
                    caller="predict_proba", callee="predict"
                )
            )

        # `predict_log_proba` method (as BaggingClassifier decides dynamically at
        # runtime):
        if hasattr(self._get_estimator(), "predict_log_proba"):
            method_mapping.add(caller="predict_log_proba", callee="predict_log_proba")

        else:
            # if `predict_log_proba` is not available in BaggingClassifier's
            # sub-estimator, the routing should go to its `predict_proba` if it is
            # available or else to its `predict` method; according to how
            # `sample_weight` is passed to the respective methods dynamically at
            # runtime:
            if hasattr(self._get_estimator(), "predict_proba"):
                method_mapping.add(caller="predict_log_proba", callee="predict_proba")

            else:
                method_mapping.add(caller="predict_log_proba", callee="predict")

        router.add(estimator=self._get_estimator(), method_mapping=method_mapping)

        """Resolve which estimator to return."""

        tags.input_tags.allow_nan = get_tags(self._get_estimator()).input_tags.allow_nan

class BaggingClassifier(ClassifierMixin, BaseBagging):
    """A Bagging classifier.

    A Bagging classifier is an ensemble meta-estimator that fits base
    classifiers each on random subsets of the original dataset and then
    aggregate their individual predictions (either by voting or by averaging)
    to form a final prediction. Such a meta-estimator can typically be used as
    a way to reduce the variance of a black-box estimator (e.g., a decision
    tree), by introducing randomization into its construction procedure and
    then making an ensemble out of it.

    This algorithm encompasses several works from the literature. When random
    subsets of the dataset are drawn as random subsets of the samples, then
    this algorithm is known as Pasting [1]_. If samples are drawn with
    replacement, then the method is known as Bagging [2]_. When random subsets
    of the dataset are drawn as random subsets of the features, then the method
    is known as Random Subspaces [3]_. Finally, when base estimators are built
    on subsets of both samples and features, then the method is known as
    Random Patches [4]_.

    Read more in the :ref:`User Guide <bagging>`.

    .. versionadded:: 0.15

        The base estimator to fit on random subsets of the dataset.
        If None, then the base estimator is a
        :class:`~sklearn.tree.DecisionTreeClassifier`.

        The number of base estimators in the ensemble.

    max_samples : int or float, default=1.0
        The number of samples to draw from X to train each base estimator (with
        replacement by default, see `bootstrap` for more details).

        - If int, then draw `max_samples` samples.
        - If float, then draw `max_samples * X.shape[0]` samples.

    max_features : int or float, default=1.0
        The number of features to draw from X to train each base estimator (
        without replacement by default, see `bootstrap_features` for more
        details).

        - If int, then draw `max_features` features.
        - If float, then draw `max(1, int(max_features * n_features_in_))` features.

    bootstrap : bool, default=True
        Whether samples are drawn with replacement. If False, sampling
        without replacement is performed.

    bootstrap_features : bool, default=False
        Whether features are drawn with replacement.

    oob_score : bool, default=False
        Whether to use out-of-bag samples to estimate
        the generalization error. Only available if bootstrap=True.

    warm_start : bool, default=False
        When set to True, reuse the solution of the previous call to fit
        and add more estimators to the ensemble, otherwise, just fit
        a whole new ensemble. See :term:`the Glossary <warm_start>`.

           *warm_start* constructor parameter.

        The number of jobs to run in parallel for both :meth:`fit` and
        :meth:`predict`. ``None`` means 1 unless in a
        :obj:`joblib.parallel_backend` context. ``-1`` means using all
        processors. See :term:`Glossary <n_jobs>` for more details.

        Controls the random resampling of the original dataset
        (sample wise and feature wise).
        If the base estimator accepts a `random_state` attribute, a different
        seed is generated for each instance in the ensemble.

        Controls the verbosity when fitting and predicting.

    estimators_samples_ : list of arrays
        The subset of drawn samples (i.e., the in-bag samples) for each base
        estimator. Each subset is defined by an array of the indices selected.

    estimators_features_ : list of arrays
        The subset of drawn features for each base estimator.

    n_classes_ : int or list

    oob_score_ : float
        Score of the training dataset obtained using an out-of-bag estimate.
        This attribute exists only when ``oob_score`` is True.

    oob_decision_function_ : ndarray of shape (n_samples, n_classes)
        Decision function computed with out-of-bag estimate on the training
        set. If n_estimators is small it might be possible that a data point
        was never left out during the bootstrap. In this case,
        `oob_decision_function_` might contain NaN. This attribute exists
        only when ``oob_score`` is True.

    See Also
    --------
    BaggingRegressor : A Bagging regressor.

    .. [1] L. Breiman, "Pasting small votes for classification in large
           databases and on-line", Machine Learning, 36(1), 85-103, 1999.

    .. [2] L. Breiman, "Bagging predictors", Machine Learning, 24(2), 123-140,
           1996.

    .. [3] T. Ho, "The random subspace method for constructing decision
           forests", Pattern Analysis and Machine Intelligence, 20(8), 832-844,
           1998.

    .. [4] G. Louppe and P. Geurts, "Ensembles on Random Patches", Machine
           Learning and Knowledge Discovery in Databases, 346-361, 2012.

    Examples
    --------
    >>> from sklearn.ensemble import BaggingClassifier
    >>> clf = BaggingClassifier(estimator=SVC(),
    ...                         n_estimators=10, random_state=0).fit(X, y)
    """

        self,
        *,
    ):
            max_samples=max_samples,
            max_features=max_features,
            bootstrap=bootstrap,
            bootstrap_features=bootstrap_features,
            oob_score=oob_score,
            warm_start=warm_start,
            n_jobs=n_jobs,
        )

        """Resolve which estimator to return (default is DecisionTreeClassifier)"""
            return DecisionTreeClassifier()
        return self.estimator

        n_samples = y.shape[0]

        predictions = np.zeros((n_samples, n_classes_))

        for estimator, samples, features in zip(
            self.estimators_, self.estimators_samples_, self.estimators_features_
        ):
            # Create mask for OOB samples
            mask = ~indices_to_mask(samples, n_samples)

            if hasattr(estimator, "predict_proba"):
                predictions[mask, :] += estimator.predict_proba(
                    (X[mask, :])[:, features]
                )

            else:
                p = estimator.predict((X[mask, :])[:, features])
                j = 0

                for i in range(n_samples):
                    if mask[i]:
                        predictions[i, p[j]] += 1
                        j += 1

        if (predictions.sum(axis=1) == 0).any():
            warn(
                "Some inputs do not have OOB scores. "
                "This probably means too few estimators were used "
                "to compute any reliable oob estimates."
            )

        oob_decision_function = predictions / predictions.sum(axis=1)[:, np.newaxis]
        oob_score = accuracy_score(y, np.argmax(predictions, axis=1))

        self.oob_decision_function_ = oob_decision_function
        self.oob_score_ = oob_score

        y = column_or_1d(y, warn=True)
        self.n_classes_ = len(self.classes_)

        return y

        """Predict class for X.

        The predicted class of an input sample is computed as the class with
        the highest mean predicted probability. If base estimators do not
        implement a ``predict_proba`` method, then it resorts to voting.

            Parameters routed to the `predict_proba` (if available) or the `predict`
            method (otherwise) of the sub-estimators via the metadata routing API.

            .. versionadded:: 1.7

                Only available if
                `sklearn.set_config(enable_metadata_routing=True)` is set. See
                details.

        Returns
        -------
        """
        _raise_for_params(params, self, "predict")

        predicted_probabilitiy = self.predict_proba(X, **params)
        return self.classes_.take((np.argmax(predicted_probabilitiy, axis=1)), axis=0)

        the mean predicted class probabilities of the base estimators in the
        ensemble. If base estimators do not implement a ``predict_proba``
        method, then it resorts to voting and the predicted class probabilities
        of an input sample represents the proportion of estimators predicting
        each class.

                details.

        Returns
        -------
        """
        _raise_for_params(params, self, "predict_proba")

        # Check data
            self,
            X,
        )

        else:
            routed_params.estimator = Bunch(predict_proba=Bunch())

        n_jobs, _, starts = _partition_estimators(self.n_estimators, self.n_jobs)

        all_proba = Parallel(
        )(
            delayed(_parallel_predict_proba)(
                self.estimators_[starts[i] : starts[i + 1]],
                self.estimators_features_[starts[i] : starts[i + 1]],
                X,
                self.n_classes_,
                predict_params=routed_params.estimator.get("predict", None),
                predict_proba_params=routed_params.estimator.get("predict_proba", None),
            )
        )

        # Reduce
        proba = sum(all_proba) / self.n_estimators

        the log of the mean predicted class probabilities of the base
        estimators in the ensemble.

            Parameters routed to the `predict_log_proba`, the `predict_proba` or the
            `proba` method of the sub-estimators via the metadata routing API. The
            routing is tried in the mentioned order depending on whether this method is
            available on the sub-estimator.

                details.

        Returns
        -------
            The class log-probabilities of the input samples. The order of the
        """
        _raise_for_params(params, self, "predict_log_proba")

        if hasattr(self.estimator_, "predict_log_proba"):
            # Check data
            X = validate_data(
                self,
                X,
                accept_sparse=["csr", "csc"],
                dtype=None,
                ensure_all_finite=False,
                reset=False,
            )

                routed_params = process_routing(self, "predict_log_proba", **params)
            else:
                routed_params.estimator = Bunch(predict_log_proba=Bunch())

            # Parallel loop
            n_jobs, _, starts = _partition_estimators(self.n_estimators, self.n_jobs)

            all_log_proba = Parallel(n_jobs=n_jobs, verbose=self.verbose)(
                delayed(_parallel_predict_log_proba)(
                    self.estimators_[starts[i] : starts[i + 1]],
                    self.estimators_features_[starts[i] : starts[i + 1]],
                    X,
                    self.n_classes_,
                    params=routed_params.estimator.predict_log_proba,
                )
                for i in range(n_jobs)
            )

            # Reduce
            log_proba = all_log_proba[0]

            for j in range(1, len(all_log_proba)):
                log_proba = np.logaddexp(log_proba, all_log_proba[j])

            log_proba -= np.log(self.n_estimators)

        else:
            log_proba = np.log(self.predict_proba(X, **params))

        return log_proba

    @available_if(
        _estimator_has("decision_function", delegates=("estimators_", "estimator"))
    )
        """Average of the decision functions of the base classifiers.

            Parameters routed to the `decision_function` method of the sub-estimators
            via the metadata routing API.

                details.

        Returns
        -------
        score : ndarray of shape (n_samples, k)
            The decision function of the input samples. The columns correspond
            to the classes in sorted order, as they appear in the attribute
            ``classes_``. Regression and binary classification are special
            cases with ``k == 1``, otherwise ``k==n_classes``.
        """
        _raise_for_params(params, self, "decision_function")

            self,
            X,
        )

        else:
            routed_params.estimator = Bunch(decision_function=Bunch())

        all_decisions = Parallel(n_jobs=n_jobs, verbose=self.verbose)(
            delayed(_parallel_decision_function)(
                X,
                params=routed_params.estimator.decision_function,
            )
        )

        # Reduce
        decisions = sum(all_decisions) / self.n_estimators

        return decisions

class BaggingRegressor(RegressorMixin, BaseBagging):
    """A Bagging regressor.

    A Bagging regressor is an ensemble meta-estimator that fits base
    regressors each on random subsets of the original dataset and then

        :class:`~sklearn.tree.DecisionTreeRegressor`.

        details).

    oob_prediction_ : ndarray of shape (n_samples,)
        Prediction computed with out-of-bag estimate on the training
        `oob_prediction_` might contain NaN. This attribute exists only
        when ``oob_score`` is True.

    See Also
    --------
    BaggingClassifier : A Bagging classifier.

           1996.

           1998.

    Examples
    --------
    >>> from sklearn.svm import SVR
    >>> from sklearn.ensemble import BaggingRegressor
    >>> X, y = make_regression(n_samples=100, n_features=4,
    ...                        n_informative=2, n_targets=1,
    >>> regr = BaggingRegressor(estimator=SVR(),
    array([-2.8720...])
    """

        self,
        *,
    ):
        )

        """Predict regression target for X.

        The predicted regression target of an input sample is computed as the
        mean predicted regression targets of the estimators in the ensemble.

            Parameters routed to the `predict` method of the sub-estimators via the
            metadata routing API.

                details.

        Returns
        -------
        """

            self,
            X,
        )

        else:
            routed_params.estimator = Bunch(predict=Bunch())

        all_y_hat = Parallel(n_jobs=n_jobs, verbose=self.verbose)(
            delayed(_parallel_predict_regression)(
                X,
                params=routed_params.estimator.predict,
            )
        )

        # Reduce
        y_hat = sum(all_y_hat) / self.n_estimators

        return y_hat

        predictions = np.zeros((n_samples,))
        n_predictions = np.zeros((n_samples,))

        ):

            predictions[mask] += estimator.predict((X[mask, :])[:, features])
            n_predictions[mask] += 1

        if (n_predictions == 0).any():
            warn(
            )
            n_predictions[n_predictions == 0] = 1

        predictions /= n_predictions

        self.oob_prediction_ = predictions
        self.oob_score_ = r2_score(y, predictions)

            return DecisionTreeRegressor()

        self,
        *,
    ):
        )

        Returns
        -------
        """

            self,
            X,
            y,
        )

            X,
            y,
        )

    def _fit(
        self,
        X,
        y,
    ):
           set (X, y).

        Returns
        -------
        """

        else:
                ]

            )

            warn(
            )

        )

        )(
                self,
                X,
                y,
            )
        )

        # Reduce
        )
        )

        return y

                seed,
            )

        """

        """

        Returns
        -------
        """

        )

            (
                )
            )

        else:
            (
                )
            )

        else:
            # runtime:

            else:

        self,
        *,
    ):
        )

        ):

                )

            else:
                j = 0

                        j += 1

            warn(
            )

        return y

                details.

        Returns
        -------
        """

                details.

        Returns
        -------
        """

            self,
            X,
        )

        else:

        )(
                X,
            )
        )

        # Reduce

                details.

        Returns
        -------
        """

                self,
                X,
            )

            else:

                    X,
                )
            )

            # Reduce

        else:

                details.

        Returns
        -------
        """

            self,
            X,
        )

        else:

                X,
            )
        )

        # Reduce

                details.

        Returns
        -------
        """

            self,
            X,
        )

        else:

                X,
            )
        )

        # Reduce

        ):

            warn(
            )

def _get_n_samples_bootstrap(n_samples, max_samples):
    """
    Get the number of samples in a bootstrap sample.

    n_samples : int
        Number of samples in the dataset.
    max_samples : int or float
        The maximum number of samples to draw from the total available:
            - if float, this indicates a fraction of the total and should be
              the interval `(0.0, 1.0]`;
            - if int, this indicates the exact number of samples;
            - if None, this indicates the total number of samples.

    Returns
    -------
    n_samples_bootstrap : int
        The total number of samples to draw for the bootstrap sample.
    """
    if max_samples is None:
        return n_samples

    if isinstance(max_samples, Integral):
        if max_samples > n_samples:
            msg = "`max_samples` must be <= n_samples={} but got value {}"
            raise ValueError(msg.format(n_samples, max_samples))
        return max_samples

    if isinstance(max_samples, Real):
        return max(round(n_samples * max_samples), 1)

def _generate_sample_indices(random_state, n_samples, n_samples_bootstrap):
    """
    Private function used to _parallel_build_trees function."""

    random_instance = check_random_state(random_state)
    sample_indices = random_instance.randint(
        0, n_samples, n_samples_bootstrap, dtype=np.int32
    )

    return sample_indices

def _generate_unsampled_indices(random_state, n_samples, n_samples_bootstrap):
    """
    Private function used to forest._set_oob_score function."""
    sample_indices = _generate_sample_indices(
        random_state, n_samples, n_samples_bootstrap
    )
    sample_counts = np.bincount(sample_indices, minlength=n_samples)
    unsampled_mask = sample_counts == 0
    indices_range = np.arange(n_samples)
    unsampled_indices = indices_range[unsampled_mask]

    return unsampled_indices

def _parallel_build_trees(
    tree,
    bootstrap,
    X,
    y,
    sample_weight,
    tree_idx,
    n_trees,
    verbose=0,
    class_weight=None,
    n_samples_bootstrap=None,
    missing_values_in_feature_mask=None,
):
    """
    Private function used to fit a single tree in parallel."""
    if verbose > 1:
        print("building tree %d of %d" % (tree_idx + 1, n_trees))

        if sample_weight is None:
            curr_sample_weight = np.ones((n_samples,), dtype=np.float64)
        else:
            curr_sample_weight = sample_weight.copy()

        indices = _generate_sample_indices(
            tree.random_state, n_samples, n_samples_bootstrap
        )
        sample_counts = np.bincount(indices, minlength=n_samples)
        curr_sample_weight *= sample_counts

        if class_weight == "subsample":
            with catch_warnings():
                simplefilter("ignore", DeprecationWarning)
                curr_sample_weight *= compute_sample_weight("auto", y, indices=indices)
        elif class_weight == "balanced_subsample":
            curr_sample_weight *= compute_sample_weight("balanced", y, indices=indices)

        tree._fit(
            X,
            y,
            sample_weight=curr_sample_weight,
            check_input=False,
            missing_values_in_feature_mask=missing_values_in_feature_mask,
        )
    else:
            X,
            y,
        )

    return tree

class BaseForest(MultiOutputMixin, BaseEnsemble, metaclass=ABCMeta):
    """
    Base class for forests of trees.

    instead.
    """

        "oob_score": ["boolean", callable],
            None,
            Interval(RealNotInt, 0.0, 1.0, closed="right"),
        ],
    }

        self,
        n_estimators=100,
        *,
        bootstrap=False,
        class_weight=None,
    ):
        )

        self.class_weight = class_weight

    def apply(self, X):
        """
        Apply trees in the forest to X, return leaf indices.

            The input samples. Internally, its dtype will be converted to
            ``dtype=np.float32``. If a sparse matrix is provided, it will be
            converted into a sparse ``csr_matrix``.

        Returns
        -------
        X_leaves : ndarray of shape (n_samples, n_estimators)
            For each datapoint x in X and for each tree in the forest,
            return the index of the leaf x ends up in.
        """
        X = self._validate_X_predict(X)
        results = Parallel(
            prefer="threads",
        )(delayed(tree.apply)(X, check_input=False) for tree in self.estimators_)

        return np.array(results).T

    def decision_path(self, X):
        """
        Return the decision path in the forest.

        .. versionadded:: 0.18

        Returns
        -------
        indicator : sparse matrix of shape (n_samples, n_nodes)
            Return a node indicator matrix where non zero elements indicates
            that the samples goes through the nodes. The matrix is of CSR
            format.

        n_nodes_ptr : ndarray of shape (n_estimators + 1,)
            The columns from indicator[n_nodes_ptr[i]:n_nodes_ptr[i+1]]
            gives the indicator value for the i-th estimator.
        """
        indicators = Parallel(
        )(
            delayed(tree.decision_path)(X, check_input=False)
            for tree in self.estimators_
        )

        n_nodes = [0]
        n_nodes.extend([i.shape[1] for i in indicators])
        n_nodes_ptr = np.array(n_nodes).cumsum()

        return sparse_hstack(indicators).tocsr(), n_nodes_ptr

        """
        Build a forest of trees from the training set (X, y).

            The training input samples. Internally, its dtype will be converted
            to ``dtype=np.float32``. If a sparse matrix is provided, it will be
            converted into a sparse ``csc_matrix``.

            Sample weights. If None, then samples are equally weighted. Splits
            that would create child nodes with net zero or negative weight are
            ignored while searching for a split in each node. In the case of
            classification, splits are also ignored if they would result in any
            single class carrying a negative weight in either child node.

        Returns
        -------
        """
        # Validate or convert input data
        if issparse(y):
            raise ValueError("sparse multilabel-indicator for y is not supported.")

            self,
            X,
            y,
            accept_sparse="csc",
            dtype=DTYPE,
        )
        # _compute_missing_values_in_feature_mask checks if X has missing values and
        # will raise an error if the underlying tree base estimator can't handle missing
        # values. Only the criterion is required to determine if the tree supports
        # missing values.
        estimator = type(self.estimator)(criterion=self.criterion)
        missing_values_in_feature_mask = (
            estimator._compute_missing_values_in_feature_mask(
                X, estimator_name=self.__class__.__name__
            )
        )

        if issparse(X):
            # Pre-sort indices to avoid that each individual tree of the
            # ensemble sorts the indices.
            X.sort_indices()

        y = np.atleast_1d(y)
        if y.ndim == 2 and y.shape[1] == 1:
            warn(
                (
                    "A column-vector y was passed when a 1d array was"
                    " expected. Please change the shape of y to "
                    "(n_samples,), for example using ravel()."
                ),
                DataConversionWarning,
                stacklevel=2,
            )

            # reshape is necessary to preserve the data contiguity against vs
            # [:, np.newaxis] that does not.

        if self.criterion == "poisson":
            if np.any(y < 0):
                    "Some value(s) of y are negative which is "
                    "not allowed for Poisson regression."
                )
            if np.sum(y) <= 0:
                    "Sum of y is not strictly positive which "
                    "is necessary for Poisson regression."
                )

        self._n_samples, self.n_outputs_ = y.shape

        y, expanded_class_weight = self._validate_y_class_weight(y)

        if getattr(y, "dtype", None) != DOUBLE or not y.flags.contiguous:
            y = np.ascontiguousarray(y, dtype=DOUBLE)

        if expanded_class_weight is not None:
                sample_weight = sample_weight * expanded_class_weight
            else:
                sample_weight = expanded_class_weight

        if not self.bootstrap and self.max_samples is not None:
                "`max_sample` cannot be set if `bootstrap=False`. "
                "Either switch to `bootstrap=True` or set "
                "`max_sample=None`."
            )
        elif self.bootstrap:
            n_samples_bootstrap = _get_n_samples_bootstrap(
                n_samples=X.shape[0], max_samples=self.max_samples
            )
        else:
            n_samples_bootstrap = None

        self._n_samples_bootstrap = n_samples_bootstrap

            )

            warn(
            )
        else:
            if self.warm_start and len(self.estimators_) > 0:
                # We draw from the random state to get the random state we
                # would have got if we hadn't used a warm_start.
                random_state.randint(MAX_INT, size=len(self.estimators_))

            trees = [
                self._make_estimator(append=False, random_state=random_state)
                for i in range(n_more_estimators)
            ]

            # Parallel loop: we prefer the threading backend as the Cython code
            # for fitting the trees is internally releasing the Python GIL
            # making threading more efficient than multiprocessing in
            # that case. However, for joblib 0.12+ we respect any
            # parallel_backend contexts set at a higher level,
            # since correctness does not rely on using threads.
            trees = Parallel(
                prefer="threads",
            )(
                delayed(_parallel_build_trees)(
                    t,
                    self.bootstrap,
                    X,
                    y,
                    i,
                    len(trees),
                    verbose=self.verbose,
                    class_weight=self.class_weight,
                    n_samples_bootstrap=n_samples_bootstrap,
                    missing_values_in_feature_mask=missing_values_in_feature_mask,
                )
                for i, t in enumerate(trees)
            )

            # Collect newly grown trees
            self.estimators_.extend(trees)

        if self.oob_score and (
            n_more_estimators > 0 or not hasattr(self, "oob_score_")
        ):
            y_type = type_of_target(y)
            if y_type == "unknown" or (
                is_classifier(self) and y_type == "multiclass-multioutput"
            ):
                # FIXME: we could consider to support multiclass-multioutput if
                # we introduce or reuse a constructor parameter (e.g.
                # oob_score) allowing our user to pass a callable defining the
                # scoring strategy on OOB sample.
                    "The type of target cannot be used to compute OOB "
                    f"estimates. Got {y_type} while only the following are "
                    "supported: continuous, continuous-multioutput, binary, "
                    "multiclass, multilabel-indicator."
                )

            if callable(self.oob_score):
                self._set_oob_score_and_attributes(
                    X, y, scoring_function=self.oob_score
                )
            else:
                self._set_oob_score_and_attributes(X, y)

        # Decapsulate classes_ attributes
        if hasattr(self, "classes_") and self.n_outputs_ == 1:

    def _set_oob_score_and_attributes(self, X, y, scoring_function=None):
        """Compute and set the OOB score and attributes.

        y : ndarray of shape (n_samples, n_outputs)
            The target matrix.
        scoring_function : callable, default=None
            Scoring function for OOB score. Default depends on whether
            this is a regression (R2 score) or classification problem
            (accuracy score).
        """

    def _compute_oob_predictions(self, X, y):
        """Compute and set the OOB score.

        Returns
        -------
        oob_pred : ndarray of shape (n_samples, n_classes, n_outputs) or \
                (n_samples, 1, n_outputs)
            The OOB predictions.
        """
        # Prediction requires X to be in CSR format
            X = X.tocsr()

        n_outputs = self.n_outputs_
        if is_classifier(self) and hasattr(self, "n_classes_"):
            # n_classes_ is a ndarray at this stage
            # all the supported type of target will have the same number of
            # classes in all outputs
            oob_pred_shape = (n_samples, self.n_classes_[0], n_outputs)
        else:
            # for regression, n_classes_ does not exist and we create an empty
            # axis to be consistent with the classification case and make
            # the array operations compatible with the 2 settings
            oob_pred_shape = (n_samples, 1, n_outputs)

        oob_pred = np.zeros(shape=oob_pred_shape, dtype=np.float64)
        n_oob_pred = np.zeros((n_samples, n_outputs), dtype=np.int64)

        n_samples_bootstrap = _get_n_samples_bootstrap(
            self.max_samples,
        )
        for estimator in self.estimators_:
            unsampled_indices = _generate_unsampled_indices(
                estimator.random_state,
                n_samples,
                n_samples_bootstrap,
            )

            y_pred = self._get_oob_predictions(estimator, X[unsampled_indices, :])
            oob_pred[unsampled_indices, ...] += y_pred
            n_oob_pred[unsampled_indices, :] += 1

        for k in range(n_outputs):
            if (n_oob_pred == 0).any():
                warn(
                    (
                        "Some inputs do not have OOB scores. This probably means "
                        "too few trees were used to compute any reliable OOB "
                        "estimates."
                    ),
                )
                n_oob_pred[n_oob_pred == 0] = 1
            oob_pred[..., k] /= n_oob_pred[..., [k]]

        return oob_pred

    def _validate_y_class_weight(self, y):
        # Default implementation
        return y, None

    def _validate_X_predict(self, X):
        """
        Validate X whenever one tries to predict, apply, predict_proba."""
        if self.estimators_[0]._support_missing_values(X):
            ensure_all_finite = "allow-nan"
        else:
            ensure_all_finite = True

            self,
            X,
            accept_sparse="csr",
            ensure_all_finite=ensure_all_finite,
        )
        if issparse(X) and (X.indices.dtype != np.intc or X.indptr.dtype != np.intc):
            raise ValueError("No support for np.int64 index based sparse matrices")
        return X

    @property
        """
        The impurity-based feature importances.

        Returns
        -------
            The values of this array sum to 1, unless all trees are single node
            trees consisting of only the root node, in which case it will be an
            array of zeros.
        """

        all_importances = Parallel(n_jobs=self.n_jobs, prefer="threads")(
            delayed(getattr)(tree, "feature_importances_")
            if tree.tree_.node_count > 1
        )

        if not all_importances:
            return np.zeros(self.n_features_in_, dtype=np.float64)

        all_importances = np.mean(all_importances, axis=0, dtype=np.float64)
        return all_importances / np.sum(all_importances)

        for tree in self.estimators_:
            if not self.bootstrap:
                yield np.arange(self._n_samples, dtype=np.int32)
            else:
                # tree.random_state is actually an immutable integer seed rather
                # than a mutable RandomState instance, so it's safe to use it
                # repeatedly when calling this property.
                seed = tree.random_state
                # Operations accessing random_state must be performed identically
                # to those in `_parallel_build_trees()`
                yield _generate_sample_indices(
                    seed, self._n_samples, self._n_samples_bootstrap
                )

    @property
        """The subset of drawn samples for each base estimator.

        """
        return [sample_indices for sample_indices in self._get_estimators_indices()]

        # Only the criterion is required to determine if the tree supports
        # missing values
        tags.input_tags.allow_nan = get_tags(estimator).input_tags.allow_nan

def _accumulate_prediction(predict, X, out, lock):
    """
    This is a utility function for joblib's Parallel.

    It can't go locally in ForestClassifier or ForestRegressor, because joblib
    complains that it cannot pickle it when placed there.
    """
    prediction = predict(X, check_input=False)
    with lock:
        if len(out) == 1:
            out[0] += prediction
        else:
            for i in range(len(out)):
                out[i] += prediction[i]

class ForestClassifier(ClassifierMixin, BaseForest, metaclass=ABCMeta):
    """
    Base class for forest of trees-based classifiers.

    instead.
    """

        self,
        *,
    ):
            class_weight=class_weight,
        )

    def _get_oob_predictions(tree, X):
        """Compute the OOB predictions for an individual tree.

        tree : DecisionTreeClassifier object
            A single decision tree classifier.
            The OOB samples.

        Returns
        -------
        y_pred : ndarray of shape (n_samples, n_classes, n_outputs)
            The OOB associated predictions.
        """
        y_pred = tree.predict_proba(X, check_input=False)
        y_pred = np.asarray(y_pred)
        if y_pred.ndim == 2:
            # binary and multiclass
            y_pred = y_pred[..., np.newaxis]
        else:
            # Roll the first `n_outputs` axis to the last axis. We will reshape
            # from a shape of (n_outputs, n_samples, n_classes) to a shape of
            # (n_samples, n_classes, n_outputs).
            y_pred = np.rollaxis(y_pred, axis=0, start=3)

            Scoring function for OOB score. Defaults to `accuracy_score`.
        """
        self.oob_decision_function_ = super()._compute_oob_predictions(X, y)
        if self.oob_decision_function_.shape[-1] == 1:
            # drop the n_outputs axis if there is a single output
            self.oob_decision_function_ = self.oob_decision_function_.squeeze(axis=-1)

        if scoring_function is None:
            scoring_function = accuracy_score

        self.oob_score_ = scoring_function(
            y, np.argmax(self.oob_decision_function_, axis=1)
        )

        y = np.copy(y)
        expanded_class_weight = None

        if self.class_weight is not None:
            y_original = np.copy(y)

        self.classes_ = []
        self.n_classes_ = []

        y_store_unique_indices = np.zeros(y.shape, dtype=int)
            classes_k, y_store_unique_indices[:, k] = np.unique(
                y[:, k], return_inverse=True
            )
            self.classes_.append(classes_k)
            self.n_classes_.append(classes_k.shape[0])
        y = y_store_unique_indices

            valid_presets = ("balanced", "balanced_subsample")
            if isinstance(self.class_weight, str):
                if self.class_weight not in valid_presets:
                        "Valid presets for class_weight include "
                        '"balanced" and "balanced_subsample".'
                        'Given "%s".' % self.class_weight
                    )
                if self.warm_start:
                    warn(
                        'class_weight presets "balanced" or '
                        '"balanced_subsample" are '
                        "not recommended for warm_start if the fitted data "
                        "differs from the full dataset. In order to use "
                        '"balanced" weights, use compute_class_weight '
                        '("balanced", classes, y). In place of y you can use '
                        "a large enough sample of the full training set "
                        "target to properly estimate the class frequency "
                        "distributions. Pass the resulting weights as the "
                        "class_weight parameter."
                    )

            if self.class_weight != "balanced_subsample" or not self.bootstrap:
                if self.class_weight == "balanced_subsample":
                    class_weight = "balanced"
                else:
                    class_weight = self.class_weight
                expanded_class_weight = compute_sample_weight(class_weight, y_original)

        return y, expanded_class_weight

        """
        Predict class for X.

        The predicted class of an input sample is a vote by the trees in
        the forest, weighted by their probability estimates. That is,
        the predicted class is the one with highest mean probability
        estimate across the trees.

        Returns
        -------
        y : ndarray of shape (n_samples,) or (n_samples, n_outputs)
        """

            return self.classes_.take(np.argmax(proba, axis=1), axis=0)

        else:
            n_samples = proba[0].shape[0]
            # all dtypes should be the same, so just take the first
            class_type = self.classes_[0].dtype
            predictions = np.empty((n_samples, self.n_outputs_), dtype=class_type)

                predictions[:, k] = self.classes_[k].take(
                    np.argmax(proba[k], axis=1), axis=0
                )

            return predictions

        """
        Predict class probabilities for X.

        The predicted class probabilities of an input sample are computed as
        the mean predicted class probabilities of the trees in the forest.
        The class probability of a single tree is the fraction of samples of
        the same class in a leaf.

        Returns
        -------
        p : ndarray of shape (n_samples, n_classes), or a list of such arrays
        """

        # Assign chunk of trees to jobs
        n_jobs, _, _ = _partition_estimators(self.n_estimators, self.n_jobs)

        # avoid storing the output of every estimator by summing them here
        all_proba = [
            np.zeros((X.shape[0], j), dtype=np.float64)
            for j in np.atleast_1d(self.n_classes_)
        ]
        lock = threading.Lock()
        Parallel(n_jobs=n_jobs, verbose=self.verbose, require="sharedmem")(
            delayed(_accumulate_prediction)(e.predict_proba, X, all_proba, lock)
            for e in self.estimators_
        )

        for proba in all_proba:
            proba /= len(self.estimators_)

        if len(all_proba) == 1:
            return all_proba[0]
        else:
            return all_proba

        """
        Predict class log-probabilities for X.

        the log of the mean predicted class probabilities of the trees in the
        forest.

        Returns
        -------
        """

        else:
                proba[k] = np.log(proba[k])

            return proba

        tags.classifier_tags.multi_label = True

class ForestRegressor(RegressorMixin, BaseForest, metaclass=ABCMeta):
    """
    Base class for forest of trees-based regressors.

    instead.
    """

        self,
        *,
    ):
        )

        """
        Predict regression target for X.

        mean predicted regression targets of the trees in the forest.

        Returns
        -------
        """

        if self.n_outputs_ > 1:
            y_hat = np.zeros((X.shape[0], self.n_outputs_), dtype=np.float64)
        else:
            y_hat = np.zeros((X.shape[0]), dtype=np.float64)

            delayed(_accumulate_prediction)(e.predict, X, [y_hat], lock)
        )

        y_hat /= len(self.estimators_)

        tree : DecisionTreeRegressor object
            A single decision tree regressor.

        Returns
        -------
        y_pred : ndarray of shape (n_samples, 1, n_outputs)
        """
        y_pred = tree.predict(X, check_input=False)
        if y_pred.ndim == 1:
            # single output regression
            y_pred = y_pred[:, np.newaxis, np.newaxis]
        else:
            # multioutput regression
            y_pred = y_pred[:, np.newaxis, :]

            Scoring function for OOB score. Defaults to `r2_score`.
        """
        self.oob_prediction_ = super()._compute_oob_predictions(X, y).squeeze(axis=1)
        if self.oob_prediction_.shape[-1] == 1:
            self.oob_prediction_ = self.oob_prediction_.squeeze(axis=-1)

            scoring_function = r2_score

        self.oob_score_ = scoring_function(y, self.oob_prediction_)

    def _compute_partial_dependence_recursion(self, grid, target_features):
        """Fast partial dependence computation.

        grid : ndarray of shape (n_samples, n_target_features), dtype=DTYPE
            The grid points on which the partial dependence should be
            evaluated.
        target_features : ndarray of shape (n_target_features), dtype=np.intp
            The set of target features for which the partial dependence
            should be evaluated.

        Returns
        -------
        averaged_predictions : ndarray of shape (n_samples,)
            The value of the partial dependence function on each grid point.
        """
        grid = np.asarray(grid, dtype=DTYPE, order="C")
        target_features = np.asarray(target_features, dtype=np.intp, order="C")
        averaged_predictions = np.zeros(
            shape=grid.shape[0], dtype=np.float64, order="C"
        )

            # Note: we don't sum in parallel because the GIL isn't released in
            # the fast method.
            tree.tree_.compute_partial_dependence(
                grid, target_features, averaged_predictions
            )
        # Average over the forest
        averaged_predictions /= len(self.estimators_)

        return averaged_predictions

class RandomForestClassifier(ForestClassifier):
    """
    A random forest classifier.

    A random forest is a meta estimator that fits a number of decision tree
    classifiers on various sub-samples of the dataset and uses averaging to
    improve the predictive accuracy and control over-fitting.
    Trees in the forest use the best split strategy, i.e. equivalent to passing
    `splitter="best"` to the underlying :class:`~sklearn.tree.DecisionTreeClassifier`.
    The sub-sample size is controlled with the `max_samples` parameter if
    `bootstrap=True` (default), otherwise the whole dataset is used to build
    each tree.

    For a comparison between tree-based ensemble models see the example
    :ref:`sphx_glr_auto_examples_ensemble_plot_forest_hist_grad_boosting_comparison.py`.

    This estimator has native support for missing values (NaNs). During training,
    the tree grower learns at each split point whether samples with missing values
    should go to the left or right child, based on the potential gain. When predicting,
    samples with missing values are assigned to the left or right child consequently.
    If no missing values were encountered for a given feature during training, then
    samples with missing values are mapped to whichever child has the most samples.

    Read more in the :ref:`User Guide <forest>`.

    n_estimators : int, default=100
        The number of trees in the forest.

           The default value of ``n_estimators`` changed from 10 to 100
           in 0.22.

    criterion : {"gini", "entropy", "log_loss"}, default="gini"
        The function to measure the quality of a split. Supported criteria are
        "gini" for the Gini impurity and "log_loss" and "entropy" both for the
        Shannon information gain, see :ref:`tree_mathematical_formulation`.
        Note: This parameter is tree-specific.

    max_depth : int, default=None
        The maximum depth of the tree. If None, then nodes are expanded until
        all leaves are pure or until all leaves contain less than
        min_samples_split samples.

    min_samples_split : int or float, default=2
        The minimum number of samples required to split an internal node:

        - If int, then consider `min_samples_split` as the minimum number.
        - If float, then `min_samples_split` is a fraction and
          `ceil(min_samples_split * n_samples)` are the minimum
          number of samples for each split.

        .. versionchanged:: 0.18
           Added float values for fractions.

    min_samples_leaf : int or float, default=1
        The minimum number of samples required to be at a leaf node.
        A split point at any depth will only be considered if it leaves at
        least ``min_samples_leaf`` training samples in each of the left and
        right branches.  This may have the effect of smoothing the model,
        especially in regression.

        - If int, then consider `min_samples_leaf` as the minimum number.
        - If float, then `min_samples_leaf` is a fraction and
          `ceil(min_samples_leaf * n_samples)` are the minimum
          number of samples for each node.

    min_weight_fraction_leaf : float, default=0.0
        The minimum weighted fraction of the sum total of weights (of all
        the input samples) required to be at a leaf node. Samples have
        equal weight when sample_weight is not provided.

    max_features : {"sqrt", "log2", None}, int or float, default="sqrt"
        The number of features to consider when looking for the best split:

        - If int, then consider `max_features` features at each split.
        - If float, then `max_features` is a fraction and
          `max(1, int(max_features * n_features_in_))` features are considered at each
          split.
        - If "sqrt", then `max_features=sqrt(n_features)`.
        - If "log2", then `max_features=log2(n_features)`.
        - If None, then `max_features=n_features`.

        .. versionchanged:: 1.1
            The default of `max_features` changed from `"auto"` to `"sqrt"`.

        Note: the search for a split does not stop until at least one
        valid partition of the node samples is found, even if it requires to
        effectively inspect more than ``max_features`` features.

    max_leaf_nodes : int, default=None
        Grow trees with ``max_leaf_nodes`` in best-first fashion.
        Best nodes are defined as relative reduction in impurity.
        If None then unlimited number of leaf nodes.

    min_impurity_decrease : float, default=0.0
        A node will be split if this split induces a decrease of the impurity
        greater than or equal to this value.

        The weighted impurity decrease equation is the following::

            N_t / N * (impurity - N_t_R / N_t * right_impurity
                                - N_t_L / N_t * left_impurity)

        where ``N`` is the total number of samples, ``N_t`` is the number of
        samples at the current node, ``N_t_L`` is the number of samples in the
        left child, and ``N_t_R`` is the number of samples in the right child.

        ``N``, ``N_t``, ``N_t_R`` and ``N_t_L`` all refer to the weighted sum,
        if ``sample_weight`` is passed.

        Whether bootstrap samples are used when building trees. If False, the
        whole dataset is used to build each tree.

    oob_score : bool or callable, default=False
        Whether to use out-of-bag samples to estimate the generalization score.
        By default, :func:`~sklearn.metrics.accuracy_score` is used.
        Provide a callable with signature `metric(y_true, y_pred)` to use a
        custom metric. Only available if `bootstrap=True`.

        The number of jobs to run in parallel. :meth:`fit`, :meth:`predict`,
        :meth:`decision_path` and :meth:`apply` are all parallelized over the
        trees. ``None`` means 1 unless in a :obj:`joblib.parallel_backend`
        context. ``-1`` means using all processors. See :term:`Glossary
        <n_jobs>` for more details.

        Controls both the randomness of the bootstrapping of the samples used
        when building trees (if ``bootstrap=True``) and the sampling of the
        features to consider when looking for the best split at each node
        (if ``max_features < n_features``).
        See :term:`Glossary <random_state>` for details.

        When set to ``True``, reuse the solution of the previous call to fit
        and add more estimators to the ensemble, otherwise, just fit a whole
        new forest. See :term:`Glossary <warm_start>` and
        :ref:`tree_ensemble_warm_start` for details.

    class_weight : {"balanced", "balanced_subsample"}, dict or list of dicts, \
            default=None
        Weights associated with classes in the form ``{class_label: weight}``.
        If not given, all classes are supposed to have weight one. For
        multi-output problems, a list of dicts can be provided in the same
        order as the columns of y.

        Note that for multioutput (including multilabel) weights should be
        defined for each class of every column in its own dict. For example,
        for four-class multilabel classification weights should be
        [{0: 1, 1: 1}, {0: 1, 1: 5}, {0: 1, 1: 1}, {0: 1, 1: 1}] instead of
        [{1:1}, {2:5}, {3:1}, {4:1}].

        The "balanced" mode uses the values of y to automatically adjust
        weights inversely proportional to class frequencies in the input data
        as ``n_samples / (n_classes * np.bincount(y))``

        The "balanced_subsample" mode is the same as "balanced" except that
        weights are computed based on the bootstrap sample for every tree
        grown.

        For multi-output, the weights of each column of y will be multiplied.

        Note that these weights will be multiplied with sample_weight (passed
        through the fit method) if sample_weight is specified.

    ccp_alpha : non-negative float, default=0.0
        Complexity parameter used for Minimal Cost-Complexity Pruning. The
        subtree with the largest cost complexity that is smaller than
        ``ccp_alpha`` will be chosen. By default, no pruning is performed. See
        :ref:`minimal_cost_complexity_pruning` for details. See
        :ref:`sphx_glr_auto_examples_tree_plot_cost_complexity_pruning.py`
        for an example of such pruning.

        .. versionadded:: 0.22

    max_samples : int or float, default=None
        If bootstrap is True, the number of samples to draw from X
        to train each base estimator.

        - If None (default), then draw `X.shape[0]` samples.
        - If float, then draw `max(round(n_samples * max_samples), 1)` samples. Thus,
          `max_samples` should be in the interval `(0.0, 1.0]`.

    monotonic_cst : array-like of int of shape (n_features), default=None
        Indicates the monotonicity constraint to enforce on each feature.
          - 1: monotonic increase
          - 0: no constraint
          - -1: monotonic decrease

        If monotonic_cst is None, no constraints are applied.

        Monotonicity constraints are not supported for:
          - multiclass classifications (i.e. when `n_classes > 2`),
          - multioutput classifications (i.e. when `n_outputs_ > 1`),
          - classifications trained on data with missing values.

        The constraints hold over the probability of the positive class.

        Read more in the :ref:`User Guide <monotonic_cst_gbdt>`.

    estimator_ : :class:`~sklearn.tree.DecisionTreeClassifier`
        The child estimator template used to create the collection of fitted

    estimators_ : list of DecisionTreeClassifier

    classes_ : ndarray of shape (n_classes,) or a list of such arrays
        The classes labels (single output problem), or a list of arrays of
        class labels (multi-output problem).

        The number of classes (single output problem), or a list containing the
        number of classes for each output (multi-output problem).

        The number of outputs when ``fit`` is performed.

    oob_decision_function_ : ndarray of shape (n_samples, n_classes) or \
            (n_samples, n_classes, n_outputs)

    See Also
    --------
    sklearn.tree.DecisionTreeClassifier : A decision tree classifier.
    sklearn.ensemble.ExtraTreesClassifier : Ensemble of extremely randomized
        tree classifiers.
    sklearn.ensemble.HistGradientBoostingClassifier : A Histogram-based Gradient
        Boosting Classification Tree, very fast for big datasets (n_samples >=
        10_000).

    Notes
    -----
    The default values for the parameters controlling the size of the trees
    (e.g. ``max_depth``, ``min_samples_leaf``, etc.) lead to fully grown and
    unpruned trees which can potentially be very large on some data sets. To
    reduce memory consumption, the complexity and size of the trees should be
    controlled by setting those parameter values.

    The features are always randomly permuted at each split. Therefore,
    the best found split may vary, even with the same training data,
    ``max_features=n_features`` and ``bootstrap=False``, if the improvement
    of the criterion is identical for several splits enumerated during the
    search of the best split. To obtain a deterministic behaviour during
    fitting, ``random_state`` has to be fixed.

    .. [1] L. Breiman, "Random Forests", Machine Learning, 45(1), 5-32, 2001.

    Examples
    --------
    >>> clf = RandomForestClassifier(max_depth=2, random_state=0)
    RandomForestClassifier(...)
    >>> print(clf.predict([[0, 0, 0, 0]]))
    [1]
    """

        **ForestClassifier._parameter_constraints,
        **DecisionTreeClassifier._parameter_constraints,
        "class_weight": [
            StrOptions({"balanced_subsample", "balanced"}),
            dict,
            list,
            None,
        ],
    }
    _parameter_constraints.pop("splitter")

        self,
        *,
        criterion="gini",
        min_samples_split=2,
        min_samples_leaf=1,
        min_weight_fraction_leaf=0.0,
        max_features="sqrt",
        max_leaf_nodes=None,
        min_impurity_decrease=0.0,
        ccp_alpha=0.0,
        monotonic_cst=None,
    ):
            estimator=DecisionTreeClassifier(),
            estimator_params=(
                "criterion",
                "max_depth",
                "min_samples_split",
                "min_samples_leaf",
                "min_weight_fraction_leaf",
                "max_features",
                "max_leaf_nodes",
                "min_impurity_decrease",
                "random_state",
                "ccp_alpha",
                "monotonic_cst",
            ),
        )

        self.criterion = criterion
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.min_samples_leaf = min_samples_leaf
        self.min_weight_fraction_leaf = min_weight_fraction_leaf
        self.max_leaf_nodes = max_leaf_nodes
        self.min_impurity_decrease = min_impurity_decrease
        self.monotonic_cst = monotonic_cst
        self.ccp_alpha = ccp_alpha

class RandomForestRegressor(ForestRegressor):
    """
    A random forest regressor.

    regressors on various sub-samples of the dataset and uses averaging to
    `splitter="best"` to the underlying :class:`~sklearn.tree.DecisionTreeRegressor`.
    each tree.

           in 0.22.

    criterion : {"squared_error", "absolute_error", "friedman_mse", "poisson"}, \
            default="squared_error"
        The function to measure the quality of a split. Supported criteria
        are "squared_error" for the mean squared error, which is equal to
        variance reduction as feature selection criterion and minimizes the L2
        loss using the mean of each terminal node, "friedman_mse", which uses
        mean squared error with Friedman's improvement score for potential
        splits, "absolute_error" for the mean absolute error, which minimizes
        the L1 loss using the median of each terminal node, and "poisson" which
        uses reduction in Poisson deviance to find splits.
        Training using "absolute_error" is significantly slower
        than when using "squared_error".

           Mean Absolute Error (MAE) criterion.

           Poisson criterion.

    max_features : {"sqrt", "log2", None}, int or float, default=1.0

          split.
        - If None or 1.0, then `max_features=n_features`.

        .. note::
            The default of 1.0 is equivalent to bagged trees and more
            randomness can be achieved by setting smaller values, e.g. 0.3.

            The default of `max_features` changed from `"auto"` to 1.0.

        By default, :func:`~sklearn.metrics.r2_score` is used.

          - 1: monotonically increasing
          - -1: monotonically decreasing

          - multioutput regressions (i.e. when `n_outputs_ > 1`),
          - regressions trained on data with missing values.

    estimator_ : :class:`~sklearn.tree.DecisionTreeRegressor`

    estimators_ : list of DecisionTreeRegressor

    oob_prediction_ : ndarray of shape (n_samples,) or (n_samples, n_outputs)
        Prediction computed with out-of-bag estimate on the training set.

    See Also
    --------
    sklearn.ensemble.ExtraTreesRegressor : Ensemble of extremely randomized
        tree regressors.
    sklearn.ensemble.HistGradientBoostingRegressor : A Histogram-based Gradient
        Boosting Regression Tree, very fast for big datasets (n_samples >=
        10_000).

    Notes
    -----

    The default value ``max_features=1.0`` uses ``n_features``
    rather than ``n_features / 3``. The latter was originally suggested in
    [1], whereas the former was more recently justified empirically in [2].

    .. [2] P. Geurts, D. Ernst., and L. Wehenkel, "Extremely randomized
           trees", Machine Learning, 63(1), 3-42, 2006.

    Examples
    --------
    >>> from sklearn.ensemble import RandomForestRegressor
    >>> regr = RandomForestRegressor(max_depth=2, random_state=0)
    RandomForestRegressor(...)
    >>> print(regr.predict([[0, 0, 0, 0]]))
    [-8.32987858]
    """

        **ForestRegressor._parameter_constraints,
        **DecisionTreeRegressor._parameter_constraints,
    }

        self,
        *,
        criterion="squared_error",
    ):
            estimator=DecisionTreeRegressor(),
            ),
        )

class ExtraTreesClassifier(ForestClassifier):
    """
    An extra-trees classifier.

    This class implements a meta estimator that fits a number of
    randomized decision trees (a.k.a. extra-trees) on various sub-samples
    of the dataset and uses averaging to improve the predictive accuracy
    and control over-fitting.

    This estimator has native support for missing values (NaNs) for
    random splits. During training, a random threshold will be chosen
    to split the non-missing values on. Then the non-missing values will be sent
    to the left and right child based on the randomly selected threshold, while
    the missing values will also be randomly sent to the left or right child.
    This is repeated for every feature considered at each split. The best split
    among these is chosen.

           in 0.22.

          split.

    bootstrap : bool, default=False

        Controls 3 sources of randomness:

        - the bootstrapping of the samples used when building trees
          (if ``bootstrap=True``)
        - the sampling of the features to consider when looking for the best
          split at each node (if ``max_features < n_features``)
        - the draw of the splits for each of the `max_features`

        grown.

        - If float, then draw `max_samples * X.shape[0]` samples. Thus,

    estimator_ : :class:`~sklearn.tree.ExtraTreeClassifier`

    See Also
    --------
    ExtraTreesRegressor : An extra-trees regressor with random splits.
    RandomForestClassifier : A random forest classifier with optimal splits.
    RandomForestRegressor : Ensemble regressor using trees with optimal splits.

    Notes
    -----

    .. [1] P. Geurts, D. Ernst., and L. Wehenkel, "Extremely randomized

    Examples
    --------
    >>> from sklearn.ensemble import ExtraTreesClassifier
    >>> X, y = make_classification(n_features=4, random_state=0)
    >>> clf = ExtraTreesClassifier(n_estimators=100, random_state=0)
    ExtraTreesClassifier(random_state=0)
    """

            dict,
            list,
            None,
        ],
    }

        self,
        *,
    ):
            estimator=ExtraTreeClassifier(),
            ),
        )

class ExtraTreesRegressor(ForestRegressor):
    """
    An extra-trees regressor.

           in 0.22.

          split.

        .. note::

    estimator_ : :class:`~sklearn.tree.ExtraTreeRegressor`

        The number of outputs.

    See Also
    --------
    ExtraTreesClassifier : An extra-trees classifier with random splits.

    Notes
    -----

    .. [1] P. Geurts, D. Ernst., and L. Wehenkel, "Extremely randomized trees",
           Machine Learning, 63(1), 3-42, 2006.

    Examples
    --------
    >>> from sklearn.datasets import load_diabetes
    >>> from sklearn.ensemble import ExtraTreesRegressor
    >>> X, y = load_diabetes(return_X_y=True)
    >>> reg = ExtraTreesRegressor(n_estimators=100, random_state=0).fit(
    ...    X_train, y_train)
    >>> reg.score(X_test, y_test)
    0.2727...
    """

    }

        self,
        *,
    ):
            estimator=ExtraTreeRegressor(),
            ),
        )

class RandomTreesEmbedding(TransformerMixin, BaseForest):
    """
    An ensemble of totally random trees.

    An unsupervised transformation of a dataset to a high-dimensional
    sparse representation. A datapoint is coded according to which leaf of
    each tree it is sorted into. Using a one-hot encoding of the leaves,
    this leads to a binary coding with as many ones as there are trees in
    the forest.

    The dimensionality of the resulting representation is
    ``n_out <= n_estimators * max_leaf_nodes``. If ``max_leaf_nodes == None``,
    the number of leaf nodes is at most ``n_estimators * 2 ** max_depth``.

    For an example of applying Random Trees Embedding to non-linear
    classification, see
    :ref:`sphx_glr_auto_examples_ensemble_plot_random_forest_embedding.py`.

    Read more in the :ref:`User Guide <random_trees_embedding>`.

        Number of trees in the forest.

           in 0.22.

    max_depth : int, default=5
        The maximum depth of each tree. If None, then nodes are expanded until

          `ceil(min_samples_split * n_samples)` is the minimum

          `ceil(min_samples_leaf * n_samples)` is the minimum

    sparse_output : bool, default=True
        Whether or not to return a sparse CSR matrix, as default behavior,
        or to return a dense array compatible with dense pipeline operators.

        The number of jobs to run in parallel. :meth:`fit`, :meth:`transform`,

        Controls the generation of the random `y` used to fit the trees
        and the draw of the splits for each feature at the trees' nodes.

    estimator_ : :class:`~sklearn.tree.ExtraTreeRegressor` instance

    estimators_ : list of :class:`~sklearn.tree.ExtraTreeRegressor` instances

        The feature importances (the higher, the more important the feature).

    one_hot_encoder_ : OneHotEncoder instance
        One-hot encoder used to create the sparse embedding.

    See Also
    --------
    ExtraTreesClassifier : An extra-trees classifier.
    ExtraTreesRegressor : An extra-trees regressor.
    RandomForestClassifier : A random forest classifier.
    RandomForestRegressor : A random forest regressor.
    sklearn.tree.ExtraTreeClassifier: An extremely randomized
        tree classifier.
    sklearn.tree.ExtraTreeRegressor : An extremely randomized
        tree regressor.

    .. [2] Moosmann, F. and Triggs, B. and Jurie, F.  "Fast discriminative
           visual codebooks using randomized clustering forests"
           NIPS 2007

    Examples
    --------
    >>> from sklearn.ensemble import RandomTreesEmbedding
    >>> X = [[0,0], [1,0], [0,1], [-1,0], [0,-1]]
    >>> random_trees = RandomTreesEmbedding(
    ...    n_estimators=5, random_state=0, max_depth=1).fit(X)
    >>> X_sparse_embedding = random_trees.transform(X)
    >>> X_sparse_embedding.toarray()
    array([[0., 1., 1., 0., 1., 0., 0., 1., 1., 0.],
           [0., 1., 1., 0., 1., 0., 0., 1., 1., 0.],
           [0., 1., 0., 1., 0., 1., 0., 1., 0., 1.],
           [1., 0., 1., 0., 1., 0., 1., 0., 1., 0.],
           [0., 1., 1., 0., 1., 0., 0., 1., 1., 0.]])
    """

        **BaseDecisionTree._parameter_constraints,
        "sparse_output": ["boolean"],
    }
    for param in ("max_features", "ccp_alpha", "splitter", "monotonic_cst"):
        _parameter_constraints.pop(param)

    criterion = "squared_error"
    max_features = 1

        self,
        *,
        max_depth=5,
        sparse_output=True,
    ):
            ),
            bootstrap=False,
            oob_score=False,
            max_samples=None,
        )

        self.sparse_output = sparse_output

        raise NotImplementedError("OOB score not supported by tree embedding")

    def fit(self, X, y=None, sample_weight=None):
        """
        Fit estimator.

            The input samples. Use ``dtype=np.float32`` for maximum
            efficiency. Sparse matrices are also supported, use sparse
            ``csc_matrix`` for maximum efficiency.

        y : Ignored

        Returns
        -------
        """
        # Parameters are validated in fit_transform
        self.fit_transform(X, y, sample_weight=sample_weight)

    def fit_transform(self, X, y=None, sample_weight=None):
        """
        Fit estimator and transform dataset.

            Input data used to build forests. Use ``dtype=np.float32`` for
            maximum efficiency.

        y : Ignored

        Returns
        -------
        X_transformed : sparse matrix of shape (n_samples, n_out)
            Transformed dataset.
        """
        y = rnd.uniform(size=_num_samples(X))
        super().fit(X, y, sample_weight=sample_weight)

        self.one_hot_encoder_ = OneHotEncoder(sparse_output=self.sparse_output)
        output = self.one_hot_encoder_.fit_transform(self.apply(X))
        self._n_features_out = output.shape[1]

        Returns
        -------
            Transformed feature names, in the format of
            `randomtreesembedding_{tree}_{leaf}`, where `tree` is the tree used
            to generate the leaf and `leaf` is the index of a leaf node
            in that tree. Note that the node indexing scheme is used to
            index both nodes with children (split nodes) and leaf nodes.
            Only the latter can be present as output features.
            As a consequence, there are missing indices in the output
            feature names.
        """
        _check_feature_names_in(
            self, input_features=input_features, generate_names=False
        )

        feature_names = [
            f"randomtreesembedding_{tree}_{leaf}"
            for tree in range(self.n_estimators)
            for leaf in self.one_hot_encoder_.categories_[tree]
        ]
        return np.asarray(feature_names, dtype=object)

        """
        Transform dataset.

            Input data to be transformed. Use ``dtype=np.float32`` for maximum
            ``csr_matrix`` for maximum efficiency.

        Returns
        -------
        """
        return self.one_hot_encoder_.transform(self.apply(X))

        self,
        *,
    ):
        )

        """

        Returns
        -------
        """

        """

        Returns
        -------
            format.

        """
        )(
        )

        """

        Returns
        -------
        """

            self,
            X,
            y,
        )
            )
        )

            warn(
                (
                ),
            )

                )
                )

            else:

            )
            )
        else:

            )

            warn(
            )
        else:

            trees = [
            ]

            )(
                    t,
                    X,
                    y,
                    i,
                )
            )

        ):
            ):
                )

                )
            else:

        """

        Returns
        -------
        """

        else:

        )
            )

                warn(
                    (
                    ),
                )

        """
        else:

            self,
            X,
        )
        return X

        """

        Returns
        -------
        """

        )

            else:
                )

        """

        self,
        *,
    ):
        )

        Returns
        -------
        """
        else:

        """

        )

            )

                    )
                    warn(
                    )

                else:

        """

        Returns
        -------
        """

        else:

                )

        """

        Returns
        -------
        """

        ]
        )

        else:

        """

        forest.

        Returns
        -------
        """

        else:

        self,
        *,
    ):
        )

        """

        Returns
        -------
        """

        else:

        )

        Returns
        -------
        """
        else:

        """

        Returns
        -------
        """
        )

            )

        self,
        *,
    ):
            ),
        )

        self,
        *,
    ):
            ),
        )

        self,
        *,
    ):
            ),
        )

        self,
        *,
    ):
            ),
        )

        self,
        *,
    ):
            ),
        )

        """

        y : Ignored

        Returns
        -------
        """

        """

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """
        )

        ]

        """

        Returns
        -------
        """

def _parallel_compute_tree_depths(
    tree,
    X,
    features,
    tree_decision_path_lengths,
    tree_avg_path_lengths,
    depths,
    lock,
):
    """Parallel computation of isolation tree depth."""
    if features is None:
        X_subset = X
    else:
        X_subset = X[:, features]

    leaves_index = tree.apply(X_subset, check_input=False)

    with lock:
        depths += (
            tree_decision_path_lengths[leaves_index]
            + tree_avg_path_lengths[leaves_index]
            - 1.0
        )

class IsolationForest(OutlierMixin, BaseBagging):
    """
    Isolation Forest Algorithm.

    Return the anomaly score of each sample using the IsolationForest algorithm

    The IsolationForest 'isolates' observations by randomly selecting a feature
    and then randomly selecting a split value between the maximum and minimum
    values of the selected feature.

    Since recursive partitioning can be represented by a tree structure, the
    number of splittings required to isolate a sample is equivalent to the path
    length from the root node to the terminating node.

    This path length, averaged over a forest of such random trees, is a
    measure of normality and our decision function.

    Random partitioning produces noticeably shorter paths for anomalies.
    Hence, when a forest of random trees collectively produce shorter path
    lengths for particular samples, they are highly likely to be anomalies.

    Read more in the :ref:`User Guide <isolation_forest>`.

    max_samples : "auto", int or float, default="auto"
        The number of samples to draw from X to train each base estimator.

        - If "auto", then `max_samples=min(256, n_samples)`.

        If max_samples is larger than the number of samples provided,
        all samples will be used for all trees (no sampling).

    contamination : 'auto' or float, default='auto'
        of outliers in the data set. Used when fitting to define the threshold
        on the scores of the samples.

        - If 'auto', the threshold is determined as in the
          original paper.
        - If float, the contamination should be in the range (0, 0.5].

           The default value of ``contamination`` changed from 0.1
           to ``'auto'``.

        The number of features to draw from X to train each base estimator.

        Note: using a float number less than 1.0 or integer less than number of
        features will enable feature subsampling and leads to a longer runtime.

        If True, individual trees are fit on random subsets of the training
        data sampled with replacement. If False, sampling without replacement
        is performed.

        The number of jobs to run in parallel for :meth:`fit`. ``None`` means 1
        unless in a :obj:`joblib.parallel_backend` context. ``-1`` means using
        all processors. See :term:`Glossary <n_jobs>` for more details.

        Controls the pseudo-randomness of the selection of the feature
        and split values for each branching step and each tree in the forest.

        Controls the verbosity of the tree building process.

        new forest. See :term:`the Glossary <warm_start>`.

        The child estimator template used to create the collection of
        fitted sub-estimators.

    estimators_ : list of ExtraTreeRegressor instances

    estimators_features_ : list of ndarray

    estimators_samples_ : list of ndarray

    max_samples_ : int
        The actual number of samples.

        Offset used to define the decision function from the raw scores. We
        have the relation: ``decision_function = score_samples - offset_``.
        ``offset_`` is defined as follows. When the contamination parameter is
        set to "auto", the offset is equal to -0.5 as the scores of inliers are
        close to 0 and the scores of outliers are close to -1. When a
        contamination parameter different than "auto" is provided, the offset
        is defined in such a way we obtain the expected number of outliers
        (samples with decision function < 0) in training.

    See Also
    --------
    sklearn.covariance.EllipticEnvelope : An object for detecting outliers in a
        Gaussian distributed dataset.
    sklearn.svm.OneClassSVM : Unsupervised Outlier Detection.
        Estimate the support of a high-dimensional distribution.
        The implementation is based on libsvm.
    sklearn.neighbors.LocalOutlierFactor : Unsupervised Outlier Detection
        using Local Outlier Factor (LOF).

    Notes
    -----
    The implementation is based on an ensemble of ExtraTreeRegressor. The
    maximum depth of each tree is set to ``ceil(log_2(n))`` where
    :math:`n` is the number of samples used to build the tree
    (see (Liu et al., 2008) for more details).

    .. [1] Liu, Fei Tony, Ting, Kai Ming and Zhou, Zhi-Hua. "Isolation forest."
           Data Mining, 2008. ICDM'08. Eighth IEEE International Conference on.
    .. [2] Liu, Fei Tony, Ting, Kai Ming and Zhou, Zhi-Hua. "Isolation-based
           anomaly detection." ACM Transactions on Knowledge Discovery from
           Data (TKDD) 6.1 (2012): 3.

    Examples
    --------
    >>> from sklearn.ensemble import IsolationForest
    >>> X = [[-1.1], [0.3], [0.5], [100]]
    >>> clf = IsolationForest(random_state=0).fit(X)
    >>> clf.predict([[0.1], [0], [90]])
    array([ 1,  1, -1])

    For an example of using isolation forest for anomaly detection see
    :ref:`sphx_glr_auto_examples_ensemble_plot_isolation_forest.py`.
    """

        ],
        "contamination": [
            Interval(Real, 0, 0.5, closed="right"),
        ],
            Integral,
            Interval(Real, 0, 1, closed="right"),
        ],
    }

        self,
        *,
        max_samples="auto",
        contamination="auto",
    ):
            estimator=None,
            # here above max_features has no links with self.max_features
            bootstrap_features=False,
        )

        return ExtraTreeRegressor(
            # here max_features has no links with self.max_features
            max_features=1,
            splitter="random",
            random_state=self.random_state,
        )

        raise NotImplementedError("OOB score not supported by iforest")

        # ExtraTreeRegressor releases the GIL, so it's more efficient to use
        # a thread-based backend rather than a process-based backend so as
        # to avoid suffering from communication overhead and extra memory
        # copies. This is only used in the fit method.
        return {"prefer": "threads"}

        """

        y : Ignored

        Returns
        -------
        """
            self, X, accept_sparse=["csc"], dtype=tree_dtype, ensure_all_finite=False
        )

        y = rnd.uniform(size=X.shape[0])

        # ensure that max_sample is in [1, n_samples]:

        if isinstance(self.max_samples, str) and self.max_samples == "auto":
            max_samples = min(256, n_samples)

        elif isinstance(self.max_samples, numbers.Integral):
            if self.max_samples > n_samples:
                warn(
                    "max_samples (%s) is greater than the "
                    "total number of samples (%s). max_samples "
                    "will be set to n_samples for estimation."
                    % (self.max_samples, n_samples)
                )
                max_samples = n_samples
            else:
                max_samples = self.max_samples
        else:  # max_samples is float
            max_samples = int(self.max_samples * X.shape[0])

        self.max_samples_ = max_samples
        max_depth = int(np.ceil(np.log2(max(max_samples, 2))))
        super()._fit(
            X,
            y,
            max_depth=max_depth,
        )

        self._average_path_length_per_tree, self._decision_path_lengths = zip(
            *[
                (
                    _average_path_length(tree.tree_.n_node_samples),
                    tree.tree_.compute_node_depths(),
                )
                for tree in self.estimators_
            ]
        )

        if self.contamination == "auto":
            # 0.5 plays a special role as described in the original paper.
            # we take the opposite as we consider the opposite of their score.
            self.offset_ = -0.5

        # Else, define offset_ wrt contamination parameter
        # To avoid performing input validation a second time we call
        # _score_samples rather than score_samples.
        # _score_samples expects a CSR matrix, so we convert if necessary.
        self.offset_ = np.percentile(self._score_samples(X), 100.0 * self.contamination)

        """
        Predict if a particular sample is an outlier or not.

            The input samples. Internally, it will be converted to
            ``dtype=np.float32`` and if a sparse matrix is provided
            to a sparse ``csr_matrix``.

        Returns
        -------
            For each observation, tells whether or not (+1 or -1) it should
            be considered as an inlier according to the fitted model.

        Notes
        -----
        The predict method can be parallelized by setting a joblib context. This
        inherently does NOT use the ``n_jobs`` parameter initialized in the class,
        which is used during ``fit``. This is because, predict may actually be faster
        without parallelization for a small number of samples,
        such as for 1000 samples or less. The user can set the
        number of jobs in the joblib context to control the number of parallel jobs.

        .. code-block:: python

            from joblib import parallel_backend

            # Note, we use threading here as the predict method is not CPU bound.
            with parallel_backend("threading", n_jobs=4):
                model.predict(X)
        """
        decision_func = self.decision_function(X)
        is_inlier = np.ones_like(decision_func, dtype=int)
        is_inlier[decision_func < 0] = -1

        """
        Average anomaly score of X of the base classifiers.

        The anomaly score of an input sample is computed as
        the mean anomaly score of the trees in the forest.

        The measure of normality of an observation given a tree is the depth
        of the leaf containing this observation, which is equivalent to
        the number of splittings required to isolate this point. In case of
        several observations n_left in the leaf, the average path length of
        a n_left samples isolation tree is added.

        Returns
        -------
        scores : ndarray of shape (n_samples,)
            The anomaly score of the input samples.
            The lower, the more abnormal. Negative scores represent outliers,
            positive scores represent inliers.

        Notes
        -----
        The decision_function method can be parallelized by setting a joblib context.
        This inherently does NOT use the ``n_jobs`` parameter initialized in the class,
        which is used during ``fit``. This is because, calculating the score may
        actually be faster without parallelization for a small number of samples,
        such as for 1000 samples or less.
        The user can set the number of jobs in the joblib context to control the
        number of parallel jobs.

            # Note, we use threading here as the decision_function method is
            # not CPU bound.
                model.decision_function(X)
        """
        # We subtract self.offset_ to make 0 be the threshold value for being
        # an outlier:

        return self.score_samples(X) - self.offset_

        """
        Opposite of the anomaly score defined in the original paper.

        Returns
        -------
            The lower, the more abnormal.

        Notes
        -----
        The score function method can be parallelized by setting a joblib context. This

            # Note, we use threading here as the score_samples method is not CPU bound.
                model.score(X)
        """
            self,
            X,
            dtype=tree_dtype,
        )

        return self._score_samples(X)

    def _score_samples(self, X):
        """Private version of score_samples without input validation.

        Input validation would remove feature names, so we disable it.
        """
        # Code structure from ForestClassifier/predict_proba

        # Take the opposite of the scores as bigger is better (here less abnormal)
        return -self._compute_chunked_score_samples(X)

    def _compute_chunked_score_samples(self, X):

        if self._max_features == X.shape[1]:
            subsample_features = False
        else:
            subsample_features = True

        # We get as many rows as possible within our working_memory budget
        # (defined by sklearn.get_config()['working_memory']) to store
        # self._max_features in each row during computation.
        #
        # Note:
        #  - this will get at least 1 row, even if 1 row of score will
        #    exceed working_memory.
        #  - this does only account for temporary memory usage while loading
        #    the data needed to compute the scores -- the returned scores
        #    themselves are 1D.

        chunk_n_rows = get_chunk_n_rows(
            row_bytes=16 * self._max_features, max_n_rows=n_samples
        )
        slices = gen_batches(n_samples, chunk_n_rows)

        scores = np.zeros(n_samples, order="f")

        for sl in slices:
            # compute score on the slices of test samples:
            scores[sl] = self._compute_score_samples(X[sl], subsample_features)

        return scores

    def _compute_score_samples(self, X, subsample_features):
        """
        Compute the score of each samples in X going through the extra trees.

        X : array-like or sparse matrix
            Data matrix.

        subsample_features : bool
            Whether features should be subsampled.

        Returns
        -------
            The score of each sample in X.
        """

        depths = np.zeros(n_samples, order="f")

        average_path_length_max_samples = _average_path_length([self._max_samples])

        # Note: we use default n_jobs value, i.e. sequential computation, which
        # we expect to be more performant that parallelizing for small number
        # of samples, e.g. < 1k samples. Default n_jobs value can be overridden
        # by using joblib.parallel_backend context manager around
        # ._compute_score_samples. Using a higher n_jobs may speed up the
        # computation of the scores, e.g. for > 1k samples. See
        # https://github.com/scikit-learn/scikit-learn/pull/28622 for more
        # details.
        Parallel(
            require="sharedmem",
        )(
            delayed(_parallel_compute_tree_depths)(
                tree,
                X,
                features if subsample_features else None,
                self._decision_path_lengths[tree_idx],
                self._average_path_length_per_tree[tree_idx],
                depths,
                lock,
            )
            for tree_idx, (tree, features) in enumerate(
                zip(self.estimators_, self.estimators_features_)
            )
        )

        denominator = len(self.estimators_) * average_path_length_max_samples
        scores = 2 ** (
            # For a single training sample, denominator and depth are 0.
            # Therefore, we set the score manually to 1.
            -np.divide(
                depths, denominator, out=np.ones_like(depths), where=denominator != 0
            )
        )

        tags.input_tags.allow_nan = True

def _average_path_length(n_samples_leaf):
    """
    The average path length in a n_samples iTree, which is equal to
    the average path length of an unsuccessful BST search since the
    latter has the same structure as an isolation tree.
    n_samples_leaf : array-like of shape (n_samples,)
        The number of training samples in each test sample leaf, for
        each estimators.

    Returns
    -------
    average_path_length : ndarray of shape (n_samples,)
    """

    n_samples_leaf = check_array(n_samples_leaf, ensure_2d=False)

    n_samples_leaf_shape = n_samples_leaf.shape
    n_samples_leaf = n_samples_leaf.reshape((1, -1))
    average_path_length = np.zeros(n_samples_leaf.shape)

    mask_1 = n_samples_leaf <= 1
    mask_2 = n_samples_leaf == 2
    not_mask = ~np.logical_or(mask_1, mask_2)

    average_path_length[mask_1] = 0.0
    average_path_length[mask_2] = 1.0
    average_path_length[not_mask] = (
        2.0 * (np.log(n_samples_leaf[not_mask] - 1.0) + np.euler_gamma)
        - 2.0 * (n_samples_leaf[not_mask] - 1.0) / n_samples_leaf[not_mask]
    )

    return average_path_length.reshape(n_samples_leaf_shape)

        self,
        *,
    ):
        )

        )

        """

        y : Ignored

        Returns
        -------
        """
        )

                warn(
                )
            else:

            X,
            y,
        )

            *[
                (
                )
            ]
        )

        """

        Returns
        -------

        Notes
        -----

        """

        """

        Returns
        -------

        Notes
        -----

        """

        """

        Returns
        -------

        Notes
        -----

        """
            self,
            X,
        )

        """

        else:

        #
        # Note:

        )

        """

        Returns
        -------
        """

        # details.
        Parallel(
        )(
                tree,
                X,
                depths,
                lock,
            )
            )
        )

            )
        )

class _BaseStacking(TransformerMixin, _BaseHeterogeneousEnsemble, metaclass=ABCMeta):
    """Base class for stacking method."""

        "estimators": [list],
        "final_estimator": [None, HasMethods("fit")],
        "passthrough": ["boolean"],
    }

        self,
        estimators,
        final_estimator=None,
        *,
        cv=None,
        stack_method="auto",
        passthrough=False,
    ):
        super().__init__(estimators=estimators)
        self.final_estimator = final_estimator
        self.stack_method = stack_method
        self.passthrough = passthrough

    def _clone_final_estimator(self, default):
        if self.final_estimator is not None:
            self.final_estimator_ = clone(self.final_estimator)
        else:
            self.final_estimator_ = clone(default)

    def _concatenate_predictions(self, X, predictions):
        """Concatenate the predictions of each first layer learner and
        possibly the input dataset `X`.

        If `X` is sparse and `self.passthrough` is False, the output of
        `transform` will be dense (the predictions). If `X` is sparse
        and `self.passthrough` is True, the output of `transform` will
        be sparse.

        This helper is in charge of ensuring the predictions are 2D arrays and
        it will drop one of the probability column when using probabilities
        in the binary case. Indeed, the p(y|c=0) = 1 - p(y|c=1)

        When `y` type is `"multilabel-indicator"`` and the method used is
        `predict_proba`, `preds` can be either a `ndarray` of shape
        `(n_samples, n_class)` or for some estimators a list of `ndarray`.
        This function will drop one of the probability column in this situation as well.
        """
        X_meta = []
        for est_idx, preds in enumerate(predictions):
            if isinstance(preds, list):
                # `preds` is here a list of `n_targets` 2D ndarrays of
                # `n_classes` columns. The k-th column contains the
                # probabilities of the samples belonging the k-th class.
                #
                # Since those probabilities must sum to one for each sample,
                # we can work with probabilities of `n_classes - 1` classes.
                # Hence we drop the first column.
                for pred in preds:
                    X_meta.append(pred[:, 1:])
            elif preds.ndim == 1:
                # Some estimator return a 1D array for predictions
                # which must be 2-dimensional arrays.
                X_meta.append(preds.reshape(-1, 1))
            elif (
                self.stack_method_[est_idx] == "predict_proba"
                and len(self.classes_) == 2
            ):
                # Remove the first column when using probabilities in
                # binary classification because both features `preds` are perfectly
                # collinear.
                X_meta.append(preds[:, 1:])
            else:
                X_meta.append(preds)

        self._n_feature_outs = [pred.shape[1] for pred in X_meta]
        if self.passthrough:
            X_meta.append(X)
            if sparse.issparse(X):
                return sparse.hstack(X_meta, format=X.format)

        return np.hstack(X_meta)

    def _method_name(name, estimator, method):
        if estimator == "drop":
        if method == "auto":
            method = ["predict_proba", "decision_function", "predict"]
        try:
            method_name = _check_response_method(estimator, method).__name__
                f"Underlying estimator {name} does not implement the method {method}."
            ) from e

        return method_name

        # estimators in Stacking*.estimators are not validated yet
    )
        """Fit the estimators.

            Dict of metadata, potentially containing sample_weight as a
            key-value pair. If sample_weight is not present, then samples are
            equally weighted. Note that sample_weight is supported only if all
            underlying estimators support sample weights.

            .. versionadded:: 1.6

        Returns
        -------
        """
        # all_estimators contains all estimators, the one to be fitted and the
        # 'drop' string.
        names, all_estimators = self._validate_estimators()
        self._validate_final_estimator()

        stack_method = [self.stack_method] * len(all_estimators)

        else:
            for name in names:
                if "sample_weight" in fit_params:
                    routed_params[name].fit["sample_weight"] = fit_params[
                        "sample_weight"
                    ]

            for estimator in all_estimators:
                if estimator != "drop":
                    check_is_fitted(estimator)
                    self.estimators_.append(estimator)
        else:
            # Fit the base estimators on the whole training data. Those
            # base estimators will be used in transform, predict, and
            # predict_proba. They are exposed publicly.
            self.estimators_ = Parallel(n_jobs=self.n_jobs)(
                delayed(_fit_single_estimator)(
                    clone(est), X, y, routed_params[name]["fit"]
                )
                for name, est in zip(names, all_estimators)
                if est != "drop"
            )

        self.named_estimators_ = Bunch()
        est_fitted_idx = 0
        for name_est, org_est in zip(names, all_estimators):
            if org_est != "drop":
                current_estimator = self.estimators_[est_fitted_idx]
                self.named_estimators_[name_est] = current_estimator
                est_fitted_idx += 1
                if hasattr(current_estimator, "feature_names_in_"):
                    self.feature_names_in_ = current_estimator.feature_names_in_
            else:
                self.named_estimators_[name_est] = "drop"

        self.stack_method_ = [
            self._method_name(name, est, meth)
            for name, est, meth in zip(names, all_estimators, stack_method)
        ]

            # Generate predictions from prefit models
            predictions = [
                getattr(estimator, predict_method)(X)
                for estimator, predict_method in zip(all_estimators, self.stack_method_)
                if estimator != "drop"
            ]
        else:
            # To train the meta-classifier using the most data as possible, we use
            # a cross-validation to obtain the output of the stacked estimators.
            # To ensure that the data provided to each estimator are the same,
            # we need to set the random state of the cv if there is one and we
            # need to take a copy.
            cv = check_cv(self.cv, y=y, classifier=is_classifier(self))
            if hasattr(cv, "random_state") and cv.random_state is None:
                cv.random_state = np.random.RandomState()

            predictions = Parallel(n_jobs=self.n_jobs)(
                delayed(cross_val_predict)(
                    clone(est),
                    X,
                    y,
                    cv=deepcopy(cv),
                    method=meth,
                    params=routed_params[name]["fit"],
                )
                for name, est, meth in zip(names, all_estimators, self.stack_method_)
            )

        # Only not None or not 'drop' estimators will be used in transform.
        # Remove the None from the method as well.
            meth
            for (meth, est) in zip(self.stack_method_, all_estimators)
            if est != "drop"
        ]

        X_meta = self._concatenate_predictions(X, predictions)
        _fit_single_estimator(self.final_estimator_, X_meta, y, fit_params=fit_params)

    @property
        try:
            check_is_fitted(self)
        except NotFittedError as nfe:
                f"{self.__class__.__name__} object has no attribute n_features_in_"
            ) from nfe
        return self.estimators_[0].n_features_in_

    def _transform(self, X):
        """Concatenate and return the predictions of the estimators."""
        predictions = [
            getattr(est, meth)(X)
            for est, meth in zip(self.estimators_, self.stack_method_)
        ]
        return self._concatenate_predictions(X, predictions)

            Input features. The input feature names are only used when `passthrough` is
            `True`.

              then names are generated: `[x0, x1, ..., x(n_features_in_ - 1)]`.

            If `passthrough` is `False`, then only the names of `estimators` are used
            to generate the output feature names.

        Returns
        -------
        """
        check_is_fitted(self, "n_features_in_")
            self, input_features, generate_names=self.passthrough
        )

        non_dropped_estimators = (
            name for name, est in self.estimators if est != "drop"
        )
        meta_names = []
        for est, n_features_out in zip(non_dropped_estimators, self._n_feature_outs):
            if n_features_out == 1:
                meta_names.append(f"{class_name}_{est}")
            else:
                meta_names.extend(
                    f"{class_name}_{est}{i}" for i in range(n_features_out)
                )

            return np.concatenate((meta_names, input_features))

        return np.asarray(meta_names, dtype=object)

        _estimator_has("predict", delegates=("final_estimator_", "final_estimator"))
    )
    def predict(self, X, **predict_params):
        """Predict target for X.

        **predict_params : dict of str -> obj
            Parameters to the `predict` called by the `final_estimator`. Note
            that this may be used to return uncertainties from some estimators
            with `return_std` or `return_cov`. Be aware that it will only
            account for uncertainty in the final estimator.

        Returns
        -------
        y_pred : ndarray of shape (n_samples,) or (n_samples, n_output)
            Predicted targets.
        """

        return self.final_estimator_.predict(self.transform(X), **predict_params)

    def _sk_visual_block_with_final_estimator(self, final_estimator):
        parallel = _VisualBlock("parallel", estimators, names=names, dash_wrapped=False)

        # final estimator is wrapped in a parallel block to show the label:
        # 'final_estimator' in the html repr
        final_block = _VisualBlock(
            "parallel", [final_estimator], names=["final_estimator"], dash_wrapped=False
        )
        return _VisualBlock("serial", (parallel, final_block), dash_wrapped=False)

        Returns
        -------
        """

        # `self.estimators` is a list of (name, est) tuples
        for name, estimator in self.estimators:
                **{name: estimator},
                method_mapping=MethodMapping().add(callee="fit", caller="fit"),
            )

        try:
            final_estimator_ = self.final_estimator_
            final_estimator_ = self.final_estimator

        router.add(
            final_estimator_=final_estimator_,
            method_mapping=MethodMapping().add(caller="predict", callee="predict"),
        )

class StackingClassifier(ClassifierMixin, _BaseStacking):
    """Stack of estimators with a final classifier.

    Stacked generalization consists in stacking the output of individual
    estimator and use a classifier to compute the final prediction. Stacking
    allows to use the strength of each individual estimator by using their
    output as input of a final estimator.

    Note that `estimators_` are fitted on the full `X` while `final_estimator_`
    is trained using cross-validated predictions of the base estimators using
    `cross_val_predict`.

    Read more in the :ref:`User Guide <stacking>`.

    estimators : list of (str, estimator)
        Base estimators which will be stacked together. Each element of the
        list is defined as a tuple of string (i.e. name) and an estimator
        instance. An estimator can be set to 'drop' using `set_params`.

        The type of estimator is generally expected to be a classifier.
        However, one can pass a regressor for some use case (e.g. ordinal
        regression).

    final_estimator : estimator, default=None
        A classifier which will be used to combine the base estimators.
        The default classifier is a
        :class:`~sklearn.linear_model.LogisticRegression`.

    cv : int, cross-validation generator, iterable, or "prefit", default=None
        Determines the cross-validation splitting strategy used in
        `cross_val_predict` to train `final_estimator`. Possible inputs for
        cv are:

        * None, to use the default 5-fold cross validation,
        * integer, to specify the number of folds in a (Stratified) KFold,
        * An object to be used as a cross-validation generator,
        * An iterable yielding train, test splits,
        * `"prefit"`, to assume the `estimators` are prefit. In this case, the
          estimators will not be refitted.

        For integer/None inputs, if the estimator is a classifier and y is
        either binary or multiclass,
        :class:`~sklearn.model_selection.StratifiedKFold` is used.
        In all other cases, :class:`~sklearn.model_selection.KFold` is used.
        These splitters are instantiated with `shuffle=False` so the splits
        will be the same across calls.

        If "prefit" is passed, it is assumed that all `estimators` have
        been fitted already. The `final_estimator_` is trained on the `estimators`
        predictions on the full training set and are **not** cross validated
        predictions. Please note that if the models have been trained on the same
        data to train the stacking model, there is a very high risk of overfitting.

            The 'prefit' option was added in 1.1

        .. note::
           A larger number of split will provide no benefits if the number
           of training samples is large enough. Indeed, the training time
           will increase. ``cv`` is not used for model evaluation but for
           prediction.

    stack_method : {'auto', 'predict_proba', 'decision_function', 'predict'}, \
            default='auto'
        Methods called for each base estimator. It can be:

        * if 'auto', it will try to invoke, for each estimator,
          `'predict_proba'`, `'decision_function'` or `'predict'` in that
          order.
        * otherwise, one of `'predict_proba'`, `'decision_function'` or
          `'predict'`. If the method is not implemented by the estimator, it
          will raise an error.

        The number of jobs to run in parallel for `fit` of all `estimators`.
        `None` means 1 unless in a `joblib.parallel_backend` context. -1 means
        using all processors. See :term:`Glossary <n_jobs>` for more details.

    passthrough : bool, default=False
        When False, only the predictions of estimators will be used as
        training data for `final_estimator`. When True, the
        `final_estimator` is trained on the predictions as well as the
        original training data.

        Verbosity level.

    classes_ : ndarray of shape (n_classes,) or list of ndarray if `y` \
        is of type `"multilabel-indicator"`.

        The elements of the `estimators` parameter, having been fitted on the
        training data. If an estimator has been set to `'drop'`, it
        will not appear in `estimators_`. When `cv="prefit"`, `estimators_`
        is set to `estimators` and is not fitted again.

    named_estimators_ : :class:`~sklearn.utils.Bunch`
        Attribute to access any fitted sub-estimators by name.

    final_estimator_ : estimator
        The classifier fit on the output of `estimators_` and responsible for
        final predictions.

    stack_method_ : list of str
        The method used by each base estimator.

    See Also
    --------
    StackingRegressor : Stack of estimators with a final regressor.

    Notes
    -----
    When `predict_proba` is used by each estimator (i.e. most of the time for
    `stack_method='auto'` or specifically for `stack_method='predict_proba'`),
    the first column predicted by each estimator will be dropped in the case
    of a binary classification problem. Indeed, both feature will be perfectly
    collinear.

    In some cases (e.g. ordinal regression), one can pass regressors as the
    first layer of the :class:`StackingClassifier`. However, note that `y` will
    be internally encoded in a numerically increasing order or lexicographic
    order. If this ordering is not adequate, one should manually numerically
    encode the classes in the desired order.

    .. [1] Wolpert, David H. "Stacked generalization." Neural networks 5.2
       (1992): 241-259.

    Examples
    --------
    >>> from sklearn.ensemble import StackingClassifier
    >>> estimators = [
    ...     ('rf', RandomForestClassifier(n_estimators=10, random_state=42)),
    ...     ('svr', make_pipeline(StandardScaler(),
    ...                           LinearSVC(random_state=42)))
    ... ]
    >>> clf = StackingClassifier(
    ...     estimators=estimators, final_estimator=LogisticRegression()
    ... )
    ...     X, y, stratify=y, random_state=42
    ... )
    >>> clf.fit(X_train, y_train).score(X_test, y_test)
    0.9...
    """

        **_BaseStacking._parameter_constraints,
        "stack_method": [
            StrOptions({"auto", "predict_proba", "decision_function", "predict"})
        ],
    }

        self,
        *,
        cv=None,
    ):
            estimators=estimators,
            final_estimator=final_estimator,
            cv=cv,
            stack_method=stack_method,
            passthrough=passthrough,
        )

    def _validate_final_estimator(self):
        self._clone_final_estimator(default=LogisticRegression())
        if not is_classifier(self.final_estimator_):
                "'final_estimator' parameter should be a classifier. Got {}".format(
                    self.final_estimator_
                )
            )

        """Overload the method of `_BaseHeterogeneousEnsemble` to be more
        lenient towards the type of `estimators`.

        Regressors can be accepted for some cases such as ordinal regression.
        """
        if len(self.estimators) == 0:
            )

            )

            Target values. Note that `y` will be internally encoded in
            numerically increasing order or lexicographic order. If the order
            matter (e.g. for ordinal regression), one should numerically encode
            the target `y` before calling :term:`fit`.

                Only available if `enable_metadata_routing=True`, which can be
                set by using ``sklearn.set_config(enable_metadata_routing=True)``.

        Returns
        -------
            Returns a fitted instance of estimator.
        """
        _raise_for_params(fit_params, self, "fit", allow=["sample_weight"])

        if type_of_target(y) == "multilabel-indicator":
            self._label_encoder = [LabelEncoder().fit(yk) for yk in y.T]
            self.classes_ = [le.classes_ for le in self._label_encoder]
            y_encoded = np.array(
                [
                    self._label_encoder[target_idx].transform(target)
                    for target_idx, target in enumerate(y.T)
                ]
            ).T
        else:
            self._label_encoder = LabelEncoder().fit(y)
            self.classes_ = self._label_encoder.classes_
            y_encoded = self._label_encoder.transform(y)

        return super().fit(X, y_encoded, **fit_params)

    )

              Parameters directly passed to the `predict` method of the
              `final_estimator`.

            - If `enable_metadata_routing=True`: Parameters safely routed to
              the `predict` method of the `final_estimator`. See :ref:`Metadata
              Routing User Guide <metadata_routing>` for more details.

            .. versionchanged:: 1.6
                `**predict_params` can be routed via metadata routing API.

        Returns
        -------
        """
            routed_params = process_routing(self, "predict", **predict_params)
        else:
            routed_params.final_estimator_ = Bunch(predict={})
            routed_params.final_estimator_.predict = predict_params

        y_pred = super().predict(X, **routed_params.final_estimator_["predict"])
        if isinstance(self._label_encoder, list):
            # Handle the multilabel-indicator case
            y_pred = np.array(
                [
                    self._label_encoder[target_idx].inverse_transform(target)
                    for target_idx, target in enumerate(y_pred.T)
                ]
            ).T
        else:
            y_pred = self._label_encoder.inverse_transform(y_pred)

        _estimator_has(
            "predict_proba", delegates=("final_estimator_", "final_estimator")
        )
    )
        """Predict class probabilities for `X` using the final estimator.

        Returns
        -------
        probabilities : ndarray of shape (n_samples, n_classes) or \
            list of ndarray of shape (n_output,)
            The class probabilities of the input samples.
        """
        y_pred = self.final_estimator_.predict_proba(self.transform(X))

            # Handle the multilabel-indicator cases
            y_pred = np.array([preds[:, 0] for preds in y_pred]).T

            "decision_function", delegates=("final_estimator_", "final_estimator")
        )
    )
        """Decision function for samples in `X` using the final estimator.

        Returns
        -------
        decisions : ndarray of shape (n_samples,), (n_samples, n_classes), \
            or (n_samples, n_classes * (n_classes-1) / 2)
            The decision function computed the final estimator.
        """
        return self.final_estimator_.decision_function(self.transform(X))

        """Return class labels or probabilities for X for each estimator.

        Returns
        -------
        y_preds : ndarray of shape (n_samples, n_estimators) or \
                (n_samples, n_classes * n_estimators)
            Prediction outputs for each estimator.
        """
        return self._transform(X)

        # If final_estimator's default changes then this should be
        # updated.
        if self.final_estimator is None:
            final_estimator = LogisticRegression()
        else:
            final_estimator = self.final_estimator
        return super()._sk_visual_block_with_final_estimator(final_estimator)

class StackingRegressor(RegressorMixin, _BaseStacking):
    """Stack of estimators with a final regressor.

    estimator and use a regressor to compute the final prediction. Stacking

        A regressor which will be used to combine the base estimators.
        The default regressor is a :class:`~sklearn.linear_model.RidgeCV`.

        cv are:

        .. note::

        The regressor fit on the output of `estimators_` and responsible for

    See Also
    --------
    StackingClassifier : Stack of estimators with a final classifier.

    Examples
    --------
    >>> from sklearn.linear_model import RidgeCV
    >>> from sklearn.svm import LinearSVR
    >>> from sklearn.ensemble import StackingRegressor
    ...     ('lr', RidgeCV()),
    ...     ('svr', LinearSVR(random_state=42))
    ... ]
    >>> reg = StackingRegressor(
    ...     estimators=estimators,
    ...     final_estimator=RandomForestRegressor(n_estimators=10,
    ...                                           random_state=42)
    ... )
    ...     X, y, random_state=42
    ... )
    >>> reg.fit(X_train, y_train).score(X_test, y_test)
    0.3...
    """

        self,
        *,
        cv=None,
    ):
            cv=cv,
            stack_method="predict",
        )

        self._clone_final_estimator(default=RidgeCV())
        if not is_regressor(self.final_estimator_):
                "'final_estimator' parameter should be a regressor. Got {}".format(
                )
            )

        Returns
        -------
        """

        return super().fit(X, y, **fit_params)

        """Return the predictions for X for each estimator.

        Returns
        -------
        y_preds : ndarray of shape (n_samples, n_estimators)
        """

    def fit_transform(self, X, y, **fit_params):
        """Fit the estimators and return the predictions for X for each estimator.

        Returns
        -------
        """

        return super().fit_transform(X, y, **fit_params)

    )

        Returns
        -------
        """
        else:

        # updated.
            final_estimator = RidgeCV()
        else:

        self,
        *,
        cv=None,
    ):

        else:

        be sparse.

        """
        X_meta = []
                #
            elif (
            ):
            else:

        try:
            ) from e

        Returns
        -------
        """

        else:
                    ]

        else:
                )
            )

            else:

        ]

            ]
        else:

                    X,
                    y,
                )
            )

            meth
        ]

        try:
            ) from nfe

        ]

            `True`.

        Returns
        -------
        """
        )

        )
            else:
                )

        Returns
        -------
        """

        )

        Returns
        -------
        """

            )

        try:

        )

        self,
        *,
        cv=None,
    ):
            cv=cv,
        )

                )
            )

        """
            )

            )

        Returns
        -------
        """

                [
                ]
            ).T
        else:

        Returns
        -------
        """
        else:

                [
                ]
            ).T
        else:

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        # updated.
        else:

        self,
        *,
        cv=None,
    ):
            cv=cv,
        )

                )
            )

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """
        else:

        # updated.
        else:

class _BaseVoting(TransformerMixin, _BaseHeterogeneousEnsemble):
    """Base class for voting.

    instead.
    """

        "weights": ["array-like", None],
    }

        return f"({idx} of {total}) Processing {name}"

    @property
    def _weights_not_none(self):
        """Get the weights of not `None` estimators."""
        if self.weights is None:
        return [w for est, w in zip(self.estimators, self.weights) if est[1] != "drop"]

    def _predict(self, X):
        """Collect results from clf.predict calls."""
        return np.asarray([est.predict(X) for est in self.estimators_]).T

        """Get common fit operations."""
        names, clfs = self._validate_estimators()

        if self.weights is not None and len(self.weights) != len(self.estimators):
                "Number of `estimators` and weights must be equal; got"
                f" {len(self.weights)} weights, {len(self.estimators)} estimators"
            )

        else:
                    ]

            delayed(_fit_single_estimator)(
                clone(clf),
                X,
                y,
                fit_params=routed_params[name]["fit"],
                message_clsname="Voting",
                message=self._log_message(name, idx + 1, len(clfs)),
            )
            for idx, (name, clf) in enumerate(zip(names, clfs))
            if clf != "drop"
        )

        # Uses 'drop' as placeholder for dropped estimators
        est_iter = iter(self.estimators_)
        for name, est in self.estimators:
            current_est = est if est == "drop" else next(est_iter)
            self.named_estimators_[name] = current_est

            if hasattr(current_est, "feature_names_in_"):
                self.feature_names_in_ = current_est.feature_names_in_

        """Return class labels or probabilities for each estimator.

        Return predictions for X for each estimator.

        X : {array-like, sparse matrix, dataframe} of shape \
                (n_samples, n_features)

        y : ndarray of shape (n_samples,), default=None

        Returns
        -------
        """

    @property
        # For consistency with other estimators we raise a AttributeError so
        # that hasattr() fails if the estimator isn't fitted.
        try:
                "{} object has no n_features_in_ attribute.".format(
                    self.__class__.__name__
                )
            ) from nfe

        return _VisualBlock("parallel", estimators, names=names)

        Returns
        -------
        """

            )

class VotingClassifier(ClassifierMixin, _BaseVoting):
    """Soft Voting/Majority Rule classifier for unfitted estimators.

    Read more in the :ref:`User Guide <voting_classifier>`.

        Invoking the ``fit`` method on the ``VotingClassifier`` will fit clones
        of those original estimators that will be stored in the class attribute
        ``self.estimators_``. An estimator can be set to ``'drop'`` using
        :meth:`set_params`.

        .. versionchanged:: 0.21
            ``'drop'`` is accepted. Using None was deprecated in 0.22 and
            support was removed in 0.24.

    voting : {'hard', 'soft'}, default='hard'
        If 'hard', uses predicted class labels for majority rule voting.
        Else if 'soft', predicts the class label based on the argmax of
        the sums of the predicted probabilities, which is recommended for
        an ensemble of well-calibrated classifiers.

    weights : array-like of shape (n_classifiers,), default=None
        Sequence of weights (`float` or `int`) to weight the occurrences of
        predicted class labels (`hard` voting) or class probabilities
        before averaging (`soft` voting). Uses uniform weights if `None`.

        The number of jobs to run in parallel for ``fit``.

    flatten_transform : bool, default=True
        Affects shape of transform output only when voting='soft'
        If voting='soft' and flatten_transform=True, transform method returns
        matrix with shape (n_samples, n_classifiers * n_classes). If
        flatten_transform=False, it returns
        (n_classifiers, n_samples, n_classes).

        If True, the time elapsed while fitting will be printed as it

        The collection of fitted sub-estimators as defined in ``estimators``
        that are not 'drop'.

    le_ : :class:`~sklearn.preprocessing.LabelEncoder`
        Transformer used to encode the labels during fit and decode during
        prediction.

        underlying classifier exposes such an attribute when fit.

    See Also
    --------
    VotingRegressor : Prediction voting regressor.

    Examples
    --------
    >>> from sklearn.ensemble import RandomForestClassifier, VotingClassifier
    >>> clf1 = LogisticRegression(random_state=1)
    >>> clf2 = RandomForestClassifier(n_estimators=50, random_state=1)
    >>> clf3 = GaussianNB()
    >>> eclf1 = VotingClassifier(estimators=[
    ...         ('lr', clf1), ('rf', clf2), ('gnb', clf3)], voting='hard')
    >>> eclf1 = eclf1.fit(X, y)
    >>> print(eclf1.predict(X))
    [1 1 1 2 2 2]
    >>> np.array_equal(eclf1.named_estimators_.lr.predict(X),
    ...                eclf1.named_estimators_['lr'].predict(X))
    True
    >>> eclf2 = VotingClassifier(estimators=[
    ...         ('lr', clf1), ('rf', clf2), ('gnb', clf3)],
    ...         voting='soft')
    >>> eclf2 = eclf2.fit(X, y)
    >>> print(eclf2.predict(X))
    [1 1 1 2 2 2]

    To drop an estimator, :meth:`set_params` can be used to remove it. Here we
    dropped one of the estimators, resulting in 2 fitted estimators:

    >>> eclf2 = eclf2.set_params(lr='drop')
    >>> len(eclf2.estimators_)
    2

    Setting `flatten_transform=True` with `voting='soft'` flattens output shape of
    `transform`:

    >>> eclf3 = VotingClassifier(estimators=[
    ...        ('lr', clf1), ('rf', clf2), ('gnb', clf3)],
    ...        voting='soft', weights=[2,1,1],
    ...        flatten_transform=True)
    >>> eclf3 = eclf3.fit(X, y)
    >>> print(eclf3.predict(X))
    [1 1 1 2 2 2]
    >>> print(eclf3.transform(X).shape)
    (6, 6)
    """

        **_BaseVoting._parameter_constraints,
        "voting": [StrOptions({"hard", "soft"})],
        "flatten_transform": ["boolean"],
    }

        self,
        *,
        voting="hard",
        weights=None,
        flatten_transform=True,
    ):
        self.voting = voting
        self.weights = weights
        self.flatten_transform = flatten_transform

        # estimators in VotingClassifier.estimators are not validated yet
    )

        Returns
        -------
        """

        y_type = type_of_target(y, input_name="y")
        if y_type in ("unknown", "continuous"):
            # raise a specific ValueError for non-classification tasks
                f"Unknown label type: {y_type}. Maybe you are trying to fit a "
                "classifier, which expects discrete classes on a "
                "regression target with continuous values."
            )
        elif y_type not in ("binary", "multiclass"):
            # raise a NotImplementedError for backward compatibility for non-supported
            # classification tasks
                f"{self.__class__.__name__} only supports binary or multiclass "
                "classification. Multilabel and multi-output classification are not "
                "supported."
            )

        self.le_ = LabelEncoder().fit(y)
        self.classes_ = self.le_.classes_
        transformed_y = self.le_.transform(y)

        return super().fit(X, transformed_y, **fit_params)

        """Predict class labels for X.

        Returns
        -------
        maj : array-like of shape (n_samples,)
            Predicted class labels.
        """
        if self.voting == "soft":
            maj = np.argmax(self.predict_proba(X), axis=1)

        else:  # 'hard' voting
            predictions = self._predict(X)
            maj = np.apply_along_axis(
                lambda x: np.argmax(np.bincount(x, weights=self._weights_not_none)),
                axis=1,
                arr=predictions,
            )

        maj = self.le_.inverse_transform(maj)

        return maj

    def _collect_probas(self, X):
        return np.asarray([clf.predict_proba(X) for clf in self.estimators_])

    def _check_voting(self):
        if self.voting == "hard":
                f"predict_proba is not available when voting={self.voting!r}"
            )

    @available_if(_check_voting)
        """Compute probabilities of possible outcomes for samples in X.

        Returns
        -------
        avg : array-like of shape (n_samples, n_classes)
            Weighted average probability for each class per sample.
        """
        avg = np.average(
            self._collect_probas(X), axis=0, weights=self._weights_not_none
        )
        return avg

        Returns
        -------
        probabilities_or_labels
            If `voting='soft'` and `flatten_transform=True`:
                returns ndarray of shape (n_samples, n_classifiers * n_classes),
                being class probabilities calculated by each classifier.
            If `voting='soft' and `flatten_transform=False`:
                ndarray of shape (n_classifiers, n_samples, n_classes)
            If `voting='hard'`:
                ndarray of shape (n_samples, n_classifiers), being
                class labels predicted by each classifier.
        """

            probas = self._collect_probas(X)
            if not self.flatten_transform:
                return probas
            return np.hstack(probas)

        else:
            return self._predict(X)

        Returns
        -------
        """
        if self.voting == "soft" and not self.flatten_transform:
                "get_feature_names_out is not supported when `voting='soft'` and "
                "`flatten_transform=False`"
            )

        _check_feature_names_in(self, input_features, generate_names=False)

        active_names = [name for name, est in self.estimators if est != "drop"]

            return np.asarray(
                [f"{class_name}_{name}" for name in active_names], dtype=object
            )

        # voting == "soft"
        names_out = [
            f"{class_name}_{name}{i}" for name in active_names for i in range(n_classes)
        ]
        return np.asarray(names_out, dtype=object)

        tags.transformer_tags.preserves_dtype = []

class VotingRegressor(RegressorMixin, _BaseVoting):
    """Prediction voting regressor for unfitted estimators.

    A voting regressor is an ensemble meta-estimator that fits several base
    regressors, each on the whole dataset. Then it averages the individual
    predictions to form a final prediction.

    For a detailed example, refer to
    :ref:`sphx_glr_auto_examples_ensemble_plot_voting_regressor.py`.

    Read more in the :ref:`User Guide <voting_regressor>`.

    .. versionadded:: 0.21

        Invoking the ``fit`` method on the ``VotingRegressor`` will fit clones

    weights : array-like of shape (n_regressors,), default=None
        predicted values before averaging. Uses uniform weights if `None`.

        underlying regressor exposes such an attribute when fit.

    See Also
    --------
    VotingClassifier : Soft Voting/Majority Rule classifier.

    Examples
    --------
    >>> from sklearn.linear_model import LinearRegression
    >>> from sklearn.ensemble import VotingRegressor
    >>> from sklearn.neighbors import KNeighborsRegressor
    >>> r1 = LinearRegression()
    >>> r2 = RandomForestRegressor(n_estimators=10, random_state=1)
    >>> r3 = KNeighborsRegressor()
    >>> X = np.array([[1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36]])
    >>> y = np.array([2, 6, 12, 20, 30, 42])
    >>> er = VotingRegressor([('lr', r1), ('rf', r2), ('r3', r3)])
    >>> print(er.fit(X, y).predict(X))
    [ 6.8...  8.4... 12.5... 17.8... 26...  34...]

    In the following example, we drop the `'lr'` estimator with
    :meth:`~VotingRegressor.set_params` and fit the remaining two estimators:

    >>> er = er.set_params(lr='drop')
    >>> er = er.fit(X, y)
    >>> len(er.estimators_)
    2
    """

    def __init__(self, estimators, *, weights=None, n_jobs=None, verbose=False):

        # estimators in VotingRegressor.estimators are not validated yet
    )

        Returns
        -------
        """

        Returns
        -------
        """
        return np.average(self._predict(X), axis=1, weights=self._weights_not_none)

        """Return predictions for X for each estimator.

        Returns
        -------
        predictions : ndarray of shape (n_samples, n_classifiers)
            Values predicted by each regressor.
        """
        return self._predict(X)

        Returns
        -------
        """
        return np.asarray(
            [f"{class_name}_{name}" for name, est in self.estimators if est != "drop"],
            dtype=object,
        )

            )

        else:
                    ]

                X,
                y,
            )
        )

        Returns
        -------
        """

        try:
                )
            ) from nfe

        Returns
        -------
        """

            )

        self,
        *,
    ):

        Returns
        -------
        """

            )
            )

        Returns
        -------
        """

                axis=1,
            )

        return maj

            )

        Returns
        -------
        """
        )
        return avg

        Returns
        -------
        """

        else:

        Returns
        -------
        """
            )

            )

        ]

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """
        )

def _safe_divide(numerator, denominator):
    """Prevents overflow and division by zero."""
    # This is used for classifiers where the denominator might become zero exactly.
    # For instance for log loss, HalfBinomialLoss, if proba=0 or proba=1 exactly, then
    # denominator = hessian = 0, and we should set the node value in the line search to
    # zero as there is no improvement of the loss possible.
    # For numerical safety, we do this already for extremely tiny values.
    if abs(denominator) < 1e-150:
        return 0.0
    else:
        # Cast to Python float to trigger Python errors, e.g. ZeroDivisionError,
        # without relying on `np.errstate` that is not supported by Pyodide.
        result = float(numerator) / float(denominator)
        # Cast to Python float to trigger a ZeroDivisionError without relying
        # on `np.errstate` that is not supported by Pyodide.
        if math.isinf(result):
            warnings.warn("overflow encountered in _safe_divide", RuntimeWarning)

def _init_raw_predictions(X, estimator, loss, use_predict_proba):
    """Return the initial raw predictions.

        The data array.
        The estimator to use to compute the predictions.
    loss : BaseLoss
        An instance of a loss function class.
    use_predict_proba : bool
        Whether estimator.predict_proba is used instead of estimator.predict.

    Returns
    -------
    raw_predictions : ndarray of shape (n_samples, K)
        The initial raw predictions. K is equal to 1 for binary
        classification and regression, and equal to the number of classes
        for multiclass classification. ``raw_predictions`` is casted
        into float64.
    """
    # TODO: Use loss.fit_intercept_only where appropriate instead of
    # DummyRegressor which is the default given by the `init` parameter,
    # see also _init_state.
    if use_predict_proba:
        # Our parameter validation, set via _fit_context and _parameter_constraints
        # already guarantees that estimator has a predict_proba method.
        predictions = estimator.predict_proba(X)
        if not loss.is_multiclass:
            predictions = predictions[:, 1]  # probability of positive class
        eps = np.finfo(np.float32).eps  # FIXME: This is quite large!
        predictions = np.clip(predictions, eps, 1 - eps, dtype=np.float64)
    else:
        predictions = estimator.predict(X).astype(np.float64)

        return loss.link.link(predictions).reshape(-1, 1)
    else:
        return loss.link.link(predictions)

def _update_terminal_regions(
    loss,
    tree,
    X,
    y,
    neg_gradient,
    raw_prediction,
    sample_mask,
    learning_rate=0.1,
    k=0,
):
    """Update the leaf values to be predicted by the tree and raw_prediction.

    The current raw predictions of the model (of this stage) are updated.

    Additionally, the terminal regions (=leaves) of the given tree are updated as well.
    This corresponds to the line search step in "Greedy Function Approximation" by
    Friedman, Algorithm 1 step 5.

    Update equals:
        argmin_{x} loss(y_true, raw_prediction_old + x * tree.value)

    For non-trivial cases like the Binomial loss, the update has no closed formula and
    is an approximation, again, see the Friedman paper.

    Also note that the update formula for the SquaredError is the identity. Therefore,
    in this case, the leaf values don't need an update and only the raw_predictions are
    updated (with the learning rate included).

    tree : tree.Tree
        The tree object.
        The target labels.
    neg_gradient : ndarray of shape (n_samples,)
        The negative gradient.
    raw_prediction : ndarray of shape (n_samples, n_trees_per_iteration)
        The raw predictions (i.e. values from the tree leaves) of the
        tree ensemble at iteration ``i - 1``.
    sample_weight : ndarray of shape (n_samples,)
        The weight of each sample.
    sample_mask : ndarray of shape (n_samples,)
        The sample mask to be used.
    learning_rate : float, default=0.1
        Learning rate shrinks the contribution of each tree by
         ``learning_rate``.
    k : int, default=0
        The index of the estimator being updated.
    """
    # compute leaf for each sample in ``X``.
    terminal_regions = tree.apply(X)

    if not isinstance(loss, HalfSquaredError):
        # mask all which are not in sample mask.
        masked_terminal_regions = terminal_regions.copy()
        masked_terminal_regions[~sample_mask] = -1

        if isinstance(loss, HalfBinomialLoss):

            def compute_update(y_, indices, neg_gradient, raw_prediction, k):
                # Make a single Newton-Raphson step, see "Additive Logistic Regression:
                # A Statistical View of Boosting" FHT00 and note that we use a slightly
                # different version (factor 2) of "F" with proba=expit(raw_prediction).
                # Our node estimate is given by:
                #    sum(w * (y - prob)) / sum(w * prob * (1 - prob))
                # we take advantage that: y - prob = neg_gradient
                neg_g = neg_gradient.take(indices, axis=0)
                prob = y_ - neg_g
                # numerator = negative gradient = y - prob
                numerator = np.average(neg_g, weights=sw)
                # denominator = hessian = prob * (1 - prob)
                denominator = np.average(prob * (1 - prob), weights=sw)
                return _safe_divide(numerator, denominator)

        elif isinstance(loss, HalfMultinomialLoss):

                K = loss.n_classes
                # numerator = negative gradient * (k - 1) / k
                # Note: The factor (k - 1)/k appears in the original papers "Greedy
                # Function Approximation" by Friedman and "Additive Logistic
                # Regression" by Friedman, Hastie, Tibshirani. This factor is, however,
                # wrong or at least arbitrary as it directly multiplies the
                # learning_rate. We keep it for backward compatibility.
                numerator *= (K - 1) / K
                # denominator = (diagonal) hessian = prob * (1 - prob)

        elif isinstance(loss, ExponentialLoss):

                # numerator = negative gradient = y * exp(-raw) - (1-y) * exp(raw)
                # denominator = hessian = y * exp(-raw) + (1-y) * exp(raw)
                # if y=0: hessian = exp(raw) = -neg_g
                #    y=1: hessian = exp(-raw) = neg_g
                hessian = neg_g.copy()
                hessian[y_ == 0] *= -1
                denominator = np.average(hessian, weights=sw)

        else:

                return loss.fit_intercept_only(
                    y_true=y_ - raw_prediction[indices, k],
                    sample_weight=sw,
                )

        # update each leaf (= perform line search)
        for leaf in np.nonzero(tree.children_left == TREE_LEAF)[0]:
            indices = np.nonzero(masked_terminal_regions == leaf)[
                0
            ]  # of terminal regions
            y_ = y.take(indices, axis=0)
            sw = None if sample_weight is None else sample_weight[indices]
            update = compute_update(y_, indices, neg_gradient, raw_prediction, k)

            # TODO: Multiply here by learning rate instead of everywhere else.
            tree.value[leaf, 0, 0] = update

    # update predictions (both in-bag and out-of-bag)
    raw_prediction[:, k] += learning_rate * tree.value[:, 0, 0].take(
        terminal_regions, axis=0
    )

def set_huber_delta(loss, y_true, raw_prediction, sample_weight=None):
    """Calculate and set self.closs.delta based on self.quantile."""
    abserr = np.abs(y_true - raw_prediction.squeeze())
    # sample_weight is always a ndarray, never None.
    delta = _weighted_percentile(abserr, sample_weight, 100 * loss.quantile)
    loss.closs.delta = float(delta)

class VerboseReporter:
    """Reports verbose output to stdout.

    verbose : int
        Verbosity level. If ``verbose==1`` output is printed once in a while
        (when iteration mod verbose_mod is zero).; if larger than 1 then output
        is printed for each update.
    """

    def __init__(self, verbose):

    def init(self, est, begin_at_stage=0):
        """Initialize reporter

        est : Estimator
            The estimator

        begin_at_stage : int, default=0
            stage at which to begin reporting
        """
        # header fields and line format str
        header_fields = ["Iter", "Train Loss"]
        verbose_fmt = ["{iter:>10d}", "{train_score:>16.4f}"]
        # do oob?
        if est.subsample < 1:
            header_fields.append("OOB Improve")
            verbose_fmt.append("{oob_impr:>16.4f}")
        header_fields.append("Remaining Time")
        verbose_fmt.append("{remaining_time:>16s}")

        # print the header line
        print(("%10s " + "%16s " * (len(header_fields) - 1)) % tuple(header_fields))

        self.verbose_fmt = " ".join(verbose_fmt)
        # plot verbose info each time i % verbose_mod == 0
        self.verbose_mod = 1
        self.start_time = time()
        self.begin_at_stage = begin_at_stage

    def update(self, j, est):
        """Update reporter with new iteration.

        j : int
            The new iteration.
            The estimator.
        """
        do_oob = est.subsample < 1
        # we need to take into account if we fit additional estimators.
        i = j - self.begin_at_stage  # iteration relative to the start iter
        if (i + 1) % self.verbose_mod == 0:
            oob_impr = est.oob_improvement_[j] if do_oob else 0
            remaining_time = (
                (est.n_estimators - (j + 1)) * (time() - self.start_time) / float(i + 1)
            )
            if remaining_time > 60:
                remaining_time = "{0:.2f}m".format(remaining_time / 60.0)
            else:
                remaining_time = "{0:.2f}s".format(remaining_time)
            print(
                self.verbose_fmt.format(
                    iter=j + 1,
                    train_score=est.train_score_[j],
                    oob_impr=oob_impr,
                    remaining_time=remaining_time,
                )
            )
            if self.verbose == 1 and ((i + 1) // (self.verbose_mod * 10) > 0):
                # adjust verbose frequency (powers of 10)
                self.verbose_mod *= 10

class BaseGradientBoosting(BaseEnsemble, metaclass=ABCMeta):
    """Abstract base class for Gradient Boosting."""

        "learning_rate": [Interval(Real, 0.0, None, closed="left")],
        "criterion": [StrOptions({"friedman_mse", "squared_error"})],
        "subsample": [Interval(Real, 0.0, 1.0, closed="right")],
        "validation_fraction": [Interval(Real, 0.0, 1.0, closed="neither")],
        "n_iter_no_change": [Interval(Integral, 1, None, closed="left"), None],
        "tol": [Interval(Real, 0.0, None, closed="left")],
    }
    _parameter_constraints.pop("monotonic_cst")

        self,
        *,
        loss,
        learning_rate,
        n_estimators,
        criterion,
        min_samples_split,
        min_samples_leaf,
        min_weight_fraction_leaf,
        max_depth,
        min_impurity_decrease,
        init,
        subsample,
        max_features,
        ccp_alpha,
        random_state,
        alpha=0.9,
        validation_fraction=0.1,
        n_iter_no_change=None,
        tol=1e-4,
    ):
        self.subsample = subsample
        self.init = init
        self.validation_fraction = validation_fraction
        self.n_iter_no_change = n_iter_no_change

    def _encode_y(self, y=None, sample_weight=None):
        """Called by fit to validate and encode y."""

    def _get_loss(self, sample_weight):
        """Get loss object from sklearn._loss.loss."""

    def _fit_stage(
        self,
        i,
        X,
        y,
        raw_predictions,
        sample_weight,
        sample_mask,
        X_csc=None,
        X_csr=None,
    ):
        """Fit another stage of ``n_trees_per_iteration_`` trees."""
        original_y = y

        if isinstance(self._loss, HuberLoss):
            set_huber_delta(
                loss=self._loss,
                y_true=y,
                raw_prediction=raw_predictions,
                sample_weight=sample_weight,
            )
        # TODO: Without oob, i.e. with self.subsample = 1.0, we could call
        # self._loss.loss_gradient and use it to set train_score_.
        # But note that train_score_[i] is the score AFTER fitting the i-th tree.
        # Note: We need the negative gradient!
        neg_gradient = -self._loss.gradient(
            y_true=y,
            raw_prediction=raw_predictions,
            sample_weight=None,  # We pass sample_weights to the tree directly.
        )
        # 2-d views of shape (n_samples, n_trees_per_iteration_) or (n_samples, 1)
        # on neg_gradient to simplify the loop over n_trees_per_iteration_.
        if neg_gradient.ndim == 1:
            neg_g_view = neg_gradient.reshape((-1, 1))
        else:
            neg_g_view = neg_gradient

        for k in range(self.n_trees_per_iteration_):
            if self._loss.is_multiclass:
                y = np.array(original_y == k, dtype=np.float64)

            # induce regression tree on the negative gradient
            tree = DecisionTreeRegressor(
                criterion=self.criterion,
                splitter="best",
                max_depth=self.max_depth,
                min_samples_split=self.min_samples_split,
                min_samples_leaf=self.min_samples_leaf,
                min_weight_fraction_leaf=self.min_weight_fraction_leaf,
                min_impurity_decrease=self.min_impurity_decrease,
                max_features=self.max_features,
                max_leaf_nodes=self.max_leaf_nodes,
                ccp_alpha=self.ccp_alpha,
            )

            if self.subsample < 1.0:
                # no inplace multiplication!
                sample_weight = sample_weight * sample_mask.astype(np.float64)

            X = X_csc if X_csc is not None else X
            tree.fit(
                X, neg_g_view[:, k], sample_weight=sample_weight, check_input=False
            )

            # update tree leaves
            X_for_tree_update = X_csr if X_csr is not None else X
            _update_terminal_regions(
                self._loss,
                tree.tree_,
                X_for_tree_update,
                y,
                neg_g_view[:, k],
                raw_predictions,
                sample_mask,
                learning_rate=self.learning_rate,
                k=k,
            )

            # add tree to ensemble
            self.estimators_[i, k] = tree

        return raw_predictions

    def _set_max_features(self):
        """Set self.max_features_."""
        if isinstance(self.max_features, str):
            if self.max_features == "auto":
                if is_classifier(self):
                    max_features = max(1, int(np.sqrt(self.n_features_in_)))
                else:
                    max_features = self.n_features_in_
            elif self.max_features == "sqrt":
                max_features = max(1, int(np.sqrt(self.n_features_in_)))
            else:  # self.max_features == "log2"
                max_features = max(1, int(np.log2(self.n_features_in_)))
        elif self.max_features is None:
            max_features = self.n_features_in_
        elif isinstance(self.max_features, Integral):
        else:  # float
            max_features = max(1, int(self.max_features * self.n_features_in_))

        self.max_features_ = max_features

    def _init_state(self):
        """Initialize model state and allocate model state data structures."""

        self.init_ = self.init
        if self.init_ is None:
                self.init_ = DummyClassifier(strategy="prior")
            elif isinstance(self._loss, (AbsoluteError, HuberLoss)):
                self.init_ = DummyRegressor(strategy="quantile", quantile=0.5)
            elif isinstance(self._loss, PinballLoss):
                self.init_ = DummyRegressor(strategy="quantile", quantile=self.alpha)
            else:
                self.init_ = DummyRegressor(strategy="mean")

        self.estimators_ = np.empty(
            (self.n_estimators, self.n_trees_per_iteration_), dtype=object
        )
        self.train_score_ = np.zeros((self.n_estimators,), dtype=np.float64)
        # do oob?
        if self.subsample < 1.0:
            self.oob_improvement_ = np.zeros((self.n_estimators), dtype=np.float64)
            self.oob_scores_ = np.zeros((self.n_estimators), dtype=np.float64)
            self.oob_score_ = np.nan

    def _clear_state(self):
        """Clear the state of the gradient boosting model."""
            self.estimators_ = np.empty((0, 0), dtype=object)
        if hasattr(self, "train_score_"):
            del self.train_score_
        if hasattr(self, "oob_improvement_"):
            del self.oob_improvement_
        if hasattr(self, "oob_scores_"):
            del self.oob_scores_
        if hasattr(self, "oob_score_"):
        if hasattr(self, "init_"):
            del self.init_
        if hasattr(self, "_rng"):
            del self._rng

    def _resize_state(self):
        """Add additional ``n_estimators`` entries to all attributes."""
        # self.n_estimators is the number of additional est to fit
        total_n_estimators = self.n_estimators
        if total_n_estimators < self.estimators_.shape[0]:
                "resize with smaller n_estimators %d < %d"
                % (total_n_estimators, self.estimators_[0])
            )

        self.estimators_ = np.resize(
            self.estimators_, (total_n_estimators, self.n_trees_per_iteration_)
        )
        self.train_score_ = np.resize(self.train_score_, total_n_estimators)
        if self.subsample < 1 or hasattr(self, "oob_improvement_"):
            # if do oob resize arrays or create new if not available
            if hasattr(self, "oob_improvement_"):
                self.oob_improvement_ = np.resize(
                    self.oob_improvement_, total_n_estimators
                )
                self.oob_scores_ = np.resize(self.oob_scores_, total_n_estimators)
                self.oob_score_ = np.nan
            else:
                self.oob_improvement_ = np.zeros(
                    (total_n_estimators,), dtype=np.float64
                )
                self.oob_scores_ = np.zeros((total_n_estimators,), dtype=np.float64)

    def _is_fitted(self):
        return len(getattr(self, "estimators_", [])) > 0

    def _check_initialized(self):
        """Check that the estimator is initialized, raising an error if not."""

        # GradientBoosting*.init is not validated yet
    )
    def fit(self, X, y, sample_weight=None, monitor=None):
        """Fit the gradient boosting model.

            Target values (strings or integers in classification, real numbers
            in regression)
            For classification, labels must correspond to classes.

        monitor : callable, default=None
            The monitor is called after each iteration with the current
            iteration, a reference to the estimator and the local variables of
            ``_fit_stages`` as keyword arguments ``callable(i, self,
            locals())``. If the callable returns ``True`` the fitting procedure
            is stopped. The monitor can be used for various things such as
            computing held-out estimates, early stopping, model introspect, and
            snapshotting.

        Returns
        -------
        """
        if not self.warm_start:
            self._clear_state()

        # Check input
        # Since check_array converts both X and y to the same dtype, but the
        # trees use different types for X and y, checking them separately.

            self,
            X,
            y,
            accept_sparse=["csr", "csc", "coo"],
        )
        sample_weight_is_none = sample_weight is None
        sample_weight = _check_sample_weight(sample_weight, X)
        if sample_weight_is_none:
            y = self._encode_y(y=y, sample_weight=None)
        else:
            y = self._encode_y(y=y, sample_weight=sample_weight)
        y = column_or_1d(y, warn=True)  # TODO: Is this still required?

        self._set_max_features()

        # self.loss is guaranteed to be a string
        self._loss = self._get_loss(sample_weight=sample_weight)

        if self.n_iter_no_change is not None:
            stratify = y if is_classifier(self) else None
            (
                X_train,
                X_val,
                y_train,
                y_val,
                sample_weight_train,
                sample_weight_val,
            ) = train_test_split(
                X,
                y,
                random_state=self.random_state,
                test_size=self.validation_fraction,
                stratify=stratify,
            )
                if self.n_classes_ != np.unique(y_train).shape[0]:
                    # We choose to error here. The problem is that the init
                    # estimator would be trained on y, which has some missing
                    # classes now, so its predictions would not have the
                    # correct shape.
                        "The training data after the early stopping split "
                        "is missing some classes. Try using another random "
                        "seed."
                    )
        else:
            X_train, y_train, sample_weight_train = X, y, sample_weight
            X_val = y_val = sample_weight_val = None

        n_samples = X_train.shape[0]

        # First time calling fit.
        if not self._is_fitted():
            # init state
            self._init_state()

            # fit initial model and initialize raw predictions
            if self.init_ == "zero":
                raw_predictions = np.zeros(
                    shape=(n_samples, self.n_trees_per_iteration_),
                    dtype=np.float64,
                )
            else:
                # XXX clean this once we have a support_sample_weight tag
                if sample_weight_is_none:
                    self.init_.fit(X_train, y_train)
                else:
                    msg = (
                        "The initial estimator {} does not support sample "
                        "weights.".format(self.init_.__class__.__name__)
                    )
                    try:
                        self.init_.fit(
                            X_train, y_train, sample_weight=sample_weight_train
                        )
                    except TypeError as e:
                        if "unexpected keyword argument 'sample_weight'" in str(e):
                            # regular estimator without SW support
                            raise ValueError(msg) from e
                        else:  # regular estimator whose input checking failed
                            raise
                    except ValueError as e:
                        if (
                            "pass parameters to specific steps of "
                            "your pipeline using the "
                            "stepname__parameter" in str(e)
                        ):  # pipeline
                            raise

                raw_predictions = _init_raw_predictions(
                    X_train, self.init_, self._loss, is_classifier(self)
                )

            begin_at_stage = 0

            # The rng state must be preserved if warm_start is True
            self._rng = check_random_state(self.random_state)

        # warm start: this is not the first time fit was called
        else:
            # add more estimators to fitted model
            # invariant: warm_start = True
            if self.n_estimators < self.estimators_.shape[0]:
                    "n_estimators=%d must be larger or equal to "
                    "estimators_.shape[0]=%d when "
                    "warm_start==True" % (self.n_estimators, self.estimators_.shape[0])
                )
            begin_at_stage = self.estimators_.shape[0]
            # The requirements of _raw_predict
            # are more constrained than fit. It accepts only CSR
            # matrices. Finite values have already been checked in _validate_data.
            X_train = check_array(
                X_train,
                dtype=DTYPE,
                order="C",
                accept_sparse="csr",
            )
            raw_predictions = self._raw_predict(X_train)
            self._resize_state()

        # fit the boosting stages
        n_stages = self._fit_stages(
            X_train,
            y_train,
            raw_predictions,
            sample_weight_train,
            self._rng,
            X_val,
            y_val,
            sample_weight_val,
            begin_at_stage,
            monitor,
        )

        # change shape of arrays after fit (early-stopping or additional ests)
        if n_stages != self.estimators_.shape[0]:
            self.estimators_ = self.estimators_[:n_stages]
            self.train_score_ = self.train_score_[:n_stages]
                # OOB scores were computed
                self.oob_improvement_ = self.oob_improvement_[:n_stages]
                self.oob_scores_ = self.oob_scores_[:n_stages]
                self.oob_score_ = self.oob_scores_[-1]
        self.n_estimators_ = n_stages

    def _fit_stages(
        self,
        X,
        y,
        X_val,
        y_val,
        sample_weight_val,
        begin_at_stage=0,
        monitor=None,
    ):
        """Iteratively fits the stages.

        For each stage it computes the progress (OOB, train score)
        and delegates to ``_fit_stage``.
        Returns the number of stages fit; might differ from ``n_estimators``
        due to early stopping.
        """
        do_oob = self.subsample < 1.0
        sample_mask = np.ones((n_samples,), dtype=bool)
        n_inbag = max(1, int(self.subsample * n_samples))

        if self.verbose:
            verbose_reporter = VerboseReporter(verbose=self.verbose)
            verbose_reporter.init(self, begin_at_stage)

        X_csc = csc_matrix(X) if issparse(X) else None
        X_csr = csr_matrix(X) if issparse(X) else None

            loss_history = np.full(self.n_iter_no_change, np.inf)
            # We create a generator to get the predictions for X_val after
            # the addition of each successive stage
            y_val_pred_iter = self._staged_raw_predict(X_val, check_input=False)

        # Older versions of GBT had its own loss functions. With the new common
        # private loss function submodule _loss, we often are a factor of 2
        # away from the old version. Here we keep backward compatibility for
        # oob_scores_ and oob_improvement_, even if the old way is quite
        # inconsistent (sometimes the gradient is half the gradient, sometimes
        # not).
        if isinstance(
            self._loss,
            (
                HalfSquaredError,
                HalfBinomialLoss,
            ),
        ):
            factor = 2
        else:
            factor = 1

        # perform boosting iterations
        i = begin_at_stage
        for i in range(begin_at_stage, self.n_estimators):
            # subsampling
            if do_oob:
                sample_mask = _random_sample_mask(n_samples, n_inbag, random_state)
                y_oob_masked = y[~sample_mask]
                sample_weight_oob_masked = sample_weight[~sample_mask]
                if i == 0:  # store the initial loss to compute the OOB score
                    initial_loss = factor * self._loss(
                        y_true=y_oob_masked,
                        raw_prediction=raw_predictions[~sample_mask],
                        sample_weight=sample_weight_oob_masked,
                    )

            # fit next stage of trees
            raw_predictions = self._fit_stage(
                i,
                X,
                y,
                random_state,
                X_csc=X_csc,
                X_csr=X_csr,
            )

            # track loss
            if do_oob:
                self.train_score_[i] = factor * self._loss(
                    y_true=y[sample_mask],
                    raw_prediction=raw_predictions[sample_mask],
                    sample_weight=sample_weight[sample_mask],
                )
                self.oob_scores_[i] = factor * self._loss(
                    y_true=y_oob_masked,
                    raw_prediction=raw_predictions[~sample_mask],
                    sample_weight=sample_weight_oob_masked,
                )
                previous_loss = initial_loss if i == 0 else self.oob_scores_[i - 1]
                self.oob_improvement_[i] = previous_loss - self.oob_scores_[i]
            else:
                # no need to fancy index w/ no subsampling
                    y_true=y,
                    raw_prediction=raw_predictions,
                )

            if self.verbose > 0:
                verbose_reporter.update(i, self)

            if monitor is not None:
                early_stopping = monitor(i, self, locals())
                if early_stopping:
                    break

            # We also provide an early stopping based on the score from
            # validation set (X_val, y_val), if n_iter_no_change is set
            if self.n_iter_no_change is not None:
                # By calling next(y_val_pred_iter), we get the predictions
                # for X_val after the addition of the current stage
                validation_loss = factor * self._loss(
                    y_val, next(y_val_pred_iter), sample_weight_val
                )

                # Require validation_score to be better (less) than at least
                # one of the last n_iter_no_change evaluations
                if np.any(validation_loss + self.tol < loss_history):
                    loss_history[i % len(loss_history)] = validation_loss
                else:
                    break

        return i + 1

    def _make_estimator(self, append=True):
        # we don't need _make_estimator
        raise NotImplementedError()

    def _raw_predict_init(self, X):
        """Check input and compute raw predictions of the init estimator."""
        self._check_initialized()
        X = self.estimators_[0, 0]._validate_X_predict(X, check_input=True)
        if self.init_ == "zero":
            raw_predictions = np.zeros(
                shape=(X.shape[0], self.n_trees_per_iteration_), dtype=np.float64
            )
        else:
            raw_predictions = _init_raw_predictions(
                X, self.init_, self._loss, is_classifier(self)
            )

    def _raw_predict(self, X):
        """Return the sum of the trees raw predictions (+ init estimator)."""
        raw_predictions = self._raw_predict_init(X)
        predict_stages(self.estimators_, X, self.learning_rate, raw_predictions)

    def _staged_raw_predict(self, X, check_input=True):
        """Compute raw predictions of ``X`` for each iteration.

        after each stage.

            If False, the input arrays X will not be checked.

        Returns
        -------
        raw_predictions : generator of ndarray of shape (n_samples, k)
            The raw predictions of the input samples. The order of the
            Regression and binary classification are special cases with
            ``k == 1``, otherwise ``k==n_classes``.
        """
        if check_input:
                self, X, dtype=DTYPE, order="C", accept_sparse="csr", reset=False
            )
        for i in range(self.estimators_.shape[0]):
            predict_stage(self.estimators_, i, X, self.learning_rate, raw_predictions)
            yield raw_predictions.copy()

    @property

        Returns
        -------
        """

        relevant_trees = [
            tree
            for stage in self.estimators_
            for tree in stage
        ]
        if not relevant_trees:
            # degenerate case where all trees have only one node
            return np.zeros(shape=self.n_features_in_, dtype=np.float64)

        relevant_feature_importances = [
            tree.tree_.compute_feature_importances(normalize=False)
            for tree in relevant_trees
        ]
        avg_feature_importances = np.mean(
            relevant_feature_importances, axis=0, dtype=np.float64
        )
        return avg_feature_importances / np.sum(avg_feature_importances)

        grid : ndarray of shape (n_samples, n_target_features), dtype=np.float32
        target_features : ndarray of shape (n_target_features,), dtype=np.intp

        Returns
        -------
        averaged_predictions : ndarray of shape \
                (n_trees_per_iteration_, n_samples)
        """
        if self.init is not None:
                "Using recursion method with a non-constant init predictor "
                "will lead to incorrect partial dependence values. "
                "Got init=%s." % self.init,
            )
        n_estimators, n_trees_per_stage = self.estimators_.shape
            (n_trees_per_stage, grid.shape[0]), dtype=np.float64, order="C"
        )

        for stage in range(n_estimators):
            for k in range(n_trees_per_stage):
                tree = self.estimators_[stage, k].tree_
                tree.compute_partial_dependence(
                    grid, target_features, averaged_predictions[k]
                )
        averaged_predictions *= self.learning_rate

        """Apply trees in the ensemble to X, return leaf indices.

            ``dtype=np.float32``. If a sparse matrix is provided, it will
            be converted to a sparse ``csr_matrix``.

        Returns
        -------
        X_leaves : array-like of shape (n_samples, n_estimators, n_classes)
            For each datapoint x in X and for each tree in the ensemble,
            return the index of the leaf x ends up in each estimator.
            In the case of binary classification n_classes is 1.
        """

        # n_classes will be equal to 1 in the binary classification or the
        # regression case.
        n_estimators, n_classes = self.estimators_.shape
        leaves = np.zeros((X.shape[0], n_estimators, n_classes))

        for i in range(n_estimators):
                estimator = self.estimators_[i, j]
                leaves[:, i, j] = estimator.apply(X, check_input=False)

        return leaves

class GradientBoostingClassifier(ClassifierMixin, BaseGradientBoosting):
    """Gradient Boosting for classification.

    This algorithm builds an additive model in a forward stage-wise fashion; it
    allows for the optimization of arbitrary differentiable loss functions. In
    each stage ``n_classes_`` regression trees are fit on the negative gradient
    of the loss function, e.g. binary or multiclass log loss. Binary
    classification is a special case where only a single regression tree is
    induced.

    :class:`~sklearn.ensemble.HistGradientBoostingClassifier` is a much faster variant
    of this algorithm for intermediate and large datasets (`n_samples >= 10_000`) and
    supports monotonic constraints.

    Read more in the :ref:`User Guide <gradient_boosting>`.

    loss : {'log_loss', 'exponential'}, default='log_loss'
        The loss function to be optimized. 'log_loss' refers to binomial and
        multinomial deviance, the same as used in logistic regression.
        It is a good choice for classification with probabilistic outputs.
        For loss 'exponential', gradient boosting recovers the AdaBoost algorithm.

        Learning rate shrinks the contribution of each tree by `learning_rate`.
        There is a trade-off between learning_rate and n_estimators.
        Values must be in the range `[0.0, inf)`.

        For an example of the effects of this parameter and its interaction with
        ``subsample``, see
        :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_regularization.py`.

        The number of boosting stages to perform. Gradient boosting
        is fairly robust to over-fitting so a large number usually
        results in better performance.

    subsample : float, default=1.0
        The fraction of samples to be used for fitting the individual base
        learners. If smaller than 1.0 this results in Stochastic Gradient
        Boosting. `subsample` interacts with the parameter `n_estimators`.
        Choosing `subsample < 1.0` leads to a reduction of variance
        and an increase in bias.
        Values must be in the range `(0.0, 1.0]`.

    criterion : {'friedman_mse', 'squared_error'}, default='friedman_mse'
        'friedman_mse' for the mean squared error with improvement score by
        Friedman, 'squared_error' for mean squared error. The default value of
        'friedman_mse' is generally the best as it can provide a better
        approximation in some cases.

        - If int, values must be in the range `[2, inf)`.
        - If float, values must be in the range `(0.0, 1.0]` and `min_samples_split`
          will be `ceil(min_samples_split * n_samples)`.

        - If int, values must be in the range `[1, inf)`.
        - If float, values must be in the range `(0.0, 1.0)` and `min_samples_leaf`
          will be `ceil(min_samples_leaf * n_samples)`.

        Values must be in the range `[0.0, 0.5]`.

    max_depth : int or None, default=3
        Maximum depth of the individual regression estimators. The maximum
        depth limits the number of nodes in the tree. Tune this parameter
        for best performance; the best value depends on the interaction
        of the input variables. If None, then nodes are expanded until
        If int, values must be in the range `[1, inf)`.

    init : estimator or 'zero', default=None
        An estimator object that is used to compute the initial predictions.
        ``init`` has to provide :term:`fit` and :term:`predict_proba`. If
        'zero', the initial raw predictions are set to zero. By default, a
        ``DummyEstimator`` predicting the classes priors is used.

        Controls the random seed given to each Tree estimator at each
        In addition, it controls the random permutation of the features at
        each split (see Notes for more details).
        It also controls the random splitting of the training data to obtain a
        validation set if `n_iter_no_change` is not None.

    max_features : {'sqrt', 'log2'}, int or float, default=None

        - If float, values must be in the range `(0.0, 1.0]` and the features
          considered at each split will be `max(1, int(max_features * n_features_in_))`.
        - If 'sqrt', then `max_features=sqrt(n_features)`.
        - If 'log2', then `max_features=log2(n_features)`.

        Choosing `max_features < n_features` leads to a reduction of variance

        Enable verbose output. If 1 then it prints progress and performance
        once in a while (the more trees the lower the frequency). If greater
        than 1 then it prints progress and performance for every tree.
        Values must be in the range `[0, inf)`.

        Values must be in the range `[2, inf)`.
        If `None`, then unlimited number of leaf nodes.

        and add more estimators to the ensemble, otherwise, just erase the
        previous solution. See :term:`the Glossary <warm_start>`.

    validation_fraction : float, default=0.1
        The proportion of training data to set aside as validation set for
        early stopping. Values must be in the range `(0.0, 1.0)`.
        Only used if ``n_iter_no_change`` is set to an integer.

    n_iter_no_change : int, default=None
        ``n_iter_no_change`` is used to decide if early stopping will be used
        to terminate training when validation score is not improving. By
        default it is set to None to disable early stopping. If set to a
        number, it will set aside ``validation_fraction`` size of the training
        data as validation and terminate training when validation score is not
        improving in all of the previous ``n_iter_no_change`` numbers of
        iterations. The split is stratified.
        See
        :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_early_stopping.py`.

        Tolerance for the early stopping. When the loss is not improving
        by at least tol for ``n_iter_no_change`` iterations (if set to a
        number), the training stops.

        ``ccp_alpha`` will be chosen. By default, no pruning is performed.
        See :ref:`minimal_cost_complexity_pruning` for details. See

    n_estimators_ : int
        The number of estimators as selected by early stopping (if
        ``n_iter_no_change`` is specified). Otherwise it is set to
        ``n_estimators``.

    n_trees_per_iteration_ : int
        The number of trees that are built at each iteration. For binary classifiers,
        this is always 1.

        .. versionadded:: 1.4.0

    oob_improvement_ : ndarray of shape (n_estimators,)
        The improvement in loss on the out-of-bag samples
        relative to the previous iteration.
        ``oob_improvement_[0]`` is the improvement in
        loss of the first stage over the ``init`` estimator.
        Only available if ``subsample < 1.0``.

    oob_scores_ : ndarray of shape (n_estimators,)
        The full history of the loss values on the out-of-bag
        samples. Only available if `subsample < 1.0`.

        The last value of the loss on the out-of-bag samples. It is
        the same as `oob_scores_[-1]`. Only available if `subsample < 1.0`.

    train_score_ : ndarray of shape (n_estimators,)
        The i-th score ``train_score_[i]`` is the loss of the
        model at iteration ``i`` on the in-bag sample.
        If ``subsample == 1`` this is the loss on the training data.

    init_ : estimator
        The estimator that provides the initial predictions. Set via the ``init``
        argument.

    estimators_ : ndarray of DecisionTreeRegressor of \
            shape (n_estimators, ``n_trees_per_iteration_``)
        The collection of fitted sub-estimators. ``n_trees_per_iteration_`` is 1 for
        binary classification, otherwise ``n_classes``.

    max_features_ : int
        The inferred value of max_features.

    See Also
    --------
    HistGradientBoostingClassifier : Histogram-based Gradient Boosting
        Classification Tree.
    RandomForestClassifier : A meta-estimator that fits a number of decision
        tree classifiers on various sub-samples of the dataset and uses
        averaging to improve the predictive accuracy and control over-fitting.
    AdaBoostClassifier : A meta-estimator that begins by fitting a classifier
        on the original dataset and then fits additional copies of the
        classifier on the same dataset where the weights of incorrectly
        classified instances are adjusted such that subsequent classifiers
        focus more on difficult cases.

    Notes
    -----
    the best found split may vary, even with the same training data and
    ``max_features=n_features``, if the improvement of the criterion is
    identical for several splits enumerated during the search of the best
    split. To obtain a deterministic behaviour during fitting,
    ``random_state`` has to be fixed.

    J. Friedman, Greedy Function Approximation: A Gradient Boosting
    Machine, The Annals of Statistics, Vol. 29, No. 5, 2001.

    J. Friedman, Stochastic Gradient Boosting, 1999

    T. Hastie, R. Tibshirani and J. Friedman.
    Elements of Statistical Learning Ed. 2, Springer, 2009.

    Examples
    --------
    The following example shows how to fit a gradient boosting classifier with
    100 decision stumps as weak learners.

    >>> from sklearn.datasets import make_hastie_10_2
    >>> from sklearn.ensemble import GradientBoostingClassifier

    >>> X, y = make_hastie_10_2(random_state=0)
    >>> X_train, X_test = X[:2000], X[2000:]
    >>> y_train, y_test = y[:2000], y[2000:]

    >>> clf = GradientBoostingClassifier(n_estimators=100, learning_rate=1.0,
    ...     max_depth=1, random_state=0).fit(X_train, y_train)
    >>> clf.score(X_test, y_test)
    0.913...
    """

        **BaseGradientBoosting._parameter_constraints,
        "loss": [StrOptions({"log_loss", "exponential"})],
        "init": [StrOptions({"zero"}), None, HasMethods(["fit", "predict_proba"])],
    }

        self,
        *,
        loss="log_loss",
        learning_rate=0.1,
        subsample=1.0,
        criterion="friedman_mse",
        max_depth=3,
        init=None,
        max_features=None,
        tol=1e-4,
    ):
            loss=loss,
            criterion=criterion,
            min_samples_split=min_samples_split,
            min_samples_leaf=min_samples_leaf,
            min_weight_fraction_leaf=min_weight_fraction_leaf,
            init=init,
            subsample=subsample,
            max_leaf_nodes=max_leaf_nodes,
            min_impurity_decrease=min_impurity_decrease,
            validation_fraction=validation_fraction,
            n_iter_no_change=n_iter_no_change,
            tol=tol,
            ccp_alpha=ccp_alpha,
        )

    def _encode_y(self, y, sample_weight):
        # encode classes into 0 ... n_classes - 1 and sets attributes classes_
        # and n_trees_per_iteration_

        label_encoder = LabelEncoder()
        encoded_y_int = label_encoder.fit_transform(y)
        self.classes_ = label_encoder.classes_
        # only 1 tree for binary classification. For multiclass classification,
        # we build 1 tree per class.
        self.n_trees_per_iteration_ = 1 if n_classes <= 2 else n_classes
        encoded_y = encoded_y_int.astype(float, copy=False)

        # From here on, it is additional to the HGBT case.
        # expose n_classes_ attribute
        self.n_classes_ = n_classes
            n_trim_classes = n_classes
        else:
            n_trim_classes = np.count_nonzero(np.bincount(encoded_y_int, sample_weight))

        if n_trim_classes < 2:
                "y contains %d class after sample_weight "
                "trimmed classes with zero weights, while a "
                "minimum of 2 classes are required." % n_trim_classes
            )
        return encoded_y

        if self.loss == "log_loss":
            if self.n_classes_ == 2:
                return HalfBinomialLoss(sample_weight=sample_weight)
            else:
                return HalfMultinomialLoss(
                    sample_weight=sample_weight, n_classes=self.n_classes_
                )
            if self.n_classes_ > 2:
                    f"loss='{self.loss}' is only suitable for a binary classification "
                    f"problem, you have n_classes={self.n_classes_}. "
                    "Please use loss='log_loss' instead."
                )
            else:
                return ExponentialLoss(sample_weight=sample_weight)

        Returns
        -------
        score : ndarray of shape (n_samples, n_classes) or (n_samples,)
            The decision function of the input samples, which corresponds to
            the raw values predicted from the trees of the ensemble . The
            order of the classes corresponds to that in the attribute
            :term:`classes_`. Regression and binary classification produce an
            array of shape (n_samples,).
        """
            self, X, dtype=DTYPE, order="C", accept_sparse="csr", reset=False
        )
        raw_predictions = self._raw_predict(X)
        if raw_predictions.shape[1] == 1:
            return raw_predictions.ravel()

        """Compute decision function of ``X`` for each iteration.

        Yields
        ------
        """
        yield from self._staged_raw_predict(X)

        Returns
        -------
        """
        raw_predictions = self.decision_function(X)
        if raw_predictions.ndim == 1:  # decision_function already squeezed it
            encoded_classes = (raw_predictions >= 0).astype(int)
        else:
            encoded_classes = np.argmax(raw_predictions, axis=1)
        return self.classes_[encoded_classes]

        """Predict class at each stage for X.

        Yields
        ------
            The predicted value of the input samples.
        """
        if self.n_classes_ == 2:  # n_trees_per_iteration_ = 1
            for raw_predictions in self._staged_raw_predict(X):
                encoded_classes = (raw_predictions.squeeze() >= 0).astype(int)
                yield self.classes_.take(encoded_classes, axis=0)
        else:
                encoded_classes = np.argmax(raw_predictions, axis=1)

        Returns
        -------

        Raises
        ------
        AttributeError
            If the ``loss`` does not support probabilities.
        """
        return self._loss.predict_proba(raw_predictions)

        Returns
        -------

        Raises
        ------
        """
        return np.log(proba)

        """Predict class probabilities at each stage for X.

        Yields
        ------
        """
        try:
                yield self._loss.predict_proba(raw_predictions)
            raise
                "loss=%r does not support predict_proba" % self.loss
            ) from e

class GradientBoostingRegressor(RegressorMixin, BaseGradientBoosting):
    """Gradient Boosting for regression.

    This estimator builds an additive model in a forward stage-wise fashion; it
    each stage a regression tree is fit on the negative gradient of the given
    loss function.

    :class:`~sklearn.ensemble.HistGradientBoostingRegressor` is a much faster variant

    loss : {'squared_error', 'absolute_error', 'huber', 'quantile'}, \
            default='squared_error'
        Loss function to be optimized. 'squared_error' refers to the squared
        error for regression. 'absolute_error' refers to the absolute error of
        regression and is a robust loss function. 'huber' is a
        combination of the two. 'quantile' allows quantile regression (use
        `alpha` to specify the quantile).
        See
        :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_quantile.py`
        for an example that demonstrates quantile regression for creating
        prediction intervals with `loss='quantile'`.

        "friedman_mse" for the mean squared error with improvement score by
        Friedman, "squared_error" for mean squared error. The default value of
        "friedman_mse" is generally the best as it can provide a better

        ``init`` has to provide :term:`fit` and :term:`predict`. If 'zero', the
        initial raw predictions are set to zero. By default a
        ``DummyEstimator`` is used, predicting either the average target value
        (for loss='squared_error'), or a quantile for the other losses.

    alpha : float, default=0.9
        The alpha-quantile of the huber loss function and the quantile
        loss function. Only if ``loss='huber'`` or ``loss='quantile'``.
        Values must be in the range `(0.0, 1.0)`.

        If None, then unlimited number of leaf nodes.

        iterations.
        See

        The number of trees that are built at each iteration. For regressors, this is
        always 1.

        argument.

    estimators_ : ndarray of DecisionTreeRegressor of shape (n_estimators, 1)

    See Also
    --------
    HistGradientBoostingRegressor : Histogram-based Gradient Boosting
    sklearn.ensemble.RandomForestRegressor : A random forest regressor.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.ensemble import GradientBoostingRegressor
    >>> X, y = make_regression(random_state=0)
    >>> reg = GradientBoostingRegressor(random_state=0)
    >>> reg.fit(X_train, y_train)
    GradientBoostingRegressor(random_state=0)
    >>> reg.predict(X_test[1:2])
    array([-61...])
    0.4...

    For a detailed example of utilizing
    :class:`~sklearn.ensemble.GradientBoostingRegressor`
    to fit an ensemble of weak predictive models, please refer to
    :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_regression.py`.
    """

        "loss": [StrOptions({"squared_error", "absolute_error", "huber", "quantile"})],
        "init": [StrOptions({"zero"}), None, HasMethods(["fit", "predict"])],
        "alpha": [Interval(Real, 0.0, 1.0, closed="neither")],
    }

        self,
        *,
        loss="squared_error",
        tol=1e-4,
    ):
            tol=tol,
        )

        # Just convert y to the expected dtype
        self.n_trees_per_iteration_ = 1
        y = y.astype(DOUBLE, copy=False)
        return y

        if self.loss in ("quantile", "huber"):
            return _LOSSES[self.loss](sample_weight=sample_weight, quantile=self.alpha)
        else:
            return _LOSSES[self.loss](sample_weight=sample_weight)

        Returns
        -------
        """
        )
        # In regression we can directly return the raw value from the trees.
        return self._raw_predict(X).ravel()

        """Predict regression target at each stage for X.

        Yields
        ------
        """
        for raw_predictions in self._staged_raw_predict(X):
            yield raw_predictions.ravel()

        Returns
        -------
        X_leaves : array-like of shape (n_samples, n_estimators)
        """

        leaves = super().apply(X)
        leaves = leaves.reshape(X.shape[0], self.estimators_.shape[0])

        """
        # do oob?

        j : int
        """
            )
            else:
            print(
                    iter=j + 1,
                )
            )

        self,
        *,
        loss,
        init,
        tol=1e-4,
    ):

        self,
        i,
        X,
        y,
    ):

                y_true=y,
            )
            y_true=y,
        )
        else:

            )

            tree.fit(
            )

                y,
                k=k,
            )

                else:

            else:

        )
        # do oob?

            )

        )
                )
            else:
                )

        Returns
        -------
        """

            self,
            X,
            y,
        )
        else:

            (
                X_train,
                X_val,
                y_train,
                y_val,
                X,
                y,
            )
                        "seed."
                    )
        else:

                )
            else:
                else:
                    msg = (
                    )
                    try:
                        )
                            raise
                        if (
                            raise

                )

        else:
                )
                X_train,
            )

            X_train,
            y_train,
            X_val,
            y_val,
            monitor,
        )

        self,
        X,
        y,
        X_val,
        y_val,
    ):

        """

        # not).
            (
            ),
        ):
            factor = 2
        else:
            factor = 1

            if do_oob:
                    )

                i,
                X,
                y,
            )

            if do_oob:
                )
                )
            else:
                    y_true=y,
                )

                    break

                )

                else:
                    break

        return i + 1

            )
        else:
            )

        Returns
        -------
        """
            )

        Returns
        -------
        """

            tree
        ]

        ]
        )

        Returns
        -------
        """
            )
        )

                )

        Returns
        -------
        """

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        else:

            )

            else:
                )
                )
            else:

        Returns
        -------
        """
        )

        Yields
        ------
        """

        Returns
        -------
        """
        else:

        Yields
        ------
        """
        else:

        Returns
        -------

        Raises
        ------
        """

        Returns
        -------

        Raises
        ------
        """

        Yields
        ------
        """
        try:
            raise
            ) from e

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        return y

        else:

        Returns
        -------
        """
        )

        Yields
        ------
        """

        Returns
        -------
        """

                )

class TreePredictor:
    """Tree class used for predictions.

    nodes : ndarray of PREDICTOR_RECORD_DTYPE
        The nodes of the tree.
    binned_left_cat_bitsets : ndarray of shape (n_categorical_splits, 8), dtype=uint32
        Array of bitsets for binned categories used in predict_binned when a
        split is categorical.
    raw_left_cat_bitsets : ndarray of shape (n_categorical_splits, 8), dtype=uint32
        Array of bitsets for raw categories used in predict when a split is
        categorical.
    """

    def __init__(self, nodes, binned_left_cat_bitsets, raw_left_cat_bitsets):
        self.nodes = nodes
        self.binned_left_cat_bitsets = binned_left_cat_bitsets
        self.raw_left_cat_bitsets = raw_left_cat_bitsets

    def get_n_leaf_nodes(self):
        """Return number of leaves."""
        return int(self.nodes["is_leaf"].sum())

    def get_max_depth(self):
        """Return maximum depth among all leaves."""
        return int(self.nodes["depth"].max())

    def predict(self, X, known_cat_bitsets, f_idx_map, n_threads):
        """Predict raw values for non-binned data.

        X : ndarray, shape (n_samples, n_features)

        known_cat_bitsets : ndarray of shape (n_categorical_features, 8)
            Array of bitsets of known categories, for each categorical feature.

        f_idx_map : ndarray of shape (n_features,)
            Map from original feature index to the corresponding index in the
            known_cat_bitsets array.

        n_threads : int
            Number of OpenMP threads to use.

        Returns
        -------
        y : ndarray, shape (n_samples,)
            The raw predicted values.
        """
        out = np.empty(X.shape[0], dtype=Y_DTYPE)

        _predict_from_raw_data(
            self.nodes,
            X,
            self.raw_left_cat_bitsets,
            known_cat_bitsets,
            f_idx_map,
            n_threads,
            out,
        )
        return out

    def predict_binned(self, X, missing_values_bin_idx, n_threads):
        """Predict raw values for binned data.

        missing_values_bin_idx : uint8
            Index of the bin that is used for missing values. This is the
            index of the last bin and is always equal to max_bins (as passed
            to the GBDT classes), or equivalently to n_bins - 1.

        Returns
        -------
        """
        _predict_from_binned_data(
            X,
            self.binned_left_cat_bitsets,
            missing_values_bin_idx,
            out,
        )
        return out

    def compute_partial_dependence(self, grid, target_features, out):

        grid : ndarray, shape (n_samples, n_target_features)
        target_features : ndarray, shape (n_target_features)
        out : ndarray, shape (n_samples)
            The value of the partial dependence function on each grid
            point.
        """
        _compute_partial_dependence(self.nodes, grid, target_features, out)

        try:

        # The dtype of feature_idx is np.intp which is platform dependent. Here, we
        # make sure that saving and loading on different bitness systems works without
        # errors. For instance, on a 64 bit Python runtime, np.intp = np.int64,
        # while on 32 bit np.intp = np.int32.
        #
        # TODO: consider always using platform agnostic dtypes for fitted
        # estimator attributes. For this particular estimator, this would
        # mean replacing the intp field of PREDICTOR_RECORD_DTYPE by an int32
        # field. Ideally this should be done consistently throughout
        # scikit-learn along with a common test.
        if self.nodes.dtype != PREDICTOR_RECORD_DTYPE:
            self.nodes = self.nodes.astype(PREDICTOR_RECORD_DTYPE, casting="same_kind")

        Returns
        -------
        """

            X,
            out,
        )
        return out

        Returns
        -------
        """
            X,
            out,
        )
        return out

            point.
        """

        try:

        #

def _find_binning_thresholds(col_data, max_bins):
    """Extract quantiles from a continuous feature.

    Missing values are ignored for finding the thresholds.

    col_data : array-like, shape (n_samples,)
        The continuous feature to bin.
    max_bins: int
        The maximum number of bins to use for non-missing values. If for a
        given feature the number of unique values is less than ``max_bins``,
        then those unique values will be used to compute the bin thresholds,
        instead of the quantiles

    Return
    ------
    binning_thresholds : ndarray of shape(min(max_bins, n_unique_values) - 1,)
        The increasing numeric values that can be used to separate the bins.
        A given value x will be mapped into bin value i iff
        bining_thresholds[i - 1] < x <= binning_thresholds[i]
    """
    # ignore missing values when computing bin thresholds
    missing_mask = np.isnan(col_data)
    if missing_mask.any():
        col_data = col_data[~missing_mask]
    # The data will be sorted anyway in np.unique and again in percentile, so we do it
    # here. Sorting also returns a contiguous array.
    col_data = np.sort(col_data)
    distinct_values = np.unique(col_data).astype(X_DTYPE)
    if len(distinct_values) <= max_bins:
        midpoints = distinct_values[:-1] + distinct_values[1:]
        midpoints *= 0.5
    else:
        # We could compute approximate midpoint percentiles using the output of
        # np.unique(col_data, return_counts) instead but this is more
        # work and the performance benefit will be limited because we
        # work on a fixed-size subsample of the full data.
        percentiles = np.linspace(0, 100, num=max_bins + 1)
        percentiles = percentiles[1:-1]
        midpoints = np.percentile(col_data, percentiles, method="midpoint").astype(
            X_DTYPE
        )
        assert midpoints.shape[0] == max_bins - 1

    # We avoid having +inf thresholds: +inf thresholds are only allowed in
    # a "split on nan" situation.
    np.clip(midpoints, a_min=None, a_max=ALMOST_INF, out=midpoints)
    return midpoints

class _BinMapper(TransformerMixin, BaseEstimator):
    """Transformer that maps a dataset into integer-valued bins.

    For continuous features, the bins are created in a feature-wise fashion,
    using quantiles so that each bins contains approximately the same number
    of samples. For large datasets, quantiles are computed on a subset of the
    data to speed-up the binning, but the quantiles should remain stable.

    For categorical features, the raw categorical values are expected to be
    in [0, 254] (this is not validated here though) and each category
    corresponds to a bin. All categorical values must be known at
    initialization: transform() doesn't know how to bin unknown categorical
    values. Note that transform() is only used on non-training data in the
    case of early stopping.

    Features with a small number of values may be binned into less than
    ``n_bins`` bins. The last bin (at index ``n_bins - 1``) is always reserved
    for missing values.

    n_bins : int, default=256
        The maximum number of bins to use (including the bin for missing
        values). Should be in [3, 256]. Non-missing values are binned on
        ``max_bins = n_bins - 1`` bins. The last bin is always reserved for
        missing values. If for a given feature the number of unique values is
        less than ``max_bins``, then those unique values will be used to
        compute the bin thresholds, instead of the quantiles. For categorical
        features indicated by ``is_categorical``, the docstring for
        ``is_categorical`` details on this procedure.
    subsample : int or None, default=2e5
        If ``n_samples > subsample``, then ``sub_samples`` samples will be
        randomly chosen to compute the quantiles. If ``None``, the whole data
        is used.
    is_categorical : ndarray of bool of shape (n_features,), default=None
        Indicates categorical features. By default, all features are
        considered continuous.
    known_categories : list of {ndarray, None} of shape (n_features,), \
            default=none
        For each categorical feature, the array indicates the set of unique
        categorical values. These should be the possible values over all the
        data, not just the training data. For continuous features, the
        corresponding entry should be None.
    random_state: int, RandomState instance or None, default=None
        Pseudo-random number generator to control the random sub-sampling.
        Pass an int for reproducible output across multiple
        function calls.
    n_threads : int, default=None
        Number of OpenMP threads to use. `_openmp_effective_n_threads` is called
        to determine the effective number of threads use, which takes cgroups CPU
        quotes into account. See the docstring of `_openmp_effective_n_threads`
        for details.

    bin_thresholds_ : list of ndarray
        For each feature, each array indicates how to map a feature into a
        binned feature. The semantic and size depends on the nature of the
        feature:
        - for real-valued features, the array corresponds to the real-valued
          bin thresholds (the upper bound of each bin). There are ``max_bins
          - 1`` thresholds, where ``max_bins = n_bins - 1`` is the number of
          bins used for non-missing values.
        - for categorical features, the array is a map from a binned category
          value to the raw category value. The size of the array is equal to
          ``min(max_bins, category_cardinality)`` where we ignore missing
          values in the cardinality.
    n_bins_non_missing_ : ndarray, dtype=np.uint32
        For each feature, gives the number of bins actually used for
        non-missing values. For features with a lot of unique values, this is
        equal to ``n_bins - 1``.
    is_categorical_ : ndarray of shape (n_features,), dtype=np.uint8
        Indicator for categorical features.
    missing_values_bin_idx_ : np.uint8
        The index of the bin where missing values are mapped. This is a
        constant across all features. This corresponds to the last bin, and
        it is always equal to ``n_bins - 1``. Note that if ``n_bins_non_missing_``
        is less than ``n_bins - 1`` for a given feature, then there are
        empty (and unused) bins.
    """

        self,
        n_bins=256,
        subsample=int(2e5),
        is_categorical=None,
        known_categories=None,
        n_threads=None,
    ):
        self.n_bins = n_bins
        self.is_categorical = is_categorical
        self.known_categories = known_categories
        self.n_threads = n_threads

        """Fit data X by computing the binning thresholds.

        The last bin is reserved for missing values, whether missing values
        are present in the data or not.

            The data to bin.
        y: None
            Ignored.

        Returns
        -------
        """
        if not (3 <= self.n_bins <= 256):
            # min is 3: at least 2 distinct bins and a missing values bin
                "n_bins={} should be no smaller than 3 and no larger than 256.".format(
                    self.n_bins
                )
            )

        X = check_array(X, dtype=[X_DTYPE], ensure_all_finite=False)
        max_bins = self.n_bins - 1

        rng = check_random_state(self.random_state)
        if self.subsample is not None and X.shape[0] > self.subsample:
            subset = rng.choice(X.shape[0], self.subsample, replace=False)
            X = X.take(subset, axis=0)

        if self.is_categorical is None:
            self.is_categorical_ = np.zeros(X.shape[1], dtype=np.uint8)
        else:
            self.is_categorical_ = np.asarray(self.is_categorical, dtype=np.uint8)

        known_categories = self.known_categories
        if known_categories is None:
            known_categories = [None] * n_features

        # validate is_categorical and known_categories parameters
        for f_idx in range(n_features):
            is_categorical = self.is_categorical_[f_idx]
            known_cats = known_categories[f_idx]
            if is_categorical and known_cats is None:
                    f"Known categories for feature {f_idx} must be provided."
                )
            if not is_categorical and known_cats is not None:
                    f"Feature {f_idx} isn't marked as a categorical feature, "
                    "but categories were passed."
                )

        self.missing_values_bin_idx_ = self.n_bins - 1

        self.bin_thresholds_ = [None] * n_features
        n_bins_non_missing = [None] * n_features

        non_cat_thresholds = Parallel(n_jobs=self.n_threads, backend="threading")(
            delayed(_find_binning_thresholds)(X[:, f_idx], max_bins)
            for f_idx in range(n_features)
            if not self.is_categorical_[f_idx]
        )

        non_cat_idx = 0
            if self.is_categorical_[f_idx]:
                # Since categories are assumed to be encoded in
                # [0, n_cats] and since n_cats <= max_bins,
                # the thresholds *are* the unique categorical values. This will
                # lead to the correct mapping in transform()
                thresholds = known_categories[f_idx]
                n_bins_non_missing[f_idx] = thresholds.shape[0]
                self.bin_thresholds_[f_idx] = thresholds
            else:
                self.bin_thresholds_[f_idx] = non_cat_thresholds[non_cat_idx]
                n_bins_non_missing[f_idx] = self.bin_thresholds_[f_idx].shape[0] + 1
                non_cat_idx += 1

        self.n_bins_non_missing_ = np.array(n_bins_non_missing, dtype=np.uint32)

        """Bin data X.

        Missing values will be mapped to the last bin.

        For categorical features, the mapping will be incorrect for unknown
        categories. Since the BinMapper is given known_categories of the
        entire training data (i.e. before the call to train_test_split() in
        case of early-stopping), this never happens.

        Returns
        -------
        X_binned : array-like of shape (n_samples, n_features)
            The binned data (fortran-aligned).
        """
        if X.shape[1] != self.n_bins_non_missing_.shape[0]:
                "This estimator was fitted with {} features but {} got passed "
                "to transform()".format(self.n_bins_non_missing_.shape[0], X.shape[1])
            )

        n_threads = _openmp_effective_n_threads(self.n_threads)
        binned = np.zeros_like(X, dtype=X_BINNED_DTYPE, order="F")
        _map_to_bins(
            X,
            self.bin_thresholds_,
            self.is_categorical_,
            self.missing_values_bin_idx_,
            binned,
        )
        return binned

    def make_known_categories_bitsets(self):
        """Create bitsets of known categories.

        Returns
        -------
        - known_cat_bitsets : ndarray of shape (n_categorical_features, 8)
        - f_idx_map : ndarray of shape (n_features,)
        """

        categorical_features_indices = np.flatnonzero(self.is_categorical_)

        n_features = self.is_categorical_.size
        n_categorical_features = categorical_features_indices.size

        f_idx_map = np.zeros(n_features, dtype=np.uint32)
        f_idx_map[categorical_features_indices] = np.arange(
            n_categorical_features, dtype=np.uint32
        )

        known_categories = self.bin_thresholds_

        known_cat_bitsets = np.zeros(
            (n_categorical_features, 8), dtype=X_BITSET_INNER_DTYPE
        )

        # TODO: complexity is O(n_categorical_features * 255). Maybe this is
        # worth cythonizing
        for mapped_f_idx, f_idx in enumerate(categorical_features_indices):
            for raw_cat_val in known_categories[f_idx]:
                set_bitset_memoryview(known_cat_bitsets[mapped_f_idx], raw_cat_val)

        return known_cat_bitsets, f_idx_map

        self,
    ):

        y: None
            Ignored.

        Returns
        -------
        """
                )
            )

        else:

                )
                )

        )

            else:

        Returns
        -------
        """
            )

            X,
            binned,
        )

        Returns
        -------
        """

        )

        )

def get_equivalent_estimator(estimator, lib="lightgbm", n_classes=None):
    """Return an unfitted estimator from another lib with matching hyperparams.

    This utility function takes care of renaming the sklearn parameters into
    their LightGBM, XGBoost or CatBoost equivalent parameters.

    # unmapped XGB parameters:
    # - min_samples_leaf
    # - min_data_in_bin
    # - min_split_gain (there is min_split_loss though?)

    # unmapped Catboost parameters:
    # max_leaves
    # min_*
    """

    if lib not in ("lightgbm", "xgboost", "catboost"):
            "accepted libs are lightgbm, xgboost, and catboost.  got {}".format(lib)
        )

    sklearn_params = estimator.get_params()

    if sklearn_params["loss"] == "auto":
            "auto loss is not accepted. We need to know if "
            "the problem is binary or multiclass classification."
        )
    if sklearn_params["early_stopping"]:
        raise NotImplementedError("Early stopping should be deactivated.")

    lightgbm_loss_mapping = {
        "squared_error": "regression_l2",
        "absolute_error": "regression_l1",
        "log_loss": "binary" if n_classes == 2 else "multiclass",
        "gamma": "gamma",
        "poisson": "poisson",
    }

    lightgbm_params = {
        "objective": lightgbm_loss_mapping[sklearn_params["loss"]],
        "learning_rate": sklearn_params["learning_rate"],
        "n_estimators": sklearn_params["max_iter"],
        "num_leaves": sklearn_params["max_leaf_nodes"],
        "max_depth": sklearn_params["max_depth"],
        "min_data_in_leaf": sklearn_params["min_samples_leaf"],
        "reg_lambda": sklearn_params["l2_regularization"],
        "max_bin": sklearn_params["max_bins"],
        "min_data_in_bin": 1,
        "min_sum_hessian_in_leaf": 1e-3,
        "min_split_gain": 0,
        "verbosity": 10 if sklearn_params["verbose"] else -10,
        "boost_from_average": True,
        "enable_bundle": False,  # also makes feature order consistent
        "subsample_for_bin": _BinMapper().subsample,
        "poisson_max_delta_step": 1e-12,
        "feature_fraction_bynode": sklearn_params["max_features"],
    }

    if sklearn_params["loss"] == "log_loss" and n_classes > 2:
        # LightGBM multiplies hessians by 2 in multiclass loss.
        lightgbm_params["min_sum_hessian_in_leaf"] *= 2
        # LightGBM 3.0 introduced a different scaling of the hessian for the multiclass
        # case.
        # It is equivalent of scaling the learning rate.
        # See https://github.com/microsoft/LightGBM/pull/3256.
        if n_classes is not None:
            lightgbm_params["learning_rate"] *= n_classes / (n_classes - 1)

    # XGB
    xgboost_loss_mapping = {
        "squared_error": "reg:linear",
        "absolute_error": "LEAST_ABSOLUTE_DEV_NOT_SUPPORTED",
        "log_loss": "reg:logistic" if n_classes == 2 else "multi:softmax",
        "gamma": "reg:gamma",
        "poisson": "count:poisson",
    }

    xgboost_params = {
        "tree_method": "hist",
        "grow_policy": "lossguide",  # so that we can set max_leaves
        "objective": xgboost_loss_mapping[sklearn_params["loss"]],
        "max_leaves": sklearn_params["max_leaf_nodes"],
        "max_depth": sklearn_params["max_depth"] or 0,
        "lambda": sklearn_params["l2_regularization"],
        "min_child_weight": 1e-3,
        "verbosity": 2 if sklearn_params["verbose"] else 0,
        "silent": sklearn_params["verbose"] == 0,
        "n_jobs": -1,
        "colsample_bynode": sklearn_params["max_features"],
    }

    # Catboost
    catboost_loss_mapping = {
        "squared_error": "RMSE",
        # catboost does not support MAE when leaf_estimation_method is Newton
        "absolute_error": "LEAST_ASBOLUTE_DEV_NOT_SUPPORTED",
        "log_loss": "Logloss" if n_classes == 2 else "MultiClass",
        "gamma": None,
        "poisson": "Poisson",
    }

    catboost_params = {
        "loss_function": catboost_loss_mapping[sklearn_params["loss"]],
        "iterations": sklearn_params["max_iter"],
        "depth": sklearn_params["max_depth"],
        "feature_border_type": "Median",
        "leaf_estimation_method": "Newton",
        "verbose": bool(sklearn_params["verbose"]),
    }

    if lib == "lightgbm":
        from lightgbm import LGBMClassifier, LGBMRegressor

        if is_classifier(estimator):
            return LGBMClassifier(**lightgbm_params)
        else:
            return LGBMRegressor(**lightgbm_params)

    elif lib == "xgboost":
        from xgboost import XGBClassifier, XGBRegressor

            return XGBClassifier(**xgboost_params)
        else:
            return XGBRegressor(**xgboost_params)

    else:
        from catboost import CatBoostClassifier, CatBoostRegressor

            return CatBoostClassifier(**catboost_params)
        else:
            return CatBoostRegressor(**catboost_params)

def _update_leaves_values(loss, grower, y_true, raw_prediction, sample_weight):
    """Update the leaf values to be predicted by the tree.

        loss.fit_intercept_only(y_true - raw_prediction)

    This is only applied if loss.differentiable is False.
    Note: It only works, if the loss is a function of the residual, as is the
    case for AbsoluteError and PinballLoss. Otherwise, one would need to get
    the minimum of loss(y_true, raw_prediction + x) in x. A few examples:
      - AbsoluteError: median(y_true - raw_prediction).
      - PinballLoss: quantile(y_true - raw_prediction).

    More background:
    For the standard gradient descent method according to "Greedy Function
    Approximation: A Gradient Boosting Machine" by Friedman, all loss functions but the
    squared loss need a line search step. BaseHistGradientBoosting, however, implements
    a so called Newton boosting where the trees are fitted to a 2nd order
    approximations of the loss in terms of gradients and hessians. In this case, the
    line search step is only necessary if the loss is not smooth, i.e. not
    differentiable, which renders the 2nd order approximation invalid. In fact,
    non-smooth losses arbitrarily set hessians to 1 and effectively use the standard
    gradient descent method with line search.
    """
    # TODO: Ideally this should be computed in parallel over the leaves using something
    # similar to _update_raw_predictions(), but this requires a cython version of
    # median().
    for leaf in grower.finalized_leaves:
        indices = leaf.sample_indices
            sw = None
        else:
            sw = sample_weight[indices]
        update = loss.fit_intercept_only(
            y_true=y_true[indices] - raw_prediction[indices],
            sample_weight=sw,
        )
        leaf.value = grower.shrinkage * update

def _patch_raw_predict(estimator, raw_predictions):
    """Context manager that patches _raw_predict to return raw_predictions.

    `raw_predictions` is typically a precomputed array to avoid redundant
    state-wise computations fitting with early stopping enabled: in this case
    `raw_predictions` is incrementally updated whenever we add a tree to the
    boosted ensemble.

    Note: this makes fitting HistGradientBoosting* models inherently non thread
    safe at fit time. However thread-safety at fit time was never guaranteed nor
    enforced for scikit-learn estimators in general.

    Thread-safety at prediction/transform time is another matter as those
    operations are typically side-effect free and therefore often thread-safe by
    default for most scikit-learn models and would like to keep it that way.
    Therefore this context manager should only be used at fit time.

    TODO: in the future, we could explore the possibility to extend the scorer
    public API to expose a way to compute vales from raw predictions. That would
    probably require also making the scorer aware of the inverse link function
    used by the estimator which is typically private API for now, hence the need
    for this patching mechanism.
    """
    orig_raw_predict = estimator._raw_predict

    def _patched_raw_predicts(*args, **kwargs):

    estimator._raw_predict = _patched_raw_predicts
    yield estimator
    estimator._raw_predict = orig_raw_predict

class BaseHistGradientBoosting(BaseEstimator, ABC):
    """Base class for histogram-based gradient boosting estimators."""

        "loss": [BaseLoss],
        "max_iter": [Interval(Integral, 1, None, closed="left")],
        "max_leaf_nodes": [Interval(Integral, 2, None, closed="left"), None],
        "max_depth": [Interval(Integral, 1, None, closed="left"), None],
        "min_samples_leaf": [Interval(Integral, 1, None, closed="left")],
        "l2_regularization": [Interval(Real, 0, None, closed="left")],
        "max_features": [Interval(RealNotInt, 0, 1, closed="right")],
        "monotonic_cst": ["array-like", dict, None],
        "interaction_cst": [
            list,
            tuple,
            StrOptions({"pairwise", "no_interactions"}),
            None,
        ],
        "n_iter_no_change": [Interval(Integral, 1, None, closed="left")],
        "validation_fraction": [
            Interval(RealNotInt, 0, 1, closed="neither"),
            None,
        ],
        "max_bins": [Interval(Integral, 2, 255, closed="both")],
        "categorical_features": ["array-like", StrOptions({"from_dtype"}), None],
        "early_stopping": [StrOptions({"auto"}), "boolean"],
        "scoring": [str, callable, None],
    }

        self,
        loss,
        *,
        max_iter,
        max_leaf_nodes,
        l2_regularization,
        max_bins,
        categorical_features,
        monotonic_cst,
        interaction_cst,
        warm_start,
        early_stopping,
        scoring,
        validation_fraction,
        n_iter_no_change,
        tol,
        verbose,
    ):
        self.l2_regularization = l2_regularization
        self.max_bins = max_bins
        self.interaction_cst = interaction_cst
        self.categorical_features = categorical_features
        self.early_stopping = early_stopping
        self.scoring = scoring

    def _validate_parameters(self):
        """Validate parameters passed to __init__.

        The parameters that are directly passed to the grower are checked in
        TreeGrower."""
        if self.monotonic_cst is not None and self.n_trees_per_iteration_ != 1:
                "monotonic constraints are not supported for multiclass classification."
            )

    def _finalize_sample_weight(self, sample_weight, y):
        """Finalize sample weight.

        Used by subclasses to adjust sample_weights. This is useful for implementing
        class weights.
        """
        return sample_weight

    def _preprocess_X(self, X, *, reset):
        """Preprocess and validate X.

        X : {array-like, pandas DataFrame} of shape (n_samples, n_features)

        reset : bool
            Whether to reset the `n_features_in_` and `feature_names_in_ attributes.

        Returns
        -------
            Validated input data.

        known_categories : list of ndarray of shape (n_categories,)
            List of known categories for each categorical feature.
        """
        # If there is a preprocessor, we let the preprocessor handle the validation.
        # Otherwise, we validate the data ourselves.
        check_X_kwargs = dict(dtype=[X_DTYPE], ensure_all_finite=False)
        if not reset:
            if self._preprocessor is None:
                return validate_data(self, X, reset=False, **check_X_kwargs)
            return self._preprocessor.transform(X)

        # At this point, reset is False, which runs during `fit`.
        self.is_categorical_ = self._check_categorical_features(X)

        if self.is_categorical_ is None:
            self._preprocessor = None
            self._is_categorical_remapped = None

            X = validate_data(self, X, **check_X_kwargs)
            return X, None

        ordinal_encoder = OrdinalEncoder(
            categories="auto",
            handle_unknown="use_encoded_value",
            unknown_value=np.nan,
            encoded_missing_value=np.nan,
            dtype=X_DTYPE,
        )

        check_X = partial(check_array, **check_X_kwargs)
        numerical_preprocessor = FunctionTransformer(check_X)
        self._preprocessor = ColumnTransformer(
            [
                ("encoder", ordinal_encoder, self.is_categorical_),
                ("numerical", numerical_preprocessor, ~self.is_categorical_),
            ]
        )
        self._preprocessor.set_output(transform="default")
        X = self._preprocessor.fit_transform(X)
        # check categories found by the OrdinalEncoder and get their encoded values
        known_categories = self._check_categories()
        self.n_features_in_ = self._preprocessor.n_features_in_
        with suppress(AttributeError):
            self.feature_names_in_ = self._preprocessor.feature_names_in_

        # The ColumnTransformer's output places the categorical features at the
        # beginning
        categorical_remapped = np.zeros(n_features, dtype=bool)
        categorical_remapped[self._preprocessor.output_indices_["encoder"]] = True
        self._is_categorical_remapped = categorical_remapped

        return X, known_categories

    def _check_categories(self):
        """Check categories found by the preprocessor and return their encoded values.

        Returns a list of length ``self.n_features_in_``, with one entry per
        input feature.

        For non-categorical features, the corresponding entry is ``None``.

        For categorical features, the corresponding entry is an array
        containing the categories as encoded by the preprocessor (an
        ``OrdinalEncoder``), excluding missing values. The entry is therefore
        ``np.arange(n_categories)`` where ``n_categories`` is the number of
        unique values in the considered feature column, after removing missing
        values.

        If ``n_categories > self.max_bins`` for any feature, a ``ValueError``
        is raised.
        """
        encoder = self._preprocessor.named_transformers_["encoder"]
        known_categories = [None] * self._preprocessor.n_features_in_
        categorical_column_indices = np.arange(self._preprocessor.n_features_in_)[
            self._preprocessor.output_indices_["encoder"]
        ]
        for feature_idx, categories in zip(
            categorical_column_indices, encoder.categories_
        ):
            # OrdinalEncoder always puts np.nan as the last category if the
            # training data has missing values. Here we remove it because it is
            # already added by the _BinMapper.
            if len(categories) and is_scalar_nan(categories[-1]):
                categories = categories[:-1]
            if categories.size > self.max_bins:
                try:
                    feature_name = repr(encoder.feature_names_in_[feature_idx])
                except AttributeError:
                    feature_name = f"at index {feature_idx}"
                    f"Categorical feature {feature_name} is expected to "
                    f"have a cardinality <= {self.max_bins} but actually "
                    f"has a cardinality of {categories.size}."
                )
            known_categories[feature_idx] = np.arange(len(categories), dtype=X_DTYPE)
        return known_categories

    def _check_categorical_features(self, X):
        """Check and validate categorical features in X

        Return
        ------
        is_categorical : ndarray of shape (n_features,) or None, dtype=bool
            Indicates whether a feature is categorical. If no feature is
            categorical, this is None.
        """
        # Special code for pandas because of a bug in recent pandas, which is
        # fixed in main and maybe included in 2.2.1, see
        # https://github.com/pandas-dev/pandas/pull/57173.
        # Also pandas versions < 1.5.1 do not support the dataframe interchange
        if _is_pandas_df(X):
            X_is_dataframe = True
            categorical_columns_mask = np.asarray(X.dtypes == "category")
        elif hasattr(X, "__dataframe__"):
            categorical_columns_mask = np.asarray(
                [
                    c.dtype[0].name == "CATEGORICAL"
                    for c in X.__dataframe__().get_columns()
                ]
            )
        else:
            X_is_dataframe = False
            categorical_columns_mask = None

        categorical_features = self.categorical_features

        categorical_by_dtype = (
            isinstance(categorical_features, str)
            and categorical_features == "from_dtype"
        )
        no_categorical_dtype = categorical_features is None or (
            categorical_by_dtype and not X_is_dataframe
        )

        if no_categorical_dtype:

        use_pandas_categorical = categorical_by_dtype and X_is_dataframe
        if use_pandas_categorical:
            categorical_features = categorical_columns_mask
        else:
            categorical_features = np.asarray(categorical_features)

        if categorical_features.size == 0:

        if categorical_features.dtype.kind not in ("i", "b", "U", "O"):
                "categorical_features must be an array-like of bool, int or "
                f"str, got: {categorical_features.dtype.name}."
            )

        if categorical_features.dtype.kind == "O":
            types = set(type(f) for f in categorical_features)
            if types != {str}:
                    "categorical_features must be an array-like of bool, int or "
                    f"str, got: {', '.join(sorted(t.__name__ for t in types))}."
                )

        # At this point `_validate_data` was not called yet because we want to use the
        # dtypes are used to discover the categorical features. Thus `feature_names_in_`
        # is not defined yet.
        feature_names_in_ = getattr(X, "columns", None)

        if categorical_features.dtype.kind in ("U", "O"):
            # check for feature names
            if feature_names_in_ is None:
                    "categorical_features should be passed as an array of "
                    "integers or as a boolean mask when the model is fitted "
                    "on data without feature names."
                )
            is_categorical = np.zeros(n_features, dtype=bool)
            feature_names = list(feature_names_in_)
            for feature_name in categorical_features:
                try:
                    is_categorical[feature_names.index(feature_name)] = True
                except ValueError as e:
                        f"categorical_features has a item value '{feature_name}' "
                        "which is not a valid feature name of the training "
                        f"data. Observed feature names: {feature_names}"
                    ) from e
        elif categorical_features.dtype.kind == "i":
            # check for categorical features as indices
            if (
                np.max(categorical_features) >= n_features
                or np.min(categorical_features) < 0
            ):
                    "categorical_features set as integer "
                    "indices must be in [0, n_features - 1]"
                )
            is_categorical[categorical_features] = True
        else:
            if categorical_features.shape[0] != n_features:
                    "categorical_features set as a boolean mask "
                    "must have shape (n_features,), got: "
                    f"{categorical_features.shape}"
                )
            is_categorical = categorical_features

        if not np.any(is_categorical):
        return is_categorical

    def _check_interaction_cst(self, n_features):
        """Check and validation for interaction constraints."""
        if self.interaction_cst is None:

        if self.interaction_cst == "no_interactions":
            interaction_cst = [[i] for i in range(n_features)]
        elif self.interaction_cst == "pairwise":
            interaction_cst = itertools.combinations(range(n_features), 2)
        else:
            interaction_cst = self.interaction_cst

        try:
            constraints = [set(group) for group in interaction_cst]
                "Interaction constraints must be a sequence of tuples or lists, got:"
                f" {self.interaction_cst!r}."
            )

        for group in constraints:
            for x in group:
                if not (isinstance(x, Integral) and 0 <= x < n_features):
                        "Interaction constraints must consist of integer indices in"
                        f" [0, n_features - 1] = [0, {n_features - 1}], specifying the"
                        " position of features, got invalid indices:"
                        f" {group!r}"
                    )

        # Add all not listed features as own group by default.
        rest = set(range(n_features)) - set().union(*constraints)
        if len(rest) > 0:
            constraints.append(rest)

        return constraints

        sample_weight : array-like of shape (n_samples,) default=None
            Weights of training data.

        Returns
        -------
        """
        fit_start_time = time()
        acc_find_split_time = 0.0  # time spent finding the best splits
        acc_apply_split_time = 0.0  # time spent splitting nodes
        acc_compute_hist_time = 0.0  # time spent computing histograms
        # time spent predicting X for gradient and hessians update
        acc_prediction_time = 0.0
        X, known_categories = self._preprocess_X(X, reset=True)
        y = _check_y(y, estimator=self)
        y = self._encode_y(y)
        # Do not create unit sample weights by default to later skip some
        # computation
            sample_weight = _check_sample_weight(sample_weight, X, dtype=np.float64)
            # TODO: remove when PDP supports sample weights
            self._fitted_with_sw = True

        sample_weight = self._finalize_sample_weight(sample_weight, y)

        # When warm starting, we want to reuse the same seed that was used
        # the first time fit was called (e.g. train/val split).
        # For feature subsampling, we want to continue with the rng we started with.
        if not self.warm_start or not self._is_fitted():
            self._random_seed = rng.randint(np.iinfo(np.uint32).max, dtype="u8")
            feature_subsample_seed = rng.randint(np.iinfo(np.uint32).max, dtype="u8")
            self._feature_subsample_rng = np.random.default_rng(feature_subsample_seed)

        self._validate_parameters()
        monotonic_cst = _check_monotonic_cst(self, self.monotonic_cst)
        # _preprocess_X places the categorical features at the beginning,
        # change the order of monotonic_cst accordingly
        if self.is_categorical_ is not None:
            monotonic_cst_remapped = np.concatenate(
                (
                    monotonic_cst[self.is_categorical_],
                    monotonic_cst[~self.is_categorical_],
                )
            )
        else:
            monotonic_cst_remapped = monotonic_cst

        # used for validation in predict
        n_samples, self._n_features = X.shape

        # Encode constraints into a list of sets of features indices (integers).
        interaction_cst = self._check_interaction_cst(self._n_features)

        # we need this stateful variable to tell raw_predict() that it was
        # called from fit() (this current method), and that the data it has
        # received is pre-binned.
        # predicting is faster on pre-binned data, so we want early stopping
        # predictions to be made on pre-binned data. Unfortunately the _scorer
        # can only call predict() or predict_proba(), not raw_predict(), and
        # there's no way to tell the scorer that it needs to predict binned
        # data.
        self._in_fit = True

        # `_openmp_effective_n_threads` is used to take cgroups CPU quotes
        # into account when determine the maximum number of threads to use.
        n_threads = _openmp_effective_n_threads()

        if isinstance(self.loss, str):
            self._loss = self._get_loss(sample_weight=sample_weight)
        elif isinstance(self.loss, BaseLoss):
            self._loss = self.loss

        if self.early_stopping == "auto":
            self.do_early_stopping_ = n_samples > 10000
        else:
            self.do_early_stopping_ = self.early_stopping

        # create validation data if needed
        self._use_validation_data = self.validation_fraction is not None
        if self.do_early_stopping_ and self._use_validation_data:
            # stratify for classification
            # instead of checking predict_proba, loss.n_classes >= 2 would also work
            stratify = y if hasattr(self._loss, "predict_proba") else None

            # Save the state of the RNG for the training and validation split.
            # This is needed in order to have the same split when using
            # warm starting.

                X_train, X_val, y_train, y_val = train_test_split(
                    X,
                    y,
                    test_size=self.validation_fraction,
                    stratify=stratify,
                    random_state=self._random_seed,
                )
                sample_weight_train = sample_weight_val = None
            else:
                # TODO: incorporate sample_weight in sampling here, as well as
                # stratify
                (
                    X_train,
                    X_val,
                    y_train,
                    y_val,
                    sample_weight_train,
                    sample_weight_val,
                ) = train_test_split(
                    X,
                    y,
                )
        else:

        # Bin the data
        # For ease of use of the API, the user-facing GBDT classes accept the
        # parameter max_bins, which doesn't take into account the bin for
        # missing values (which is always allocated). However, since max_bins
        # isn't the true maximal number of bins, all other private classes
        # (binmapper, histbuilder...) accept n_bins instead, which is the
        # actual total number of bins. Everywhere in the code, the
        # convention is that n_bins == max_bins + 1
        n_bins = self.max_bins + 1  # + 1 for missing values
        self._bin_mapper = _BinMapper(
            is_categorical=self._is_categorical_remapped,
            known_categories=known_categories,
            random_state=self._random_seed,
            n_threads=n_threads,
        )
        X_binned_train = self._bin_data(X_train, is_training_data=True)
        if X_val is not None:
            X_binned_val = self._bin_data(X_val, is_training_data=False)
        else:
            X_binned_val = None

        # Uses binned data to check for missing values
        has_missing_values = (
            (X_binned_train == self._bin_mapper.missing_values_bin_idx_)
            .any(axis=0)
            .astype(np.uint8)
        )

            print("Fitting gradient boosted rounds:")

        n_samples = X_binned_train.shape[0]
        scoring_is_predefined_string = self.scoring in _SCORERS
        need_raw_predictions_val = X_binned_val is not None and (
            scoring_is_predefined_string or self.scoring == "loss"
        )
        # First time calling fit, or no warm start
        if not (self._is_fitted() and self.warm_start):
            # Clear random state and score attributes

            # initialize raw_predictions: those are the accumulated values
            # predicted by the trees for the training data. raw_predictions has
            # shape (n_samples, n_trees_per_iteration) where
            # n_trees_per_iterations is n_classes in multiclass classification,
            # else 1.
            # self._baseline_prediction has shape (1, n_trees_per_iteration)
            self._baseline_prediction = self._loss.fit_intercept_only(
                y_true=y_train, sample_weight=sample_weight_train
            ).reshape((1, -1))
                shape=(n_samples, self.n_trees_per_iteration_),
                dtype=self._baseline_prediction.dtype,
                order="F",
            )
            raw_predictions += self._baseline_prediction

            # predictors is a matrix (list of lists) of TreePredictor objects
            # with shape (n_iter_, n_trees_per_iteration)
            self._predictors = predictors = []

            # Initialize structures and attributes related to early stopping
            self._scorer = None  # set if scoring != loss
            raw_predictions_val = None  # set if use val and scoring is a string
            self.train_score_ = []
            self.validation_score_ = []

            if self.do_early_stopping_:
                # populate train_score and validation_score with the
                # predictions of the initial model (before the first tree)

                # Create raw_predictions_val for storing the raw predictions of
                # the validation data.
                if need_raw_predictions_val:
                    raw_predictions_val = np.zeros(
                        shape=(X_binned_val.shape[0], self.n_trees_per_iteration_),
                        dtype=self._baseline_prediction.dtype,
                        order="F",
                    )

                    raw_predictions_val += self._baseline_prediction

                if self.scoring == "loss":
                    # we're going to compute scoring w.r.t the loss. As losses
                    # take raw predictions as input (unlike the scorers), we
                    # can optimize a bit and avoid repeating computing the
                    # predictions of the previous trees. We'll reuse
                    # raw_predictions (as it's needed for training anyway) for
                    # evaluating the training loss.

                    self._check_early_stopping_loss(
                        raw_predictions=raw_predictions,
                        y_train=y_train,
                        sample_weight_train=sample_weight_train,
                        raw_predictions_val=raw_predictions_val,
                        y_val=y_val,
                        sample_weight_val=sample_weight_val,
                        n_threads=n_threads,
                    )
                else:
                    self._scorer = check_scoring(self, self.scoring)
                    # _scorer is a callable with signature (est, X, y) and
                    # calls est.predict() or est.predict_proba() depending on
                    # its nature.
                    # Unfortunately, each call to _scorer() will compute
                    # the predictions of all the trees. So we use a subset of
                    # the training set to compute train scores.

                    # Compute the subsample set
                    (
                        X_binned_small_train,
                        y_small_train,
                        sample_weight_small_train,
                        indices_small_train,
                    ) = self._get_small_trainset(
                        X_binned_train,
                        y_train,
                        sample_weight_train,
                        self._random_seed,
                    )

                    # If the scorer is a predefined string, then we optimize
                    # the evaluation by reusing the incrementally updated raw
                    # predictions.
                    if scoring_is_predefined_string:
                        raw_predictions_small_train = raw_predictions[
                            indices_small_train
                        ]
                    else:
                        raw_predictions_small_train = None

                    self._check_early_stopping_scorer(
                        X_binned_val,
                        y_val,
                        sample_weight_val,
                        raw_predictions_small_train=raw_predictions_small_train,
                    )

        else:
            # Check that the maximum number of iterations is not smaller
            # than the number of iterations from the previous fit
            if self.max_iter < self.n_iter_:
                    "max_iter=%d must be larger than or equal to "
                    "n_iter_=%d when warm_start==True" % (self.max_iter, self.n_iter_)
                )

            # Convert array attributes to lists
            self.train_score_ = self.train_score_.tolist()
            self.validation_score_ = self.validation_score_.tolist()

            # Compute raw predictions
            raw_predictions = self._raw_predict(X_binned_train, n_threads=n_threads)
            if self.do_early_stopping_ and need_raw_predictions_val:
                raw_predictions_val = self._raw_predict(
                    X_binned_val, n_threads=n_threads
                )
            else:
                raw_predictions_val = None

            if self.do_early_stopping_ and self.scoring != "loss":
                # Compute the subsample set
                (
                    X_binned_small_train,
                    y_small_train,
                    sample_weight_small_train,
                    indices_small_train,
                ) = self._get_small_trainset(
                    X_binned_train, y_train, sample_weight_train, self._random_seed
                )

            # Get the predictors from the previous fit
            predictors = self._predictors

            begin_at_stage = self.n_iter_

        # initialize gradients and hessians (empty arrays).
        # shape = (n_samples, n_trees_per_iteration).
        gradient, hessian = self._loss.init_gradient_and_hessian(
            n_samples=n_samples, dtype=G_H_DTYPE, order="F"
        )

        for iteration in range(begin_at_stage, self.max_iter):
            if self.verbose >= 2:
                iteration_start_time = time()
                print(
                    "[{}/{}] ".format(iteration + 1, self.max_iter), end="", flush=True
                )

            # Update gradients and hessians, inplace
            # Note that self._loss expects shape (n_samples,) for
            # n_trees_per_iteration = 1 else shape (n_samples, n_trees_per_iteration).
            if self._loss.constant_hessian:
                self._loss.gradient(
                    y_true=y_train,
                    sample_weight=sample_weight_train,
                    gradient_out=gradient,
                    n_threads=n_threads,
                )
            else:
                self._loss.gradient_hessian(
                    hessian_out=hessian,
                )

            # Append a list since there may be more than 1 predictor per iter
            predictors.append([])

            # 2-d views of shape (n_samples, n_trees_per_iteration_) or (n_samples, 1)
            # on gradient and hessian to simplify the loop over n_trees_per_iteration_.
            if gradient.ndim == 1:
                g_view = gradient.reshape((-1, 1))
                h_view = hessian.reshape((-1, 1))
            else:
                g_view = gradient
                h_view = hessian

            # Build `n_trees_per_iteration` trees.
            for k in range(self.n_trees_per_iteration_):
                grower = TreeGrower(
                    X_binned=X_binned_train,
                    gradients=g_view[:, k],
                    hessians=h_view[:, k],
                    n_bins=n_bins,
                    n_bins_non_missing=self._bin_mapper.n_bins_non_missing_,
                    has_missing_values=has_missing_values,
                    is_categorical=self._is_categorical_remapped,
                    monotonic_cst=monotonic_cst_remapped,
                    interaction_cst=interaction_cst,
                    max_leaf_nodes=self.max_leaf_nodes,
                    max_depth=self.max_depth,
                    min_samples_leaf=self.min_samples_leaf,
                    l2_regularization=self.l2_regularization,
                    feature_fraction_per_split=self.max_features,
                    rng=self._feature_subsample_rng,
                    shrinkage=self.learning_rate,
                )
                grower.grow()

                acc_apply_split_time += grower.total_apply_split_time
                acc_find_split_time += grower.total_find_split_time
                acc_compute_hist_time += grower.total_compute_hist_time

                if not self._loss.differentiable:
                    _update_leaves_values(
                        loss=self._loss,
                        grower=grower,
                        y_true=y_train,
                        raw_prediction=raw_predictions[:, k],
                        sample_weight=sample_weight_train,
                    )

                predictor = grower.make_predictor(
                    binning_thresholds=self._bin_mapper.bin_thresholds_
                )
                predictors[-1].append(predictor)

                # Update raw_predictions with the predictions of the newly
                # created tree.
                tic_pred = time()
                _update_raw_predictions(raw_predictions[:, k], grower, n_threads)
                toc_pred = time()
                acc_prediction_time += toc_pred - tic_pred

            should_early_stop = False
                # Update raw_predictions_val with the newest tree(s)
                    for k, pred in enumerate(self._predictors[-1]):
                        raw_predictions_val[:, k] += pred.predict_binned(
                            X_binned_val,
                            self._bin_mapper.missing_values_bin_idx_,
                            n_threads,
                        )

                    should_early_stop = self._check_early_stopping_loss(
                    )

                else:
                    # If the scorer is a predefined string, then we optimize the
                    # evaluation by reusing the incrementally computed raw predictions.
                        ]
                    else:

                    should_early_stop = self._check_early_stopping_scorer(
                        y_val,
                    )

                self._print_iteration_stats(iteration_start_time)

            # maybe we could also early stop if all the trees are stumps?
            if should_early_stop:
                break

            duration = time() - fit_start_time
            n_total_leaves = sum(
                predictor.get_n_leaf_nodes()
                for predictors_at_ith_iteration in self._predictors
                for predictor in predictors_at_ith_iteration
            )
            n_predictors = sum(
                len(predictors_at_ith_iteration)
            )
            print(
                "Fit {} trees in {:.3f} s, ({} total leaves)".format(
                    n_predictors, duration, n_total_leaves
                )
            )
            print(
                "{:<32} {:.3f}s".format(
                    "Time spent computing histograms:", acc_compute_hist_time
                )
            )
            print(
                    "Time spent finding best splits:", acc_find_split_time
                )
            )
            print(
                    "Time spent applying splits:", acc_apply_split_time
                )
            )
            print(
                "{:<32} {:.3f}s".format("Time spent predicting:", acc_prediction_time)
            )

        self.train_score_ = np.asarray(self.train_score_)
        self.validation_score_ = np.asarray(self.validation_score_)
        del self._in_fit  # hard delete so we're sure it can't be used anymore

        return len(getattr(self, "_predictors", [])) > 0

        for var in ("train_score_", "validation_score_"):
            if hasattr(self, var):
                delattr(self, var)

    def _get_small_trainset(self, X_binned_train, y_train, sample_weight_train, seed):
        """Compute the indices of the subsample set and return this set.

        For efficiency, we need to subsample the training set to compute scores
        with scorers.
        """
        # TODO: incorporate sample_weights here in `resample`
        subsample_size = 10000
        if X_binned_train.shape[0] > subsample_size:
            indices = np.arange(X_binned_train.shape[0])
            stratify = y_train if is_classifier(self) else None
            indices = resample(
                indices,
                n_samples=subsample_size,
                replace=False,
                random_state=seed,
            )
            X_binned_small_train = X_binned_train[indices]
            y_small_train = y_train[indices]
            if sample_weight_train is not None:
                sample_weight_small_train = sample_weight_train[indices]
            else:
                sample_weight_small_train = None
            X_binned_small_train = np.ascontiguousarray(X_binned_small_train)
            return (
                X_binned_small_train,
                y_small_train,
                sample_weight_small_train,
                indices,
            )
        else:
            return X_binned_train, y_train, sample_weight_train, slice(None)

    def _check_early_stopping_scorer(
        self,
        X_binned_small_train,
        y_small_train,
        sample_weight_small_train,
        X_binned_val,
        y_val,
        raw_predictions_small_train=None,
        raw_predictions_val=None,
    ):
        """Check if fitting should be early-stopped based on scorer.

        Scores are computed on validation data or on training data.
        """
            y_small_train = self.classes_[y_small_train.astype(int)]

        self.train_score_.append(
            self._score_with_raw_predictions(
                raw_predictions_small_train,
            )
        )

        if self._use_validation_data:
                y_val = self.classes_[y_val.astype(int)]
            self.validation_score_.append(
                self._score_with_raw_predictions(
                    X_binned_val, y_val, sample_weight_val, raw_predictions_val
                )
            )
            return self._should_stop(self.validation_score_)
        else:
            return self._should_stop(self.train_score_)

    def _score_with_raw_predictions(self, X, y, sample_weight, raw_predictions=None):
        if raw_predictions is None:
            patcher_raw_predict = nullcontext()
        else:
            patcher_raw_predict = _patch_raw_predict(self, raw_predictions)

        with patcher_raw_predict:
                return self._scorer(self, X, y)
            else:
                return self._scorer(self, X, y, sample_weight=sample_weight)

    def _check_early_stopping_loss(
        self,
        y_train,
        sample_weight_train,
        raw_predictions_val,
        y_val,
        n_threads=1,
    ):
        """Check if fitting should be early-stopped based on loss.

        """
            -self._loss(
                y_true=y_train,
                sample_weight=sample_weight_train,
                n_threads=n_threads,
            )
        )

                -self._loss(
                    y_true=y_val,
                    raw_prediction=raw_predictions_val,
                    sample_weight=sample_weight_val,
                )
            )
        else:

    def _should_stop(self, scores):
        """
        Return True (do early stopping) if the last n scores aren't better
        than the (n-1)th-to-last score, up to some tolerance.
        """
        reference_position = self.n_iter_no_change + 1
        if len(scores) < reference_position:

        # A higher score is always better. Higher tol means that it will be
        # harder for subsequent iteration to be considered an improvement upon
        # the reference score, and therefore it is more likely to early stop
        # because of the lack of significant improvement.
        reference_score = scores[-reference_position] + self.tol
        recent_scores = scores[-reference_position + 1 :]
        recent_improvements = [score > reference_score for score in recent_scores]
        return not any(recent_improvements)

    def _bin_data(self, X, is_training_data):

        If is_training_data, then fit the _bin_mapper attribute.
        Else, the binned data is converted to a C-contiguous array.
        """

        description = "training" if is_training_data else "validation"
            print(
                "Binning {:.3f} GB of {} data: ".format(X.nbytes / 1e9, description),
                end="",
                flush=True,
            )
        tic = time()
        if is_training_data:
            X_binned = self._bin_mapper.fit_transform(X)  # F-aligned array
        else:
            X_binned = self._bin_mapper.transform(X)  # F-aligned array
            # We convert the array to C-contiguous since predicting is faster
            # with this layout (training is faster on F-arrays though)
            X_binned = np.ascontiguousarray(X_binned)
        toc = time()
            duration = toc - tic
            print("{:.3f} s".format(duration))

        return X_binned

    def _print_iteration_stats(self, iteration_start_time):
        """Print info about the current fitting iteration."""
        log_msg = ""

        predictors_of_ith_iteration = [
            predictors_list
            for predictors_list in self._predictors[-1]
            if predictors_list
        ]
        n_trees = len(predictors_of_ith_iteration)
        max_depth = max(
            predictor.get_max_depth() for predictor in predictors_of_ith_iteration
        )
        n_leaves = sum(
            predictor.get_n_leaf_nodes() for predictor in predictors_of_ith_iteration
        )

        if n_trees == 1:
            log_msg += "{} tree, {} leaves, ".format(n_trees, n_leaves)
        else:
            log_msg += "{} trees, {} leaves ".format(n_trees, n_leaves)
            log_msg += "({} on avg), ".format(int(n_leaves / n_trees))

        log_msg += "max depth = {}, ".format(max_depth)

        if self.do_early_stopping_:
            if self.scoring == "loss":
                factor = -1  # score_ arrays contain the negative loss
                name = "loss"
            else:
                factor = 1
                name = "score"
            log_msg += "train {}: {:.5f}, ".format(name, factor * self.train_score_[-1])
            if self._use_validation_data:
                log_msg += "val {}: {:.5f}, ".format(
                    name, factor * self.validation_score_[-1]
                )

        iteration_time = time() - iteration_start_time
        log_msg += "in {:0.3f}s".format(iteration_time)

        print(log_msg)

    def _raw_predict(self, X, n_threads=None):
        """Return the sum of the leaves values over all predictors.

        n_threads : int, default=None
            Number of OpenMP threads to use. `_openmp_effective_n_threads` is called
            to determine the effective number of threads use, which takes cgroups CPU
            quotes into account. See the docstring of `_openmp_effective_n_threads`
            for details.

        Returns
        -------
        raw_predictions : array, shape (n_samples, n_trees_per_iteration)
        """
        is_binned = getattr(self, "_in_fit", False)
        if not is_binned:
            X = self._preprocess_X(X, reset=False)

        raw_predictions = np.zeros(
            shape=(n_samples, self.n_trees_per_iteration_),
            dtype=self._baseline_prediction.dtype,
        )
        raw_predictions += self._baseline_prediction

        # We intentionally decouple the number of threads used at prediction
        # time from the number of threads used at fit time because the model
        # can be deployed on a different machine for prediction purposes.
        n_threads = _openmp_effective_n_threads(n_threads)
        self._predict_iterations(
            X, self._predictors, raw_predictions, is_binned, n_threads
        )

    def _predict_iterations(self, X, predictors, raw_predictions, is_binned, n_threads):
        """Add the predictions of the predictors to raw_predictions."""
            (
                known_cat_bitsets,
                f_idx_map,
            ) = self._bin_mapper.make_known_categories_bitsets()

        for predictors_of_ith_iteration in predictors:
            for k, predictor in enumerate(predictors_of_ith_iteration):
                if is_binned:
                    predict = partial(
                        predictor.predict_binned,
                        missing_values_bin_idx=self._bin_mapper.missing_values_bin_idx_,
                    )
                else:
                        predictor.predict,
                        known_cat_bitsets=known_cat_bitsets,
                        f_idx_map=f_idx_map,
                    )
                raw_predictions[:, k] += predict(X)

    def _staged_raw_predict(self, X):

        Yields
        ------
        raw_predictions : generator of ndarray of shape \
            (n_samples, n_trees_per_iteration)
        """
        X = self._preprocess_X(X, reset=False)
        if X.shape[1] != self._n_features:
                "X has {} features but this estimator was trained with "
                "{} features.".format(X.shape[1], self._n_features)
            )
        )

        for iteration in range(len(self._predictors)):
            self._predict_iterations(
                X,
                self._predictors[iteration : iteration + 1],
                is_binned=False,
            )

        grid : ndarray, shape (n_samples, n_target_features), dtype=np.float32
        target_features : ndarray, shape (n_target_features), dtype=np.intp

        Returns
        -------
        averaged_predictions : ndarray, shape \
                (n_trees_per_iteration, n_samples)
        """

        if getattr(self, "_fitted_with_sw", False):
                "{} does not support partial dependence "
                "plots with the 'recursion' method when "
                "sample weights were given during fit "
                "time.".format(self.__class__.__name__)
            )

        grid = np.asarray(grid, dtype=X_DTYPE, order="C")
            (self.n_trees_per_iteration_, grid.shape[0]), dtype=Y_DTYPE
        )

        for predictors_of_ith_iteration in self._predictors:
                predictor.compute_partial_dependence(
                )
        # Note that the learning rate is already accounted for in the leaves
        # values.

        pass

    def _encode_y(self, y=None):
        pass

    @property
    def n_iter_(self):
        """Number of iterations of the boosting process."""
        return len(self._predictors)

class HistGradientBoostingRegressor(RegressorMixin, BaseHistGradientBoosting):
    """Histogram-based Gradient Boosting Regression Tree.

    This estimator is much faster than
    :class:`GradientBoostingRegressor<sklearn.ensemble.GradientBoostingRegressor>`
    for big datasets (n_samples >= 10 000).

    This estimator has native support for missing values (NaNs). During
    training, the tree grower learns at each split point whether samples
    with missing values should go to the left or right child, based on the
    potential gain. When predicting, samples with missing values are
    assigned to the left or right child consequently. If no missing values
    were encountered for a given feature during training, then samples with
    missing values are mapped to whichever child has the most samples.
    See :ref:`sphx_glr_auto_examples_ensemble_plot_hgbt_regression.py` for a
    usecase example of this feature.

    This implementation is inspired by
    `LightGBM <https://github.com/Microsoft/LightGBM>`_.

    Read more in the :ref:`User Guide <histogram_based_gradient_boosting>`.

    loss : {'squared_error', 'absolute_error', 'gamma', 'poisson', 'quantile'}, \
        The loss function to use in the boosting process. Note that the
        "squared error", "gamma" and "poisson" losses actually implement
        "half least squares loss", "half gamma deviance" and "half poisson
        deviance" to simplify the computation of the gradient. Furthermore,
        "gamma" and "poisson" losses internally use a log-link, "gamma"
        requires ``y > 0`` and "poisson" requires ``y >= 0``.
        "quantile" uses the pinball loss.

           Added option 'poisson'.

           Added option 'quantile'.

        .. versionchanged:: 1.3
           Added option 'gamma'.

    quantile : float, default=None
        If loss is "quantile", this parameter specifies which quantile to be estimated
        and must be between 0 and 1.
        The learning rate, also known as *shrinkage*. This is used as a
        multiplicative factor for the leaves values. Use ``1`` for no
        shrinkage.
        The maximum number of iterations of the boosting process, i.e. the
        maximum number of trees.
    max_leaf_nodes : int or None, default=31
        The maximum number of leaves for each tree. Must be strictly greater
        than 1. If None, there is no maximum limit.
    max_depth : int or None, default=None
        The maximum depth of each tree. The depth of a tree is the number of
        edges to go from the root to the deepest leaf.
        Depth isn't constrained by default.
    min_samples_leaf : int, default=20
        The minimum number of samples per leaf. For small datasets with less
        than a few hundred samples, it is recommended to lower this value
        since only very shallow trees would be built.
    l2_regularization : float, default=0
        The L2 regularization parameter penalizing leaves with small hessians.
        Use ``0`` for no regularization (default).
    max_features : float, default=1.0
        Proportion of randomly chosen features in each and every node split.
        This is a form of regularization, smaller values make the trees weaker
        learners and might prevent overfitting.
        If interaction constraints from `interaction_cst` are present, only allowed
        features are taken into account for the subsampling.

    max_bins : int, default=255
        The maximum number of bins to use for non-missing values. Before
        training, each feature of the input array `X` is binned into
        integer-valued bins, which allows for a much faster training stage.
        Features with a small number of unique values may use less than
        ``max_bins`` bins. In addition to the ``max_bins`` bins, one more bin
        is always reserved for missing values. Must be no larger than 255.
    categorical_features : array-like of {bool, int, str} of shape (n_features) \
            or shape (n_categorical_features,), default='from_dtype'
        Indicates the categorical features.

        - None : no feature will be considered categorical.
        - boolean array-like : boolean mask indicating categorical features.
        - integer array-like : integer indices indicating categorical
          features.
        - str array-like: names of categorical features (assuming the training
          data has feature names).
        - `"from_dtype"`: dataframe columns with dtype "category" are
          considered to be categorical features. The input must be an object
          exposing a ``__dataframe__`` method such as pandas or polars
          DataFrames to use this feature.

        For each categorical feature, there must be at most `max_bins` unique
        categories. Negative values for categorical features encoded as numeric
        dtypes are treated as missing values. All categorical values are
        converted to floating point numbers. This means that categorical values
        of 1.0 and 1 are treated as the same category.

        Read more in the :ref:`User Guide <categorical_support_gbdt>` and
        :ref:`sphx_glr_auto_examples_ensemble_plot_gradient_boosting_categorical.py`.

           Added support for feature names.

           Added `"from_dtype"` option.

           The default value changed from `None` to `"from_dtype"`.

    monotonic_cst : array-like of int of shape (n_features) or dict, default=None
        Monotonic constraint to enforce on each feature are specified using the
        following integer values:

        - 1: monotonic increase
        - 0: no constraint
        - -1: monotonic decrease

        If a dict with str keys, map feature to monotonic constraints by name.
        If an array, the features are mapped to constraints by position. See
        :ref:`monotonic_cst_features_names` for a usage example.

           Accept dict of constraints with feature names as keys.

    interaction_cst : {"pairwise", "no_interactions"} or sequence of lists/tuples/sets \
            of int, default=None
        Specify interaction constraints, the sets of features which can
        interact with each other in child node splits.

        Each item specifies the set of feature indices that are allowed
        to interact with each other. If there are more features than
        specified in these constraints, they are treated as if they were
        specified as an additional set.

        The strings "pairwise" and "no_interactions" are shorthands for
        allowing only pairwise or no interactions, respectively.

        For instance, with 5 features in total, `interaction_cst=[{0, 1}]`
        is equivalent to `interaction_cst=[{0, 1}, {2, 3, 4}]`,
        and specifies that each branch of a tree will either only split
        on features 0 and 1 or only split on features 2, 3 and 4.

        See :ref:`this example<ice-vs-pdp>` on how to use `interaction_cst`.

        and add more estimators to the ensemble. For results to be valid, the
        estimator should be re-trained on the same data only.
        See :term:`the Glossary <warm_start>`.
    early_stopping : 'auto' or bool, default='auto'
        If 'auto', early stopping is enabled if the sample size is larger than
        10000. If True, early stopping is enabled, otherwise early stopping is
        disabled.

    scoring : str or callable or None, default='loss'
        Scoring method to use for early stopping. Only used if `early_stopping`
        is enabled. Options:

        - str: see :ref:`scoring_string_names` for options.
        - callable: a scorer callable object (e.g., function) with signature
          ``scorer(estimator, X, y)``. See :ref:`scoring_callable` for details.
        - `None`: the :ref:`coefficient of determination <r2_score>`
          (:math:`R^2`) is used.
        - 'loss': early stopping is checked w.r.t the loss value.

    validation_fraction : int or float or None, default=0.1
        Proportion (or absolute size) of training data to set aside as
        validation data for early stopping. If None, early stopping is done on
        the training data. Only used if early stopping is performed.
    n_iter_no_change : int, default=10
        Used to determine when to "early stop". The fitting process is
        stopped when none of the last ``n_iter_no_change`` scores are better
        than the ``n_iter_no_change - 1`` -th-to-last one, up to some
        tolerance. Only used if early stopping is performed.
    tol : float, default=1e-7
        The absolute tolerance to use when comparing scores during early
        stopping. The higher the tolerance, the more likely we are to early
        stop: higher tolerance means that it will be harder for subsequent
        iterations to be considered an improvement upon the reference score.
        The verbosity level. If not zero, print some information about the
        fitting process. ``1`` prints only summary info, ``2`` prints info per
        iteration.
        Pseudo-random number generator to control the subsampling in the
        binning process, and the train/validation data split if early stopping
        is enabled.

    do_early_stopping_ : bool
        Indicates whether early stopping is used during training.
        The number of iterations as selected by early stopping, depending on
        the `early_stopping` parameter. Otherwise it corresponds to max_iter.
        The number of tree that are built at each iteration. For regressors,
    train_score_ : ndarray, shape (n_iter_+1,)
        The scores at each iteration on the training data. The first entry
        is the score of the ensemble before the first iteration. Scores are
        computed according to the ``scoring`` parameter. If ``scoring`` is
        not 'loss', scores are computed on a subset of at most 10 000
        samples. Empty if no early stopping.
    validation_score_ : ndarray, shape (n_iter_+1,)
        The scores at each iteration on the held-out validation data. The
        first entry is the score of the ensemble before the first iteration.
        Scores are computed according to the ``scoring`` parameter. Empty if
        no early stopping or if ``validation_fraction`` is None.
    is_categorical_ : ndarray, shape (n_features, ) or None
        Boolean mask for the categorical features. ``None`` if there are no
        categorical features.

    See Also
    --------
    GradientBoostingRegressor : Exact gradient boosting method that does not
        scale as good on datasets with a large number of samples.
    RandomForestRegressor : A meta-estimator that fits a number of decision
        tree regressors on various sub-samples of the dataset and uses
        averaging to improve the statistical performance and control
        over-fitting.
    AdaBoostRegressor : A meta-estimator that begins by fitting a regressor
        regressor on the same dataset but where the weights of instances are
        adjusted according to the error of the current prediction. As such,
        subsequent regressors focus more on difficult cases.

    Examples
    --------
    >>> from sklearn.ensemble import HistGradientBoostingRegressor
    >>> est = HistGradientBoostingRegressor().fit(X, y)
    >>> est.score(X, y)
    0.92...
    """

        **BaseHistGradientBoosting._parameter_constraints,
        "loss": [
                {
                    "squared_error",
                    "absolute_error",
                    "poisson",
                    "gamma",
                    "quantile",
                }
            ),
            BaseLoss,
        ],
        "quantile": [Interval(Real, 0, 1, closed="both"), None],
    }

        self,
        *,
        quantile=None,
        max_leaf_nodes=31,
        min_samples_leaf=20,
        l2_regularization=0.0,
        max_bins=255,
        categorical_features="from_dtype",
        interaction_cst=None,
        early_stopping="auto",
        scoring="loss",
        n_iter_no_change=10,
        tol=1e-7,
    ):
            l2_regularization=l2_regularization,
            max_bins=max_bins,
            monotonic_cst=monotonic_cst,
            interaction_cst=interaction_cst,
            categorical_features=categorical_features,
            early_stopping=early_stopping,
            scoring=scoring,
            tol=tol,
        )

        """Predict values for X.

        Returns
        -------
        """
        # Return inverse link of raw predictions after converting
        # shape (n_samples, 1) to (n_samples,)
        return self._loss.link.inverse(self._raw_predict(X).ravel())

        """Predict regression target for each iteration.

        Yields
        ------
            The predicted values of the input samples, for each iteration.
        """
            yield self._loss.link.inverse(raw_predictions.ravel())

    def _encode_y(self, y):
        y = y.astype(Y_DTYPE, copy=False)
        if self.loss == "gamma":
            # Ensure y > 0
            if not np.all(y > 0):
                raise ValueError("loss='gamma' requires strictly positive y.")
        elif self.loss == "poisson":
            # Ensure y >= 0 and sum(y) > 0
            if not (np.all(y >= 0) and np.sum(y) > 0):
                    "loss='poisson' requires non-negative y and sum(y) > 0."
                )
        return y

        if self.loss == "quantile":
            return _LOSSES[self.loss](
                sample_weight=sample_weight, quantile=self.quantile
            )
        else:

class HistGradientBoostingClassifier(ClassifierMixin, BaseHistGradientBoosting):
    """Histogram-based Gradient Boosting Classification Tree.

    :class:`GradientBoostingClassifier<sklearn.ensemble.GradientBoostingClassifier>`

    loss : {'log_loss'}, default='log_loss'
        The loss function to use in the boosting process.

        For binary classification problems, 'log_loss' is also known as logistic loss,
        binomial deviance or binary crossentropy. Internally, the model fits one tree
        per boosting iteration and uses the logistic sigmoid function (expit) as
        inverse link function to compute the predicted positive class probability.

        For multiclass classification problems, 'log_loss' is also known as multinomial
        deviance or categorical crossentropy. Internally, the model fits one tree per
        boosting iteration and per class and uses the softmax function as inverse link
        function to compute the predicted probabilities of the classes.

        maximum number of trees for binary classification. For multiclass
        classification, `n_classes` trees per iteration are built.

          features.

        Read more in the :ref:`User Guide <categorical_support_gbdt>`.

        The constraints are only valid for binary classifications and hold
        over the probability of the positive class.

        disabled.

        - `None`: :ref:`accuracy <accuracy_score>` is used.

        The absolute tolerance to use when comparing scores. The higher the
        tolerance, the more likely we are to early stop: higher tolerance
        means that it will be harder for subsequent iterations to be
        considered an improvement upon the reference score.
    class_weight : dict or 'balanced', default=None
        Weights associated with classes in the form `{class_label: weight}`.
        If not given, all classes are supposed to have weight one.
        as `n_samples / (n_classes * np.bincount(y))`.
        through the fit method) if `sample_weight` is specified.

    classes_ : array, shape = (n_classes,)
        The number of tree that are built at each iteration. This is equal to 1
        for binary classification, and to ``n_classes`` for multiclass
        classification.

    See Also
    --------
    GradientBoostingClassifier : Exact gradient boosting method that does not

    Examples
    --------
    >>> from sklearn.ensemble import HistGradientBoostingClassifier
    >>> clf = HistGradientBoostingClassifier().fit(X, y)
    1.0
    """

        "loss": [StrOptions({"log_loss"}), BaseLoss],
        "class_weight": [dict, StrOptions({"balanced"}), None],
    }

        self,
        *,
        tol=1e-7,
    ):
            tol=tol,
        )

        """Adjust sample_weights with class_weights."""
        if self.class_weight is None:
            return sample_weight

        expanded_class_weight = compute_sample_weight(self.class_weight, y)

            return sample_weight * expanded_class_weight
        else:
            return expanded_class_weight

        Returns
        -------
        """
        # TODO: This could be done in parallel
            # np.argmax([0.5, 0.5]) is 0, not 1. Therefore "> 0" not ">= 0" to be
            # consistent with the multiclass case.
            encoded_classes = (raw_predictions.ravel() > 0).astype(int)
        else:

        """Predict classes at each iteration.

        Yields
        ------
            The predicted classes of the input samples, for each iteration.
        """
            if raw_predictions.shape[1] == 1:
                # np.argmax([0, 0]) is 0, not 1, therefore "> 0" not ">= 0"
                encoded_classes = (raw_predictions.ravel() > 0).astype(int)
            else:
            yield self.classes_.take(encoded_classes, axis=0)

        Returns
        -------
        p : ndarray, shape (n_samples, n_classes)
        """

        """Predict class probabilities at each iteration.

        Yields
        ------
            The predicted class probabilities of the input samples,
            for each iteration.
        """
            yield self._loss.predict_proba(raw_predictions)

        Returns
        -------
        decision : ndarray, shape (n_samples,) or \
                (n_samples, n_trees_per_iteration)
            The raw predicted values (i.e. the sum of the trees leaves) for
            each sample. n_trees_per_iteration is equal to the number of
            classes in multiclass classification.
        """
        decision = self._raw_predict(X)
        if decision.shape[1] == 1:
            decision = decision.ravel()
        return decision

        Yields
        ------
        decision : generator of ndarray of shape (n_samples,) or \
        """
        for staged_decision in self._staged_raw_predict(X):
            if staged_decision.shape[1] == 1:
                staged_decision = staged_decision.ravel()
            yield staged_decision

        encoded_y = label_encoder.fit_transform(y)
        encoded_y = encoded_y.astype(Y_DTYPE, copy=False)

        # At this point self.loss == "log_loss"
        if self.n_trees_per_iteration_ == 1:
            return HalfBinomialLoss(sample_weight=sample_weight)
        else:
            return HalfMultinomialLoss(
                sample_weight=sample_weight, n_classes=self.n_trees_per_iteration_
            )

        self,
        loss,
        *,
        max_iter,
        max_bins,
        scoring,
        tol,
        verbose,
    ):

            )

        """

        Returns
        -------

        """

        )

            [
            ]
        )

        values.

        is raised.
        """
        ]
        ):
                try:
                )

        Return
        ------
        """
                [
                ]
            )
        else:

        )
        )

        else:

            )

                )

                )
                try:
                    ) from e
            if (
            ):
                )
        else:
                )

        else:

        try:
            )

                    )

        Returns
        -------
        """

                (
                )
            )
        else:

        # data.

        else:

                    X,
                    y,
                )
            else:
                # stratify
                (
                    X_train,
                    X_val,
                    y_train,
                    y_val,
                    X,
                    y,
                )
        else:

        )
        else:

        )

        )

            # else 1.
            )

                    )

                    )
                else:

                    (
                        y_train,
                    )

                        ]
                    else:

                        y_val,
                    )

        else:
                )

                )
            else:

                (
                )

        )

                print(
                )

                )
            else:
                )

            else:

                )

                    )

                )

                        )

                    )

                else:
                        ]
                    else:

                        y_val,
                    )

                break

            )
            )
            print(
                )
            )
            print(
                )
            )
            print(
                )
            )
            print(
                )
            )
            print(
            )

        """
                indices,
            )
            else:
            return (
                indices,
            )
        else:

        self,
        y_val,
    ):

        """

            )
        )

                )
            )
        else:

        else:

            else:

        self,
        y_train,
        y_val,
    ):

        """
            )
        )

                )
            )
        else:

        """
        """

        """

            print(
                end="",
            )
        else:

        ]
        )
        )

        else:

            else:
                factor = 1
                )

        Returns
        -------
        """

        )

        )

            (

                    )
                else:
                    )

        Yields
        ------
        """
            )
        )

                X,
            )

        Returns
        -------
        """

            )

        )

                )
        # values.

        self,
        *,
        tol=1e-7,
    ):
            tol=tol,
        )

        Returns
        -------
        """

        Yields
        ------
        """

                )
        return y

            )
        else:

        self,
        *,
        tol=1e-7,
    ):
            tol=tol,
        )

        else:

        Returns
        -------
        """
        else:

        Yields
        ------
        """
            else:

        Returns
        -------
        """

        Yields
        ------
        """

        Returns
        -------
        """

        Yields
        ------
        """

        else:
            )

class TreeNode:
    """Tree Node class used in TreeGrower.

    This isn't used for prediction purposes, only for training (see
    TreePredictor).

    depth : int
        The depth of the node, i.e. its distance from the root.
    sample_indices : ndarray of shape (n_samples_at_node,), dtype=np.uint32
        The indices of the samples at the node.
    partition_start : int
        start position of the node's sample_indices in splitter.partition.
    partition_stop : int
        stop position of the node's sample_indices in splitter.partition.
    sum_gradients : float
        The sum of the gradients of the samples at the node.
    sum_hessians : float
        The sum of the hessians of the samples at the node.

    depth : int
    split_info : SplitInfo or None
        The result of the split evaluation.
    is_leaf : bool
        True if node is a leaf
    left_child : TreeNode or None
        The left child of the node. None for leaves.
    right_child : TreeNode or None
        The right child of the node. None for leaves.
    value : float or None
        The value of the leaf, as computed in finalize_leaf(). None for
        non-leaf nodes.
    allowed_features : None or ndarray, dtype=int
        Indices of features allowed to split for children.
    interaction_cst_indices : None or list of ints
        Indices of the interaction sets that have to be applied on splits of
        child nodes. The fewer sets the stronger the constraint as fewer sets
        contain fewer features.
    children_lower_bound : float
    children_upper_bound : float
    """

        self,
        *,
        depth,
        sample_indices,
        partition_start,
        partition_stop,
        sum_gradients,
        sum_hessians,
        value=None,
    ):
        self.depth = depth
        self.sample_indices = sample_indices
        self.n_samples = sample_indices.shape[0]
        self.sum_gradients = sum_gradients
        self.sum_hessians = sum_hessians
        self.value = value
        self.is_leaf = False
        self.allowed_features = None
        self.interaction_cst_indices = None
        self.set_children_bounds(float("-inf"), float("+inf"))
        self.split_info = None
        self.left_child = None
        self.right_child = None
        self.histograms = None
        # start and stop indices of the node in the splitter.partition
        # array. Concretely,
        # self.sample_indices = view(self.splitter.partition[start:stop])
        # Please see the comments about splitter.partition and
        # splitter.split_indices for more info about this design.
        # These 2 attributes are only used in _update_raw_prediction, because we
        # need to iterate over the leaves and I don't know how to efficiently
        # store the sample_indices views because they're all of different sizes.
        self.partition_start = partition_start
        self.partition_stop = partition_stop

    def set_children_bounds(self, lower, upper):
        """Set children values bounds to respect monotonic constraints."""

        # These are bounds for the node's *children* values, not the node's
        # value. The bounds are used in the splitter when considering potential
        # left and right child.
        self.children_lower_bound = lower
        self.children_upper_bound = upper

    def __lt__(self, other_node):
        """Comparison for priority queue.

        Nodes with high gain are higher priority than nodes with low gain.

        heapq.heappush only need the '<' operator.
        heapq.heappop take the smallest item first (smaller is higher
        priority).

        other_node : TreeNode
            The node to compare with.
        """
        return self.split_info.gain > other_node.split_info.gain

class TreeGrower:
    """Tree grower class used to build a tree.

    The tree is fitted to predict the values of a Newton-Raphson step. The
    splits are considered in a best-first fashion, and the quality of a
    split is defined in splitting._split_gain.

    X_binned : ndarray of shape (n_samples, n_features), dtype=np.uint8
        The binned input samples. Must be Fortran-aligned.
    gradients : ndarray of shape (n_samples,)
        The gradients of each training sample. Those are the gradients of the
        loss w.r.t the predictions, evaluated at iteration ``i - 1``.
    hessians : ndarray of shape (n_samples,)
        The hessians of each training sample. Those are the hessians of the
        The maximum number of leaves for each tree. If None, there is no
        maximum limit.
        The minimum number of samples per leaf.
    min_gain_to_split : float, default=0.
        The minimum gain needed to split a node. Splits with lower gain will
        be ignored.
    min_hessian_to_split : float, default=1e-3
        The minimum sum of hessians needed in each node. Splits that result in
        at least one child having a sum of hessians less than
        ``min_hessian_to_split`` are discarded.
        The total number of bins, including the bin for missing values. Used
        to define the shape of the histograms.
    n_bins_non_missing : ndarray, dtype=np.uint32, default=None
        non-missing values. For features with a lot of unique values, this
        is equal to ``n_bins - 1``. If it's an int, all features are
        considered to have the same number of bins. If None, all features
        are considered to have ``n_bins - 1`` bins.
    has_missing_values : bool or ndarray, dtype=bool, default=False
        Whether each feature contains missing values (in the training data).
        If it's a bool, the same value is used for all features.
        Indicates categorical features.
    monotonic_cst : array-like of int of shape (n_features,), dtype=int, default=None
        Indicates the monotonic constraint to enforce on each feature.

    interaction_cst : list of sets of integers, default=None
        List of interaction constraints.
    l2_regularization : float, default=0.
    feature_fraction_per_split : float, default=1
    rng : Generator
        Numpy random Generator used for feature subsampling.
    shrinkage : float, default=1.
        The shrinkage parameter to apply to the leaves values, also known as
        learning rate.

    histogram_builder : HistogramBuilder
    splitter : Splitter
    root : TreeNode
    finalized_leaves : list of TreeNode
    splittable_nodes : list of TreeNode
    missing_values_bin_idx : int
        Equals n_bins - 1
    n_categorical_splits : int
    n_features : int
    n_nodes : int
    total_find_split_time : float
        Time spent finding the best splits
    total_compute_hist_time : float
        Time spent computing histograms
    total_apply_split_time : float
        Time spent splitting nodes
    with_monotonic_cst : bool
        Whether there are monotonic constraints that apply. False iff monotonic_cst is
        None.
    """

        self,
        X_binned,
        gradients,
        hessians,
        min_gain_to_split=0.0,
        min_hessian_to_split=1e-3,
        n_bins_non_missing=None,
        has_missing_values=False,
        feature_fraction_per_split=1.0,
        rng=np.random.default_rng(),
        shrinkage=1.0,
    ):
        self._validate_parameters(
            X_binned,
            min_gain_to_split,
            min_hessian_to_split,
        )

        if n_bins_non_missing is None:
            n_bins_non_missing = n_bins - 1

        if isinstance(n_bins_non_missing, numbers.Integral):
            n_bins_non_missing = np.array(
                [n_bins_non_missing] * X_binned.shape[1], dtype=np.uint32
            )
        else:
            n_bins_non_missing = np.asarray(n_bins_non_missing, dtype=np.uint32)

        if isinstance(has_missing_values, bool):
            has_missing_values = [has_missing_values] * X_binned.shape[1]
        has_missing_values = np.asarray(has_missing_values, dtype=np.uint8)

        # `monotonic_cst` validation is done in _validate_monotonic_cst
        # at the estimator level and therefore the following should not be
        # needed when using the public API.
        if monotonic_cst is None:
            monotonic_cst = np.full(
                shape=X_binned.shape[1],
                fill_value=MonotonicConstraint.NO_CST,
                dtype=np.int8,
            )
        else:
            monotonic_cst = np.asarray(monotonic_cst, dtype=np.int8)
        self.with_monotonic_cst = np.any(monotonic_cst != MonotonicConstraint.NO_CST)

        if is_categorical is None:
            is_categorical = np.zeros(shape=X_binned.shape[1], dtype=np.uint8)
        else:
            is_categorical = np.asarray(is_categorical, dtype=np.uint8)

        if np.any(
            np.logical_and(
                is_categorical == 1, monotonic_cst != MonotonicConstraint.NO_CST
            )
        ):
            raise ValueError("Categorical features cannot have monotonic constraints.")

        hessians_are_constant = hessians.shape[0] == 1
        self.histogram_builder = HistogramBuilder(
            X_binned, n_bins, gradients, hessians, hessians_are_constant, n_threads
        )
        missing_values_bin_idx = n_bins - 1
        self.splitter = Splitter(
            X_binned=X_binned,
            n_bins_non_missing=n_bins_non_missing,
            missing_values_bin_idx=missing_values_bin_idx,
            has_missing_values=has_missing_values,
            is_categorical=is_categorical,
            min_hessian_to_split=min_hessian_to_split,
            min_gain_to_split=min_gain_to_split,
            hessians_are_constant=hessians_are_constant,
            feature_fraction_per_split=feature_fraction_per_split,
            rng=rng,
        )
        self.X_binned = X_binned
        self.min_gain_to_split = min_gain_to_split
        self.n_bins_non_missing = n_bins_non_missing
        self.missing_values_bin_idx = missing_values_bin_idx
        self.has_missing_values = has_missing_values
        self.n_features = X_binned.shape[1]
        self.splittable_nodes = []
        self.finalized_leaves = []
        self.total_find_split_time = 0.0  # time spent finding the best splits
        self.total_compute_hist_time = 0.0  # time spent computing histograms
        self.total_apply_split_time = 0.0  # time spent splitting nodes
        self.n_categorical_splits = 0
        self._initialize_root()
        self.n_nodes = 1

    def _validate_parameters(
        self,
        X_binned,
        min_gain_to_split,
        min_hessian_to_split,
    ):

        Also validate parameters passed to splitter.
        """
        if X_binned.dtype != np.uint8:
            raise NotImplementedError("X_binned must be of type uint8.")
        if not X_binned.flags.f_contiguous:
                "X_binned should be passed as Fortran contiguous "
                "array for maximum efficiency."
            )
        if min_gain_to_split < 0:
                "min_gain_to_split={} must be positive.".format(min_gain_to_split)
            )
        if min_hessian_to_split < 0:
                "min_hessian_to_split={} must be positive.".format(min_hessian_to_split)
            )

    def grow(self):
        """Grow the tree, from root to leaves."""
        while self.splittable_nodes:
            self.split_next()

        self._apply_shrinkage()

    def _apply_shrinkage(self):
        """Multiply leaves values by shrinkage parameter.

        This must be done at the very end of the growing process. If this were
        done during the growing process e.g. in finalize_leaf(), then a leaf
        would be shrunk but its sibling would potentially not be (if it's a
        non-leaf), which would lead to a wrong computation of the 'middle'
        value needed to enforce the monotonic constraints.
        """
        for leaf in self.finalized_leaves:
            leaf.value *= self.shrinkage

    def _initialize_root(self):
        """Initialize root node and finalize it if needed."""
        if self.interaction_cst is not None:
            allowed_features = set().union(*self.interaction_cst)
            allowed_features = np.fromiter(
                allowed_features, dtype=np.uint32, count=len(allowed_features)
            )
            arbitrary_feature = allowed_features[0]
        else:
            allowed_features = None
            arbitrary_feature = 0

        # TreeNode init needs the total sum of gradients and hessians. Therefore, we
        # first compute the histograms and then compute the total grad/hess on an
        # arbitrary feature histogram. This way we replace a loop over n_samples by a
        # loop over n_bins.
        histograms = self.histogram_builder.compute_histograms_brute(
            self.splitter.partition,  # =self.root.sample_indices
            allowed_features,
        )
        self.total_compute_hist_time += time() - tic

        n_samples = self.X_binned.shape[0]
        depth = 0
        histogram_array = np.asarray(histograms[arbitrary_feature])
        sum_gradients = histogram_array["sum_gradients"].sum()
        if self.histogram_builder.hessians_are_constant:
            sum_hessians = self.histogram_builder.hessians[0] * n_samples
        else:
            sum_hessians = histogram_array["sum_hessians"].sum()
        self.root = TreeNode(
            depth=depth,
            sample_indices=self.splitter.partition,
            partition_start=0,
            partition_stop=n_samples,
            sum_gradients=sum_gradients,
            sum_hessians=sum_hessians,
            value=0,
        )

        if self.root.n_samples < 2 * self.min_samples_leaf:
            # Do not even bother computing any splitting statistics.
            self._finalize_leaf(self.root)
            return
        if sum_hessians < self.splitter.min_hessian_to_split:
            return

            self.root.interaction_cst_indices = range(len(self.interaction_cst))
            self.root.allowed_features = allowed_features

        self.root.histograms = histograms

        self._compute_best_split_and_push(self.root)
        self.total_find_split_time += time() - tic

    def _compute_best_split_and_push(self, node):
        """Compute the best possible split (SplitInfo) of a given node.

        Also push it in the heap of splittable nodes if gain isn't zero.
        The gain of a node is 0 if either all the leaves are pure
        (best gain = 0), or if no split would satisfy the constraints,
        (min_hessians_to_split, min_gain_to_split, min_samples_leaf)
        """

        node.split_info = self.splitter.find_node_split(
            n_samples=node.n_samples,
            histograms=node.histograms,
            sum_gradients=node.sum_gradients,
            sum_hessians=node.sum_hessians,
            value=node.value,
            lower_bound=node.children_lower_bound,
            upper_bound=node.children_upper_bound,
            allowed_features=node.allowed_features,
        )

        if node.split_info.gain <= 0:  # no valid split
            self._finalize_leaf(node)
        else:
            heappush(self.splittable_nodes, node)

    def split_next(self):
        """Split the node with highest potential gain.

        Returns
        -------
        left : TreeNode
            The resulting left child.
        right : TreeNode
            The resulting right child.
        """
        # Consider the node with the highest loss reduction (a.k.a. gain)
        node = heappop(self.splittable_nodes)

        (
            sample_indices_left,
            sample_indices_right,
            right_child_pos,
        ) = self.splitter.split_indices(node.split_info, node.sample_indices)
        self.total_apply_split_time += time() - tic

        depth = node.depth + 1
        n_leaf_nodes = len(self.finalized_leaves) + len(self.splittable_nodes)
        n_leaf_nodes += 2

        left_child_node = TreeNode(
            sample_indices=sample_indices_left,
            partition_start=node.partition_start,
            partition_stop=node.partition_start + right_child_pos,
            sum_gradients=node.split_info.sum_gradient_left,
            sum_hessians=node.split_info.sum_hessian_left,
            value=node.split_info.value_left,
        )
        right_child_node = TreeNode(
            sample_indices=sample_indices_right,
            partition_start=left_child_node.partition_stop,
            partition_stop=node.partition_stop,
            sum_gradients=node.split_info.sum_gradient_right,
            sum_hessians=node.split_info.sum_hessian_right,
            value=node.split_info.value_right,
        )

        node.right_child = right_child_node
        node.left_child = left_child_node

        # set interaction constraints (the indices of the constraints sets)
            # Calculate allowed_features and interaction_cst_indices only once. Child
            # nodes inherit them before they get split.
            (
                left_child_node.allowed_features,
                left_child_node.interaction_cst_indices,
            ) = self._compute_interactions(node)
            right_child_node.interaction_cst_indices = (
                left_child_node.interaction_cst_indices
            )
            right_child_node.allowed_features = left_child_node.allowed_features

        if not self.has_missing_values[node.split_info.feature_idx]:
            # If no missing values are encountered at fit time, then samples
            # with missing values during predict() will go to whichever child
            # has the most samples.
            node.split_info.missing_go_to_left = (
                left_child_node.n_samples > right_child_node.n_samples
            )

        self.n_nodes += 2
        self.n_categorical_splits += node.split_info.is_categorical

        if self.max_leaf_nodes is not None and n_leaf_nodes == self.max_leaf_nodes:
            self._finalize_leaf(left_child_node)
            self._finalize_leaf(right_child_node)
            self._finalize_splittable_nodes()
            return left_child_node, right_child_node

        if self.max_depth is not None and depth == self.max_depth:

        if left_child_node.n_samples < self.min_samples_leaf * 2:
        if right_child_node.n_samples < self.min_samples_leaf * 2:

        if self.with_monotonic_cst:
            # Set value bounds for respecting monotonic constraints
            # See test_nodes_values() for details
            if (
                self.monotonic_cst[node.split_info.feature_idx]
                == MonotonicConstraint.NO_CST
            ):
                lower_left = lower_right = node.children_lower_bound
                upper_left = upper_right = node.children_upper_bound
            else:
                mid = (left_child_node.value + right_child_node.value) / 2
                if (
                    self.monotonic_cst[node.split_info.feature_idx]
                    == MonotonicConstraint.POS
                ):
                    lower_left, upper_left = node.children_lower_bound, mid
                    lower_right, upper_right = mid, node.children_upper_bound
                else:  # NEG
                    lower_left, upper_left = mid, node.children_upper_bound
                    lower_right, upper_right = node.children_lower_bound, mid
            left_child_node.set_children_bounds(lower_left, upper_left)
            right_child_node.set_children_bounds(lower_right, upper_right)

        # Compute histograms of children, and compute their best possible split
        # (if needed)
        should_split_left = not left_child_node.is_leaf
        should_split_right = not right_child_node.is_leaf
        if should_split_left or should_split_right:
            # We will compute the histograms of both nodes even if one of them
            # is a leaf, since computing the second histogram is very cheap
            # (using histogram subtraction).
            n_samples_left = left_child_node.sample_indices.shape[0]
            n_samples_right = right_child_node.sample_indices.shape[0]
            if n_samples_left < n_samples_right:
                smallest_child = left_child_node
                largest_child = right_child_node
            else:
                smallest_child = right_child_node
                largest_child = left_child_node

            # We use the brute O(n_samples) method on the child that has the
            # smallest number of samples, and the subtraction trick O(n_bins)
            # on the other one.
            # Note that both left and right child have the same allowed_features.
            tic = time()
            smallest_child.histograms = self.histogram_builder.compute_histograms_brute(
                smallest_child.sample_indices, smallest_child.allowed_features
            )
            largest_child.histograms = (
                self.histogram_builder.compute_histograms_subtraction(
                    node.histograms,
                    smallest_child.histograms,
                    smallest_child.allowed_features,
                )
            )
            # node.histograms is reused in largest_child.histograms. To break cyclic
            # memory references and help garbage collection, we set it to None.
            node.histograms = None
            self.total_compute_hist_time += time() - tic

            if should_split_left:
                self._compute_best_split_and_push(left_child_node)
            if should_split_right:
                self._compute_best_split_and_push(right_child_node)
            self.total_find_split_time += time() - tic

            # Release memory used by histograms as they are no longer needed
            # for leaf nodes since they won't be split.
            for child in (left_child_node, right_child_node):
                if child.is_leaf:
                    del child.histograms

        # Release memory used by histograms as they are no longer needed for
        # internal nodes once children histograms have been computed.
        del node.histograms

        return left_child_node, right_child_node

    def _compute_interactions(self, node):
        r"""Compute features allowed by interactions to be inherited by child nodes.

        Example: Assume constraints [{0, 1}, {1, 2}].
           1      <- Both constraint groups could be applied from now on
          / \
         1   2    <- Left split still fulfills both constraint groups.
        / \ / \      Right split at feature 2 has only group {1, 2} from now on.

        LightGBM uses the same logic for overlapping groups. See
        https://github.com/microsoft/LightGBM/issues/4481 for details.

        Parameters:
        node : TreeNode
            A node that might have children. Based on its feature_idx, the interaction
            constraints for possible child nodes are computed.

        Returns
        -------
        allowed_features : ndarray, dtype=uint32
            Indices of features allowed to split for children.
        interaction_cst_indices : list of ints
            Indices of the interaction sets that have to be applied on splits of
            child nodes. The fewer sets the stronger the constraint as fewer sets
            contain fewer features.
        """
        # Note:
        #  - Case of no interactions is already captured before function call.
        #  - This is for nodes that are already split and have a
        #    node.split_info.feature_idx.
        allowed_features = set()
        interaction_cst_indices = []
        for i in node.interaction_cst_indices:
            if node.split_info.feature_idx in self.interaction_cst[i]:
                interaction_cst_indices.append(i)
                allowed_features.update(self.interaction_cst[i])
        return (
            np.fromiter(allowed_features, dtype=np.uint32, count=len(allowed_features)),
            interaction_cst_indices,
        )

    def _finalize_leaf(self, node):
        """Make node a leaf of the tree being grown."""

        node.is_leaf = True
        self.finalized_leaves.append(node)

    def _finalize_splittable_nodes(self):
        """Transform all splittable nodes into leaves.

        Used when some constraint is met e.g. maximum number of leaves or
        maximum depth."""
        while len(self.splittable_nodes) > 0:
            node = self.splittable_nodes.pop()

    def make_predictor(self, binning_thresholds):
        """Make a TreePredictor object out of the current tree.

        binning_thresholds : array-like of floats
            Corresponds to the bin_thresholds_ attribute of the BinMapper.
            For each feature, this stores:

            - the bin frontiers for continuous features
            - the unique raw category values for categorical features

        Returns
        -------
        A TreePredictor object.
        """
        predictor_nodes = np.zeros(self.n_nodes, dtype=PREDICTOR_RECORD_DTYPE)
        binned_left_cat_bitsets = np.zeros(
            (self.n_categorical_splits, 8), dtype=X_BITSET_INNER_DTYPE
        )
        raw_left_cat_bitsets = np.zeros(
        )
        _fill_predictor_arrays(
            predictor_nodes,
            binned_left_cat_bitsets,
            raw_left_cat_bitsets,
            self.root,
            binning_thresholds,
            self.n_bins_non_missing,
        )
        return TreePredictor(
            predictor_nodes, binned_left_cat_bitsets, raw_left_cat_bitsets
        )

def _fill_predictor_arrays(
    predictor_nodes,
    binned_left_cat_bitsets,
    raw_left_cat_bitsets,
    grower_node,
    binning_thresholds,
    n_bins_non_missing,
    next_free_node_idx=0,
    next_free_bitset_idx=0,
):
    """Helper used in make_predictor to set the TreePredictor fields."""
    node = predictor_nodes[next_free_node_idx]
    node["count"] = grower_node.n_samples
    node["depth"] = grower_node.depth
    if grower_node.split_info is not None:
        node["gain"] = grower_node.split_info.gain
    else:
        node["gain"] = -1

    node["value"] = grower_node.value

    if grower_node.is_leaf:
        # Leaf node
        node["is_leaf"] = True
        return next_free_node_idx + 1, next_free_bitset_idx

    split_info = grower_node.split_info
    feature_idx, bin_idx = split_info.feature_idx, split_info.bin_idx
    node["feature_idx"] = feature_idx
    node["bin_threshold"] = bin_idx
    node["missing_go_to_left"] = split_info.missing_go_to_left
    node["is_categorical"] = split_info.is_categorical

    if split_info.bin_idx == n_bins_non_missing[feature_idx] - 1:
        # Split is on the last non-missing bin: it's a "split on nans".
        # All nans go to the right, the rest go to the left.
        # Note: for categorical splits, bin_idx is 0 and we rely on the bitset
        node["num_threshold"] = np.inf
    elif split_info.is_categorical:
        categories = binning_thresholds[feature_idx]
        node["bitset_idx"] = next_free_bitset_idx
        binned_left_cat_bitsets[next_free_bitset_idx] = split_info.left_cat_bitset
        set_raw_bitset_from_binned_bitset(
            raw_left_cat_bitsets[next_free_bitset_idx],
            split_info.left_cat_bitset,
            categories,
        )
        next_free_bitset_idx += 1
    else:
        node["num_threshold"] = binning_thresholds[feature_idx][bin_idx]

    next_free_node_idx += 1

    node["left"] = next_free_node_idx
    next_free_node_idx, next_free_bitset_idx = _fill_predictor_arrays(
        predictor_nodes,
        binned_left_cat_bitsets,
        raw_left_cat_bitsets,
        grower_node.left_child,
        binning_thresholds=binning_thresholds,
        n_bins_non_missing=n_bins_non_missing,
        next_free_node_idx=next_free_node_idx,
        next_free_bitset_idx=next_free_bitset_idx,
    )

    node["right"] = next_free_node_idx
    return _fill_predictor_arrays(
        grower_node.right_child,
    )

        self,
        *,
        depth,
    ):

        """

        self,
        X_binned,
        hessians,
    ):
            X_binned,
        )

            )
        else:

            )
        else:

        else:

        if np.any(
            )
        ):

        )
            rng=rng,
        )

        self,
        X_binned,
    ):

        """
            )
            )
            )

        """

            )
        else:

        )

        depth = 0
        else:
            value=0,
        )

            return
            return

        """

        )

        else:

        Returns
        -------
        """

        (

        )
        )

            (
            )

            )

            if (
            ):
            else:
                if (
                ):
                else:  # NEG

            else:

            )
                )
            )

          / \

        Returns
        -------
        """
        # Note:
        return (
        )

        Returns
        -------
        """
        )
        )
        )
        )

def test_histogram_split(n_bins):
    rng = np.random.RandomState(42)
    feature_idx = 0
    l2_regularization = 0
    min_hessian_to_split = 1e-3
    min_samples_leaf = 1
    min_gain_to_split = 0.0
    X_binned = np.asfortranarray(
        rng.randint(0, n_bins - 1, size=(int(1e4), 1)), dtype=X_BINNED_DTYPE
    )
    binned_feature = X_binned.T[feature_idx]
    sample_indices = np.arange(binned_feature.shape[0], dtype=np.uint32)
    ordered_hessians = np.ones_like(binned_feature, dtype=G_H_DTYPE)
    all_hessians = ordered_hessians
    sum_hessians = all_hessians.sum()
    hessians_are_constant = False

    for true_bin in range(1, n_bins - 2):
        for sign in [-1, 1]:
            ordered_gradients = np.full_like(binned_feature, sign, dtype=G_H_DTYPE)
            ordered_gradients[binned_feature <= true_bin] *= -1
            all_gradients = ordered_gradients
            sum_gradients = all_gradients.sum()

            builder = HistogramBuilder(
                X_binned,
                n_bins,
                all_gradients,
                all_hessians,
                hessians_are_constant,
                n_threads,
            )
                [n_bins - 1] * X_binned.shape[1], dtype=np.uint32
            )
            has_missing_values = np.array([False] * X_binned.shape[1], dtype=np.uint8)
            monotonic_cst = np.array(
                [MonotonicConstraint.NO_CST] * X_binned.shape[1], dtype=np.int8
            )
            is_categorical = np.zeros_like(monotonic_cst, dtype=np.uint8)
            missing_values_bin_idx = n_bins - 1
            splitter = Splitter(
                X_binned,
                n_bins_non_missing,
                missing_values_bin_idx,
                has_missing_values,
                is_categorical,
                monotonic_cst,
                l2_regularization,
                min_hessian_to_split,
                min_samples_leaf,
                min_gain_to_split,
            )

            histograms = builder.compute_histograms_brute(sample_indices)
            value = compute_node_value(
                sum_gradients, sum_hessians, -np.inf, np.inf, l2_regularization
            )
            split_info = splitter.find_node_split(
                sample_indices.shape[0], histograms, sum_gradients, sum_hessians, value
            )

            assert split_info.bin_idx == true_bin
            assert split_info.gain >= 0
            assert split_info.feature_idx == feature_idx
            assert (
                split_info.n_samples_left + split_info.n_samples_right
                == sample_indices.shape[0]
            )
            # Constant hessian: 1. per sample.
            assert split_info.n_samples_left == split_info.sum_hessian_left

def test_gradient_and_hessian_sanity(constant_hessian):
    # This test checks that the values of gradients and hessians are
    # consistent in different places:
    # - in split_info: si.sum_gradient_left + si.sum_gradient_right must be
    #   equal to the gradient at the node. Same for hessians.
    # - in the histograms: summing 'sum_gradients' over the bins must be
    #   constant across all features, and those sums must be equal to the
    #   node's gradient. Same for hessians.

    n_bins = 10
    n_features = 20
    n_samples = 500
    l2_regularization = 0.0

    X_binned = rng.randint(
        0, n_bins, size=(n_samples, n_features), dtype=X_BINNED_DTYPE
    )
    X_binned = np.asfortranarray(X_binned)
    sample_indices = np.arange(n_samples, dtype=np.uint32)
    all_gradients = rng.randn(n_samples).astype(G_H_DTYPE)
    sum_gradients = all_gradients.sum()
    if constant_hessian:
        all_hessians = np.ones(1, dtype=G_H_DTYPE)
        sum_hessians = 1 * n_samples
    else:
        all_hessians = rng.lognormal(size=n_samples).astype(G_H_DTYPE)
        sum_hessians = all_hessians.sum()

    builder = HistogramBuilder(
        X_binned, n_bins, all_gradients, all_hessians, constant_hessian, n_threads
    )
    n_bins_non_missing = np.array([n_bins - 1] * X_binned.shape[1], dtype=np.uint32)
    has_missing_values = np.array([False] * X_binned.shape[1], dtype=np.uint8)
    monotonic_cst = np.array(
        [MonotonicConstraint.NO_CST] * X_binned.shape[1], dtype=np.int8
    )
    is_categorical = np.zeros_like(monotonic_cst, dtype=np.uint8)
    missing_values_bin_idx = n_bins - 1
    splitter = Splitter(
        X_binned,
        n_bins_non_missing,
        missing_values_bin_idx,
        has_missing_values,
        is_categorical,
        constant_hessian,
    )

    hists_parent = builder.compute_histograms_brute(sample_indices)
    value_parent = compute_node_value(
        sum_gradients, sum_hessians, -np.inf, np.inf, l2_regularization
    )
    si_parent = splitter.find_node_split(
        n_samples, hists_parent, sum_gradients, sum_hessians, value_parent
    )
    sample_indices_left, sample_indices_right, _ = splitter.split_indices(
        si_parent, sample_indices
    )

    hists_left = builder.compute_histograms_brute(sample_indices_left)
    value_left = compute_node_value(
        si_parent.sum_gradient_left,
        si_parent.sum_hessian_left,
        -np.inf,
        np.inf,
    )
    hists_right = builder.compute_histograms_brute(sample_indices_right)
    value_right = compute_node_value(
        si_parent.sum_gradient_right,
        si_parent.sum_hessian_right,
        -np.inf,
        np.inf,
    )
    si_left = splitter.find_node_split(
        n_samples,
        hists_left,
        value_left,
    )
    si_right = splitter.find_node_split(
        hists_right,
        value_right,
    )

    # make sure that si.sum_gradient_left + si.sum_gradient_right have their
    # expected value, same for hessians
    for si, indices in (
        (si_parent, sample_indices),
        (si_left, sample_indices_left),
        (si_right, sample_indices_right),
    ):
        gradient = si.sum_gradient_right + si.sum_gradient_left
        expected_gradient = all_gradients[indices].sum()
        hessian = si.sum_hessian_right + si.sum_hessian_left
        if constant_hessian:
            expected_hessian = indices.shape[0] * all_hessians[0]
        else:
            expected_hessian = all_hessians[indices].sum()

        assert np.isclose(gradient, expected_gradient)
        assert np.isclose(hessian, expected_hessian)

    # make sure sum of gradients in histograms are the same for all features,
    # and make sure they're equal to their expected value
    hists_parent = np.asarray(hists_parent, dtype=HISTOGRAM_DTYPE)
    hists_left = np.asarray(hists_left, dtype=HISTOGRAM_DTYPE)
    hists_right = np.asarray(hists_right, dtype=HISTOGRAM_DTYPE)
    for hists, indices in (
        (hists_parent, sample_indices),
        (hists_left, sample_indices_left),
        (hists_right, sample_indices_right),
    ):
        # note: gradients and hessians have shape (n_features,),
        # we're comparing them to *scalars*. This has the benefit of also
        # making sure that all the entries are equal across features.
        gradients = hists["sum_gradients"].sum(axis=1)  # shape = (n_features,)
        expected_gradient = all_gradients[indices].sum()  # scalar
        hessians = hists["sum_hessians"].sum(axis=1)
            # 0 is not the actual hessian, but it's not computed in this case
            expected_hessian = 0.0
        else:

        assert np.allclose(gradients, expected_gradient)
        assert np.allclose(hessians, expected_hessian)

def test_split_indices():
    # Check that split_indices returns the correct splits and that
    # splitter.partition is consistent with what is returned.
    rng = np.random.RandomState(421)

    n_bins = 5
    n_samples = 10

    # split will happen on feature 1 and on bin 3
    X_binned = [
        [0, 0],
        [0, 3],
        [0, 4],
        [0, 0],
        [0, 0],
        [0, 0],
        [0, 0],
        [0, 4],
        [0, 0],
        [0, 4],
    ]
    X_binned = np.asfortranarray(X_binned, dtype=X_BINNED_DTYPE)
    all_hessians = np.ones(1, dtype=G_H_DTYPE)
    sum_hessians = 1 * n_samples
    hessians_are_constant = True

        X_binned, n_bins, all_gradients, all_hessians, hessians_are_constant, n_threads
    )
    n_bins_non_missing = np.array([n_bins] * X_binned.shape[1], dtype=np.uint32)
    )
        X_binned,
        hessians_are_constant,
    )

    assert np.all(sample_indices == splitter.partition)

    histograms = builder.compute_histograms_brute(sample_indices)
    value = compute_node_value(
    )
    si_root = splitter.find_node_split(
        n_samples, histograms, sum_gradients, sum_hessians, value
    )

    # sanity checks for best split
    assert si_root.feature_idx == 1
    assert si_root.bin_idx == 3

    samples_left, samples_right, position_right = splitter.split_indices(
        si_root, splitter.partition
    )
    assert set(samples_left) == set([0, 1, 3, 4, 5, 6, 8])
    assert set(samples_right) == set([2, 7, 9])

    assert list(samples_left) == list(splitter.partition[:position_right])
    assert list(samples_right) == list(splitter.partition[position_right:])

    # Check that the resulting split indices sizes are consistent with the
    # count statistics anticipated when looking for the best split.
    assert samples_left.shape[0] == si_root.n_samples_left
    assert samples_right.shape[0] == si_root.n_samples_right

def test_min_gain_to_split():
    # Try to split a pure node (all gradients are equal, same for hessians)
    # with min_gain_to_split = 0 and make sure that the node is not split (best
    # possible gain = -1). Note: before the strict inequality comparison, this
    # test would fail because the node would be split with a gain of 0.
    min_hessian_to_split = 0
    n_bins = 255
        rng.randint(0, n_bins, size=(n_samples, 1)), dtype=X_BINNED_DTYPE
    )
    binned_feature = X_binned[:, 0]
    all_hessians = np.ones_like(binned_feature, dtype=G_H_DTYPE)
    all_gradients = np.ones_like(binned_feature, dtype=G_H_DTYPE)

    )
    )
        X_binned,
    )

    )
    split_info = splitter.find_node_split(
    )
    assert split_info.gain == -1

def test_splitting_missing_values(
    X_binned,
    all_gradients,
    has_missing_values,
    expected_split_on_nan,
    expected_bin_idx,
    expected_go_to_left,
):
    # Make sure missing values are properly supported.
    # we build an artificial example with gradients such that the best split
    # is on bin_idx=3, when there are no missing values.
    # Then we introduce missing values and:
    #   - make sure the chosen bin is correct (find_best_bin()): it's
    #     still the same split, even though the index of the bin may change
    #   - make sure the missing values are mapped to the correct child
    #     (split_indices())

    n_bins = max(X_binned) + 1
    n_samples = len(X_binned)

    X_binned = np.array(X_binned, dtype=X_BINNED_DTYPE).reshape(-1, 1)
    all_gradients = np.array(all_gradients, dtype=G_H_DTYPE)
    has_missing_values = np.array([has_missing_values], dtype=np.uint8)

    )

    n_bins_non_missing = np.array([n_bins_non_missing], dtype=np.uint32)
    )
        X_binned,
    )

    )
    )

    assert split_info.bin_idx == expected_bin_idx
    if has_missing_values:
        assert split_info.missing_go_to_left == expected_go_to_left

    split_on_nan = split_info.bin_idx == n_bins_non_missing[0] - 1
    assert split_on_nan == expected_split_on_nan

    # Make sure the split is properly computed.
    # This also make sure missing values are properly assigned to the correct
    # child in split_indices()
    samples_left, samples_right, _ = splitter.split_indices(
        split_info, splitter.partition
    )

    if not expected_split_on_nan:
        # When we don't split on nans, the split should always be the same.
        assert set(samples_left) == set([0, 1, 2, 3])
        assert set(samples_right) == set([4, 5, 6, 7, 8, 9])
    else:
        # When we split on nans, samples with missing values are always mapped
        # to the right child.
        missing_samples_indices = np.flatnonzero(
            np.array(X_binned) == missing_values_bin_idx
        )
        non_missing_samples_indices = np.flatnonzero(
            np.array(X_binned) != missing_values_bin_idx
        )

        assert set(samples_right) == set(missing_samples_indices)
        assert set(samples_left) == set(non_missing_samples_indices)

def test_splitting_categorical_cat_smooth(
    X_binned, has_missing_values, n_bins_non_missing
):
    # Checks categorical splits are correct when the MIN_CAT_SUPPORT constraint
    # isn't respected: there are no splits

    X_binned = np.array([X_binned], dtype=X_BINNED_DTYPE).T

    all_gradients = np.ones(n_samples, dtype=G_H_DTYPE)
    sum_hessians = n_samples

    )

    )
    is_categorical = np.ones_like(monotonic_cst, dtype=np.uint8)

        X_binned,
    )

    )
    )

    # no split found

def _assert_categories_equals_bitset(categories, bitset):
    # assert that the bitset exactly corresponds to the categories
    # bitset is assumed to be an array of 8 uint32 elements

    # form bitset from threshold
    expected_bitset = np.zeros(8, dtype=np.uint32)
    for cat in categories:
        idx = cat // 32
        shift = cat % 32
        expected_bitset[idx] |= 1 << shift

    # check for equality
    assert_array_equal(expected_bitset, bitset)

def test_splitting_categorical_sanity(
    X_binned,
    expected_categories_left,
    missing_values_bin_idx,
    expected_missing_go_to_left,
):
    # Tests various combinations of categorical splits

    )

    )

        X_binned,
    )

    )
    )

    assert split_info.is_categorical
    assert split_info.gain > 0
    _assert_categories_equals_bitset(
        expected_categories_left, split_info.left_cat_bitset
    )
        assert split_info.missing_go_to_left == expected_missing_go_to_left
    # If there is no missing value during training, the flag missing_go_to_left
    # is set later in the grower.

    # make sure samples are split correctly
    )

    left_mask = np.isin(X_binned.ravel(), expected_categories_left)
    assert_array_equal(sample_indices[left_mask], samples_left)
    assert_array_equal(sample_indices[~left_mask], samples_right)

def test_split_interaction_constraints():
    """Check that allowed_features are respected."""
    n_features = 4
    # features 1 and 2 are not allowed to be split on
    allowed_features = np.array([0, 3], dtype=np.uint32)
    n_bins = 5

    split_features = []

    # The loop is to ensure that we split at least once on each allowed feature (0, 3).
    # This is tracked by split_features and checked at the end.
    for i in range(10):
        rng = np.random.RandomState(919 + i)
        X_binned = np.asfortranarray(
            rng.randint(0, n_bins - 1, size=(n_samples, n_features)),
            dtype=X_BINNED_DTYPE,
        )
        X_binned = np.asfortranarray(X_binned, dtype=X_BINNED_DTYPE)

        # Make feature 1 very important
        all_gradients = (10 * X_binned[:, 1] + rng.randn(n_samples)).astype(G_H_DTYPE)
        sum_gradients = all_gradients.sum()

        builder = HistogramBuilder(
            X_binned,
            n_bins,
            all_gradients,
            all_hessians,
            hessians_are_constant,
        )
        n_bins_non_missing = np.array([n_bins] * X_binned.shape[1], dtype=np.uint32)
        has_missing_values = np.array([False] * X_binned.shape[1], dtype=np.uint8)
        monotonic_cst = np.array(
            [MonotonicConstraint.NO_CST] * X_binned.shape[1], dtype=np.int8
        )
        is_categorical = np.zeros_like(monotonic_cst, dtype=np.uint8)
        splitter = Splitter(
            X_binned,
            n_bins_non_missing,
            has_missing_values,
            is_categorical,
            monotonic_cst,
            l2_regularization,
            min_samples_leaf,
        )

        assert np.all(sample_indices == splitter.partition)

        histograms = builder.compute_histograms_brute(sample_indices)
        value = compute_node_value(
            sum_gradients, sum_hessians, -np.inf, np.inf, l2_regularization
        )

        # with all features allowed, feature 1 should be split on as it is the most
        # important one by construction of the gradients
        si_root = splitter.find_node_split(
            histograms,
            sum_gradients,
            sum_hessians,
            value,
            allowed_features=None,
        )
        assert si_root.feature_idx == 1

        # only features 0 and 3 are allowed to be split on
            value,
            allowed_features=allowed_features,
        )
        split_features.append(si_root.feature_idx)
        assert si_root.feature_idx in allowed_features

    # make sure feature 0 and feature 3 are split on in the constraint setting
    assert set(allowed_features) == set(split_features)

def test_split_feature_fraction_per_split(forbidden_features):
    """Check that feature_fraction_per_split is respected.

    Because we set `n_features = 4` and `feature_fraction_per_split = 0.25`, it means
    that calling `splitter.find_node_split` will be allowed to select a split for a
    single completely random feature at each call. So if we iterate enough, we should
    cover all the allowed features, irrespective of the values of the gradients and
    Hessians of the objective.
    """
    allowed_features = np.array(
        list(set(range(n_features)) - forbidden_features), dtype=np.uint32
    )
    n_bins = 5
    n_samples = 40
    rng = np.random.default_rng(42)

    all_gradients = rng.uniform(low=0.5, high=1, size=n_samples).astype(G_H_DTYPE)

        rng.integers(low=0, high=n_bins - 1, size=(n_samples, n_features)),
        dtype=X_BINNED_DTYPE,
    )
        X_binned,
        n_bins,
        all_gradients,
        all_hessians,
        n_threads,
    )
    )
    )

    params = dict(
        X_binned=X_binned,
        missing_values_bin_idx=missing_values_bin_idx,
        has_missing_values=has_missing_values,
        is_categorical=is_categorical,
        monotonic_cst=monotonic_cst,
        l2_regularization=l2_regularization,
        min_hessian_to_split=min_hessian_to_split,
        min_samples_leaf=min_samples_leaf,
        min_gain_to_split=min_gain_to_split,
        hessians_are_constant=hessians_are_constant,
        rng=rng,
    )
    splitter_subsample = Splitter(
        feature_fraction_per_split=0.25,  # THIS is the important setting here.
        **params,
    )
    splitter_all_features = Splitter(feature_fraction_per_split=1.0, **params)

    assert np.all(sample_indices == splitter_subsample.partition)

    split_features_subsample = []
    split_features_all = []
    # The loop is to ensure that we split at least once on each feature.
    for i in range(20):
        si_root = splitter_subsample.find_node_split(
            value,
        )
        split_features_subsample.append(si_root.feature_idx)

        # This second splitter is our "counterfactual".
        si_root = splitter_all_features.find_node_split(
            value,
        )
        split_features_all.append(si_root.feature_idx)

    # Make sure all features are split on.
    assert set(split_features_subsample) == set(allowed_features)

    # Make sure, our counterfactual always splits on same feature.
    assert len(set(split_features_all)) == 1

def test_same_predictions_regression(
    seed, loss, min_samples_leaf, n_samples, max_leaf_nodes
):
    # Make sure sklearn has the same predictions as lightgbm for easy targets.
    #
    # In particular when the size of the trees are bound and the number of
    # samples is large enough, the structure of the prediction trees found by
    # LightGBM and sklearn should be exactly identical.
    #
    # Notes:
    # - Several candidate splits may have equal gains when the number of
    #   samples in a node is low (and because of float errors). Therefore the
    #   predictions on the test set might differ if the structure of the tree
    #   is not exactly the same. To avoid this issue we only compare the
    #   predictions on the test set when the number of samples is large enough
    #   and max_leaf_nodes is low enough.
    # - To ignore discrepancies caused by small differences in the binning
    #   strategy, data is pre-binned if n_samples > 255.
    # - We don't check the absolute_error loss here. This is because
    #   LightGBM's computation of the median (used for the initial value of
    #   raw_prediction) is a bit off (they'll e.g. return midpoints when there
    #   is no need to.). Since these tests only run 1 iteration, the
    #   discrepancy between the initial values leads to biggish differences in
    #   the predictions. These differences are much smaller with more
    #   iterations.
    pytest.importorskip("lightgbm")

    rng = np.random.RandomState(seed=seed)
    max_iter = 1
    max_bins = 255

    X, y = make_regression(
        n_samples=n_samples, n_features=5, n_informative=5, random_state=0
    )

    if loss in ("gamma", "poisson"):
        # make the target positive
        y = np.abs(y) + np.mean(np.abs(y))

    if n_samples > 255:
        # bin data and convert it to float32 so that the estimator doesn't
        # treat it as pre-binned
        X = _BinMapper(n_bins=max_bins + 1).fit_transform(X).astype(np.float32)

    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=rng)

    est_sklearn = HistGradientBoostingRegressor(
        loss=loss,
        max_bins=max_bins,
        learning_rate=1,
        early_stopping=False,
        max_leaf_nodes=max_leaf_nodes,
    )
    est_lightgbm = get_equivalent_estimator(est_sklearn, lib="lightgbm")
    est_lightgbm.set_params(min_sum_hessian_in_leaf=0)

    est_lightgbm.fit(X_train, y_train)
    est_sklearn.fit(X_train, y_train)

    # We need X to be treated an numerical data, not pre-binned data.
    X_train, X_test = X_train.astype(np.float32), X_test.astype(np.float32)

    pred_lightgbm = est_lightgbm.predict(X_train)
    pred_sklearn = est_sklearn.predict(X_train)
        # More than 65% of the predictions must be close up to the 2nd decimal.
        # TODO: We are not entirely satisfied with this lax comparison, but the root
        # cause is not clear, maybe algorithmic differences. One such example is the
        # poisson_max_delta_step parameter of LightGBM which does not exist in HGBT.
        assert (
            np.mean(np.isclose(pred_lightgbm, pred_sklearn, rtol=1e-2, atol=1e-2))
            > 0.65
        )
    else:
        # Less than 1% of the predictions may deviate more than 1e-3 in relative terms.
        assert np.mean(np.isclose(pred_lightgbm, pred_sklearn, rtol=1e-3)) > 1 - 0.01

    if max_leaf_nodes < 10 and n_samples >= 1000 and loss in ("squared_error",):
        pred_lightgbm = est_lightgbm.predict(X_test)
        pred_sklearn = est_sklearn.predict(X_test)
        # Less than 1% of the predictions may deviate more than 1e-4 in relative terms.
        assert np.mean(np.isclose(pred_lightgbm, pred_sklearn, rtol=1e-4)) > 1 - 0.01

def test_same_predictions_classification(
    seed, min_samples_leaf, n_samples, max_leaf_nodes
):
    # Same as test_same_predictions_regression but for classification

    n_classes = 2

    X, y = make_classification(
        n_samples=n_samples,
        n_classes=n_classes,
        n_features=5,
        n_informative=5,
        n_redundant=0,
        random_state=0,
    )

    est_sklearn = HistGradientBoostingClassifier(
    )
    est_lightgbm = get_equivalent_estimator(
        est_sklearn, lib="lightgbm", n_classes=n_classes
    )

    assert np.mean(pred_sklearn == pred_lightgbm) > 0.89

    acc_lightgbm = accuracy_score(y_train, pred_lightgbm)
    acc_sklearn = accuracy_score(y_train, pred_sklearn)
    np.testing.assert_almost_equal(acc_lightgbm, acc_sklearn)

    if max_leaf_nodes < 10 and n_samples >= 1000:
        assert np.mean(pred_sklearn == pred_lightgbm) > 0.89

        acc_lightgbm = accuracy_score(y_test, pred_lightgbm)
        acc_sklearn = accuracy_score(y_test, pred_sklearn)
        np.testing.assert_almost_equal(acc_lightgbm, acc_sklearn, decimal=2)

def test_same_predictions_multiclass_classification(
):

    n_classes = 3
    lr = 1

        n_clusters_per_class=1,
    )

        learning_rate=lr,
    )
    )

    proba_lightgbm = est_lightgbm.predict_proba(X_train)
    proba_sklearn = est_sklearn.predict_proba(X_train)
    # assert more than 75% of the predicted probabilities are the same up to
    # the second decimal
    assert np.mean(np.abs(proba_lightgbm - proba_sklearn) < 1e-2) > 0.75

    np.testing.assert_allclose(acc_lightgbm, acc_sklearn, rtol=0, atol=5e-2)

        proba_lightgbm = est_lightgbm.predict_proba(X_train)
        proba_sklearn = est_sklearn.predict_proba(X_train)
        # assert more than 75% of the predicted probabilities are the same up
        # to the second decimal
        assert np.mean(np.abs(proba_lightgbm - proba_sklearn) < 1e-2) > 0.75

def test_regression_dataset(n_bins):
        n_samples=500, n_features=10, n_informative=5, random_state=42
    )
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

    mapper = _BinMapper(n_bins=n_bins, random_state=42)
    X_train_binned = mapper.fit_transform(X_train)

    # Init gradients and hessians to that of least squares loss
    gradients = -y_train.astype(G_H_DTYPE)
    hessians = np.ones(1, dtype=G_H_DTYPE)

    min_samples_leaf = 10
    max_leaf_nodes = 30
    grower = TreeGrower(
        X_train_binned,
        hessians,
        n_bins=n_bins,
        n_bins_non_missing=mapper.n_bins_non_missing_,
    )
    grower.grow()

    predictor = grower.make_predictor(binning_thresholds=mapper.bin_thresholds_)

    known_cat_bitsets = np.zeros((0, 8), dtype=X_BITSET_INNER_DTYPE)
    f_idx_map = np.zeros(0, dtype=np.uint32)

    y_pred_train = predictor.predict(X_train, known_cat_bitsets, f_idx_map, n_threads)
    assert r2_score(y_train, y_pred_train) > 0.82

    y_pred_test = predictor.predict(X_test, known_cat_bitsets, f_idx_map, n_threads)
    assert r2_score(y_test, y_pred_test) > 0.67

def test_infinite_values_and_thresholds(num_threshold, expected_predictions):
    # Make sure infinite values and infinite thresholds are handled properly.
    # In particular, if a value is +inf and the threshold is ALMOST_INF the
    # sample should go to the right child. If the threshold is inf (split on
    # nan), the +inf sample will go to the left child.

    X = np.array([-np.inf, 10, 20, np.inf]).reshape(-1, 1)
    nodes = np.zeros(3, dtype=PREDICTOR_RECORD_DTYPE)

    # We just construct a simple tree with 1 root and 2 children
    # parent node
    nodes[0]["left"] = 1
    nodes[0]["right"] = 2
    nodes[0]["feature_idx"] = 0
    nodes[0]["num_threshold"] = num_threshold

    # left child
    nodes[1]["is_leaf"] = True
    nodes[1]["value"] = 0

    # right child
    nodes[2]["is_leaf"] = True
    nodes[2]["value"] = 1

    binned_cat_bitsets = np.zeros((0, 8), dtype=X_BITSET_INNER_DTYPE)
    raw_categorical_bitsets = np.zeros((0, 8), dtype=X_BITSET_INNER_DTYPE)
    known_cat_bitset = np.zeros((0, 8), dtype=X_BITSET_INNER_DTYPE)

    predictor = TreePredictor(nodes, binned_cat_bitsets, raw_categorical_bitsets)
    predictions = predictor.predict(X, known_cat_bitset, f_idx_map, n_threads)

    assert np.all(predictions == expected_predictions)

def test_categorical_predictor(bins_go_left, expected_predictions):
    # Test predictor outputs are correct with categorical features

    X_binned = np.array([[0, 1, 2, 3, 4, 5]], dtype=X_BINNED_DTYPE).T
    categories = np.array([2, 5, 6, 8, 10, 15], dtype=X_DTYPE)

    bins_go_left = np.array(bins_go_left, dtype=X_BINNED_DTYPE)

    nodes[0]["is_categorical"] = True
    nodes[0]["missing_go_to_left"] = True

    nodes[1]["value"] = 1

    nodes[2]["value"] = 0

    binned_cat_bitsets = np.zeros((1, 8), dtype=X_BITSET_INNER_DTYPE)
    raw_categorical_bitsets = np.zeros((1, 8), dtype=X_BITSET_INNER_DTYPE)
    for go_left in bins_go_left:
        set_bitset_memoryview(binned_cat_bitsets[0], go_left)

    set_raw_bitset_from_binned_bitset(
        raw_categorical_bitsets[0], binned_cat_bitsets[0], categories
    )

    # Check binned data gives correct predictions
    prediction_binned = predictor.predict_binned(
        X_binned, missing_values_bin_idx=6, n_threads=n_threads
    )
    assert_allclose(prediction_binned, expected_predictions)

    # manually construct bitset
    known_cat_bitsets = np.zeros((1, 8), dtype=np.uint32)
    known_cat_bitsets[0, 0] = np.sum(2**categories, dtype=np.uint32)
    f_idx_map = np.array([0], dtype=np.uint32)

    # Check with un-binned data
    predictions = predictor.predict(
        categories.reshape(-1, 1), known_cat_bitsets, f_idx_map, n_threads
    )
    assert_allclose(predictions, expected_predictions)

    # Check missing goes left because missing_values_bin_idx=6
    X_binned_missing = np.array([[6]], dtype=X_BINNED_DTYPE).T
    predictions = predictor.predict_binned(
        X_binned_missing, missing_values_bin_idx=6, n_threads=n_threads
    )
    assert_allclose(predictions, [1])

    # missing and unknown go left
        np.array([[np.nan, 17]], dtype=X_DTYPE).T,
        known_cat_bitsets,
        f_idx_map,
    )
    assert_allclose(predictions, [1, 1])

def _make_dumb_dataset(n_samples):
    """Make a dumb dataset to test early stopping."""
    X_dumb = rng.randn(n_samples, 1)
    y_dumb = (X_dumb[:, 0] > 0).astype("int64")
    return X_dumb, y_dumb

def test_init_parameters_validation(GradientBoosting, X, y, params, err_msg):
    with pytest.raises(ValueError, match=err_msg):
        GradientBoosting(**params).fit(X, y)

def test_early_stopping_regression(
    scoring, validation_fraction, early_stopping, n_iter_no_change, tol
):
    max_iter = 200

    X, y = make_regression(n_samples=50, random_state=0)

    gb = HistGradientBoostingRegressor(
        verbose=1,  # just for coverage
        min_samples_leaf=5,  # easier to overfit fast
        scoring=scoring,
        tol=tol,
        early_stopping=early_stopping,
        validation_fraction=validation_fraction,
        n_iter_no_change=n_iter_no_change,
    )
    gb.fit(X, y)

    if early_stopping:
        assert n_iter_no_change <= gb.n_iter_ < max_iter
    else:
        assert gb.n_iter_ == max_iter

def test_early_stopping_classification(
    data, scoring, validation_fraction, early_stopping, n_iter_no_change, tol
):
    max_iter = 50

    X, y = data

    gb = HistGradientBoostingClassifier(
        verbose=2,  # just for coverage
        tol=tol,
    )

    if early_stopping is True:
    else:

def test_early_stopping_default(GradientBoosting, X, y):
    # Test that early stopping is enabled by default if and only if there
    # are more than 10000 samples
    gb = GradientBoosting(max_iter=10, n_iter_no_change=2, tol=1e-1)
    if X.shape[0] > 10000:
        assert gb.n_iter_ < gb.max_iter
    else:
        assert gb.n_iter_ == gb.max_iter

def test_should_stop(scores, n_iter_no_change, tol, stopping):
    gbdt = HistGradientBoostingClassifier(n_iter_no_change=n_iter_no_change, tol=tol)
    assert gbdt._should_stop(scores) == stopping

def test_absolute_error():
    # For coverage only.
    X, y = make_regression(n_samples=500, random_state=0)
    gbdt = HistGradientBoostingRegressor(loss="absolute_error", random_state=0)
    gbdt.fit(X, y)
    assert gbdt.score(X, y) > 0.9

def test_absolute_error_sample_weight():
    # non regression test for issue #19400
    # make sure no error is thrown during fit of
    # HistGradientBoostingRegressor with absolute_error loss function
    # and passing sample_weight
    X = rng.uniform(-1, 1, size=(n_samples, 2))
    y = rng.uniform(-1, 1, size=n_samples)
    sample_weight = rng.uniform(0, 1, size=n_samples)
    gbdt = HistGradientBoostingRegressor(loss="absolute_error")
    gbdt.fit(X, y, sample_weight=sample_weight)

def test_gamma_y_positive(y):
    # Test that ValueError is raised if any y_i <= 0.
    err_msg = r"loss='gamma' requires strictly positive y."
    gbdt = HistGradientBoostingRegressor(loss="gamma", random_state=0)
        gbdt.fit(np.zeros(shape=(len(y), 1)), y)

def test_gamma():
    # For a Gamma distributed target, we expect an HGBT trained with the Gamma deviance
    # (loss) to give better results than an HGBT with any other loss function, measured
    # in out-of-sample Gamma deviance as metric/score.
    # Note that squared error could potentially predict negative values which is
    # invalid (np.inf) for the Gamma deviance. A Poisson HGBT (having a log link)
    # does not have that defect.
    # Important note: It seems that a Poisson HGBT almost always has better
    # out-of-sample performance than the Gamma HGBT, measured in Gamma deviance.
    # LightGBM shows the same behaviour. Hence, we only compare to a squared error
    # HGBT, but not to a Poisson deviance HGBT.
    n_train, n_test, n_features = 500, 100, 20
    X = make_low_rank_matrix(
        n_samples=n_train + n_test,
        n_features=n_features,
        random_state=rng,
    )
    # We create a log-linear Gamma model. This gives y.min ~ 1e-2, y.max ~ 1e2
    coef = rng.uniform(low=-10, high=20, size=n_features)
    # Numpy parametrizes gamma(shape=k, scale=theta) with mean = k * theta and
    # variance = k * theta^2. We parametrize it instead with mean = exp(X @ coef)
    # and variance = dispersion * mean^2 by setting k = 1 / dispersion,
    # theta =  dispersion * mean.
    dispersion = 0.5
    y = rng.gamma(shape=1 / dispersion, scale=dispersion * np.exp(X @ coef))
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=n_test, random_state=rng
    )
    gbdt_gamma = HistGradientBoostingRegressor(loss="gamma", random_state=123)
    gbdt_mse = HistGradientBoostingRegressor(loss="squared_error", random_state=123)
    dummy = DummyRegressor(strategy="mean")
    for model in (gbdt_gamma, gbdt_mse, dummy):
        model.fit(X_train, y_train)

    for X, y in [(X_train, y_train), (X_test, y_test)]:
        loss_gbdt_gamma = mean_gamma_deviance(y, gbdt_gamma.predict(X))
        # We restrict the squared error HGBT to predict at least the minimum seen y at
        # train time to make it strictly positive.
        loss_gbdt_mse = mean_gamma_deviance(
            y, np.maximum(np.min(y_train), gbdt_mse.predict(X))
        )
        loss_dummy = mean_gamma_deviance(y, dummy.predict(X))
        assert loss_gbdt_gamma < loss_dummy
        assert loss_gbdt_gamma < loss_gbdt_mse

def test_quantile_asymmetric_error(quantile):
    """Test quantile regression for asymmetric distributed targets."""
    n_samples = 10_000
    # take care that X @ coef + intercept > 0
    X = np.concatenate(
        (
            np.abs(rng.randn(n_samples)[:, None]),
            -rng.randint(2, size=(n_samples, 1)),
        ),
        axis=1,
    )
    intercept = 1.23
    coef = np.array([0.5, -2])
    # For an exponential distribution with rate lambda, e.g. exp(-lambda * x),
    # the quantile at level q is:
    #   quantile(q) = - log(1 - q) / lambda
    #   scale = 1/lambda = -quantile(q) / log(1-q)
    y = rng.exponential(
        scale=-(X @ coef + intercept) / np.log(1 - quantile), size=n_samples
    )
    model = HistGradientBoostingRegressor(
        loss="quantile",
        quantile=quantile,
        max_iter=25,
        max_leaf_nodes=10,
    ).fit(X, y)
    assert_allclose(np.mean(model.predict(X) > y), quantile, rtol=1e-2)

    pinball_loss = PinballLoss(quantile=quantile)
    loss_true_quantile = pinball_loss(y, X @ coef + intercept)
    loss_pred_quantile = pinball_loss(y, model.predict(X))
    # we are overfitting
    assert loss_pred_quantile <= loss_true_quantile

def test_poisson_y_positive(y):
    # Test that ValueError is raised if either one y_i < 0 or sum(y_i) <= 0.
    err_msg = r"loss='poisson' requires non-negative y and sum\(y\) > 0."
    gbdt = HistGradientBoostingRegressor(loss="poisson", random_state=0)

def test_poisson():
    # For Poisson distributed target, Poisson loss should give better results
    # than least squares measured in Poisson deviance as metric.
    n_train, n_test, n_features = 500, 100, 100
        n_samples=n_train + n_test, n_features=n_features, random_state=rng
    )
    # We create a log-linear Poisson model and downscale coef as it will get
    # exponentiated.
    coef = rng.uniform(low=-2, high=2, size=n_features) / np.max(X, axis=0)
    y = rng.poisson(lam=np.exp(X @ coef))
    )
    gbdt_pois = HistGradientBoostingRegressor(loss="poisson", random_state=rng)
    gbdt_ls = HistGradientBoostingRegressor(loss="squared_error", random_state=rng)
    gbdt_pois.fit(X_train, y_train)
    gbdt_ls.fit(X_train, y_train)
    dummy = DummyRegressor(strategy="mean").fit(X_train, y_train)

        metric_pois = mean_poisson_deviance(y, gbdt_pois.predict(X))
        # squared_error might produce non-positive predictions => clip
        metric_ls = mean_poisson_deviance(y, np.clip(gbdt_ls.predict(X), 1e-15, None))
        metric_dummy = mean_poisson_deviance(y, dummy.predict(X))
        assert metric_pois < metric_ls
        assert metric_pois < metric_dummy

def test_binning_train_validation_are_separated():
    # Make sure training and validation data are binned separately.
    # See issue 13926

    validation_fraction = 0.2
        early_stopping=True, validation_fraction=validation_fraction, random_state=rng
    )
    gb.fit(X_classification, y_classification)
    mapper_training_data = gb._bin_mapper

    # Note that since the data is small there is no subsampling and the
    # random_state doesn't matter
    mapper_whole_data = _BinMapper(random_state=0)
    mapper_whole_data.fit(X_classification)

    n_samples = X_classification.shape[0]
    assert np.all(
        mapper_training_data.n_bins_non_missing_
        == int((1 - validation_fraction) * n_samples)
    )
        != mapper_whole_data.n_bins_non_missing_
    )

def test_missing_values_trivial():
    # sanity check for missing values support. With only one feature and
    # y == isnan(X), the gbdt is supposed to reach perfect accuracy on the
    # training set.

    n_features = 1

    X = rng.normal(size=(n_samples, n_features))
    mask = rng.binomial(1, 0.5, size=X.shape).astype(bool)
    X[mask] = np.nan
    y = mask.ravel()
    gb = HistGradientBoostingClassifier()

    assert gb.score(X, y) == pytest.approx(1)

def test_missing_values_resilience(
    problem,
    missing_proportion,
    expected_min_score_classification,
    expected_min_score_regression,
):
    # Make sure the estimators can deal with missing values and still yield
    # decent predictions

    n_samples = 1000
    if problem == "regression":
        X, y = make_regression(
            n_samples=n_samples,
            n_features=n_features,
            n_informative=n_features,
            random_state=rng,
        )
        gb = HistGradientBoostingRegressor()
        expected_min_score = expected_min_score_regression
    else:
        X, y = make_classification(
            n_redundant=0,
            n_repeated=0,
        )
        gb = HistGradientBoostingClassifier()
        expected_min_score = expected_min_score_classification

    mask = rng.binomial(1, missing_proportion, size=X.shape).astype(bool)

    assert gb.score(X, y) > expected_min_score

def test_zero_division_hessians(data):
    # non regression test for issue #14018
    # make sure we avoid zero division errors when computing the leaves values.

    # If the learning rate is too high, the raw predictions are bad and will
    # saturate the softmax (or sigmoid in binary classif). This leads to
    # probabilities being exactly 0 or 1, gradients being constant, and
    # hessians being zero.
    X, y = data
    gb = HistGradientBoostingClassifier(learning_rate=100, max_iter=10)

def test_small_trainset():
    # Make sure that the small trainset is stratified and has the expected
    # length (10k samples)
    n_samples = 20000
    original_distrib = {0: 0.1, 1: 0.2, 2: 0.3, 3: 0.4}
    X = rng.randn(n_samples).reshape(n_samples, 1)
    y = [
        [class_] * int(prop * n_samples) for (class_, prop) in original_distrib.items()
    ]
    y = shuffle(np.concatenate(y))

    # Compute the small training set
    X_small, y_small, *_ = gb._get_small_trainset(
        X, y, seed=42, sample_weight_train=None
    )

    # Compute the class distribution in the small training set
    unique, counts = np.unique(y_small, return_counts=True)
    small_distrib = {class_: count / 10000 for (class_, count) in zip(unique, counts)}

    # Test that the small training set has the expected length
    assert X_small.shape[0] == 10000
    assert y_small.shape[0] == 10000

    # Test that the class distributions in the whole dataset and in the small
    # training set are identical
    assert small_distrib == pytest.approx(original_distrib)

def test_missing_values_minmax_imputation():
    # Compare the buit-in missing value handling of Histogram GBC with an
    # a-priori missing value imputation strategy that should yield the same
    # results in terms of decision function.
    #
    # Each feature (containing NaNs) is replaced by 2 features:
    # - one where the nans are replaced by min(feature) - 1
    # - one where the nans are replaced by max(feature) + 1
    # A split where nans go to the left has an equivalent split in the
    # first (min) feature, and a split where nans go to the right has an
    # equivalent split in the second (max) feature.
    #
    # Assuming the data is such that there is never a tie to select the best
    # feature to split on during training, the learned decision trees should be
    # strictly equivalent (learn a sequence of splits that encode the same
    # decision function).
    #
    # The MinMaxImputer transformer is meant to be a toy implementation of the
    # "Missing In Attributes" (MIA) missing value handling for decision trees
    # https://www.sciencedirect.com/science/article/abs/pii/S0167865508000305
    # The implementation of MIA as an imputation transformer was suggested by
    # "Remark 3" in :arxiv:'<1902.06931>`

    class MinMaxImputer(TransformerMixin, BaseEstimator):
        def fit(self, X, y=None):
            mm = MinMaxScaler().fit(X)
            self.data_min_ = mm.data_min_
            self.data_max_ = mm.data_max_

        def transform(self, X):
            X_min, X_max = X.copy(), X.copy()

            for feature_idx in range(X.shape[1]):
                nan_mask = np.isnan(X[:, feature_idx])
                X_min[nan_mask, feature_idx] = self.data_min_[feature_idx] - 1
                X_max[nan_mask, feature_idx] = self.data_max_[feature_idx] + 1

            return np.concatenate([X_min, X_max], axis=1)

    def make_missing_value_data(n_samples=int(1e4), seed=0):
        rng = np.random.RandomState(seed)
        X, y = make_regression(n_samples=n_samples, n_features=4, random_state=rng)

        # Pre-bin the data to ensure a deterministic handling by the 2
        # strategies and also make it easier to insert np.nan in a structured
        # way:
        X = KBinsDiscretizer(
            n_bins=42, encode="ordinal", quantile_method="averaged_inverted_cdf"
        ).fit_transform(X)

        # First feature has missing values completely at random:
        rnd_mask = rng.rand(X.shape[0]) > 0.9
        X[rnd_mask, 0] = np.nan

        # Second and third features have missing values for extreme values
        # (censoring missingness):
        low_mask = X[:, 1] == 0
        X[low_mask, 1] = np.nan

        high_mask = X[:, 2] == X[:, 2].max()
        X[high_mask, 2] = np.nan

        # Make the last feature nan pattern very informative:
        y_max = np.percentile(y, 70)
        y_max_mask = y >= y_max
        y[y_max_mask] = y_max
        X[y_max_mask, 3] = np.nan

        # Check that there is at least one missing value in each feature:
        for feature_idx in range(X.shape[1]):
            assert any(np.isnan(X[:, feature_idx]))

        # Let's use a test set to check that the learned decision function is
        # the same as evaluated on unseen data. Otherwise it could just be the
        # case that we find two independent ways to overfit the training set.
        return train_test_split(X, y, random_state=rng)

    # n_samples need to be large enough to minimize the likelihood of having
    # several candidate splits with the same gain value in a given tree.
    X_train, X_test, y_train, y_test = make_missing_value_data(
        n_samples=int(1e4), seed=0
    )

    # Use a small number of leaf nodes and iterations so as to keep
    # under-fitting models to minimize the likelihood of ties when training the
    # model.
    gbm1 = HistGradientBoostingRegressor(max_iter=100, max_leaf_nodes=5, random_state=0)
    gbm1.fit(X_train, y_train)

    gbm2 = make_pipeline(MinMaxImputer(), clone(gbm1))
    gbm2.fit(X_train, y_train)

    # Check that the model reach the same score:
    assert gbm1.score(X_train, y_train) == pytest.approx(gbm2.score(X_train, y_train))

    assert gbm1.score(X_test, y_test) == pytest.approx(gbm2.score(X_test, y_test))

    # Check the individual prediction match as a finer grained
    # decision function check.
    assert_allclose(gbm1.predict(X_train), gbm2.predict(X_train))
    assert_allclose(gbm1.predict(X_test), gbm2.predict(X_test))

def test_infinite_values():
    # Basic test for infinite values

    X = np.array([-np.inf, 0, 1, np.inf]).reshape(-1, 1)
    y = np.array([0, 0, 1, 1])

    gbdt = HistGradientBoostingRegressor(min_samples_leaf=1)
    np.testing.assert_allclose(gbdt.predict(X), y, atol=1e-4)

def test_consistent_lengths():
    sample_weight = np.array([0.1, 0.3, 0.1])
    gbdt = HistGradientBoostingRegressor()
    with pytest.raises(ValueError, match=r"sample_weight.shape == \(3,\), expected"):
        gbdt.fit(X, y, sample_weight)

    with pytest.raises(
        ValueError, match="Found input variables with inconsistent number"
    ):
        gbdt.fit(X, y[1:])

def test_infinite_values_missing_values():
    # High level test making sure that inf and nan values are properly handled
    # when both are present. This is similar to
    # test_split_on_nan_with_infinite_values() in test_grower.py, though we
    # cannot check the predictions for binned values here.

    X = np.asarray([-np.inf, 0, 1, np.inf, np.nan]).reshape(-1, 1)
    y_isnan = np.isnan(X.ravel())
    y_isinf = X.ravel() == np.inf

    stump_clf = HistGradientBoostingClassifier(
        min_samples_leaf=1, max_iter=1, learning_rate=1, max_depth=2
    )

    assert stump_clf.fit(X, y_isinf).score(X, y_isinf) == 1
    assert stump_clf.fit(X, y_isnan).score(X, y_isnan) == 1

def test_string_target_early_stopping(scoring):
    # Regression tests for #14709 where the targets need to be encoded before
    # to compute the score
    X = rng.randn(100, 10)
    y = np.array(["x"] * 50 + ["y"] * 50, dtype=object)
    gbrt = HistGradientBoostingClassifier(n_iter_no_change=10, scoring=scoring)
    gbrt.fit(X, y)

def test_zero_sample_weights_regression():
    # Make sure setting a SW to zero amounts to ignoring the corresponding
    # sample

    X = [[1, 0], [1, 0], [1, 0], [0, 1]]
    y = [0, 0, 1, 0]
    # ignore the first 2 training samples by setting their weight to 0
    sample_weight = [0, 0, 1, 1]
    gb = HistGradientBoostingRegressor(min_samples_leaf=1)
    gb.fit(X, y, sample_weight=sample_weight)
    assert gb.predict([[1, 0]])[0] > 0.5

def test_zero_sample_weights_classification():
    # sample

    gb = HistGradientBoostingClassifier(loss="log_loss", min_samples_leaf=1)
    assert_array_equal(gb.predict([[1, 0]]), [1])

    X = [[1, 0], [1, 0], [1, 0], [0, 1], [1, 1]]
    y = [0, 0, 1, 0, 2]
    sample_weight = [0, 0, 1, 1, 1]

def test_sample_weight_effect(problem, duplication):
    # High level test to make sure that duplicating a sample is equivalent to
    # giving it weight of 2.

    # fails for n_samples > 255 because binning does not take sample weights
    # into account. Keeping n_samples <= 255 makes
    # sure only unique values are used so SW have no effect on binning.
    n_samples = 255
            random_state=0,
        )
        Klass = HistGradientBoostingRegressor
    else:
        n_classes = 2 if problem == "binary_classification" else 3
            n_clusters_per_class=1,
            n_classes=n_classes,
        )
        Klass = HistGradientBoostingClassifier

    # This test can't pass if min_samples_leaf > 1 because that would force 2
    # samples to be in the same node in est_sw, while these samples would be
    # free to be separate in est_dup: est_dup would just group together the
    # duplicated samples.
    est = Klass(min_samples_leaf=1)

    # Create dataset with duplicate and corresponding sample weights
    if duplication == "half":
        lim = n_samples // 2
    else:
        lim = n_samples
    X_dup = np.r_[X, X[:lim]]
    y_dup = np.r_[y, y[:lim]]
    sample_weight = np.ones(shape=(n_samples))
    sample_weight[:lim] = 2

    est_sw = clone(est).fit(X, y, sample_weight=sample_weight)
    est_dup = clone(est).fit(X_dup, y_dup)

    # checking raw_predict is stricter than just predict for classification
    assert np.allclose(est_sw._raw_predict(X_dup), est_dup._raw_predict(X_dup))

def test_sum_hessians_are_sample_weight(Loss):
    # For losses with constant hessians, the sum_hessians field of the
    # histograms must be equal to the sum of the sample weight of samples at
    # the corresponding bin.

    X, y = make_regression(n_samples=n_samples, n_features=n_features, random_state=rng)
    bin_mapper = _BinMapper()
    X_binned = bin_mapper.fit_transform(X)

    # While sample weights are supposed to be positive, this still works.
    sample_weight = rng.normal(size=n_samples)

    loss = Loss(sample_weight=sample_weight)
    gradients, hessians = loss.init_gradient_and_hessian(
        n_samples=n_samples, dtype=G_H_DTYPE
    )
    gradients, hessians = gradients.reshape((-1, 1)), hessians.reshape((-1, 1))
    raw_predictions = rng.normal(size=(n_samples, 1))
    loss.gradient_hessian(
        y_true=y,
        raw_prediction=raw_predictions,
        sample_weight=sample_weight,
        gradient_out=gradients,
        hessian_out=hessians,
        n_threads=n_threads,
    )

    # build sum_sample_weight which contains the sum of the sample weights at
    # each bin (for each feature). This must be equal to the sum_hessians
    # field of the corresponding histogram
    sum_sw = np.zeros(shape=(n_features, bin_mapper.n_bins))
    for feature_idx in range(n_features):
        for sample_idx in range(n_samples):
            sum_sw[feature_idx, X_binned[sample_idx, feature_idx]] += sample_weight[
                sample_idx
            ]

    # Build histogram
        X_binned, gradients[:, 0], hessians[:, 0], n_bins=bin_mapper.n_bins
    )
    histograms = grower.histogram_builder.compute_histograms_brute(
        grower.root.sample_indices
    )

        for bin_idx in range(bin_mapper.n_bins):
            assert histograms[feature_idx, bin_idx]["sum_hessians"] == (
                pytest.approx(sum_sw[feature_idx, bin_idx], rel=1e-5)
            )

def test_max_depth_max_leaf_nodes():
    # Non regression test for
    # https://github.com/scikit-learn/scikit-learn/issues/16179
    # there was a bug when the max_depth and the max_leaf_nodes criteria were
    # met at the same time, which would lead to max_leaf_nodes not being
    # respected.
    X, y = make_classification(random_state=0)
    est = HistGradientBoostingClassifier(max_depth=2, max_leaf_nodes=3, max_iter=1).fit(
        X, y
    )
    tree = est._predictors[0][0]
    assert tree.get_max_depth() == 2
    assert tree.get_n_leaf_nodes() == 3  # would be 4 prior to bug fix

def test_early_stopping_on_test_set_with_warm_start():
    # Non regression test for #16661 where second fit fails with
    # warm_start=True, early_stopping is on, and no validation set
        max_iter=1,
        warm_start=True,
        early_stopping=True,
        n_iter_no_change=1,
        validation_fraction=None,
    )

    # does not raise on second call
    gb.set_params(max_iter=2)

def test_early_stopping_with_sample_weights(monkeypatch):
    """Check that sample weights is passed in to the scorer and _raw_predict is not
    called."""

    mock_scorer = Mock(side_effect=get_scorer("neg_median_absolute_error"))

    def mock_check_scoring(estimator, scoring):
        assert scoring == "neg_median_absolute_error"
        return mock_scorer

    monkeypatch.setattr(
        sklearn.ensemble._hist_gradient_boosting.gradient_boosting,
        "check_scoring",
        mock_check_scoring,
    )

    X, y = make_regression(random_state=0)
    sample_weight = np.ones_like(y)
    hist = HistGradientBoostingRegressor(
        max_iter=2,
        scoring="neg_median_absolute_error",
    )
    mock_raw_predict = Mock(side_effect=hist._raw_predict)
    hist._raw_predict = mock_raw_predict
    hist.fit(X, y, sample_weight=sample_weight)

    # _raw_predict should never be called with scoring as a string
    assert mock_raw_predict.call_count == 0

    # For scorer is called twice (train and val) for the baseline score, and twice
    # per iteration (train and val) after that. So 6 times in total for `max_iter=2`.
    assert mock_scorer.call_count == 6
    for arg_list in mock_scorer.call_args_list:
        assert "sample_weight" in arg_list[1]

def test_raw_predict_is_called_with_custom_scorer():
    """Custom scorer will still call _raw_predict."""

        scoring=mock_scorer,
    )
    hist.fit(X, y)

    # `_raw_predict` and scorer is called twice (train and val) for the baseline score,
    # and twice per iteration (train and val) after that. So 6 times in total for
    # `max_iter=2`.
    assert mock_raw_predict.call_count == 6

def test_single_node_trees(Est):
    # Make sure it's still possible to build single-node trees. In that case
    # the value of the root is set to 0. That's a correct value: if the tree is
    # single-node that's because min_gain_to_split is not respected right from
    # the root, so we don't want the tree to have any impact on the
    # predictions.

    y[:] = 1  # constant target will lead to a single root node

    est = Est(max_iter=20)
    est.fit(X, y)

    assert all(len(predictor[0].nodes) == 1 for predictor in est._predictors)
    assert all(predictor[0].nodes[0]["value"] == 0 for predictor in est._predictors)
    # Still gives correct predictions thanks to the baseline prediction
    assert_allclose(est.predict(X), y)

def test_custom_loss(Est, loss, X, y):
    est = Est(loss=loss, max_iter=20)

def test_staged_predict(HistGradientBoosting, X, y):
    # Test whether staged predictor eventually gives
    # the same prediction.
        X, y, test_size=0.5, random_state=0
    )
    gb = HistGradientBoosting(max_iter=10)

    # test raise NotFittedError if not fitted
        next(gb.staged_predict(X_test))

    gb.fit(X_train, y_train)

    # test if the staged predictions of each iteration
    # are equal to the corresponding predictions of the same estimator
    # trained from scratch.
    # this also test limit case when max_iter = 1
    method_names = (
        ["predict"]
        if is_regressor(gb)
        else ["predict", "predict_proba", "decision_function"]
    )
    for method_name in method_names:
        staged_method = getattr(gb, "staged_" + method_name)
        staged_predictions = list(staged_method(X_test))
        assert len(staged_predictions) == gb.n_iter_
        for n_iter, staged_predictions in enumerate(staged_method(X_test), 1):
            aux = HistGradientBoosting(max_iter=n_iter)
            aux.fit(X_train, y_train)
            pred_aux = getattr(aux, method_name)(X_test)

            assert_allclose(staged_predictions, pred_aux)
            assert staged_predictions.shape == pred_aux.shape

def test_unknown_categories_nan(
    insert_missing, Est, bool_categorical_parameter, missing_value
):
    # Make sure no error is raised at predict if a category wasn't seen during
    # fit. We also make sure they're treated as nans.

    f1 = rng.rand(n_samples)
    f2 = rng.randint(4, size=n_samples)
    X = np.c_[f1, f2]
    y = np.zeros(shape=n_samples)
    y[X[:, 1] % 2 == 0] = 1

    if bool_categorical_parameter:
        categorical_features = [False, True]
    else:
        categorical_features = [1]

    if insert_missing:
        mask = rng.binomial(1, 0.01, size=X.shape).astype(bool)
        assert mask.sum() > 0
        X[mask] = missing_value

    est = Est(max_iter=20, categorical_features=categorical_features).fit(X, y)
    assert_array_equal(est.is_categorical_, [False, True])

    # Make sure no error is raised on unknown categories and nans
    # unknown categories will be treated as nans
    X_test = np.zeros((10, X.shape[1]), dtype=float)
    X_test[:5, 1] = 30
    X_test[5:, 1] = missing_value
    assert len(np.unique(est.predict(X_test))) == 1

def test_categorical_encoding_strategies():
    # Check native categorical handling vs different encoding strategies. We
    # make sure that native encoding needs only 1 split to achieve a perfect
    # prediction on a simple dataset. In contrast, OneHotEncoded data needs
    # more depth / splits, and treating categories as ordered (just using
    # OrdinalEncoder) requires even more depth.

    # dataset with one random continuous feature, and one categorical feature
    # with values in [0, 5], e.g. from an OrdinalEncoder.
    # class == 1 iff categorical value in {0, 2, 4}
    f2 = rng.randint(6, size=n_samples)

    # make sure dataset is balanced so that the baseline_prediction doesn't
    # influence predictions too much with max_iter = 1
    assert 0.49 < y.mean() < 0.51

    native_cat_specs = [
        [False, True],
        [1],
    ]
    try:
        import pandas as pd

        X = pd.DataFrame(X, columns=["f_0", "f_1"])
        native_cat_specs.append(["f_1"])
        pass

    for native_cat_spec in native_cat_specs:
        clf_cat = HistGradientBoostingClassifier(
            max_iter=1, max_depth=1, categorical_features=native_cat_spec
        )
        clf_cat.fit(X, y)

        # Using native categorical encoding, we get perfect predictions with just
        # one split
        assert cross_val_score(clf_cat, X, y).mean() == 1

    # quick sanity check for the bitset: 0, 2, 4 = 2**0 + 2**2 + 2**4 = 21
    expected_left_bitset = [21, 0, 0, 0, 0, 0, 0, 0]
    left_bitset = clf_cat.fit(X, y)._predictors[0][0].raw_left_cat_bitsets[0]
    assert_array_equal(left_bitset, expected_left_bitset)

    # Treating categories as ordered, we need more depth / more splits to get
    # the same predictions
    clf_no_cat = HistGradientBoostingClassifier(
        max_iter=1, max_depth=4, categorical_features=None
    )
    assert cross_val_score(clf_no_cat, X, y).mean() < 0.9

    clf_no_cat.set_params(max_depth=5)
    assert cross_val_score(clf_no_cat, X, y).mean() == 1

    # Using OHEd data, we need less splits than with pure OEd data, but we
    # still need more splits than with the native categorical splits
    ct = make_column_transformer(
        (OneHotEncoder(sparse_output=False), [1]), remainder="passthrough"
    )
    X_ohe = ct.fit_transform(X)
    clf_no_cat.set_params(max_depth=2)
    assert cross_val_score(clf_no_cat, X_ohe, y).mean() < 0.9

    clf_no_cat.set_params(max_depth=3)
    assert cross_val_score(clf_no_cat, X_ohe, y).mean() == 1

def test_categorical_spec_errors(
    Est, categorical_features, monotonic_cst, expected_msg
):
    # Test errors when categories are specified incorrectly
    X, y = make_classification(random_state=0, n_features=4, n_samples=n_samples)
    X[:, 0] = rng.randint(0, 10, size=n_samples)
    X[:, 1] = rng.randint(0, 10, size=n_samples)
    est = Est(categorical_features=categorical_features, monotonic_cst=monotonic_cst)

    with pytest.raises(ValueError, match=expected_msg):
        est.fit(X, y)

def test_categorical_spec_errors_with_feature_names(Est):
    pd = pytest.importorskip("pandas")
    X = pd.DataFrame(
        {
            "f0": range(n_samples),
            "f1": range(n_samples),
            "f2": [1.0] * n_samples,
        }
    )
    y = [0, 1] * (n_samples // 2)

    est = Est(categorical_features=["f0", "f1", "f3"])
    expected_msg = re.escape(
        "categorical_features has a item value 'f3' which is not a valid "
        "feature name of the training data."
    )

    est = Est(categorical_features=["f0", "f1"])
        "categorical_features should be passed as an array of integers or "
        "as a boolean mask when the model is fitted on data without feature "
        "names."
    )
        est.fit(X.to_numpy(), y)

def test_categorical_spec_no_categories(Est, categorical_features, as_array):
    # Make sure we can properly detect that no categorical features are present
    # even if the categorical_features parameter is not None
    X = np.arange(10).reshape(5, 2)
    y = np.arange(5)
    if as_array:
        categorical_features = np.asarray(categorical_features)
    est = Est(categorical_features=categorical_features).fit(X, y)
    assert est.is_categorical_ is None

def test_categorical_bad_encoding_errors(Est, use_pandas, feature_name):
    # Test errors when categories are encoded incorrectly

    gb = Est(categorical_features=[True], max_bins=2)

    if use_pandas:
        pd = pytest.importorskip("pandas")
        X = pd.DataFrame({"f0": [0, 1, 2]})
    else:
        X = np.array([[0, 1, 2]]).T
    y = np.arange(3)
    msg = (
        f"Categorical feature {feature_name} is expected to have a "
        "cardinality <= 2 but actually has a cardinality of 3."
    )
        gb.fit(X, y)

    # nans are ignored in the counts
    X = np.array([[0, 1, np.nan]]).T

def test_uint8_predict(Est):
    # https://github.com/scikit-learn/scikit-learn/issues/18408
    # Make sure X can be of dtype uint8 (i.e. X_BINNED_DTYPE) in predict. It
    # will be converted to X_DTYPE.

    X = rng.randint(0, 100, size=(10, 2)).astype(np.uint8)
    y = rng.randint(0, 2, size=10).astype(np.uint8)
    est = Est()
    est.predict(X)

def test_check_interaction_cst(interaction_cst, n_features, result):
    """Check that _check_interaction_cst returns the expected list of sets"""
    est = HistGradientBoostingRegressor()
    est.set_params(interaction_cst=interaction_cst)
    assert est._check_interaction_cst(n_features) == result

def test_interaction_cst_numerically():
    """Check that interaction constraints have no forbidden interactions."""
    X = rng.uniform(size=(n_samples, 2))
    # Construct y with a strong interaction term
    # y = x0 + x1 + 5 * x0 * x1
    y = np.hstack((X, 5 * X[:, [0]] * X[:, [1]])).sum(axis=1)

    est = HistGradientBoostingRegressor(random_state=42)
    est_no_interactions = HistGradientBoostingRegressor(
        interaction_cst=[{0}, {1}], random_state=42
    )
    est_no_interactions.fit(X, y)

    delta = 0.25
    # Make sure we do not extrapolate out of the training set as tree-based estimators
    # are very bad in doing so.
    X_test = X[(X[:, 0] < 1 - delta) & (X[:, 1] < 1 - delta)]
    X_delta_d_0 = X_test + [delta, 0]
    X_delta_0_d = X_test + [0, delta]
    X_delta_d_d = X_test + [delta, delta]

    # Note: For the y from above as a function of x0 and x1, we have
    # y(x0+d, x1+d) = y(x0, x1) + 5 * d * (2/5 + x0 + x1) + 5 * d**2
    # y(x0+d, x1)   = y(x0, x1) + 5 * d * (1/5 + x1)
    # y(x0,   x1+d) = y(x0, x1) + 5 * d * (1/5 + x0)
    # Without interaction constraints, we would expect a result of 5 * d**2 for the
    # following expression, but zero with constraints in place.
        est_no_interactions.predict(X_delta_d_d)
        + est_no_interactions.predict(X_test)
        - est_no_interactions.predict(X_delta_d_0)
        - est_no_interactions.predict(X_delta_0_d),
        0,
        atol=1e-12,
    )

    # Correct result of the expressions is 5 * delta**2. But this is hard to achieve by
    # a fitted tree-based model. However, with 100 iterations the expression should
    # at least be positive!
        est.predict(X_delta_d_d)
        + est.predict(X_test)
        - est.predict(X_delta_d_0)
        - est.predict(X_delta_0_d)
        > 0.01
    )

def test_no_user_warning_with_scoring():
    """Check that no UserWarning is raised when scoring is set.

    Non-regression test for #22907.
    """
    X_df = pd.DataFrame(X, columns=[f"col{i}" for i in range(X.shape[1])])

    est = HistGradientBoostingRegressor(
        random_state=0, scoring="neg_mean_absolute_error", early_stopping=True
    )
    with warnings.catch_warnings():
        warnings.simplefilter("error", UserWarning)
        est.fit(X_df, y)

def test_class_weights():
    """High level test to check class_weights."""

        n_informative=n_features,
        n_classes=2,
    )
    y_is_1 = y == 1

    # class_weight is the same as sample weights with the corresponding class
    clf = HistGradientBoostingClassifier(
        min_samples_leaf=2, random_state=0, max_depth=2
    )
    sample_weight[y_is_1] = 3.0
    clf.fit(X, y, sample_weight=sample_weight)

    class_weight = {0: 1.0, 1: 3.0}
    clf_class_weighted = clone(clf).set_params(class_weight=class_weight)
    clf_class_weighted.fit(X, y)

    assert_allclose(clf.decision_function(X), clf_class_weighted.decision_function(X))

    # Check that sample_weight and class_weight are multiplicative
    clf.fit(X, y, sample_weight=sample_weight**2)
    clf_class_weighted.fit(X, y, sample_weight=sample_weight)

    # Make imbalanced dataset
    X_imb = np.concatenate((X[~y_is_1], X[y_is_1][:10]))
    y_imb = np.concatenate((y[~y_is_1], y[y_is_1][:10]))

    # class_weight="balanced" is the same as sample_weights to be
    # inversely proportional to n_samples / (n_classes * np.bincount(y))
    clf_balanced = clone(clf).set_params(class_weight="balanced")
    clf_balanced.fit(X_imb, y_imb)

    class_weight = y_imb.shape[0] / (2 * np.bincount(y_imb))
    sample_weight = class_weight[y_imb]
    clf_sample_weight = clone(clf).set_params(class_weight=None)
    clf_sample_weight.fit(X_imb, y_imb, sample_weight=sample_weight)

        clf_balanced.decision_function(X_imb),
        clf_sample_weight.decision_function(X_imb),
    )

def test_unknown_category_that_are_negative():
    """Check that unknown categories that are negative does not error.

    Non-regression test for #24274.
    """
    X = np.c_[rng.rand(n_samples), rng.randint(4, size=n_samples)]

        categorical_features=[False, True],
        max_iter=10,

    # Check that negative values from the second column are treated like a
    # missing category
    X_test_neg = np.asarray([[1, -2], [3, -4]])
    X_test_nan = np.asarray([[1, np.nan], [3, np.nan]])

    assert_allclose(hist.predict(X_test_neg), hist.predict(X_test_nan))

def test_dataframe_categorical_results_same_as_ndarray(
    dataframe_lib, HistGradientBoosting
):
    """Check that pandas categorical give the same results as ndarray."""
    pytest.importorskip(dataframe_lib)

    n_samples = 5_000
    n_cardinality = 50
    max_bins = 100
    f_num = rng.rand(n_samples)
    f_cat = rng.randint(n_cardinality, size=n_samples)

    # Make f_cat an informative feature
    y = (f_cat % 3 == 0) & (f_num > 0.2)

    X = np.c_[f_num, f_cat]
    f_cat = [f"cat{c:0>3}" for c in f_cat]
    X_df = _convert_container(
        np.asarray([f_num, f_cat]).T,
        dataframe_lib,
        ["f_num", "f_cat"],
        categorical_feature_names=["f_cat"],
    )

    X_train, X_test, X_train_df, X_test_df, y_train, y_test = train_test_split(
        X, X_df, y, random_state=0
    )

    hist_kwargs = dict(max_iter=10, max_bins=max_bins, random_state=0)
    hist_np = HistGradientBoosting(categorical_features=[False, True], **hist_kwargs)
    hist_np.fit(X_train, y_train)

    hist_pd = HistGradientBoosting(categorical_features="from_dtype", **hist_kwargs)
    hist_pd.fit(X_train_df, y_train)

    # Check categories are correct and sorted
    categories = hist_pd._preprocessor.named_transformers_["encoder"].categories_[0]
    assert_array_equal(categories, np.unique(f_cat))

    assert len(hist_np._predictors) == len(hist_pd._predictors)
    for predictor_1, predictor_2 in zip(hist_np._predictors, hist_pd._predictors):
        assert len(predictor_1[0].nodes) == len(predictor_2[0].nodes)

    score_np = hist_np.score(X_test, y_test)
    score_pd = hist_pd.score(X_test_df, y_test)
    assert score_np == pytest.approx(score_pd)
    assert_allclose(hist_np.predict(X_test), hist_pd.predict(X_test_df))

def test_dataframe_categorical_errors(dataframe_lib, HistGradientBoosting):
    """Check error cases for pandas categorical feature."""
    msg = "Categorical feature 'f_cat' is expected to have a cardinality <= 16"
    hist = HistGradientBoosting(categorical_features="from_dtype", max_bins=16)

    f_cat = rng.randint(0, high=100, size=100).astype(str)
        f_cat[:, None], dataframe_lib, ["f_cat"], categorical_feature_names=["f_cat"]
    )
    y = rng.randint(0, high=2, size=100)

        hist.fit(X_df, y)

def test_categorical_different_order_same_model(dataframe_lib):
    """Check that the order of the categorical gives same model."""
    n_samples = 1_000
    f_ints = rng.randint(low=0, high=2, size=n_samples)

    # Construct a target with some noise
    y = f_ints.copy()
    flipped = rng.choice([True, False], size=n_samples, p=[0.1, 0.9])
    y[flipped] = 1 - y[flipped]

    # Construct categorical where 0 -> A and 1 -> B and 1 -> A and 0 -> B
    f_cat_a_b = np.asarray(["A", "B"])[f_ints]
    f_cat_b_a = np.asarray(["B", "A"])[f_ints]
    df_a_b = _convert_container(
        f_cat_a_b[:, None],
        ["f_cat"],
    )
    df_b_a = _convert_container(
        f_cat_b_a[:, None],
    )

    hist_a_b = HistGradientBoostingClassifier(
        categorical_features="from_dtype", random_state=0
    )
    hist_b_a = HistGradientBoostingClassifier(
    )

    hist_a_b.fit(df_a_b, y)
    hist_b_a.fit(df_b_a, y)

    assert len(hist_a_b._predictors) == len(hist_b_a._predictors)
    for predictor_1, predictor_2 in zip(hist_a_b._predictors, hist_b_a._predictors):

def get_different_bitness_node_ndarray(node_ndarray):
    new_dtype_for_indexing_fields = np.int64 if _IS_32BIT else np.int32

    # field names in Node struct with np.intp types (see
    # sklearn/ensemble/_hist_gradient_boosting/common.pyx)
    indexing_field_names = ["feature_idx"]

    new_dtype_dict = {
        name: dtype for name, (dtype, _) in node_ndarray.dtype.fields.items()
    }
    for name in indexing_field_names:
        new_dtype_dict[name] = new_dtype_for_indexing_fields

    new_dtype = np.dtype(
        {"names": list(new_dtype_dict.keys()), "formats": list(new_dtype_dict.values())}
    )
    return node_ndarray.astype(new_dtype, casting="same_kind")

def reduce_predictor_with_different_bitness(predictor):
    cls, args, state = predictor.__reduce__()

    new_state = state.copy()
    new_state["nodes"] = get_different_bitness_node_ndarray(new_state["nodes"])

    return (cls, args, new_state)

def test_different_bitness_pickle():

    clf = HistGradientBoostingClassifier(random_state=0, max_depth=3)
    clf.fit(X, y)
    score = clf.score(X, y)

    def pickle_dump_with_different_bitness():
        f = io.BytesIO()
        p = pickle.Pickler(f)
        p.dispatch_table = copyreg.dispatch_table.copy()
        p.dispatch_table[TreePredictor] = reduce_predictor_with_different_bitness

        p.dump(clf)
        f.seek(0)
        return f

    # Simulate loading a pickle of the same model trained on a platform with different
    # bitness that than the platform it will be used to make predictions on:
    new_clf = pickle.load(pickle_dump_with_different_bitness())
    new_score = new_clf.score(X, y)
    assert score == pytest.approx(new_score)

def test_different_bitness_joblib_pickle():
    # Make sure that a platform specific pickle generated on a 64 bit
    # platform can be converted at pickle load time into an estimator
    # with Cython code that works with the host's native integer precision
    # to index nodes in the tree data structure when the host is a 32 bit
    # platform (and vice versa).
    #
    # This is in particular useful to be able to train a model on a 64 bit Linux
    # server and deploy the model as part of a (32 bit) WASM in-browser
    # application using pyodide.

    def joblib_dump_with_different_bitness():
        p = NumpyPickler(f)

        f.seek(0)
        return f

    new_clf = joblib.load(joblib_dump_with_different_bitness())

def test_pandas_nullable_dtype():
    # Non regression test for https://github.com/scikit-learn/scikit-learn/issues/28317

    rng = np.random.default_rng(0)
    X = pd.DataFrame({"a": rng.integers(10, size=100)}).astype(pd.Int64Dtype())
    y = rng.integers(2, size=100)

    clf = HistGradientBoostingClassifier()

        # way:

        f.seek(0)
        return f

        f.seek(0)
        return f

def test_find_binning_thresholds_regular_data():
    data = np.linspace(0, 10, 1001)
    bin_thresholds = _find_binning_thresholds(data, max_bins=10)
    assert_allclose(bin_thresholds, [1, 2, 3, 4, 5, 6, 7, 8, 9])

    bin_thresholds = _find_binning_thresholds(data, max_bins=5)
    assert_allclose(bin_thresholds, [2, 4, 6, 8])

def test_find_binning_thresholds_small_regular_data():
    data = np.linspace(0, 10, 11)

    bin_thresholds = _find_binning_thresholds(data, max_bins=11)
    assert_allclose(bin_thresholds, np.arange(10) + 0.5)

    bin_thresholds = _find_binning_thresholds(data, max_bins=255)

def test_find_binning_thresholds_random_data():
    bin_thresholds = [
        _find_binning_thresholds(DATA[:, i], max_bins=255) for i in range(2)
    ]
    for i in range(len(bin_thresholds)):
        assert bin_thresholds[i].shape == (254,)  # 255 - 1
        assert bin_thresholds[i].dtype == DATA.dtype

        bin_thresholds[0][[64, 128, 192]], np.array([-0.7, 0.0, 0.7]), atol=1e-1
    )

        bin_thresholds[1][[64, 128, 192]], np.array([9.99, 10.00, 10.01]), atol=1e-2
    )

def test_find_binning_thresholds_low_n_bins():
        _find_binning_thresholds(DATA[:, i], max_bins=128) for i in range(2)
    ]
        assert bin_thresholds[i].shape == (127,)  # 128 - 1

def test_invalid_n_bins(n_bins):
    err_msg = "n_bins={} should be no smaller than 3 and no larger than 256".format(
        n_bins
    )
        _BinMapper(n_bins=n_bins).fit(DATA)

def test_bin_mapper_n_features_transform():
    mapper = _BinMapper(n_bins=42, random_state=42).fit(DATA)
    err_msg = "This estimator was fitted with 2 features but 4 got passed"
        mapper.transform(np.repeat(DATA, 2, axis=1))

def test_map_to_bins(max_bins):
        _find_binning_thresholds(DATA[:, i], max_bins=max_bins) for i in range(2)
    ]
    binned = np.zeros_like(DATA, dtype=X_BINNED_DTYPE, order="F")
    is_categorical = np.zeros(2, dtype=np.uint8)
    last_bin_idx = max_bins
    _map_to_bins(DATA, bin_thresholds, is_categorical, last_bin_idx, n_threads, binned)
    assert binned.shape == DATA.shape
    assert binned.dtype == np.uint8
    assert binned.flags.f_contiguous

    min_indices = DATA.argmin(axis=0)
    max_indices = DATA.argmax(axis=0)

    for feature_idx, min_idx in enumerate(min_indices):
        assert binned[min_idx, feature_idx] == 0
    for feature_idx, max_idx in enumerate(max_indices):
        assert binned[max_idx, feature_idx] == max_bins - 1

def test_bin_mapper_random_data(max_bins):
    n_samples, n_features = DATA.shape

    expected_count_per_bin = n_samples // max_bins
    tol = int(0.05 * expected_count_per_bin)

    # max_bins is the number of bins for non-missing values
    n_bins = max_bins + 1
    mapper = _BinMapper(n_bins=n_bins, random_state=42).fit(DATA)
    binned = mapper.transform(DATA)

    assert binned.shape == (n_samples, n_features)
    assert_array_equal(binned.min(axis=0), np.array([0, 0]))
    assert_array_equal(binned.max(axis=0), np.array([max_bins - 1, max_bins - 1]))
    assert len(mapper.bin_thresholds_) == n_features
    for bin_thresholds_feature in mapper.bin_thresholds_:
        assert bin_thresholds_feature.shape == (max_bins - 1,)
        assert bin_thresholds_feature.dtype == DATA.dtype
    assert np.all(mapper.n_bins_non_missing_ == max_bins)

    # Check that the binned data is approximately balanced across bins.
        for bin_idx in range(max_bins):
            count = (binned[:, feature_idx] == bin_idx).sum()
            assert abs(count - expected_count_per_bin) < tol

def test_bin_mapper_small_random_data(n_samples, max_bins):
    data = np.random.RandomState(42).normal(size=n_samples).reshape(-1, 1)
    assert len(np.unique(data)) == n_samples

    binned = mapper.fit_transform(data)

    assert binned.shape == data.shape
    assert_array_equal(binned.ravel()[np.argsort(data.ravel())], np.arange(n_samples))

def test_bin_mapper_identity_repeated_values(max_bins, n_distinct, multiplier):
    data = np.array(list(range(n_distinct)) * multiplier).reshape(-1, 1)
    binned = _BinMapper(n_bins=n_bins).fit_transform(data)
    assert_array_equal(data, binned)

def test_bin_mapper_repeated_values_invariance(n_distinct):
    distinct_values = rng.normal(size=n_distinct)
    assert len(np.unique(distinct_values)) == n_distinct

    repeated_indices = rng.randint(low=0, high=n_distinct, size=1000)
    data = distinct_values[repeated_indices]
    rng.shuffle(data)
    assert_array_equal(np.unique(data), np.sort(distinct_values))

    data = data.reshape(-1, 1)

    mapper_1 = _BinMapper(n_bins=n_distinct + 1)
    binned_1 = mapper_1.fit_transform(data)
    assert_array_equal(np.unique(binned_1[:, 0]), np.arange(n_distinct))

    # Adding more bins to the mapper yields the same results (same thresholds)
    mapper_2 = _BinMapper(n_bins=min(256, n_distinct * 3) + 1)
    binned_2 = mapper_2.fit_transform(data)

    assert_allclose(mapper_1.bin_thresholds_[0], mapper_2.bin_thresholds_[0])
    assert_array_equal(binned_1, binned_2)

def test_bin_mapper_identity_small(max_bins, scale, offset):
    data = np.arange(max_bins).reshape(-1, 1) * scale + offset
    assert_array_equal(binned, np.arange(max_bins).reshape(-1, 1))

def test_bin_mapper_idempotence(max_bins_small, max_bins_large):
    assert max_bins_large >= max_bins_small
    data = np.random.RandomState(42).normal(size=30000).reshape(-1, 1)
    mapper_small = _BinMapper(n_bins=max_bins_small + 1)
    mapper_large = _BinMapper(n_bins=max_bins_small + 1)
    binned_small = mapper_small.fit_transform(data)
    binned_large = mapper_large.fit_transform(binned_small)
    assert_array_equal(binned_small, binned_large)

def test_n_bins_non_missing(n_bins, diff):
    # Check that n_bins_non_missing is n_unique_values when
    # there are not a lot of unique values, else n_bins - 1.

    n_unique_values = n_bins + diff
    X = list(range(n_unique_values)) * 2
    X = np.array(X).reshape(-1, 1)
    mapper = _BinMapper(n_bins=n_bins).fit(X)
    assert np.all(mapper.n_bins_non_missing_ == min(n_bins - 1, n_unique_values))

def test_subsample():
    # Make sure bin thresholds are different when applying subsampling
    mapper_no_subsample = _BinMapper(subsample=None, random_state=0).fit(DATA)
    mapper_subsample = _BinMapper(subsample=256, random_state=0).fit(DATA)

    for feature in range(DATA.shape[1]):
        assert not np.allclose(
            mapper_no_subsample.bin_thresholds_[feature],
            mapper_subsample.bin_thresholds_[feature],
            rtol=1e-4,
        )

def test_missing_values_support(n_bins, n_bins_non_missing, X_trans_expected):
    # check for missing values: make sure nans are mapped to the last bin
    # and that the _BinMapper attributes are correct

    X = [
        [1, 1, 0],
        [np.nan, np.nan, 0],
        [2, 1, 0],
        [np.nan, 2, 1],
        [3, 2, 1],
        [4, 1, 0],
    ]

    X = np.array(X)

    mapper = _BinMapper(n_bins=n_bins)
    mapper.fit(X)

    assert_array_equal(mapper.n_bins_non_missing_, n_bins_non_missing)

    for feature_idx in range(X.shape[1]):
        assert (
            len(mapper.bin_thresholds_[feature_idx])
            == n_bins_non_missing[feature_idx] - 1
        )

    assert mapper.missing_values_bin_idx_ == n_bins - 1

    X_trans = mapper.transform(X)
    assert_array_equal(X_trans, X_trans_expected)

    # Make sure infinite values are properly handled.

    bin_mapper.fit(X)
    assert_allclose(bin_mapper.bin_thresholds_[0], [-np.inf, 0.5, ALMOST_INF])
    assert bin_mapper.n_bins_non_missing_ == [4]

    expected_binned_X = np.array([0, 1, 2, 3]).reshape(-1, 1)
    assert_array_equal(bin_mapper.transform(X), expected_binned_X)

def test_categorical_feature(n_bins):
    # Basic test for categorical features
    # we make sure that categories are mapped into [0, n_categories - 1] and
    # that nans are mapped to the last bin
    X = np.array(
        [[4] * 500 + [1] * 3 + [10] * 4 + [0] * 4 + [13] + [7] * 5 + [np.nan] * 2],
        dtype=X_DTYPE,
    ).T
    known_categories = [np.unique(X[~np.isnan(X)])]

    bin_mapper = _BinMapper(
        is_categorical=np.array([True]),
        known_categories=known_categories,
    ).fit(X)
    assert bin_mapper.n_bins_non_missing_ == [6]
    assert_array_equal(bin_mapper.bin_thresholds_[0], [0, 1, 4, 7, 10, 13])

    X = np.array([[0, 1, 4, np.nan, 7, 10, 13]], dtype=X_DTYPE).T
    expected_trans = np.array([[0, 1, 2, n_bins - 1, 3, 4, 5]]).T
    assert_array_equal(bin_mapper.transform(X), expected_trans)

    # Negative categories are mapped to the missing values' bin
    # (i.e. the bin of index `missing_values_bin_idx_ == n_bins - 1).
    # Unknown positive categories does not happen in practice and tested
    # for illustration purpose.
    X = np.array([[-4, -1, 100]], dtype=X_DTYPE).T
    expected_trans = np.array([[n_bins - 1, n_bins - 1, 6]]).T

def test_categorical_feature_negative_missing():
    """Make sure bin mapper treats negative categories as missing values."""
        [[4] * 500 + [1] * 3 + [5] * 10 + [-1] * 3 + [np.nan] * 4], dtype=X_DTYPE
    ).T
        n_bins=4,
        known_categories=[np.array([1, 4, 5], dtype=X_DTYPE)],
    ).fit(X)

    assert bin_mapper.n_bins_non_missing_ == [3]

    X = np.array([[-1, 1, 3, 5, np.nan]], dtype=X_DTYPE).T

    # Negative values for categorical features are considered as missing values.
    # They are mapped to the bin of index `bin_mapper.missing_values_bin_idx_`,
    # which is 3 here.
    assert bin_mapper.missing_values_bin_idx_ == 3
    expected_trans = np.array([[3, 0, 1, 2, 3]]).T

def test_categorical_with_numerical_features(n_bins):
    # basic check for binmapper with mixed data
    X1 = np.arange(10, 20).reshape(-1, 1)  # numerical
    X2 = np.arange(10, 15).reshape(-1, 1)  # categorical
    X2 = np.r_[X2, X2]
    X = np.c_[X1, X2]
    known_categories = [None, np.unique(X2).astype(X_DTYPE)]

        is_categorical=np.array([False, True]),
    ).fit(X)

    assert_array_equal(bin_mapper.n_bins_non_missing_, [10, 5])

    bin_thresholds = bin_mapper.bin_thresholds_
    assert len(bin_thresholds) == 2
    assert_array_equal(bin_thresholds[1], np.arange(10, 15))

    expected_X_trans = [
        [0, 0],
        [1, 1],
        [2, 2],
        [3, 3],
        [4, 4],
        [5, 0],
        [6, 1],
        [7, 2],
        [8, 3],
        [9, 4],
    ]
    assert_array_equal(bin_mapper.transform(X), expected_X_trans)

def test_make_known_categories_bitsets():
    # Check the output of make_known_categories_bitsets
        [[14, 2, 30], [30, 4, 70], [40, 10, 180], [40, 240, 180]], dtype=X_DTYPE
    )

        is_categorical=np.array([False, True, True]),
        known_categories=[None, X[:, 1], X[:, 2]],
    )

    known_cat_bitsets, f_idx_map = bin_mapper.make_known_categories_bitsets()

    # Note that for non-categorical features, values are left to 0
    expected_f_idx_map = np.array([0, 0, 1], dtype=np.uint8)
    assert_allclose(expected_f_idx_map, f_idx_map)

    expected_cat_bitset = np.zeros((2, 8), dtype=np.uint32)

    # first categorical feature: [2, 4, 10, 240]
    f_idx = 1
    mapped_f_idx = f_idx_map[f_idx]
    expected_cat_bitset[mapped_f_idx, 0] = 2**2 + 2**4 + 2**10
    # 240 = 32**7 + 16, therefore the 16th bit of the 7th array is 1.
    expected_cat_bitset[mapped_f_idx, 7] = 2**16

    # second categorical feature [30, 70, 180]
    f_idx = 2
    expected_cat_bitset[mapped_f_idx, 0] = 2**30
    expected_cat_bitset[mapped_f_idx, 2] = 2**6
    expected_cat_bitset[mapped_f_idx, 5] = 2**20

    assert_allclose(expected_cat_bitset, known_cat_bitsets)

def test_categorical_parameters(is_categorical, known_categories, match):
    # test the validation of the is_categorical and known_categories parameters

    X = np.array([[1, 2, 3]], dtype=X_DTYPE)

        is_categorical=is_categorical, known_categories=known_categories
    )
    with pytest.raises(ValueError, match=match):
        bin_mapper.fit(X)

def _assert_predictor_equal(gb_1, gb_2, X):
    """Assert that two HistGBM instances are identical."""
    # Check identical nodes for each tree
    for pred_ith_1, pred_ith_2 in zip(gb_1._predictors, gb_2._predictors):
        for predictor_1, predictor_2 in zip(pred_ith_1, pred_ith_2):
            assert_array_equal(predictor_1.nodes, predictor_2.nodes)

    # Check identical predictions
    assert_allclose(gb_1.predict(X), gb_2.predict(X))

def test_max_iter_with_warm_start_validation(GradientBoosting, X, y):
    # Check that a ValueError is raised when the maximum number of iterations
    # is smaller than the number of iterations from the previous fit when warm
    # start is True.

    estimator = GradientBoosting(max_iter=10, early_stopping=False, warm_start=True)
    estimator.fit(X, y)
    estimator.set_params(max_iter=5)
    err_msg = (
        "max_iter=5 must be larger than or equal to n_iter_=10 when warm_start==True"
    )
        estimator.fit(X, y)

def test_warm_start_yields_identical_results(GradientBoosting, X, y):
    # Make sure that fitting 50 iterations and then 25 with warm start is
    # equivalent to fitting 75 iterations.

    rng = 42
    gb_warm_start = GradientBoosting(
        n_iter_no_change=100, max_iter=50, random_state=rng, warm_start=True
    )
    gb_warm_start.fit(X, y).set_params(max_iter=75).fit(X, y)

    gb_no_warm_start = GradientBoosting(
        n_iter_no_change=100, max_iter=75, random_state=rng, warm_start=False
    )
    gb_no_warm_start.fit(X, y)

    # Check that both predictors are equal
    _assert_predictor_equal(gb_warm_start, gb_no_warm_start, X)

def test_warm_start_max_depth(GradientBoosting, X, y):
    # Test if possible to fit trees of different depth in ensemble.
    gb = GradientBoosting(
        max_iter=20,
        max_depth=2,
    )
    gb.set_params(max_iter=30, max_depth=3, n_iter_no_change=110)

    # First 20 trees have max_depth == 2
        assert gb._predictors[i][0].get_max_depth() == 2
    # Last 10 trees have max_depth == 3
    for i in range(1, 11):
        assert gb._predictors[-i][0].get_max_depth() == 3

def test_warm_start_early_stopping(GradientBoosting, X, y, scoring):
    # Make sure that early stopping occurs after a small number of iterations
    # when fitting a second time with warm starting.

    n_iter_no_change = 5
        max_iter=10000,
        random_state=42,
        tol=1e-3,
    )
    n_iter_first_fit = gb.n_iter_
    n_iter_second_fit = gb.n_iter_
    assert 0 < n_iter_second_fit - n_iter_first_fit < n_iter_no_change

def test_warm_start_equal_n_estimators(GradientBoosting, X, y):
    # Test if warm start with equal n_estimators does nothing
    gb_1 = GradientBoosting(max_depth=2, early_stopping=False)
    gb_1.fit(X, y)

    gb_2 = clone(gb_1)
    gb_2.set_params(max_iter=gb_1.max_iter, warm_start=True, n_iter_no_change=5)
    gb_2.fit(X, y)

    _assert_predictor_equal(gb_1, gb_2, X)

def test_warm_start_clear(GradientBoosting, X, y):
    # Test if fit clears state.
    gb_1 = GradientBoosting(n_iter_no_change=5, random_state=42)

    gb_2 = GradientBoosting(n_iter_no_change=5, random_state=42, warm_start=True)
    gb_2.fit(X, y)  # inits state
    gb_2.set_params(warm_start=False)
    gb_2.fit(X, y)  # clears old state and equals est

    # Check that both predictors have the same train_score_ and
    # validation_score_ attributes
    assert_allclose(gb_1.train_score_, gb_2.train_score_)
    assert_allclose(gb_1.validation_score_, gb_2.validation_score_)

def test_random_seeds_warm_start(GradientBoosting, X, y, rng_type):
    # Make sure the seeds for train/val split and small trainset subsampling
    # are correctly set in a warm start context.
    def _get_rng(rng_type):
        # Helper to avoid consuming rngs
        if rng_type == "none":
        elif rng_type == "int":
            return 42
        else:
            return np.random.RandomState(0)

    random_state = _get_rng(rng_type)
    gb_1 = GradientBoosting(early_stopping=True, max_iter=2, random_state=random_state)
    gb_1.set_params(scoring=check_scoring(gb_1))
    random_seed_1_1 = gb_1._random_seed

    random_seed_1_2 = gb_1._random_seed  # clear the old state, different seed

    gb_2 = GradientBoosting(
        early_stopping=True, max_iter=2, random_state=random_state, warm_start=True
    )
    gb_2.set_params(scoring=check_scoring(gb_2))
    random_seed_2_1 = gb_2._random_seed
    random_seed_2_2 = gb_2._random_seed

    # Without warm starting, the seeds should be
    # * all different if random state is None
    # * all equal if random state is an integer
    # * different when refitting and equal with a new estimator (because
    #   the random state is mutated)
    if rng_type == "none":
        assert random_seed_1_1 != random_seed_1_2 != random_seed_2_1
    elif rng_type == "int":
        assert random_seed_1_1 == random_seed_1_2 == random_seed_2_1
    else:
        assert random_seed_1_1 == random_seed_2_1 != random_seed_1_2

    # With warm starting, the seeds must be equal
    assert random_seed_2_1 == random_seed_2_2

            return 42
        else:

def test_build_histogram(build_func):
    binned_feature = np.array([0, 2, 0, 1, 2, 0, 2, 1], dtype=X_BINNED_DTYPE)

    # Small sample_indices (below unrolling threshold)
    ordered_gradients = np.array([0, 1, 3], dtype=G_H_DTYPE)
    ordered_hessians = np.array([1, 1, 2], dtype=G_H_DTYPE)

    sample_indices = np.array([0, 2, 3], dtype=np.uint32)
    hist = np.zeros((1, 3), dtype=HISTOGRAM_DTYPE)
    build_func(
        0, sample_indices, binned_feature, ordered_gradients, ordered_hessians, hist
    )
    hist = hist[0]
    assert_array_equal(hist["count"], [2, 1, 0])
    assert_allclose(hist["sum_gradients"], [1, 3, 0])
    assert_allclose(hist["sum_hessians"], [2, 2, 0])

    # Larger sample_indices (above unrolling threshold)
    sample_indices = np.array([0, 2, 3, 6, 7], dtype=np.uint32)
    ordered_gradients = np.array([0, 1, 3, 0, 1], dtype=G_H_DTYPE)
    ordered_hessians = np.array([1, 1, 2, 1, 0], dtype=G_H_DTYPE)

    )
    assert_array_equal(hist["count"], [2, 2, 1])
    assert_allclose(hist["sum_gradients"], [1, 4, 0])
    assert_allclose(hist["sum_hessians"], [2, 2, 1])

def test_histogram_sample_order_independence():
    # Make sure the order of the samples has no impact on the histogram
    # computations
    n_sub_samples = 100
    n_bins = 256

    binned_feature = rng.randint(0, n_bins - 1, size=n_samples, dtype=X_BINNED_DTYPE)
    sample_indices = rng.choice(
        np.arange(n_samples, dtype=np.uint32), n_sub_samples, replace=False
    )
    ordered_gradients = rng.randn(n_sub_samples).astype(G_H_DTYPE)
    hist_gc = np.zeros((1, n_bins), dtype=HISTOGRAM_DTYPE)
    _build_histogram_no_hessian(
        0, sample_indices, binned_feature, ordered_gradients, hist_gc
    )

    ordered_hessians = rng.exponential(size=n_sub_samples).astype(G_H_DTYPE)
    hist_ghc = np.zeros((1, n_bins), dtype=HISTOGRAM_DTYPE)
    _build_histogram(
        0, sample_indices, binned_feature, ordered_gradients, ordered_hessians, hist_ghc
    )

    permutation = rng.permutation(n_sub_samples)
    hist_gc_perm = np.zeros((1, n_bins), dtype=HISTOGRAM_DTYPE)
        0,
        sample_indices[permutation],
        binned_feature,
        ordered_gradients[permutation],
        hist_gc_perm,
    )

    hist_ghc_perm = np.zeros((1, n_bins), dtype=HISTOGRAM_DTYPE)
        0,
        ordered_hessians[permutation],
        hist_ghc_perm,
    )

    hist_gc = hist_gc[0]
    hist_ghc = hist_ghc[0]
    hist_gc_perm = hist_gc_perm[0]
    hist_ghc_perm = hist_ghc_perm[0]

    assert_allclose(hist_gc["sum_gradients"], hist_gc_perm["sum_gradients"])
    assert_array_equal(hist_gc["count"], hist_gc_perm["count"])

    assert_allclose(hist_ghc["sum_gradients"], hist_ghc_perm["sum_gradients"])
    assert_allclose(hist_ghc["sum_hessians"], hist_ghc_perm["sum_hessians"])
    assert_array_equal(hist_ghc["count"], hist_ghc_perm["count"])

def test_unrolled_equivalent_to_naive(constant_hessian):
    # Make sure the different unrolled histogram computations give the same
    # results as the naive one.
    n_bins = 5
    sample_indices = np.arange(n_samples).astype(np.uint32)
    binned_feature = rng.randint(0, n_bins - 1, size=n_samples, dtype=np.uint8)
    ordered_gradients = rng.randn(n_samples).astype(G_H_DTYPE)
        ordered_hessians = np.ones(n_samples, dtype=G_H_DTYPE)
    else:
        ordered_hessians = rng.lognormal(size=n_samples).astype(G_H_DTYPE)

    hist_gc_root = np.zeros((1, n_bins), dtype=HISTOGRAM_DTYPE)
    hist_ghc_root = np.zeros((1, n_bins), dtype=HISTOGRAM_DTYPE)
    hist_naive = np.zeros((1, n_bins), dtype=HISTOGRAM_DTYPE)

    _build_histogram_root_no_hessian(0, binned_feature, ordered_gradients, hist_gc_root)
    _build_histogram_root(
        0, binned_feature, ordered_gradients, ordered_hessians, hist_ghc_root
    )
    )
    )
    _build_histogram_naive(
        0,
        ordered_gradients,
        ordered_hessians,
        hist_naive,
    )

    hist_naive = hist_naive[0]
    hist_gc_root = hist_gc_root[0]
    hist_ghc_root = hist_ghc_root[0]
    for hist in (hist_gc_root, hist_ghc_root, hist_gc, hist_ghc):
        assert_array_equal(hist["count"], hist_naive["count"])
        assert_allclose(hist["sum_gradients"], hist_naive["sum_gradients"])
    for hist in (hist_ghc_root, hist_ghc):
        assert_allclose(hist["sum_hessians"], hist_naive["sum_hessians"])
    for hist in (hist_gc_root, hist_gc):
        assert_array_equal(hist["sum_hessians"], np.zeros(n_bins))

def test_hist_subtraction(constant_hessian):
    # Make sure the histogram subtraction trick gives the same result as the
    # classical method.
    n_bins = 5
    else:

    hist_parent = np.zeros((1, n_bins), dtype=HISTOGRAM_DTYPE)
        _build_histogram_no_hessian(
            0, sample_indices, binned_feature, ordered_gradients, hist_parent
        )
    else:
        _build_histogram(
            0,
            sample_indices,
            binned_feature,
            ordered_gradients,
            ordered_hessians,
            hist_parent,
        )

    mask = rng.randint(0, 2, n_samples).astype(bool)

    sample_indices_left = sample_indices[mask]
    ordered_gradients_left = ordered_gradients[mask]
    ordered_hessians_left = ordered_hessians[mask]
    hist_left = np.zeros((1, n_bins), dtype=HISTOGRAM_DTYPE)
            0, sample_indices_left, binned_feature, ordered_gradients_left, hist_left
        )
    else:
            0,
            ordered_gradients_left,
            ordered_hessians_left,
            hist_left,
        )

    sample_indices_right = sample_indices[~mask]
    ordered_gradients_right = ordered_gradients[~mask]
    ordered_hessians_right = ordered_hessians[~mask]
    hist_right = np.zeros((1, n_bins), dtype=HISTOGRAM_DTYPE)
            0, sample_indices_right, binned_feature, ordered_gradients_right, hist_right
        )
    else:
            0,
            ordered_gradients_right,
            ordered_hessians_right,
            hist_right,
        )

    hist_left_sub = np.copy(hist_parent)
    hist_right_sub = np.copy(hist_parent)
    _subtract_histograms(0, n_bins, hist_left_sub, hist_right)
    _subtract_histograms(0, n_bins, hist_right_sub, hist_left)

    for key in ("count", "sum_hessians", "sum_gradients"):
        assert_allclose(hist_left[key], hist_left_sub[key], rtol=1e-6)
        assert_allclose(hist_right[key], hist_right_sub[key], rtol=1e-6)

def _make_training_data(n_bins=256, constant_hessian=True):
    n_samples = 10000

    # Generate some test data directly binned so as to test the grower code
    # independently of the binning logic.
    X_binned = rng.randint(0, n_bins - 1, size=(n_samples, 2), dtype=X_BINNED_DTYPE)

    def true_decision_function(input_features):
        """Ground truth decision function

        This is a very simple yet asymmetric decision tree. Therefore the
        grower code should have no trouble recovering the decision function
        from 10000 training samples.
        """
        if input_features[0] <= n_bins // 2:
            return -1
        else:
            return -1 if input_features[1] <= n_bins // 3 else 1

    target = np.array([true_decision_function(x) for x in X_binned], dtype=Y_DTYPE)

    # Assume a square loss applied to an initial model that always predicts 0
    # (hardcoded for this test):
    all_gradients = target.astype(G_H_DTYPE)
    shape_hessians = 1 if constant_hessian else all_gradients.shape
    all_hessians = np.ones(shape=shape_hessians, dtype=G_H_DTYPE)

    return X_binned, all_gradients, all_hessians

def _check_children_consistency(parent, left, right):
    # Make sure the samples are correctly dispatched from a parent to its
    # children
    assert parent.left_child is left
    assert parent.right_child is right

    # each sample from the parent is propagated to one of the two children
    assert len(left.sample_indices) + len(right.sample_indices) == len(
        parent.sample_indices
    )

    assert set(left.sample_indices).union(set(right.sample_indices)) == set(
    )

    # samples are sent either to the left or the right node, never to both
    assert set(left.sample_indices).intersection(set(right.sample_indices)) == set()

def test_grow_tree(n_bins, constant_hessian, stopping_param, shrinkage):
    X_binned, all_gradients, all_hessians = _make_training_data(
        n_bins=n_bins, constant_hessian=constant_hessian
    )
    n_samples = X_binned.shape[0]

    if stopping_param == "max_leaf_nodes":
        stopping_param = {"max_leaf_nodes": 3}
    else:
        stopping_param = {"min_gain_to_split": 0.01}

        X_binned,
        shrinkage=shrinkage,
        **stopping_param,
    )

    # The root node is not yet split, but the best possible split has
    # already been evaluated:
    assert grower.root.left_child is None
    assert grower.root.right_child is None

    root_split = grower.root.split_info
    assert root_split.feature_idx == 0
    assert root_split.bin_idx == n_bins // 2
    assert len(grower.splittable_nodes) == 1

    # Calling split next applies the next split and computes the best split
    # for each of the two newly introduced children nodes.
    left_node, right_node = grower.split_next()

    # All training samples have ben split in the two nodes, approximately
    # 50%/50%
    _check_children_consistency(grower.root, left_node, right_node)
    assert len(left_node.sample_indices) > 0.4 * n_samples
    assert len(left_node.sample_indices) < 0.6 * n_samples

    if grower.min_gain_to_split > 0:
        # The left node is too pure: there is no gain to split it further.
        assert left_node.split_info.gain < grower.min_gain_to_split
        assert left_node in grower.finalized_leaves

    # The right node can still be split further, this time on feature #1
    split_info = right_node.split_info
    assert split_info.gain > 1.0
    assert split_info.feature_idx == 1
    assert split_info.bin_idx == n_bins // 3
    assert right_node.left_child is None
    assert right_node.right_child is None

    # The right split has not been applied yet. Let's do it now:
    right_left_node, right_right_node = grower.split_next()
    _check_children_consistency(right_node, right_left_node, right_right_node)
    assert len(right_left_node.sample_indices) > 0.1 * n_samples
    assert len(right_left_node.sample_indices) < 0.2 * n_samples

    assert len(right_right_node.sample_indices) > 0.2 * n_samples
    assert len(right_right_node.sample_indices) < 0.4 * n_samples

    # All the leafs are pure, it is not possible to split any further:
    assert not grower.splittable_nodes

    grower._apply_shrinkage()

    # Check the values of the leaves:
    assert grower.root.left_child.value == approx(shrinkage)
    assert grower.root.right_child.left_child.value == approx(shrinkage)
    assert grower.root.right_child.right_child.value == approx(-shrinkage, rel=1e-3)

def test_predictor_from_grower():
    # Build a tree on the toy 3-leaf dataset to extract the predictor.
    X_binned, all_gradients, all_hessians = _make_training_data(n_bins=n_bins)
        X_binned,
        max_leaf_nodes=3,
        min_samples_leaf=5,
    )
    assert grower.n_nodes == 5  # (2 decision nodes + 3 leaves)

    # Check that the node structure can be converted into a predictor
    # object to perform predictions at scale
    # We pass undefined binning_thresholds because we won't use predict anyway
    predictor = grower.make_predictor(
        binning_thresholds=np.zeros((X_binned.shape[1], n_bins))
    )
    assert predictor.nodes.shape[0] == 5
    assert predictor.nodes["is_leaf"].sum() == 3

    # Probe some predictions for each leaf of the tree
    # each group of 3 samples corresponds to a condition in _make_training_data
    input_data = np.array(
        [
            [0, 0],
            [42, 99],
            [128, 254],
            [129, 0],
            [129, 85],
            [254, 85],
            [129, 86],
            [129, 254],
            [242, 100],
        ],
        dtype=np.uint8,
    )
        input_data, missing_values_bin_idx, n_threads
    )
    expected_targets = [1, 1, 1, 1, 1, 1, -1, -1, -1]
    assert np.allclose(predictions, expected_targets)

    # Check that training set can be recovered exactly:
    predictions = predictor.predict_binned(X_binned, missing_values_bin_idx, n_threads)
    assert np.allclose(predictions, -all_gradients)

def test_min_samples_leaf(n_samples, min_samples_leaf, n_bins, constant_hessian, noise):
    rng = np.random.RandomState(seed=0)
    # data = linear target, 3 features, 1 irrelevant.
    X = rng.normal(size=(n_samples, 3))
    y = X[:, 0] - X[:, 1]
    if noise:
        y_scale = y.std()
        y += rng.normal(scale=noise, size=n_samples) * y_scale
    X = mapper.fit_transform(X)

    all_gradients = y.astype(G_H_DTYPE)
    shape_hessian = 1 if constant_hessian else all_gradients.shape
    all_hessians = np.ones(shape=shape_hessian, dtype=G_H_DTYPE)
        X,
        max_leaf_nodes=n_samples,
    )

    if n_samples >= min_samples_leaf:
        for node in predictor.nodes:
            if node["is_leaf"]:
                assert node["count"] >= min_samples_leaf
    else:
        assert predictor.nodes.shape[0] == 1
        assert predictor.nodes[0]["is_leaf"]
        assert predictor.nodes[0]["count"] == n_samples

def test_min_samples_leaf_root(n_samples, min_samples_leaf):
    # Make sure root node isn't split if n_samples is not at least twice
    # min_samples_leaf

    all_hessians = np.ones(shape=1, dtype=G_H_DTYPE)
        X,
    )
    if n_samples >= min_samples_leaf * 2:
        assert len(grower.finalized_leaves) >= 2
    else:
        assert len(grower.finalized_leaves) == 1

def assert_is_stump(grower):
    # To assert that stumps are created when max_depth=1
    for leaf in (grower.root.left_child, grower.root.right_child):
        assert leaf.left_child is None
        assert leaf.right_child is None

def test_max_depth(max_depth):
    # Make sure max_depth parameter works as expected

    grower = TreeGrower(X, all_gradients, all_hessians, max_depth=max_depth)

    depth = max(leaf.depth for leaf in grower.finalized_leaves)
    assert depth == max_depth

    if max_depth == 1:
        assert_is_stump(grower)

def test_input_validation():
    X_binned, all_gradients, all_hessians = _make_training_data()

    X_binned_float = X_binned.astype(np.float32)
    with pytest.raises(NotImplementedError, match="X_binned must be of type uint8"):
        TreeGrower(X_binned_float, all_gradients, all_hessians)

    X_binned_C_array = np.ascontiguousarray(X_binned)
        ValueError, match="X_binned should be passed as Fortran contiguous array"
    ):
        TreeGrower(X_binned_C_array, all_gradients, all_hessians)

def test_init_parameters_validation():
    with pytest.raises(ValueError, match="min_gain_to_split=-1 must be positive"):
        TreeGrower(X_binned, all_gradients, all_hessians, min_gain_to_split=-1)

    with pytest.raises(ValueError, match="min_hessian_to_split=-1 must be positive"):
        TreeGrower(X_binned, all_gradients, all_hessians, min_hessian_to_split=-1)

def test_missing_value_predict_only():
    # Make sure that missing values are supported at predict time even if they
    # were not encountered in the training data: the missing values are
    # assigned to whichever child has the most samples.

    X_binned = rng.randint(0, 256, size=(n_samples, 1), dtype=np.uint8)

    gradients = rng.normal(size=n_samples).astype(G_H_DTYPE)
    hessians = np.ones(shape=1, dtype=G_H_DTYPE)

        X_binned, gradients, hessians, min_samples_leaf=5, has_missing_values=False
    )

        binning_thresholds=np.zeros((X_binned.shape[1], X_binned.max() + 1))
    )

    # go from root to a leaf, always following node with the most samples.
    # That's the path nans are supposed to take
    node = predictor.nodes[0]
    while not node["is_leaf"]:
        left = predictor.nodes[node["left"]]
        right = predictor.nodes[node["right"]]
        node = left if left["count"] > right["count"] else right

    prediction_main_path = node["value"]

    # now build X_test with only nans, and make sure all predictions are equal
    # to prediction_main_path
    all_nans = np.full(shape=(n_samples, 1), fill_value=np.nan)

    y_pred = predictor.predict(all_nans, known_cat_bitsets, f_idx_map, n_threads)
    assert np.all(y_pred == prediction_main_path)

def test_split_on_nan_with_infinite_values():
    # Make sure the split on nan situations are respected even when there are
    # samples with +inf values (we set the threshold to +inf when we have a
    # split on nan so this test makes sure this does not introduce edge-case
    # bugs). We need to use the private API so that we can also test
    # predict_binned().

    X = np.array([0, 1, np.inf, np.nan, np.nan]).reshape(-1, 1)
    # the gradient values will force a split on nan situation
    gradients = np.array([0, 0, 0, 100, 100], dtype=G_H_DTYPE)

    n_bins_non_missing = 3
    has_missing_values = True
        X_binned,
        hessians,
    )

    predictor = grower.make_predictor(binning_thresholds=bin_mapper.bin_thresholds_)

    # sanity check: this was a split on nan
    assert predictor.nodes[0]["num_threshold"] == np.inf
    assert predictor.nodes[0]["bin_threshold"] == n_bins_non_missing - 1

    # Make sure in particular that the +inf sample is mapped to the left child
    # Note that lightgbm "fails" here and will assign the inf sample to the
    # right child, even though it's a "split on nan" situation.
    predictions = predictor.predict(X, known_cat_bitsets, f_idx_map, n_threads)
    predictions_binned = predictor.predict_binned(
        X_binned,
        missing_values_bin_idx=bin_mapper.missing_values_bin_idx_,
    )
    np.testing.assert_allclose(predictions, -gradients)
    np.testing.assert_allclose(predictions_binned, -gradients)

def test_grow_tree_categories():
    # Check that the grower produces the right predictor tree when a split is
    # categorical
    X_binned = np.array([[0, 1] * 11 + [1]], dtype=X_BINNED_DTYPE).T

    all_gradients = np.array([10, 1] * 11 + [1], dtype=G_H_DTYPE)
    is_categorical = np.ones(1, dtype=np.uint8)

        X_binned,
        n_bins=4,
    )
    assert grower.n_nodes == 3

    categories = [np.array([4, 9], dtype=X_DTYPE)]
    predictor = grower.make_predictor(binning_thresholds=categories)
    root = predictor.nodes[0]
    assert root["count"] == 23
    assert root["depth"] == 0
    assert root["is_categorical"]

    left, right = predictor.nodes[root["left"]], predictor.nodes[root["right"]]

    # arbitrary validation, but this means ones go to the left.
    assert left["count"] >= right["count"]

    # check binned category value (1)
    expected_binned_cat_bitset = [2**1] + [0] * 7
    binned_cat_bitset = predictor.binned_left_cat_bitsets
    assert_array_equal(binned_cat_bitset[0], expected_binned_cat_bitset)

    # check raw category value (9)
    expected_raw_cat_bitsets = [2**9] + [0] * 7
    raw_cat_bitsets = predictor.raw_left_cat_bitsets
    assert_array_equal(raw_cat_bitsets[0], expected_raw_cat_bitsets)

    # Note that since there was no missing values during training, the missing
    # values aren't part of the bitsets. However, we expect the missing values
    # to go to the biggest child (i.e. the left one).
    # The left child has a value of -1 = negative gradient.
    assert root["missing_go_to_left"]

    # make sure binned missing values are mapped to the left child during
    # prediction
        np.asarray([[6]]).astype(X_BINNED_DTYPE),
        missing_values_bin_idx=6,
    )
    assert_allclose(prediction_binned, [-1])  # negative gradient

    # make sure raw missing values are mapped to the left child during
    known_cat_bitsets = np.zeros((1, 8), dtype=np.uint32)  # ignored anyway
    prediction = predictor.predict(
        np.array([[np.nan]]), known_cat_bitsets, f_idx_map, n_threads
    )
    assert_allclose(prediction, [-1])

def test_ohe_equivalence(min_samples_leaf, n_unique_categories, target):
    # Make sure that native categorical splits are equivalent to using a OHE,
    # when given enough depth

    X_binned = rng.randint(0, n_unique_categories, size=(n_samples, 1), dtype=np.uint8)

    X_ohe = OneHotEncoder(sparse_output=False).fit_transform(X_binned)
    X_ohe = np.asfortranarray(X_ohe).astype(np.uint8)

    if target == "equal":
        gradients = X_binned.reshape(-1)
    elif target == "binary":
        gradients = (X_binned % 2).reshape(-1)
    else:
        gradients = rng.randn(n_samples)
    gradients = gradients.astype(G_H_DTYPE)

    grower_params = {
        "min_samples_leaf": min_samples_leaf,
        "max_depth": None,
        "max_leaf_nodes": None,
    }

        X_binned, gradients, hessians, is_categorical=[True], **grower_params
    )
    # we pass undefined bin_thresholds because we won't use predict()
        binning_thresholds=np.zeros((1, n_unique_categories))
    )
    preds = predictor.predict_binned(
        X_binned, missing_values_bin_idx=255, n_threads=n_threads
    )

    grower_ohe = TreeGrower(X_ohe, gradients, hessians, **grower_params)
    grower_ohe.grow()
    predictor_ohe = grower_ohe.make_predictor(
        binning_thresholds=np.zeros((X_ohe.shape[1], n_unique_categories))
    )
    preds_ohe = predictor_ohe.predict_binned(
        X_ohe, missing_values_bin_idx=255, n_threads=n_threads
    )

    assert predictor.get_max_depth() <= predictor_ohe.get_max_depth()
    if target == "binary" and n_unique_categories > 2:
        # OHE needs more splits to achieve the same predictions
        assert predictor.get_max_depth() < predictor_ohe.get_max_depth()

    np.testing.assert_allclose(preds, preds_ohe)

def test_grower_interaction_constraints():
    """Check that grower respects interaction constraints."""
    n_features = 6
    interaction_cst = [{0, 1}, {1, 2}, {3, 4, 5}]
    n_bins = 6
    root_feature_splits = []

    def get_all_children(node):
        res = []
        if node.is_leaf:
            return res
        for n in [node.left_child, node.right_child]:
            res.append(n)
            res.extend(get_all_children(n))
        return res

    for seed in range(20):

        X_binned = rng.randint(
            0, n_bins - 1, size=(n_samples, n_features), dtype=X_BINNED_DTYPE
        )
        X_binned = np.asfortranarray(X_binned)
        gradients = rng.normal(size=n_samples).astype(G_H_DTYPE)
        hessians = np.ones(shape=1, dtype=G_H_DTYPE)

        grower = TreeGrower(
            X_binned,
            gradients,
            hessians,
            min_samples_leaf=1,
        )
        grower.grow()

        root_feature_idx = grower.root.split_info.feature_idx
        root_feature_splits.append(root_feature_idx)

        feature_idx_to_constraint_set = {
            0: {0, 1},
            1: {0, 1, 2},
            2: {1, 2},
            3: {3, 4, 5},
            4: {3, 4, 5},
            5: {3, 4, 5},
        }

        root_constraint_set = feature_idx_to_constraint_set[root_feature_idx]
        for node in (grower.root.left_child, grower.root.right_child):
            # Root's children's allowed_features must be the root's constraints set.
            assert_array_equal(node.allowed_features, list(root_constraint_set))
        for node in get_all_children(grower.root):
            if node.is_leaf:
                continue
            # Ensure that each node uses a subset of features of its parent node.
            parent_interaction_cst_indices = set(node.interaction_cst_indices)
            right_interactions_cst_indices = set(
                node.right_child.interaction_cst_indices
            )
            left_interactions_cst_indices = set(node.left_child.interaction_cst_indices)

            assert right_interactions_cst_indices.issubset(
                parent_interaction_cst_indices
            )
            assert left_interactions_cst_indices.issubset(
            )
            # The features used for split must have been present in the root's
            # constraint set.
            assert node.split_info.feature_idx in root_constraint_set

    # Make sure that every feature is used at least once as split for the root node.
    assert (
        len(set(root_feature_splits))
        == len(set().union(*interaction_cst))
        == n_features
    )

        """
            return -1
        else:

        res = []
            return res
        return res

def assert_leaves_values_monotonic(predictor, monotonic_cst):
    # make sure leaves values (from left to right) are either all increasing
    # or all decreasing (or neither) depending on the monotonic constraint.
    nodes = predictor.nodes

    def get_leaves_values():
        """get leaves values from left to right"""
        values = []

        def depth_first_collect_leaf_values(node_idx):
            node = nodes[node_idx]
                values.append(node["value"])
                return
            depth_first_collect_leaf_values(node["left"])
            depth_first_collect_leaf_values(node["right"])

        depth_first_collect_leaf_values(0)  # start at root (0)
        return values

    values = get_leaves_values()

    if monotonic_cst == MonotonicConstraint.NO_CST:
        # some increasing, some decreasing
        assert not is_increasing(values) and not is_decreasing(values)
    elif monotonic_cst == MonotonicConstraint.POS:
        # all increasing
        assert is_increasing(values)
    else:  # NEG
        # all decreasing
        assert is_decreasing(values)

def assert_children_values_monotonic(predictor, monotonic_cst):
    # Make sure siblings values respect the monotonic constraints. Left should
    # be lower (resp greater) than right child if constraint is POS (resp.
    # NEG).
    # Note that this property alone isn't enough to ensure full monotonicity,
    # since we also need to guanrantee that all the descendents of the left
    # child won't be greater (resp. lower) than the right child, or its
    # descendents. That's why we need to bound the predicted values (this is
    # tested in assert_children_values_bounded)
    left_lower = []
    left_greater = []
    for node in nodes:
        if node["is_leaf"]:
            continue

        left_idx = node["left"]
        right_idx = node["right"]

        if nodes[left_idx]["value"] < nodes[right_idx]["value"]:
            left_lower.append(node)
        elif nodes[left_idx]["value"] > nodes[right_idx]["value"]:
            left_greater.append(node)

        assert left_lower and left_greater
        assert left_lower and not left_greater
    else:  # NEG
        assert not left_lower and left_greater

def assert_children_values_bounded(grower, monotonic_cst):
    # Make sure that the values of the children of a node are bounded by the
    # middle value between that node and its sibling (if there is a monotonic
    # constraint).
    # As a bonus, we also check that the siblings values are properly ordered
    # which is slightly redundant with assert_children_values_monotonic (but
    # this check is done on the grower nodes whereas
    # assert_children_values_monotonic is done on the predictor nodes)

        return

    def recursively_check_children_node_values(node, right_sibling=None):
            return
        if right_sibling is not None:
            middle = (node.value + right_sibling.value) / 2
            if monotonic_cst == MonotonicConstraint.POS:
                assert node.left_child.value <= node.right_child.value <= middle
                if not right_sibling.is_leaf:
                    assert (
                        middle
                        <= right_sibling.left_child.value
                        <= right_sibling.right_child.value
                    )
            else:  # NEG
                assert node.left_child.value >= node.right_child.value >= middle
                    assert (
                        middle
                        >= right_sibling.left_child.value
                        >= right_sibling.right_child.value
                    )

        recursively_check_children_node_values(
            node.left_child, right_sibling=node.right_child
        )
        recursively_check_children_node_values(node.right_child)

    recursively_check_children_node_values(grower.root)

def test_nodes_values(monotonic_cst, seed):
    # Build a single tree with only one feature, and make sure the nodes
    # values respect the monotonic constraints.

    # Considering the following tree with a monotonic POS constraint, we
    # should have:
    #
    #       root
    #      /    \
    #     5     10    # middle = 7.5
    #    / \   / \
    #   a  b  c  d
    #
    # a <= b and c <= d  (assert_children_values_monotonic)
    # a, b <= middle <= c, d (assert_children_values_bounded)
    # a <= b <= c <= d (assert_leaves_values_monotonic)
    #
    # The last one is a consequence of the others, but can't hurt to check

    rng = np.random.RandomState(seed)
    X_binned = rng.randint(0, 255, size=(n_samples, n_features), dtype=np.uint8)

        X_binned, gradients, hessians, monotonic_cst=[monotonic_cst], shrinkage=0.1
    )

    # grow() will shrink the leaves values at the very end. For our comparison
    # tests, we need to revert the shrinkage of the leaves, else we would
    # compare the value of a leaf (shrunk) with a node (not shrunk) and the
    # test would not be correct.
    for leave in grower.finalized_leaves:
        leave.value /= grower.shrinkage

    )

    # The consistency of the bounds can only be checked on the tree grower
    # as the node bounds are not copied into the predictor tree. The
    # consistency checks on the values of node children and leaves can be
    # done either on the grower tree or on the predictor tree. We only
    # do those checks on the predictor tree as the latter is derived from
    # the former.
    assert_children_values_monotonic(predictor, monotonic_cst)
    assert_children_values_bounded(grower, monotonic_cst)
    assert_leaves_values_monotonic(predictor, monotonic_cst)

def test_predictions(global_random_seed, use_feature_names):
    # Train a model with a POS constraint on the first non-categorical feature
    # and a NEG constraint on the second non-categorical feature, and make sure
    # the constraints are respected by checking the predictions.
    # test adapted from lightgbm's test_monotone_constraint(), itself inspired
    # by https://xgboost.readthedocs.io/en/latest/tutorials/monotonic.html

    f_0 = rng.rand(n_samples)  # positive correlation with y
    f_1 = rng.rand(n_samples)  # negative correlation with y

    # extra categorical features, no correlation with y,
    # to check the correctness of monotonicity constraint remapping, see issue #28898
    f_a = rng.randint(low=0, high=9, size=n_samples)
    f_b = rng.randint(low=0, high=9, size=n_samples)
    f_c = rng.randint(low=0, high=9, size=n_samples)

    X = np.c_[f_a, f_0, f_b, f_1, f_c]
    columns_name = ["f_a", "f_0", "f_b", "f_1", "f_c"]
    constructor_name = "dataframe" if use_feature_names else "array"
    X = _convert_container(X, constructor_name, columns_name=columns_name)

    noise = rng.normal(loc=0.0, scale=0.01, size=n_samples)
    y = 5 * f_0 + np.sin(10 * np.pi * f_0) - 5 * f_1 - np.cos(10 * np.pi * f_1) + noise

    if use_feature_names:
        monotonic_cst = {"f_0": +1, "f_1": -1}
        categorical_features = ["f_a", "f_b", "f_c"]
    else:
        monotonic_cst = [0, +1, 0, -1, 0]
        categorical_features = [0, 2, 4]

    gbdt = HistGradientBoostingRegressor(
        monotonic_cst=monotonic_cst, categorical_features=categorical_features
    )

    linspace = np.linspace(0, 1, 100)
    sin = np.sin(linspace)
    constant = np.full_like(linspace, fill_value=0.5)

    # We now assert the predictions properly respect the constraints, on each
    # feature. When testing for a feature we need to set the other one to a
    # constant, because the monotonic constraints are only a "all else being
    # equal" type of constraints:
    # a constraint on the first feature only means that
    # x0 < x0' => f(x0, x1) < f(x0', x1)
    # while x1 stays constant.
    # The constraint does not guanrantee that
    # x0 < x0' => f(x0, x1) < f(x0', x1')

    # First non-categorical feature (POS)
    # assert pred is all increasing when f_0 is all increasing
    X = np.c_[constant, linspace, constant, constant, constant]
    pred = gbdt.predict(X)
    assert is_increasing(pred)
    # assert pred actually follows the variations of f_0
    X = np.c_[constant, sin, constant, constant, constant]
    assert np.all((np.diff(pred) >= 0) == (np.diff(sin) >= 0))

    # Second non-categorical feature (NEG)
    # assert pred is all decreasing when f_1 is all increasing
    X = np.c_[constant, constant, constant, linspace, constant]
    assert is_decreasing(pred)
    # assert pred actually follows the inverse variations of f_1
    X = np.c_[constant, constant, constant, sin, constant]
    assert ((np.diff(pred) <= 0) == (np.diff(sin) >= 0)).all()

def test_input_error():
    X = [[1, 2], [2, 3], [3, 4]]
    y = [0, 1, 2]

    gbdt = HistGradientBoostingRegressor(monotonic_cst=[1, 0, -1])
        ValueError, match=re.escape("monotonic_cst has shape (3,) but the input data")
    ):
        gbdt.fit(X, y)

    for monotonic_cst in ([1, 3], [1, -3], [0.3, -0.7]):
        gbdt = HistGradientBoostingRegressor(monotonic_cst=monotonic_cst)
        expected_msg = re.escape(
            "must be an array-like of -1, 0 or 1. Observed values:"
        )
        with pytest.raises(ValueError, match=expected_msg):
            gbdt.fit(X, y)

    gbdt = HistGradientBoostingClassifier(monotonic_cst=[0, 1])
        ValueError,
        match="monotonic constraints are not supported for multiclass classification",
    ):

def test_input_error_related_to_feature_names():
    X = pd.DataFrame({"a": [0, 1, 2], "b": [0, 1, 2]})
    y = np.array([0, 1, 0])

    monotonic_cst = {"d": 1, "a": 1, "c": -1}
    gbdt = HistGradientBoostingRegressor(monotonic_cst=monotonic_cst)
        "monotonic_cst contains 2 unexpected feature names: ['c', 'd']."
    )

    monotonic_cst = {k: 1 for k in "abcdefghijklmnopqrstuvwxyz"}
        "monotonic_cst contains 24 unexpected feature names: "
        "['c', 'd', 'e', 'f', 'g', '...']."
    )

    monotonic_cst = {"a": 1}
        "HistGradientBoostingRegressor was not fitted on data with feature "
        "names. Pass monotonic_cst as an integer array instead."
    )
        gbdt.fit(X.values, y)

    monotonic_cst = {"b": -1, "a": "+"}
    expected_msg = re.escape("monotonic_cst['a'] must be either -1, 0 or 1. Got '+'.")

def test_bounded_value_min_gain_to_split():
    # The purpose of this test is to show that when computing the gain at a
    # given split, the value of the current node should be properly bounded to
    # respect the monotonic constraints, because it strongly interacts with
    # min_gain_to_split. We build a simple example where gradients are [1, 1,
    # 100, 1, 1] (hessians are all ones). The best split happens on the 3rd
    # bin, and depending on whether the value of the node is bounded or not,
    # the min_gain_to_split constraint is or isn't satisfied.
    n_bins = n_samples = 5
    X_binned = np.arange(n_samples).reshape(-1, 1).astype(X_BINNED_DTYPE)
    all_hessians = np.ones(n_samples, dtype=G_H_DTYPE)
    all_gradients = np.array([1, 1, 100, 1, 1], dtype=G_H_DTYPE)

    )
    )
    children_lower_bound, children_upper_bound = -np.inf, np.inf

    min_gain_to_split = 2000
        X_binned,
    )

    # Since the gradient array is [1, 1, 100, 1, 1]
    # the max possible gain happens on the 3rd bin (or equivalently in the 2nd)
    # and is equal to about 1307, which less than min_gain_to_split = 2000, so
    # the node is considered unsplittable (gain = -1)
    current_lower_bound, current_upper_bound = -np.inf, np.inf
        current_lower_bound,
        current_upper_bound,
    )
    # the unbounded value is equal to -sum_gradients / sum_hessians
    assert value == pytest.approx(-104 / 5)
        histograms,
        value,
        lower_bound=children_lower_bound,
        upper_bound=children_upper_bound,
    )
    assert split_info.gain == -1  # min_gain_to_split not respected

    # here again the max possible gain is on the 3rd bin but we now cap the
    # value of the node into [-10, inf].
    # This means the gain is now about 2430 which is more than the
    # min_gain_to_split constraint.
    current_lower_bound, current_upper_bound = -10, np.inf
    )
    assert value == -10
        value,
    )
    assert split_info.gain > min_gain_to_split

        values = []

                return

            return
                    assert (
                        middle
                    )
            else:  # NEG
                    assert (
                        middle
                    )

        )

                return

def test_set_get_bitset(values_to_insert, expected_bitset):
    n_32bits_ints = 3
    bitset = np.zeros(n_32bits_ints, dtype=np.uint32)
    for value in values_to_insert:
        set_bitset_memoryview(bitset, value)
    assert_allclose(expected_bitset, bitset)
    for value in range(32 * n_32bits_ints):
        if value in values_to_insert:
            assert in_bitset_memoryview(bitset, value)
        else:
            assert not in_bitset_memoryview(bitset, value)

def test_raw_bitset_from_binned_bitset(
    raw_categories, binned_cat_to_insert, expected_raw_bitset
):
    binned_bitset = np.zeros(2, dtype=np.uint32)
    raw_bitset = np.zeros(2, dtype=np.uint32)
    raw_categories = np.asarray(raw_categories, dtype=X_DTYPE)

    for val in binned_cat_to_insert:
        set_bitset_memoryview(binned_bitset, val)

    set_raw_bitset_from_binned_bitset(raw_bitset, binned_bitset, raw_categories)

    assert_allclose(expected_raw_bitset, raw_bitset)
    for binned_cat_val, raw_cat_val in enumerate(raw_categories):
        if binned_cat_val in binned_cat_to_insert:
            assert in_bitset_memoryview(raw_bitset, raw_cat_val)
        else:
            assert not in_bitset_memoryview(raw_bitset, raw_cat_val)

def test_ensemble_heterogeneous_estimators_behavior(X, y, estimator):
    # check that the behavior of `estimators`, `estimators_`,
    # `named_estimators`, `named_estimators_` is consistent across all
    # ensemble classes and when using `set_params()`.

    # before fit
    assert "svm" in estimator.named_estimators
    assert estimator.named_estimators.svm is estimator.estimators[1][1]
    assert estimator.named_estimators.svm is estimator.named_estimators["svm"]

    # check fitted attributes
    assert len(estimator.named_estimators) == 3
    assert len(estimator.named_estimators_) == 3
    assert sorted(list(estimator.named_estimators_.keys())) == sorted(
        ["lr", "svm", "rf"]
    )

    # check that set_params() does not add a new attribute
    estimator_new_params = clone(estimator)
    svm_estimator = SVC() if is_classifier(estimator) else SVR()
    estimator_new_params.set_params(svm=svm_estimator).fit(X, y)
    assert not hasattr(estimator_new_params, "svm")
    assert (
        estimator_new_params.named_estimators.lr.get_params()
        == estimator.named_estimators.lr.get_params()
    )
    assert (
        estimator_new_params.named_estimators.rf.get_params()
        == estimator.named_estimators.rf.get_params()
    )

    # check the behavior when setting an dropping an estimator
    estimator_dropped = clone(estimator)
    estimator_dropped.set_params(svm="drop")
    estimator_dropped.fit(X, y)
    assert len(estimator_dropped.named_estimators) == 3
    assert estimator_dropped.named_estimators.svm == "drop"
    assert len(estimator_dropped.named_estimators_) == 3
    assert sorted(list(estimator_dropped.named_estimators_.keys())) == sorted(
    )
    for sub_est in estimator_dropped.named_estimators_:
        # check that the correspondence is correct
        assert not isinstance(sub_est, type(estimator.named_estimators.svm))

    # check that we can set the parameters of the underlying classifier
    estimator.set_params(svm__C=10.0)
    estimator.set_params(rf__max_depth=5)
    assert (
        estimator.get_params()["svm__C"]
        == estimator.get_params()["svm"].get_params()["C"]
    )
    assert (
        estimator.get_params()["rf__max_depth"]
        == estimator.get_params()["rf"].get_params()["max_depth"]
    )

def test_ensemble_heterogeneous_estimators_type(Ensemble):
    # check that ensemble will fail during validation if the underlying
    # estimators are not of the same type (i.e. classifier or regressor)
    # StackingClassifier can have an underlying regresor so it's not checked
    if issubclass(Ensemble, ClassifierMixin):
        X, y = make_classification(n_samples=10)
        estimators = [("lr", LinearRegression())]
        ensemble_type = "classifier"
    else:
        X, y = make_regression(n_samples=10)
        estimators = [("lr", LogisticRegression())]
        ensemble_type = "regressor"
    ensemble = Ensemble(estimators=estimators)

    err_msg = "should be a {}".format(ensemble_type)
        ensemble.fit(X, y)

def test_ensemble_heterogeneous_estimators_name_validation(X, y, Ensemble):
    # raise an error when the name contains dunder
        estimators = [("lr__", LogisticRegression())]
    else:
        estimators = [("lr__", LinearRegression())]

    err_msg = r"Estimator names must not contain __: got \['lr__'\]"

    # raise an error when the name is not unique
        estimators = [("lr", LogisticRegression()), ("lr", LogisticRegression())]
    else:
        estimators = [("lr", LinearRegression()), ("lr", LinearRegression())]

    err_msg = r"Names provided are not unique: \['lr', 'lr'\]"

    # raise an error when the name conflicts with the parameters
        estimators = [("estimators", LogisticRegression())]
    else:
        estimators = [("estimators", LinearRegression())]

    err_msg = "Estimator names conflict with constructor arguments"

def test_ensemble_heterogeneous_estimators_all_dropped(X, y, estimator):
    # check that we raise a consistent error when all estimators are
    # dropped
    estimator.set_params(lr="drop")
    with pytest.raises(ValueError, match="All estimators are dropped."):

def test_heterogeneous_ensemble_support_missing_values(Ensemble, Estimator, X, y):
    # check that Voting and Stacking predictor delegate the missing values
    # validation to the underlying estimator.
    X = X.copy()
    mask = np.random.choice([1, 0], X.shape, p=[0.1, 0.9]).astype(bool)
    pipe = make_pipeline(SimpleImputer(), Estimator())
    ensemble = Ensemble(estimators=[("pipe1", pipe), ("pipe2", pipe)])
    ensemble.fit(X, y).score(X, y)

def test_stacking_classifier_iris(cv, final_estimator, passthrough):
    # prescale the data to avoid convergence warning without using a pipeline
    # for later assert
        scale(X_iris), y_iris, stratify=y_iris, random_state=42
    )
    estimators = [("lr", LogisticRegression()), ("svc", LinearSVC())]
    clf = StackingClassifier(
        estimators=estimators,
        final_estimator=final_estimator,
        cv=cv,
        passthrough=passthrough,
    )
    clf.fit(X_train, y_train)
    clf.predict(X_test)
    clf.predict_proba(X_test)
    assert clf.score(X_test, y_test) > 0.8

    X_trans = clf.transform(X_test)
    expected_column_count = 10 if passthrough else 6
    assert X_trans.shape[1] == expected_column_count
    if passthrough:
        assert_allclose(X_test, X_trans[:, -4:])

    clf.set_params(lr="drop")
    if final_estimator is None:
        # LogisticRegression has decision_function method
        clf.decision_function(X_test)

    expected_column_count_drop = 7 if passthrough else 3
    assert X_trans.shape[1] == expected_column_count_drop

def test_stacking_classifier_drop_column_binary_classification():
    # check that a column is dropped in binary classification
    X, y = load_breast_cancer(return_X_y=True)
    X_train, X_test, y_train, _ = train_test_split(
        scale(X), y, stratify=y, random_state=42
    )

    # both classifiers implement 'predict_proba' and will both drop one column
    estimators = [
        ("lr", LogisticRegression()),
        ("rf", RandomForestClassifier(random_state=42)),
    ]
    clf = StackingClassifier(estimators=estimators, cv=3)

    assert X_trans.shape[1] == 2

    # LinearSVC does not implement 'predict_proba' and will not drop one column
    clf.set_params(estimators=estimators)

def test_stacking_classifier_drop_estimator():
    )
    estimators = [("lr", "drop"), ("svc", LinearSVC(random_state=0))]
    rf = RandomForestClassifier(n_estimators=10, random_state=42)
        estimators=[("svc", LinearSVC(random_state=0))],
        final_estimator=rf,
        cv=5,
    )
    clf_drop = StackingClassifier(estimators=estimators, final_estimator=rf, cv=5)

    clf_drop.fit(X_train, y_train)
    assert_allclose(clf.predict(X_test), clf_drop.predict(X_test))
    assert_allclose(clf.predict_proba(X_test), clf_drop.predict_proba(X_test))
    assert_allclose(clf.transform(X_test), clf_drop.transform(X_test))

def test_stacking_regressor_drop_estimator():
        scale(X_diabetes), y_diabetes, random_state=42
    )
    estimators = [("lr", "drop"), ("svr", LinearSVR(random_state=0))]
    rf = RandomForestRegressor(n_estimators=10, random_state=42)
    reg = StackingRegressor(
        estimators=[("svr", LinearSVR(random_state=0))],
        cv=5,
    )
    reg_drop = StackingRegressor(estimators=estimators, final_estimator=rf, cv=5)

    reg.fit(X_train, y_train)
    reg_drop.fit(X_train, y_train)
    assert_allclose(reg.predict(X_test), reg_drop.predict(X_test))
    assert_allclose(reg.transform(X_test), reg_drop.transform(X_test))

def test_stacking_regressor_diabetes(cv, final_estimator, predict_params, passthrough):
    )
    estimators = [("lr", LinearRegression()), ("svr", LinearSVR())]
        cv=cv,
    )
    result = reg.predict(X_test, **predict_params)
    expected_result_length = 2 if predict_params else 1
    if predict_params:
        assert len(result) == expected_result_length

    X_trans = reg.transform(X_test)
    expected_column_count = 12 if passthrough else 2
        assert_allclose(X_test, X_trans[:, -10:])

    reg.set_params(lr="drop")
    reg.predict(X_test)

    expected_column_count_drop = 11 if passthrough else 1

def test_stacking_regressor_sparse_passthrough(sparse_container):
    # Check passthrough behavior on a sparse X matrix
        sparse_container(scale(X_diabetes)), y_diabetes, random_state=42
    )
    clf = StackingRegressor(
        estimators=estimators, final_estimator=rf, cv=5, passthrough=True
    )
    assert_allclose_dense_sparse(X_test, X_trans[:, -10:])
    assert sparse.issparse(X_trans)
    assert X_test.format == X_trans.format

def test_stacking_classifier_sparse_passthrough(sparse_container):
        sparse_container(scale(X_iris)), y_iris, random_state=42
    )
    )
    assert_allclose_dense_sparse(X_test, X_trans[:, -4:])

def test_stacking_classifier_drop_binary_prob():
    # check that classifier will drop one of the probability column for
    # binary classification problem

    # Select only the 2 first classes
    X_, y_ = scale(X_iris[:100]), y_iris[:100]

    estimators = [("lr", LogisticRegression()), ("rf", RandomForestClassifier())]
    clf = StackingClassifier(estimators=estimators)
    clf.fit(X_, y_)
    X_meta = clf.transform(X_)
    assert X_meta.shape[1] == 2

class NoWeightRegressor(RegressorMixin, BaseEstimator):
        self.reg = DummyRegressor()
        return self.reg.fit(X, y)

        return np.ones(X.shape[0])

class NoWeightClassifier(ClassifierMixin, BaseEstimator):
        self.clf = DummyClassifier(strategy="stratified")
        return self.clf.fit(X, y)

def test_stacking_classifier_error(y, params, type_err, msg_err):
    with pytest.raises(type_err, match=msg_err):
        clf = StackingClassifier(**params, cv=3)
        clf.fit(scale(X_iris), y, sample_weight=np.ones(X_iris.shape[0]))

def test_stacking_regressor_error(y, params, type_err, msg_err):
        reg = StackingRegressor(**params, cv=3)
        reg.fit(scale(X_diabetes), y, sample_weight=np.ones(X_diabetes.shape[0]))

def test_stacking_randomness(estimator, X, y):
    # checking that fixing the random state of the CV will lead to the same
    # results
    estimator_full = clone(estimator)
    estimator_full.set_params(
        cv=KFold(shuffle=True, random_state=np.random.RandomState(0))
    )

    estimator_drop = clone(estimator)
    estimator_drop.set_params(lr="drop")
    estimator_drop.set_params(
    )

        estimator_full.fit(X, y).transform(X)[:, 1:],
        estimator_drop.fit(X, y).transform(X),
    )

def test_stacking_classifier_stratify_default():
    # check that we stratify the classes for the default CV
        estimators=[
            ("lr", LogisticRegression(max_iter=10_000)),
            ("svm", LinearSVC(max_iter=10_000)),
        ]
    )
    # since iris is not shuffled, a simple k-fold would not contain the
    # 3 classes during training
    clf.fit(X_iris, y_iris)

def test_stacking_with_sample_weight(stacker, X, y):
    # check that sample weights has an influence on the fitting
    # note: ConvergenceWarning are catch since we are not worrying about the
    # convergence here
    n_half_samples = len(y) // 2
    total_sample_weight = np.array(
        [0.1] * n_half_samples + [0.9] * (len(y) - n_half_samples)
    )
    X_train, X_test, y_train, _, sample_weight_train, _ = train_test_split(
        X, y, total_sample_weight, random_state=42
    )

    with ignore_warnings(category=ConvergenceWarning):
        stacker.fit(X_train, y_train)
    y_pred_no_weight = stacker.predict(X_test)

        stacker.fit(X_train, y_train, sample_weight=np.ones(y_train.shape))
    y_pred_unit_weight = stacker.predict(X_test)

    assert_allclose(y_pred_no_weight, y_pred_unit_weight)

        stacker.fit(X_train, y_train, sample_weight=sample_weight_train)
    y_pred_biased = stacker.predict(X_test)

    assert np.abs(y_pred_no_weight - y_pred_biased).sum() > 0

def test_stacking_classifier_sample_weight_fit_param():
    # check sample_weight is passed to all invocations of fit
    stacker = StackingClassifier(
        estimators=[("lr", CheckingClassifier(expected_sample_weight=True))],
        final_estimator=CheckingClassifier(expected_sample_weight=True),
    )
    stacker.fit(X_iris, y_iris, sample_weight=np.ones(X_iris.shape[0]))

def test_stacking_cv_influence(stacker, X, y):
    # check that the stacking affects the fit of the final estimator but not
    # the fit of the base estimators
    stacker_cv_3 = clone(stacker)
    stacker_cv_5 = clone(stacker)

    stacker_cv_3.set_params(cv=3)
    stacker_cv_5.set_params(cv=5)

    stacker_cv_3.fit(X, y)
    stacker_cv_5.fit(X, y)

    # the base estimators should be identical
    for est_cv_3, est_cv_5 in zip(stacker_cv_3.estimators_, stacker_cv_5.estimators_):
        assert_allclose(est_cv_3.coef_, est_cv_5.coef_)

    # the final estimator should be different
    with pytest.raises(AssertionError, match="Not equal"):
        assert_allclose(
            stacker_cv_3.final_estimator_.coef_, stacker_cv_5.final_estimator_.coef_
        )

def test_stacking_prefit(Stacker, Estimator, stack_method, final_estimator, X, y):
    """Check the behaviour of stacking when `cv='prefit'`"""
    X_train1, X_train2, y_train1, y_train2 = train_test_split(
        X, y, random_state=42, test_size=0.5
    )
        ("d0", Estimator().fit(X_train1, y_train1)),
        ("d1", Estimator().fit(X_train1, y_train1)),
    ]

    # mock out fit and stack_method to be asserted later
    for _, estimator in estimators:
        estimator.fit = Mock(name="fit")
        stack_func = getattr(estimator, stack_method)
        predict_method_mocked = Mock(side_effect=stack_func)
        # Mocking a method will not provide a `__name__` while Python methods
        # do and we are using it in `_get_response_method`.
        predict_method_mocked.__name__ = stack_method
        setattr(estimator, stack_method, predict_method_mocked)

    stacker = Stacker(
        estimators=estimators, cv="prefit", final_estimator=final_estimator
    )
    stacker.fit(X_train2, y_train2)

    assert stacker.estimators_ == [estimator for _, estimator in estimators]
    # fit was not called again
    assert all(estimator.fit.call_count == 0 for estimator in stacker.estimators_)

    # stack method is called with the proper inputs
    for estimator in stacker.estimators_:
        stack_func_mock = getattr(estimator, stack_method)
        stack_func_mock.assert_called_with(X_train2)

def test_stacking_prefit_error(stacker, X, y):
    # check that NotFittedError is raised
    # if base estimators are not fitted when cv="prefit"
        stacker.fit(X, y)

def test_stacking_without_n_features_in(make_dataset, Stacking, Estimator):
    # Stacking supports estimators without `n_features_in_`. Regression test
    # for #17353

    class MyEstimator(Estimator):
        """Estimator without n_features_in_"""

        def fit(self, X, y):
            super().fit(X, y)
            del self.n_features_in_

    X, y = make_dataset(random_state=0, n_samples=100)
    stacker = Stacking(estimators=[("lr", MyEstimator())])

    msg = f"{Stacking.__name__} object has no attribute n_features_in_"
    with pytest.raises(AttributeError, match=msg):
        stacker.n_features_in_

    # Does not raise
    stacker.fit(X, y)

    msg = "'MyEstimator' object has no attribute 'n_features_in_'"

def test_stacking_classifier_multilabel_predict_proba(estimator):
    """Check the behaviour for the multilabel classification case and the
    `predict_proba` stacking method.

    Estimators are not consistent with the output arrays and we need to ensure that
    we handle all cases.
    """
        X_multilabel, y_multilabel, stratify=y_multilabel, random_state=42
    )
    n_outputs = 3

    estimators = [("est", estimator)]
        final_estimator=KNeighborsClassifier(),
        stack_method="predict_proba",
    ).fit(X_train, y_train)

    X_trans = stacker.transform(X_test)
    assert X_trans.shape == (X_test.shape[0], n_outputs)
    # we should not have any collinear classes and thus nothing should sum to 1
    assert not any(np.isclose(X_trans.sum(axis=1), 1.0))

    y_pred = stacker.predict(X_test)
    assert y_pred.shape == y_test.shape

def test_stacking_classifier_multilabel_decision_function():
    `decision_function` stacking method. Only `RidgeClassifier` supports this
    case.
    """
    )

    estimators = [("est", RidgeClassifier())]
        stack_method="decision_function",

def test_stacking_classifier_multilabel_auto_predict(stack_method, passthrough):
    """Check the behaviour for the multilabel classification case for stack methods
    supported for all estimators or automatically picked up.
    """
    )
    y_train_before_fit = y_train.copy()

        ("mlp", MLPClassifier(random_state=42)),
        ("ridge", RidgeClassifier()),
    ]
    final_estimator = KNeighborsClassifier()

        stack_method=stack_method,

    # make sure we don't change `y_train` inplace
    assert_array_equal(y_train_before_fit, y_train)

    y_pred = clf.predict(X_test)

    if stack_method == "auto":
        expected_stack_methods = ["predict_proba", "predict_proba", "decision_function"]
    else:
        expected_stack_methods = ["predict"] * len(estimators)
    assert clf.stack_method_ == expected_stack_methods

    n_features_X_trans = n_outputs * len(estimators)
        n_features_X_trans += X_train.shape[1]
    assert X_trans.shape == (X_test.shape[0], n_features_X_trans)

    assert_array_equal(clf.classes_, [np.array([0, 1])] * n_outputs)

def test_get_feature_names_out(
    stacker, feature_names, X, y, expected_names, passthrough
):
    """Check get_feature_names_out works for stacking."""

    stacker.set_params(passthrough=passthrough)
    stacker.fit(scale(X), y)

        expected_names = np.concatenate((expected_names, feature_names))

    names_out = stacker.get_feature_names_out(feature_names)
    assert_array_equal(names_out, expected_names)

def test_stacking_classifier_base_regressor():
    """Check that a regressor can be used as the first layer in `StackingClassifier`."""
    )
    clf = StackingClassifier(estimators=[("ridge", Ridge())])

def test_stacking_final_estimator_attribute_error():
    """Check that we raise the proper AttributeError when the final estimator
    does not implement the `decision_function` method, which is decorated with
    `available_if`.

    https://github.com/scikit-learn/scikit-learn/issues/28108
    """
    X, y = make_classification(random_state=42)

        ("rf", RandomForestClassifier(n_estimators=2, random_state=42)),
    ]
    # RandomForestClassifier does not implement 'decision_function' and should raise
    # an AttributeError
    final_estimator = RandomForestClassifier(n_estimators=2, random_state=42)
        estimators=estimators, final_estimator=final_estimator, cv=3
    )

    outer_msg = "This 'StackingClassifier' has no attribute 'decision_function'"
    inner_msg = "'RandomForestClassifier' object has no attribute 'decision_function'"
    with pytest.raises(AttributeError, match=outer_msg) as exec_info:
        clf.fit(X, y).decision_function(X)
    assert isinstance(exec_info.value.__cause__, AttributeError)
    assert inner_msg in str(exec_info.value.__cause__)

def test_routing_passed_metadata_not_supported(Estimator, Child):
    """Test that the right error message is raised when metadata is passed while
    not supported when `enable_metadata_routing=False`."""

        ValueError, match="is only supported if enable_metadata_routing=True"
    ):
        Estimator(["clf", Child()]).fit(
            X_iris, y_iris, sample_weight=[1, 1, 1, 1, 1], metadata="a"
        )

def test_get_metadata_routing_without_fit(Estimator, Child):
    # Test that metadata_routing() doesn't raise when called before fit.
    est = Estimator([("sub_est", Child())])
    est.get_metadata_routing()

def test_metadata_routing_for_stacking_estimators(Estimator, Child, prop, prop_value):
    """Test that metadata is routed correctly for Stacking*."""

    est = Estimator(
        [
            (
                "sub_est1",
                Child(registry=_Registry()).set_fit_request(**{prop: True}),
            ),
            (
                "sub_est2",
            ),
        ],
        final_estimator=Child(registry=_Registry()).set_predict_request(**{prop: True}),
    )

    est.fit(X_iris, y_iris, **{prop: prop_value})
    est.fit_transform(X_iris, y_iris, **{prop: prop_value})

    est.predict(X_iris, **{prop: prop_value})

    for estimator in est.estimators:
        # access sub-estimator in (name, est) with estimator[1]:
        registry = estimator[1].registry
        assert len(registry)
        for sub_est in registry:
            check_recorded_metadata(
                obj=sub_est,
                method="fit",
                parent="fit",
                split_params=(prop),
                **{prop: prop_value},
            )
    # access final_estimator:
    registry = est.final_estimator_.registry
    assert len(registry)
    check_recorded_metadata(
        obj=registry[-1],
        method="predict",
        parent="predict",
        split_params=(prop),
        **{prop: prop_value},
    )

def test_metadata_routing_error_for_stacking_estimators(Estimator, Child):
    """Test that the right error is raised when metadata is not requested."""
    sample_weight, metadata = np.ones(X_iris.shape[0]), "a"

    error_message = (
        "[sample_weight, metadata] are passed but are not explicitly set as requested"
        f" or not requested for {Child.__name__}.fit"
    )

    with pytest.raises(ValueError, match=re.escape(error_message)):
        est.fit(X_iris, y_iris, sample_weight=sample_weight, metadata=metadata)

def test_classification_toy(name):
    """Check classification on a toy dataset."""
    ForestClassifier = FOREST_CLASSIFIERS[name]

    clf = ForestClassifier(n_estimators=10, random_state=1)
    assert_array_equal(clf.predict(T), true_result)
    assert 10 == len(clf)

    clf = ForestClassifier(n_estimators=10, max_features=1, random_state=1)

    # also test apply
    leaf_indices = clf.apply(X)
    assert leaf_indices.shape == (len(X), clf.n_estimators)

def test_iris_criterion(name, criterion):
    # Check consistency on dataset iris.

    clf = ForestClassifier(n_estimators=10, criterion=criterion, random_state=1)
    clf.fit(iris.data, iris.target)
    score = clf.score(iris.data, iris.target)
    assert score > 0.9, "Failed with criterion %s and score = %f" % (criterion, score)

    clf = ForestClassifier(
        n_estimators=10, criterion=criterion, max_features=2, random_state=1
    )
    assert score > 0.5, "Failed with criterion %s and score = %f" % (criterion, score)

def test_regression_criterion(name, criterion):
    # Check consistency on regression dataset.
    ForestRegressor = FOREST_REGRESSORS[name]

    reg = ForestRegressor(n_estimators=5, criterion=criterion, random_state=1)
    reg.fit(X_reg, y_reg)
    score = reg.score(X_reg, y_reg)
    assert score > 0.93, (
        "Failed with max_features=None, criterion %s and score = %f"
        % (
            criterion,
            score,
        )
    )

    reg = ForestRegressor(
        n_estimators=5, criterion=criterion, max_features=6, random_state=1
    )
    assert score > 0.92, "Failed with max_features=6, criterion %s and score = %f" % (
        score,
    )

def test_poisson_vs_mse():
    """Test that random forest with poisson criterion performs better than
    mse for a poisson target.

    There is a similar test for DecisionTreeRegressor.
    """
    n_train, n_test, n_features = 500, 500, 10
    X = datasets.make_low_rank_matrix(
    )
    #  We create a log-linear Poisson model and downscale coef as it will get
    )
    # We prevent some overfitting by setting min_samples_split=10.
    forest_poi = RandomForestRegressor(
        criterion="poisson", min_samples_leaf=10, max_features="sqrt", random_state=rng
    )
    forest_mse = RandomForestRegressor(
        min_samples_leaf=10,
    )

    forest_poi.fit(X_train, y_train)
    forest_mse.fit(X_train, y_train)

    for X, y, data_name in [(X_train, y_train, "train"), (X_test, y_test, "test")]:
        metric_poi = mean_poisson_deviance(y, forest_poi.predict(X))
        # squared_error forest might produce non-positive predictions => clip
        # If y = 0 for those, the poisson deviance gets too good.
        # If we drew more samples, we would eventually get y > 0 and the
        # poisson deviance would explode, i.e. be undefined. Therefore, we do
        # not clip to a tiny value like 1e-15, but to 1e-6. This acts like a
        # small penalty to the non-positive predictions.
        metric_mse = mean_poisson_deviance(
            y, np.clip(forest_mse.predict(X), 1e-6, None)
        )
        # As squared_error might correctly predict 0 in train set, its train
        # score can be better than Poisson. This is no longer the case for the
        # test set. But keep the above comment for clipping in mind.
        if data_name == "test":
            assert metric_poi < metric_mse
        assert metric_poi < 0.8 * metric_dummy

def test_balance_property_random_forest(criterion):
    """ "Test that sum(y_pred)==sum(y_true) on the training set."""
    )

    reg = RandomForestRegressor(
        criterion=criterion, n_estimators=10, bootstrap=False, random_state=rng
    )
    reg.fit(X, y)

    assert np.sum(reg.predict(X)) == pytest.approx(np.sum(y))

def test_regressor_attributes(name):
    # Regression models should not have a classes_ attribute.
    r = FOREST_REGRESSORS[name](random_state=0)
    assert not hasattr(r, "classes_")
    assert not hasattr(r, "n_classes_")

    r.fit([[1, 2, 3], [4, 5, 6]], [1, 2])

def test_probability(name):
    # Predict probabilities.
    with np.errstate(divide="ignore"):
        clf = ForestClassifier(
            n_estimators=10, random_state=1, max_features=1, max_depth=1
        )
        clf.fit(iris.data, iris.target)
        assert_array_almost_equal(
            np.sum(clf.predict_proba(iris.data), axis=1), np.ones(iris.data.shape[0])
        )
            clf.predict_proba(iris.data), np.exp(clf.predict_log_proba(iris.data))
        )

def test_importances(dtype, name, criterion):
    tolerance = 0.01
    if name in FOREST_REGRESSORS and criterion == "absolute_error":
        tolerance = 0.05

    # cast as dtype
    X = X_large.astype(dtype, copy=False)
    y = y_large.astype(dtype, copy=False)

    ForestEstimator = FOREST_ESTIMATORS[name]

    est = ForestEstimator(n_estimators=10, criterion=criterion, random_state=0)
    importances = est.feature_importances_

    # The forest estimator can detect that only the first 3 features of the
    # dataset are informative:
    n_important = np.sum(importances > 0.1)
    assert importances.shape[0] == 10
    assert n_important == 3
    assert np.all(importances[:3] > 0.1)

    # Check with parallel
    est.set_params(n_jobs=2)
    importances_parallel = est.feature_importances_
    assert_array_almost_equal(importances, importances_parallel)

    # Check with sample weights
    sample_weight = check_random_state(0).randint(1, 10, len(X))
    est = ForestEstimator(n_estimators=10, random_state=0, criterion=criterion)
    est.fit(X, y, sample_weight=sample_weight)
    assert np.all(importances >= 0.0)

    for scale in [0.5, 100]:
        est = ForestEstimator(n_estimators=10, random_state=0, criterion=criterion)
        est.fit(X, y, sample_weight=scale * sample_weight)
        importances_bis = est.feature_importances_
        assert np.abs(importances - importances_bis).mean() < tolerance

def test_importances_asymptotic():
    # Check whether variable importances of totally randomized trees
    # converge towards their theoretical values (See Louppe et al,
    # Understanding variable importances in forests of randomized trees, 2013).

    def binomial(k, n):
        return 0 if k < 0 or k > n else comb(int(n), int(k), exact=True)

    def entropy(samples):
        n_samples = len(samples)
        entropy = 0.0

        for count in np.bincount(samples):
            p = 1.0 * count / n_samples
            if p > 0:
                entropy -= p * np.log2(p)

        return entropy

    def mdi_importance(X_m, X, y):

        features = list(range(n_features))
        features.pop(X_m)
        values = [np.unique(X[:, i]) for i in range(n_features)]

        imp = 0.0

        for k in range(n_features):
            # Weight of each B of size k
            coef = 1.0 / (binomial(k, n_features) * (n_features - k))

            # For all B of size k
            for B in combinations(features, k):
                # For all values B=b
                for b in product(*[values[B[j]] for j in range(k)]):
                    mask_b = np.ones(n_samples, dtype=bool)

                    for j in range(k):
                        mask_b &= X[:, B[j]] == b[j]

                    X_, y_ = X[mask_b, :], y[mask_b]
                    n_samples_b = len(X_)

                    if n_samples_b > 0:
                        children = []

                        for xi in values[X_m]:
                            mask_xi = X_[:, X_m] == xi
                            children.append(y_[mask_xi])

                        imp += (
                            coef
                            * (1.0 * n_samples_b / n_samples)  # P(B=b)
                            * (
                                entropy(y_)
                                - sum(
                                    [
                                        entropy(c) * len(c) / n_samples_b
                                        for c in children
                                    ]
                                )
                            )
                        )

        return imp

    data = np.array(
        [
            [0, 0, 1, 0, 0, 1, 0, 1],
            [1, 0, 1, 1, 1, 0, 1, 2],
            [1, 0, 1, 1, 0, 1, 1, 3],
            [0, 1, 1, 1, 0, 1, 0, 4],
            [1, 1, 0, 1, 0, 1, 1, 5],
            [1, 1, 0, 1, 1, 1, 1, 6],
            [1, 0, 1, 0, 0, 1, 0, 7],
            [1, 1, 1, 1, 1, 1, 1, 8],
            [1, 1, 1, 1, 0, 1, 1, 9],
            [1, 1, 1, 0, 1, 1, 1, 0],
        ]
    )

    X, y = np.array(data[:, :7], dtype=bool), data[:, 7]

    # Compute true importances
    true_importances = np.zeros(n_features)

    for i in range(n_features):
        true_importances[i] = mdi_importance(i, X, y)

    # Estimate importances with totally randomized trees
    clf = ExtraTreesClassifier(
        n_estimators=500, max_features=1, criterion="log_loss", random_state=0

    importances = (
        sum(
            for tree in clf.estimators_
        )
        / clf.n_estimators
    )

    # Check correctness
    assert_almost_equal(entropy(y), sum(importances))
    assert np.abs(true_importances - importances).mean() < 0.01

def test_unfitted_feature_importances(name):
    err_msg = (
        "This {} instance is not fitted yet. Call 'fit' with "
        "appropriate arguments before using this estimator.".format(name)
    )
    with pytest.raises(NotFittedError, match=err_msg):
        getattr(FOREST_ESTIMATORS[name](), "feature_importances_")

def test_forest_classifier_oob(
    ForestClassifier, X, y, X_type, lower_bound_accuracy, oob_score
):
    """Check that OOB score is close to score on a test set."""
    X = _convert_container(X, constructor_name=X_type)
        X,
        y,
        test_size=0.5,
    )
    classifier = ForestClassifier(
        n_estimators=40,
        oob_score=oob_score,
    )

    assert not hasattr(classifier, "oob_score_")
    assert not hasattr(classifier, "oob_decision_function_")

    classifier.fit(X_train, y_train)
    if callable(oob_score):
        test_score = oob_score(y_test, classifier.predict(X_test))
    else:
        test_score = classifier.score(X_test, y_test)
        assert classifier.oob_score_ >= lower_bound_accuracy

    abs_diff = abs(test_score - classifier.oob_score_)
    assert abs_diff <= 0.11, f"{abs_diff=} is greater than 0.11"

    assert hasattr(classifier, "oob_score_")
    assert not hasattr(classifier, "oob_prediction_")
    assert hasattr(classifier, "oob_decision_function_")

    if y.ndim == 1:
        expected_shape = (X_train.shape[0], len(set(y)))
    else:
        expected_shape = (X_train.shape[0], len(set(y[:, 0])), y.shape[1])
    assert classifier.oob_decision_function_.shape == expected_shape

def test_forest_regressor_oob(ForestRegressor, X, y, X_type, lower_bound_r2, oob_score):
    """Check that forest-based regressor provide an OOB score close to the
    score on a test set."""
        X,
        y,
    )
    regressor = ForestRegressor(
    )

    assert not hasattr(regressor, "oob_score_")
    assert not hasattr(regressor, "oob_prediction_")

    regressor.fit(X_train, y_train)
        test_score = oob_score(y_test, regressor.predict(X_test))
    else:
        test_score = regressor.score(X_test, y_test)
        assert regressor.oob_score_ >= lower_bound_r2

    assert abs(test_score - regressor.oob_score_) <= 0.1

    assert hasattr(regressor, "oob_score_")
    assert hasattr(regressor, "oob_prediction_")
    assert not hasattr(regressor, "oob_decision_function_")

        expected_shape = (X_train.shape[0],)
    else:
        expected_shape = (X_train.shape[0], y.ndim)
    assert regressor.oob_prediction_.shape == expected_shape

def test_forest_oob_warning(ForestEstimator):
    """Check that a warning is raised when not enough estimator and the OOB
    estimates will be inaccurate."""
    estimator = ForestEstimator(
        n_estimators=1,
        oob_score=True,
    )
    with pytest.warns(UserWarning, match="Some inputs do not have OOB scores"):
        estimator.fit(iris.data, iris.target)

def test_forest_oob_score_requires_bootstrap(ForestEstimator):
    """Check that we raise an error if OOB score is requested without
    activating bootstrapping.
    """
    X = iris.data
    y = iris.target
    err_msg = "Out of bag estimation only available if bootstrap=True"
    estimator = ForestEstimator(oob_score=True, bootstrap=False)

def test_classifier_error_oob_score_multiclass_multioutput(ForestClassifier):
    """Check that we raise an error with when requesting OOB score with
    multiclass-multioutput classification target.
    """
    y = rng.randint(low=0, high=5, size=(iris.data.shape[0], 2))
    y_type = type_of_target(y)
    assert y_type == "multiclass-multioutput"
    estimator = ForestClassifier(oob_score=True, bootstrap=True)
    err_msg = "The type of target cannot be used to compute OOB estimates"

def test_forest_multioutput_integral_regression_target(ForestRegressor):
    """Check that multioutput regression with integral values is not interpreted
    as a multiclass-multioutput target and OOB score can be computed.
    """
    y = rng.randint(low=0, high=10, size=(iris.data.shape[0], 2))
    estimator = ForestRegressor(
        n_estimators=30, oob_score=True, bootstrap=True, random_state=0
    )

    n_samples_bootstrap = _get_n_samples_bootstrap(len(X), estimator.max_samples)
    n_samples_test = X.shape[0] // 4
    oob_pred = np.zeros([n_samples_test, 2])
    for sample_idx, sample in enumerate(X[:n_samples_test]):
        n_samples_oob = 0
        oob_pred_sample = np.zeros(2)
        for tree in estimator.estimators_:
            oob_unsampled_indices = _generate_unsampled_indices(
                tree.random_state, len(X), n_samples_bootstrap
            )
            if sample_idx in oob_unsampled_indices:
                n_samples_oob += 1
                oob_pred_sample += tree.predict(sample.reshape(1, -1)).squeeze()
        oob_pred[sample_idx] = oob_pred_sample / n_samples_oob
    assert_allclose(oob_pred, estimator.oob_prediction_[:n_samples_test])

def test_random_trees_embedding_raise_error_oob(oob_score):
    with pytest.raises(TypeError, match="got an unexpected keyword argument"):
        RandomTreesEmbedding(oob_score=oob_score)
    with pytest.raises(NotImplementedError, match="OOB score not supported"):
        RandomTreesEmbedding()._set_oob_score_and_attributes(X, y)

def test_gridsearch(name):
    # Check that base trees can be grid-searched.
    forest = FOREST_CLASSIFIERS[name]()
    clf = GridSearchCV(forest, {"n_estimators": (1, 2), "max_depth": (1, 2)})

def test_parallel(name):
    """Check parallel computations in classification"""
    if name in FOREST_CLASSIFIERS:
        X = iris.data
        y = iris.target
    elif name in FOREST_REGRESSORS:
        X = X_reg
        y = y_reg

    forest = ForestEstimator(n_estimators=10, n_jobs=3, random_state=0)

    forest.fit(X, y)
    assert len(forest) == 10

    forest.set_params(n_jobs=1)
    y1 = forest.predict(X)
    forest.set_params(n_jobs=2)
    y2 = forest.predict(X)
    assert_array_almost_equal(y1, y2, 3)

def test_pickle(name):
    # Check pickability.
        X = iris.data[::2]
        y = iris.target[::2]
        X = X_reg[::2]
        y = y_reg[::2]

    obj = ForestEstimator(random_state=0)
    obj.fit(X, y)
    score = obj.score(X, y)
    pickle_object = pickle.dumps(obj)

    obj2 = pickle.loads(pickle_object)
    assert type(obj2) == obj.__class__
    score2 = obj2.score(X, y)
    assert score == score2

def test_multioutput(name):
    # Check estimators on multi-output problems.

    X_train = [
        [-2, -1],
        [-1, -1],
        [-1, -2],
        [1, 1],
        [1, 2],
        [2, 1],
        [-2, 1],
        [-1, 1],
        [-1, 2],
        [2, -1],
        [1, -1],
        [1, -2],
    ]
    y_train = [
        [-1, 0],
        [-1, 0],
        [-1, 0],
        [1, 1],
        [1, 1],
        [1, 1],
        [-1, 2],
        [-1, 2],
        [-1, 2],
        [1, 3],
        [1, 3],
        [1, 3],
    ]
    X_test = [[-1, -1], [1, 1], [-1, 1], [1, -1]]
    y_test = [[-1, 0], [1, 1], [-1, 2], [1, 3]]

    est = FOREST_ESTIMATORS[name](random_state=0, bootstrap=False)
    y_pred = est.fit(X_train, y_train).predict(X_test)
    assert_array_almost_equal(y_pred, y_test)

        with np.errstate(divide="ignore"):
            proba = est.predict_proba(X_test)
            assert len(proba) == 2
            assert proba[0].shape == (4, 2)
            assert proba[1].shape == (4, 4)

            log_proba = est.predict_log_proba(X_test)
            assert len(log_proba) == 2
            assert log_proba[0].shape == (4, 2)
            assert log_proba[1].shape == (4, 4)

def test_multioutput_string(name):
    # Check estimators on multi-output problems with string outputs.

    X_train = [
        [-2, -1],
        [-1, -1],
        [-1, -2],
        [1, 1],
        [1, 2],
        [2, 1],
        [-2, 1],
        [-1, 1],
        [-1, 2],
        [2, -1],
        [1, -1],
        [1, -2],
    ]
    y_train = [
        ["red", "blue"],
        ["green", "green"],
        ["red", "purple"],
        ["green", "yellow"],
    ]
    y_test = [
    ]

    assert_array_equal(y_pred, y_test)

        proba = est.predict_proba(X_test)
        assert len(proba) == 2
        assert proba[0].shape == (4, 2)
        assert proba[1].shape == (4, 4)

        log_proba = est.predict_log_proba(X_test)
        assert len(log_proba) == 2
        assert log_proba[0].shape == (4, 2)
        assert log_proba[1].shape == (4, 4)

def test_classes_shape(name):
    # Test that n_classes_ and classes_ have proper shape.

    # Classification, single output
    clf = ForestClassifier(random_state=0).fit(X, y)

    assert clf.n_classes_ == 2
    assert_array_equal(clf.classes_, [-1, 1])

    # Classification, multi-output
    _y = np.vstack((y, np.array(y) * 2)).T
    clf = ForestClassifier(random_state=0).fit(X, _y)

    assert_array_equal(clf.n_classes_, [2, 2])
    assert_array_equal(clf.classes_, [[-1, 1], [-2, 2]])

def test_random_trees_dense_type():
    # Test that the `sparse_output` parameter of RandomTreesEmbedding
    # works by returning a dense array.

    # Create the RTE with sparse=False
    hasher = RandomTreesEmbedding(n_estimators=10, sparse_output=False)
    X, y = datasets.make_circles(factor=0.5)
    X_transformed = hasher.fit_transform(X)

    # Assert that type is ndarray, not scipy.sparse.csr_matrix
    assert isinstance(X_transformed, np.ndarray)

def test_random_trees_dense_equal():
    # works by returning the same array for both argument values.

    # Create the RTEs
    hasher_dense = RandomTreesEmbedding(
        n_estimators=10, sparse_output=False, random_state=0
    )
    hasher_sparse = RandomTreesEmbedding(
        n_estimators=10, sparse_output=True, random_state=0
    )
    X_transformed_dense = hasher_dense.fit_transform(X)
    X_transformed_sparse = hasher_sparse.fit_transform(X)

    # Assert that dense and sparse hashers have same array.
    assert_array_equal(X_transformed_sparse.toarray(), X_transformed_dense)

def test_random_hasher():
    # test random forest hashing on circles dataset
    # make sure that it is linearly separable.
    # even after projected to two SVD dimensions
    # Note: Not all random_states produce perfect results.
    hasher = RandomTreesEmbedding(n_estimators=30, random_state=1)

    # test fit and transform:
    assert_array_equal(hasher.fit(X).transform(X).toarray(), X_transformed.toarray())

    # one leaf active per data point per forest
    assert X_transformed.shape[0] == X.shape[0]
    assert_array_equal(X_transformed.sum(axis=1), hasher.n_estimators)
    svd = TruncatedSVD(n_components=2)
    X_reduced = svd.fit_transform(X_transformed)
    linear_clf = LinearSVC()
    linear_clf.fit(X_reduced, y)
    assert linear_clf.score(X_reduced, y) == 1.0

def test_random_hasher_sparse_data(csc_container):
    X, y = datasets.make_multilabel_classification(random_state=0)
    X_transformed_sparse = hasher.fit_transform(csc_container(X))
    assert_array_equal(X_transformed_sparse.toarray(), X_transformed.toarray())

def test_parallel_train():
    rng = check_random_state(12321)
    n_samples, n_features = 80, 30
    X_train = rng.randn(n_samples, n_features)
    y_train = rng.randint(0, 2, n_samples)

    clfs = [
        RandomForestClassifier(n_estimators=20, n_jobs=n_jobs, random_state=12345).fit(
            X_train, y_train
        )
        for n_jobs in [1, 2, 3, 8, 16, 32]
    ]

    X_test = rng.randn(n_samples, n_features)
    probas = [clf.predict_proba(X_test) for clf in clfs]
    for proba1, proba2 in itertools.pairwise(probas):
        assert_array_almost_equal(proba1, proba2)

def test_distribution():

    # Single variable with 4 values
    X = rng.randint(0, 4, size=(1000, 1))
    y = rng.rand(1000)
    n_trees = 500

    reg = ExtraTreesRegressor(n_estimators=n_trees, random_state=42).fit(X, y)

    uniques = defaultdict(int)
    for tree in reg.estimators_:
        tree = "".join(
            ("%d,%d/" % (f, int(t)) if f >= 0 else "-")
            for f, t in zip(tree.tree_.feature, tree.tree_.threshold)
        )

        uniques[tree] += 1

    uniques = sorted([(1.0 * count / n_trees, tree) for tree, count in uniques.items()])

    # On a single variable problem where X_0 has 4 equiprobable values, there
    # are 5 ways to build a random tree. The more compact (0,1/0,0/--0,2/--) of
    # them has probability 1/3 while the 4 others have probability 1/6.

    assert len(uniques) == 5
    assert 0.20 > uniques[0][0]  # Rough approximation of 1/6.
    assert 0.20 > uniques[1][0]
    assert 0.20 > uniques[2][0]
    assert 0.20 > uniques[3][0]
    assert uniques[4][0] > 0.3
    assert uniques[4][1] == "0,1/0,0/--0,2/--"

    # Two variables, one with 2 values, one with 3 values
    X = np.empty((1000, 2))
    X[:, 0] = np.random.randint(0, 2, 1000)
    X[:, 1] = np.random.randint(0, 3, 1000)

    reg = ExtraTreesRegressor(max_features=1, random_state=1).fit(X, y)

        )

    uniques = [(count, tree) for tree, count in uniques.items()]
    assert len(uniques) == 8

def test_max_leaf_nodes_max_depth(name):
    X, y = hastie_X, hastie_y

    # Test precedence of max_leaf_nodes over max_depth.
    est = ForestEstimator(
        max_depth=1, max_leaf_nodes=4, n_estimators=1, random_state=0
    assert est.estimators_[0].get_depth() == 1

    est = ForestEstimator(max_depth=1, n_estimators=1, random_state=0).fit(X, y)

def test_min_samples_split(name):

    est = ForestEstimator(min_samples_split=10, n_estimators=1, random_state=0)
    node_idx = est.estimators_[0].tree_.children_left != -1
    node_samples = est.estimators_[0].tree_.n_node_samples[node_idx]

    assert np.min(node_samples) > len(X) * 0.5 - 1, "Failed with {0}".format(name)

    est = ForestEstimator(min_samples_split=0.5, n_estimators=1, random_state=0)

def test_min_samples_leaf(name):

    # Test if leaves contain more than leaf_count training examples

    est = ForestEstimator(min_samples_leaf=5, n_estimators=1, random_state=0)
    out = est.estimators_[0].tree_.apply(X)
    node_counts = np.bincount(out)
    # drop inner nodes
    leaf_count = node_counts[node_counts != 0]
    assert np.min(leaf_count) > 4, "Failed with {0}".format(name)

    est = ForestEstimator(min_samples_leaf=0.25, n_estimators=1, random_state=0)
    assert np.min(leaf_count) > len(X) * 0.25 - 1, "Failed with {0}".format(name)

def test_min_weight_fraction_leaf(name):

    # Test if leaves contain at least min_weight_fraction_leaf of the
    # training set
    weights = rng.rand(X.shape[0])
    total_weight = np.sum(weights)

    # test both DepthFirstTreeBuilder and BestFirstTreeBuilder
    # by setting max_leaf_nodes
    for frac in np.linspace(0, 0.5, 6):
        est = ForestEstimator(
            min_weight_fraction_leaf=frac, n_estimators=1, random_state=0
        )
        if "RandomForest" in name:
            est.bootstrap = False

        est.fit(X, y, sample_weight=weights)
        out = est.estimators_[0].tree_.apply(X)
        node_weights = np.bincount(out, weights=weights)
        # drop inner nodes
        leaf_weights = node_weights[node_weights != 0]
        assert np.min(leaf_weights) >= total_weight * est.min_weight_fraction_leaf, (
            "Failed with {0} min_weight_fraction_leaf={1}".format(
                name, est.min_weight_fraction_leaf
            )
        )

def test_sparse_input(name, sparse_container):
    X, y = datasets.make_multilabel_classification(random_state=0, n_samples=50)

    dense = ForestEstimator(random_state=0, max_depth=2).fit(X, y)
    sparse = ForestEstimator(random_state=0, max_depth=2).fit(sparse_container(X), y)

    assert_array_almost_equal(sparse.apply(X), dense.apply(X))

    if name in FOREST_CLASSIFIERS or name in FOREST_REGRESSORS:
        assert_array_almost_equal(sparse.predict(X), dense.predict(X))
            sparse.feature_importances_, dense.feature_importances_
        )

        assert_array_almost_equal(sparse.predict_proba(X), dense.predict_proba(X))
            sparse.predict_log_proba(X), dense.predict_log_proba(X)
        )

    if name in FOREST_TRANSFORMERS:
            sparse.transform(X).toarray(), dense.transform(X).toarray()
        )
            sparse.fit_transform(X).toarray(), dense.fit_transform(X).toarray()
        )

def test_memory_layout(name, dtype):
    # Test that it works no matter the memory layout

    # Dense
    for container, kwargs in (
        (np.asarray, {}),  # Nothing
        (np.asarray, {"order": "C"}),  # C-order
        (np.asarray, {"order": "F"}),  # F-order
        (np.ascontiguousarray, {}),  # Contiguous
    ):
        X = container(iris.data, dtype=dtype, **kwargs)
        assert_array_almost_equal(est.fit(X, y).predict(X), y)

    # Sparse (if applicable)
    if est.estimator.splitter in SPARSE_SPLITTERS:
        for sparse_container in COO_CONTAINERS + CSC_CONTAINERS + CSR_CONTAINERS:
            X = sparse_container(iris.data, dtype=dtype)
            y = iris.target
            assert_array_almost_equal(est.fit(X, y).predict(X), y)

    # Strided
    X = np.asarray(iris.data[::3], dtype=dtype)
    y = iris.target[::3]
    assert_array_almost_equal(est.fit(X, y).predict(X), y)

def test_1d_input(name):
    X = iris.data[:, 0]
    X_2d = iris.data[:, 0].reshape((-1, 1))

    with ignore_warnings():
        ForestEstimator = FOREST_ESTIMATORS[name]
        with pytest.raises(ValueError):
            ForestEstimator(n_estimators=1, random_state=0).fit(X, y)

        est = ForestEstimator(random_state=0)
        est.fit(X_2d, y)

        if name in FOREST_CLASSIFIERS or name in FOREST_REGRESSORS:
            with pytest.raises(ValueError):
                est.predict(X)

def test_class_weights(name):
    # Check class_weights resemble sample_weights behavior.

    # Iris is balanced, so no effect expected for using 'balanced' weights
    clf1 = ForestClassifier(random_state=0)
    clf1.fit(iris.data, iris.target)
    clf2 = ForestClassifier(class_weight="balanced", random_state=0)
    clf2.fit(iris.data, iris.target)
    assert_almost_equal(clf1.feature_importances_, clf2.feature_importances_)

    # Make a multi-output problem with three copies of Iris
    iris_multi = np.vstack((iris.target, iris.target, iris.target)).T
    # Create user-defined weights that should balance over the outputs
    clf3 = ForestClassifier(
        class_weight=[
            {0: 2.0, 1: 2.0, 2: 1.0},
            {0: 2.0, 1: 1.0, 2: 2.0},
            {0: 1.0, 1: 2.0, 2: 2.0},
        ],
    )
    clf3.fit(iris.data, iris_multi)
    assert_almost_equal(clf2.feature_importances_, clf3.feature_importances_)
    # Check against multi-output "balanced" which should also have no effect
    clf4 = ForestClassifier(class_weight="balanced", random_state=0)
    clf4.fit(iris.data, iris_multi)
    assert_almost_equal(clf3.feature_importances_, clf4.feature_importances_)

    # Inflate importance of class 1, check against user-defined weights
    sample_weight = np.ones(iris.target.shape)
    sample_weight[iris.target == 1] *= 100
    class_weight = {0: 1.0, 1: 100.0, 2: 1.0}
    clf1.fit(iris.data, iris.target, sample_weight)
    clf2 = ForestClassifier(class_weight=class_weight, random_state=0)

    clf1.fit(iris.data, iris.target, sample_weight**2)
    clf2.fit(iris.data, iris.target, sample_weight)

def test_class_weight_balanced_and_bootstrap_multi_output(name):
    # Test class_weight works for multi-output"""
    clf = ForestClassifier(class_weight="balanced", random_state=0)
    clf.fit(X, _y)
        class_weight=[{-1: 0.5, 1: 1.0}, {-2: 1.0, 2: 1.0}], random_state=0
    )
    # smoke test for balanced subsample
    clf = ForestClassifier(class_weight="balanced_subsample", random_state=0)

def test_class_weight_errors(name):
    # Test if class_weight raises errors and warnings when expected.

    # Warning warm_start with preset
    clf = ForestClassifier(class_weight="balanced", warm_start=True, random_state=0)

    warn_msg = (
        "Warm-start fitting without increasing n_estimators does not fit new trees."
    )
        clf.fit(X, _y)

    # Incorrect length list for multi-output
    clf = ForestClassifier(class_weight=[{-1: 0.5, 1: 1.0}], random_state=0)
    with pytest.raises(ValueError):

def test_warm_start(name):
    # Test if fitting incrementally with warm start gives a forest of the
    # right size and the same results as a normal fit.
    est_ws = None
    for n_estimators in [5, 10]:
        if est_ws is None:
            est_ws = ForestEstimator(
                n_estimators=n_estimators, random_state=42, warm_start=True
            )
        else:
            est_ws.set_params(n_estimators=n_estimators)
        est_ws.fit(X, y)
        assert len(est_ws) == n_estimators

    est_no_ws = ForestEstimator(n_estimators=10, random_state=42, warm_start=False)
    est_no_ws.fit(X, y)

    assert set([tree.random_state for tree in est_ws]) == set(
        [tree.random_state for tree in est_no_ws]
    )

        est_ws.apply(X), est_no_ws.apply(X), err_msg="Failed with {0}".format(name)
    )

def test_warm_start_clear(name):
    # Test if fit clears state and grows a new forest when warm_start==False.
    est = ForestEstimator(n_estimators=5, max_depth=1, warm_start=False, random_state=1)

    est_2 = ForestEstimator(
        n_estimators=5, max_depth=1, warm_start=True, random_state=2
    )
    est_2.fit(X, y)  # inits state
    est_2.set_params(warm_start=False, random_state=1)
    est_2.fit(X, y)  # clears old state and equals est

    assert_array_almost_equal(est_2.apply(X), est.apply(X))

def test_warm_start_smaller_n_estimators(name):
    # Test if warm start second fit with smaller n_estimators raises error.
    est = ForestEstimator(n_estimators=5, max_depth=1, warm_start=True)
    est.set_params(n_estimators=4)

def test_warm_start_equal_n_estimators(name):
    # Test if warm start with equal n_estimators does nothing and returns the
    # same forest and raises a warning.
    est = ForestEstimator(n_estimators=5, max_depth=3, warm_start=True, random_state=1)

        n_estimators=5, max_depth=3, warm_start=True, random_state=1
    )
    est_2.fit(X, y)
    # Now est_2 equals est.

    est_2.set_params(random_state=2)
    )
        est_2.fit(X, y)
    # If we had fit the trees again we would have got a different forest as we
    # changed the random state.
    assert_array_equal(est.apply(X), est_2.apply(X))

def test_warm_start_oob(name):
    # Test that the warm start computes oob score when asked.
    # Use 15 estimators to avoid 'some inputs do not have OOB scores' warning.
        n_estimators=15,
        random_state=1,
    )

        n_estimators=5,
    )

    est_2.set_params(warm_start=True, oob_score=True, n_estimators=15)

    assert hasattr(est_2, "oob_score_")
    assert est.oob_score_ == est_2.oob_score_

    # Test that oob_score is computed even if we don't need to train
    # additional trees.
    est_3 = ForestEstimator(
    )
    est_3.fit(X, y)
    assert not hasattr(est_3, "oob_score_")

    est_3.set_params(oob_score=True)
    ignore_warnings(est_3.fit)(X, y)

    assert est.oob_score_ == est_3.oob_score_

def test_oob_not_computed_twice(name):
    # Check that oob_score is not computed twice when warm_start=True.

        n_estimators=10, warm_start=True, bootstrap=True, oob_score=True
    )

    with patch.object(
        est, "_set_oob_score_and_attributes", wraps=est._set_oob_score_and_attributes
    ) as mock_set_oob_score_and_attributes:

        with pytest.warns(UserWarning, match="Warm-start fitting without increasing"):
            est.fit(X, y)

        mock_set_oob_score_and_attributes.assert_called_once()

def test_dtype_convert(n_classes=15):
    classifier = RandomForestClassifier(random_state=0, bootstrap=False)

    X = np.eye(n_classes)
    y = [ch for ch in "ABCDEFGHIJKLMNOPQRSTU"[:n_classes]]

    result = classifier.fit(X, y).predict(X)
    assert_array_equal(classifier.classes_, y)
    assert_array_equal(result, y)

def test_decision_path(name):
    indicator, n_nodes_ptr = est.decision_path(X)

    assert indicator.shape[1] == n_nodes_ptr[-1]
    assert indicator.shape[0] == n_samples
        np.diff(n_nodes_ptr), [e.tree_.node_count for e in est.estimators_]
    )

    # Assert that leaves index are correct
    leaves = est.apply(X)
    for est_id in range(leaves.shape[1]):
        leave_indicator = [
            indicator[i, n_nodes_ptr[est_id] + j]
            for i, j in enumerate(leaves[:, est_id])
        ]
        assert_array_almost_equal(leave_indicator, np.ones(shape=n_samples))

def test_min_impurity_decrease():
    X, y = datasets.make_hastie_10_2(n_samples=100, random_state=1)
    all_estimators = [
        RandomForestClassifier,
        RandomForestRegressor,
        ExtraTreesClassifier,
        ExtraTreesRegressor,
    ]

    for Estimator in all_estimators:
        est = Estimator(min_impurity_decrease=0.1)
        for tree in est.estimators_:
            # Simply check if the parameter is passed on correctly. Tree tests
            # will suffice for the actual working of this param
            assert tree.min_impurity_decrease == 0.1

def test_poisson_y_positive_check():
    est = RandomForestRegressor(criterion="poisson")
    X = np.zeros((3, 3))

    y = [-1, 1, 3]
    err_msg = (
        r"Some value\(s\) of y are negative which is "
        r"not allowed for Poisson regression."
    )

    y = [0, 0, 0]
    err_msg = (
        r"Sum of y is not strictly positive which "
        r"is necessary for Poisson regression."
    )

class MyBackend(DEFAULT_JOBLIB_BACKEND):  # type: ignore[valid-type,misc]
    def __init__(self, *args, **kwargs):
        self.count = 0
        super().__init__(*args, **kwargs)

    def start_call(self):
        self.count += 1
        return super().start_call()

def test_backend_respected():
    clf = RandomForestClassifier(n_estimators=10, n_jobs=2)

    with joblib.parallel_backend("testing") as (ba, n_jobs):
        clf.fit(X, y)

    assert ba.count > 0

    # predict_proba requires shared memory. Ensure that's honored.
    with joblib.parallel_backend("testing") as (ba, _):
        clf.predict_proba(X)

    assert ba.count == 0

def test_forest_feature_importances_sum():
        n_samples=15, n_informative=3, random_state=1, n_classes=3
    )
    clf = RandomForestClassifier(
        min_samples_leaf=5, random_state=42, n_estimators=200
    assert math.isclose(1, clf.feature_importances_.sum(), abs_tol=1e-7)

def test_forest_degenerate_feature_importances():
    # build a forest of single node trees. See #13636
    X = np.zeros((10, 10))
    y = np.ones((10,))
    gbr = RandomForestRegressor(n_estimators=10).fit(X, y)
    assert_array_equal(gbr.feature_importances_, np.zeros(10, dtype=np.float64))

def test_max_samples_bootstrap(name):
    # Check invalid `max_samples` values
    est = FOREST_CLASSIFIERS_REGRESSORS[name](bootstrap=False, max_samples=0.5)
    err_msg = (
        r"`max_sample` cannot be set if `bootstrap=False`. "
        r"Either switch to `bootstrap=True` or set "
        r"`max_sample=None`."
    )

def test_large_max_samples_exception(name):
    # Check invalid `max_samples`
    est = FOREST_CLASSIFIERS_REGRESSORS[name](bootstrap=True, max_samples=int(1e9))
    match = "`max_samples` must be <= n_samples=6 but got value 1000000000"

def test_max_samples_boundary_regressors(name):
        X_reg, y_reg, train_size=0.7, test_size=0.3, random_state=0
    )

    ms_1_model = FOREST_REGRESSORS[name](
        bootstrap=True, max_samples=1.0, random_state=0
    )
    ms_1_predict = ms_1_model.fit(X_train, y_train).predict(X_test)

    ms_None_model = FOREST_REGRESSORS[name](
        bootstrap=True, max_samples=None, random_state=0
    )
    ms_None_predict = ms_None_model.fit(X_train, y_train).predict(X_test)

    ms_1_ms = mean_squared_error(ms_1_predict, y_test)
    ms_None_ms = mean_squared_error(ms_None_predict, y_test)

    assert ms_1_ms == pytest.approx(ms_None_ms)

def test_max_samples_boundary_classifiers(name):
        X_large, y_large, random_state=0, stratify=y_large
    )

    ms_1_model = FOREST_CLASSIFIERS[name](
    )
    ms_1_proba = ms_1_model.fit(X_train, y_train).predict_proba(X_test)

    ms_None_model = FOREST_CLASSIFIERS[name](
    )
    ms_None_proba = ms_None_model.fit(X_train, y_train).predict_proba(X_test)

    np.testing.assert_allclose(ms_1_proba, ms_None_proba)

def test_forest_y_sparse(csr_container):
    X = [[1, 2, 3]]
    y = csr_container([[4, 5, 6]])
    est = RandomForestClassifier()
    msg = "sparse multilabel-indicator for y is not supported."

def test_little_tree_with_small_max_samples(ForestClass):
    rng = np.random.RandomState(1)

    X = rng.randn(10000, 2)
    y = rng.randn(10000) > 0

    # First fit with no restriction on max samples
    est1 = ForestClass(
    )

    # Second fit with max samples restricted to just 2
    est2 = ForestClass(
        max_samples=2,
    )

    est1.fit(X, y)
    est2.fit(X, y)

    tree1 = est1.estimators_[0].tree_
    tree2 = est2.estimators_[0].tree_

    msg = "Tree without `max_samples` restriction should have more nodes"
    assert tree1.node_count > tree2.node_count, msg

def test_mse_criterion_object_segfault_smoke_test(Forest):
    # This is a smoke test to ensure that passing a mutable criterion
    # does not cause a segfault when fitting with concurrent threads.
    # Non-regression test for:
    # https://github.com/scikit-learn/scikit-learn/issues/12623
    from sklearn.tree._criterion import MSE

    y = y_reg.reshape(-1, 1)
    n_samples, n_outputs = y.shape
    mse_criterion = MSE(n_outputs, n_samples)
    est = FOREST_REGRESSORS[Forest](n_estimators=2, n_jobs=2, criterion=mse_criterion)

    est.fit(X_reg, y)

def test_random_trees_embedding_feature_names_out():
    """Check feature names out for Random Trees Embedding."""
    random_state = np.random.RandomState(0)
    X = np.abs(random_state.randn(100, 4))
    hasher = RandomTreesEmbedding(
        n_estimators=2, max_depth=2, sparse_output=False, random_state=0
    ).fit(X)
    names = hasher.get_feature_names_out()
    expected_names = [
        f"randomtreesembedding_{tree}_{leaf}"
        # Note: nodes with indices 0, 1 and 4 are internal split nodes and
        # therefore do not appear in the expected output feature names.
        for tree, leaf in [
            (0, 2),
            (0, 3),
            (0, 5),
            (0, 6),
            (1, 2),
            (1, 3),
            (1, 5),
            (1, 6),
        ]
    ]
    assert_array_equal(expected_names, names)

def test_read_only_buffer(csr_container, monkeypatch):
    """RandomForestClassifier must work on readonly sparse data.

    Non-regression test for: https://github.com/scikit-learn/scikit-learn/issues/25333
    """
        sklearn.ensemble._forest,
        "Parallel",
        partial(Parallel, max_nbytes=100),
    )

    X, y = make_classification(n_samples=100, n_features=200, random_state=rng)
    X = csr_container(X, copy=True)

    clf = RandomForestClassifier(n_jobs=2, random_state=rng)
    cross_val_score(clf, X, y, cv=2)

def test_round_samples_to_one_when_samples_too_low(class_weight):
    """Check low max_samples works and is rounded to one.

    Non-regression test for gh-24037.
    """
    X, y = datasets.load_wine(return_X_y=True)
    forest = RandomForestClassifier(
        n_estimators=10, max_samples=1e-4, class_weight=class_weight, random_state=0
    )

def test_estimators_samples(ForestClass, bootstrap, seed):
    """Estimators_samples_ property should be consistent.

    Tests consistency across fits and whether or not the seed for the random generator
    is set.
    """
    X, y = make_hastie_10_2(n_samples=200, random_state=1)

        max_samples = 0.5
    else:
        max_samples = None
    est = ForestClass(
        max_samples=max_samples,
        max_features=0.5,
        random_state=seed,
        bootstrap=bootstrap,
    )

    estimators_samples = est.estimators_samples_.copy()

    # Test repeated calls result in same set of indices
    assert_array_equal(estimators_samples, est.estimators_samples_)
    estimators = est.estimators_

    assert isinstance(estimators_samples, list)
    assert len(estimators_samples) == len(estimators)
    assert estimators_samples[0].dtype == np.int32

    for i in range(len(estimators)):
        if bootstrap:
            assert len(estimators_samples[i]) == len(X) // 2

            # the bootstrap should be a resampling with replacement
            assert len(np.unique(estimators_samples[i])) < len(estimators_samples[i])
        else:
            assert len(set(estimators_samples[i])) == len(X)

    estimator_index = 0
    estimator_samples = estimators_samples[estimator_index]
    estimator = estimators[estimator_index]

    X_train = X[estimator_samples]
    y_train = y[estimator_samples]

    orig_tree_values = estimator.tree_.value
    estimator.fit(X_train, y_train)
    new_tree_values = estimator.tree_.value
    assert_allclose(orig_tree_values, new_tree_values)

def test_missing_values_is_resilient(make_data, Forest):
    """Check that forest can deal with missing values and has decent performance."""

    n_samples, n_features = 1000, 10
    X, y = make_data(n_samples=n_samples, n_features=n_features, random_state=rng)

    # Create dataset with missing values
    X_missing = X.copy()
    X_missing[rng.choice([False, True], size=X.shape, p=[0.95, 0.05])] = np.nan
    assert np.isnan(X_missing).any()

    X_missing_train, X_missing_test, y_train, y_test = train_test_split(
        X_missing, y, random_state=0
    )

    # Train forest with missing values
    forest_with_missing = Forest(random_state=rng, n_estimators=50)
    forest_with_missing.fit(X_missing_train, y_train)
    score_with_missing = forest_with_missing.score(X_missing_test, y_test)

    # Train forest without missing values
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
    forest = Forest(random_state=rng, n_estimators=50)
    forest.fit(X_train, y_train)
    score_without_missing = forest.score(X_test, y_test)

    # Score is still 80 percent of the forest's score that had no missing values
    assert score_with_missing >= 0.80 * score_without_missing

def test_missing_value_is_predictive(Forest):
    """Check that the forest learns when missing values are only present for
    a predictive feature."""
    n_samples = 300
    expected_score = 0.75

    X_non_predictive = rng.standard_normal(size=(n_samples, 10))
    y = rng.randint(0, high=2, size=n_samples)

    # Create a predictive feature using `y` and with some noise
    X_random_mask = rng.choice([False, True], size=n_samples, p=[0.95, 0.05])
    y_mask = y.astype(bool)
    y_mask[X_random_mask] = ~y_mask[X_random_mask]

    predictive_feature = rng.standard_normal(size=n_samples)
    predictive_feature[y_mask] = np.nan
    assert np.isnan(predictive_feature).any()

    X_predictive = X_non_predictive.copy()
    X_predictive[:, 5] = predictive_feature

    (
        X_predictive_train,
        X_predictive_test,
        X_non_predictive_train,
        X_non_predictive_test,
        y_train,
        y_test,
    ) = train_test_split(X_predictive, X_non_predictive, y, random_state=0)
    forest_predictive = Forest(random_state=0).fit(X_predictive_train, y_train)
    forest_non_predictive = Forest(random_state=0).fit(X_non_predictive_train, y_train)

    predictive_test_score = forest_predictive.score(X_predictive_test, y_test)

    assert predictive_test_score >= expected_score
    assert predictive_test_score >= forest_non_predictive.score(
        X_non_predictive_test, y_test
    )

def test_non_supported_criterion_raises_error_with_missing_values(Forest):
    """Raise error for unsupported criterion when there are missing values."""
    X = np.array([[0, 1, 2], [np.nan, 0, 2.0]])
    y = [0.5, 1.0]

    forest = Forest(criterion="absolute_error")

    msg = ".*does not accept missing values"
        forest.fit(X, y)

            if p > 0:

        imp = 0.0

                        imp += (
                            coef
                            * (
                                - sum(
                                    [
                                    ]
                                )
                            )
                        )

        return imp

def test_exponential_n_classes_gt_2():
    """Test exponential loss raises for n_classes > 2."""
    clf = GradientBoostingClassifier(loss="exponential")
    msg = "loss='exponential' is only suitable for a binary classification"

def test_raise_if_init_has_no_predict_proba():
    """Test raise if init_ has no predict_proba method."""
    clf = GradientBoostingClassifier(init=GradientBoostingRegressor)
    msg = (
        "The 'init' parameter of GradientBoostingClassifier must be a str among "
        "{'zero'}, None or an object implementing 'fit' and 'predict_proba'."
    )

def test_classification_toy(loss, global_random_seed):
    # Check classification on a toy dataset.
    clf = GradientBoostingClassifier(
        loss=loss, n_estimators=10, random_state=global_random_seed
    )

        clf.predict(T)

    assert 10 == len(clf.estimators_)

    log_loss_decrease = clf.train_score_[:-1] - clf.train_score_[1:]
    assert np.any(log_loss_decrease >= 0.0)

    leaves = clf.apply(X)
    assert leaves.shape == (6, 10, 1)

def test_classification_synthetic(loss, global_random_seed):
    # Test GradientBoostingClassifier on synthetic dataset used by
    # Hastie et al. in ESLII - Figure 10.9
    # Note that Figure 10.9 reuses the dataset generated for figure 10.2
    # and should have 2_000 train data points and 10_000 test data points.
    # Here we intentionally use a smaller variant to make the test run faster,
    # but the conclusions are still the same, despite the smaller datasets.
    X, y = datasets.make_hastie_10_2(n_samples=2000, random_state=global_random_seed)

    split_idx = 500
    X_train, X_test = X[:split_idx], X[split_idx:]
    y_train, y_test = y[:split_idx], y[split_idx:]

    # Increasing the number of trees should decrease the test error
    common_params = {
        "max_depth": 1,
        "learning_rate": 1.0,
        "loss": loss,
        "random_state": global_random_seed,
    }
    gbrt_10_stumps = GradientBoostingClassifier(n_estimators=10, **common_params)
    gbrt_10_stumps.fit(X_train, y_train)

    gbrt_50_stumps = GradientBoostingClassifier(n_estimators=50, **common_params)
    gbrt_50_stumps.fit(X_train, y_train)

    assert gbrt_10_stumps.score(X_test, y_test) < gbrt_50_stumps.score(X_test, y_test)

    # Decision stumps are better suited for this dataset with a large number of
    # estimators.
        "n_estimators": 200,
    }
    gbrt_stumps = GradientBoostingClassifier(max_depth=1, **common_params)
    gbrt_stumps.fit(X_train, y_train)

    gbrt_10_nodes = GradientBoostingClassifier(max_leaf_nodes=10, **common_params)
    gbrt_10_nodes.fit(X_train, y_train)

    assert gbrt_stumps.score(X_test, y_test) > gbrt_10_nodes.score(X_test, y_test)

def test_regression_dataset(loss, subsample, global_random_seed):
    # Check consistency on regression dataset with least squares
    # and least absolute deviation.
    ones = np.ones(len(y_reg))
    last_y_pred = None
    for sample_weight in [None, ones, 2 * ones]:
        # learning_rate, max_depth and n_estimators were adjusted to get a mode
        # that is accurate enough to reach a low MSE on the training set while
        # keeping the resource used to execute this test low enough.
        reg = GradientBoostingRegressor(
            n_estimators=30,
            max_depth=4,
            min_samples_split=2,
            random_state=global_random_seed,
            learning_rate=0.5,
        )

        reg.fit(X_reg, y_reg, sample_weight=sample_weight)
        leaves = reg.apply(X_reg)
        assert leaves.shape == (100, 30)

        y_pred = reg.predict(X_reg)
        mse = mean_squared_error(y_reg, y_pred)
        assert mse < 0.05

        if last_y_pred is not None:
            # FIXME: We temporarily bypass this test. This is due to the fact
            # that GBRT with and without `sample_weight` do not use the same
            # implementation of the median during the initialization with the
            # `DummyRegressor`. In the future, we should make sure that both
            # implementations should be the same. See PR #17377 for more.
            # assert_allclose(last_y_pred, y_pred)
            pass

        last_y_pred = y_pred

def test_iris(subsample, sample_weight, global_random_seed):
    if sample_weight == 1:
        sample_weight = np.ones(len(iris.target))
        random_state=global_random_seed,
        subsample=subsample,
    )
    clf.fit(iris.data, iris.target, sample_weight=sample_weight)
    assert score > 0.9

    leaves = clf.apply(iris.data)
    assert leaves.shape == (150, 100, 3)

def test_regression_synthetic(global_random_seed):
    # Test on synthetic regression datasets used in Leo Breiman,
    # `Bagging Predictors?. Machine Learning 24(2): 123-140 (1996).
    random_state = check_random_state(global_random_seed)
    regression_params = {
        "n_estimators": 100,
        "max_depth": 4,
        "min_samples_split": 2,
        "learning_rate": 0.1,
        "loss": "squared_error",
    }

    # Friedman1
    X, y = datasets.make_friedman1(n_samples=1200, random_state=random_state, noise=1.0)
    X_train, y_train = X[:200], y[:200]
    X_test, y_test = X[200:], y[200:]

    clf = GradientBoostingRegressor(**regression_params)
    mse = mean_squared_error(y_test, clf.predict(X_test))
    assert mse < 6.5

    # Friedman2
    X, y = datasets.make_friedman2(n_samples=1200, random_state=random_state)

    assert mse < 2500.0

    # Friedman3
    X, y = datasets.make_friedman3(n_samples=1200, random_state=random_state)

    assert mse < 0.025

def test_feature_importances(GradientBoosting, X, y):
    # smoke test to check that the gradient boosting expose an attribute
    # feature_importances_
    gbdt = GradientBoosting()
    assert not hasattr(gbdt, "feature_importances_")
    assert hasattr(gbdt, "feature_importances_")

def test_probability_log(global_random_seed):
    clf = GradientBoostingClassifier(n_estimators=100, random_state=global_random_seed)

        clf.predict_proba(T)

    # check if probabilities are in [0, 1].
    y_proba = clf.predict_proba(T)
    assert np.all(y_proba >= 0.0)
    assert np.all(y_proba <= 1.0)

    # derive predictions from probabilities
    y_pred = clf.classes_.take(y_proba.argmax(axis=1), axis=0)
    assert_array_equal(y_pred, true_result)

def test_single_class_with_sample_weight():
    sample_weight = [0, 0, 0, 1, 1, 1]
    clf = GradientBoostingClassifier(n_estimators=100, random_state=1)
    msg = (
        "y contains 1 class after sample_weight trimmed classes with "
        "zero weights, while a minimum of 2 classes are required."
    )
        clf.fit(X, y, sample_weight=sample_weight)

def test_check_inputs_predict_stages(csc_container):
    # check that predict_stages through an error if the type of X is not
    # supported
    x, y = datasets.make_hastie_10_2(n_samples=100, random_state=1)
    x_sparse_csc = csc_container(x)
    clf.fit(x, y)
    score = np.zeros((y.shape)).reshape(-1, 1)
    err_msg = "When X is a sparse matrix, a CSR format is expected"
        predict_stages(clf.estimators_, x_sparse_csc, clf.learning_rate, score)
    x_fortran = np.asfortranarray(x)
    with pytest.raises(ValueError, match="X should be C-ordered np.ndarray"):
        predict_stages(clf.estimators_, x_fortran, clf.learning_rate, score)

def test_max_feature_regression(global_random_seed):
    # Test to make sure random state is set properly.
    X, y = datasets.make_hastie_10_2(n_samples=12000, random_state=global_random_seed)

    X_train, X_test = X[:2000], X[2000:]
    y_train, y_test = y[:2000], y[2000:]

    gbrt = GradientBoostingClassifier(
        min_samples_split=5,
        max_features=2,
    )
    gbrt.fit(X_train, y_train)
    log_loss = gbrt._loss(y_test, gbrt.decision_function(X_test))
    assert log_loss < 0.5, "GB failed with deviance %.4f" % log_loss

def test_feature_importance_regression(
    fetch_california_housing_fxt, global_random_seed
):
    """Test that Gini importance is calculated correctly.

    This test follows the example from [1]_ (pg. 373).

    .. [1] Friedman, J., Hastie, T., & Tibshirani, R. (2001). The elements
       of statistical learning. New York: Springer series in statistics.
    """
    california = fetch_california_housing_fxt()
    X, y = california.data, california.target
        X, y, random_state=global_random_seed
    )

    reg = GradientBoostingRegressor(
        loss="huber",
        max_leaf_nodes=6,
    )
    sorted_idx = np.argsort(reg.feature_importances_)[::-1]
    sorted_features = [california.feature_names[s] for s in sorted_idx]

    # The most important feature is the median income by far.
    assert sorted_features[0] == "MedInc"

    # The three subsequent features are the following. Their relative ordering
    # might change a bit depending on the randomness of the trees and the
    # train / test split.
    assert set(sorted_features[1:4]) == {"Longitude", "AveOccup", "Latitude"}

def test_max_features():
    # Test if max features is set properly for floats and str.
    X, y = datasets.make_hastie_10_2(n_samples=12000, random_state=1)
    _, n_features = X.shape

    X_train = X[:2000]
    y_train = y[:2000]

    gbrt = GradientBoostingClassifier(n_estimators=1, max_features=None)
    assert gbrt.max_features_ == n_features

    gbrt = GradientBoostingRegressor(n_estimators=1, max_features=None)

    gbrt = GradientBoostingRegressor(n_estimators=1, max_features=0.3)
    assert gbrt.max_features_ == int(n_features * 0.3)

    gbrt = GradientBoostingRegressor(n_estimators=1, max_features="sqrt")
    assert gbrt.max_features_ == int(np.sqrt(n_features))

    gbrt = GradientBoostingRegressor(n_estimators=1, max_features="log2")
    assert gbrt.max_features_ == int(np.log2(n_features))

    gbrt = GradientBoostingRegressor(n_estimators=1, max_features=0.01 / X.shape[1])
    assert gbrt.max_features_ == 1

def test_staged_predict():
    # Test whether staged decision function eventually gives
    X, y = datasets.make_friedman1(n_samples=1200, random_state=1, noise=1.0)
    X_test = X[200:]
    clf = GradientBoostingRegressor()
    # test raise ValueError if not fitted
        np.fromiter(clf.staged_predict(X_test), dtype=np.float64)

    # test if prediction for last stage equals ``predict``
    for y in clf.staged_predict(X_test):
        assert y.shape == y_pred.shape

    assert_array_almost_equal(y_pred, y)

def test_staged_predict_proba():
    # Test whether staged predict proba eventually gives
    X, y = datasets.make_hastie_10_2(n_samples=1200, random_state=1)
    clf = GradientBoostingClassifier(n_estimators=20)
    # test raise NotFittedError if not
        np.fromiter(clf.staged_predict_proba(X_test), dtype=np.float64)

    for y_pred in clf.staged_predict(X_test):
        assert y_test.shape == y_pred.shape

    assert_array_equal(clf.predict(X_test), y_pred)

    # test if prediction for last stage equals ``predict_proba``
    for staged_proba in clf.staged_predict_proba(X_test):
        assert y_test.shape[0] == staged_proba.shape[0]
        assert 2 == staged_proba.shape[1]

    assert_array_almost_equal(clf.predict_proba(X_test), staged_proba)

def test_staged_functions_defensive(Estimator, global_random_seed):
    # test that staged_functions make defensive copies
    X = rng.uniform(size=(10, 3))
    y = (4 * X[:, 0]).astype(int) + 1  # don't predict zeros
    estimator = Estimator()
    for func in ["predict", "decision_function", "predict_proba"]:
        staged_func = getattr(estimator, "staged_" + func, None)
        if staged_func is None:
            # regressor has no staged_predict_proba
            continue
        with warnings.catch_warnings(record=True):
            staged_result = list(staged_func(X))
        staged_result[1][:] = 0
        assert np.all(staged_result[0] != 0)

def test_serialization():
    # Check model serialization.

    assert 100 == len(clf.estimators_)

    try:
        import cPickle as pickle
        import pickle

    serialized_clf = pickle.dumps(clf, protocol=pickle.HIGHEST_PROTOCOL)
    clf = None
    clf = pickle.loads(serialized_clf)

def test_degenerate_targets():
    # Check if we can fit even though all targets are equal.

    # classifier should raise exception
        clf.fit(X, np.ones(len(X)))

    clf = GradientBoostingRegressor(n_estimators=100, random_state=1)
    clf.fit(X, np.ones(len(X)))
    clf.predict([rng.rand(2)])
    assert_array_equal(np.ones((1,), dtype=np.float64), clf.predict([rng.rand(2)]))

def test_quantile_loss(global_random_seed):
    # Check if quantile loss with alpha=0.5 equals absolute_error.
    clf_quantile = GradientBoostingRegressor(
        max_depth=4,
        alpha=0.5,
    )

    clf_quantile.fit(X_reg, y_reg)
    y_quantile = clf_quantile.predict(X_reg)

    clf_ae = GradientBoostingRegressor(
        loss="absolute_error",
    )

    clf_ae.fit(X_reg, y_reg)
    y_ae = clf_ae.predict(X_reg)
    assert_allclose(y_quantile, y_ae)

def test_symbol_labels():
    # Test with non-integer class labels.

    symbol_y = list(map(str, y))

    clf.fit(X, symbol_y)
    assert_array_equal(clf.predict(T), list(map(str, true_result)))

def test_float_class_labels():
    # Test with float class labels.

    float_y = np.asarray(y, dtype=np.float32)

    clf.fit(X, float_y)
    assert_array_equal(clf.predict(T), np.asarray(true_result, dtype=np.float32))

def test_shape_y():

    y_ = np.asarray(y, dtype=np.int32)
    y_ = y_[:, np.newaxis]

    # This will raise a DataConversionWarning that we want to
    # "always" raise, elsewhere the warnings gets ignored in the
    # later tests, and the tests that check for this warning fail
        "A column-vector y was passed when a 1d array was expected. "
        "Please change the shape of y to \\(n_samples, \\), for "
        "example using ravel()."
    )
    with pytest.warns(DataConversionWarning, match=warn_msg):
        clf.fit(X, y_)

def test_mem_layout():
    # Test with different memory layouts of X and y
    X_ = np.asfortranarray(X)
    clf.fit(X_, y)

    X_ = np.ascontiguousarray(X)

    y_ = np.ascontiguousarray(y_)
    clf.fit(X, y_)

    y_ = np.asfortranarray(y_)

def test_oob_improvement(GradientBoostingEstimator):
    # Test if oob improvement has correct shape and regression test.
    estimator = GradientBoostingEstimator(
        n_estimators=100, random_state=1, subsample=0.5
    )
    assert estimator.oob_improvement_.shape[0] == 100
    # hard-coded regression test - change if modification in OOB computation
        estimator.oob_improvement_[:5],
        np.array([0.19, 0.15, 0.12, -0.11, 0.11]),
        decimal=2,
    )

def test_oob_scores(GradientBoostingEstimator):
    # Test if oob scores has correct shape and regression test.
    )
    assert estimator.oob_scores_.shape[0] == 100
    assert estimator.oob_scores_[-1] == pytest.approx(estimator.oob_score_)

        subsample=0.5,
        n_iter_no_change=5,
    )
    assert estimator.oob_scores_.shape[0] < 100

def test_oob_attributes_error(GradientBoostingEstimator, oob_attribute):
    """
    Check that we raise an AttributeError when the OOB statistics were not computed.
    """
    )
    with pytest.raises(AttributeError):
        estimator.oob_attribute

def test_oob_multilcass_iris():
    # Check OOB improvement on multi-class dataset.
    estimator = GradientBoostingClassifier(
        n_estimators=100, loss="log_loss", random_state=1, subsample=0.5
    )
    estimator.fit(iris.data, iris.target)
    score = estimator.score(iris.data, iris.target)
    assert estimator.oob_improvement_.shape[0] == estimator.n_estimators
    assert estimator.oob_scores_.shape[0] == estimator.n_estimators

    )
    assert estimator.oob_improvement_.shape[0] < estimator.n_estimators
    assert estimator.oob_scores_.shape[0] < estimator.n_estimators

def test_verbose_output():
    # Check verbose=1 does not cause error.
    import sys
    from io import StringIO

    old_stdout = sys.stdout
    sys.stdout = StringIO()
        n_estimators=100, random_state=1, verbose=1, subsample=0.8
    )
    verbose_output = sys.stdout
    sys.stdout = old_stdout

    # check output
    verbose_output.seek(0)
    header = verbose_output.readline().rstrip()
    # with OOB
    true_header = " ".join(["%10s"] + ["%16s"] * 3) % (
        "Iter",
        "Train Loss",
        "OOB Improve",
        "Remaining Time",
    )
    assert true_header == header

    n_lines = sum(1 for l in verbose_output.readlines())
    # one for 1-10 and then 9 for 20-100
    assert 10 + 9 == n_lines

def test_more_verbose_output():
    # Check verbose=2 does not cause error.
    import sys

    clf = GradientBoostingClassifier(n_estimators=100, random_state=1, verbose=2)

    # no OOB
    true_header = " ".join(["%10s"] + ["%16s"] * 2) % (
        "Iter",
    )

    # 100 lines for n_estimators==100
    assert 100 == n_lines

def test_warm_start(Cls, global_random_seed):
    # Test if warm start equals fit.
    X, y = datasets.make_hastie_10_2(n_samples=100, random_state=global_random_seed)
    est = Cls(n_estimators=200, max_depth=1, random_state=global_random_seed)

    est_ws = Cls(
        n_estimators=100, max_depth=1, warm_start=True, random_state=global_random_seed
    )
    est_ws.fit(X, y)
    est_ws.set_params(n_estimators=200)

    if Cls is GradientBoostingRegressor:
        assert_allclose(est_ws.predict(X), est.predict(X))
    else:
        # Random state is preserved and hence predict_proba must also be
        # same
        assert_array_equal(est_ws.predict(X), est.predict(X))
        assert_allclose(est_ws.predict_proba(X), est.predict_proba(X))

def test_warm_start_n_estimators(Cls, global_random_seed):
    # Test if warm start equals fit - set n_estimators.
    est = Cls(n_estimators=300, max_depth=1, random_state=global_random_seed)

    )
    est_ws.set_params(n_estimators=300)

    assert_allclose(est_ws.predict(X), est.predict(X))

def test_warm_start_max_depth(Cls):
    est = Cls(n_estimators=100, max_depth=1, warm_start=True)
    est.set_params(n_estimators=110, max_depth=2)

    # last 10 trees have different depth
    assert est.estimators_[0, 0].max_depth == 1
        assert est.estimators_[-i, 0].max_depth == 2

def test_warm_start_clear(Cls):
    est = Cls(n_estimators=100, max_depth=1)

    est_2 = Cls(n_estimators=100, max_depth=1, warm_start=True)
    est_2.set_params(warm_start=False)

    assert_array_almost_equal(est_2.predict(X), est.predict(X))

def test_warm_start_state_oob_scores(GradientBoosting):
    """
    Check that the states of the OOB scores are cleared when used with `warm_start`.
    """
    n_estimators = 100
    estimator = GradientBoosting(
        n_estimators=n_estimators,
        max_depth=1,
    )
    oob_scores, oob_score = estimator.oob_scores_, estimator.oob_score_
    assert len(oob_scores) == n_estimators
    assert oob_scores[-1] == pytest.approx(oob_score)

    n_more_estimators = 200
    estimator.set_params(n_estimators=n_more_estimators).fit(X, y)
    assert len(estimator.oob_scores_) == n_more_estimators
    assert_allclose(estimator.oob_scores_[:n_estimators], oob_scores)

    estimator.set_params(n_estimators=n_estimators, warm_start=False).fit(X, y)
    assert estimator.oob_scores_ is not oob_scores
    assert estimator.oob_score_ is not oob_score
    assert_allclose(estimator.oob_scores_, oob_scores)
    assert estimator.oob_score_ == pytest.approx(oob_score)

def test_warm_start_smaller_n_estimators(Cls):
    # Test if warm start with smaller n_estimators raises error
    est.set_params(n_estimators=99)

def test_warm_start_equal_n_estimators(Cls):

    est2 = clone(est)
    est2.set_params(n_estimators=est.n_estimators, warm_start=True)

    assert_array_almost_equal(est2.predict(X), est.predict(X))

def test_warm_start_oob_switch(Cls):
    # Test if oob can be turned on during warm start.
    est.set_params(n_estimators=110, subsample=0.5)

    assert_array_equal(est.oob_improvement_[:100], np.zeros(100))
    assert_array_equal(est.oob_scores_[:100], np.zeros(100))

    # the last 10 are not zeros
    assert (est.oob_improvement_[-10:] != 0.0).all()
    assert (est.oob_scores_[-10:] != 0.0).all()

    assert est.oob_scores_[-1] == pytest.approx(est.oob_score_)

def test_warm_start_oob(Cls):
    # Test if warm start OOB equals fit.
    est = Cls(n_estimators=200, max_depth=1, subsample=0.5, random_state=1)

        n_estimators=100, max_depth=1, subsample=0.5, random_state=1, warm_start=True
    )

    assert_array_almost_equal(est_ws.oob_improvement_[:100], est.oob_improvement_[:100])
    assert_array_almost_equal(est_ws.oob_scores_[:100], est.oob_scores_[:100])
    assert est_ws.oob_scores_[-1] == pytest.approx(est_ws.oob_score_)

def test_warm_start_sparse(Cls, sparse_container):
    # Test that all sparse matrix types are supported
    est_dense = Cls(
    )
    est_dense.fit(X, y)
    est_dense.predict(X)
    est_dense.set_params(n_estimators=200)
    y_pred_dense = est_dense.predict(X)

    X_sparse = sparse_container(X)

    est_sparse = Cls(
    )
    est_sparse.fit(X_sparse, y)
    est_sparse.predict(X)
    est_sparse.set_params(n_estimators=200)
    y_pred_sparse = est_sparse.predict(X)

        est_dense.oob_improvement_[:100], est_sparse.oob_improvement_[:100]
    )
    assert est_dense.oob_scores_[-1] == pytest.approx(est_dense.oob_score_)
    assert_array_almost_equal(est_dense.oob_scores_[:100], est_sparse.oob_scores_[:100])
    assert est_sparse.oob_scores_[-1] == pytest.approx(est_sparse.oob_score_)
    assert_array_almost_equal(y_pred_dense, y_pred_sparse)

def test_warm_start_fortran(Cls, global_random_seed):
    # Test that feeding a X in Fortran-ordered is giving the same results as
    # in C-ordered
    est_c = Cls(n_estimators=1, random_state=global_random_seed, warm_start=True)
    est_fortran = Cls(n_estimators=1, random_state=global_random_seed, warm_start=True)

    est_c.fit(X, y)
    est_c.set_params(n_estimators=11)

    X_fortran = np.asfortranarray(X)
    est_fortran.fit(X_fortran, y)
    est_fortran.set_params(n_estimators=11)

    assert_allclose(est_c.predict(X), est_fortran.predict(X))

def early_stopping_monitor(i, est, locals):
    """Returns True on the 10th iteration."""
    if i == 9:
    else:

def test_monitor_early_stopping(Cls):
    # Test if monitor return value works.

    est = Cls(n_estimators=20, max_depth=1, random_state=1, subsample=0.5)
    est.fit(X, y, monitor=early_stopping_monitor)
    assert est.n_estimators == 20  # this is not altered
    assert est.estimators_.shape[0] == 10
    assert est.train_score_.shape[0] == 10
    assert est.oob_improvement_.shape[0] == 10
    assert est.oob_scores_.shape[0] == 10

    # try refit
    est.set_params(n_estimators=30)
    assert est.n_estimators == 30
    assert est.estimators_.shape[0] == 30
    assert est.train_score_.shape[0] == 30
    assert est.oob_improvement_.shape[0] == 30
    assert est.oob_scores_.shape[0] == 30

    est = Cls(
        n_estimators=20, max_depth=1, random_state=1, subsample=0.5, warm_start=True
    )
    assert est.n_estimators == 20

    # try refit
    est.set_params(n_estimators=30, warm_start=False)

def test_complete_classification():
    # Test greedy trees with max_depth + 1 leafs.
    from sklearn.tree._tree import TREE_LEAF

    k = 4

    est = GradientBoostingClassifier(
        n_estimators=20, max_depth=None, random_state=1, max_leaf_nodes=k + 1
    )

    tree = est.estimators_[0, 0].tree_
    assert tree.max_depth == k
    assert tree.children_left[tree.children_left == TREE_LEAF].shape[0] == k + 1

def test_complete_regression():

    k = 4

    est = GradientBoostingRegressor(
    )
    est.fit(X_reg, y_reg)

    tree = est.estimators_[-1, 0].tree_

def test_zero_estimator_reg(global_random_seed):
    # Test if init='zero' works for regression by checking that it is better
    # than a simple baseline.

    baseline = DummyRegressor(strategy="mean").fit(X_reg, y_reg)
    mse_baseline = mean_squared_error(baseline.predict(X_reg), y_reg)
        init="zero",
        learning_rate=0.5,
    )
    y_pred = est.predict(X_reg)
    mse_gbdt = mean_squared_error(y_reg, y_pred)
    assert mse_gbdt < mse_baseline

def test_zero_estimator_clf(global_random_seed):
    # Test if init='zero' works for classification.
    y = np.array(iris.target)

        n_estimators=20, max_depth=1, random_state=global_random_seed, init="zero"
    )

    assert est.score(X, y) > 0.96

    # binary clf
    mask = y != 0
    y[mask] = 1
    y[~mask] = 0
    )

def test_max_leaf_nodes_max_depth(GBEstimator):

    k = 4

    est = GBEstimator(max_depth=1, max_leaf_nodes=k).fit(X, y)
    assert tree.max_depth == 1

    est = GBEstimator(max_depth=1).fit(X, y)

def test_min_impurity_decrease(GBEstimator):

    est = GBEstimator(min_impurity_decrease=0.1)
    for tree in est.estimators_.flat:
        # Simply check if the parameter is passed on correctly. Tree tests
        # will suffice for the actual working of this param
        assert tree.min_impurity_decrease == 0.1

def test_warm_start_wo_nestimators_change():
    # Test if warm_start does nothing if n_estimators is not changed.
    # Regression test for #3513.
    clf = GradientBoostingClassifier(n_estimators=10, warm_start=True)
    clf.fit([[0, 1], [2, 3]], [0, 1])
    assert clf.estimators_.shape[0] == 10

def test_non_uniform_weights_toy_edge_case_reg(loss, value):
    gb = GradientBoostingRegressor(learning_rate=1.0, n_estimators=2, loss=loss)
    assert gb.predict([[1, 0]])[0] >= value

def test_non_uniform_weights_toy_edge_case_clf():
    for loss in ("log_loss", "exponential"):
        gb = GradientBoostingClassifier(n_estimators=5, loss=loss)
        gb.fit(X, y, sample_weight=sample_weight)
        assert_array_equal(gb.predict([[1, 0]]), [1])

def test_sparse_input(EstimatorClass, sparse_container):
    y, X = datasets.make_multilabel_classification(
        random_state=0, n_samples=50, n_features=1, n_classes=20
    )
    y = y[:, 0]

    dense = EstimatorClass(
        n_estimators=10, random_state=0, max_depth=2, min_impurity_decrease=1e-7
    sparse = EstimatorClass(
    ).fit(X_sparse, y)

    assert_array_almost_equal(sparse.predict(X), dense.predict(X))
    assert_array_almost_equal(sparse.feature_importances_, dense.feature_importances_)

    assert_array_almost_equal(sparse.predict(X_sparse), dense.predict(X))
    assert_array_almost_equal(dense.predict(X_sparse), sparse.predict(X))

    if issubclass(EstimatorClass, GradientBoostingClassifier):
        )

            sparse.decision_function(X_sparse), sparse.decision_function(X)
        )
            dense.decision_function(X_sparse), sparse.decision_function(X)
        )
        for res_sparse, res in zip(
            sparse.staged_decision_function(X_sparse),
            sparse.staged_decision_function(X),
        ):
            assert_array_almost_equal(res_sparse, res)

def test_gradient_boosting_early_stopping(GradientBoostingEstimator):
    # Check if early stopping works as expected, that is empirically check that the
    # number of trained estimators is increasing when the tolerance decreases.

    X, y = make_classification(n_samples=1000, random_state=0)
    n_estimators = 1000

    gb_large_tol = GradientBoostingEstimator(
        tol=1e-1,
    )

    gb_small_tol = GradientBoostingEstimator(
        tol=1e-3,
    )

    gb_large_tol.fit(X_train, y_train)
    gb_small_tol.fit(X_train, y_train)

    assert gb_large_tol.n_estimators_ < gb_small_tol.n_estimators_ < n_estimators

    assert gb_large_tol.score(X_test, y_test) > 0.7
    assert gb_small_tol.score(X_test, y_test) > 0.7

def test_gradient_boosting_without_early_stopping():
    # When early stopping is not used, the number of trained estimators
    # must be the one specified.

    gbc = GradientBoostingClassifier(
        n_estimators=50, learning_rate=0.1, max_depth=3, random_state=42
    )
    gbc.fit(X, y)
    gbr = GradientBoostingRegressor(
        n_estimators=30, learning_rate=0.1, max_depth=3, random_state=42
    )
    gbr.fit(X, y)

    # The number of trained estimators must be the one specified.
    assert gbc.n_estimators_ == 50
    assert gbr.n_estimators_ == 30

def test_gradient_boosting_validation_fraction():

    )
    gbc2 = clone(gbc).set_params(validation_fraction=0.3)
    gbc3 = clone(gbc).set_params(n_iter_no_change=20)

    )
    gbr2 = clone(gbr).set_params(validation_fraction=0.3)
    gbr3 = clone(gbr).set_params(n_iter_no_change=20)

    # Check if validation_fraction has an effect
    gbc.fit(X_train, y_train)
    gbc2.fit(X_train, y_train)
    assert gbc.n_estimators_ != gbc2.n_estimators_

    gbr.fit(X_train, y_train)
    gbr2.fit(X_train, y_train)
    assert gbr.n_estimators_ != gbr2.n_estimators_

    # Check if n_estimators_ increase monotonically with n_iter_no_change
    # Set validation
    gbc3.fit(X_train, y_train)
    gbr3.fit(X_train, y_train)
    assert gbr.n_estimators_ < gbr3.n_estimators_
    assert gbc.n_estimators_ < gbc3.n_estimators_

def test_early_stopping_stratified():
    # Make sure data splitting for early stopping is stratified
    X = [[1, 2], [2, 3], [3, 4], [4, 5]]
    y = [0, 0, 0, 1]

    gbc = GradientBoostingClassifier(n_iter_no_change=5)
        ValueError, match="The least populated class in y has only 1 member"
    ):
        gbc.fit(X, y)

def _make_multiclass():
    return make_classification(n_classes=3, n_clusters_per_class=1)

def test_gradient_boosting_with_init(
    gb, dataset_maker, init_estimator, global_random_seed
):
    # Check that GradientBoostingRegressor works when init is a sklearn
    # estimator.
    # Check that an error is raised if trying to fit with sample weight but
    # initial estimator does not support sample weight

    X, y = dataset_maker()
    sample_weight = np.random.RandomState(global_random_seed).rand(100)

    # init supports sample weights
    init_est = init_estimator()
    gb(init=init_est).fit(X, y, sample_weight=sample_weight)

    # init does not support sample weights
    init_est = NoSampleWeightWrapper(init_estimator())
    gb(init=init_est).fit(X, y)  # ok no sample weights
    with pytest.raises(ValueError, match="estimator.*does not support sample weights"):
        gb(init=init_est).fit(X, y, sample_weight=sample_weight)

def test_gradient_boosting_with_init_pipeline():
    # Check that the init estimator can be a pipeline (see issue #13466)

    init = make_pipeline(LinearRegression())
    gb = GradientBoostingRegressor(init=init)
    gb.fit(X, y)  # pipeline without sample_weight works fine

        match="The initial estimator Pipeline does not support sample weights",
    ):
        gb.fit(X, y, sample_weight=np.ones(X.shape[0]))

    # Passing sample_weight to a pipeline raises a ValueError. This test makes
    # sure we make the distinction between ValueError raised by a pipeline that
    # was passed sample_weight, and a InvalidParameterError raised by a regular
    # estimator whose input checking failed.
    invalid_nu = 1.5
    err_msg = (
        "The 'nu' parameter of NuSVR must be a float in the"
        f" range (0.0, 1.0]. Got {invalid_nu} instead."
    )
    with pytest.raises(InvalidParameterError, match=re.escape(err_msg)):
        # Note that NuSVR properly supports sample_weight
        init = NuSVR(gamma="auto", nu=invalid_nu)
        gb = GradientBoostingRegressor(init=init)

def test_early_stopping_n_classes():
    # when doing early stopping (_, , y_train, _ = train_test_split(X, y))
    # there might be classes in y that are missing in y_train. As the init
    # estimator will be trained on y_train, we need to raise an error if this
    # happens.

    X = [[1]] * 10
    y = [0, 0] + [1] * 8  # only 2 negative class over 10 samples
    gb = GradientBoostingClassifier(
        n_iter_no_change=5, random_state=0, validation_fraction=0.8
    )
        ValueError, match="The training data after the early stopping split"
    ):

    # No error if we let training data be big enough
        n_iter_no_change=5, random_state=0, validation_fraction=0.4
    )

def test_gbr_degenerate_feature_importances():
    # growing an ensemble of single node trees. See #13620
    gbr = GradientBoostingRegressor().fit(X, y)

def test_huber_vs_mean_and_median():
    """Check that huber lies between absolute and squared error."""
    n_rep = 100
    y = np.tile(np.arange(n_samples), n_rep)
    x1 = np.minimum(y, n_samples / 2)
    x2 = np.minimum(-y, -n_samples / 2)
    X = np.c_[x1, x2]

    # We want an asymmetric distribution.
    y = y + rng.exponential(scale=1, size=y.shape)

    gbt_absolute_error = GradientBoostingRegressor(loss="absolute_error").fit(X, y)
    gbt_huber = GradientBoostingRegressor(loss="huber").fit(X, y)
    gbt_squared_error = GradientBoostingRegressor().fit(X, y)

    gbt_huber_predictions = gbt_huber.predict(X)
    assert np.all(gbt_absolute_error.predict(X) <= gbt_huber_predictions)
    assert np.all(gbt_huber_predictions <= gbt_squared_error.predict(X))

def test_safe_divide():
    """Test that _safe_divide handles division by zero."""
        warnings.simplefilter("error")
        assert _safe_divide(np.float64(1e300), 0) == 0
        assert _safe_divide(np.float64(0.0), np.float64(0.0)) == 0
    with pytest.warns(RuntimeWarning, match="overflow"):
        # np.finfo(float).max = 1.7976931348623157e+308
        _safe_divide(np.float64(1e300), 1e-10)

def test_squared_error_exact_backward_compat():
    """Test squared error GBT backward compat on a simple dataset.

    The results to compare against are taken from scikit-learn v1.2.0.
    """
    y = np.arange(n_samples)
    gbt = GradientBoostingRegressor(loss="squared_error", n_estimators=100).fit(X, y)

    pred_result = np.array(
        [
            1.39245726e-04,
            1.00010468e00,
            2.00007043e00,
            3.00004051e00,
            4.00000802e00,
            4.99998972e00,
            5.99996312e00,
            6.99993395e00,
            7.99989372e00,
            8.99985660e00,
        ]
    )
    assert_allclose(gbt.predict(X), pred_result, rtol=1e-8)

    train_score = np.array(
        [
            4.87246390e-08,
            3.95590036e-08,
            3.21267865e-08,
            2.60970300e-08,
            2.11820178e-08,
            1.71995782e-08,
            1.39695549e-08,
            1.13391770e-08,
            9.19931587e-09,
            7.47000575e-09,
        ]
    )
    assert_allclose(gbt.train_score_[-10:], train_score, rtol=1e-8)

    # Same but with sample_weights
    sample_weights = np.tile([1, 10], n_samples // 2)
    gbt = GradientBoostingRegressor(loss="squared_error", n_estimators=100).fit(
        X, y, sample_weight=sample_weights
    )

        [
            1.52391462e-04,
            1.00011168e00,
            2.00007724e00,
            3.00004638e00,
            4.00001302e00,
            4.99999873e00,
            5.99997093e00,
            6.99994329e00,
            7.99991290e00,
            8.99988727e00,
        ]
    )
    assert_allclose(gbt.predict(X), pred_result, rtol=1e-6, atol=1e-5)

        [
            4.12445296e-08,
            3.34418322e-08,
            2.71151383e-08,
            2.19782469e-08,
            1.78173649e-08,
            1.44461976e-08,
            1.17120123e-08,
            9.49485678e-09,
            7.69772505e-09,
            6.24155316e-09,
        ]
    )
    assert_allclose(gbt.train_score_[-10:], train_score, rtol=1e-3, atol=1e-11)

def test_huber_exact_backward_compat():
    """Test huber GBT backward compat on a simple dataset.

    """
    gbt = GradientBoostingRegressor(loss="huber", n_estimators=100, alpha=0.8).fit(X, y)

    assert_allclose(gbt._loss.closs.delta, 0.0001655688041282133)

        [
            1.48120765e-04,
            9.99949174e-01,
            2.00116957e00,
            2.99986716e00,
            4.00012064e00,
            5.00002462e00,
            5.99998898e00,
            6.99692549e00,
            8.00006356e00,
            8.99985099e00,
        ]
    )

        [
            2.59484709e-07,
            2.19165900e-07,
            1.89644782e-07,
            1.64556454e-07,
            1.38705110e-07,
            1.20373736e-07,
            1.04746082e-07,
            9.13835687e-08,
            8.20245756e-08,
            7.17122188e-08,
        ]
    )

def test_binomial_error_exact_backward_compat():
    """Test binary log_loss GBT backward compat on a simple dataset.

    """
    y = np.arange(n_samples) % 2
    gbt = GradientBoostingClassifier(loss="log_loss", n_estimators=100).fit(X, y)

        [
            [9.99978098e-01, 2.19017313e-05],
            [2.19017313e-05, 9.99978098e-01],
        ]
    )
    assert_allclose(gbt.predict_proba(X), pred_result, rtol=1e-8)

        [
            1.07742210e-04,
            9.74889078e-05,
            8.82113863e-05,
            7.98167784e-05,
            7.22210566e-05,
            6.53481907e-05,
            5.91293869e-05,
            5.35023988e-05,
            4.84109045e-05,
            4.38039423e-05,
        ]
    )

def test_multinomial_error_exact_backward_compat():
    """Test multiclass log_loss GBT backward compat on a simple dataset.

    """
    y = np.arange(n_samples) % 4

        [
            [9.99999727e-01, 1.11956255e-07, 8.04921671e-08, 8.04921668e-08],
            [1.11956254e-07, 9.99999727e-01, 8.04921671e-08, 8.04921668e-08],
            [1.19417637e-07, 1.19417637e-07, 9.99999675e-01, 8.60526098e-08],
            [1.19417637e-07, 1.19417637e-07, 8.60526088e-08, 9.99999675e-01],
        ]
    )

        [
            1.13300150e-06,
            9.75183397e-07,
            8.39348103e-07,
            7.22433588e-07,
            6.21804338e-07,
            5.35191943e-07,
            4.60643966e-07,
            3.96479930e-07,
            3.41253434e-07,
            2.93719550e-07,
        ]
    )

def test_gb_denominator_zero(global_random_seed):
    """Test _update_terminal_regions denominator is not zero.

    For instance for log loss based binary classification, the line search step might
    become nan/inf as denominator = hessian = prob * (1 - prob) and prob = 0 or 1 can
    happen.
    Here, we create a situation were this happens (at least with roughly 80%) based
    on the random seed.
    """
    X, y = datasets.make_hastie_10_2(n_samples=100, random_state=20)

    params = {
        "subsample": 0.5,
        "max_leaf_nodes": 4,
        "min_samples_leaf": 2,
    }

    clf = GradientBoostingClassifier(**params)
    # _safe_devide would raise a RuntimeWarning

def test_samme_proba():
    # Test the `_samme_proba` helper function.

    # Define some example (bad) `predict_proba` output.
    probs = np.array(
        [[1, 1e-6, 0], [0.19, 0.6, 0.2], [-999, 0.51, 0.5], [1e-6, 1, 1e-9]]
    )
    probs /= np.abs(probs.sum(axis=1))[:, np.newaxis]

    # _samme_proba calls estimator.predict_proba.
    # Make a mock object so I can control what gets returned.
    class MockEstimator:
        def predict_proba(self, X):
            assert_array_equal(X.shape, probs.shape)
            return probs

    mock = MockEstimator()

    samme_proba = _samme_proba(mock, 3, np.ones_like(probs))

    assert_array_equal(samme_proba.shape, probs.shape)
    assert np.isfinite(samme_proba).all()

    # Make sure that the correct elements come out as smallest --
    # `_samme_proba` should preserve the ordering in each example.
    assert_array_equal(np.argmin(samme_proba, axis=1), [2, 0, 0, 2])
    assert_array_equal(np.argmax(samme_proba, axis=1), [0, 1, 1, 1])

def test_oneclass_adaboost_proba():
    # Test predict_proba robustness for one class label input.
    # In response to issue #7501
    # https://github.com/scikit-learn/scikit-learn/issues/7501
    y_t = np.ones(len(X))
    clf = AdaBoostClassifier().fit(X, y_t)
    assert_array_almost_equal(clf.predict_proba(X), np.ones((len(X), 1)))

def test_classification_toy():
    clf = AdaBoostClassifier(random_state=0)
    clf.fit(X, y_class)
    assert_array_equal(clf.predict(T), y_t_class)
    assert_array_equal(np.unique(np.asarray(y_t_class)), clf.classes_)
    assert clf.predict_proba(T).shape == (len(T), 2)
    assert clf.decision_function(T).shape == (len(T),)

def test_regression_toy():
    clf = AdaBoostRegressor(random_state=0)
    clf.fit(X, y_regr)
    assert_array_equal(clf.predict(T), y_t_regr)

def test_iris():
    classes = np.unique(iris.target)

    clf = AdaBoostClassifier()

    assert_array_equal(classes, clf.classes_)
    proba = clf.predict_proba(iris.data)

    assert proba.shape[1] == len(classes)
    assert clf.decision_function(iris.data).shape[1] == len(classes)

    assert score > 0.9, f"Failed with {score = }"

    # Check we used multiple estimators
    assert len(clf.estimators_) > 1
    # Check for distinct random states (see issue #7408)
    assert len(set(est.random_state for est in clf.estimators_)) == len(clf.estimators_)

def test_diabetes(loss):
    # Check consistency on dataset diabetes.
    reg = AdaBoostRegressor(loss=loss, random_state=0)
    reg.fit(diabetes.data, diabetes.target)
    score = reg.score(diabetes.data, diabetes.target)
    assert score > 0.55

    assert len(reg.estimators_) > 1
    assert len(set(est.random_state for est in reg.estimators_)) == len(reg.estimators_)

    # Check staged predictions.
    iris_weights = rng.randint(10, size=iris.target.shape)
    diabetes_weights = rng.randint(10, size=diabetes.target.shape)

    clf = AdaBoostClassifier(n_estimators=10)
    clf.fit(iris.data, iris.target, sample_weight=iris_weights)

    predictions = clf.predict(iris.data)
    staged_predictions = [p for p in clf.staged_predict(iris.data)]
    staged_probas = [p for p in clf.staged_predict_proba(iris.data)]
    score = clf.score(iris.data, iris.target, sample_weight=iris_weights)
    staged_scores = [
        s for s in clf.staged_score(iris.data, iris.target, sample_weight=iris_weights)
    ]

    assert len(staged_predictions) == 10
    assert_array_almost_equal(predictions, staged_predictions[-1])
    assert len(staged_probas) == 10
    assert_array_almost_equal(proba, staged_probas[-1])
    assert len(staged_scores) == 10
    assert_array_almost_equal(score, staged_scores[-1])

    # AdaBoost regression
    clf = AdaBoostRegressor(n_estimators=10, random_state=0)
    clf.fit(diabetes.data, diabetes.target, sample_weight=diabetes_weights)

    predictions = clf.predict(diabetes.data)
    staged_predictions = [p for p in clf.staged_predict(diabetes.data)]
    score = clf.score(diabetes.data, diabetes.target, sample_weight=diabetes_weights)
        s
        for s in clf.staged_score(
            diabetes.data, diabetes.target, sample_weight=diabetes_weights
        )
    ]

def test_gridsearch():
    # AdaBoost classification
    boost = AdaBoostClassifier(estimator=DecisionTreeClassifier())
    parameters = {
        "n_estimators": (1, 2),
        "estimator__max_depth": (1, 2),
    }
    clf = GridSearchCV(boost, parameters)

    boost = AdaBoostRegressor(estimator=DecisionTreeRegressor(), random_state=0)
    parameters = {"n_estimators": (1, 2), "estimator__max_depth": (1, 2)}
    clf.fit(diabetes.data, diabetes.target)

def test_pickle():
    import pickle

    # Adaboost classifier
    obj = AdaBoostClassifier()
    obj.fit(iris.data, iris.target)
    score = obj.score(iris.data, iris.target)
    s = pickle.dumps(obj)

    obj2 = pickle.loads(s)
    score2 = obj2.score(iris.data, iris.target)

    # Adaboost regressor
    obj = AdaBoostRegressor(random_state=0)
    obj.fit(diabetes.data, diabetes.target)
    score = obj.score(diabetes.data, diabetes.target)

    score2 = obj2.score(diabetes.data, diabetes.target)

def test_importances():
    # Check variable importances.
    X, y = datasets.make_classification(
        n_samples=2000,
        n_features=10,
        n_informative=3,
        n_repeated=0,
        shuffle=False,
    )

    importances = clf.feature_importances_

    assert (importances[:3, np.newaxis] >= importances[3:]).all()

def test_adaboost_classifier_sample_weight_error():
    # Test that it gives proper exception on incorrect sample weight.
    msg = re.escape("sample_weight.shape == (1,), expected (6,)")
        clf.fit(X, y_class, sample_weight=np.asarray([-1]))

def test_estimator():
    # Test different estimators.
    from sklearn.ensemble import RandomForestClassifier

    # XXX doesn't work with y_class because RF doesn't support classes_
    # Shouldn't AdaBoost run a LabelBinarizer?
    clf = AdaBoostClassifier(RandomForestClassifier())

    clf = AdaBoostClassifier(SVC())

    from sklearn.ensemble import RandomForestRegressor

    clf = AdaBoostRegressor(RandomForestRegressor(), random_state=0)

    clf = AdaBoostRegressor(SVR(), random_state=0)

    # Check that an empty discrete ensemble fails in fit, not predict.
    X_fail = [[1, 1], [1, 1], [1, 1], [1, 1]]
    y_fail = ["foo", "bar", 1, 2]
    with pytest.raises(ValueError, match="worse than random"):
        clf.fit(X_fail, y_fail)

def test_sample_weights_infinite():
    msg = "Sample weights have reached infinite values"
    clf = AdaBoostClassifier(n_estimators=30, learning_rate=23.0)
    with pytest.warns(UserWarning, match=msg):

def test_sparse_classification(sparse_container, expected_internal_type):
    # Check classification with sparse input.

    class CustomSVC(SVC):
        """SVC variant that records the nature of the training set."""

        def fit(self, X, y, sample_weight=None):
            """Modification on fit caries data type for later verification."""
            super().fit(X, y, sample_weight=sample_weight)
            self.data_type_ = type(X)

    X, y = datasets.make_multilabel_classification(
        n_classes=1, n_samples=15, n_features=5, random_state=42
    )
    # Flatten y to a 1d array
    y = np.ravel(y)

    X_train_sparse = sparse_container(X_train)
    X_test_sparse = sparse_container(X_test)

    # Trained on sparse format
    sparse_classifier = AdaBoostClassifier(
        estimator=CustomSVC(probability=True),
    ).fit(X_train_sparse, y_train)

    # Trained on dense format
    dense_classifier = AdaBoostClassifier(

    # predict
    sparse_clf_results = sparse_classifier.predict(X_test_sparse)
    dense_clf_results = dense_classifier.predict(X_test)
    assert_array_equal(sparse_clf_results, dense_clf_results)

    # decision_function
    sparse_clf_results = sparse_classifier.decision_function(X_test_sparse)
    dense_clf_results = dense_classifier.decision_function(X_test)
    assert_array_almost_equal(sparse_clf_results, dense_clf_results)

    # predict_log_proba
    sparse_clf_results = sparse_classifier.predict_log_proba(X_test_sparse)
    dense_clf_results = dense_classifier.predict_log_proba(X_test)

    # predict_proba
    sparse_clf_results = sparse_classifier.predict_proba(X_test_sparse)
    dense_clf_results = dense_classifier.predict_proba(X_test)

    # score
    sparse_clf_results = sparse_classifier.score(X_test_sparse, y_test)
    dense_clf_results = dense_classifier.score(X_test, y_test)

    # staged_decision_function
    sparse_clf_results = sparse_classifier.staged_decision_function(X_test_sparse)
    dense_clf_results = dense_classifier.staged_decision_function(X_test)
    for sparse_clf_res, dense_clf_res in zip(sparse_clf_results, dense_clf_results):
        assert_array_almost_equal(sparse_clf_res, dense_clf_res)

    # staged_predict
    sparse_clf_results = sparse_classifier.staged_predict(X_test_sparse)
    dense_clf_results = dense_classifier.staged_predict(X_test)
        assert_array_equal(sparse_clf_res, dense_clf_res)

    # staged_predict_proba
    sparse_clf_results = sparse_classifier.staged_predict_proba(X_test_sparse)
    dense_clf_results = dense_classifier.staged_predict_proba(X_test)

    # staged_score
    sparse_clf_results = sparse_classifier.staged_score(X_test_sparse, y_test)
    dense_clf_results = dense_classifier.staged_score(X_test, y_test)

    # Verify sparsity of data is maintained during training
    types = [i.data_type_ for i in sparse_classifier.estimators_]

    assert all([t == expected_internal_type for t in types])

def test_sparse_regression(sparse_container, expected_internal_type):
    # Check regression with sparse input.

    class CustomSVR(SVR):
        """SVR variant that records the nature of the training set."""

    X, y = datasets.make_regression(
        n_samples=15, n_features=50, n_targets=1, random_state=42
    )

    sparse_regressor = AdaBoostRegressor(estimator=CustomSVR(), random_state=1).fit(
        X_train_sparse, y_train
    )

    dense_regressor = AdaBoostRegressor(estimator=CustomSVR(), random_state=1).fit(
        X_train, y_train
    )

    # predict
    sparse_regr_results = sparse_regressor.predict(X_test_sparse)
    dense_regr_results = dense_regressor.predict(X_test)
    assert_array_almost_equal(sparse_regr_results, dense_regr_results)

    sparse_regr_results = sparse_regressor.staged_predict(X_test_sparse)
    dense_regr_results = dense_regressor.staged_predict(X_test)
    for sparse_regr_res, dense_regr_res in zip(sparse_regr_results, dense_regr_results):
        assert_array_almost_equal(sparse_regr_res, dense_regr_res)

    types = [i.data_type_ for i in sparse_regressor.estimators_]

def test_sample_weight_adaboost_regressor():
    """
    AdaBoostRegressor should work without sample_weights in the base estimator
    The random weighted sampling is done internally in the _boost method in
    AdaBoostRegressor.
    """

    class DummyEstimator(BaseEstimator):
            pass

        def predict(self, X):
            return np.zeros(X.shape[0])

    boost = AdaBoostRegressor(DummyEstimator(), n_estimators=3)
    boost.fit(X, y_regr)
    assert len(boost.estimator_weights_) == len(boost.estimator_errors_)

def test_multidimensional_X():
    """
    Check that the AdaBoost estimators can work with n-dimensional
    data matrix
    """

    X = rng.randn(51, 3, 3)
    yc = rng.choice([0, 1], 51)
    yr = rng.randn(51)

    boost = AdaBoostClassifier(DummyClassifier(strategy="most_frequent"))
    boost.fit(X, yc)
    boost.predict(X)
    boost.predict_proba(X)

    boost = AdaBoostRegressor(DummyRegressor())
    boost.fit(X, yr)

def test_adaboostclassifier_without_sample_weight():
    X, y = iris.data, iris.target
    estimator = NoSampleWeightWrapper(DummyClassifier())
    clf = AdaBoostClassifier(estimator=estimator)
    err_msg = "{} doesn't support sample_weight".format(estimator.__class__.__name__)

def test_adaboostregressor_sample_weight():
    # check that giving weight will have an influence on the error computed
    # for a weak learner
    X = np.linspace(0, 100, num=1000)
    y = (0.8 * X + 0.2) + (rng.rand(X.shape[0]) * 0.0001)
    X = X.reshape(-1, 1)

    # add an arbitrary outlier
    X[-1] *= 10
    y[-1] = 10000

    # random_state=0 ensure that the underlying bootstrap will use the outlier
    regr_no_outlier = AdaBoostRegressor(
        estimator=LinearRegression(), n_estimators=1, random_state=0
    )
    regr_with_weight = clone(regr_no_outlier)
    regr_with_outlier = clone(regr_no_outlier)

    # fit 3 models:
    # - a model containing the outlier
    # - a model without the outlier
    # - a model containing the outlier but with a null sample-weight
    regr_with_outlier.fit(X, y)
    regr_no_outlier.fit(X[:-1], y[:-1])
    sample_weight[-1] = 0
    regr_with_weight.fit(X, y, sample_weight=sample_weight)

    score_with_outlier = regr_with_outlier.score(X[:-1], y[:-1])
    score_no_outlier = regr_no_outlier.score(X[:-1], y[:-1])
    score_with_weight = regr_with_weight.score(X[:-1], y[:-1])

    assert score_with_outlier < score_no_outlier
    assert score_with_outlier < score_with_weight
    assert score_no_outlier == pytest.approx(score_with_weight)

def test_adaboost_consistent_predict():
    # check that predict_proba and predict give consistent results
    # regression test for:
    # https://github.com/scikit-learn/scikit-learn/issues/14084
        *datasets.load_digits(return_X_y=True), random_state=42
    )
    model = AdaBoostClassifier(random_state=42)
    model.fit(X_train, y_train)

        np.argmax(model.predict_proba(X_test), axis=1), model.predict(X_test)
    )

def test_adaboost_negative_weight_error(model, X, y):
    sample_weight[-1] = -10

    err_msg = "Negative values in data passed to `sample_weight`"
        model.fit(X, y, sample_weight=sample_weight)

def test_adaboost_numerically_stable_feature_importance_with_small_weights():
    """Check that we don't create NaN feature importance with numerically
    instable inputs.

    https://github.com/scikit-learn/scikit-learn/issues/20320
    """
    X = rng.normal(size=(1000, 10))
    y = rng.choice([0, 1], size=1000)
    sample_weight = np.ones_like(y) * 1e-263
    tree = DecisionTreeClassifier(max_depth=10, random_state=12)
    ada_model = AdaBoostClassifier(estimator=tree, n_estimators=20, random_state=12)
    ada_model.fit(X, y, sample_weight=sample_weight)
    assert np.isnan(ada_model.feature_importances_).sum() == 0

def test_adaboost_decision_function(global_random_seed):
    """Check that the decision function respects the symmetric constraint for weak
    learners.

    https://github.com/scikit-learn/scikit-learn/issues/26520
    """
        n_classes=n_classes, n_clusters_per_class=1, random_state=global_random_seed
    )
    clf = AdaBoostClassifier(n_estimators=1, random_state=global_random_seed).fit(X, y)

    y_score = clf.decision_function(X)
    assert_allclose(y_score.sum(axis=1), 0, atol=1e-8)

    # With a single learner, we expect to have a decision function in
    # {1, - 1 / (n_classes - 1)}.
    assert set(np.unique(y_score)) == {1, -1 / (n_classes - 1)}

    # We can assert the same for staged_decision_function since we have a single learner
    for y_score in clf.staged_decision_function(X):
        assert_allclose(y_score.sum(axis=1), 0, atol=1e-8)

        # With a single learner, we expect to have a decision function in
        # {1, - 1 / (n_classes - 1)}.
        assert set(np.unique(y_score)) == {1, -1 / (n_classes - 1)}

    clf.set_params(n_estimators=5).fit(X, y)

def test_deprecated_algorithm():
    adaboost_clf = AdaBoostClassifier(n_estimators=1, algorithm="SAMME")
    with pytest.warns(FutureWarning, match="The parameter 'algorithm' is deprecated"):
        adaboost_clf.fit(X, y_class)

            pass

def test_base():
    # Check BaseEnsemble methods.
    ensemble = BaggingClassifier(
        estimator=Perceptron(random_state=None), n_estimators=3
    )

    iris = load_iris()
    ensemble.fit(iris.data, iris.target)
    ensemble.estimators_ = []  # empty the list and create estimators manually

    ensemble._make_estimator()
    random_state = np.random.RandomState(3)
    ensemble._make_estimator(random_state=random_state)
    ensemble._make_estimator(append=False)

    assert 3 == len(ensemble)
    assert 3 == len(ensemble.estimators_)

    assert isinstance(ensemble[0], Perceptron)
    assert ensemble[0].random_state is None
    assert isinstance(ensemble[1].random_state, int)
    assert isinstance(ensemble[2].random_state, int)
    assert ensemble[1].random_state != ensemble[2].random_state

    np_int_ensemble = BaggingClassifier(
        estimator=Perceptron(), n_estimators=np.int32(3)
    )
    np_int_ensemble.fit(iris.data, iris.target)

def test_set_random_states():
    # Linear Discriminant Analysis doesn't have random state: smoke test
    _set_random_states(LinearDiscriminantAnalysis(), random_state=17)

    clf1 = Perceptron(random_state=None)
    assert clf1.random_state is None
    # check random_state is None still sets
    _set_random_states(clf1, None)
    assert isinstance(clf1.random_state, int)

    # check random_state fixes results in consistent initialisation
    _set_random_states(clf1, 3)
    clf2 = Perceptron(random_state=None)
    _set_random_states(clf2, 3)
    assert clf1.random_state == clf2.random_state

    # nested random_state

    def make_steps():
        return [
            ("sel", SelectFromModel(Perceptron(random_state=None))),
            ("clf", Perceptron(random_state=None)),
        ]

    est1 = Pipeline(make_steps())
    _set_random_states(est1, 3)
    assert isinstance(est1.steps[0][1].estimator.random_state, int)
    assert isinstance(est1.steps[1][1].random_state, int)
    assert (
        est1.get_params()["sel__estimator__random_state"]
        != est1.get_params()["clf__random_state"]
    )

    # ensure multiple random_state parameters are invariant to get_params()
    # iteration order

    class AlphaParamPipeline(Pipeline):
        def get_params(self, *args, **kwargs):
            params = Pipeline.get_params(self, *args, **kwargs).items()
            return OrderedDict(sorted(params))

    class RevParamPipeline(Pipeline):
            return OrderedDict(sorted(params, reverse=True))

    for cls in [AlphaParamPipeline, RevParamPipeline]:
        est2 = cls(make_steps())
        _set_random_states(est2, 3)
        assert (
            est1.get_params()["sel__estimator__random_state"]
            == est2.get_params()["sel__estimator__random_state"]
        )
        assert (
            est1.get_params()["clf__random_state"]
            == est2.get_params()["clf__random_state"]
        )

        return [
        ]

def test_voting_classifier_estimator_init(params, err_msg):
    ensemble = VotingClassifier(**params)

def test_predictproba_hardvoting():
    eclf = VotingClassifier(
        estimators=[("lr1", LogisticRegression()), ("lr2", LogisticRegression())],
    )

    inner_msg = "predict_proba is not available when voting='hard'"
    outer_msg = "'VotingClassifier' has no attribute 'predict_proba'"
        eclf.predict_proba

    assert not hasattr(eclf, "predict_proba")
    eclf.fit(X_scaled, y)

def test_notfitted():
        voting="soft",
    )
    ereg = VotingRegressor([("dr", DummyRegressor())])
    msg = (
        "This %s instance is not fitted yet. Call 'fit'"
        " with appropriate arguments before using this estimator."
    )
    with pytest.raises(NotFittedError, match=msg % "VotingClassifier"):
        eclf.predict(X)
        eclf.predict_proba(X)
        eclf.transform(X)
    with pytest.raises(NotFittedError, match=msg % "VotingRegressor"):
        ereg.predict(X_r)
        ereg.transform(X_r)

def test_majority_label_iris(global_random_seed):
    """Check classification by majority label on dataset iris."""
    clf1 = LogisticRegression(solver="liblinear", random_state=global_random_seed)
    clf2 = RandomForestClassifier(n_estimators=10, random_state=global_random_seed)
    clf3 = GaussianNB()
        estimators=[("lr", clf1), ("rf", clf2), ("gnb", clf3)], voting="hard"
    )
    scores = cross_val_score(eclf, X, y, scoring="accuracy")

    assert scores.mean() >= 0.9

def test_tie_situation():
    """Check voting classifier selects smaller class label in tie situation."""
    clf1 = LogisticRegression(random_state=123, solver="liblinear")
    clf2 = RandomForestClassifier(random_state=123)
    eclf = VotingClassifier(estimators=[("lr", clf1), ("rf", clf2)], voting="hard")
    assert clf1.fit(X, y).predict(X)[73] == 2
    assert clf2.fit(X, y).predict(X)[73] == 1
    assert eclf.fit(X, y).predict(X)[73] == 1

def test_weights_iris(global_random_seed):
    """Check classification by average probabilities on dataset iris."""
    clf1 = LogisticRegression(random_state=global_random_seed)
        estimators=[("lr", clf1), ("rf", clf2), ("gnb", clf3)],
        weights=[1, 2, 10],
    )
    scores = cross_val_score(eclf, X_scaled, y, scoring="accuracy")

def test_weights_regressor():
    """Check weighted average regression prediction on diabetes dataset."""
    reg1 = DummyRegressor(strategy="mean")
    reg2 = DummyRegressor(strategy="median")
    reg3 = DummyRegressor(strategy="quantile", quantile=0.2)
    ereg = VotingRegressor(
        [("mean", reg1), ("median", reg2), ("quantile", reg3)], weights=[1, 2, 10]
    )

    X_r_train, X_r_test, y_r_train, y_r_test = train_test_split(
        X_r, y_r, test_size=0.25
    )

    reg1_pred = reg1.fit(X_r_train, y_r_train).predict(X_r_test)
    reg2_pred = reg2.fit(X_r_train, y_r_train).predict(X_r_test)
    reg3_pred = reg3.fit(X_r_train, y_r_train).predict(X_r_test)
    ereg_pred = ereg.fit(X_r_train, y_r_train).predict(X_r_test)

    avg = np.average(
        np.asarray([reg1_pred, reg2_pred, reg3_pred]), axis=0, weights=[1, 2, 10]
    )
    assert_almost_equal(ereg_pred, avg, decimal=2)

    ereg_weights_none = VotingRegressor(
        [("mean", reg1), ("median", reg2), ("quantile", reg3)], weights=None
    )
    ereg_weights_equal = VotingRegressor(
        [("mean", reg1), ("median", reg2), ("quantile", reg3)], weights=[1, 1, 1]
    )
    ereg_weights_none.fit(X_r_train, y_r_train)
    ereg_weights_equal.fit(X_r_train, y_r_train)
    ereg_none_pred = ereg_weights_none.predict(X_r_test)
    ereg_equal_pred = ereg_weights_equal.predict(X_r_test)
    assert_almost_equal(ereg_none_pred, ereg_equal_pred, decimal=2)

def test_predict_on_toy_problem(global_random_seed):
    """Manually check predicted class labels for toy dataset."""

        [[-1.1, -1.5], [-1.2, -1.4], [-3.4, -2.2], [1.1, 1.2], [2.1, 1.4], [3.1, 2.3]]
    )

    y = np.array([1, 1, 1, 2, 2, 2])

    assert_array_equal(clf1.fit(X, y).predict(X), [1, 1, 1, 2, 2, 2])
    assert_array_equal(clf2.fit(X, y).predict(X), [1, 1, 1, 2, 2, 2])
    assert_array_equal(clf3.fit(X, y).predict(X), [1, 1, 1, 2, 2, 2])

        weights=[1, 1, 1],
    )
    assert_array_equal(eclf.fit(X, y).predict(X), [1, 1, 1, 2, 2, 2])

    )

def test_predict_proba_on_toy_problem():
    """Calculate predicted probabilities on toy dataset."""
    clf1 = LogisticRegression(random_state=123)
    X = np.array([[-1.1, -1.5], [-1.2, -1.4], [-3.4, -2.2], [1.1, 1.2]])
    y = np.array([1, 1, 2, 2])

    clf1_res = np.array(
        [
            [0.59790391, 0.40209609],
            [0.57622162, 0.42377838],
            [0.50728456, 0.49271544],
            [0.40241774, 0.59758226],
        ]
    )

    clf2_res = np.array([[0.8, 0.2], [0.8, 0.2], [0.2, 0.8], [0.3, 0.7]])

    clf3_res = np.array(
        [[0.9985082, 0.0014918], [0.99845843, 0.00154157], [0.0, 1.0], [0.0, 1.0]]
    )

    t00 = (2 * clf1_res[0][0] + clf2_res[0][0] + clf3_res[0][0]) / 4
    t11 = (2 * clf1_res[1][1] + clf2_res[1][1] + clf3_res[1][1]) / 4
    t21 = (2 * clf1_res[2][1] + clf2_res[2][1] + clf3_res[2][1]) / 4
    t31 = (2 * clf1_res[3][1] + clf2_res[3][1] + clf3_res[3][1]) / 4

        weights=[2, 1, 1],
    )
    eclf_res = eclf.fit(X, y).predict_proba(X)

    assert_almost_equal(t00, eclf_res[0][0], decimal=1)
    assert_almost_equal(t11, eclf_res[1][1], decimal=1)
    assert_almost_equal(t21, eclf_res[2][1], decimal=1)
    assert_almost_equal(t31, eclf_res[3][1], decimal=1)

        eclf = VotingClassifier(
            estimators=[("lr", clf1), ("rf", clf2), ("gnb", clf3)], voting="hard"
        )
        eclf.fit(X, y).predict_proba(X)

def test_multilabel():
    """Check if error is raised for multilabel classification."""
    X, y = make_multilabel_classification(
        n_classes=2, n_labels=1, allow_unlabeled=False, random_state=123
    )
    clf = OneVsRestClassifier(SVC(kernel="linear"))

    eclf = VotingClassifier(estimators=[("ovr", clf)], voting="hard")

    try:
        eclf.fit(X, y)
    except NotImplementedError:
        return

    """Check GridSearch support."""
    clf1 = LogisticRegression(random_state=1)
    clf2 = RandomForestClassifier(random_state=1, n_estimators=3)
        estimators=[("lr", clf1), ("rf", clf2), ("gnb", clf3)], voting="soft"
    )

    params = {
        "lr__C": [1.0, 100.0],
        "voting": ["soft", "hard"],
        "weights": [[0.5, 0.5, 0.5], [1.0, 0.5, 0.5]],
    }

    grid = GridSearchCV(estimator=eclf, param_grid=params, cv=2)
    grid.fit(X_scaled, y)

def test_parallel_fit(global_random_seed):
    """Check parallel backend of VotingClassifier on toy dataset."""

    eclf1 = VotingClassifier(
        estimators=[("lr", clf1), ("rf", clf2), ("gnb", clf3)], voting="soft", n_jobs=1
    eclf2 = VotingClassifier(
        estimators=[("lr", clf1), ("rf", clf2), ("gnb", clf3)], voting="soft", n_jobs=2

    assert_array_equal(eclf1.predict(X), eclf2.predict(X))
    assert_array_almost_equal(eclf1.predict_proba(X), eclf2.predict_proba(X))

def test_sample_weight(global_random_seed):
    """Tests sample_weight parameter of VotingClassifier"""
    clf3 = SVC(probability=True, random_state=global_random_seed)
        estimators=[("lr", clf1), ("rf", clf2), ("svc", clf3)], voting="soft"
    ).fit(X_scaled, y, sample_weight=np.ones((len(y),)))
    ).fit(X_scaled, y)
    assert_array_equal(eclf1.predict(X_scaled), eclf2.predict(X_scaled))
        eclf1.predict_proba(X_scaled), eclf2.predict_proba(X_scaled)
    )
    sample_weight = np.random.RandomState(global_random_seed).uniform(size=(len(y),))
    eclf3 = VotingClassifier(estimators=[("lr", clf1)], voting="soft")
    eclf3.fit(X_scaled, y, sample_weight=sample_weight)
    clf1.fit(X_scaled, y, sample_weight)
    assert_array_equal(eclf3.predict(X_scaled), clf1.predict(X_scaled))
        eclf3.predict_proba(X_scaled), clf1.predict_proba(X_scaled)
    )

    # check that an error is raised and indicative if sample_weight is not
    # supported.
    clf4 = KNeighborsClassifier()
    eclf3 = VotingClassifier(
        estimators=[("lr", clf1), ("svc", clf3), ("knn", clf4)], voting="soft"
    )
    msg = "Underlying estimator KNeighborsClassifier does not support sample weights."
    with pytest.raises(TypeError, match=msg):
        eclf3.fit(X_scaled, y, sample_weight=sample_weight)

    # check that _fit_single_estimator will raise the right error
    # it should raise the original error if this is not linked to sample_weight
    class ClassifierErrorFit(ClassifierMixin, BaseEstimator):
        def fit(self, X_scaled, y, sample_weight):
            raise TypeError("Error unrelated to sample_weight.")

    clf = ClassifierErrorFit()
    with pytest.raises(TypeError, match="Error unrelated to sample_weight"):
        clf.fit(X_scaled, y, sample_weight=sample_weight)

def test_sample_weight_kwargs():
    """Check that VotingClassifier passes sample_weight as kwargs"""

    class MockClassifier(ClassifierMixin, BaseEstimator):
        """Mock Classifier to check that sample_weight is received as kwargs"""

        def fit(self, X, y, *args, **sample_weight):
            assert "sample_weight" in sample_weight

    clf = MockClassifier()
    eclf = VotingClassifier(estimators=[("mock", clf)], voting="soft")

    # Should not raise an error.
    eclf.fit(X, y, sample_weight=np.ones((len(y),)))

def test_voting_classifier_set_params(global_random_seed):
    # check equivalence in the output when setting underlying estimators
    clf2 = RandomForestClassifier(
        n_estimators=10, random_state=global_random_seed, max_depth=None
    )

        [("lr", clf1), ("rf", clf2)], voting="soft", weights=[1, 2]
        [("lr", clf1), ("nb", clf3)], voting="soft", weights=[1, 2]
    )
    eclf2.set_params(nb=clf2).fit(X_scaled, y)

    )
    assert eclf2.estimators[0][1].get_params() == clf1.get_params()
    assert eclf2.estimators[1][1].get_params() == clf2.get_params()

def test_set_estimator_drop():
    # VotingClassifier set_params should be able to set estimators as drop
    # Test predict
    clf2 = RandomForestClassifier(n_estimators=10, random_state=123)
        estimators=[("lr", clf1), ("rf", clf2), ("nb", clf3)],
        weights=[1, 0, 0.5],

        weights=[1, 1, 0.5],
    )
    eclf2.set_params(rf="drop").fit(X, y)

    assert dict(eclf2.estimators)["rf"] == "drop"
    assert len(eclf2.estimators_) == 2
    assert all(
        isinstance(est, (LogisticRegression, GaussianNB)) for est in eclf2.estimators_
    )
    assert eclf2.get_params()["rf"] == "drop"

    eclf1.set_params(voting="soft").fit(X, y)
    eclf2.set_params(voting="soft").fit(X, y)

    msg = "All estimators are dropped. At least one is required"
        eclf2.set_params(lr="drop", rf="drop", nb="drop").fit(X, y)

    # Test soft voting transform
    X1 = np.array([[1], [2]])
    y1 = np.array([1, 2])
        estimators=[("rf", clf2), ("nb", clf3)],
        weights=[0, 0.5],
        flatten_transform=False,
    ).fit(X1, y1)

        weights=[1, 0.5],
    )
    eclf2.set_params(rf="drop").fit(X1, y1)
        eclf1.transform(X1),
        np.array([[[0.7, 0.3], [0.3, 0.7]], [[1.0, 0.0], [0.0, 1.0]]]),
    )
    assert_array_almost_equal(eclf2.transform(X1), np.array([[[1.0, 0.0], [0.0, 1.0]]]))
    eclf1.set_params(voting="hard")
    eclf2.set_params(voting="hard")
    assert_array_equal(eclf1.transform(X1), np.array([[0, 0], [1, 1]]))
    assert_array_equal(eclf2.transform(X1), np.array([[0], [1]]))

def test_estimator_weights_format(global_random_seed):
    # Test estimator weights inputs as list and array
        estimators=[("lr", clf1), ("rf", clf2)], weights=[1, 2], voting="soft"
    )
        estimators=[("lr", clf1), ("rf", clf2)], weights=np.array((1, 2)), voting="soft"
    )
    eclf1.fit(X_scaled, y)
    eclf2.fit(X_scaled, y)
    )

def test_transform(global_random_seed):
    """Check transform method of VotingClassifier on toy dataset."""

    assert_array_equal(eclf1.transform(X).shape, (4, 6))
    assert_array_equal(eclf2.transform(X).shape, (4, 6))
    assert_array_equal(eclf3.transform(X).shape, (3, 4, 2))
    assert_array_almost_equal(eclf1.transform(X), eclf2.transform(X))
        eclf3.transform(X).swapaxes(0, 1).reshape((4, 6)), eclf2.transform(X)
    )

def test_none_estimator_with_weights(X, y, voter):
    # check that an estimator can be set to 'drop' and passing some weight
    # regression test for
    # https://github.com/scikit-learn/scikit-learn/issues/13777
    voter = clone(voter)
    # Scaled to solve ConvergenceWarning throw by Logistic Regression
    X_scaled = StandardScaler().fit_transform(X)
    voter.fit(X_scaled, y, sample_weight=np.ones(y.shape))
    voter.set_params(lr="drop")
    y_pred = voter.predict(X_scaled)
    assert y_pred.shape == y.shape

def test_n_features_in(est):
    X = [[1, 2], [3, 4], [5, 6]]
    y = [0, 1, 2]

    assert not hasattr(est, "n_features_in_")
    assert est.n_features_in_ == 2

def test_voting_verbose(estimator, capsys):

    pattern = (
        r"\[Voting\].*\(1 of 2\) Processing lr, total=.*\n"
        r"\[Voting\].*\(2 of 2\) Processing rf, total=.*\n$"
    )
    clone(estimator).fit(X, y)
    assert re.match(pattern, capsys.readouterr()[0])

def test_get_features_names_out_regressor():
    """Check get_feature_names_out output for regressor."""

    y = [0, 1, 2]

    voting = VotingRegressor(
            ("lr", LinearRegression()),
            ("tree", DecisionTreeRegressor(random_state=0)),
            ("ignore", "drop"),
        ]
    )
    voting.fit(X, y)

    names_out = voting.get_feature_names_out()
    expected_names = ["votingregressor_lr", "votingregressor_tree"]

def test_get_features_names_out_classifier(kwargs, expected_names):
    """Check get_feature_names_out for classifier for different settings."""
    X = [[1, 2], [3, 4], [5, 6], [1, 1.2]]
    y = [0, 1, 2, 0]

    voting = VotingClassifier(
            ("lr", LogisticRegression(random_state=0)),
            ("tree", DecisionTreeClassifier(random_state=0)),
        ],
        **kwargs,
    )
    X_trans = voting.transform(X)

    assert X_trans.shape[1] == len(expected_names)

def test_get_features_names_out_classifier_error():
    """Check that error is raised when voting="soft" and flatten_transform=False."""
    y = [0, 1, 2]

        ],
    )

    msg = (
        "get_feature_names_out is not supported when `voting='soft'` and "
        "`flatten_transform=False`"
    )
        voting.get_feature_names_out()

    X = np.array([[0, 1], [2, 2], [4, 6]])
    y = [1, 2, 3]

    ):
        Estimator(["clf", Child()]).fit(X, y, sample_weight=[1, 1, 1], metadata="a")

def test_metadata_routing_for_voting_estimators(Estimator, Child, prop):
    """Test that metadata is routed correctly for Voting*."""
    y = [1, 2, 3]
    sample_weight, metadata = [1, 1, 1], "a"

        [
            (
            ),
            (
            ),
        ]
    )

    est.fit(X, y, **{prop: sample_weight if prop == "sample_weight" else metadata})

        if prop == "sample_weight":
            kwargs = {prop: sample_weight}
        else:
            kwargs = {prop: metadata}
        # access sub-estimator in (name, est) with estimator[1]
            check_recorded_metadata(obj=sub_est, method="fit", parent="fit", **kwargs)

def test_metadata_routing_error_for_voting_estimators(Estimator, Child):
    y = [1, 2, 3]

    )

        est.fit(X, y, sample_weight=sample_weight, metadata=metadata)

def test_classification():
    # Check classification for various parameter settings.
    rng = check_random_state(0)
        iris.data, iris.target, random_state=rng
    )
    grid = ParameterGrid(
        {
            "max_samples": [0.5, 1.0],
            "max_features": [1, 4],
            "bootstrap": [True, False],
            "bootstrap_features": [True, False],
        }
    )
        None,
        DummyClassifier(),
        Perceptron(max_iter=20),
        DecisionTreeClassifier(max_depth=2),
        KNeighborsClassifier(),
        SVC(),
    ]
    # Try different parameter settings with different base classifiers without
    # doing the full cartesian product to keep the test durations low.
    for params, estimator in zip(grid, cycle(estimators)):
        BaggingClassifier(
            n_estimators=2,
            **params,
        ).fit(X_train, y_train).predict(X_test)

def test_sparse_classification(sparse_container, params, method):
    # Check classification for various parameter settings on sparse input.

        """SVC variant that records the nature of the training set"""

        scale(iris.data), iris.target, random_state=rng
    )

    sparse_classifier = BaggingClassifier(
        estimator=CustomSVC(kernel="linear", decision_function_shape="ovr"),
        **params,
    sparse_results = getattr(sparse_classifier, method)(X_test_sparse)

    dense_classifier = BaggingClassifier(
        **params,
    dense_results = getattr(dense_classifier, method)(X_test)
    assert_array_almost_equal(sparse_results, dense_results)

    sparse_type = type(X_train_sparse)

    assert all([t == sparse_type for t in types])

def test_regression():
    # Check regression for various parameter settings.
        diabetes.data[:50], diabetes.target[:50], random_state=rng
    )
        {
            "max_features": [0.5, 1.0],
        }
    )

    for estimator in [
        None,
        DummyRegressor(),
        DecisionTreeRegressor(),
        KNeighborsRegressor(),
        SVR(),
    ]:
        for params in grid:
            BaggingRegressor(estimator=estimator, random_state=rng, **params).fit(
                X_train, y_train
            ).predict(X_test)

def test_sparse_regression(sparse_container):
    # Check regression for various parameter settings on sparse input.
    )

    parameter_sets = [
        {
            "max_samples": 0.5,
            "max_features": 2,
            "bootstrap": True,
            "bootstrap_features": True,
        },
        {
            "max_samples": 1.0,
            "max_features": 4,
        },
        {"max_features": 2, "bootstrap": False, "bootstrap_features": True},
        {"max_samples": 0.5, "bootstrap": True, "bootstrap_features": False},
    ]

    for params in parameter_sets:
        # Trained on sparse format
        sparse_classifier = BaggingRegressor(
            estimator=CustomSVR(), random_state=1, **params
        ).fit(X_train_sparse, y_train)
        sparse_results = sparse_classifier.predict(X_test_sparse)

        # Trained on dense format
        dense_results = (
            BaggingRegressor(estimator=CustomSVR(), random_state=1, **params)
            .fit(X_train, y_train)
            .predict(X_test)
        )

        sparse_type = type(X_train_sparse)
        types = [i.data_type_ for i in sparse_classifier.estimators_]

        assert_array_almost_equal(sparse_results, dense_results)
        assert all([t == sparse_type for t in types])

class DummySizeEstimator(BaseEstimator):
        self.training_size_ = X.shape[0]
        self.training_hash_ = joblib.hash(X)

def test_bootstrap_samples():
    # Test that bootstrapping samples generate non-perfect base estimators.
        diabetes.data, diabetes.target, random_state=rng
    )

    estimator = DecisionTreeRegressor().fit(X_train, y_train)

    # without bootstrap, all trees are perfect on the training set
    ensemble = BaggingRegressor(
        estimator=DecisionTreeRegressor(),

    assert estimator.score(X_train, y_train) == ensemble.score(X_train, y_train)

    # with bootstrap, trees are no longer perfect on the training set

    assert estimator.score(X_train, y_train) > ensemble.score(X_train, y_train)

    # check that each sampling correspond to a complete bootstrap resample.
    # the size of each bootstrap should be the same as the input data but
    # the data should be different (checked using the hash of the data).
    ensemble = BaggingRegressor(estimator=DummySizeEstimator(), bootstrap=True).fit(
    )
    training_hash = []
    for estimator in ensemble.estimators_:
        assert estimator.training_size_ == X_train.shape[0]
        training_hash.append(estimator.training_hash_)
    assert len(set(training_hash)) == len(training_hash)

def test_bootstrap_features():
    # Test that bootstrapping features may generate duplicate features.
    )

    for features in ensemble.estimators_features_:
        assert diabetes.data.shape[1] == np.unique(features).shape[0]

        bootstrap_features=True,

        assert diabetes.data.shape[1] > np.unique(features).shape[0]

def test_probability():
    )

    with np.errstate(divide="ignore", invalid="ignore"):
        # Normal case
        ensemble = BaggingClassifier(
            estimator=DecisionTreeClassifier(), random_state=rng
        ).fit(X_train, y_train)

            np.sum(ensemble.predict_proba(X_test), axis=1), np.ones(len(X_test))
        )

            ensemble.predict_proba(X_test), np.exp(ensemble.predict_log_proba(X_test))
        )

        # Degenerate case, where some classes are missing
            estimator=LogisticRegression(), random_state=rng, max_samples=5

        )

        )

def test_oob_score_classification():
    # Check that oob prediction is a good estimation of the generalization
    # error.
    )

    for estimator in [DecisionTreeClassifier(), SVC()]:
        clf = BaggingClassifier(
            n_estimators=100,
            bootstrap=True,
            oob_score=True,

        test_score = clf.score(X_test, y_test)

        assert abs(test_score - clf.oob_score_) < 0.1

        # Test with few estimators
        warn_msg = (
            "Some inputs do not have OOB scores. This probably means too few "
            "estimators were used to compute any reliable oob estimates."
        )
        with pytest.warns(UserWarning, match=warn_msg):
            clf = BaggingClassifier(
                estimator=estimator,
                n_estimators=1,
                bootstrap=True,
                oob_score=True,
                random_state=rng,
            )
            clf.fit(X_train, y_train)

def test_oob_score_regression():
    # error.
    )

    clf = BaggingRegressor(

    test_score = clf.score(X_test, y_test)

    assert abs(test_score - clf.oob_score_) < 0.1

    # Test with few estimators
        "Some inputs do not have OOB scores. This probably means too few "
        "estimators were used to compute any reliable oob estimates."
    )
        regr = BaggingRegressor(
            n_estimators=1,
        )
        regr.fit(X_train, y_train)

def test_single_estimator():
    # Check singleton ensembles.
    )

    clf1 = BaggingRegressor(
        estimator=KNeighborsRegressor(),

    clf2 = KNeighborsRegressor().fit(X_train, y_train)

    assert_array_almost_equal(clf1.predict(X_test), clf2.predict(X_test))

def test_error():
    # Test support of decision_function
    base = DecisionTreeClassifier()
    assert not hasattr(BaggingClassifier(base).fit(X, y), "decision_function")

def test_parallel_classification():
    # Check parallel classification.
        iris.data, iris.target, random_state=0
    )

        DecisionTreeClassifier(), n_jobs=3, random_state=0

    y1 = ensemble.predict_proba(X_test)
    ensemble.set_params(n_jobs=1)
    y2 = ensemble.predict_proba(X_test)
    assert_array_almost_equal(y1, y2)

        DecisionTreeClassifier(), n_jobs=1, random_state=0

    y3 = ensemble.predict_proba(X_test)
    assert_array_almost_equal(y1, y3)

        SVC(decision_function_shape="ovr"), n_jobs=3, random_state=0

    decisions1 = ensemble.decision_function(X_test)
    decisions2 = ensemble.decision_function(X_test)
    assert_array_almost_equal(decisions1, decisions2)

        SVC(decision_function_shape="ovr"), n_jobs=1, random_state=0

    decisions3 = ensemble.decision_function(X_test)
    assert_array_almost_equal(decisions1, decisions3)

def test_parallel_regression():
    # Check parallel regression.

    )

    ensemble = BaggingRegressor(DecisionTreeRegressor(), n_jobs=3, random_state=0).fit(
    )

    y1 = ensemble.predict(X_test)
    ensemble.set_params(n_jobs=2)
    y2 = ensemble.predict(X_test)

    ensemble = BaggingRegressor(DecisionTreeRegressor(), n_jobs=1, random_state=0).fit(
    )

    y3 = ensemble.predict(X_test)

    # Check that bagging ensembles can be grid-searched.
    # Transform iris into a binary classification task
    y[y == 2] = 1

    # Grid search with scoring based on decision_function
    parameters = {"n_estimators": (1, 2), "estimator__C": (1, 2)}

    GridSearchCV(BaggingClassifier(SVC()), parameters, scoring="roc_auc").fit(X, y)

    # Check estimator and its default values.

    # Classification
    )

    ensemble = BaggingClassifier(None, n_jobs=3, random_state=0).fit(X_train, y_train)

    assert isinstance(ensemble.estimator_, DecisionTreeClassifier)

    ensemble = BaggingClassifier(Perceptron(), n_jobs=3, random_state=0).fit(
    )

    assert isinstance(ensemble.estimator_, Perceptron)

    # Regression
    )

    ensemble = BaggingRegressor(None, n_jobs=3, random_state=0).fit(X_train, y_train)

    assert isinstance(ensemble.estimator_, DecisionTreeRegressor)

    )

    ensemble = BaggingRegressor(SVR(), n_jobs=3, random_state=0).fit(X_train, y_train)
    assert isinstance(ensemble.estimator_, SVR)

def test_bagging_with_pipeline():
    estimator = BaggingClassifier(
        make_pipeline(SelectKBest(k=1), DecisionTreeClassifier()), max_features=2
    )
    assert isinstance(estimator[0].steps[-1][1].random_state, int)

class DummyZeroEstimator(BaseEstimator):

        return self.classes_[np.zeros(X.shape[0], dtype=int)]

def test_bagging_sample_weight_unsupported_but_passed():
    estimator = BaggingClassifier(DummyZeroEstimator())

    estimator.fit(iris.data, iris.target).predict(iris.data)
        estimator.fit(
            iris.data,
            iris.target,
            sample_weight=rng.randint(10, size=(iris.data.shape[0])),
        )

def test_warm_start(random_state=42):
    X, y = make_hastie_10_2(n_samples=20, random_state=1)

    clf_ws = None
        if clf_ws is None:
            clf_ws = BaggingClassifier(
                n_estimators=n_estimators, random_state=random_state, warm_start=True
            )
        else:
            clf_ws.set_params(n_estimators=n_estimators)
        clf_ws.fit(X, y)
        assert len(clf_ws) == n_estimators

    clf_no_ws = BaggingClassifier(
        n_estimators=10, random_state=random_state, warm_start=False
    )
    clf_no_ws.fit(X, y)

    assert set([tree.random_state for tree in clf_ws]) == set(
        [tree.random_state for tree in clf_no_ws]
    )

def test_warm_start_smaller_n_estimators():
    # Test if warm start'ed second fit with smaller n_estimators raises error.
    clf = BaggingClassifier(n_estimators=5, warm_start=True)
    clf.set_params(n_estimators=4)

def test_warm_start_equal_n_estimators():
    # Test that nothing happens when fitting without increasing n_estimators
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=43)

    clf = BaggingClassifier(n_estimators=5, warm_start=True, random_state=83)

    # modify X to nonsense values, this should not change anything
    X_train += 1.0

    warn_msg = "Warm-start fitting without increasing n_estimators does not"
        clf.fit(X_train, y_train)
    assert_array_equal(y_pred, clf.predict(X_test))

def test_warm_start_equivalence():
    # warm started classifier with 5+5 estimators should be equivalent to
    # one classifier with 10 estimators

    clf_ws = BaggingClassifier(n_estimators=5, warm_start=True, random_state=3141)
    clf_ws.fit(X_train, y_train)
    clf_ws.set_params(n_estimators=10)
    y1 = clf_ws.predict(X_test)

    clf = BaggingClassifier(n_estimators=10, warm_start=False, random_state=3141)
    y2 = clf.predict(X_test)

def test_warm_start_with_oob_score_fails():
    # Check using oob_score and warm_start simultaneously fails
    clf = BaggingClassifier(n_estimators=5, warm_start=True, oob_score=True)

def test_oob_score_removed_on_warm_start():
    X, y = make_hastie_10_2(n_samples=100, random_state=1)

    clf = BaggingClassifier(n_estimators=5, oob_score=True)

    clf.set_params(warm_start=True, oob_score=False, n_estimators=10)

        getattr(clf, "oob_score_")

def test_oob_score_consistency():
    # Make sure OOB scores are identical when random_state, estimator, and
    # training data are fixed and fitting is done twice
    bagging = BaggingClassifier(
        max_samples=0.5,
    )
    assert bagging.fit(X, y).oob_score_ == bagging.fit(X, y).oob_score_

def test_estimators_samples():
    # Check that format of estimators_samples_ is correct and that results
    # generated at fit time can be identically reproduced at a later time
    # using data saved in object attributes.
        LogisticRegression(),
    )
    bagging.fit(X, y)

    # Get relevant attributes
    estimators_samples = bagging.estimators_samples_
    estimators_features = bagging.estimators_features_
    estimators = bagging.estimators_

    # Test for correct formatting
    assert len(estimators_samples[0]) == len(X) // 2
    assert estimators_samples[0].dtype.kind == "i"

    # Re-fit single estimator to test for consistent sampling
    estimator_features = estimators_features[estimator_index]

    X_train = (X[estimator_samples])[:, estimator_features]

    orig_coefs = estimator.coef_
    new_coefs = estimator.coef_

    assert_array_almost_equal(orig_coefs, new_coefs)

def test_estimators_samples_deterministic():
    # This test is a regression test to check that with a random step
    # (e.g. SparseRandomProjection) and a given random state, the results
    # generated at fit time can be identically reproduced at a later time using
    # data saved in object attributes. Check issue #9524 for full discussion.

    base_pipeline = make_pipeline(
        SparseRandomProjection(n_components=2), LogisticRegression()
    )
    clf = BaggingClassifier(estimator=base_pipeline, max_samples=0.5, random_state=0)
    pipeline_estimator_coef = clf.estimators_[0].steps[-1][1].coef_.copy()

    estimator = clf.estimators_[0]
    estimator_sample = clf.estimators_samples_[0]
    estimator_feature = clf.estimators_features_[0]

    X_train = (X[estimator_sample])[:, estimator_feature]
    y_train = y[estimator_sample]

    assert_array_equal(estimator.steps[-1][1].coef_, pipeline_estimator_coef)

def test_max_samples_consistency():
    # Make sure validated max_samples and original max_samples are identical
    # when valid integer max_samples supplied by user
    max_samples = 100
    X, y = make_hastie_10_2(n_samples=2 * max_samples, random_state=1)
    )
    assert bagging._max_samples == max_samples

def test_set_oob_score_label_encoding():
    # Make sure the oob_score doesn't change when the labels change
    # See: https://github.com/scikit-learn/scikit-learn/issues/8933
    random_state = 5
    X = [[-1], [0], [1]] * 5
    Y1 = ["A", "B", "C"] * 5
    Y2 = [-1, 0, 1] * 5
    Y3 = [0, 1, 2] * 5
    x1 = (
        BaggingClassifier(oob_score=True, random_state=random_state)
        .fit(X, Y1)
        .oob_score_
    )
    x2 = (
        .fit(X, Y2)
    )
    x3 = (
        .fit(X, Y3)
    )
    assert [x1, x2] == [x3, x3]

def replace(X):
    X = X.astype("float", copy=True)
    X[~np.isfinite(X)] = 0
    return X

def test_bagging_regressor_with_missing_inputs():
    # Check that BaggingRegressor can accept X with missing/infinite data
        [
            [1, 3, 5],
            [2, None, 6],
            [2, np.nan, 6],
            [2, np.inf, 6],
            [2, -np.inf, 6],
        ]
    )
    y_values = [
        np.array([2, 3, 3, 3, 3]),
        np.array(
            [
                [2, 1, 9],
                [3, 6, 8],
                [3, 6, 8],
                [3, 6, 8],
                [3, 6, 8],
            ]
        ),
    ]
    for y in y_values:
        regressor = DecisionTreeRegressor()
        pipeline = make_pipeline(FunctionTransformer(replace), regressor)
        pipeline.fit(X, y).predict(X)
        bagging_regressor = BaggingRegressor(pipeline)
        y_hat = bagging_regressor.fit(X, y).predict(X)
        assert y.shape == y_hat.shape

        # Verify that exceptions can be raised by wrapper regressor
        pipeline = make_pipeline(regressor)
            pipeline.fit(X, y)
            bagging_regressor.fit(X, y)

def test_bagging_classifier_with_missing_inputs():
    # Check that BaggingClassifier can accept X with missing/infinite data
        [
            [1, 3, 5],
        ]
    )
    y = np.array([3, 6, 6, 6, 6])
    classifier = DecisionTreeClassifier()
    pipeline = make_pipeline(FunctionTransformer(replace), classifier)
    pipeline.fit(X, y).predict(X)
    bagging_classifier = BaggingClassifier(pipeline)
    bagging_classifier.fit(X, y)
    y_hat = bagging_classifier.predict(X)
    assert y.shape == y_hat.shape
    bagging_classifier.predict_log_proba(X)
    bagging_classifier.predict_proba(X)

    # Verify that exceptions can be raised by wrapper classifier
    pipeline = make_pipeline(classifier)
        pipeline.fit(X, y)
        bagging_classifier.fit(X, y)

def test_bagging_small_max_features():
    # Check that Bagging estimator can accept low fractional max_features

    X = np.array([[1, 2], [3, 4]])
    y = np.array([1, 0])

    bagging = BaggingClassifier(LogisticRegression(), max_features=0.3, random_state=1)

def test_bagging_get_estimators_indices(global_random_seed):
    # Check that Bagging estimator can generate sample indices properly
    # https://github.com/scikit-learn/scikit-learn/issues/16436

    X = rng.randn(13, 4)
    y = np.arange(13)

    class MyEstimator(DecisionTreeRegressor):
        """An estimator which stores y indices information at fit."""

            self._sample_indices = y

    clf = BaggingRegressor(estimator=MyEstimator(), n_estimators=1, random_state=0)

    assert_array_equal(clf.estimators_[0]._sample_indices, clf.estimators_samples_[0])

def test_bagging_allow_nan_tag(bagging, expected_allow_nan):
    """Check that bagging inherits allow_nan tag."""
    assert bagging.__sklearn_tags__().input_tags.allow_nan == expected_allow_nan

def test_bagging_with_metadata_routing(model):
    """Make sure that metadata routing works with non-default estimator."""
    model.fit(iris.data, iris.target)

def test_metadata_routing_with_dynamic_method_selection(sub_estimator, caller, callee):
    """Test that metadata routing works in `BaggingClassifier` with dynamic selection of
    the sub-estimator's methods. Here we test only specific test cases, where
    sub-estimator methods are not present and are not tested with `ConsumingClassifier`
    (which possesses all the methods) in
    sklearn/tests/test_metaestimators_metadata_routing.py: `BaggingClassifier.predict()`
    dynamically routes to `predict` if the sub-estimator doesn't have `predict_proba`
    and `BaggingClassifier.predict_log_proba()` dynamically routes to `predict_proba` if
    the sub-estimator doesn't have `predict_log_proba`, or to `predict`, if it doesn't
    have it.
    """
    X = np.array([[0, 2], [1, 4], [2, 6]])
    y = [1, 2, 3]
    sample_weight, metadata = [1], "a"
    registry = _Registry()
    estimator = sub_estimator(registry=registry)
    set_callee_request = "set_" + callee + "_request"
    getattr(estimator, set_callee_request)(sample_weight=True, metadata=True)

    bagging = BaggingClassifier(estimator=estimator)
    getattr(bagging, caller)(
        X=np.array([[1, 1], [1, 3], [0, 2]]),
        metadata=metadata,
    )

    for estimator in registry:
        check_recorded_metadata(
            obj=estimator,
            method=callee,
            parent=caller,
            metadata=metadata,
        )

def test_bagging_without_support_metadata_routing(model):
    """Make sure that we still can use an estimator that does not implement the
    metadata routing."""

def test_iforest(global_random_seed):
    """Check Isolation Forest for various parameter settings."""
    X_train = np.array([[0, 1], [1, 2]])
    X_test = np.array([[2, 1], [1, 1]])

        {"n_estimators": [3], "max_samples": [0.5, 1.0, 3], "bootstrap": [True, False]}
    )

            IsolationForest(random_state=global_random_seed, **params).fit(
                X_train

def test_iforest_sparse(global_random_seed, sparse_container):
    """Check IForest for various parameter settings on sparse input."""
    rng = check_random_state(global_random_seed)
    X_train, X_test = train_test_split(diabetes.data[:50], random_state=rng)
    grid = ParameterGrid({"max_samples": [0.5, 1.0], "bootstrap": [True, False]})

    for params in grid:
        sparse_classifier = IsolationForest(
            n_estimators=10, random_state=global_random_seed, **params
        ).fit(X_train_sparse)

        dense_classifier = IsolationForest(
        ).fit(X_train)
        dense_results = dense_classifier.predict(X_test)

        assert_array_equal(sparse_results, dense_results)

def test_iforest_error():
    """Test that it gives proper exception on deficient input."""

    # The dataset has less than 256 samples, explicitly setting
    # max_samples > n_samples should result in a warning. If not set
    # explicitly there should be no warning
    warn_msg = "max_samples will be set to n_samples for estimation"
        IsolationForest(max_samples=1000).fit(X)
        IsolationForest(max_samples="auto").fit(X)
        IsolationForest(max_samples=np.int64(2)).fit(X)

    # test X_test n_features match X_train one:
        IsolationForest().fit(X).predict(X[:, 1:])

def test_recalculate_max_depth():
    """Check max_depth recalculation when max_samples is reset to n_samples"""
    clf = IsolationForest().fit(X)
    for est in clf.estimators_:
        assert est.max_depth == int(np.ceil(np.log2(X.shape[0])))

def test_max_samples_attribute():
    assert clf.max_samples_ == X.shape[0]

    clf = IsolationForest(max_samples=500)
        clf.fit(X)

    clf = IsolationForest(max_samples=0.4).fit(X)
    assert clf.max_samples_ == 0.4 * X.shape[0]

def test_iforest_parallel_regression(global_random_seed):
    """Check parallel regression."""

    X_train, X_test = train_test_split(diabetes.data, random_state=rng)

    ensemble = IsolationForest(n_jobs=3, random_state=global_random_seed).fit(X_train)

    ensemble = IsolationForest(n_jobs=1, random_state=global_random_seed).fit(X_train)

def test_iforest_performance(global_random_seed):
    """Test Isolation Forest performs well"""

    # Generate train/test data
    X = 0.3 * rng.randn(600, 2)
    X = rng.permutation(np.vstack((X + 2, X - 2)))
    X_train = X[:1000]

    # Generate some abnormal novel observations
    X_outliers = rng.uniform(low=-1, high=1, size=(200, 2))
    X_test = np.vstack((X[1000:], X_outliers))
    y_test = np.array([0] * 200 + [1] * 200)

    # fit the model
    clf = IsolationForest(max_samples=100, random_state=rng).fit(X_train)

    # predict scores (the lower, the more normal)
    y_pred = -clf.decision_function(X_test)

    # check that there is at most 6 errors (false positive or false negative)
    assert roc_auc_score(y_test, y_pred) > 0.98

def test_iforest_works(contamination, global_random_seed):
    # toy sample (the last two samples are outliers)
    X = [[-2, -1], [-1, -1], [-1, -2], [1, 1], [1, 2], [2, 1], [7, 4], [-5, 9]]

    # Test IsolationForest
    clf = IsolationForest(random_state=global_random_seed, contamination=contamination)
    decision_func = -clf.decision_function(X)
    pred = clf.predict(X)
    # assert detect outliers:
    assert np.min(decision_func[-2:]) > np.max(decision_func[:-2])
    assert_array_equal(pred, 6 * [1] + 2 * [-1])

    # Make sure validated max_samples in iforest and BaseBagging are identical
    assert clf.max_samples_ == clf._max_samples

def test_iforest_subsampled_features():
    # It tests non-regression for #5732 which failed at predict.
    )
    clf = IsolationForest(max_features=0.8)

def test_iforest_average_path_length():
    # It tests non-regression for #8549 which used the wrong formula
    # for average path length, strictly for the integer case
    # Updated to check average path length when input is <= 2 (issue #11839)
    result_one = 2.0 * (np.log(4.0) + np.euler_gamma) - 2.0 * 4.0 / 5.0
    result_two = 2.0 * (np.log(998.0) + np.euler_gamma) - 2.0 * 998.0 / 999.0
    assert_allclose(_average_path_length([0]), [0.0])
    assert_allclose(_average_path_length([1]), [0.0])
    assert_allclose(_average_path_length([2]), [1.0])
    assert_allclose(_average_path_length([5]), [result_one])
    assert_allclose(_average_path_length([999]), [result_two])
        _average_path_length(np.array([1, 2, 5, 999])),
        [0.0, 1.0, result_one, result_two],
    )
    # _average_path_length is increasing
    avg_path_length = _average_path_length(np.arange(5))
    assert_array_equal(avg_path_length, np.sort(avg_path_length))

    clf1 = IsolationForest(contamination=0.1).fit(X_train)
    clf2 = IsolationForest().fit(X_train)
    )
    )
    )

def test_iforest_warm_start():
    """Test iterative addition of iTrees to an iForest"""

    X = rng.randn(20, 2)

    # fit first 10 trees
    clf = IsolationForest(
        n_estimators=10, max_samples=20, random_state=rng, warm_start=True
    )
    # remember the 1st tree
    tree_1 = clf.estimators_[0]
    # fit another 10 trees
    clf.set_params(n_estimators=20)
    # expecting 20 fitted trees and no overwritten trees
    assert len(clf.estimators_) == 20
    assert clf.estimators_[0] is tree_1

def test_iforest_chunks_works1(
    mocked_get_chunk, contamination, n_predict_calls, global_random_seed
):
    test_iforest_works(contamination, global_random_seed)
    assert mocked_get_chunk.call_count == n_predict_calls

def test_iforest_chunks_works2(
):

def test_iforest_with_uniform_data():
    """Test whether iforest predicts inliers when using uniform data"""

    # 2-d array of all 1s
    X = np.ones((100, 10))
    iforest = IsolationForest()
    iforest.fit(X)

    assert all(iforest.predict(X) == 1)
    assert all(iforest.predict(rng.randn(100, 10)) == 1)
    assert all(iforest.predict(X + 1) == 1)
    assert all(iforest.predict(X - 1) == 1)

    # 2-d array where columns contain the same value across rows
    X = np.repeat(rng.randn(1, 10), 100, 0)

    assert all(iforest.predict(np.ones((100, 10))) == 1)

    # Single row
    X = rng.randn(1, 10)

def test_iforest_with_n_jobs_does_not_segfault(csc_container):
    """Check that Isolation Forest does not segfault with n_jobs=2

    Non-regression test for #23252
    """
    X, _ = make_classification(n_samples=85_000, n_features=100, random_state=0)
    X = csc_container(X)
    IsolationForest(n_estimators=10, max_samples=256, n_jobs=2).fit(X)

def test_iforest_preserve_feature_names():
    """Check that feature names are preserved when contamination is not "auto".

    Feature names are required for consistency checks during scoring.

    Non-regression test for Issue #25844
    """

    X = pd.DataFrame(data=rng.randn(4), columns=["a"])
    model = IsolationForest(random_state=0, contamination=0.05)

        model.fit(X)

def test_iforest_sparse_input_float_contamination(sparse_container):
    """Check that `IsolationForest` accepts sparse matrix input and float value for
    contamination.

    https://github.com/scikit-learn/scikit-learn/issues/27626
    """
    X, _ = make_classification(n_samples=50, n_features=4, random_state=0)
    X = sparse_container(X)
    X.sort_indices()
    contamination = 0.1
    iforest = IsolationForest(
        n_estimators=5, contamination=contamination, random_state=0
    ).fit(X)

    X_decision = iforest.decision_function(X)
    assert (X_decision < 0).sum() / X.shape[0] == pytest.approx(contamination)

def test_iforest_predict_parallel(global_random_seed, contamination, n_jobs):
    """Check that `IsolationForest.predict` is parallelized."""

        random_state=global_random_seed, contamination=contamination, n_jobs=None
    )

    clf_parallel = IsolationForest(
        random_state=global_random_seed, contamination=contamination, n_jobs=-1
    )
    clf_parallel.fit(X)
    with parallel_backend("threading", n_jobs=n_jobs):
        pred_paralell = clf_parallel.predict(X)

    # assert the same results as non-parallel
    assert_array_equal(pred, pred_paralell)

class GaussianProcessRegressor(MultiOutputMixin, RegressorMixin, BaseEstimator):
    """Gaussian process regression (GPR).

    The implementation is based on Algorithm 2.1 of [RW2006]_.

    In addition to standard scikit-learn estimator API,
    :class:`GaussianProcessRegressor`:

    * allows prediction without prior fitting (based on the GP prior)
    * provides an additional method `sample_y(X)`, which evaluates samples
      drawn from the GPR (prior or posterior) at given inputs
    * exposes a method `log_marginal_likelihood(theta)`, which can be used
      externally for other ways of selecting hyperparameters, e.g., via
      Markov chain Monte Carlo.

    To learn the difference between a point-estimate approach vs. a more
    Bayesian modelling approach, refer to the example entitled
    :ref:`sphx_glr_auto_examples_gaussian_process_plot_compare_gpr_krr.py`.

    Read more in the :ref:`User Guide <gaussian_process>`.

    kernel : kernel instance, default=None
        The kernel specifying the covariance function of the GP. If None is
        passed, the kernel ``ConstantKernel(1.0, constant_value_bounds="fixed")
        * RBF(1.0, length_scale_bounds="fixed")`` is used as default. Note that
        the kernel hyperparameters are optimized during fitting unless the
        bounds are marked as "fixed".

    alpha : float or ndarray of shape (n_samples,), default=1e-10
        Value added to the diagonal of the kernel matrix during fitting.
        This can prevent a potential numerical issue during fitting, by
        ensuring that the calculated values form a positive definite matrix.
        It can also be interpreted as the variance of additional Gaussian
        measurement noise on the training observations. Note that this is
        different from using a `WhiteKernel`. If an array is passed, it must
        have the same number of entries as the data used for fitting and is
        used as datapoint-dependent noise level. Allowing to specify the
        noise level directly as a parameter is mainly for convenience and
        for consistency with :class:`~sklearn.linear_model.Ridge`.
        For an example illustrating how the alpha parameter controls
        the noise variance in Gaussian Process Regression, see
        :ref:`sphx_glr_auto_examples_gaussian_process_plot_gpr_noisy_targets.py`.

    optimizer : "fmin_l_bfgs_b", callable or None, default="fmin_l_bfgs_b"
        Can either be one of the internally supported optimizers for optimizing
        the kernel's parameters, specified by a string, or an externally
        defined optimizer passed as a callable. If a callable is passed, it
        must have the signature::

            def optimizer(obj_func, initial_theta, bounds):
                # * 'obj_func': the objective function to be minimized, which
                #   takes the hyperparameters theta as a parameter and an
                #   optional flag eval_gradient, which determines if the
                #   gradient is returned additionally to the function value
                # * 'initial_theta': the initial value for theta, which can be
                #   used by local optimizers
                # * 'bounds': the bounds on the values of theta
                ....
                # Returned are the best found hyperparameters theta and
                # the corresponding value of the target function.
                return theta_opt, func_min

        Per default, the L-BFGS-B algorithm from `scipy.optimize.minimize`
        is used. If None is passed, the kernel's parameters are kept fixed.
        Available internal optimizers are: `{'fmin_l_bfgs_b'}`.

    n_restarts_optimizer : int, default=0
        The number of restarts of the optimizer for finding the kernel's
        parameters which maximize the log-marginal likelihood. The first run
        of the optimizer is performed from the kernel's initial parameters,
        the remaining ones (if any) from thetas sampled log-uniform randomly
        from the space of allowed theta-values. If greater than 0, all bounds
        must be finite. Note that `n_restarts_optimizer == 0` implies that one
        run is performed.

    normalize_y : bool, default=False
        Whether or not to normalize the target values `y` by removing the mean
        and scaling to unit-variance. This is recommended for cases where
        zero-mean, unit-variance priors are used. Note that, in this
        implementation, the normalisation is reversed before the GP predictions
        are reported.

    copy_X_train : bool, default=True
        If True, a persistent copy of the training data is stored in the
        object. Otherwise, just a reference to the training data is stored,
        which might cause predictions to change if the data is modified
        externally.

    n_targets : int, default=None
        The number of dimensions of the target values. Used to decide the number
        of outputs when sampling from the prior distributions (i.e. calling
        :meth:`sample_y` before :meth:`fit`). This parameter is ignored once
        :meth:`fit` has been called.

        Determines random number generation used to initialize the centers.

    X_train_ : array-like of shape (n_samples, n_features) or list of object
        Feature vectors or other representations of training data (also
        required for prediction).

    y_train_ : array-like of shape (n_samples,) or (n_samples, n_targets)
        Target values in training data (also required for prediction).

    kernel_ : kernel instance
        The kernel used for prediction. The structure of the kernel is the
        same as the one passed as parameter but with optimized hyperparameters.

    L_ : array-like of shape (n_samples, n_samples)
        Lower-triangular Cholesky decomposition of the kernel in ``X_train_``.

    alpha_ : array-like of shape (n_samples,)
        Dual coefficients of training data points in kernel space.

    log_marginal_likelihood_value_ : float
        The log-marginal-likelihood of ``self.kernel_.theta``.

    See Also
    --------
    GaussianProcessClassifier : Gaussian process classification (GPC)
        based on Laplace approximation.

    .. [RW2006] `Carl E. Rasmussen and Christopher K.I. Williams,
       "Gaussian Processes for Machine Learning",
       MIT Press 2006 <https://www.gaussianprocess.org/gpml/chapters/RW.pdf>`_

    Examples
    --------
    >>> from sklearn.datasets import make_friedman2
    >>> from sklearn.gaussian_process import GaussianProcessRegressor
    >>> from sklearn.gaussian_process.kernels import DotProduct, WhiteKernel
    >>> X, y = make_friedman2(n_samples=500, noise=0, random_state=0)
    >>> kernel = DotProduct() + WhiteKernel()
    >>> gpr = GaussianProcessRegressor(kernel=kernel,
    ...         random_state=0).fit(X, y)
    >>> gpr.score(X, y)
    0.3680...
    >>> gpr.predict(X[:2,:], return_std=True)
    (array([653.0..., 592.1...]), array([316.6..., 316.6...]))
    """

        "kernel": [None, Kernel],
        "alpha": [Interval(Real, 0, None, closed="left"), np.ndarray],
        "optimizer": [StrOptions({"fmin_l_bfgs_b"}), callable, None],
        "n_restarts_optimizer": [Interval(Integral, 0, None, closed="left")],
        "normalize_y": ["boolean"],
        "copy_X_train": ["boolean"],
        "n_targets": [Interval(Integral, 1, None, closed="left"), None],
    }

        self,
        kernel=None,
        *,
        alpha=1e-10,
        optimizer="fmin_l_bfgs_b",
        n_restarts_optimizer=0,
        normalize_y=False,
        copy_X_train=True,
        n_targets=None,
    ):
        self.optimizer = optimizer
        self.n_restarts_optimizer = n_restarts_optimizer
        self.normalize_y = normalize_y
        self.copy_X_train = copy_X_train
        self.n_targets = n_targets

        """Fit Gaussian process regression model.

        X : array-like of shape (n_samples, n_features) or list of object
            Feature vectors or other representations of training data.

        Returns
        -------
            GaussianProcessRegressor class instance.
        """
        if self.kernel is None:  # Use an RBF kernel as default
            self.kernel_ = C(1.0, constant_value_bounds="fixed") * RBF(
                1.0, length_scale_bounds="fixed"
            )
        else:
            self.kernel_ = clone(self.kernel)

        self._rng = check_random_state(self.random_state)

        if self.kernel_.requires_vector_input:
            dtype, ensure_2d = "numeric", True
        else:
            dtype, ensure_2d = None, False
            self,
            X,
            y,
            y_numeric=True,
            ensure_2d=ensure_2d,
            dtype=dtype,
        )

        n_targets_seen = y.shape[1] if y.ndim > 1 else 1
        if self.n_targets is not None and n_targets_seen != self.n_targets:
                "The number of targets seen in `y` is different from the parameter "
                f"`n_targets`. Got {n_targets_seen} != {self.n_targets}."
            )

        # Normalize target value
        if self.normalize_y:
            self._y_train_mean = np.mean(y, axis=0)
            self._y_train_std = _handle_zeros_in_scale(np.std(y, axis=0), copy=False)

            # Remove mean and make unit variance
            y = (y - self._y_train_mean) / self._y_train_std

        else:
            shape_y_stats = (y.shape[1],) if y.ndim == 2 else 1
            self._y_train_mean = np.zeros(shape=shape_y_stats)
            self._y_train_std = np.ones(shape=shape_y_stats)

        if np.iterable(self.alpha) and self.alpha.shape[0] != y.shape[0]:
            if self.alpha.shape[0] == 1:
                self.alpha = self.alpha[0]
            else:
                    "alpha must be a scalar or an array with same number of "
                    f"entries as y. ({self.alpha.shape[0]} != {y.shape[0]})"
                )

        self.X_train_ = np.copy(X) if self.copy_X_train else X
        self.y_train_ = np.copy(y) if self.copy_X_train else y

        if self.optimizer is not None and self.kernel_.n_dims > 0:
            # Choose hyperparameters based on maximizing the log-marginal
            # likelihood (potentially starting from several initial values)
            def obj_func(theta, eval_gradient=True):
                if eval_gradient:
                    lml, grad = self.log_marginal_likelihood(
                        theta, eval_gradient=True, clone_kernel=False
                    )
                    return -lml, -grad
                else:
                    return -self.log_marginal_likelihood(theta, clone_kernel=False)

            # First optimize starting from theta specified in kernel
            optima = [
                (
                    self._constrained_optimization(
                        obj_func, self.kernel_.theta, self.kernel_.bounds
                    )
                )
            ]

            # Additional runs are performed from log-uniform chosen initial
            # theta
            if self.n_restarts_optimizer > 0:
                if not np.isfinite(self.kernel_.bounds).all():
                        "Multiple optimizer restarts (n_restarts_optimizer>0) "
                        "requires that all bounds are finite."
                    )
                bounds = self.kernel_.bounds
                for iteration in range(self.n_restarts_optimizer):
                    theta_initial = self._rng.uniform(bounds[:, 0], bounds[:, 1])
                    optima.append(
                        self._constrained_optimization(obj_func, theta_initial, bounds)
                    )
            # Select result from run with minimal (negative) log-marginal
            # likelihood
            lml_values = list(map(itemgetter(1), optima))
            self.kernel_.theta = optima[np.argmin(lml_values)][0]
            self.kernel_._check_bounds_params()

            self.log_marginal_likelihood_value_ = -np.min(lml_values)
        else:
            self.log_marginal_likelihood_value_ = self.log_marginal_likelihood(
                self.kernel_.theta, clone_kernel=False
            )

        # Precompute quantities required for predictions which are independent
        # of actual query points
        # Alg. 2.1, page 19, line 2 -> L = cholesky(K + sigma^2 I)
        K = self.kernel_(self.X_train_)
        K[np.diag_indices_from(K)] += self.alpha
        try:
            self.L_ = cholesky(K, lower=GPR_CHOLESKY_LOWER, check_finite=False)
        except np.linalg.LinAlgError as exc:
            exc.args = (
                (
                    f"The kernel, {self.kernel_}, is not returning a positive "
                    "definite matrix. Try gradually increasing the 'alpha' "
                    "parameter of your GaussianProcessRegressor estimator."
                ),
            ) + exc.args
            raise
        # Alg 2.1, page 19, line 3 -> alpha = L^T \ (L \ y)
        self.alpha_ = cho_solve(
            (self.L_, GPR_CHOLESKY_LOWER),
            self.y_train_,
            check_finite=False,
        )

    def predict(self, X, return_std=False, return_cov=False):
        """Predict using the Gaussian process regression model.

        We can also predict based on an unfitted model by using the GP prior.
        In addition to the mean of the predictive distribution, optionally also
        returns its standard deviation (`return_std=True`) or covariance
        (`return_cov=True`). Note that at most one of the two can be requested.

            Query points where the GP is evaluated.

            If True, the standard-deviation of the predictive distribution at
            the query points is returned along with the mean.

        return_cov : bool, default=False
            If True, the covariance of the joint predictive distribution at

        Returns
        -------
        y_mean : ndarray of shape (n_samples,) or (n_samples, n_targets)
            Mean of predictive distribution at query points.

        y_std : ndarray of shape (n_samples,) or (n_samples, n_targets), optional
            Standard deviation of predictive distribution at query points.
            Only returned when `return_std` is True.

        y_cov : ndarray of shape (n_samples, n_samples) or \
                (n_samples, n_samples, n_targets), optional
            Covariance of joint predictive distribution at query points.
            Only returned when `return_cov` is True.
        """
        if return_std and return_cov:
                "At most one of return_std or return_cov can be requested."
            )

        if self.kernel is None or self.kernel.requires_vector_input:
        else:

        X = validate_data(self, X, ensure_2d=ensure_2d, dtype=dtype, reset=False)

        if not hasattr(self, "X_train_"):  # Unfitted;predict based on GP prior
            if self.kernel is None:
                kernel = C(1.0, constant_value_bounds="fixed") * RBF(
                    1.0, length_scale_bounds="fixed"
                )
            else:
                kernel = self.kernel

            n_targets = self.n_targets if self.n_targets is not None else 1
            y_mean = np.zeros(shape=(X.shape[0], n_targets)).squeeze()

            if return_cov:
                y_cov = kernel(X)
                if n_targets > 1:
                    y_cov = np.repeat(
                        np.expand_dims(y_cov, -1), repeats=n_targets, axis=-1
                    )
                return y_mean, y_cov
            elif return_std:
                y_var = kernel.diag(X)
                    y_var = np.repeat(
                        np.expand_dims(y_var, -1), repeats=n_targets, axis=-1
                    )
                return y_mean, np.sqrt(y_var)
            else:
                return y_mean
        else:  # Predict based on GP posterior
            # Alg 2.1, page 19, line 4 -> f*_bar = K(X_test, X_train) . alpha
            K_trans = self.kernel_(X, self.X_train_)
            y_mean = K_trans @ self.alpha_

            # undo normalisation
            y_mean = self._y_train_std * y_mean + self._y_train_mean

            # if y_mean has shape (n_samples, 1), reshape to (n_samples,)
            if y_mean.ndim > 1 and y_mean.shape[1] == 1:
                y_mean = np.squeeze(y_mean, axis=1)

            # Alg 2.1, page 19, line 5 -> v = L \ K(X_test, X_train)^T
            V = solve_triangular(
                self.L_, K_trans.T, lower=GPR_CHOLESKY_LOWER, check_finite=False
            )

                # Alg 2.1, page 19, line 6 -> K(X_test, X_test) - v^T. v
                y_cov = self.kernel_(X) - V.T @ V

                # undo normalisation
                y_cov = np.outer(y_cov, self._y_train_std**2).reshape(*y_cov.shape, -1)
                # if y_cov has shape (n_samples, n_samples, 1), reshape to
                # (n_samples, n_samples)
                if y_cov.shape[2] == 1:
                    y_cov = np.squeeze(y_cov, axis=2)

                # Compute variance of predictive distribution
                # Use einsum to avoid explicitly forming the large matrix
                # V^T @ V just to extract its diagonal afterward.
                y_var = self.kernel_.diag(X).copy()
                y_var -= np.einsum("ij,ji->i", V.T, V)

                # Check if any of the variances is negative because of
                # numerical issues. If yes: set the variance to 0.
                y_var_negative = y_var < 0
                if np.any(y_var_negative):
                        "Predicted variances smaller than 0. "
                        "Setting those variances to 0."
                    )
                    y_var[y_var_negative] = 0.0

                y_var = np.outer(y_var, self._y_train_std**2).reshape(*y_var.shape, -1)

                # if y_var has shape (n_samples, 1), reshape to (n_samples,)
                if y_var.shape[1] == 1:
                    y_var = np.squeeze(y_var, axis=1)

            else:

    def sample_y(self, X, n_samples=1, random_state=0):
        """Draw samples from Gaussian process and evaluate at X.

        X : array-like of shape (n_samples_X, n_features) or list of object

        n_samples : int, default=1
            Number of samples drawn from the Gaussian process per query point.

        random_state : int, RandomState instance or None, default=0
            Determines random number generation to randomly draw samples.
            Pass an int for reproducible results across multiple function
            calls.
            See :term:`Glossary <random_state>`.

        Returns
        -------
        y_samples : ndarray of shape (n_samples_X, n_samples), or \
            (n_samples_X, n_targets, n_samples)
            Values of n_samples samples drawn from Gaussian process and
            evaluated at query points.
        """
        rng = check_random_state(random_state)

        y_mean, y_cov = self.predict(X, return_cov=True)
        if y_mean.ndim == 1:
            y_samples = rng.multivariate_normal(y_mean, y_cov, n_samples).T
        else:
            y_samples = [
                rng.multivariate_normal(
                    y_mean[:, target], y_cov[..., target], n_samples
                ).T[:, np.newaxis]
                for target in range(y_mean.shape[1])
            ]
            y_samples = np.hstack(y_samples)
        return y_samples

    def log_marginal_likelihood(
        self, theta=None, eval_gradient=False, clone_kernel=True
    ):
        """Return log-marginal likelihood of theta for training data.

        theta : array-like of shape (n_kernel_params,) default=None
            Kernel hyperparameters for which the log-marginal likelihood is
            evaluated. If None, the precomputed log_marginal_likelihood
            of ``self.kernel_.theta`` is returned.

        eval_gradient : bool, default=False
            If True, the gradient of the log-marginal likelihood with respect
            to the kernel hyperparameters at position theta is returned
            additionally. If True, theta must not be None.

        clone_kernel : bool, default=True
            If True, the kernel attribute is copied. If False, the kernel
            attribute is modified, but may result in a performance improvement.

        Returns
        -------
        log_likelihood : float
            Log-marginal likelihood of theta for training data.

        log_likelihood_gradient : ndarray of shape (n_kernel_params,), optional
            Gradient of the log-marginal likelihood with respect to the kernel
            hyperparameters at position theta.
            Only returned when eval_gradient is True.
        """
        if theta is None:
            if eval_gradient:
                raise ValueError("Gradient can only be evaluated for theta!=None")
            return self.log_marginal_likelihood_value_

        if clone_kernel:
            kernel = self.kernel_.clone_with_theta(theta)
        else:
            kernel = self.kernel_
            kernel.theta = theta

        if eval_gradient:
            K, K_gradient = kernel(self.X_train_, eval_gradient=True)
        else:
            K = kernel(self.X_train_)

        try:
            L = cholesky(K, lower=GPR_CHOLESKY_LOWER, check_finite=False)
        except np.linalg.LinAlgError:
            return (-np.inf, np.zeros_like(theta)) if eval_gradient else -np.inf

        # Support multi-dimensional output of self.y_train_
        y_train = self.y_train_
        if y_train.ndim == 1:
            y_train = y_train[:, np.newaxis]

        alpha = cho_solve((L, GPR_CHOLESKY_LOWER), y_train, check_finite=False)

        # Alg 2.1, page 19, line 7
        # -0.5 . y^T . alpha - sum(log(diag(L))) - n_samples / 2 log(2*pi)
        # y is originally thought to be a (1, n_samples) row vector. However,
        # in multioutputs, y is of shape (n_samples, 2) and we need to compute
        # y^T . alpha for each output, independently using einsum. Thus, it
        # is equivalent to:
        # for output_idx in range(n_outputs):
        #     log_likelihood_dims[output_idx] = (
        #         y_train[:, [output_idx]] @ alpha[:, [output_idx]]
        #     )
        log_likelihood_dims = -0.5 * np.einsum("ik,ik->k", y_train, alpha)
        log_likelihood_dims -= np.log(np.diag(L)).sum()
        log_likelihood_dims -= K.shape[0] / 2 * np.log(2 * np.pi)
        # the log likelihood is sum-up across the outputs
        log_likelihood = log_likelihood_dims.sum(axis=-1)

            # Eq. 5.9, p. 114, and footnote 5 in p. 114
            # 0.5 * trace((alpha . alpha^T - K^-1) . K_gradient)
            # alpha is supposed to be a vector of (n_samples,) elements. With
            # multioutputs, alpha is a matrix of size (n_samples, n_outputs).
            # Therefore, we want to construct a matrix of
            # (n_samples, n_samples, n_outputs) equivalent to
            # for output_idx in range(n_outputs):
            #     output_alpha = alpha[:, [output_idx]]
            #     inner_term[..., output_idx] = output_alpha @ output_alpha.T
            inner_term = np.einsum("ik,jk->ijk", alpha, alpha)
            # compute K^-1 of shape (n_samples, n_samples)
            K_inv = cho_solve(
                (L, GPR_CHOLESKY_LOWER), np.eye(K.shape[0]), check_finite=False
            )
            # create a new axis to use broadcasting between inner_term and
            # K_inv
            inner_term -= K_inv[..., np.newaxis]
            # Since we are interested about the trace of
            # inner_term @ K_gradient, we don't explicitly compute the
            # matrix-by-matrix operation and instead use an einsum. Therefore
            # it is equivalent to:
            # for param_idx in range(n_kernel_params):
            #     for output_idx in range(n_output):
            #         log_likehood_gradient_dims[param_idx, output_idx] = (
            #             inner_term[..., output_idx] @
            #             K_gradient[..., param_idx]
            #         )
            log_likelihood_gradient_dims = 0.5 * np.einsum(
                "ijl,jik->kl", inner_term, K_gradient
            )
            # the log likelihood gradient is the sum-up across the outputs
            log_likelihood_gradient = log_likelihood_gradient_dims.sum(axis=-1)

            return log_likelihood, log_likelihood_gradient
        else:
            return log_likelihood

    def _constrained_optimization(self, obj_func, initial_theta, bounds):
        if self.optimizer == "fmin_l_bfgs_b":
            opt_res = scipy.optimize.minimize(
                obj_func,
                initial_theta,
                method="L-BFGS-B",
                jac=True,
                bounds=bounds,
            )
            _check_optimize_result("lbfgs", opt_res)
            theta_opt, func_min = opt_res.x, opt_res.fun
        elif callable(self.optimizer):
            theta_opt, func_min = self.optimizer(obj_func, initial_theta, bounds=bounds)
        else:
            raise ValueError(f"Unknown optimizer {self.optimizer}.")

        return theta_opt, func_min

        self,
        *,
    ):

        Returns
        -------
        """
            )
        else:

        else:
            self,
            X,
            y,
        )

            )

        else:

            else:
                )

                    )
                else:

            optima = [
                (
                    )
                )
            ]

            # theta
                    )
                    )

        else:
            )

        try:
                (
                ),
            raise
        )

        Returns
        -------

        """
            )

        else:

                )
            else:

                    )
                    )
            else:

            )

                    )

            else:

            calls.

        Returns
        -------
        """

        else:
            ]

    ):

        Returns
        -------

        """

        else:

        else:

        try:

        #     )

            )
            # K_inv
            #         )
            )

        else:

                obj_func,
                jac=True,
            )
        else:

                    )
                else:

class _BinaryGaussianProcessClassifierLaplace(BaseEstimator):
    """Binary Gaussian process classification based on Laplace approximation.

    The implementation is based on Algorithm 3.1, 3.2, and 5.1 from [RW2006]_.

    Internally, the Laplace approximation is used for approximating the
    non-Gaussian posterior by a Gaussian.

    Currently, the implementation is restricted to using the logistic link
    function.

        passed, the kernel "1.0 * RBF(1.0)" is used as default. Note that
        the kernel's hyperparameters are optimized during fitting.

    optimizer : 'fmin_l_bfgs_b' or callable, default='fmin_l_bfgs_b'
        must have the  signature::

                # * 'obj_func' is the objective function to be maximized, which
                #   takes the hyperparameters theta as parameter and an
                ....

        Per default, the 'L-BFGS-B' algorithm from scipy.optimize.minimize
        Available internal optimizers are::

            'fmin_l_bfgs_b'

        must be finite. Note that n_restarts_optimizer=0 implies that one

    max_iter_predict : int, default=100
        The maximum number of iterations in Newton's method for approximating
        the posterior during predict. Smaller values will reduce computation
        time at the cost of worse results.

        If warm-starts are enabled, the solution of the last Newton iteration
        on the Laplace approximation of the posterior mode is used as
        initialization for the next call of _posterior_mode(). This can speed
        up convergence when _posterior_mode is called several times on similar
        problems as in hyperparameter optimization. See :term:`the Glossary
        <warm_start>`.

    y_train_ : array-like of shape (n_samples,)
        Target values in training data (also required for prediction)

    kernel_ : kernl instance
        same as the one passed as parameter but with optimized hyperparameters

        Lower-triangular Cholesky decomposition of the kernel in X_train_

    pi_ : array-like of shape (n_samples,)
        The probabilities of the positive class for the training points
        X_train_

    W_sr_ : array-like of shape (n_samples,)
        Square root of W, the Hessian of log-likelihood of the latent function
        values for the observed labels. Since W is diagonal, only the diagonal
        of sqrt(W) is stored.

        The log-marginal-likelihood of ``self.kernel_.theta``

    """

        self,
        *,
        max_iter_predict=100,
    ):
        self.max_iter_predict = max_iter_predict

        """Fit Gaussian process classification model.

            Target values, must be binary.

        Returns
        -------
        self : returns an instance of self.
        """
            )
        else:

        self.rng = check_random_state(self.random_state)

        # Encode class labels and check that it is a binary classification
        # problem
        self.y_train_ = label_encoder.fit_transform(y)
        if self.classes_.size > 2:
                "%s supports only binary classification. y contains classes %s"
                % (self.__class__.__name__, self.classes_)
            )
        elif self.classes_.size == 1:
                "{0:s} requires 2 classes; got {1:d} class".format(
                    self.__class__.__name__, self.classes_.size
                )
            )

                    )
                else:

            optima = [
                self._constrained_optimization(
                    obj_func, self.kernel_.theta, self.kernel_.bounds
                )
            ]

            # theta
                    )
                    theta_initial = np.exp(self.rng.uniform(bounds[:, 0], bounds[:, 1]))
                    )

        else:
                self.kernel_.theta
            )

        _, (self.pi_, self.W_sr_, self.L_, _, _) = self._posterior_mode(
            K, return_temporaries=True
        )

            Query points where the GP is evaluated for classification.

        Returns
        -------
            Predicted target values for X, values are from ``classes_``
        """

        # As discussed on Section 3.4.2 of GPML, for making hard binary
        # decisions, it is enough to compute the MAP of the posterior and
        # pass it through the link function
        K_star = self.kernel_(self.X_train_, X)  # K_star =k(x_star)
        f_star = K_star.T.dot(self.y_train_ - self.pi_)  # Algorithm 3.2,Line 4

        return np.where(f_star > 0, self.classes_[1], self.classes_[0])

        """Return probability estimates for the test vector X.

        Returns
        -------
            order, as they appear in the attribute ``classes_``.
        """

        # Based on Algorithm 3.2 of GPML
        f_star = K_star.T.dot(self.y_train_ - self.pi_)  # Line 4
        v = solve(self.L_, self.W_sr_[:, np.newaxis] * K_star)  # Line 5
        # Line 6 (compute np.diag(v.T.dot(v)) via einsum)
        var_f_star = self.kernel_.diag(X) - np.einsum("ij,ij->j", v, v)

        # Line 7:
        # Approximate \int log(z) * N(z | f_star, var_f_star)
        # Approximation is due to Williams & Barber, "Bayesian Classification
        # with Gaussian Processes", Appendix A: Approximate the logistic
        # sigmoid by a linear combination of 5 error functions.
        # For information on how this integral can be computed see
        # blitiri.blogspot.de/2012/11/gaussian-integral-of-error-function.html
        alpha = 1 / (2 * var_f_star)
        gamma = LAMBDAS * f_star
        integrals = (
            np.sqrt(np.pi / alpha)
            * erf(gamma * np.sqrt(alpha / (alpha + LAMBDAS**2)))
            / (2 * np.sqrt(var_f_star * 2 * np.pi))
        )
        pi_star = (COEFS * integrals).sum(axis=0) + 0.5 * COEFS.sum()

        return np.vstack((1 - pi_star, pi_star)).T

    ):
        """Returns log-marginal likelihood of theta for training data.

        theta : array-like of shape (n_kernel_params,), default=None

        Returns
        -------

        log_likelihood_gradient : ndarray of shape (n_kernel_params,), \
                optional
            Only returned when `eval_gradient` is True.
        """

        else:

        else:

        # Compute log-marginal-likelihood Z and also store some temporaries
        # which can be reused for computing Z's gradient
        Z, (pi, W_sr, L, b, a) = self._posterior_mode(K, return_temporaries=True)

        if not eval_gradient:
            return Z

        # Compute gradient based on Algorithm 5.1 of GPML
        d_Z = np.empty(theta.shape[0])
        # XXX: Get rid of the np.diag() in the next line
        R = W_sr[:, np.newaxis] * cho_solve((L, True), np.diag(W_sr))  # Line 7
        C = solve(L, W_sr[:, np.newaxis] * K)  # Line 8
        # Line 9: (use einsum to compute np.diag(C.T.dot(C))))
        s_2 = (
            -0.5
            * (np.diag(K) - np.einsum("ij, ij -> j", C, C))
            * (pi * (1 - pi) * (1 - 2 * pi))
        )  # third derivative

        for j in range(d_Z.shape[0]):
            C = K_gradient[:, :, j]  # Line 11
            # Line 12: (R.T.ravel().dot(C.ravel()) = np.trace(R.dot(C)))
            s_1 = 0.5 * a.T.dot(C).dot(a) - 0.5 * R.T.ravel().dot(C.ravel())

            b = C.dot(self.y_train_ - pi)  # Line 13
            s_3 = b - K.dot(R.dot(b))  # Line 14

            d_Z[j] = s_1 + s_2.T.dot(s_3)  # Line 15

        return Z, d_Z

    def _posterior_mode(self, K, return_temporaries=False):
        """Mode-finding for binary Laplace GPC and fixed kernel.

        This approximates the posterior of the latent function values for given
        inputs and target observations with a Gaussian approximation and uses
        Newton's iteration to find the mode of this approximation.
        """
        # Based on Algorithm 3.1 of GPML

        # If warm_start are enabled, we reuse the last solution for the
        # posterior mode as initialization; otherwise, we initialize with 0
        if (
            self.warm_start
            and hasattr(self, "f_cached")
            and self.f_cached.shape == self.y_train_.shape
        ):
            f = self.f_cached
        else:
            f = np.zeros_like(self.y_train_, dtype=np.float64)

        # Use Newton's iteration method to find mode of Laplace approximation
        log_marginal_likelihood = -np.inf
        for _ in range(self.max_iter_predict):
            # Line 4
            pi = expit(f)
            W = pi * (1 - pi)
            # Line 5
            W_sr = np.sqrt(W)
            W_sr_K = W_sr[:, np.newaxis] * K
            B = np.eye(W.shape[0]) + W_sr_K * W_sr
            L = cholesky(B, lower=True)
            # Line 6
            b = W * f + (self.y_train_ - pi)
            # Line 7
            a = b - W_sr * cho_solve((L, True), W_sr_K.dot(b))
            # Line 8
            f = K.dot(a)

            # Line 10: Compute log marginal likelihood in loop and use as
            #          convergence criterion
            lml = (
                -0.5 * a.T.dot(f)
                - np.log1p(np.exp(-(self.y_train_ * 2 - 1) * f)).sum()
                - np.log(np.diag(L)).sum()
            )
            # Check if we have converged (log marginal likelihood does
            # not decrease)
            # XXX: more complex convergence criterion
            if lml - log_marginal_likelihood < 1e-10:
                break
            log_marginal_likelihood = lml

        self.f_cached = f  # Remember solution for later warm-starts
        if return_temporaries:
            return log_marginal_likelihood, (pi, W_sr, L, b, a)
        else:
            return log_marginal_likelihood

                obj_func, initial_theta, method="L-BFGS-B", jac=True, bounds=bounds
            )
        else:
            raise ValueError("Unknown optimizer %s." % self.optimizer)

class GaussianProcessClassifier(ClassifierMixin, BaseEstimator):
    """Gaussian process classification (GPC) based on Laplace approximation.

    function. For multi-class classification, several binary one-versus rest
    classifiers are fitted. Note that this class thus does not implement
    a true multi-class Laplace approximation.

        the kernel's hyperparameters are optimized during fitting. Also kernel
        cannot be a `CompoundKernel`.

    optimizer : 'fmin_l_bfgs_b', callable or None, default='fmin_l_bfgs_b'

                ....

    multi_class : {'one_vs_rest', 'one_vs_one'}, default='one_vs_rest'
        Specifies how multi-class classification problems are handled.
        Supported are 'one_vs_rest' and 'one_vs_one'. In 'one_vs_rest',
        one binary Gaussian process classifier is fitted for each class, which
        is trained to separate this class from the rest. In 'one_vs_one', one
        binary Gaussian process classifier is fitted for each pair of classes,
        which is trained to separate these two classes. The predictions of
        these binary predictors are combined into multi-class predictions.
        Note that 'one_vs_one' does not support predicting probability
        estimates.

        The number of jobs to use for the computation: the specified
        multiclass problems are computed in parallel.

    base_estimator_ : ``Estimator`` instance
        The estimator instance that defines the likelihood function
        using the observed data.

        The kernel used for prediction. In case of binary classification,
        the structure of the kernel is the same as the one passed as parameter
        but with optimized hyperparameters. In case of multi-class
        classification, a CompoundKernel is returned which consists of the
        different kernels used in the one-versus-rest classifiers.

        The number of classes in the training data

    See Also
    --------
    GaussianProcessRegressor : Gaussian process regression (GPR).

    Examples
    --------
    >>> from sklearn.gaussian_process import GaussianProcessClassifier
    >>> from sklearn.gaussian_process.kernels import RBF
    >>> kernel = 1.0 * RBF(1.0)
    >>> gpc = GaussianProcessClassifier(kernel=kernel,
    >>> gpc.score(X, y)
    0.9866...
    >>> gpc.predict_proba(X[:2,:])
    array([[0.83548752, 0.03228706, 0.13222543],
           [0.79064206, 0.06525643, 0.14410151]])

    For a comparison of the GaussianProcessClassifier with other classifiers see:
    :ref:`sphx_glr_auto_examples_classification_plot_classification_probability.py`.
    """

        "kernel": [Kernel, None],
        "max_iter_predict": [Interval(Integral, 1, None, closed="left")],
        "multi_class": [StrOptions({"one_vs_rest", "one_vs_one"})],
    }

        self,
        *,
        multi_class="one_vs_rest",
    ):
        self.multi_class = multi_class

        Returns
        -------
        """
        if isinstance(self.kernel, CompoundKernel):
            raise ValueError("kernel cannot be a CompoundKernel")

            X, y = validate_data(
                self, X, y, multi_output=False, ensure_2d=True, dtype="numeric"
            )
        else:
                self, X, y, multi_output=False, ensure_2d=False, dtype=None
            )

        self.base_estimator_ = _BinaryGaussianProcessClassifierLaplace(
            kernel=self.kernel,
            optimizer=self.optimizer,
            n_restarts_optimizer=self.n_restarts_optimizer,
            max_iter_predict=self.max_iter_predict,
            warm_start=self.warm_start,
            copy_X_train=self.copy_X_train,
        )

        self.n_classes_ = self.classes_.size
        if self.n_classes_ == 1:
                "GaussianProcessClassifier requires 2 or more "
                "distinct classes; got %d class (only class %s "
                "is present)" % (self.n_classes_, self.classes_[0])
            )
        if self.n_classes_ > 2:
            if self.multi_class == "one_vs_rest":
                self.base_estimator_ = OneVsRestClassifier(
                    self.base_estimator_, n_jobs=self.n_jobs
                )
            elif self.multi_class == "one_vs_one":
                self.base_estimator_ = OneVsOneClassifier(
                )
            else:
                raise ValueError("Unknown multi-class mode %s" % self.multi_class)

        self.base_estimator_.fit(X, y)

            self.log_marginal_likelihood_value_ = np.mean(
                [
                    estimator.log_marginal_likelihood()
                    for estimator in self.base_estimator_.estimators_
                ]
            )
        else:
            self.log_marginal_likelihood_value_ = (
                self.base_estimator_.log_marginal_likelihood()
            )

        Returns
        -------
            Predicted target values for X, values are from ``classes_``.
        """

            X = validate_data(self, X, ensure_2d=True, dtype="numeric", reset=False)
        else:
            X = validate_data(self, X, ensure_2d=False, dtype=None, reset=False)

        return self.base_estimator_.predict(X)

        Returns
        -------
        """
        if self.n_classes_ > 2 and self.multi_class == "one_vs_one":
                "one_vs_one multi-class mode does not support "
                "predicting probability estimates. Use "
                "one_vs_rest mode instead."
            )

        else:

        return self.base_estimator_.predict_proba(X)

    @property
    def kernel_(self):
        """Return the kernel of the base estimator."""
            return self.base_estimator_.kernel_
        else:
            return CompoundKernel(
                [estimator.kernel_ for estimator in self.base_estimator_.estimators_]
            )

    ):

        In the case of multi-class classification, the mean log-marginal
        likelihood of the one-versus-rest classifiers are returned.

            evaluated. In the case of multi-class classification, theta may
            be the  hyperparameters of the compound kernel or of an individual
            kernel. In the latter case, all individual kernel get assigned the
            same theta values. If None, the precomputed log_marginal_likelihood

            additionally. Note that gradient computation is not supported
            for non-binary classification. If True, theta must not be None.

        Returns
        -------

        """

        theta = np.asarray(theta)
            return self.base_estimator_.log_marginal_likelihood(
                theta, eval_gradient, clone_kernel=clone_kernel
            )
        else:
                raise NotImplementedError(
                    "Gradient of log-marginal-likelihood not implemented for "
                    "multi-class GPC."
                )
            estimators = self.base_estimator_.estimators_
            n_dims = estimators[0].kernel_.n_dims
            if theta.shape[0] == n_dims:  # use same theta for all sub-kernels
                return np.mean(
                    [
                        estimator.log_marginal_likelihood(
                            theta, clone_kernel=clone_kernel
                        )
                        for i, estimator in enumerate(estimators)
                    ]
                )
            elif theta.shape[0] == n_dims * self.classes_.shape[0]:
                # theta for compound kernel
                    [
                            theta[n_dims * i : n_dims * (i + 1)],
                            clone_kernel=clone_kernel,
                        )
                    ]
                )
            else:
                    "Shape of theta must be either %d or %d. "
                    "Obtained theta with shape %d."
                    % (n_dims, n_dims * self.classes_.shape[0], theta.shape[0])
                )

        self,
        *,
    ):

        Returns
        -------
        """
            )
        else:

        # problem
            )
                )
            )

                    )
                else:

            optima = [
                )
            ]

            # theta
                    )
                    )

        else:
            )

        )

        Returns
        -------
        """

        Returns
        -------
        """

        # Line 7:
        )

    ):

        Returns
        -------

                optional
        """

        else:

        else:

            return Z

        s_2 = (
            -0.5

        """

        if (
        ):
        else:

            # Line 4
            # Line 5
            # Line 6
            # Line 7
            # Line 8

            lml = (
            )
                break

        else:

            )
        else:

        self,
        *,
    ):

        Returns
        -------
        """

            )
        else:
            )

        )

            )
                )
                )
            else:

                [
                ]
            )
        else:
            )

        Returns
        -------
        """

        else:

        Returns
        -------
        """
            )

        else:

        else:
            )

    ):

        Returns
        -------

        """

            )
        else:
                )
                    [
                        )
                    ]
                )
                    [
                        )
                    ]
                )
            else:
                )

def _check_length_scale(X, length_scale):
    length_scale = np.squeeze(length_scale).astype(float)
    if np.ndim(length_scale) > 1:
        raise ValueError("length_scale cannot be of dimension greater than 1")
    if np.ndim(length_scale) == 1 and X.shape[1] != length_scale.shape[0]:
            "Anisotropic kernel must have the same number of "
            "dimensions as data (%d!=%d)" % (length_scale.shape[0], X.shape[1])
        )
    return length_scale

class Hyperparameter(
    namedtuple(
        "Hyperparameter", ("name", "value_type", "bounds", "n_elements", "fixed")
    )
):
    """A kernel hyperparameter's specification in form of a namedtuple.

    name : str
        The name of the hyperparameter. Note that a kernel using a
        hyperparameter with name "x" must have the attributes self.x and
        self.x_bounds

    value_type : str
        The type of the hyperparameter. Currently, only "numeric"
        hyperparameters are supported.

    bounds : pair of floats >= 0 or "fixed"
        The lower and upper bound on the parameter. If n_elements>1, a pair
        of 1d array with n_elements each may be given alternatively. If
        the string "fixed" is passed as bounds, the hyperparameter's value
        cannot be changed.

    n_elements : int, default=1
        The number of elements of the hyperparameter value. Defaults to 1,
        which corresponds to a scalar hyperparameter. n_elements > 1
        corresponds to a hyperparameter which is vector-valued,
        such as, e.g., anisotropic length-scales.

    fixed : bool, default=None
        Whether the value of this hyperparameter is fixed, i.e., cannot be
        changed during hyperparameter tuning. If None is passed, the "fixed" is
        derived based on the given bounds.

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import ConstantKernel
    >>> from sklearn.gaussian_process.kernels import Hyperparameter
    >>> X, y = make_friedman2(n_samples=50, noise=0, random_state=0)
    >>> kernel = ConstantKernel(constant_value=1.0,
    ...    constant_value_bounds=(0.0, 10.0))

    We can access each hyperparameter:

    >>> for hyperparameter in kernel.hyperparameters:
    ...    print(hyperparameter)
    Hyperparameter(name='constant_value', value_type='numeric',
    bounds=array([[ 0., 10.]]), n_elements=1, fixed=False)

    >>> params = kernel.get_params()
    >>> for key in sorted(params): print(f"{key} : {params[key]}")
    constant_value : 1.0
    constant_value_bounds : (0.0, 10.0)
    """

    # A raw namedtuple is very memory efficient as it packs the attributes
    # in a struct to get rid of the __dict__ of attributes in particular it
    # does not copy the string for the keys on each instance.
    # By deriving a namedtuple class just to introduce the __init__ method we
    # would also reintroduce the __dict__ on the instance. By telling the
    # Python interpreter that this subclass uses static __slots__ instead of
    # dynamic attributes. Furthermore we don't need any additional slot in the
    # subclass so we set __slots__ to the empty tuple.
    __slots__ = ()

    def __new__(cls, name, value_type, bounds, n_elements=1, fixed=None):
        if not isinstance(bounds, str) or bounds != "fixed":
            bounds = np.atleast_2d(bounds)
            if n_elements > 1:  # vector-valued parameter
                if bounds.shape[0] == 1:
                    bounds = np.repeat(bounds, n_elements, 0)
                elif bounds.shape[0] != n_elements:
                        "Bounds on %s should have either 1 or "
                        "%d dimensions. Given are %d"
                        % (name, n_elements, bounds.shape[0])
                    )

        if fixed is None:
            fixed = isinstance(bounds, str) and bounds == "fixed"
        return super().__new__(cls, name, value_type, bounds, n_elements, fixed)

    # This is mainly a testing utility to check that two hyperparameters
    # are equal.
    def __eq__(self, other):
        return (
            self.name == other.name
            and self.value_type == other.value_type
            and np.all(self.bounds == other.bounds)
            and self.n_elements == other.n_elements
            and self.fixed == other.fixed
        )

class Kernel(metaclass=ABCMeta):
    """Base class for all kernels.

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import Kernel, RBF
    >>> class CustomKernel(Kernel):
    ...     def __init__(self, length_scale=1.0):
    ...         self.length_scale = length_scale
    ...     def __call__(self, X, Y=None):
    ...         if Y is None:
    ...             Y = X
    ...         return np.inner(X, X if Y is None else Y) ** 2
    ...     def diag(self, X):
    ...         return np.ones(X.shape[0])
    ...     def is_stationary(self):
    ...         return True
    >>> kernel = CustomKernel(length_scale=2.0)
    >>> print(kernel(X))
    [[ 25 121]
     [121 625]]
    """

        """Get parameters of this kernel.

        Returns
        -------
        """
        params = dict()

        cls = self.__class__
        init_sign = signature(init)
        args, varargs = [], []
        for parameter in init_sign.parameters.values():
            if parameter.kind != parameter.VAR_KEYWORD and parameter.name != "self":
                args.append(parameter.name)
            if parameter.kind == parameter.VAR_POSITIONAL:
                varargs.append(parameter.name)

        if len(varargs) != 0:
                "scikit-learn kernels should always "
                "specify their parameters in the signature"
                " of their __init__ (no varargs)."
                " %s doesn't follow this convention." % (cls,)
            )
        for arg in args:
            params[arg] = getattr(self, arg)

        """Set the parameters of this kernel.

        The method works on simple kernels as well as on nested kernels.
        The latter have parameters of the form ``<component>__<parameter>``
        so that it's possible to update each component of a nested object.

        Returns
        -------
        self
        """
            # Simple optimisation to gain speed (inspect is slow)
            split = key.split("__", 1)
            if len(split) > 1:
                # nested objects case
                name, sub_name = split
                if name not in valid_params:
                        "Invalid parameter %s for kernel %s. "
                        "Check the list of available parameters "
                        "with `kernel.get_params().keys()`." % (name, self)
                    )
                sub_object = valid_params[name]
                sub_object.set_params(**{sub_name: value})
            else:
                # simple objects case
                if key not in valid_params:
                        "with `kernel.get_params().keys()`."
                        % (key, self.__class__.__name__)
                    )

    def clone_with_theta(self, theta):
        """Returns a clone of self with given hyperparameters theta.

        theta : ndarray of shape (n_dims,)
            The hyperparameters
        """
        cloned = clone(self)
        cloned.theta = theta
        return cloned

    @property
    def n_dims(self):
        """Returns the number of non-fixed hyperparameters of the kernel."""
        return self.theta.shape[0]

    @property
    def hyperparameters(self):
        """Returns a list of all hyperparameter specifications."""
        r = [
            getattr(self, attr)
            for attr in dir(self)
            if attr.startswith("hyperparameter_")
        ]
        return r

    @property
    def theta(self):
        """Returns the (flattened, log-transformed) non-fixed hyperparameters.

        Note that theta are typically the log-transformed values of the
        kernel's hyperparameters as this representation of the search space
        is more amenable for hyperparameter search, as hyperparameters like
        length-scales naturally live on a log-scale.

        Returns
        -------
            The non-fixed, log-transformed hyperparameters of the kernel
        """
        theta = []
        params = self.get_params()
        for hyperparameter in self.hyperparameters:
            if not hyperparameter.fixed:
                theta.append(params[hyperparameter.name])
        if len(theta) > 0:
            return np.log(np.hstack(theta))
        else:
            return np.array([])

    @theta.setter
    def theta(self, theta):
        """Sets the (flattened, log-transformed) non-fixed hyperparameters.

        """
        i = 0
            if hyperparameter.fixed:
                continue
            if hyperparameter.n_elements > 1:
                # vector-valued parameter
                params[hyperparameter.name] = np.exp(
                    theta[i : i + hyperparameter.n_elements]
                )
                i += hyperparameter.n_elements
            else:
                params[hyperparameter.name] = np.exp(theta[i])
                i += 1

        if i != len(theta):
                "theta has not the correct number of entries."
                " Should be %d; given are %d" % (i, len(theta))
            )
        self.set_params(**params)

    @property
    def bounds(self):
        """Returns the log-transformed bounds on the theta.

        Returns
        -------
        bounds : ndarray of shape (n_dims, 2)
            The log-transformed bounds on the kernel's hyperparameters theta
        """
        bounds = [
            hyperparameter.bounds
            for hyperparameter in self.hyperparameters
            if not hyperparameter.fixed
        ]
        if len(bounds) > 0:
            return np.log(np.vstack(bounds))
        else:

    def __add__(self, b):
        if not isinstance(b, Kernel):
            return Sum(self, ConstantKernel(b))
        return Sum(self, b)

    def __radd__(self, b):
            return Sum(ConstantKernel(b), self)
        return Sum(b, self)

    def __mul__(self, b):
            return Product(self, ConstantKernel(b))
        return Product(self, b)

    def __rmul__(self, b):
            return Product(ConstantKernel(b), self)
        return Product(b, self)

    def __pow__(self, b):
        return Exponentiation(self, b)

    def __eq__(self, b):
        if type(self) != type(b):
        params_a = self.get_params()
        params_b = b.get_params()
        for key in set(list(params_a.keys()) + list(params_b.keys())):
            if np.any(params_a.get(key, None) != params_b.get(key, None)):
                return False

        return "{0}({1})".format(
            self.__class__.__name__, ", ".join(map("{0:.3g}".format, self.theta))
        )

    def __call__(self, X, Y=None, eval_gradient=False):
        """Evaluate the kernel."""

    def diag(self, X):
        """Returns the diagonal of the kernel k(X, X).

        The result of this method is identical to np.diag(self(X)); however,
        it can be evaluated more efficiently since only the diagonal is
        evaluated.

            Left argument of the returned kernel k(X, Y)

        Returns
        -------
        K_diag : ndarray of shape (n_samples_X,)
            Diagonal of kernel k(X, X)
        """

    def is_stationary(self):
        """Returns whether the kernel is stationary."""

    @property
    def requires_vector_input(self):
        """Returns whether the kernel is defined on fixed-length feature
        vectors or generic objects. Defaults to True for backward
        compatibility."""

    def _check_bounds_params(self):
        """Called after fitting to warn if bounds may have been too tight."""
        list_close = np.isclose(self.bounds, np.atleast_2d(self.theta).T)
        idx = 0
        for hyp in self.hyperparameters:
            if hyp.fixed:
                continue
            for dim in range(hyp.n_elements):
                if list_close[idx, 0]:
                        "The optimal value found for "
                        "dimension %s of parameter %s is "
                        "close to the specified lower "
                        "bound %s. Decreasing the bound and"
                        " calling fit again may find a "
                        "better value." % (dim, hyp.name, hyp.bounds[dim][0]),
                        ConvergenceWarning,
                    )
                elif list_close[idx, 1]:
                        "close to the specified upper "
                        "bound %s. Increasing the bound and"
                        "better value." % (dim, hyp.name, hyp.bounds[dim][1]),
                    )
                idx += 1

class NormalizedKernelMixin:
    """Mixin for kernels which are normalized: k(X, X)=1.

    """

        X : ndarray of shape (n_samples_X, n_features)

        Returns
        -------
        """

class StationaryKernelMixin:
    """Mixin for kernels which are stationary: k(X, Y)= f(X-Y).

    """

class GenericKernelMixin:
    """Mixin for kernels which operate on generic objects such as variable-
    length sequences, trees, and graphs.

    """

    @property
        """Whether the kernel works only on fixed-length feature vectors."""

class CompoundKernel(Kernel):
    """Kernel which is composed of a set of other kernels.

    kernels : list of Kernels
        The other kernels

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import WhiteKernel
    >>> from sklearn.gaussian_process.kernels import CompoundKernel
    >>> kernel = CompoundKernel(
    ...     [WhiteKernel(noise_level=3.0), RBF(length_scale=2.0)])
    >>> print(kernel.bounds)
    [[-11.51292546  11.51292546]
     [-11.51292546  11.51292546]]
    >>> print(kernel.n_dims)
    2
    >>> print(kernel.theta)
    [1.09861229 0.69314718]
    """

    def __init__(self, kernels):
        self.kernels = kernels

        Returns
        -------
        """
        return dict(kernels=self.kernels)

    @property

        Returns
        -------
        """
        return np.hstack([kernel.theta for kernel in self.kernels])

        theta : array of shape (n_dims,)
        """
        k_dims = self.k1.n_dims
        for i, kernel in enumerate(self.kernels):
            kernel.theta = theta[i * k_dims : (i + 1) * k_dims]

    @property

        Returns
        -------
        bounds : array of shape (n_dims, 2)
        """
        return np.vstack([kernel.bounds for kernel in self.kernels])

        """Return the kernel k(X, Y) and optionally its gradient.

        Note that this compound kernel returns the results of all simple kernel
        stacked along an additional axis.

        X : array-like of shape (n_samples_X, n_features) or list of object, \

        Y : array-like of shape (n_samples_X, n_features) or list of object, \
            Right argument of the returned kernel k(X, Y). If None, k(X, X)
            is evaluated instead.

            Determines whether the gradient with respect to the log of the
            kernel hyperparameter is computed.

        Returns
        -------
        K : ndarray of shape (n_samples_X, n_samples_Y, n_kernels)
            Kernel k(X, Y)

        K_gradient : ndarray of shape \
                (n_samples_X, n_samples_X, n_dims, n_kernels), optional
            The gradient of the kernel k(X, X) with respect to the log of the
            hyperparameter of the kernel. Only returned when `eval_gradient`
            is True.
        """
            K = []
            K_grad = []
            for kernel in self.kernels:
                K_single, K_grad_single = kernel(X, Y, eval_gradient)
                K.append(K_single)
                K_grad.append(K_grad_single[..., np.newaxis])
            return np.dstack(K), np.concatenate(K_grad, 3)
        else:
            return np.dstack([kernel(X, Y, eval_gradient) for kernel in self.kernels])

        if type(self) != type(b) or len(self.kernels) != len(b.kernels):
        return np.all(
            [self.kernels[i] == b.kernels[i] for i in range(len(self.kernels))]
        )

        return np.all([kernel.is_stationary() for kernel in self.kernels])

    @property
        """Returns whether the kernel is defined on discrete structures."""
        return np.any([kernel.requires_vector_input for kernel in self.kernels])

        The result of this method is identical to `np.diag(self(X))`; however,

            Argument to the kernel.

        Returns
        -------
        K_diag : ndarray of shape (n_samples_X, n_kernels)
        """
        return np.vstack([kernel.diag(X) for kernel in self.kernels]).T

class KernelOperator(Kernel):
    """Base class for all kernel operators.

    """

    def __init__(self, k1, k2):
        self.k1 = k1
        self.k2 = k2

        Returns
        -------
        """
        params = dict(k1=self.k1, k2=self.k2)
        if deep:
            deep_items = self.k1.get_params().items()
            params.update(("k1__" + k, val) for k, val in deep_items)
            deep_items = self.k2.get_params().items()
            params.update(("k2__" + k, val) for k, val in deep_items)

    @property
        """Returns a list of all hyperparameter."""
        r = [
            Hyperparameter(
                "k1__" + hyperparameter.name,
                hyperparameter.value_type,
                hyperparameter.bounds,
                hyperparameter.n_elements,
            )
            for hyperparameter in self.k1.hyperparameters
        ]

        for hyperparameter in self.k2.hyperparameters:
            r.append(
                Hyperparameter(
                    "k2__" + hyperparameter.name,
                    hyperparameter.value_type,
                    hyperparameter.bounds,
                    hyperparameter.n_elements,
                )
            )
        return r

    @property

        Returns
        -------
        """
        return np.append(self.k1.theta, self.k2.theta)

        """
        k1_dims = self.k1.n_dims
        self.k1.theta = theta[:k1_dims]
        self.k2.theta = theta[k1_dims:]

    @property

        Returns
        -------
        """
        if self.k1.bounds.size == 0:
            return self.k2.bounds
        if self.k2.bounds.size == 0:
            return self.k1.bounds
        return np.vstack((self.k1.bounds, self.k2.bounds))

        return (self.k1 == b.k1 and self.k2 == b.k2) or (
            self.k1 == b.k2 and self.k2 == b.k1
        )

        return self.k1.is_stationary() and self.k2.is_stationary()

    @property
        return self.k1.requires_vector_input or self.k2.requires_vector_input

class Sum(KernelOperator):
    """The `Sum` kernel takes two kernels :math:`k_1` and :math:`k_2`
    and combines them via

    .. math::
        k_{sum}(X, Y) = k_1(X, Y) + k_2(X, Y)

    Note that the `__add__` magic method is overridden, so
    `Sum(RBF(), RBF())` is equivalent to using the + operator
    with `RBF() + RBF()`.

    Read more in the :ref:`User Guide <gp_kernels>`.

    k1 : Kernel
        The first base-kernel of the sum-kernel

    k2 : Kernel
        The second base-kernel of the sum-kernel

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import RBF, Sum, ConstantKernel
    >>> kernel = Sum(ConstantKernel(2), RBF())
    1.0
    >>> kernel
    1.41**2 + RBF(length_scale=1)
    """

        Y : array-like of shape (n_samples_X, n_features) or list of object,\

            Determines whether the gradient with respect to the log of
            the kernel hyperparameter is computed.

        Returns
        -------
        K : ndarray of shape (n_samples_X, n_samples_Y)

        K_gradient : ndarray of shape (n_samples_X, n_samples_X, n_dims),\
                optional
            is True.
        """
            K1, K1_gradient = self.k1(X, Y, eval_gradient=True)
            K2, K2_gradient = self.k2(X, Y, eval_gradient=True)
            return K1 + K2, np.dstack((K1_gradient, K2_gradient))
        else:
            return self.k1(X, Y) + self.k2(X, Y)

        Returns
        -------
        """
        return self.k1.diag(X) + self.k2.diag(X)

        return "{0} + {1}".format(self.k1, self.k2)

class Product(KernelOperator):
    """The `Product` kernel takes two kernels :math:`k_1` and :math:`k_2`

    .. math::
        k_{prod}(X, Y) = k_1(X, Y) * k_2(X, Y)

    Note that the `__mul__` magic method is overridden, so
    `Product(RBF(), RBF())` is equivalent to using the * operator
    with `RBF() * RBF()`.

    k1 : Kernel
        The first base-kernel of the product-kernel

    k2 : Kernel
        The second base-kernel of the product-kernel

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import (RBF, Product,
    ...            ConstantKernel)
    >>> kernel = Product(ConstantKernel(2), RBF())
    1.0
    >>> kernel
    1.41**2 * RBF(length_scale=1)
    """

        Y : array-like of shape (n_samples_Y, n_features) or list of object,\

        Returns
        -------

        K_gradient : ndarray of shape (n_samples_X, n_samples_X, n_dims), \
                optional
            is True.
        """
            return K1 * K2, np.dstack(
                (K1_gradient * K2[:, :, np.newaxis], K2_gradient * K1[:, :, np.newaxis])
            )
        else:
            return self.k1(X, Y) * self.k2(X, Y)

        Returns
        -------
        """
        return self.k1.diag(X) * self.k2.diag(X)

        return "{0} * {1}".format(self.k1, self.k2)

class Exponentiation(Kernel):
    """The Exponentiation kernel takes one base kernel and a scalar parameter
    :math:`p` and combines them via

    .. math::
        k_{exp}(X, Y) = k(X, Y) ^p

    Note that the `__pow__` magic method is overridden, so
    `Exponentiation(RBF(), 2)` is equivalent to using the ** operator
    with `RBF() ** 2`.

    kernel : Kernel
        The base kernel

    exponent : float
        The exponent for the base kernel

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import (RationalQuadratic,
    ...            Exponentiation)
    >>> kernel = Exponentiation(RationalQuadratic(), exponent=2)
    >>> gpr = GaussianProcessRegressor(kernel=kernel, alpha=5,
    0.419...
    >>> gpr.predict(X[:1,:], return_std=True)
    (array([635.5...]), array([0.559...]))
    """

    def __init__(self, kernel, exponent):
        self.exponent = exponent

        Returns
        -------
        """
        params = dict(kernel=self.kernel, exponent=self.exponent)
        if deep:
            deep_items = self.kernel.get_params().items()
            params.update(("kernel__" + k, val) for k, val in deep_items)

    @property
        r = []
        for hyperparameter in self.kernel.hyperparameters:
            r.append(
                    "kernel__" + hyperparameter.name,
                )
            )
        return r

    @property

        Returns
        -------
        """
        return self.kernel.theta

        """
        self.kernel.theta = theta

    @property

        Returns
        -------
        """
        return self.kernel.bounds

        return self.kernel == b.kernel and self.exponent == b.exponent

        Returns
        -------

                optional
            is True.
        """
            K, K_gradient = self.kernel(X, Y, eval_gradient=True)
            K_gradient *= self.exponent * K[:, :, np.newaxis] ** (self.exponent - 1)
            return K**self.exponent, K_gradient
        else:
            K = self.kernel(X, Y, eval_gradient=False)
            return K**self.exponent

        Returns
        -------
        """
        return self.kernel.diag(X) ** self.exponent

        return "{0} ** {1}".format(self.kernel, self.exponent)

        return self.kernel.is_stationary()

    @property
        return self.kernel.requires_vector_input

class ConstantKernel(StationaryKernelMixin, GenericKernelMixin, Kernel):
    """Constant kernel.

    Can be used as part of a product-kernel where it scales the magnitude of
    the other factor (kernel) or as part of a sum-kernel, where it modifies
    the mean of the Gaussian process.

    .. math::
        k(x_1, x_2) = constant\\_value \\;\\forall\\; x_1, x_2

    Adding a constant kernel is equivalent to adding a constant::

            kernel = RBF() + ConstantKernel(constant_value=2)

    is the same as::

            kernel = RBF() + 2

    constant_value : float, default=1.0
        The constant value which defines the covariance:
        k(x_1, x_2) = constant_value

    constant_value_bounds : pair of floats >= 0 or "fixed", default=(1e-5, 1e5)
        The lower and upper bound on `constant_value`.
        If set to "fixed", `constant_value` cannot be changed during
        hyperparameter tuning.

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import RBF, ConstantKernel
    >>> kernel = RBF() + ConstantKernel(constant_value=2)
    0.3696...
    (array([606.1...]), array([0.24...]))
    """

    def __init__(self, constant_value=1.0, constant_value_bounds=(1e-5, 1e5)):
        self.constant_value = constant_value
        self.constant_value_bounds = constant_value_bounds

    @property
    def hyperparameter_constant_value(self):
        return Hyperparameter("constant_value", "numeric", self.constant_value_bounds)

            Only supported when Y is None.

        Returns
        -------

            optional
            hyperparameter of the kernel. Only returned when eval_gradient
            is True.
        """
        if Y is None:
            Y = X
        elif eval_gradient:
            raise ValueError("Gradient can only be evaluated when Y is None.")

        K = np.full(
            (_num_samples(X), _num_samples(Y)),
            self.constant_value,
            dtype=np.array(self.constant_value).dtype,
        )
            if not self.hyperparameter_constant_value.fixed:
                return (
                    K,
                    np.full(
                        (_num_samples(X), _num_samples(X), 1),
                        self.constant_value,
                        dtype=np.array(self.constant_value).dtype,
                    ),
                )
            else:
                return K, np.empty((_num_samples(X), _num_samples(X), 0))
        else:
            return K

        Returns
        -------
        """
        return np.full(
            _num_samples(X),
        )

        return "{0:.3g}**2".format(np.sqrt(self.constant_value))

class WhiteKernel(StationaryKernelMixin, GenericKernelMixin, Kernel):
    """White kernel.

    The main use-case of this kernel is as part of a sum-kernel where it
    explains the noise of the signal as independently and identically
    normally-distributed. The parameter noise_level equals the variance of this
    noise.

    .. math::
        k(x_1, x_2) = noise\\_level \\text{ if } x_i == x_j \\text{ else } 0

    noise_level : float, default=1.0
        Parameter controlling the noise level (variance)

    noise_level_bounds : pair of floats >= 0 or "fixed", default=(1e-5, 1e5)
        The lower and upper bound on 'noise_level'.
        If set to "fixed", 'noise_level' cannot be changed during

    Examples
    --------
    >>> kernel = DotProduct() + WhiteKernel(noise_level=0.5)
    0.3680...
    (array([653.0..., 592.1... ]), array([316.6..., 316.6...]))
    """

    def __init__(self, noise_level=1.0, noise_level_bounds=(1e-5, 1e5)):
        self.noise_level = noise_level
        self.noise_level_bounds = noise_level_bounds

    @property
    def hyperparameter_noise_level(self):
        return Hyperparameter("noise_level", "numeric", self.noise_level_bounds)

        Returns
        -------

            optional
            is True.
        """
        if Y is not None and eval_gradient:

            K = self.noise_level * np.eye(_num_samples(X))
                if not self.hyperparameter_noise_level.fixed:
                    return (
                        K,
                        self.noise_level * np.eye(_num_samples(X))[:, :, np.newaxis],
                    )
                else:
                    return K, np.empty((_num_samples(X), _num_samples(X), 0))
            else:
                return K
        else:
            return np.zeros((_num_samples(X), _num_samples(Y)))

        Returns
        -------
        """
            _num_samples(X), self.noise_level, dtype=np.array(self.noise_level).dtype
        )

        return "{0}(noise_level={1:.3g})".format(
            self.__class__.__name__, self.noise_level
        )

class RBF(StationaryKernelMixin, NormalizedKernelMixin, Kernel):
    """Radial basis function kernel (aka squared-exponential kernel).

    The RBF kernel is a stationary kernel. It is also known as the
    "squared exponential" kernel. It is parameterized by a length scale
    parameter :math:`l>0`, which can either be a scalar (isotropic variant
    of the kernel) or a vector with the same number of dimensions as the inputs
    X (anisotropic variant of the kernel). The kernel is given by:

    .. math::
        k(x_i, x_j) = \\exp\\left(- \\frac{d(x_i, x_j)^2}{2l^2} \\right)

    where :math:`l` is the length scale of the kernel and
    :math:`d(\\cdot,\\cdot)` is the Euclidean distance.
    For advice on how to set the length scale parameter, see e.g. [1]_.

    This kernel is infinitely differentiable, which implies that GPs with this
    kernel as covariance function have mean square derivatives of all orders,
    and are thus very smooth.
    See [2]_, Chapter 4, Section 4.2, for further details of the RBF kernel.

    length_scale : float or ndarray of shape (n_features,), default=1.0
        The length scale of the kernel. If a float, an isotropic kernel is
        used. If an array, an anisotropic kernel is used where each dimension
        of l defines the length-scale of the respective feature dimension.

    length_scale_bounds : pair of floats >= 0 or "fixed", default=(1e-5, 1e5)
        The lower and upper bound on 'length_scale'.
        If set to "fixed", 'length_scale' cannot be changed during

    .. [1] `David Duvenaud (2014). "The Kernel Cookbook:
        Advice on Covariance functions".
        <https://www.cs.toronto.edu/~duvenaud/cookbook/>`_

    .. [2] `Carl Edward Rasmussen, Christopher K. I. Williams (2006).
        "Gaussian Processes for Machine Learning". The MIT Press.
        <http://www.gaussianprocess.org/gpml/>`_

    Examples
    --------
    0.9866...
    array([[0.8354..., 0.03228..., 0.1322...],
           [0.7906..., 0.0652..., 0.1441...]])
    """

    def __init__(self, length_scale=1.0, length_scale_bounds=(1e-5, 1e5)):
        self.length_scale = length_scale
        self.length_scale_bounds = length_scale_bounds

    @property
    def anisotropic(self):
        return np.iterable(self.length_scale) and len(self.length_scale) > 1

    @property
    def hyperparameter_length_scale(self):
        if self.anisotropic:
            return Hyperparameter(
                "length_scale",
                "numeric",
                self.length_scale_bounds,
                len(self.length_scale),
            )
        return Hyperparameter("length_scale", "numeric", self.length_scale_bounds)

        Y : ndarray of shape (n_samples_Y, n_features), default=None
            if evaluated instead.

        Returns
        -------

                optional
            is True.
        """
        X = np.atleast_2d(X)
        length_scale = _check_length_scale(X, self.length_scale)
            dists = pdist(X / length_scale, metric="sqeuclidean")
            K = np.exp(-0.5 * dists)
            # convert from upper-triangular matrix to square matrix
            K = squareform(K)
            np.fill_diagonal(K, 1)
        else:
                raise ValueError("Gradient can only be evaluated when Y is None.")
            dists = cdist(X / length_scale, Y / length_scale, metric="sqeuclidean")

            if self.hyperparameter_length_scale.fixed:
                # Hyperparameter l kept fixed
                return K, np.empty((X.shape[0], X.shape[0], 0))
            elif not self.anisotropic or length_scale.shape[0] == 1:
                K_gradient = (K * squareform(dists))[:, :, np.newaxis]
                return K, K_gradient
            elif self.anisotropic:
                # We need to recompute the pairwise dimension-wise distances
                K_gradient = (X[:, np.newaxis, :] - X[np.newaxis, :, :]) ** 2 / (
                    length_scale**2
                )
                K_gradient *= K[..., np.newaxis]
        else:
            return K

            return "{0}(length_scale=[{1}])".format(
                self.__class__.__name__,
                ", ".join(map("{0:.3g}".format, self.length_scale)),
            )
        else:  # isotropic
            return "{0}(length_scale={1:.3g})".format(
                self.__class__.__name__, np.ravel(self.length_scale)[0]
            )

class Matern(RBF):
    """Matern kernel.

    The class of Matern kernels is a generalization of the :class:`RBF`.
    It has an additional parameter :math:`\\nu` which controls the
    smoothness of the resulting function. The smaller :math:`\\nu`,
    the less smooth the approximated function is.
    As :math:`\\nu\\rightarrow\\infty`, the kernel becomes equivalent to
    the :class:`RBF` kernel. When :math:`\\nu = 1/2`, the Matérn kernel
    becomes identical to the absolute exponential kernel.
    Important intermediate values are
    :math:`\\nu=1.5` (once differentiable functions)
    and :math:`\\nu=2.5` (twice differentiable functions).

    The kernel is given by:

    .. math::
         k(x_i, x_j) =  \\frac{1}{\\Gamma(\\nu)2^{\\nu-1}}\\Bigg(
         \\frac{\\sqrt{2\\nu}}{l} d(x_i , x_j )
         \\Bigg)^\\nu K_\\nu\\Bigg(
         \\frac{\\sqrt{2\\nu}}{l} d(x_i , x_j )\\Bigg)

    where :math:`d(\\cdot,\\cdot)` is the Euclidean distance,
    :math:`K_{\\nu}(\\cdot)` is a modified Bessel function and
    :math:`\\Gamma(\\cdot)` is the gamma function.
    See [1]_, Chapter 4, Section 4.2, for details regarding the different
    variants of the Matern kernel.

    nu : float, default=1.5
        The parameter nu controlling the smoothness of the learned function.
        The smaller nu, the less smooth the approximated function is.
        For nu=inf, the kernel becomes equivalent to the RBF kernel and for
        nu=0.5 to the absolute exponential kernel. Important intermediate
        values are nu=1.5 (once differentiable functions) and nu=2.5
        (twice differentiable functions). Note that values of nu not in
        [0.5, 1.5, 2.5, inf] incur a considerably higher computational cost
        (appr. 10 times higher) since they require to evaluate the modified
        Bessel function. Furthermore, in contrast to l, nu is kept fixed to
        its initial value and not optimized.

    .. [1] `Carl Edward Rasmussen, Christopher K. I. Williams (2006).

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import Matern
    >>> kernel = 1.0 * Matern(length_scale=1.0, nu=1.5)
    0.9866...
    array([[0.8513..., 0.0368..., 0.1117...],
            [0.8086..., 0.0693..., 0.1220...]])
    """

    def __init__(self, length_scale=1.0, length_scale_bounds=(1e-5, 1e5), nu=1.5):
        super().__init__(length_scale, length_scale_bounds)
        self.nu = nu

        Returns
        -------

                optional
            is True.
        """
            dists = pdist(X / length_scale, metric="euclidean")
        else:
            dists = cdist(X / length_scale, Y / length_scale, metric="euclidean")

        if self.nu == 0.5:
            K = np.exp(-dists)
        elif self.nu == 1.5:
            K = dists * math.sqrt(3)
            K = (1.0 + K) * np.exp(-K)
        elif self.nu == 2.5:
            K = dists * math.sqrt(5)
            K = (1.0 + K + K**2 / 3.0) * np.exp(-K)
        elif self.nu == np.inf:
            K = np.exp(-(dists**2) / 2.0)
        else:  # general case; expensive to evaluate
            K = dists
            K[K == 0.0] += np.finfo(float).eps  # strict zeros result in nan
            tmp = math.sqrt(2 * self.nu) * K
            K.fill((2 ** (1.0 - self.nu)) / gamma(self.nu))
            K *= tmp**self.nu
            K *= kv(self.nu, tmp)

                K_gradient = np.empty((X.shape[0], X.shape[0], 0))

            # We need to recompute the pairwise dimension-wise distances
            if self.anisotropic:
                D = (X[:, np.newaxis, :] - X[np.newaxis, :, :]) ** 2 / (length_scale**2)
            else:
                D = squareform(dists**2)[:, :, np.newaxis]

            if self.nu == 0.5:
                denominator = np.sqrt(D.sum(axis=2))[:, :, np.newaxis]
                divide_result = np.zeros_like(D)
                np.divide(
                    D,
                    denominator,
                    out=divide_result,
                    where=denominator != 0,
                )
                K_gradient = K[..., np.newaxis] * divide_result
            elif self.nu == 1.5:
                K_gradient = 3 * D * np.exp(-np.sqrt(3 * D.sum(-1)))[..., np.newaxis]
            elif self.nu == 2.5:
                tmp = np.sqrt(5 * D.sum(-1))[..., np.newaxis]
                K_gradient = 5.0 / 3.0 * D * (tmp + 1) * np.exp(-tmp)
            elif self.nu == np.inf:
                K_gradient = D * K[..., np.newaxis]
            else:
                # approximate gradient numerically
                def f(theta):  # helper function
                    return self.clone_with_theta(theta)(X, Y)

                return K, _approx_fprime(self.theta, f, 1e-10)

            if not self.anisotropic:
                return K, K_gradient[:, :].sum(-1)[:, :, np.newaxis]
            else:
        else:
            return K

            return "{0}(length_scale=[{1}], nu={2:.3g})".format(
                self.nu,
            )
        else:
            return "{0}(length_scale={1:.3g}, nu={2:.3g})".format(
                self.__class__.__name__, np.ravel(self.length_scale)[0], self.nu
            )

class RationalQuadratic(StationaryKernelMixin, NormalizedKernelMixin, Kernel):
    """Rational Quadratic kernel.

    The RationalQuadratic kernel can be seen as a scale mixture (an infinite
    sum) of RBF kernels with different characteristic length scales. It is
    parameterized by a length scale parameter :math:`l>0` and a scale
    mixture parameter :math:`\\alpha>0`. Only the isotropic variant
    where length_scale :math:`l` is a scalar is supported at the moment.

    .. math::
        k(x_i, x_j) = \\left(
        1 + \\frac{d(x_i, x_j)^2 }{ 2\\alpha  l^2}\\right)^{-\\alpha}

    where :math:`\\alpha` is the scale mixture parameter, :math:`l` is
    the length scale of the kernel and :math:`d(\\cdot,\\cdot)` is the
    Euclidean distance.
    For advice on how to set the parameters, see e.g. [1]_.

    length_scale : float > 0, default=1.0
        The length scale of the kernel.

    alpha : float > 0, default=1.0
        Scale mixture parameter

    alpha_bounds : pair of floats >= 0 or "fixed", default=(1e-5, 1e5)
        The lower and upper bound on 'alpha'.
        If set to "fixed", 'alpha' cannot be changed during

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import RationalQuadratic
    >>> kernel = RationalQuadratic(length_scale=1.0, alpha=1.5)
    0.9733...
    array([[0.8881..., 0.0566..., 0.05518...],
            [0.8678..., 0.0707... , 0.0614...]])
    """

        self,
        length_scale=1.0,
        length_scale_bounds=(1e-5, 1e5),
        alpha_bounds=(1e-5, 1e5),
    ):
        self.alpha_bounds = alpha_bounds

    @property

    @property
    def hyperparameter_alpha(self):
        return Hyperparameter("alpha", "numeric", self.alpha_bounds)

        Returns
        -------

        K_gradient : ndarray of shape (n_samples_X, n_samples_X, n_dims)
            is True.
        """
        if len(np.atleast_1d(self.length_scale)) > 1:
                "RationalQuadratic kernel only supports isotropic version, "
                "please use a single scalar for length_scale"
            )
            dists = squareform(pdist(X, metric="sqeuclidean"))
            tmp = dists / (2 * self.alpha * self.length_scale**2)
            base = 1 + tmp
            K = base**-self.alpha
        else:
            dists = cdist(X, Y, metric="sqeuclidean")
            K = (1 + dists / (2 * self.alpha * self.length_scale**2)) ** -self.alpha

            # gradient with respect to length_scale
            if not self.hyperparameter_length_scale.fixed:
                length_scale_gradient = dists * K / (self.length_scale**2 * base)
                length_scale_gradient = length_scale_gradient[:, :, np.newaxis]
            else:  # l is kept fixed
                length_scale_gradient = np.empty((K.shape[0], K.shape[1], 0))

            # gradient with respect to alpha
            if not self.hyperparameter_alpha.fixed:
                alpha_gradient = K * (
                    -self.alpha * np.log(base)
                    + dists / (2 * self.length_scale**2 * base)
                )
                alpha_gradient = alpha_gradient[:, :, np.newaxis]
            else:  # alpha is kept fixed
                alpha_gradient = np.empty((K.shape[0], K.shape[1], 0))

            return K, np.dstack((alpha_gradient, length_scale_gradient))
        else:
            return K

        return "{0}(alpha={1:.3g}, length_scale={2:.3g})".format(
            self.__class__.__name__, self.alpha, self.length_scale
        )

class ExpSineSquared(StationaryKernelMixin, NormalizedKernelMixin, Kernel):
    r"""Exp-Sine-Squared kernel (aka periodic kernel).

    The ExpSineSquared kernel allows one to model functions which repeat
    themselves exactly. It is parameterized by a length scale
    parameter :math:`l>0` and a periodicity parameter :math:`p>0`.
    Only the isotropic variant where :math:`l` is a scalar is
    supported at the moment. The kernel is given by:

    .. math::
        k(x_i, x_j) = \text{exp}\left(-
        \frac{ 2\sin^2(\pi d(x_i, x_j)/p) }{ l^ 2} \right)

    where :math:`l` is the length scale of the kernel, :math:`p` the
    periodicity of the kernel and :math:`d(\cdot,\cdot)` is the

    periodicity : float > 0, default=1.0
        The periodicity of the kernel.

    periodicity_bounds : pair of floats >= 0 or "fixed", default=(1e-5, 1e5)
        The lower and upper bound on 'periodicity'.
        If set to "fixed", 'periodicity' cannot be changed during

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import ExpSineSquared
    >>> kernel = ExpSineSquared(length_scale=1, periodicity=1)
    0.0144...
    (array([425.6..., 457.5...]), array([0.3894..., 0.3467...]))
    """

        self,
        periodicity=1.0,
        periodicity_bounds=(1e-5, 1e5),
    ):
        self.periodicity = periodicity
        self.periodicity_bounds = periodicity_bounds

    @property
        """Returns the length scale"""

    @property
    def hyperparameter_periodicity(self):
        return Hyperparameter("periodicity", "numeric", self.periodicity_bounds)

        Returns
        -------

                optional
            is True.
        """
            dists = squareform(pdist(X, metric="euclidean"))
            arg = np.pi * dists / self.periodicity
            sin_of_arg = np.sin(arg)
            K = np.exp(-2 * (sin_of_arg / self.length_scale) ** 2)
        else:
            dists = cdist(X, Y, metric="euclidean")
            K = np.exp(
                -2 * (np.sin(np.pi / self.periodicity * dists) / self.length_scale) ** 2
            )

            cos_of_arg = np.cos(arg)
                length_scale_gradient = 4 / self.length_scale**2 * sin_of_arg**2 * K
            else:  # length_scale is kept fixed
            # gradient with respect to p
            if not self.hyperparameter_periodicity.fixed:
                periodicity_gradient = (
                    4 * arg / self.length_scale**2 * cos_of_arg * sin_of_arg * K
                )
                periodicity_gradient = periodicity_gradient[:, :, np.newaxis]
            else:  # p is kept fixed
                periodicity_gradient = np.empty((K.shape[0], K.shape[1], 0))

            return K, np.dstack((length_scale_gradient, periodicity_gradient))
        else:
            return K

        return "{0}(length_scale={1:.3g}, periodicity={2:.3g})".format(
            self.__class__.__name__, self.length_scale, self.periodicity
        )

class DotProduct(Kernel):
    r"""Dot-Product kernel.

    The DotProduct kernel is non-stationary and can be obtained from linear
    regression by putting :math:`N(0, 1)` priors on the coefficients
    of :math:`x_d (d = 1, . . . , D)` and a prior of :math:`N(0, \sigma_0^2)`
    on the bias. The DotProduct kernel is invariant to a rotation of
    the coordinates about the origin, but not translations.
    It is parameterized by a parameter sigma_0 :math:`\sigma`
    which controls the inhomogenity of the kernel. For :math:`\sigma_0^2 =0`,
    the kernel is called the homogeneous linear kernel, otherwise
    it is inhomogeneous. The kernel is given by

    .. math::
        k(x_i, x_j) = \sigma_0 ^ 2 + x_i \cdot x_j

    The DotProduct kernel is commonly combined with exponentiation.

    See [1]_, Chapter 4, Section 4.2, for further details regarding the
    DotProduct kernel.

    sigma_0 : float >= 0, default=1.0
        Parameter controlling the inhomogenity of the kernel. If sigma_0=0,
        the kernel is homogeneous.

    sigma_0_bounds : pair of floats >= 0 or "fixed", default=(1e-5, 1e5)
        The lower and upper bound on 'sigma_0'.
        If set to "fixed", 'sigma_0' cannot be changed during

    Examples
    --------
    0.3680...
    """

    def __init__(self, sigma_0=1.0, sigma_0_bounds=(1e-5, 1e5)):
        self.sigma_0 = sigma_0
        self.sigma_0_bounds = sigma_0_bounds

    @property
    def hyperparameter_sigma_0(self):
        return Hyperparameter("sigma_0", "numeric", self.sigma_0_bounds)

        Returns
        -------

                optional
            is True.
        """
            K = np.inner(X, X) + self.sigma_0**2
        else:
            K = np.inner(X, Y) + self.sigma_0**2

            if not self.hyperparameter_sigma_0.fixed:
                K_gradient = np.empty((K.shape[0], K.shape[1], 1))
                K_gradient[..., 0] = 2 * self.sigma_0**2
            else:
        else:
            return K

            Left argument of the returned kernel k(X, Y).

        Returns
        -------
            Diagonal of kernel k(X, X).
        """
        return np.einsum("ij,ij->i", X, X) + self.sigma_0**2

        return "{0}(sigma_0={1:.3g})".format(self.__class__.__name__, self.sigma_0)

def _approx_fprime(xk, f, epsilon, args=()):
    f0 = f(*((xk,) + args))
    grad = np.zeros((f0.shape[0], f0.shape[1], len(xk)), float)
    ei = np.zeros((len(xk),), float)
    for k in range(len(xk)):
        ei[k] = 1.0
        d = epsilon * ei
        grad[:, :, k] = (f(*((xk + d,) + args)) - f0) / d[k]
        ei[k] = 0.0
    return grad

class PairwiseKernel(Kernel):
    """Wrapper for kernels in sklearn.metrics.pairwise.

    A thin wrapper around the functionality of the kernels in
    sklearn.metrics.pairwise.

    Note: Evaluation of eval_gradient is not analytic but numeric and all
          kernels support only isotropic distances. The parameter gamma is
          considered to be a hyperparameter and may be optimized. The other
          kernel parameters are set directly at initialization and are kept
          fixed.

        Parameter gamma of the pairwise kernel specified by metric. It should
        be positive.

    gamma_bounds : pair of floats >= 0 or "fixed", default=(1e-5, 1e5)
        The lower and upper bound on 'gamma'.
        If set to "fixed", 'gamma' cannot be changed during

    metric : {"linear", "additive_chi2", "chi2", "poly", "polynomial", \
              "rbf", "laplacian", "sigmoid", "cosine"} or callable, \
              default="linear"
        The metric to use when calculating kernel between instances in a
        feature array. If metric is a string, it must be one of the metrics
        in pairwise.PAIRWISE_KERNEL_FUNCTIONS.
        If metric is "precomputed", X is assumed to be a kernel matrix.
        Alternatively, if metric is a callable function, it is called on each
        pair of instances (rows) and the resulting value recorded. The callable
        should take two arrays from X as input and return a value indicating
        the distance between them.

    pairwise_kernels_kwargs : dict, default=None
        All entries of this dict (if any) are passed as keyword arguments to
        the pairwise kernel function.

    Examples
    --------
    >>> from sklearn.gaussian_process.kernels import PairwiseKernel
    >>> kernel = PairwiseKernel(metric='rbf')
    0.9733...
    array([[0.8880..., 0.05663..., 0.05532...],
           [0.8676..., 0.07073..., 0.06165...]])
    """

        self,
        gamma=1.0,
        gamma_bounds=(1e-5, 1e5),
        metric="linear",
        pairwise_kernels_kwargs=None,
    ):
        self.gamma_bounds = gamma_bounds
        self.metric = metric
        self.pairwise_kernels_kwargs = pairwise_kernels_kwargs

    @property
    def hyperparameter_gamma(self):
        return Hyperparameter("gamma", "numeric", self.gamma_bounds)

        Returns
        -------

                optional
            is True.
        """
        pairwise_kernels_kwargs = self.pairwise_kernels_kwargs
        if self.pairwise_kernels_kwargs is None:
            pairwise_kernels_kwargs = {}

        K = pairwise_kernels(
            X,
            Y,
            metric=self.metric,
            gamma=self.gamma,
            **pairwise_kernels_kwargs,
        )
            if self.hyperparameter_gamma.fixed:
            else:
                def f(gamma):  # helper function
                    return pairwise_kernels(
                        X,
                        Y,
                        metric=self.metric,
                        gamma=np.exp(gamma),
                        filter_params=True,
                        **pairwise_kernels_kwargs,
                    )

        else:
            return K

        Returns
        -------
        """
        # We have to fall back to slow way of computing diagonal
        return np.apply_along_axis(self, 1, X).ravel()

        return self.metric in ["rbf"]

        return "{0}(gamma={1}, metric={2})".format(
            self.__class__.__name__, self.gamma, self.metric
        )

                    )

        return (
        )

        Returns
        -------
        """

            )

        Returns
        -------
        self
        """
                    )
            else:
                    )

        """

        r = [
        ]
        return r

        Returns
        -------
        """
        theta = []
        else:

        """
        i = 0
                continue
                )
            else:
                i += 1

            )

        Returns
        -------
        """
        bounds = [
        ]
        else:

        )

        Returns
        -------
        """

        idx = 0
                continue
                    )
                    )
                idx += 1

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        """

        Returns
        -------
        """

        Returns
        -------

            is True.
        """
            K = []
            K_grad = []
        else:

        )

        Returns
        -------
        """

        Returns
        -------
        """
        if deep:

        r = [
            )
        ]

            r.append(
                )
            )
        return r

        Returns
        -------
        """

        """

        Returns
        -------
        """

        )

        Returns
        -------

                optional
            is True.
        """
        else:

        Returns
        -------
        """

        Returns
        -------

                optional
            is True.
        """
            )
        else:

        Returns
        -------
        """

        Returns
        -------
        """
        if deep:

        r = []
            r.append(
                )
            )
        return r

        Returns
        -------
        """

        """

        Returns
        -------
        """

        Returns
        -------

                optional
            is True.
        """
        else:

        Returns
        -------
        """

        Returns
        -------

            optional
            is True.
        """
            Y = X

        )
                return (
                    K,
                    np.full(
                    ),
                )
            else:
        else:
            return K

        Returns
        -------
        """
        )

        Returns
        -------

            optional
            is True.
        """

                    return (
                        K,
                    )
                else:
            else:
                return K
        else:

        Returns
        -------
        """
        )

        )

            )

        Returns
        -------

                optional
            is True.
        """
        else:

                )
        else:
            return K

            )
            )

        Returns
        -------

                optional
            is True.
        """
        else:

            K = dists

            else:

                    D,
                )
            else:

            else:
        else:
            return K

                self.nu,
            )
        else:
            )

        self,
    ):

        Returns
        -------

            is True.
        """
            )
        else:

                )

        else:
            return K

        )

        self,
    ):

        Returns
        -------

                optional
            is True.
        """
        else:
            K = np.exp(
            )

                )

        else:
            return K

        )

        Returns
        -------

                optional
            is True.
        """
        else:

            else:
        else:
            return K

        Returns
        -------
        """

        self,
    ):

        Returns
        -------

                optional
            is True.
        """

            X,
            Y,
        )
            else:
                        X,
                        Y,
                    )

        else:
            return K

        Returns
        -------
        """

        )

                        X,
                        Y,
                    )

def test_kernel_gradient(kernel):
    # Compare analytic and numeric gradient of kernels.
    kernel = clone(kernel)  # make tests independent of one-another
    K, K_gradient = kernel(X, eval_gradient=True)

    assert K_gradient.shape[0] == X.shape[0]
    assert K_gradient.shape[1] == X.shape[0]
    assert K_gradient.shape[2] == kernel.theta.shape[0]

    def eval_kernel_for_theta(theta):
        kernel_clone = kernel.clone_with_theta(theta)
        K = kernel_clone(X, eval_gradient=False)
        return K

    K_gradient_approx = _approx_fprime(kernel.theta, eval_kernel_for_theta, 1e-10)

    assert_almost_equal(K_gradient, K_gradient_approx, 4)

def test_kernel_theta(kernel):
    # Check that parameter vector theta of kernel is set correctly.
    theta = kernel.theta
    _, K_gradient = kernel(X, eval_gradient=True)

    # Determine kernel parameters that contribute to theta
    init_sign = signature(kernel.__class__.__init__).parameters.values()
    args = [p.name for p in init_sign if p.name != "self"]
    theta_vars = map(
        lambda s: s[0 : -len("_bounds")], filter(lambda s: s.endswith("_bounds"), args)
    )
    assert set(hyperparameter.name for hyperparameter in kernel.hyperparameters) == set(
        theta_vars
    )

    # Check that values returned in theta are consistent with
    # hyperparameter values (being their logarithms)
    for i, hyperparameter in enumerate(kernel.hyperparameters):
        assert theta[i] == np.log(getattr(kernel, hyperparameter.name))

    # Fixed kernel parameters must be excluded from theta and gradient.
        # create copy with certain hyperparameter fixed
        params = kernel.get_params()
        params[hyperparameter.name + "_bounds"] = "fixed"
        kernel_class = kernel.__class__
        new_kernel = kernel_class(**params)
        # Check that theta and K_gradient are identical with the fixed
        # dimension left out
        _, K_gradient_new = new_kernel(X, eval_gradient=True)
        assert theta.shape[0] == new_kernel.theta.shape[0] + 1
        assert K_gradient.shape[2] == K_gradient_new.shape[2] + 1
        if i > 0:
            assert theta[:i] == new_kernel.theta[:i]
            assert_array_equal(K_gradient[..., :i], K_gradient_new[..., :i])
        if i + 1 < len(kernel.hyperparameters):
            assert theta[i + 1 :] == new_kernel.theta[i:]
            assert_array_equal(K_gradient[..., i + 1 :], K_gradient_new[..., i:])

    # Check that values of theta are modified correctly
        theta[i] = np.log(42)
        kernel.theta = theta
        assert_almost_equal(getattr(kernel, hyperparameter.name), 42)

        setattr(kernel, hyperparameter.name, 43)
        assert_almost_equal(kernel.theta[i], np.log(43))

def test_auto_vs_cross(kernel):
    # Auto-correlation and cross-correlation should be consistent.
    K_auto = kernel(X)
    K_cross = kernel(X, X)
    assert_almost_equal(K_auto, K_cross, 5)

def test_kernel_diag(kernel):
    # Test that diag method of kernel returns consistent results.
    K_call_diag = np.diag(kernel(X))
    K_diag = kernel.diag(X)
    assert_almost_equal(K_call_diag, K_diag, 5)

def test_kernel_operator_commutative():
    # Adding kernels and multiplying kernels should be commutative.
    # Check addition
    assert_almost_equal((RBF(2.0) + 1.0)(X), (1.0 + RBF(2.0))(X))

    # Check multiplication
    assert_almost_equal((3.0 * RBF(2.0))(X), (RBF(2.0) * 3.0)(X))

def test_kernel_anisotropic():
    # Anisotropic kernel should be consistent with isotropic kernels.
    kernel = 3.0 * RBF([0.5, 2.0])

    K = kernel(X)
    X1 = X.copy()
    X1[:, 0] *= 4
    K1 = 3.0 * RBF(2.0)(X1)
    assert_almost_equal(K, K1)

    X2 = X.copy()
    X2[:, 1] /= 4
    K2 = 3.0 * RBF(0.5)(X2)
    assert_almost_equal(K, K2)

    # Check getting and setting via theta
    kernel.theta = kernel.theta + np.log(2)
    assert_array_equal(kernel.theta, np.log([6.0, 1.0, 4.0]))
    assert_array_equal(kernel.k2.length_scale, [1.0, 4.0])

def test_kernel_stationary(kernel):
    # Test stationarity of kernels.
    K = kernel(X, X + 1)
    assert_almost_equal(K[0, 0], np.diag(K))

def test_kernel_input_type(kernel):
    # Test whether kernels is for vectors or structured data
    if isinstance(kernel, Exponentiation):
        assert kernel.requires_vector_input == kernel.kernel.requires_vector_input
    if isinstance(kernel, KernelOperator):
        assert kernel.requires_vector_input == (
            kernel.k1.requires_vector_input or kernel.k2.requires_vector_input
        )

def test_compound_kernel_input_type():
    kernel = CompoundKernel([WhiteKernel(noise_level=3.0)])
    assert not kernel.requires_vector_input

    kernel = CompoundKernel([WhiteKernel(noise_level=3.0), RBF(length_scale=2.0)])
    assert kernel.requires_vector_input

def check_hyperparameters_equal(kernel1, kernel2):
    # Check that hyperparameters of two kernels are equal
    for attr in set(dir(kernel1) + dir(kernel2)):
        if attr.startswith("hyperparameter_"):
            attr_value1 = getattr(kernel1, attr)
            attr_value2 = getattr(kernel2, attr)
            assert attr_value1 == attr_value2

def test_kernel_clone(kernel):
    # Test that sklearn's clone works correctly on kernels.
    kernel_cloned = clone(kernel)

    # XXX: Should this be fixed?
    # This differs from the sklearn's estimators equality check.
    assert kernel == kernel_cloned
    assert id(kernel) != id(kernel_cloned)

    # Check that all constructor parameters are equal.
    assert kernel.get_params() == kernel_cloned.get_params()

    # Check that all hyperparameters are equal.
    check_hyperparameters_equal(kernel, kernel_cloned)

def test_kernel_clone_after_set_params(kernel):
    # This test is to verify that using set_params does not
    # break clone on kernels.
    # This used to break because in kernels such as the RBF, non-trivial
    # logic that modified the length scale used to be in the constructor
    # See https://github.com/scikit-learn/scikit-learn/issues/6961
    # for more details.
    bounds = (1e-5, 1e5)
    params = kernel.get_params()
    # RationalQuadratic kernel is isotropic.
    isotropic_kernels = (ExpSineSquared, RationalQuadratic)
    if "length_scale" in params and not isinstance(kernel, isotropic_kernels):
        length_scale = params["length_scale"]
        if np.iterable(length_scale):
            # XXX unreached code as of v0.22
            params["length_scale"] = length_scale[0]
            params["length_scale_bounds"] = bounds
        else:
            params["length_scale"] = [length_scale] * 2
            params["length_scale_bounds"] = bounds * 2
        kernel_cloned.set_params(**params)
        kernel_cloned_clone = clone(kernel_cloned)
        assert kernel_cloned_clone.get_params() == kernel_cloned.get_params()
        assert id(kernel_cloned_clone) != id(kernel_cloned)
        check_hyperparameters_equal(kernel_cloned, kernel_cloned_clone)

def test_matern_kernel():
    # Test consistency of Matern kernel for special values of nu.
    K = Matern(nu=1.5, length_scale=1.0)(X)
    # the diagonal elements of a matern kernel are 1
    assert_array_almost_equal(np.diag(K), np.ones(X.shape[0]))
    # matern kernel for coef0==0.5 is equal to absolute exponential kernel
    K_absexp = np.exp(-euclidean_distances(X, X, squared=False))
    K = Matern(nu=0.5, length_scale=1.0)(X)
    assert_array_almost_equal(K, K_absexp)
    # matern kernel with coef0==inf is equal to RBF kernel
    K_rbf = RBF(length_scale=1.0)(X)
    K = Matern(nu=np.inf, length_scale=1.0)(X)
    assert_array_almost_equal(K, K_rbf)
    assert_allclose(K, K_rbf)
    # test that special cases of matern kernel (coef0 in [0.5, 1.5, 2.5])
    # result in nearly identical results as the general case for coef0 in
    # [0.5 + tiny, 1.5 + tiny, 2.5 + tiny]
    tiny = 1e-10
    for nu in [0.5, 1.5, 2.5]:
        K1 = Matern(nu=nu, length_scale=1.0)(X)
        K2 = Matern(nu=nu + tiny, length_scale=1.0)(X)
        assert_array_almost_equal(K1, K2)
    # test that coef0==large is close to RBF
    large = 100
    K1 = Matern(nu=large, length_scale=1.0)(X)
    K2 = RBF(length_scale=1.0)(X)
    assert_array_almost_equal(K1, K2, decimal=2)

def test_kernel_versus_pairwise(kernel):
    # Check that GP kernels can also be used as pairwise kernels.

    # Test auto-kernel
    if kernel != kernel_rbf_plus_white:
        # For WhiteKernel: k(X) != k(X,X). This is assumed by
        # pairwise_kernels
        K1 = kernel(X)
        K2 = pairwise_kernels(X, metric=kernel)

    # Test cross-kernel
    K1 = kernel(X, Y)
    K2 = pairwise_kernels(X, Y, metric=kernel)
    assert_array_almost_equal(K1, K2)

def test_set_get_params(kernel):
    # Check that set_params()/get_params() is consistent with kernel.theta.

    # Test get_params()
    index = 0
    for hyperparameter in kernel.hyperparameters:
        if isinstance("string", type(hyperparameter.bounds)):
            if hyperparameter.bounds == "fixed":
                continue
        size = hyperparameter.n_elements
        if size > 1:  # anisotropic kernels
            assert_almost_equal(
                np.exp(kernel.theta[index : index + size]), params[hyperparameter.name]
            )
            index += size
        else:
                np.exp(kernel.theta[index]), params[hyperparameter.name]
            )
            index += 1
    # Test set_params()
    index = 0
    value = 10  # arbitrary value
                continue
            kernel.set_params(**{hyperparameter.name: [value] * size})
                np.exp(kernel.theta[index : index + size]), [value] * size
            )
        else:
            kernel.set_params(**{hyperparameter.name: value})
            assert_almost_equal(np.exp(kernel.theta[index]), value)
            index += 1

def test_repr_kernels(kernel):
    # Smoke-test for repr in kernels.

    repr(kernel)

def test_rational_quadratic_kernel():
    kernel = RationalQuadratic(length_scale=[1.0, 1.0])
    message = (
        "RationalQuadratic kernel only supports isotropic "
        "version, please use a single "
        "scalar for length_scale"
    )
    with pytest.raises(AttributeError, match=message):
        kernel(X)

        return K

class MiniSeqKernel(GenericKernelMixin, StationaryKernelMixin, Kernel):
    """
    A minimal (but valid) convolutional kernel for sequences of variable
    length.
    """

    def __init__(self, baseline_similarity=0.5, baseline_similarity_bounds=(1e-5, 1)):
        self.baseline_similarity = baseline_similarity
        self.baseline_similarity_bounds = baseline_similarity_bounds

    @property
    def hyperparameter_baseline_similarity(self):
        return Hyperparameter(
            "baseline_similarity", "numeric", self.baseline_similarity_bounds
        )

    def _f(self, s1, s2):
        return sum(
            [1.0 if c1 == c2 else self.baseline_similarity for c1 in s1 for c2 in s2]
        )

    def _g(self, s1, s2):
        return sum([0.0 if c1 == c2 else 1.0 for c1 in s1 for c2 in s2])

            Y = X

            return (
                np.array([[self._f(x, y) for y in Y] for x in X]),
                np.array([[[self._g(x, y)] for y in Y] for x in X]),
            )
        else:
            return np.array([[self._f(x, y) for y in Y] for x in X])

        return np.array([self._f(x, x) for x in X])

        )

        )

            Y = X

            return (
            )
        else:

def test_gpr_interpolation(kernel):
    if sys.maxsize <= 2**32:
        pytest.xfail("This test may fail on 32 bit Python")

    # Test the interpolating property for different kernels.
    gpr = GaussianProcessRegressor(kernel=kernel).fit(X, y)
    y_pred, y_cov = gpr.predict(X, return_cov=True)

    assert_almost_equal(y_pred, y)
    assert_almost_equal(np.diag(y_cov), 0.0)

def test_gpr_interpolation_structured():
    kernel = MiniSeqKernel(baseline_similarity_bounds="fixed")
    X = ["A", "B", "C"]
    y = np.array([1, 2, 3])

        kernel(X, eval_gradient=True)[1].ravel(), (1 - np.eye(len(X))).ravel()
    )

def test_lml_improving(kernel):

    # Test that hyperparameter-tuning improves log-marginal likelihood.
    assert gpr.log_marginal_likelihood(gpr.kernel_.theta) > gpr.log_marginal_likelihood(
        kernel.theta
    )

def test_lml_precomputed(kernel):
    # Test that lml of optimized kernel is stored correctly.
    assert gpr.log_marginal_likelihood(gpr.kernel_.theta) == pytest.approx(
        gpr.log_marginal_likelihood()
    )

def test_lml_without_cloning_kernel(kernel):
    input_theta = np.ones(gpr.kernel_.theta.shape, dtype=np.float64)

    gpr.log_marginal_likelihood(input_theta, clone_kernel=False)
    assert_almost_equal(gpr.kernel_.theta, input_theta, 7)

def test_converged_to_local_maximum(kernel):
    # Test that we are in local maximum after hyperparameter-optimization.

    lml, lml_gradient = gpr.log_marginal_likelihood(gpr.kernel_.theta, True)

        (np.abs(lml_gradient) < 1e-4)
        | (gpr.kernel_.theta == gpr.kernel_.bounds[:, 0])
        | (gpr.kernel_.theta == gpr.kernel_.bounds[:, 1])
    )

def test_solution_inside_bounds(kernel):
    # Test that hyperparameter-optimization remains in bounds#

    bounds = gpr.kernel_.bounds
    max_ = np.finfo(gpr.kernel_.theta.dtype).max
    bounds[~np.isfinite(bounds[:, 1]), 1] = max_

    assert_array_less(bounds[:, 0], gpr.kernel_.theta + tiny)
    assert_array_less(gpr.kernel_.theta, bounds[:, 1] + tiny)

def test_lml_gradient(kernel):
    # Compare analytic and numeric gradient of log marginal likelihood.

    lml, lml_gradient = gpr.log_marginal_likelihood(kernel.theta, True)
    lml_gradient_approx = approx_fprime(
        kernel.theta, lambda theta: gpr.log_marginal_likelihood(theta, False), 1e-10
    )

    assert_almost_equal(lml_gradient, lml_gradient_approx, 3)

def test_prior(kernel):
    # Test that GP prior has mean 0 and identical variances.
    gpr = GaussianProcessRegressor(kernel=kernel)

    y_mean, y_cov = gpr.predict(X, return_cov=True)

    assert_almost_equal(y_mean, 0, 5)
    if len(gpr.kernel.theta) > 1:
        # XXX: quite hacky, works only for current kernels
        assert_almost_equal(np.diag(y_cov), np.exp(kernel.theta[0]), 5)
    else:
        assert_almost_equal(np.diag(y_cov), 1, 5)

def test_sample_statistics(kernel):
    # Test that statistics of samples drawn from GP are correct.

    y_mean, y_cov = gpr.predict(X2, return_cov=True)

    samples = gpr.sample_y(X2, 300000)

    # More digits accuracy would require many more samples
    assert_almost_equal(y_mean, np.mean(samples, 1), 1)
        np.diag(y_cov) / np.diag(y_cov).max(),
        np.var(samples, 1) / np.diag(y_cov).max(),
        1,
    )

def test_no_optimizer():
    # Test that kernel parameters are unmodified when optimizer is None.
    kernel = RBF(1.0)
    gpr = GaussianProcessRegressor(kernel=kernel, optimizer=None).fit(X, y)
    assert np.exp(gpr.kernel_.theta) == 1.0

def test_predict_cov_vs_std(kernel, target):

    # Test that predicted std.-dev. is consistent with cov's diagonal.
    y_mean, y_std = gpr.predict(X2, return_std=True)
    assert_almost_equal(np.sqrt(np.diag(y_cov)), y_std)

def test_anisotropic_kernel():
    # Test that GPR can identify meaningful anisotropic length-scales.
    # We learn a function which varies in one dimension ten-times slower
    # than in the other. The corresponding length-scales should differ by at
    # least a factor 5
    X = rng.uniform(-1, 1, (50, 2))
    y = X[:, 0] + 0.1 * X[:, 1]

    kernel = RBF([1.0, 1.0])
    assert np.exp(gpr.kernel_.theta[1]) > np.exp(gpr.kernel_.theta[0]) * 5

def test_random_starts():
    # Test that an increasing number of random-starts of GP fitting only
    # increases the log marginal likelihood of the chosen theta.
    n_samples, n_features = 25, 2
    X = rng.randn(n_samples, n_features) * 2 - 1
    y = (
        np.sin(X).sum(axis=1)
        + np.sin(3 * X).sum(axis=1)
        + rng.normal(scale=0.1, size=n_samples)
    )

    kernel = C(1.0, (1e-2, 1e2)) * RBF(
        length_scale=[1.0] * n_features, length_scale_bounds=[(1e-4, 1e2)] * n_features
    ) + WhiteKernel(noise_level=1e-5, noise_level_bounds=(1e-5, 1e1))
    last_lml = -np.inf
    for n_restarts_optimizer in range(5):
        gp = GaussianProcessRegressor(
            kernel=kernel,
            n_restarts_optimizer=n_restarts_optimizer,
        ).fit(X, y)
        lml = gp.log_marginal_likelihood(gp.kernel_.theta)
        assert lml > last_lml - np.finfo(np.float32).eps
        last_lml = lml

def test_y_normalization(kernel):
    """
    Test normalization of the target values in GP

    Fitting non-normalizing GP on normalized y and fitting normalizing GP
    on unnormalized y should yield identical results. Note that, here,
    'normalized y' refers to y that has been made zero mean and unit
    variance.

    """

    y_mean = np.mean(y)
    y_std = np.std(y)
    y_norm = (y - y_mean) / y_std

    # Fit non-normalizing GP on normalized y
    gpr.fit(X, y_norm)

    # Fit normalizing GP on unnormalized y
    gpr_norm = GaussianProcessRegressor(kernel=kernel, normalize_y=True)
    gpr_norm.fit(X, y)

    # Compare predicted mean, std-devs and covariances
    y_pred, y_pred_std = gpr.predict(X2, return_std=True)
    y_pred = y_pred * y_std + y_mean
    y_pred_std = y_pred_std * y_std
    y_pred_norm, y_pred_std_norm = gpr_norm.predict(X2, return_std=True)

    assert_almost_equal(y_pred, y_pred_norm)
    assert_almost_equal(y_pred_std, y_pred_std_norm)

    _, y_cov = gpr.predict(X2, return_cov=True)
    y_cov = y_cov * y_std**2
    _, y_cov_norm = gpr_norm.predict(X2, return_cov=True)

    assert_almost_equal(y_cov, y_cov_norm)

def test_large_variance_y():
    """
    Here we test that, when noramlize_y=True, our GP can produce a
    sensible fit to training data whose variance is significantly
    larger than unity. This test was made in response to issue #15612.

    GP predictions are verified against predictions that were made
    using GPy which, here, is treated as the 'gold standard'. Note that we
    only investigate the RBF kernel here, as that is what was used in the
    GPy implementation.

    The following code can be used to recreate the GPy data:

    --------------------------------------------------------------------------
    import GPy

    kernel_gpy = GPy.kern.RBF(input_dim=1, lengthscale=1.)
    gpy = GPy.models.GPRegression(X, np.vstack(y_large), kernel_gpy)
    gpy.optimize()
    y_pred_gpy, y_var_gpy = gpy.predict(X2)
    y_pred_std_gpy = np.sqrt(y_var_gpy)
    """

    # Here we utilise a larger variance version of the training data
    y_large = 10 * y

    # Standard GP with normalize_y=True
    RBF_params = {"length_scale": 1.0}
    kernel = RBF(**RBF_params)
    gpr = GaussianProcessRegressor(kernel=kernel, normalize_y=True)
    gpr.fit(X, y_large)

    # 'Gold standard' mean predictions from GPy
    y_pred_gpy = np.array(
        [15.16918303, -27.98707845, -39.31636019, 14.52605515, 69.18503589]
    )

    # 'Gold standard' std predictions from GPy
    y_pred_std_gpy = np.array(
        [7.78860962, 3.83179178, 0.63149951, 0.52745188, 0.86170042]
    )

    # Based on numerical experiments, it's reasonable to expect our
    # GP's mean predictions to get within 7% of predictions of those
    # made by GPy.
    assert_allclose(y_pred, y_pred_gpy, rtol=0.07, atol=0)

    # GP's std predictions to get within 15% of predictions of those
    assert_allclose(y_pred_std, y_pred_std_gpy, rtol=0.15, atol=0)

def test_y_multioutput():
    # Test that GPR can deal with multi-dimensional target values
    y_2d = np.vstack((y, y * 2)).T

    # Test for fixed kernel that first dimension of 2d GP equals the output
    # of 1d GP and that second dimension is twice as large
    kernel = RBF(length_scale=1.0)

    gpr = GaussianProcessRegressor(kernel=kernel, optimizer=None, normalize_y=False)
    gpr.fit(X, y)

    gpr_2d = GaussianProcessRegressor(kernel=kernel, optimizer=None, normalize_y=False)
    gpr_2d.fit(X, y_2d)

    y_pred_1d, y_std_1d = gpr.predict(X2, return_std=True)
    y_pred_2d, y_std_2d = gpr_2d.predict(X2, return_std=True)
    _, y_cov_1d = gpr.predict(X2, return_cov=True)
    _, y_cov_2d = gpr_2d.predict(X2, return_cov=True)

    assert_almost_equal(y_pred_1d, y_pred_2d[:, 0])
    assert_almost_equal(y_pred_1d, y_pred_2d[:, 1] / 2)

    # Standard deviation and covariance do not depend on output
    for target in range(y_2d.shape[1]):
        assert_almost_equal(y_std_1d, y_std_2d[..., target])
        assert_almost_equal(y_cov_1d, y_cov_2d[..., target])

    y_sample_1d = gpr.sample_y(X2, n_samples=10)
    y_sample_2d = gpr_2d.sample_y(X2, n_samples=10)

    assert y_sample_1d.shape == (5, 10)
    assert y_sample_2d.shape == (5, 2, 10)
    # Only the first target will be equal
    assert_almost_equal(y_sample_1d, y_sample_2d[:, 0, :])

    # Test hyperparameter optimization
    for kernel in kernels:
        gpr = GaussianProcessRegressor(kernel=kernel, normalize_y=True)
        gpr.fit(X, y)

        gpr_2d = GaussianProcessRegressor(kernel=kernel, normalize_y=True)
        gpr_2d.fit(X, np.vstack((y, y)).T)

        assert_almost_equal(gpr.kernel_.theta, gpr_2d.kernel_.theta, 4)

def test_custom_optimizer(kernel):
    # Test that GPR can use externally defined optimizers.
    # Define a dummy optimizer that simply tests 50 random hyperparameters
    def optimizer(obj_func, initial_theta, bounds):
        rng = np.random.RandomState(0)
        theta_opt, func_min = (
            initial_theta,
            obj_func(initial_theta, eval_gradient=False),
        )
        for _ in range(50):
            theta = np.atleast_1d(
                rng.uniform(np.maximum(-2, bounds[:, 0]), np.minimum(1, bounds[:, 1]))
            )
            f = obj_func(theta, eval_gradient=False)
            if f < func_min:
                theta_opt, func_min = theta, f

    gpr = GaussianProcessRegressor(kernel=kernel, optimizer=optimizer)
    # Checks that optimizer improved marginal likelihood
        gpr.kernel.theta
    )

def test_gpr_correct_error_message():
    X = np.arange(12).reshape(6, -1)
    y = np.ones(6)
    kernel = DotProduct()
    gpr = GaussianProcessRegressor(kernel=kernel, alpha=0.0)
    message = (
        "The kernel, %s, is not returning a "
        "positive definite matrix. Try gradually increasing "
        "the 'alpha' parameter of your "
        "GaussianProcessRegressor estimator." % kernel
    )
    with pytest.raises(np.linalg.LinAlgError, match=re.escape(message)):

def test_duplicate_input(kernel):
    # Test GPR can handle two different output-values for the same input.
    gpr_equal_inputs = GaussianProcessRegressor(kernel=kernel, alpha=1e-2)
    gpr_similar_inputs = GaussianProcessRegressor(kernel=kernel, alpha=1e-2)

    X_ = np.vstack((X, X[0]))
    y_ = np.hstack((y, y[0] + 1))
    gpr_equal_inputs.fit(X_, y_)

    X_ = np.vstack((X, X[0] + 1e-15))
    gpr_similar_inputs.fit(X_, y_)

    X_test = np.linspace(0, 10, 100)[:, None]
    y_pred_equal, y_std_equal = gpr_equal_inputs.predict(X_test, return_std=True)
    y_pred_similar, y_std_similar = gpr_similar_inputs.predict(X_test, return_std=True)

    assert_almost_equal(y_pred_equal, y_pred_similar)
    assert_almost_equal(y_std_equal, y_std_similar)

def test_no_fit_default_predict():
    # Test that GPR predictions without fit does not break by default.
    default_kernel = C(1.0, constant_value_bounds="fixed") * RBF(
        1.0, length_scale_bounds="fixed"
    )
    gpr1 = GaussianProcessRegressor()
    _, y_std1 = gpr1.predict(X, return_std=True)
    _, y_cov1 = gpr1.predict(X, return_cov=True)

    gpr2 = GaussianProcessRegressor(kernel=default_kernel)
    _, y_std2 = gpr2.predict(X, return_std=True)
    _, y_cov2 = gpr2.predict(X, return_cov=True)

    assert_array_almost_equal(y_std1, y_std2)
    assert_array_almost_equal(y_cov1, y_cov2)

def test_warning_bounds():
    kernel = RBF(length_scale_bounds=[1e-5, 1e-3])
    warning_message = (
        "The optimal value found for dimension 0 of parameter "
        "length_scale is close to the specified upper bound "
        "0.001. Increasing the bound and calling fit again may "
        "find a better value."
    )
    with pytest.warns(ConvergenceWarning, match=warning_message):

    kernel_sum = WhiteKernel(noise_level_bounds=[1e-5, 1e-3]) + RBF(
        length_scale_bounds=[1e3, 1e5]
    )
    gpr_sum = GaussianProcessRegressor(kernel=kernel_sum)
    with warnings.catch_warnings(record=True) as record:
        warnings.simplefilter("always")
        gpr_sum.fit(X, y)

        assert len(record) == 2

        assert issubclass(record[0].category, ConvergenceWarning)
        assert (
            record[0].message.args[0] == "The optimal value found for "
            "dimension 0 of parameter "
            "k1__noise_level is close to the "
            "specified upper bound 0.001. "
            "Increasing the bound and calling "
            "fit again may find a better value."
        )

        assert issubclass(record[1].category, ConvergenceWarning)
        assert (
            record[1].message.args[0] == "The optimal value found for "
            "k2__length_scale is close to the "
            "specified lower bound 1000.0. "
            "Decreasing the bound and calling "
        )

    X_tile = np.tile(X, 2)
    kernel_dims = RBF(length_scale=[1.0, 2.0], length_scale_bounds=[1e1, 1e2])
    gpr_dims = GaussianProcessRegressor(kernel=kernel_dims)

        gpr_dims.fit(X_tile, y)

        assert (
            "length_scale is close to the "
            "specified lower bound 10.0. "
        )

        assert (
            "dimension 1 of parameter "
        )

def test_bound_check_fixed_hyperparameter():
    # Regression test for issue #17943
    # Check that having a hyperparameter with fixed bounds doesn't cause an
    # error
    k1 = 50.0**2 * RBF(length_scale=50.0)  # long term smooth rising trend
    k2 = ExpSineSquared(
        length_scale=1.0, periodicity=1.0, periodicity_bounds="fixed"
    )  # seasonal component
    kernel = k1 + k2
    GaussianProcessRegressor(kernel=kernel).fit(X, y)

def test_constant_target(kernel):
    """Check that the std. dev. is affected to 1 when normalizing a constant
    feature.
    https://github.com/scikit-learn/scikit-learn/issues/18318
    NaN where affected to the target when scaling due to null std. dev. with
    constant target.
    """
    y_constant = np.ones(X.shape[0], dtype=np.float64)

    gpr.fit(X, y_constant)
    assert gpr._y_train_std == pytest.approx(1.0)

    assert_allclose(y_pred, y_constant)
    # set atol because we compare to zero
    assert_allclose(np.diag(y_cov), 0.0, atol=1e-9)

    # Test multi-target data
    n_samples, n_targets = X.shape[0], 2
    y = np.concatenate(
        [
            rng.normal(size=(n_samples, 1)),  # non-constant target
            np.full(shape=(n_samples, 1), fill_value=2),  # constant target
        ],
        axis=1,
    )

    Y_pred, Y_cov = gpr.predict(X, return_cov=True)

    assert_allclose(Y_pred[:, 1], 2)
    assert_allclose(np.diag(Y_cov[..., 1]), 0.0, atol=1e-9)

    assert Y_pred.shape == (n_samples, n_targets)
    assert Y_cov.shape == (n_samples, n_samples, n_targets)

def test_gpr_consistency_std_cov_non_invertible_kernel():
    """Check the consistency between the returned std. dev. and the covariance.
    https://github.com/scikit-learn/scikit-learn/issues/19936
    Inconsistencies were observed when the kernel cannot be inverted (or
    numerically stable).
    """
    kernel = C(8.98576054e05, (1e-12, 1e12)) * RBF(
        [5.91326520e02, 1.32584051e03], (1e-12, 1e12)
    ) + WhiteKernel(noise_level=1e-5)
    gpr = GaussianProcessRegressor(kernel=kernel, alpha=0, optimizer=None)
    X_train = np.array(
        [
            [0.0, 0.0],
            [1.54919334, -0.77459667],
            [-1.54919334, 0.0],
            [0.0, -1.54919334],
            [0.77459667, 0.77459667],
            [-0.77459667, 1.54919334],
        ]
    )
    y_train = np.array(
        [
            [-2.14882017e-10],
            [-4.66975823e00],
            [4.01823986e00],
            [-1.30303674e00],
            [-1.35760156e00],
            [3.31215668e00],
        ]
    )
    gpr.fit(X_train, y_train)
    X_test = np.array(
        [
            [-1.93649167, -1.93649167],
            [1.93649167, -1.93649167],
            [-1.93649167, 1.93649167],
            [1.93649167, 1.93649167],
        ]
    )
    pred1, std = gpr.predict(X_test, return_std=True)
    pred2, cov = gpr.predict(X_test, return_cov=True)
    assert_allclose(std, np.sqrt(np.diagonal(cov)), rtol=1e-5)

def test_gpr_fit_error(params, TypeError, err_msg):
    """Check that expected error are raised during fit."""
    gpr = GaussianProcessRegressor(**params)
    with pytest.raises(TypeError, match=err_msg):

def test_gpr_lml_error():
    """Check that we raise the proper error in the LML method."""
    gpr = GaussianProcessRegressor(kernel=RBF()).fit(X, y)

    err_msg = "Gradient can only be evaluated for theta!=None"
        gpr.log_marginal_likelihood(eval_gradient=True)

def test_gpr_predict_error():
    """Check that we raise the proper error during predict."""

    err_msg = "At most one of return_std or return_cov can be requested."
    with pytest.raises(RuntimeError, match=err_msg):
        gpr.predict(X, return_cov=True, return_std=True)

def test_predict_shapes(normalize_y, n_targets):
    """Check the shapes of y_mean, y_std, and y_cov in single-output
    (n_targets=None) and multi-output settings, including the edge case when
    n_targets=1, where the sklearn convention is to squeeze the predictions.

    https://github.com/scikit-learn/scikit-learn/issues/17394
    https://github.com/scikit-learn/scikit-learn/issues/18065
    https://github.com/scikit-learn/scikit-learn/issues/22174
    """
    rng = np.random.RandomState(1234)

    n_features, n_samples_train, n_samples_test = 6, 9, 7

    y_train_shape = (n_samples_train,)
    if n_targets is not None:
        y_train_shape = y_train_shape + (n_targets,)

    # By convention single-output data is squeezed upon prediction
    y_test_shape = (n_samples_test,)
    if n_targets is not None and n_targets > 1:
        y_test_shape = y_test_shape + (n_targets,)

    X_train = rng.randn(n_samples_train, n_features)
    X_test = rng.randn(n_samples_test, n_features)
    y_train = rng.randn(*y_train_shape)

    model = GaussianProcessRegressor(normalize_y=normalize_y)

    y_pred, y_std = model.predict(X_test, return_std=True)
    _, y_cov = model.predict(X_test, return_cov=True)

    assert y_pred.shape == y_test_shape
    assert y_std.shape == y_test_shape
    assert y_cov.shape == (n_samples_test,) + y_test_shape

def test_sample_y_shapes(normalize_y, n_targets):
    """Check the shapes of y_samples in single-output (n_targets=0) and
    multi-output settings, including the edge case when n_targets=1, where the
    sklearn convention is to squeeze the predictions.

    https://github.com/scikit-learn/scikit-learn/issues/22175
    """

    n_features, n_samples_train = 6, 9
    # Number of spatial locations to predict at
    n_samples_X_test = 7
    # Number of sample predictions per test point
    n_samples_y_test = 5

        y_test_shape = (n_samples_X_test, n_targets, n_samples_y_test)
    else:
        y_test_shape = (n_samples_X_test, n_samples_y_test)

    X_test = rng.randn(n_samples_X_test, n_features)

    # FIXME: before fitting, the estimator does not have information regarding
    # the number of targets and default to 1. This is inconsistent with the shape
    # provided after `fit`. This assert should be made once the following issue
    # is fixed:
    # https://github.com/scikit-learn/scikit-learn/issues/22430
    # y_samples = model.sample_y(X_test, n_samples=n_samples_y_test)
    # assert y_samples.shape == y_test_shape

    y_samples = model.sample_y(X_test, n_samples=n_samples_y_test)
    assert y_samples.shape == y_test_shape

def test_sample_y_shape_with_prior(n_targets, n_samples):
    """Check the output shape of `sample_y` is consistent before and after `fit`."""
    rng = np.random.RandomState(1024)

    X = rng.randn(10, 3)
    y = rng.randn(10, n_targets if n_targets is not None else 1)

    model = GaussianProcessRegressor(n_targets=n_targets)
    shape_before_fit = model.sample_y(X, n_samples=n_samples).shape
    model.fit(X, y)
    shape_after_fit = model.sample_y(X, n_samples=n_samples).shape
    assert shape_before_fit == shape_after_fit

def test_predict_shape_with_prior(n_targets):
    """Check the output shape of `predict` with prior distribution."""

    n_sample = 10
    X = rng.randn(n_sample, 3)
    y = rng.randn(n_sample, n_targets if n_targets is not None else 1)

    mean_prior, cov_prior = model.predict(X, return_cov=True)
    _, std_prior = model.predict(X, return_std=True)

    mean_post, cov_post = model.predict(X, return_cov=True)
    _, std_post = model.predict(X, return_std=True)

    assert mean_prior.shape == mean_post.shape
    assert cov_prior.shape == cov_post.shape
    assert std_prior.shape == std_post.shape

def test_n_targets_error():
    """Check that an error is raised when the number of targets seen at fit is
    inconsistent with n_targets.
    """
    y = rng.randn(10, 2)

    model = GaussianProcessRegressor(n_targets=1)
    with pytest.raises(ValueError, match="The number of targets seen in `y`"):
        model.fit(X, y)

class CustomKernel(C):
    """
    A custom kernel that has a diag method that returns the first column of the
    input matrix X. This is a helper for the test to check that the input
    matrix X is not mutated.
    """

        return X[:, 0]

def test_gpr_predict_input_not_modified():
    """
    Check that the input X is not modified by the predict method of the
    GaussianProcessRegressor when setting return_std=True.

    https://github.com/scikit-learn/scikit-learn/issues/24340
    """
    gpr = GaussianProcessRegressor(kernel=CustomKernel()).fit(X, y)

    X2_copy = np.copy(X2)
    _, _ = gpr.predict(X2, return_std=True)

    assert_allclose(X2, X2_copy)

        )
            )

def test_predict_consistent(kernel):
    # Check binary predict decision has also predicted probability above 0.5.
    gpc = GaussianProcessClassifier(kernel=kernel).fit(X, y)
    assert_array_equal(gpc.predict(X), gpc.predict_proba(X)[:, 1] >= 0.5)

def test_predict_consistent_structured():
    X = ["A", "AB", "B"]
    y = np.array([True, False, True])

    assert gpc.log_marginal_likelihood(gpc.kernel_.theta) > gpc.log_marginal_likelihood(
    )

        gpc.log_marginal_likelihood(gpc.kernel_.theta), gpc.log_marginal_likelihood(), 7
    )

    # Test that clone_kernel=False has side-effects of kernel.theta.
    input_theta = np.ones(gpc.kernel_.theta.shape, dtype=np.float64)

    gpc.log_marginal_likelihood(input_theta, clone_kernel=False)
    assert_almost_equal(gpc.kernel_.theta, input_theta, 7)

    lml, lml_gradient = gpc.log_marginal_likelihood(gpc.kernel_.theta, True)

        | (gpc.kernel_.theta == gpc.kernel_.bounds[:, 0])
        | (gpc.kernel_.theta == gpc.kernel_.bounds[:, 1])
    )

    lml, lml_gradient = gpc.log_marginal_likelihood(kernel.theta, True)
        kernel.theta, lambda theta: gpc.log_marginal_likelihood(theta, False), 1e-10
    )

def test_random_starts(global_random_seed):
    y = (np.sin(X).sum(axis=1) + np.sin(3 * X).sum(axis=1)) > 0

        length_scale=[1e-3] * n_features, length_scale_bounds=[(1e-4, 1e2)] * n_features
    )
        gp = GaussianProcessClassifier(

def test_custom_optimizer(kernel, global_random_seed):
    # Test that GPC can use externally defined optimizers.
    # Define a dummy optimizer that simply tests 10 random hyperparameters
        rng = np.random.RandomState(global_random_seed)
        )
        for _ in range(10):
            )

    gpc = GaussianProcessClassifier(kernel=kernel, optimizer=optimizer)
    gpc.fit(X, y_mc)
    assert gpc.log_marginal_likelihood(
        gpc.kernel_.theta
    ) >= gpc.log_marginal_likelihood(kernel.theta)

def test_multi_class(kernel):
    # Test GPC for multi-class classification problems.
    gpc = GaussianProcessClassifier(kernel=kernel)

    y_prob = gpc.predict_proba(X2)
    assert_almost_equal(y_prob.sum(1), 1)

    y_pred = gpc.predict(X2)
    assert_array_equal(np.argmax(y_prob, 1), y_pred)

def test_multi_class_n_jobs(kernel):
    # Test that multi-class GPC produces identical results with n_jobs>1.

    gpc_2 = GaussianProcessClassifier(kernel=kernel, n_jobs=2)
    gpc_2.fit(X, y_mc)

    y_prob_2 = gpc_2.predict_proba(X2)
    assert_almost_equal(y_prob, y_prob_2)

    )
        gpc.fit(X, y)

    )
    gpc_sum = GaussianProcessClassifier(kernel=kernel_sum)
        gpc_sum.fit(X, y)

        assert (
        )

        assert (
        )

    gpc_dims = GaussianProcessClassifier(kernel=kernel_dims)

        gpc_dims.fit(X_tile, y)

        assert (
            "specified upper bound 100.0. "
        )

        assert (
        )

def test_gpc_fit_error(params, error_type, err_msg):
    gpc = GaussianProcessClassifier(**params)
    with pytest.raises(error_type, match=err_msg):

        )
            )

class DictVectorizer(TransformerMixin, BaseEstimator):
    """Transforms lists of feature-value mappings to vectors.

    This transformer turns lists of mappings (dict-like objects) of feature
    names to feature values into Numpy arrays or scipy.sparse matrices for use
    with scikit-learn estimators.

    When feature values are strings, this transformer will do a binary one-hot
    (aka one-of-K) coding: one boolean-valued feature is constructed for each
    of the possible string values that the feature can take on. For instance,
    a feature "f" that can take on the values "ham" and "spam" will become two
    features in the output, one signifying "f=ham", the other "f=spam".

    If a feature value is a sequence or set of strings, this transformer
    will iterate over the values and will count the occurrences of each string
    value.

    However, note that this transformer will only do a binary one-hot encoding
    when feature values are of type string. If categorical features are
    represented as numeric values such as int or iterables of strings, the
    DictVectorizer can be followed by
    :class:`~sklearn.preprocessing.OneHotEncoder` to complete
    binary one-hot encoding.

    Features that do not occur in a sample (mapping) will have a zero value
    in the resulting array/matrix.

    For an efficiency comparison of the different feature extractors, see
    :ref:`sphx_glr_auto_examples_text_plot_hashing_vs_dict_vectorizer.py`.

    Read more in the :ref:`User Guide <dict_feature_extraction>`.

    dtype : dtype, default=np.float64
        The type of feature values. Passed to Numpy array/scipy.sparse matrix
        constructors as the dtype argument.
    separator : str, default="="
        Separator string used when constructing new features for one-hot
        coding.
    sparse : bool, default=True
        Whether transform should produce scipy.sparse matrices.
    sort : bool, default=True
        Whether ``feature_names_`` and ``vocabulary_`` should be
        sorted when fitting.

    vocabulary_ : dict
        A dictionary mapping feature names to feature indices.

    feature_names_ : list
        A list of length n_features containing the feature names (e.g., "f=ham"
        and "f=spam").

    See Also
    --------
    FeatureHasher : Performs vectorization using only a hash function.
    sklearn.preprocessing.OrdinalEncoder : Handles nominal/categorical
        features encoded as columns of arbitrary data types.

    Examples
    --------
    >>> from sklearn.feature_extraction import DictVectorizer
    >>> v = DictVectorizer(sparse=False)
    >>> D = [{'foo': 1, 'bar': 2}, {'foo': 3, 'baz': 1}]
    >>> X = v.fit_transform(D)
    >>> X
    array([[2., 0., 1.],
           [0., 1., 3.]])
    >>> v.inverse_transform(X) == [{'bar': 2.0, 'foo': 1.0},
    ...                            {'baz': 1.0, 'foo': 3.0}]
    True
    >>> v.transform({'foo': 4, 'unseen_feature': 3})
    array([[0., 0., 4.]])
    """

    # This isn't something that people should be routing / using in a pipeline.
    __metadata_request__inverse_transform = {"dict_type": metadata_routing.UNUSED}

        "dtype": "no_validation",  # validation delegated to numpy,
        "separator": [str],
        "sparse": ["boolean"],
        "sort": ["boolean"],
    }

    def __init__(self, *, dtype=np.float64, separator="=", sparse=True, sort=True):
        self.dtype = dtype
        self.separator = separator
        self.sparse = sparse
        self.sort = sort

    def _add_iterable_element(
        self,
        f,
        v,
        feature_names,
        vocab,
        *,
        fitting=True,
        transforming=False,
        indices=None,
        values=None,
    ):
        """Add feature names for iterable of strings"""
        for vv in v:
            if isinstance(vv, str):
                feature_name = "%s%s%s" % (f, self.separator, vv)
                vv = 1
            else:
                    f"Unsupported type {type(vv)} in iterable "
                    "value. Only iterables of string are "
                    "supported."
                )
            if fitting and feature_name not in vocab:
                vocab[feature_name] = len(feature_names)
                feature_names.append(feature_name)

            if transforming and feature_name in vocab:
                indices.append(vocab[feature_name])
                values.append(self.dtype(vv))

        """Learn a list of feature name -> indices mappings.

        X : Mapping or iterable over Mappings
            Dict(s) or Mapping(s) from feature names (arbitrary Python
            objects) to feature values (strings or convertible to dtype).

               Accepts multiple string values for one categorical feature.

        y : (ignored)
            Ignored parameter.

        Returns
        -------
            DictVectorizer class instance.
        """
        feature_names = []
        vocab = {}

        for x in X:
            for f, v in x.items():
                if isinstance(v, str):
                    feature_name = "%s%s%s" % (f, self.separator, v)
                elif isinstance(v, Number) or (v is None):
                    feature_name = f
                elif isinstance(v, Mapping):
                    raise TypeError(
                        f"Unsupported value type {type(v)} "
                        f"for {f}: {v}.\n"
                        "Mapping objects are not supported."
                    )
                elif isinstance(v, Iterable):
                    feature_name = None
                    self._add_iterable_element(f, v, feature_names, vocab)

                if feature_name is not None:
                    if feature_name not in vocab:
                        vocab[feature_name] = len(feature_names)
                        feature_names.append(feature_name)

        if self.sort:
            feature_names.sort()
            vocab = {f: i for i, f in enumerate(feature_names)}

        self.feature_names_ = feature_names
        self.vocabulary_ = vocab

    def _transform(self, X, fitting):
        # Sanity check: Python's array has no way of explicitly requesting the
        # signed 32-bit integers that scipy.sparse needs, so we use the next
        # best thing: typecode "i" (int). However, if that gives larger or
        # smaller integers than 32-bit ones, np.frombuffer screws up.
        assert array("i").itemsize == 4, (
            "sizeof(int) != 4 on your platform; please report this at"
            " https://github.com/scikit-learn/scikit-learn/issues and"
            " include the output from platform.platform() in your bug report"
        )

        dtype = self.dtype
        if fitting:
            feature_names = []
            vocab = {}
        else:
            feature_names = self.feature_names_
            vocab = self.vocabulary_

        transforming = True

        # Process everything as sparse regardless of setting
        X = [X] if isinstance(X, Mapping) else X

        indices = array("i")
        indptr = [0]
        # XXX we could change values to an array.array as well, but it
        # would require (heuristic) conversion of dtype to typecode...
        values = []

        # collect all the possible feature names and build sparse matrix at
        # same time
        for x in X:
                    v = 1
                elif not isinstance(v, Mapping) and isinstance(v, Iterable):
                    self._add_iterable_element(
                        f,
                        v,
                        feature_names,
                        vocab,
                        fitting=fitting,
                        transforming=transforming,
                        indices=indices,
                        values=values,
                    )
                else:
                        f"Unsupported value Type {type(v)} "
                        f"{type(v)} objects are not supported."
                    )

                    if fitting and feature_name not in vocab:

                    if feature_name in vocab:
                        indices.append(vocab[feature_name])
                        values.append(self.dtype(v))

            indptr.append(len(indices))

        if len(indptr) == 1:
            raise ValueError("Sample sequence X is empty.")

        indices = np.frombuffer(indices, dtype=np.intc)
        shape = (len(indptr) - 1, len(vocab))

        result_matrix = sp.csr_matrix(
            (values, indices, indptr), shape=shape, dtype=dtype
        )

        # Sort everything if asked
        if fitting and self.sort:
            map_index = np.empty(len(feature_names), dtype=np.int32)
            for new_val, f in enumerate(feature_names):
                map_index[new_val] = vocab[f]
                vocab[f] = new_val
            result_matrix = result_matrix[:, map_index]

        if self.sparse:
            result_matrix.sort_indices()
        else:
            result_matrix = result_matrix.toarray()

            self.feature_names_ = feature_names
            self.vocabulary_ = vocab

        return result_matrix

    def fit_transform(self, X, y=None):
        """Learn a list of feature name -> indices mappings and transform X.

        Like fit(X) followed by transform(X), but does not require
        materializing X in memory.

        Returns
        -------
        Xa : {array, sparse matrix}
            Feature vectors; always 2-d.
        """
        return self._transform(X, fitting=True)

    def inverse_transform(self, X, dict_type=dict):
        """Transform array or sparse matrix X back to feature mappings.

        X must have been produced by this DictVectorizer's transform or
        fit_transform method; it may only have passed through transformers
        that preserve the number of features and their order.

        In the case of one-hot/one-of-K coding, the constructed feature
        names and values are returned rather than the original ones.

            Sample matrix.
        dict_type : type, default=dict
            Constructor for feature mappings. Must conform to the
            collections.Mapping API.

        Returns
        -------
        D : list of dict_type objects of shape (n_samples,)
            Feature mappings for the samples in X.
        """
        check_is_fitted(self, "feature_names_")

        # COO matrix is not subscriptable
        X = check_array(X, accept_sparse=["csr", "csc"])

        names = self.feature_names_
        dicts = [dict_type() for _ in range(n_samples)]

        if sp.issparse(X):
            for i, j in zip(*X.nonzero()):
                dicts[i][names[j]] = X[i, j]
        else:
            for i, d in enumerate(dicts):
                for j, v in enumerate(X[i, :]):
                    if v != 0:
                        d[names[j]] = X[i, j]

        return dicts

        """Transform feature->value dicts to array or sparse matrix.

        Named features not encountered during fit or fit_transform will be
        silently ignored.

        X : Mapping or iterable over Mappings of shape (n_samples,)

        Returns
        -------
        """
        check_is_fitted(self, ["feature_names_", "vocabulary_"])
        return self._transform(X, fitting=False)

        Returns
        -------
        """
        if any(not isinstance(name, str) for name in self.feature_names_):
            feature_names = [str(name) for name in self.feature_names_]
        else:

    def restrict(self, support, indices=False):
        """Restrict the features to those in support using feature selection.

        This function modifies the estimator in-place.

        support : array-like
            Boolean mask or list of indices (as returned by the get_support
            member of feature selectors).
        indices : bool, default=False
            Whether support is a list of indices.

        Returns
        -------

        Examples
        --------
        >>> from sklearn.feature_extraction import DictVectorizer
        >>> from sklearn.feature_selection import SelectKBest, chi2
        >>> v = DictVectorizer()
        >>> D = [{'foo': 1, 'bar': 2}, {'foo': 3, 'baz': 1}]
        >>> X = v.fit_transform(D)
        >>> support = SelectKBest(chi2, k=2).fit(X, [0, 1])
        >>> v.get_feature_names_out()
        array(['bar', 'baz', 'foo'], ...)
        >>> v.restrict(support.get_support())
        DictVectorizer()
        array(['bar', 'foo'], ...)
        """

        if not indices:
            support = np.where(support)[0]

        new_vocab = {}
        for i in support:
            new_vocab[names[i]] = len(new_vocab)

        self.vocabulary_ = new_vocab
        self.feature_names_ = [
            f for f, i in sorted(new_vocab.items(), key=itemgetter(1))
        ]

        tags.input_tags.dict = True

        self,
        f,
        v,
        vocab,
        *,
    ):
        for vv in v:
                vv = 1
            else:
                )

        Returns
        -------
        """
        vocab = {}

        for x in X:
                    )

        )

            vocab = {}
        else:

        values = []

        # same time
        for x in X:
                    v = 1
                        f,
                        v,
                        vocab,
                    )
                else:
                    )

        )

        else:

        Returns
        -------
        """

        Returns
        -------
        """

        else:
                    if v != 0:

        Returns
        -------
        """

        Returns
        -------
        """
        else:

        Returns
        -------

        Examples
        --------
        """

        ]

def _iteritems(d):
    """Like d.iteritems, but accepts any collections.Mapping."""
    return d.iteritems() if hasattr(d, "iteritems") else d.items()

class FeatureHasher(TransformerMixin, BaseEstimator):
    """Implements feature hashing, aka the hashing trick.

    This class turns sequences of symbolic feature names (strings) into
    scipy.sparse matrices, using a hash function to compute the matrix column
    corresponding to a name. The hash function employed is the signed 32-bit
    version of Murmurhash3.

    Feature names of type byte string are used as-is. Unicode strings are
    converted to UTF-8 first, but no Unicode normalization is done.
    Feature values must be (finite) numbers.

    This class is a low-memory alternative to DictVectorizer and
    CountVectorizer, intended for large-scale (online) learning and situations
    where memory is tight, e.g. when running prediction code on embedded
    devices.

    Read more in the :ref:`User Guide <feature_hashing>`.

    n_features : int, default=2**20
        The number of features (columns) in the output matrices. Small numbers
        of features are likely to cause hash collisions, but large numbers
        will cause larger coefficient dimensions in linear learners.
    input_type : str, default='dict'
        Choose a string from {'dict', 'pair', 'string'}.
        Either "dict" (the default) to accept dictionaries over
        (feature_name, value); "pair" to accept pairs of (feature_name, value);
        or "string" to accept single strings.
        feature_name should be a string, while value should be a number.
        In the case of "string", a value of 1 is implied.
        The feature_name is hashed to find the appropriate column for the
        feature. The value's sign might be flipped in the output (but see
        non_negative, below).
    dtype : numpy dtype, default=np.float64
        The type of feature values. Passed to scipy.sparse matrix constructors
        as the dtype argument. Do not set this to bool, np.boolean or any
        unsigned integer type.
    alternate_sign : bool, default=True
        When True, an alternating sign is added to the features as to
        approximately conserve the inner product in the hashed space even for
        small n_features. This approach is similar to sparse random projection.

            ``alternate_sign`` replaces the now deprecated ``non_negative``
            parameter.

    See Also
    --------
    DictVectorizer : Vectorizes string-valued features using a hash table.
    sklearn.preprocessing.OneHotEncoder : Handles nominal/categorical features.

    Notes
    -----
    This estimator is :term:`stateless` and does not need to be fitted.
    However, we recommend to call :meth:`fit_transform` instead of
    :meth:`transform`, as parameter validation is only performed in
    :meth:`fit`.

    Examples
    --------
    >>> from sklearn.feature_extraction import FeatureHasher
    >>> h = FeatureHasher(n_features=10)
    >>> D = [{'dog': 1, 'cat':2, 'elephant':4},{'dog': 2, 'run': 5}]
    >>> f = h.transform(D)
    >>> f.toarray()
    array([[ 0.,  0., -4., -1.,  0.,  0.,  0.,  0.,  0.,  2.],
           [ 0.,  0.,  0., -2., -5.,  0.,  0.,  0.,  0.,  0.]])

    With `input_type="string"`, the input must be an iterable over iterables of
    strings:

    >>> h = FeatureHasher(n_features=8, input_type="string")
    >>> raw_X = [["dog", "cat", "snake"], ["snake", "dog"], ["cat", "bird"]]
    >>> f = h.transform(raw_X)
    array([[ 0.,  0.,  0., -1.,  0., -1.,  0.,  1.],
           [ 0.,  0.,  0., -1.,  0., -1.,  0.,  0.],
           [ 0., -1.,  0.,  0.,  0.,  0.,  0.,  1.]])
    """

    # raw_X should have been called X
    __metadata_request__transform = {"raw_X": metadata_routing.UNUSED}

        "n_features": [Interval(Integral, 1, np.iinfo(np.int32).max, closed="both")],
        "input_type": [StrOptions({"dict", "pair", "string"})],
        "dtype": "no_validation",  # delegate to numpy
        "alternate_sign": ["boolean"],
    }

        self,
        n_features=(2**20),
        *,
        input_type="dict",
        dtype=np.float64,
        alternate_sign=True,
    ):
        self.input_type = input_type
        self.n_features = n_features
        self.alternate_sign = alternate_sign

    def fit(self, X=None, y=None):

        X : Ignored

        y : Ignored

        Returns
        -------
            FeatureHasher class instance.
        """

    def transform(self, raw_X):
        """Transform a sequence of instances to a scipy.sparse matrix.

        raw_X : iterable over iterable over raw features, length = n_samples
            Samples. Each sample must be iterable an (e.g., a list or tuple)
            containing/generating feature names (and optionally values, see
            the input_type constructor argument) which will be hashed.
            raw_X need not support the len function, so it can be the result
            of a generator; n_samples is determined on the fly.

        Returns
        -------
        X : sparse matrix of shape (n_samples, n_features)
            Feature matrix, for use with estimators or further transformers.
        """
        raw_X = iter(raw_X)
        if self.input_type == "dict":
            raw_X = (_iteritems(d) for d in raw_X)
        elif self.input_type == "string":
            first_raw_X = next(raw_X)
            if isinstance(first_raw_X, str):
                    "Samples can not be a single string. The input must be an iterable"
                    " over iterables of strings."
                )
            raw_X_ = chain([first_raw_X], raw_X)
            raw_X = (((f, 1) for f in x) for x in raw_X_)

        indices, indptr, values = _hashing_transform(
            raw_X, self.n_features, self.dtype, self.alternate_sign, seed=0
        )
        n_samples = indptr.shape[0] - 1

        if n_samples == 0:
            raise ValueError("Cannot vectorize empty sequence.")

        X = sp.csr_matrix(
            (values, indices, indptr),
            dtype=self.dtype,
            shape=(n_samples, self.n_features),
        )
        X.sum_duplicates()  # also sorts the indices

        return X

        if self.input_type == "string":
            tags.input_tags.string = True
        elif self.input_type == "dict":
            tags.input_tags.dict = True

        self,
        *,
    ):

        X : Ignored

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """
                )

        )

        )

        return X

def _make_edges_3d(n_x, n_y, n_z=1):
    """Returns a list of edges for a 3D image.

    n_x : int
        The size of the grid in the x direction.
    n_y : int
        The size of the grid in the y direction.
    n_z : integer, default=1
        The size of the grid in the z direction, defaults to 1
    """
    vertices = np.arange(n_x * n_y * n_z).reshape((n_x, n_y, n_z))
    edges_deep = np.vstack((vertices[:, :, :-1].ravel(), vertices[:, :, 1:].ravel()))
    edges_right = np.vstack((vertices[:, :-1].ravel(), vertices[:, 1:].ravel()))
    edges_down = np.vstack((vertices[:-1].ravel(), vertices[1:].ravel()))
    edges = np.hstack((edges_deep, edges_right, edges_down))
    return edges

def _compute_gradient_3d(edges, img):
    _, n_y, n_z = img.shape
    gradient = np.abs(
        img[
            edges[0] // (n_y * n_z),
            (edges[0] % (n_y * n_z)) // n_z,
            (edges[0] % (n_y * n_z)) % n_z,
        ]
        - img[
            edges[1] // (n_y * n_z),
            (edges[1] % (n_y * n_z)) // n_z,
            (edges[1] % (n_y * n_z)) % n_z,
        ]
    )
    return gradient

def _mask_edges_weights(mask, edges, weights=None):
    """Apply a mask to edges (weighted or not)"""
    inds = np.arange(mask.size)
    inds = inds[mask.ravel()]
    ind_mask = np.logical_and(np.isin(edges[0], inds), np.isin(edges[1], inds))
    edges = edges[:, ind_mask]
    if weights is not None:
        weights = weights[ind_mask]
    if len(edges.ravel()):
        maxval = edges.max()
    else:
        maxval = 0
    order = np.searchsorted(np.flatnonzero(mask), np.arange(maxval + 1))
    edges = order[edges]
    if weights is None:
        return edges
    else:
        return edges, weights

def _to_graph(
    n_x, n_y, n_z, mask=None, img=None, return_as=sparse.coo_matrix, dtype=None
):
    """Auxiliary function for img_to_graph and grid_to_graph"""
    edges = _make_edges_3d(n_x, n_y, n_z)

    if dtype is None:  # To not overwrite input dtype
        if img is None:
            dtype = int
        else:
            dtype = img.dtype

    if img is not None:
        img = np.atleast_3d(img)
        weights = _compute_gradient_3d(edges, img)
        if mask is not None:
            edges, weights = _mask_edges_weights(mask, edges, weights)
            diag = img.squeeze()[mask]
        else:
            diag = img.ravel()
        n_voxels = diag.size
    else:
            mask = mask.astype(dtype=bool, copy=False)
            edges = _mask_edges_weights(mask, edges)
            n_voxels = np.sum(mask)
        else:
            n_voxels = n_x * n_y * n_z
        weights = np.ones(edges.shape[1], dtype=dtype)
        diag = np.ones(n_voxels, dtype=dtype)

    diag_idx = np.arange(n_voxels)
    i_idx = np.hstack((edges[0], edges[1]))
    j_idx = np.hstack((edges[1], edges[0]))
    graph = sparse.coo_matrix(
        (
            np.hstack((weights, weights, diag)),
            (np.hstack((i_idx, diag_idx)), np.hstack((j_idx, diag_idx))),
        ),
        (n_voxels, n_voxels),
        dtype=dtype,
    )
    if return_as is np.ndarray:
        return graph.toarray()
    return return_as(graph)

def img_to_graph(img, *, mask=None, return_as=sparse.coo_matrix, dtype=None):
    """Graph of the pixel-to-pixel gradient connections.

    Edges are weighted with the gradient values.

    Read more in the :ref:`User Guide <image_feature_extraction>`.

    img : array-like of shape (height, width) or (height, width, channel)
        2D or 3D image.
    mask : ndarray of shape (height, width) or \
            (height, width, channel), dtype=bool, default=None
        An optional mask of the image, to consider only part of the
        pixels.
    return_as : np.ndarray or a sparse matrix class, \
            default=sparse.coo_matrix
        The class to use to build the returned adjacency matrix.
    dtype : dtype, default=None
        The data of the returned sparse matrix. By default it is the
        dtype of img.

    Returns
    -------
    graph : ndarray or a sparse matrix class
        The computed adjacency matrix.

    Examples
    --------
    >>> from sklearn.feature_extraction.image import img_to_graph
    >>> img = np.array([[0, 0], [0, 1]])
    >>> img_to_graph(img, return_as=np.ndarray)
    array([[0, 0, 0, 0],
           [0, 0, 0, 1],
           [0, 1, 1, 1]])
    """
    img = np.atleast_3d(img)
    n_x, n_y, n_z = img.shape
    return _to_graph(n_x, n_y, n_z, mask, img, return_as, dtype)

def grid_to_graph(
    n_x, n_y, n_z=1, *, mask=None, return_as=sparse.coo_matrix, dtype=int
):
    """Graph of the pixel-to-pixel connections.

    Edges exist if 2 voxels are connected.

    n_x : int
        Dimension in x axis.
    n_y : int
        Dimension in y axis.
    n_z : int, default=1
        Dimension in z axis.
    mask : ndarray of shape (n_x, n_y, n_z), dtype=bool, default=None
        pixels.
    dtype : dtype, default=int
        The data of the returned sparse matrix. By default it is int.

    Returns
    -------
    graph : np.ndarray or a sparse matrix class

    Examples
    --------
    >>> from sklearn.feature_extraction.image import grid_to_graph
    >>> shape_img = (4, 4, 1)
    >>> mask = np.zeros(shape=shape_img, dtype=bool)
    >>> mask[[1, 2], [1, 2], :] = True
    >>> graph = grid_to_graph(*shape_img, mask=mask)
    >>> print(graph)
    <COOrdinate sparse matrix of dtype 'int64'
      with 2 stored elements and shape (2, 2)>
      Coords	Values
      (0, 0)    1
      (1, 1)    1
    """
    return _to_graph(n_x, n_y, n_z, mask=mask, return_as=return_as, dtype=dtype)

def _compute_n_patches(i_h, i_w, p_h, p_w, max_patches=None):
    """Compute the number of patches that will be extracted in an image.

    i_h : int
        The image height
    i_w : int
        The image with
    p_h : int
        The height of a patch
    p_w : int
        The width of a patch
    max_patches : int or float, default=None
        The maximum number of patches to extract. If `max_patches` is a float
        between 0 and 1, it is taken to be a proportion of the total number
        of patches. If `max_patches` is None, all possible patches are extracted.
    """
    n_h = i_h - p_h + 1
    n_w = i_w - p_w + 1
    all_patches = n_h * n_w

    if max_patches:
        if isinstance(max_patches, (Integral)) and max_patches < all_patches:
            return max_patches
        elif isinstance(max_patches, (Integral)) and max_patches >= all_patches:
            return all_patches
        elif isinstance(max_patches, (Real)) and 0 < max_patches < 1:
            return int(max_patches * all_patches)
        else:
            raise ValueError("Invalid value for max_patches: %r" % max_patches)
    else:
        return all_patches

def _extract_patches(arr, patch_shape=8, extraction_step=1):
    """Extracts patches of any n-dimensional array in place using strides.

    Given an n-dimensional array it will return a 2n-dimensional array with
    the first n dimensions indexing patch position and the last n indexing
    the patch content. This operation is immediate (O(1)). A reshape
    performed on the first n dimensions will cause numpy to copy data, leading
    to a list of extracted patches.

    arr : ndarray
        n-dimensional array of which patches are to be extracted

    patch_shape : int or tuple of length arr.ndim.default=8
        Indicates the shape of the patches to be extracted. If an
        integer is given, the shape will be a hypercube of
        sidelength given by its value.

    extraction_step : int or tuple of length arr.ndim, default=1
        Indicates step size at which extraction shall be performed.
        If integer is given, then the step is uniform in all dimensions.

    Returns
    -------
    patches : strided ndarray
        2n-dimensional array indexing patches on first n dimensions and
        containing patches on the last n dimensions. These dimensions
        are fake, but this way no data is copied. A simple reshape invokes
        a copying operation to obtain a list of patches:
        result.reshape([-1] + list(patch_shape))
    """

    arr_ndim = arr.ndim

    if isinstance(patch_shape, Number):
        patch_shape = tuple([patch_shape] * arr_ndim)
    if isinstance(extraction_step, Number):
        extraction_step = tuple([extraction_step] * arr_ndim)

    patch_strides = arr.strides

    slices = tuple(slice(None, None, st) for st in extraction_step)
    indexing_strides = arr[slices].strides

    patch_indices_shape = (
        (np.array(arr.shape) - np.array(patch_shape)) // np.array(extraction_step)
    ) + 1

    shape = tuple(list(patch_indices_shape) + list(patch_shape))
    strides = tuple(list(indexing_strides) + list(patch_strides))

    patches = as_strided(arr, shape=shape, strides=strides)
    return patches

def extract_patches_2d(image, patch_size, *, max_patches=None, random_state=None):
    """Reshape a 2D image into a collection of patches.

    The resulting patches are allocated in a dedicated array.

    image : ndarray of shape (image_height, image_width) or \
        (image_height, image_width, n_channels)
        The original image data. For color images, the last dimension specifies
        the channel: a RGB image would have `n_channels=3`.

    patch_size : tuple of int (patch_height, patch_width)
        The dimensions of one patch.

        of patches. If `max_patches` is None it corresponds to the total number
        of patches that can be extracted.

        Determines the random number generator used for random sampling when
        `max_patches` is not None. Use an int to make the randomness
        deterministic.

    Returns
    -------
    patches : array of shape (n_patches, patch_height, patch_width) or \
        (n_patches, patch_height, patch_width, n_channels)
        The collection of patches extracted from the image, where `n_patches`
        is either `max_patches` or the total number of patches that can be
        extracted.

    Examples
    --------
    >>> from sklearn.datasets import load_sample_image
    >>> from sklearn.feature_extraction import image
    >>> # Use the array data from the first image in this dataset:
    >>> one_image = load_sample_image("china.jpg")
    >>> print('Image shape: {}'.format(one_image.shape))
    Image shape: (427, 640, 3)
    >>> patches = image.extract_patches_2d(one_image, (2, 2))
    >>> print('Patches shape: {}'.format(patches.shape))
    Patches shape: (272214, 2, 2, 3)
    >>> # Here are just two of these patches:
    >>> print(patches[1])
    [[[174 201 231]
      [174 201 231]]
     [[173 200 230]
      [173 200 230]]]
    >>> print(patches[800])
    [[[187 214 243]
      [188 215 244]]
     [[187 214 243]
      [188 215 244]]]
    """
    i_h, i_w = image.shape[:2]
    p_h, p_w = patch_size

    if p_h > i_h:
            "Height of the patch should be less than the height of the image."
        )

    if p_w > i_w:
            "Width of the patch should be less than the width of the image."
        )

    image = check_array(image, allow_nd=True)
    image = image.reshape((i_h, i_w, -1))
    n_colors = image.shape[-1]

    extracted_patches = _extract_patches(
        image, patch_shape=(p_h, p_w, n_colors), extraction_step=1
    )

    n_patches = _compute_n_patches(i_h, i_w, p_h, p_w, max_patches)
        i_s = rng.randint(i_h - p_h + 1, size=n_patches)
        j_s = rng.randint(i_w - p_w + 1, size=n_patches)
        patches = extracted_patches[i_s, j_s, 0]
    else:
        patches = extracted_patches

    patches = patches.reshape(-1, p_h, p_w, n_colors)
    # remove the color dimension if useless
    if patches.shape[-1] == 1:
        return patches.reshape((n_patches, p_h, p_w))
    else:
        return patches

def reconstruct_from_patches_2d(patches, image_size):
    """Reconstruct the image from all of its patches.

    Patches are assumed to overlap and the image is constructed by filling in
    the patches from left to right, top to bottom, averaging the overlapping
    regions.

    patches : ndarray of shape (n_patches, patch_height, patch_width) or \
        The complete set of patches. If the patches contain colour information,
        channels are indexed along the last dimension: RGB patches would
        have `n_channels=3`.

    image_size : tuple of int (image_height, image_width) or \
        The size of the image that will be reconstructed.

    Returns
    -------
    image : ndarray of shape image_size
        The reconstructed image.

    Examples
    --------
    >>> image_patches = image.extract_patches_2d(image=one_image, patch_size=(10, 10))
    >>> print('Patches shape: {}'.format(image_patches.shape))
    Patches shape: (263758, 10, 10, 3)
    >>> image_reconstructed = image.reconstruct_from_patches_2d(
    ...     patches=image_patches,
    ...     image_size=one_image.shape
    ... )
    >>> print(f"Reconstructed shape: {image_reconstructed.shape}")
    Reconstructed shape: (427, 640, 3)
    """
    i_h, i_w = image_size[:2]
    p_h, p_w = patches.shape[1:3]
    img = np.zeros(image_size)
    # compute the dimensions of the patches array
    for p, (i, j) in zip(patches, product(range(n_h), range(n_w))):
        img[i : i + p_h, j : j + p_w] += p

    for i in range(i_h):
        for j in range(i_w):
            # divide by the amount of overlap
            # XXX: is this the most efficient way? memory-wise yes, cpu wise?
            img[i, j] /= float(min(i + 1, p_h, i_h - i) * min(j + 1, p_w, i_w - j))
    return img

class PatchExtractor(TransformerMixin, BaseEstimator):
    """Extracts patches from a collection of images.

    .. versionadded:: 0.9

    patch_size : tuple of int (patch_height, patch_width), default=None
        The dimensions of one patch. If set to None, the patch size will be
        automatically set to `(img_height // 10, img_width // 10)`, where
        `img_height` and `img_width` are the dimensions of the input images.

        The maximum number of patches per image to extract. If `max_patches` is
        a float in (0, 1), it is taken to mean a proportion of the total number
        of patches. If set to None, extract all possible patches.

        `max_patches is not None`. Use an int to make the randomness

    See Also
    --------
    reconstruct_from_patches_2d : Reconstruct image from all of its patches.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.datasets import load_sample_images
    >>> # Use the array data from the second image in this dataset:
    >>> X = load_sample_images().images[1]
    >>> X = X[None, ...]
    >>> print(f"Image shape: {X.shape}")
    Image shape: (1, 427, 640, 3)
    >>> pe = image.PatchExtractor(patch_size=(10, 10))
    >>> pe_trans = pe.transform(X)
    >>> print(f"Patches shape: {pe_trans.shape}")
    >>> X_reconstructed = image.reconstruct_from_patches_2d(pe_trans, X.shape[1:])
    >>> print(f"Reconstructed shape: {X_reconstructed.shape}")
    """

        "patch_size": [tuple, None],
        "max_patches": [
            None,
        ],
    }

    def __init__(self, *, patch_size=None, max_patches=None, random_state=None):
        self.patch_size = patch_size
        self.max_patches = max_patches

        """Only validate the parameters of the estimator.

        This method allows to: (i) validate the parameters of the estimator  and

        X : ndarray of shape (n_samples, image_height, image_width) or \
                (n_samples, image_height, image_width, n_channels)
            Array of images from which to extract patches. For color images,
            the last dimension specifies the channel: a RGB image would have
            `n_channels=3`.

        y : Ignored

        Returns
        -------
        """

        """Transform the image samples in `X` into a matrix of patch data.

        Returns
        -------
        patches : array of shape (n_patches, patch_height, patch_width) or \
                (n_patches, patch_height, patch_width, n_channels)
            The collection of patches extracted from the images, where
            `n_patches` is either `n_samples * max_patches` or the total
            number of patches that can be extracted.
        """
            self,
            X=X,
            ensure_min_samples=1,
            ensure_min_features=1,
        )
        n_imgs, img_height, img_width = X.shape[:3]
        if self.patch_size is None:
            patch_size = img_height // 10, img_width // 10
        else:
            if len(self.patch_size) != 2:
                    "patch_size must be a tuple of two integers. Got"
                    f" {self.patch_size} instead."
                )
            patch_size = self.patch_size

        X = np.reshape(X, (n_imgs, img_height, img_width, -1))
        n_channels = X.shape[-1]

        # compute the dimensions of the patches array
        patch_height, patch_width = patch_size
        n_patches = _compute_n_patches(
            img_height, img_width, patch_height, patch_width, self.max_patches
        )
        patches_shape = (n_imgs * n_patches,) + patch_size
        if n_channels > 1:
            patches_shape += (n_channels,)

        # extract the patches
        patches = np.empty(patches_shape)
        for ii, image in enumerate(X):
            patches[ii * n_patches : (ii + 1) * n_patches] = extract_patches_2d(
                image,
                patch_size,
                max_patches=self.max_patches,
            )

        tags.input_tags.three_d_array = True

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """
            self,
            X=X,
        )
        else:
                )

        )

                image,
            )

def _preprocess(doc, accent_function=None, lower=False):
    """Chain together an optional series of text preprocessing steps to
    apply to a document.

    doc: str
        The string to preprocess
    accent_function: callable, default=None
        Function for handling accented characters. Common strategies include
        normalizing and removing.
    lower: bool, default=False
        Whether to use str.lower to lowercase all of the text

    Returns
    -------
    doc: str
        preprocessed string
    """
    if lower:
        doc = doc.lower()
    if accent_function is not None:
        doc = accent_function(doc)
    return doc

def _analyze(
    doc,
    analyzer=None,
    tokenizer=None,
    ngrams=None,
    preprocessor=None,
    decoder=None,
    stop_words=None,
):
    """Chain together an optional series of text processing steps to go from
    a single document to ngrams, with or without tokenizing or preprocessing.

    If analyzer is used, only the decoder argument is used, as the analyzer is
    intended to replace the preprocessor, tokenizer, and ngrams steps.

    analyzer: callable, default=None
    tokenizer: callable, default=None
    ngrams: callable, default=None
    preprocessor: callable, default=None
    decoder: callable, default=None
    stop_words: list, default=None

    Returns
    -------
    ngrams: list
        A sequence of tokens, possibly with pairs, triples, etc.
    """

    if decoder is not None:
        doc = decoder(doc)
    if analyzer is not None:
        doc = analyzer(doc)
    else:
        if preprocessor is not None:
            doc = preprocessor(doc)
        if tokenizer is not None:
            doc = tokenizer(doc)
        if ngrams is not None:
            if stop_words is not None:
                doc = ngrams(doc, stop_words)
            else:
                doc = ngrams(doc)
    return doc

def strip_accents_unicode(s):
    """Transform accentuated unicode symbols into their simple counterpart.

    Warning: the python-level loop and join operations make this
    implementation 20 times slower than the strip_accents_ascii basic
    normalization.

    s : str
        The string to strip.

    Returns
    -------
    s : str
        The stripped string.

    See Also
    --------
    strip_accents_ascii : Remove accentuated char for any unicode symbol that
        has a direct ASCII equivalent.
    """
    try:
        # If `s` is ASCII-compatible, then it does not contain any accented
        # characters and we can avoid an expensive list comprehension
        s.encode("ASCII", errors="strict")
        return s
    except UnicodeEncodeError:
        normalized = unicodedata.normalize("NFKD", s)
        return "".join([c for c in normalized if not unicodedata.combining(c)])

def strip_accents_ascii(s):
    """Transform accentuated unicode symbols into ascii or nothing.

    Warning: this solution is only suited for languages that have a direct
    transliteration to ASCII symbols.

    s : str

    Returns
    -------
    s : str

    See Also
    --------
    strip_accents_unicode : Remove accentuated char for any unicode symbol.
    """
    nkfd_form = unicodedata.normalize("NFKD", s)
    return nkfd_form.encode("ASCII", "ignore").decode("ASCII")

def strip_tags(s):
    """Basic regexp based HTML / XML tag stripper function.

    For serious HTML/XML preprocessing you should rather use an external
    library such as lxml or BeautifulSoup.

    s : str

    Returns
    -------
    s : str
    """
    return re.compile(r"<([^>]+)>", flags=re.UNICODE).sub(" ", s)

def _check_stop_list(stop):
    if stop == "english":
        return ENGLISH_STOP_WORDS
    elif isinstance(stop, str):
        raise ValueError("not a built-in stop list: %s" % stop)
    elif stop is None:
        return None
    else:  # assume it's a collection
        return frozenset(stop)

class _VectorizerMixin:
    """Provides common code for text vectorizers (tokenization logic)."""

    _white_spaces = re.compile(r"\s\s+")

    def decode(self, doc):
        """Decode the input into a string of unicode symbols.

        The decoding strategy depends on the vectorizer parameters.

        doc : bytes or str
            The string to decode.

        Returns
        -------
        doc: str
            A string of unicode symbols.
        """
        if self.input == "filename":
            with open(doc, "rb") as fh:
                doc = fh.read()

        elif self.input == "file":
            doc = doc.read()

        if isinstance(doc, bytes):
            doc = doc.decode(self.encoding, self.decode_error)

        if doc is np.nan:
                "np.nan is an invalid document, expected byte or unicode string."
            )

        return doc

    def _word_ngrams(self, tokens, stop_words=None):
        """Turn tokens into a sequence of n-grams after stop words filtering"""
        # handle stop words
        if stop_words is not None:
            tokens = [w for w in tokens if w not in stop_words]

        # handle token n-grams
        min_n, max_n = self.ngram_range
        if max_n != 1:
            original_tokens = tokens
            if min_n == 1:
                # no need to do any slicing for unigrams
                # just iterate through the original tokens
                tokens = list(original_tokens)
                min_n += 1
            else:
                tokens = []

            n_original_tokens = len(original_tokens)

            # bind method outside of loop to reduce overhead
            tokens_append = tokens.append
            space_join = " ".join

            for n in range(min_n, min(max_n + 1, n_original_tokens + 1)):
                for i in range(n_original_tokens - n + 1):
                    tokens_append(space_join(original_tokens[i : i + n]))

        return tokens

    def _char_ngrams(self, text_document):
        """Tokenize text_document into a sequence of character n-grams"""
        # normalize white spaces
        text_document = self._white_spaces.sub(" ", text_document)

        text_len = len(text_document)
        if min_n == 1:
            # no need to do any slicing for unigrams
            # iterate through the string
            ngrams = list(text_document)
            min_n += 1
        else:
            ngrams = []

        # bind method outside of loop to reduce overhead
        ngrams_append = ngrams.append

        for n in range(min_n, min(max_n + 1, text_len + 1)):
            for i in range(text_len - n + 1):
                ngrams_append(text_document[i : i + n])
        return ngrams

    def _char_wb_ngrams(self, text_document):
        """Whitespace sensitive char-n-gram tokenization.

        Tokenize text_document into a sequence of character n-grams
        operating only inside word boundaries. n-grams at the edges
        of words are padded with space."""

        ngrams = []

        for w in text_document.split():
            w = " " + w + " "
            w_len = len(w)
            for n in range(min_n, max_n + 1):
                offset = 0
                ngrams_append(w[offset : offset + n])
                while offset + n < w_len:
                    offset += 1
                    ngrams_append(w[offset : offset + n])
                if offset == 0:  # count a short word (w_len < n) only once
                    break

    def build_preprocessor(self):
        """Return a function to preprocess the text before tokenization.

        Returns
        -------
        preprocessor: callable
              A function to preprocess the text before tokenization.
        """
        if self.preprocessor is not None:
            return self.preprocessor

        # accent stripping
        if not self.strip_accents:
            strip_accents = None
        elif callable(self.strip_accents):
            strip_accents = self.strip_accents
        elif self.strip_accents == "ascii":
            strip_accents = strip_accents_ascii
        elif self.strip_accents == "unicode":
            strip_accents = strip_accents_unicode
        else:
                'Invalid value for "strip_accents": %s' % self.strip_accents
            )

        return partial(_preprocess, accent_function=strip_accents, lower=self.lowercase)

    def build_tokenizer(self):
        """Return a function that splits a string into a sequence of tokens.

        Returns
        -------
        tokenizer: callable
              A function to split a string into a sequence of tokens.
        """
        if self.tokenizer is not None:
            return self.tokenizer
        token_pattern = re.compile(self.token_pattern)

        if token_pattern.groups > 1:
                "More than 1 capturing group in token pattern. Only a single "
                "group should be captured."
            )

        return token_pattern.findall

    def get_stop_words(self):
        """Build or fetch the effective stop words list.

        Returns
        -------
        stop_words: list or None
                A list of stop words.
        """
        return _check_stop_list(self.stop_words)

    def _check_stop_words_consistency(self, stop_words, preprocess, tokenize):
        """Check if stop words are consistent

        Returns
        -------
        is_consistent : True if stop words are consistent with the preprocessor
                        and tokenizer, False if they are not, None if the check
                        was previously performed, "error" if it could not be
                        performed (e.g. because of the use of a custom
                        preprocessor / tokenizer)
        """
        if id(self.stop_words) == getattr(self, "_stop_words_id", None):
            # Stop words are were previously validated

        # NB: stop_words is validated, unlike self.stop_words
        try:
            inconsistent = set()
            for w in stop_words or ():
                tokens = list(tokenize(preprocess(w)))
                for token in tokens:
                    if token not in stop_words:
                        inconsistent.add(token)
            self._stop_words_id = id(self.stop_words)

            if inconsistent:
                    "Your stop_words may be inconsistent with "
                    "your preprocessing. Tokenizing the stop "
                    "words generated tokens %r not in "
                    "stop_words." % sorted(inconsistent)
                )
            return not inconsistent
            # Failed to check stop words consistency (e.g. because a custom
            # preprocessor or tokenizer was used)
            return "error"

    def build_analyzer(self):
        """Return a callable to process input data.

        The callable handles preprocessing, tokenization, and n-grams generation.

        Returns
        -------
        analyzer: callable
            A function to handle preprocessing, tokenization
            and n-grams generation.
        """

        if callable(self.analyzer):
            return partial(_analyze, analyzer=self.analyzer, decoder=self.decode)

        preprocess = self.build_preprocessor()

        if self.analyzer == "char":
            return partial(
                _analyze,
                ngrams=self._char_ngrams,
                preprocessor=preprocess,
                decoder=self.decode,
            )

        elif self.analyzer == "char_wb":
                _analyze,
                ngrams=self._char_wb_ngrams,
            )

        elif self.analyzer == "word":
            stop_words = self.get_stop_words()
            tokenize = self.build_tokenizer()
            self._check_stop_words_consistency(stop_words, preprocess, tokenize)
                _analyze,
                ngrams=self._word_ngrams,
                tokenizer=tokenize,
                stop_words=stop_words,
            )

        else:
                "%s is not a valid tokenization scheme/analyzer" % self.analyzer
            )

    def _validate_vocabulary(self):
        vocabulary = self.vocabulary
        if vocabulary is not None:
            if isinstance(vocabulary, set):
                vocabulary = sorted(vocabulary)
            if not isinstance(vocabulary, Mapping):
                vocab = {}
                for i, t in enumerate(vocabulary):
                    if vocab.setdefault(t, i) != i:
                        msg = "Duplicate term in vocabulary: %r" % t
                        raise ValueError(msg)
                vocabulary = vocab
            else:
                indices = set(vocabulary.values())
                if len(indices) != len(vocabulary):
                    raise ValueError("Vocabulary contains repeated indices.")
                for i in range(len(vocabulary)):
                    if i not in indices:
                        msg = "Vocabulary of size %d doesn't contain index %d." % (
                            len(vocabulary),
                            i,
                        )
            if not vocabulary:
                raise ValueError("empty vocabulary passed to fit")
            self.fixed_vocabulary_ = True
            self.vocabulary_ = dict(vocabulary)
        else:
            self.fixed_vocabulary_ = False

    def _check_vocabulary(self):
        """Check if vocabulary is empty or missing (not fitted)"""
        if not hasattr(self, "vocabulary_"):
            self._validate_vocabulary()
            if not self.fixed_vocabulary_:
                raise NotFittedError("Vocabulary not fitted or provided")

        if len(self.vocabulary_) == 0:
            raise ValueError("Vocabulary is empty")

    def _validate_ngram_range(self):
        """Check validity of ngram_range parameter"""
        min_n, max_m = self.ngram_range
        if min_n > max_m:
                "Invalid value for ngram_range=%s "
                "lower boundary larger than the upper boundary." % str(self.ngram_range)
            )

    def _warn_for_unused_params(self):
        if self.tokenizer is not None and self.token_pattern is not None:
                "The parameter 'token_pattern' will not be used"
                " since 'tokenizer' is not None'"
            )

        if self.preprocessor is not None and callable(self.analyzer):
                "The parameter 'preprocessor' will not be used"
                " since 'analyzer' is callable'"
            )

        if (
            self.ngram_range != (1, 1)
            and self.ngram_range is not None
            and callable(self.analyzer)
        ):
                "The parameter 'ngram_range' will not be used"
            )
        if self.analyzer != "word" or callable(self.analyzer):
            if self.stop_words is not None:
                    "The parameter 'stop_words' will not be used"
                    " since 'analyzer' != 'word'"
                )
            if (
                self.token_pattern is not None
                and self.token_pattern != r"(?u)\b\w\w+\b"
            ):
                    "The parameter 'token_pattern' will not be used"
                )
            if self.tokenizer is not None:
                    "The parameter 'tokenizer' will not be used"
                )

class HashingVectorizer(
    TransformerMixin, _VectorizerMixin, BaseEstimator, auto_wrap_output_keys=None
):
    r"""Convert a collection of text documents to a matrix of token occurrences.

    It turns a collection of text documents into a scipy.sparse matrix holding
    token occurrence counts (or binary occurrence information), possibly
    normalized as token frequencies if norm='l1' or projected on the euclidean
    unit sphere if norm='l2'.

    This text vectorizer implementation uses the hashing trick to find the
    token string name to feature integer index mapping.

    This strategy has several advantages:

    - it is very low memory scalable to large datasets as there is no need to
      store a vocabulary dictionary in memory.

    - it is fast to pickle and un-pickle as it holds no state besides the
      constructor parameters.

    - it can be used in a streaming (partial fit) or parallel pipeline as there
      is no state computed during fit.

    There are also a couple of cons (vs using a CountVectorizer with an
    in-memory vocabulary):

    - there is no way to compute the inverse transform (from feature indices to
      string feature names) which can be a problem when trying to introspect
      which features are most important to a model.

    - there can be collisions: distinct tokens can be mapped to the same
      feature index. However in practice this is rarely an issue if n_features
      is large enough (e.g. 2 ** 18 for text classification problems).

    - no IDF weighting as this would render the transformer stateful.

    The hash function employed is the signed 32-bit version of Murmurhash3.

    For an example of document clustering and comparison with
    :class:`~sklearn.feature_extraction.text.TfidfVectorizer`, see
    :ref:`sphx_glr_auto_examples_text_plot_document_clustering.py`.

    Read more in the :ref:`User Guide <text_feature_extraction>`.

    input : {'filename', 'file', 'content'}, default='content'
        - If `'filename'`, the sequence passed as an argument to fit is
          expected to be a list of filenames that need reading to fetch
          the raw content to analyze.

        - If `'file'`, the sequence items must have a 'read' method (file-like
          object) that is called to fetch the bytes in memory.

        - If `'content'`, the input is expected to be a sequence of items that
          can be of type string or byte.

    encoding : str, default='utf-8'
        If bytes or files are given to analyze, this encoding is used to
        decode.

    decode_error : {'strict', 'ignore', 'replace'}, default='strict'
        Instruction on what to do if a byte sequence is given to analyze that
        contains characters not of the given `encoding`. By default, it is
        'strict', meaning that a UnicodeDecodeError will be raised. Other
        values are 'ignore' and 'replace'.

    strip_accents : {'ascii', 'unicode'} or callable, default=None
        Remove accents and perform other character normalization
        during the preprocessing step.
        'ascii' is a fast method that only works on characters that have
        a direct ASCII mapping.
        'unicode' is a slightly slower method that works on any character.
        None (default) means no character normalization is performed.

        Both 'ascii' and 'unicode' use NFKD normalization from
        :func:`unicodedata.normalize`.

    lowercase : bool, default=True
        Convert all characters to lowercase before tokenizing.

    preprocessor : callable, default=None
        Override the preprocessing (string transformation) stage while
        preserving the tokenizing and n-grams generation steps.
        Only applies if ``analyzer`` is not callable.

    tokenizer : callable, default=None
        Override the string tokenization step while preserving the
        preprocessing and n-grams generation steps.
        Only applies if ``analyzer == 'word'``.

    stop_words : {'english'}, list, default=None
        If 'english', a built-in stop word list for English is used.
        There are several known issues with 'english' and you should
        consider an alternative (see :ref:`stop_words`).

        If a list, that list is assumed to contain stop words, all of which
        will be removed from the resulting tokens.

    token_pattern : str or None, default=r"(?u)\\b\\w\\w+\\b"
        Regular expression denoting what constitutes a "token", only used
        if ``analyzer == 'word'``. The default regexp selects tokens of 2
        or more alphanumeric characters (punctuation is completely ignored
        and always treated as a token separator).

        If there is a capturing group in token_pattern then the
        captured group content, not the entire match, becomes the token.
        At most one capturing group is permitted.

    ngram_range : tuple (min_n, max_n), default=(1, 1)
        The lower and upper boundary of the range of n-values for different
        n-grams to be extracted. All values of n such that min_n <= n <= max_n
        will be used. For example an ``ngram_range`` of ``(1, 1)`` means only
        unigrams, ``(1, 2)`` means unigrams and bigrams, and ``(2, 2)`` means
        only bigrams.

    analyzer : {'word', 'char', 'char_wb'} or callable, default='word'
        Whether the feature should be made of word or character n-grams.
        Option 'char_wb' creates character n-grams only from text inside
        word boundaries; n-grams at the edges of words are padded with space.

        If a callable is passed it is used to extract the sequence of features
        out of the raw, unprocessed input.

            Since v0.21, if ``input`` is ``'filename'`` or ``'file'``, the data
            is first read from the file and then passed to the given callable
            analyzer.

    n_features : int, default=(2 ** 20)

    binary : bool, default=False
        If True, all non zero counts are set to 1. This is useful for discrete
        probabilistic models that model binary events rather than integer
        counts.

    norm : {'l1', 'l2'}, default='l2'
        Norm used to normalize term vectors. None for no normalization.

    dtype : type, default=np.float64
        Type of the matrix returned by fit_transform() or transform().

    See Also
    --------
    CountVectorizer : Convert a collection of text documents to a matrix of
        token counts.
    TfidfVectorizer : Convert a collection of raw documents to a matrix of
        TF-IDF features.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.feature_extraction.text import HashingVectorizer
    >>> corpus = [
    ...     'This is the first document.',
    ...     'This document is the second document.',
    ...     'And this is the third one.',
    ...     'Is this the first document?',
    ... ]
    >>> vectorizer = HashingVectorizer(n_features=2**4)
    >>> X = vectorizer.fit_transform(corpus)
    >>> print(X.shape)
    (4, 16)
    """

        "input": [StrOptions({"filename", "file", "content"})],
        "encoding": [str],
        "decode_error": [StrOptions({"strict", "ignore", "replace"})],
        "strip_accents": [StrOptions({"ascii", "unicode"}), None, callable],
        "lowercase": ["boolean"],
        "preprocessor": [callable, None],
        "tokenizer": [callable, None],
        "stop_words": [StrOptions({"english"}), list, None],
        "token_pattern": [str, None],
        "ngram_range": [tuple],
        "analyzer": [StrOptions({"word", "char", "char_wb"}), callable],
        "n_features": [Interval(Integral, 1, np.iinfo(np.int32).max, closed="left")],
        "binary": ["boolean"],
        "norm": [StrOptions({"l1", "l2"}), None],
    }

        self,
        *,
        input="content",
        encoding="utf-8",
        decode_error="strict",
        strip_accents=None,
        lowercase=True,
        preprocessor=None,
        tokenizer=None,
        stop_words=None,
        token_pattern=r"(?u)\b\w\w+\b",
        ngram_range=(1, 1),
        analyzer="word",
        binary=False,
        norm="l2",
    ):
        self.input = input
        self.encoding = encoding
        self.decode_error = decode_error
        self.strip_accents = strip_accents
        self.preprocessor = preprocessor
        self.tokenizer = tokenizer
        self.analyzer = analyzer
        self.lowercase = lowercase
        self.token_pattern = token_pattern
        self.stop_words = stop_words
        self.ngram_range = ngram_range
        self.binary = binary

    def partial_fit(self, X, y=None):

        X : ndarray of shape [n_samples, n_features]

        y : Ignored

        Returns
        -------
            HashingVectorizer instance.
        """

        y : Ignored

        Returns
        -------
        """
        # triggers a parameter validation
        if isinstance(X, str):
                "Iterable over raw text documents expected, string object received."
            )

        self._warn_for_unused_params()
        self._validate_ngram_range()

        self._get_hasher().fit(X, y=y)

        """Transform a sequence of documents to a document-term matrix.

        X : iterable over raw text documents, length = n_samples
            Samples. Each sample must be a text document (either bytes or
            unicode strings, file name or file object depending on the
            constructor argument) which will be tokenized and hashed.

        Returns
        -------
            Document-term matrix.
        """
            )

        analyzer = self.build_analyzer()
        X = self._get_hasher().transform(analyzer(doc) for doc in X)
        if self.binary:
            X.data.fill(1)
        if self.norm is not None:
            X = normalize(X, norm=self.norm, copy=False)
        return X

        y : any
            Ignored. This parameter exists only for compatibility with
            sklearn.pipeline.Pipeline.

        Returns
        -------
        """
        return self.fit(X, y).transform(X)

    def _get_hasher(self):
        return FeatureHasher(
            n_features=self.n_features,
            input_type="string",
            alternate_sign=self.alternate_sign,
        )

        tags.input_tags.string = True

def _document_frequency(X):
    """Count the number of non-zero values for each feature in sparse X."""
    if sp.issparse(X) and X.format == "csr":
        return np.bincount(X.indices, minlength=X.shape[1])
    else:
        return np.diff(X.indptr)

class CountVectorizer(_VectorizerMixin, BaseEstimator):
    r"""Convert a collection of text documents to a matrix of token counts.

    This implementation produces a sparse representation of the counts using
    scipy.sparse.csr_matrix.

    If you do not provide an a-priori dictionary and you do not use an analyzer
    that does some kind of feature selection then the number of features will
    be equal to the vocabulary size found by analyzing the data.

        decode.

        'unicode' is a slightly slower method that works on any characters.

        Override the preprocessing (strip_accents and lowercase) stage while

        If None, no stop words will be used. In this case, setting `max_df`
        to a higher value, such as in the range (0.7, 1.0), can automatically detect
        and filter stop words based on intra corpus document frequency of terms.

        if ``analyzer == 'word'``. The default regexp select tokens of 2

        word n-grams or char n-grams to be extracted. All values of n such
        such that min_n <= n <= max_n will be used. For example an
        ``ngram_range`` of ``(1, 1)`` means only unigrams, ``(1, 2)`` means
        unigrams and bigrams, and ``(2, 2)`` means only bigrams.

        Whether the feature should be made of word n-gram or character
        n-grams.

        Since v0.21, if ``input`` is ``filename`` or ``file``, the data is
        first read from the file and then passed to the given callable
        analyzer.

    max_df : float in range [0.0, 1.0] or int, default=1.0
        When building the vocabulary ignore terms that have a document
        frequency strictly higher than the given threshold (corpus-specific
        stop words).
        If float, the parameter represents a proportion of documents, integer
        absolute counts.
        This parameter is ignored if vocabulary is not None.

    min_df : float in range [0.0, 1.0] or int, default=1
        frequency strictly lower than the given threshold. This value is also
        called cut-off in the literature.

    max_features : int, default=None
        If not None, build a vocabulary that only consider the top
        `max_features` ordered by term frequency across the corpus.
        Otherwise, all features are used.

    vocabulary : Mapping or iterable, default=None
        Either a Mapping (e.g., a dict) where keys are terms and values are
        indices in the feature matrix, or an iterable over terms. If not
        given, a vocabulary is determined from the input documents. Indices
        in the mapping should not be repeated and should not have any gap
        between 0 and the largest index.

        counts.

    dtype : dtype, default=np.int64

        A mapping of terms to feature indices.

    fixed_vocabulary_ : bool
        True if a fixed vocabulary of term to indices mapping
        is provided by the user.

    See Also
    --------
    HashingVectorizer : Convert a collection of text documents to a
        matrix of token counts.

    TfidfVectorizer : Convert a collection of raw documents to a matrix
        of TF-IDF features.

    Examples
    --------
    >>> from sklearn.feature_extraction.text import CountVectorizer
    ... ]
    >>> vectorizer = CountVectorizer()
    >>> vectorizer.get_feature_names_out()
    array(['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third',
           'this'], ...)
    >>> print(X.toarray())
    [[0 1 1 1 0 0 1 0 1]
     [0 2 0 1 0 1 1 0 1]
     [1 0 0 1 1 0 1 1 1]
     [0 1 1 1 0 0 1 0 1]]
    >>> vectorizer2 = CountVectorizer(analyzer='word', ngram_range=(2, 2))
    >>> X2 = vectorizer2.fit_transform(corpus)
    >>> vectorizer2.get_feature_names_out()
    array(['and this', 'document is', 'first document', 'is the', 'is this',
           'second document', 'the first', 'the second', 'the third', 'third one',
           'this document', 'this is', 'this the'], ...)
     >>> print(X2.toarray())
     [[0 0 1 1 0 0 1 0 0 0 0 1 0]
     [0 1 0 1 0 1 0 1 0 0 1 0 0]
     [1 0 0 1 0 0 0 0 1 1 0 1 0]
     [0 0 1 0 1 0 1 0 0 0 0 0 1]]
    """

    # raw_documents should not be in the routing mechanism. It should have been
    # called X in the first place.
    __metadata_request__fit = {"raw_documents": metadata_routing.UNUSED}
    __metadata_request__transform = {"raw_documents": metadata_routing.UNUSED}

        "max_df": [
            Interval(RealNotInt, 0, 1, closed="both"),
        ],
        "min_df": [
        ],
        "max_features": [Interval(Integral, 1, None, closed="left"), None],
        "vocabulary": [Mapping, HasMethods("__iter__"), None],
    }

        self,
        *,
        max_df=1.0,
        min_df=1,
        vocabulary=None,
        dtype=np.int64,
    ):
        self.max_df = max_df
        self.min_df = min_df
        self.vocabulary = vocabulary

    def _sort_features(self, X, vocabulary):
        """Sort features by name

        Returns a reordered matrix and modifies the vocabulary in place
        """
        sorted_features = sorted(vocabulary.items())
        map_index = np.empty(len(sorted_features), dtype=X.indices.dtype)
        for new_val, (term, old_val) in enumerate(sorted_features):
            vocabulary[term] = new_val
            map_index[old_val] = new_val

        X.indices = map_index.take(X.indices, mode="clip")
        return X

    def _limit_features(self, X, vocabulary, high=None, low=None, limit=None):
        """Remove too rare or too common features.

        Prune features that are non zero in more samples than high or less
        documents than low, modifying the vocabulary, and restricting it to
        at most the limit most frequent.

        This does not prune samples with zero features.
        """
        if high is None and low is None and limit is None:
            return X, set()

        # Calculate a mask based on document frequencies
        dfs = _document_frequency(X)
        mask = np.ones(len(dfs), dtype=bool)
        if high is not None:
            mask &= dfs <= high
        if low is not None:
            mask &= dfs >= low
        if limit is not None and mask.sum() > limit:
            tfs = np.asarray(X.sum(axis=0)).ravel()
            mask_inds = (-tfs[mask]).argsort()[:limit]
            new_mask = np.zeros(len(dfs), dtype=bool)
            new_mask[np.where(mask)[0][mask_inds]] = True
            mask = new_mask

        new_indices = np.cumsum(mask) - 1  # maps old indices to new
        for term, old_index in list(vocabulary.items()):
            if mask[old_index]:
                vocabulary[term] = new_indices[old_index]
            else:
                del vocabulary[term]
        kept_indices = np.where(mask)[0]
        if len(kept_indices) == 0:
                "After pruning, no terms remain. Try a lower min_df or a higher max_df."
            )
        return X[:, kept_indices]

    def _count_vocab(self, raw_documents, fixed_vocab):
        """Create sparse feature matrix, and vocabulary where fixed_vocab=False"""
        if fixed_vocab:
            vocabulary = self.vocabulary_
        else:
            # Add a new value when a new vocabulary item is seen
            vocabulary = defaultdict()
            vocabulary.default_factory = vocabulary.__len__

        analyze = self.build_analyzer()
        j_indices = []
        indptr = []

        values = _make_int_array()
        indptr.append(0)
        for doc in raw_documents:
            feature_counter = {}
            for feature in analyze(doc):
                try:
                    feature_idx = vocabulary[feature]
                    if feature_idx not in feature_counter:
                        feature_counter[feature_idx] = 1
                    else:
                        feature_counter[feature_idx] += 1
                except KeyError:
                    # Ignore out-of-vocabulary items for fixed_vocab=True
                    continue

            j_indices.extend(feature_counter.keys())
            values.extend(feature_counter.values())
            indptr.append(len(j_indices))

        if not fixed_vocab:
            # disable defaultdict behaviour
            vocabulary = dict(vocabulary)
                    "empty vocabulary; perhaps the documents only contain stop words"
                )

        if indptr[-1] > np.iinfo(np.int32).max:  # = 2**31 - 1
            if _IS_32BIT:
                    (
                        "sparse CSR array has {} non-zero "
                        "elements and requires 64 bit indexing, "
                        "which is unsupported with 32 bit Python."
                    ).format(indptr[-1])
                )
            indices_dtype = np.int64

        else:
            indices_dtype = np.int32
        j_indices = np.asarray(j_indices, dtype=indices_dtype)
        indptr = np.asarray(indptr, dtype=indices_dtype)
        values = np.frombuffer(values, dtype=np.intc)

            (values, j_indices, indptr),
            shape=(len(indptr) - 1, len(vocabulary)),
        )
        X.sort_indices()
        return vocabulary, X

    def fit(self, raw_documents, y=None):
        """Learn a vocabulary dictionary of all tokens in the raw documents.

        raw_documents : iterable
            An iterable which generates either str, unicode or file objects.

        y : None
            This parameter is ignored.

        Returns
        -------
            Fitted vectorizer.
        """
        self.fit_transform(raw_documents)

    def fit_transform(self, raw_documents, y=None):
        """Learn the vocabulary dictionary and return document-term matrix.

        This is equivalent to fit followed by transform, but more efficiently
        implemented.

        y : None

        Returns
        -------
        X : array of shape (n_samples, n_features)
        """
        # We intentionally don't call the transform method to make
        # fit_transform overridable without unwanted side effects in
        # TfidfVectorizer.
        if isinstance(raw_documents, str):
            )

        self._validate_vocabulary()
        max_df = self.max_df
        min_df = self.min_df
        max_features = self.max_features

        if self.fixed_vocabulary_ and self.lowercase:
            for term in self.vocabulary:
                if any(map(str.isupper, term)):
                        "Upper case characters found in"
                        " vocabulary while 'lowercase'"
                        " is True. These entries will not"
                        " be matched with any documents"
                    )
                    break

        vocabulary, X = self._count_vocab(raw_documents, self.fixed_vocabulary_)

        if not self.fixed_vocabulary_:
            n_doc = X.shape[0]
            max_doc_count = max_df if isinstance(max_df, Integral) else max_df * n_doc
            min_doc_count = min_df if isinstance(min_df, Integral) else min_df * n_doc
            if max_doc_count < min_doc_count:
                raise ValueError("max_df corresponds to < documents than min_df")
            if max_features is not None:
                X = self._sort_features(X, vocabulary)
            X = self._limit_features(
                X, vocabulary, max_doc_count, min_doc_count, max_features
            )
            if max_features is None:
            self.vocabulary_ = vocabulary

        return X

    def transform(self, raw_documents):
        """Transform documents to document-term matrix.

        Extract token counts out of raw text documents using the vocabulary
        fitted with fit or the one provided to the constructor.

        Returns
        -------
        """
            )
        self._check_vocabulary()

        # use the same matrix-building strategy as fit_transform
        _, X = self._count_vocab(raw_documents, fixed_vocab=True)
        return X

        """Return terms per document with nonzero entries in X.

        Returns
        -------
        X_inv : list of arrays of shape (n_samples,)
            List of arrays of terms.
        """
        # We need CSR format for fast row manipulations.
        X = check_array(X, accept_sparse="csr")

        terms = np.array(list(self.vocabulary_.keys()))
        indices = np.array(list(self.vocabulary_.values()))
        inverse_vocabulary = terms[np.argsort(indices)]

            return [
                inverse_vocabulary[X[i, :].nonzero()[1]].ravel()
                for i in range(n_samples)
            ]
        else:
            return [
                inverse_vocabulary[np.flatnonzero(X[i, :])].ravel()
            ]

        Returns
        -------
        """
            [t for t, i in sorted(self.vocabulary_.items(), key=itemgetter(1))],
        )

def _make_int_array():
    """Construct an array.array of a type suitable for scipy.sparse indices."""
    return array.array(str("i"))

class TfidfTransformer(
    OneToOneFeatureMixin, TransformerMixin, BaseEstimator, auto_wrap_output_keys=None
):
    """Transform a count matrix to a normalized tf or tf-idf representation.

    Tf means term-frequency while tf-idf means term-frequency times inverse
    document-frequency. This is a common term weighting scheme in information
    retrieval, that has also found good use in document classification.

    The goal of using tf-idf instead of the raw frequencies of occurrence of a
    token in a given document is to scale down the impact of tokens that occur
    very frequently in a given corpus and that are hence empirically less
    informative than features that occur in a small fraction of the training
    corpus.

    The formula that is used to compute the tf-idf for a term t of a document d
    in a document set is tf-idf(t, d) = tf(t, d) * idf(t), and the idf is
    computed as idf(t) = log [ n / df(t) ] + 1 (if ``smooth_idf=False``), where
    n is the total number of documents in the document set and df(t) is the
    document frequency of t; the document frequency is the number of documents
    in the document set that contain the term t. The effect of adding "1" to
    the idf in the equation above is that terms with zero idf, i.e., terms
    that occur in all documents in a training set, will not be entirely
    ignored.
    (Note that the idf formula above differs from the standard textbook
    notation that defines the idf as
    idf(t) = log [ n / (df(t) + 1) ]).

    If ``smooth_idf=True`` (the default), the constant "1" is added to the
    numerator and denominator of the idf as if an extra document was seen
    containing every term in the collection exactly once, which prevents
    zero divisions: idf(t) = log [ (1 + n) / (1 + df(t)) ] + 1.

    Furthermore, the formulas used to compute tf and idf depend
    on parameter settings that correspond to the SMART notation used in IR
    as follows:

    Tf is "n" (natural) by default, "l" (logarithmic) when
    ``sublinear_tf=True``.
    Idf is "t" when use_idf is given, "n" (none) otherwise.
    Normalization is "c" (cosine) when ``norm='l2'``, "n" (none)
    when ``norm=None``.

    norm : {'l1', 'l2'} or None, default='l2'
        Each output row will have unit norm, either:

        - 'l2': Sum of squares of vector elements is 1. The cosine
          similarity between two vectors is their dot product when l2 norm has
          been applied.
        - 'l1': Sum of absolute values of vector elements is 1.
          See :func:`~sklearn.preprocessing.normalize`.
        - None: No normalization.

    use_idf : bool, default=True
        Enable inverse-document-frequency reweighting. If False, idf(t) = 1.

    smooth_idf : bool, default=True
        Smooth idf weights by adding one to document frequencies, as if an
        extra document was seen containing every term in the collection
        exactly once. Prevents zero divisions.

    sublinear_tf : bool, default=False
        Apply sublinear tf scaling, i.e. replace tf with 1 + log(tf).

    idf_ : array of shape (n_features)
        The inverse document frequency (IDF) vector; only defined
        if  ``use_idf`` is True.

    See Also
    --------
    CountVectorizer : Transforms text into a sparse matrix of n-gram counts.

    HashingVectorizer : Convert a collection of text documents to a matrix
        of token occurrences.

    .. [Yates2011] R. Baeza-Yates and B. Ribeiro-Neto (2011). Modern
                   Information Retrieval. Addison Wesley, pp. 68-74.

    .. [MRS2008] C.D. Manning, P. Raghavan and H. Schütze  (2008).
                   Introduction to Information Retrieval. Cambridge University
                   Press, pp. 118-120.

    Examples
    --------
    >>> from sklearn.feature_extraction.text import TfidfTransformer
    >>> corpus = ['this is the first document',
    ...           'this document is the second document',
    ...           'and this is the third one',
    ...           'is this the first document']
    >>> vocabulary = ['this', 'document', 'first', 'is', 'second', 'the',
    ...               'and', 'one']
    >>> pipe = Pipeline([('count', CountVectorizer(vocabulary=vocabulary)),
    ...                  ('tfid', TfidfTransformer())]).fit(corpus)
    >>> pipe['count'].transform(corpus).toarray()
    array([[1, 1, 1, 1, 0, 1, 0, 0],
           [1, 2, 0, 1, 1, 1, 0, 0],
           [1, 0, 0, 1, 0, 1, 1, 1],
           [1, 1, 1, 1, 0, 1, 0, 0]])
    >>> pipe['tfid'].idf_
    array([1.        , 1.22314355, 1.51082562, 1.        , 1.91629073,
           1.        , 1.91629073, 1.91629073])
    >>> pipe.transform(corpus).shape
    (4, 8)
    """

        "use_idf": ["boolean"],
        "smooth_idf": ["boolean"],
        "sublinear_tf": ["boolean"],
    }

    def __init__(self, *, norm="l2", use_idf=True, smooth_idf=True, sublinear_tf=False):
        self.use_idf = use_idf
        self.smooth_idf = smooth_idf
        self.sublinear_tf = sublinear_tf

        """Learn the idf vector (global term weights).

            A matrix of term/token counts.

        y : None
            This parameter is not needed to compute tf-idf.

        Returns
        -------
            Fitted transformer.
        """
        # large sparse data is not supported for 32bit platforms because
        # _document_frequency uses np.bincount which works on arrays of
        # dtype NPY_INTP which is int32 for 32bit platforms. See #20923
            self, X, accept_sparse=("csr", "csc"), accept_large_sparse=not _IS_32BIT
        )
        if not sp.issparse(X):
            X = sp.csr_matrix(X)
        dtype = X.dtype if X.dtype in (np.float64, np.float32) else np.float64

        if self.use_idf:
            n_samples, _ = X.shape
            df = _document_frequency(X)
            df = df.astype(dtype, copy=False)

            # perform idf smoothing if required
            df += float(self.smooth_idf)
            n_samples += int(self.smooth_idf)

            # log+1 instead of log makes sure terms with zero idf don't get
            # suppressed entirely.
            # Force the dtype of `idf_` to be the same as `df`. In NumPy < 2, the dtype
            # was depending on the value of `n_samples`.
            self.idf_ = np.full_like(df, fill_value=n_samples, dtype=dtype)
            self.idf_ /= df
            # `np.log` preserves the dtype of `df` and thus `dtype`.
            np.log(self.idf_, out=self.idf_)
            self.idf_ += 1.0

    def transform(self, X, copy=True):
        """Transform a count matrix to a tf or tf-idf representation.

        X : sparse matrix of (n_samples, n_features)

        copy : bool, default=True
            Whether to copy X and operate on the copy or perform in-place
            operations. `copy=False` will only be effective with CSR sparse matrix.

        Returns
        -------
        vectors : sparse matrix of shape (n_samples, n_features)
            Tf-idf-weighted document-term matrix.
        """
            self,
            X,
            copy=copy,
        )
            X = sp.csr_matrix(X, dtype=X.dtype)

        if self.sublinear_tf:
            np.log(X.data, X.data)
            X.data += 1.0

        if hasattr(self, "idf_"):
            # the columns of X (CSR matrix) can be accessed with `X.indices `and
            # multiplied with the corresponding `idf` value
            X.data *= self.idf_[X.indices]

        return X

        # FIXME: np.float16 could be preserved if _inplace_csr_row_normalize_l2
        # accepted it.

class TfidfVectorizer(CountVectorizer):
    r"""Convert a collection of raw documents to a matrix of TF-IDF features.

    Equivalent to :class:`CountVectorizer` followed by
    :class:`TfidfTransformer`.

    For an example of usage, see
    :ref:`sphx_glr_auto_examples_text_plot_document_classification_20newsgroups.py`.

    :class:`~sklearn.feature_extraction.text.HashingVectorizer`, see

        decode.

            analyzer.

        If a string, it is passed to _check_stop_list and the appropriate stop
        list is returned. 'english' is currently the only supported string
        value.

    token_pattern : str, default=r"(?u)\\b\\w\\w+\\b"

    max_df : float or int, default=1.0
        If float in range [0.0, 1.0], the parameter represents a proportion of
        documents, integer absolute counts.

    min_df : float or int, default=1
        If float in range of [0.0, 1.0], the parameter represents a proportion
        of documents, integer absolute counts.

        given, a vocabulary is determined from the input documents.

        If True, all non-zero term counts are set to 1. This does not mean
        outputs will have only 0/1 values, only that the tf term in tf-idf
        is binary. (Set `binary` to True, `use_idf` to False and
        `norm` to None to get 0/1 outputs).

    dtype : dtype, default=float64

    idf_ : array of shape (n_features,)
        if ``use_idf`` is True.

    See Also
    --------

    TfidfTransformer : Performs the TF-IDF transformation from a provided
        matrix of counts.

    Examples
    --------
    >>> from sklearn.feature_extraction.text import TfidfVectorizer
    ... ]
    >>> vectorizer = TfidfVectorizer()
    (4, 9)
    """

    _parameter_constraints: dict = {**CountVectorizer._parameter_constraints}
    _parameter_constraints.update(
        {
            "norm": [StrOptions({"l1", "l2"}), None],
            "use_idf": ["boolean"],
            "smooth_idf": ["boolean"],
            "sublinear_tf": ["boolean"],
        }
    )

        self,
        *,
        min_df=1,
        use_idf=True,
        smooth_idf=True,
        sublinear_tf=False,
    ):
            input=input,
            encoding=encoding,
            decode_error=decode_error,
            strip_accents=strip_accents,
            lowercase=lowercase,
            preprocessor=preprocessor,
            tokenizer=tokenizer,
            analyzer=analyzer,
            stop_words=stop_words,
            token_pattern=token_pattern,
            ngram_range=ngram_range,
            max_df=max_df,
            min_df=min_df,
            vocabulary=vocabulary,
            binary=binary,
        )

    # Broadcast the TF-IDF parameters to the underlying transformer instance
    # for easy grid search and repr

    @property
    def idf_(self):
        """Inverse document frequency vector, only defined if `use_idf=True`.

        Returns
        -------
        ndarray of shape (n_features,)
        """
        if not hasattr(self, "_tfidf"):
            raise NotFittedError(
                f"{self.__class__.__name__} is not fitted yet. Call 'fit' with "
                "appropriate arguments before using this attribute."
            )
        return self._tfidf.idf_

    @idf_.setter
    def idf_(self, value):
        if not self.use_idf:
            raise ValueError("`idf_` cannot be set when `user_idf=False`.")
            # We should support transferring `idf_` from another `TfidfTransformer`
            # and therefore, we need to create the transformer instance it does not
            # exist yet.
            self._tfidf = TfidfTransformer(
                norm=self.norm,
                use_idf=self.use_idf,
                smooth_idf=self.smooth_idf,
                sublinear_tf=self.sublinear_tf,
            )
        if hasattr(self, "vocabulary_"):
            if len(self.vocabulary_) != len(value):
                    "idf length = %d must be equal to vocabulary size = %d"
                    % (len(value), len(self.vocabulary))
                )
        self._tfidf.idf_ = value

    def _check_params(self):
        if self.dtype not in FLOAT_DTYPES:
                "Only {} 'dtype' should be used. {} 'dtype' will "
                "be converted to np.float64.".format(FLOAT_DTYPES, self.dtype),
            )

        """Learn vocabulary and idf from training set.

        y : None
            This parameter is not needed to compute tfidf.

        Returns
        -------
        """
        self._check_params()
        self._tfidf = TfidfTransformer(
            norm=self.norm,
            use_idf=self.use_idf,
            smooth_idf=self.smooth_idf,
            sublinear_tf=self.sublinear_tf,
        )
        X = super().fit_transform(raw_documents)
        self._tfidf.fit(X)

        """Learn vocabulary and idf, return document-term matrix.

        y : None

        Returns
        -------
        """
        )
        # X is already a transformed view of raw_documents so
        # we set copy to False
        return self._tfidf.transform(X, copy=False)

        Uses the vocabulary and document frequencies (df) learned by fit (or
        fit_transform).

        Returns
        -------
        """
        check_is_fitted(self, msg="The TF-IDF vectorizer is not fitted")

        X = super().transform(raw_documents)

        Returns
        -------
        doc: str
        """

            )

        return doc

                min_n += 1
            else:
                tokens = []

            min_n += 1
        else:
            ngrams = []

        ngrams = []

            w = " " + w + " "
                offset = 0
                    offset += 1
                    break

        Returns
        -------
        """

        else:
            )

        Returns
        -------
        """

            )

        Returns
        -------
        """

        Returns
        -------
        """

        try:

                )

        Returns
        -------
        """

                _analyze,
            )

                _analyze,
            )

                _analyze,
            )

        else:
            )

                vocab = {}
            else:
                            i,
                        )
        else:

            )

            )

            )

        if (
        ):
            )
                )
            if (
            ):
                )
                )

        self,
        *,
    ):

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """
            )

        Returns
        -------
        """
            )

        return X

        y : any

        Returns
        -------
        """

        )

        self,
        *,
        min_df=1,
    ):

        """

        return X

        """

            else:
            )

        else:

        indptr = []

                try:
                    else:
                    continue

                )

                    (
                )

        else:

        )

        y : None

        Returns
        -------
        """

        y : None

        Returns
        -------
        """
            )

                    )
                    break

            )

        return X

        Returns
        -------
        """
            )

        return X

        Returns
        -------
        """

            return [
            ]
        else:
            return [
            ]

        Returns
        -------
        """
        )

        y : None

        Returns
        -------
        """
        )

        Returns
        -------
        """
            self,
            X,
        )

        return X

        self,
        *,
        min_df=1,
    ):
        )

        Returns
        -------
        """
            )

            )
                )

            )

        y : None

        Returns
        -------
        """
        )

        y : None

        Returns
        -------
        """
        )

        Returns
        -------
        """

def test_img_to_graph():
    x, y = np.mgrid[:4, :4] - 10
    grad_x = img_to_graph(x)
    grad_y = img_to_graph(y)
    assert grad_x.nnz == grad_y.nnz
    # Negative elements are the diagonal: the elements of the original
    # image. Positive elements are the values of the gradient, they
    # should all be equal on grad_x and grad_y
    np.testing.assert_array_equal(
        grad_x.data[grad_x.data > 0], grad_y.data[grad_y.data > 0]
    )

def test_img_to_graph_sparse():
    # Check that the edges are in the right position
    #  when using a sparse image with a singleton component
    mask = np.zeros((2, 3), dtype=bool)
    mask[0, 0] = 1
    mask[:, 2] = 1
    x = np.zeros((2, 3))
    x[0, 0] = 1
    x[0, 2] = -1
    x[1, 2] = -2
    grad_x = img_to_graph(x, mask=mask).todense()
    desired = np.array([[1, 0, 0], [0, -1, 1], [0, 1, -2]])
    np.testing.assert_array_equal(grad_x, desired)

def test_grid_to_graph():
    # Checking that the function works with graphs containing no edges
    size = 2
    roi_size = 1
    # Generating two convex parts with one vertex
    # Thus, edges will be empty in _to_graph
    mask = np.zeros((size, size), dtype=bool)
    mask[0:roi_size, 0:roi_size] = True
    mask[-roi_size:, -roi_size:] = True
    mask = mask.reshape(size**2)
    A = grid_to_graph(n_x=size, n_y=size, mask=mask, return_as=np.ndarray)
    assert connected_components(A)[0] == 2

    # check ordering
    graph = grid_to_graph(2, 3, 1, mask=mask.ravel()).todense()
    desired = np.array([[1, 0, 0], [0, 1, 1], [0, 1, 1]])
    np.testing.assert_array_equal(graph, desired)

    # Checking that the function works whatever the type of mask is
    mask = np.ones((size, size), dtype=np.int16)
    A = grid_to_graph(n_x=size, n_y=size, n_z=size, mask=mask)
    assert connected_components(A)[0] == 1

    # Checking dtype of the graph
    mask = np.ones((size, size))
    A = grid_to_graph(n_x=size, n_y=size, n_z=size, mask=mask, dtype=bool)
    assert A.dtype == bool
    A = grid_to_graph(n_x=size, n_y=size, n_z=size, mask=mask, dtype=int)
    assert A.dtype == int
    A = grid_to_graph(n_x=size, n_y=size, n_z=size, mask=mask, dtype=np.float64)
    assert A.dtype == np.float64

def test_connect_regions(raccoon_face_fxt):
    face = raccoon_face_fxt
    # subsample by 4 to reduce run time
    face = face[::4, ::4]
    for thr in (50, 150):
        mask = face > thr
        graph = img_to_graph(face, mask=mask)
        assert ndimage.label(mask)[1] == connected_components(graph)[0]

def test_connect_regions_with_grid(raccoon_face_fxt):

    mask = face > 50
    graph = grid_to_graph(*face.shape, mask=mask)
    assert ndimage.label(mask)[1] == connected_components(graph)[0]

    mask = face > 150
    graph = grid_to_graph(*face.shape, mask=mask, dtype=None)

def downsampled_face(raccoon_face_fxt):
    face = face[::2, ::2] + face[1::2, ::2] + face[::2, 1::2] + face[1::2, 1::2]
    face = face.astype(np.float32)
    face /= 16.0
    return face

def orange_face(downsampled_face):
    face = downsampled_face
    face_color = np.zeros(face.shape + (3,))
    face_color[:, :, 0] = 256 - face
    face_color[:, :, 1] = 256 - face / 2
    face_color[:, :, 2] = 256 - face / 4
    return face_color

def _make_images(face):
    # make a collection of faces
    images = np.zeros((3,) + face.shape)
    images[0] = face
    images[1] = face + 1
    images[2] = face + 2
    return images

def downsampled_face_collection(downsampled_face):
    return _make_images(downsampled_face)

def test_extract_patches_all(downsampled_face):
    i_h, i_w = face.shape
    p_h, p_w = 16, 16
    expected_n_patches = (i_h - p_h + 1) * (i_w - p_w + 1)
    patches = extract_patches_2d(face, (p_h, p_w))
    assert patches.shape == (expected_n_patches, p_h, p_w)

def test_extract_patches_all_color(orange_face):
    face = orange_face
    i_h, i_w = face.shape[:2]
    assert patches.shape == (expected_n_patches, p_h, p_w, 3)

def test_extract_patches_all_rect(downsampled_face):
    face = face[:, 32:97]
    p_h, p_w = 16, 12

def test_extract_patches_max_patches(downsampled_face):

    patches = extract_patches_2d(face, (p_h, p_w), max_patches=100)
    assert patches.shape == (100, p_h, p_w)

    expected_n_patches = int(0.5 * (i_h - p_h + 1) * (i_w - p_w + 1))
    patches = extract_patches_2d(face, (p_h, p_w), max_patches=0.5)

        extract_patches_2d(face, (p_h, p_w), max_patches=2.0)
        extract_patches_2d(face, (p_h, p_w), max_patches=-1.0)

def test_extract_patch_same_size_image(downsampled_face):
    # Request patches of the same size as image
    # Should return just the single patch a.k.a. the image
    patches = extract_patches_2d(face, face.shape, max_patches=2)
    assert patches.shape[0] == 1

def test_extract_patches_less_than_max_patches(downsampled_face):
    p_h, p_w = 3 * i_h // 4, 3 * i_w // 4
    # this is 3185

    patches = extract_patches_2d(face, (p_h, p_w), max_patches=4000)

def test_reconstruct_patches_perfect(downsampled_face):

    face_reconstructed = reconstruct_from_patches_2d(patches, face.shape)
    np.testing.assert_array_almost_equal(face, face_reconstructed)

def test_reconstruct_patches_perfect_color(orange_face):

def test_patch_extractor_fit(downsampled_face_collection):
    faces = downsampled_face_collection
    extr = PatchExtractor(patch_size=(8, 8), max_patches=100, random_state=0)
    assert extr == extr.fit(faces)

def test_patch_extractor_max_patches(downsampled_face_collection):
    i_h, i_w = faces.shape[1:3]
    p_h, p_w = 8, 8

    max_patches = 100
    expected_n_patches = len(faces) * max_patches
    extr = PatchExtractor(
        patch_size=(p_h, p_w), max_patches=max_patches, random_state=0
    )
    patches = extr.transform(faces)

    max_patches = 0.5
    expected_n_patches = len(faces) * int(
        (i_h - p_h + 1) * (i_w - p_w + 1) * max_patches
    )
    )

def test_patch_extractor_max_patches_default(downsampled_face_collection):
    extr = PatchExtractor(max_patches=100, random_state=0)
    assert patches.shape == (len(faces) * 100, 19, 25)

def test_patch_extractor_all_patches(downsampled_face_collection):
    expected_n_patches = len(faces) * (i_h - p_h + 1) * (i_w - p_w + 1)
    extr = PatchExtractor(patch_size=(p_h, p_w), random_state=0)

def test_patch_extractor_color(orange_face):
    faces = _make_images(orange_face)

def test_extract_patches_strided():
    image_shapes_1D = [(10,), (10,), (11,), (10,)]
    patch_sizes_1D = [(1,), (2,), (3,), (8,)]
    patch_steps_1D = [(1,), (1,), (4,), (2,)]

    expected_views_1D = [(10,), (9,), (3,), (2,)]
    last_patch_1D = [(10,), (8,), (8,), (2,)]

    image_shapes_2D = [(10, 20), (10, 20), (10, 20), (11, 20)]
    patch_sizes_2D = [(2, 2), (10, 10), (10, 11), (6, 6)]
    patch_steps_2D = [(5, 5), (3, 10), (3, 4), (4, 2)]

    expected_views_2D = [(2, 4), (1, 2), (1, 3), (2, 8)]
    last_patch_2D = [(5, 15), (0, 10), (0, 8), (4, 14)]

    image_shapes_3D = [(5, 4, 3), (3, 3, 3), (7, 8, 9), (7, 8, 9)]
    patch_sizes_3D = [(2, 2, 3), (2, 2, 2), (1, 7, 3), (1, 3, 3)]
    patch_steps_3D = [(1, 2, 10), (1, 1, 1), (2, 1, 3), (3, 3, 4)]

    expected_views_3D = [(4, 2, 1), (2, 2, 2), (4, 2, 3), (3, 2, 2)]
    last_patch_3D = [(3, 2, 0), (1, 1, 1), (6, 1, 6), (6, 3, 4)]

    image_shapes = image_shapes_1D + image_shapes_2D + image_shapes_3D
    patch_sizes = patch_sizes_1D + patch_sizes_2D + patch_sizes_3D
    patch_steps = patch_steps_1D + patch_steps_2D + patch_steps_3D
    expected_views = expected_views_1D + expected_views_2D + expected_views_3D
    last_patches = last_patch_1D + last_patch_2D + last_patch_3D

    for image_shape, patch_size, patch_step, expected_view, last_patch in zip(
        image_shapes, patch_sizes, patch_steps, expected_views, last_patches
    ):
        image = np.arange(np.prod(image_shape)).reshape(image_shape)
        patches = _extract_patches(
            image, patch_shape=patch_size, extraction_step=patch_step
        )

        ndim = len(image_shape)

        assert patches.shape[:ndim] == expected_view
        last_patch_slices = tuple(
            slice(i, i + j, None) for i, j in zip(last_patch, patch_size)
        )
        assert (
            patches[(-1, None, None) * ndim] == image[last_patch_slices].squeeze()
        ).all()

def test_extract_patches_square(downsampled_face):
    # test same patch size for all dimensions
    p = 8
    expected_n_patches = ((i_h - p + 1), (i_w - p + 1))
    patches = _extract_patches(face, patch_shape=p)
    assert patches.shape == (expected_n_patches[0], expected_n_patches[1], p, p)

def test_width_patch():
    # width and height of the patch should be less than the image
    x = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
        extract_patches_2d(x, (4, 1))
        extract_patches_2d(x, (1, 4))

def test_patch_extractor_wrong_input(orange_face):
    """Check that an informative error is raised if the patch_size is not valid."""
    err_msg = "patch_size must be a tuple of two integers"
    extractor = PatchExtractor(patch_size=(8, 8, 8))
        extractor.transform(faces)

def test_feature_hasher_dicts():
    feature_hasher = FeatureHasher(n_features=16)
    assert "dict" == feature_hasher.input_type

    raw_X = [{"foo": "bar", "dada": 42, "tzara": 37}, {"foo": "baz", "gaga": "string1"}]
    X1 = FeatureHasher(n_features=16).transform(raw_X)
    gen = (iter(d.items()) for d in raw_X)
    X2 = FeatureHasher(n_features=16, input_type="pair").transform(gen)
    assert_array_equal(X1.toarray(), X2.toarray())

def test_feature_hasher_strings():
    # mix byte and Unicode strings; note that "foo" is a duplicate in row 0
    raw_X = [
        ["foo", "bar", "baz", "foo".encode("ascii")],
        ["bar".encode("ascii"), "baz", "quux"],
    ]

    for lg_n_features in (7, 9, 11, 16, 22):
        n_features = 2**lg_n_features

        it = (x for x in raw_X)  # iterable

        feature_hasher = FeatureHasher(
            n_features=n_features, input_type="string", alternate_sign=False
        )
        X = feature_hasher.transform(it)

        assert X.shape[0] == len(raw_X)
        assert X.shape[1] == n_features

        assert X[0].sum() == 4
        assert X[1].sum() == 3

        assert X.nnz == 6

def test_feature_hasher_single_string(raw_X):
    """FeatureHasher raises error when a sample is a single string.

    Non-regression test for gh-13199.
    """
    msg = "Samples can not be a single string"

    feature_hasher = FeatureHasher(n_features=10, input_type="string")
        feature_hasher.transform(raw_X)

def test_hashing_transform_seed():
    # check the influence of the seed when computing the hashes
    raw_X = [
    ]

    raw_X_ = (((f, 1) for f in x) for x in raw_X)
    indices, indptr, _ = _hashing_transform(raw_X_, 2**7, str, False)

    indices_0, indptr_0, _ = _hashing_transform(raw_X_, 2**7, str, False, seed=0)
    assert_array_equal(indices, indices_0)
    assert_array_equal(indptr, indptr_0)

    indices_1, _, _ = _hashing_transform(raw_X_, 2**7, str, False, seed=1)
    with pytest.raises(AssertionError):
        assert_array_equal(indices, indices_1)

def test_feature_hasher_pairs():
    raw_X = (
        iter(d.items())
        for d in [{"foo": 1, "bar": 2}, {"baz": 3, "quux": 4, "foo": -1}]
    )
    feature_hasher = FeatureHasher(n_features=16, input_type="pair")
    x1, x2 = feature_hasher.transform(raw_X).toarray()
    x1_nz = sorted(np.abs(x1[x1 != 0]))
    x2_nz = sorted(np.abs(x2[x2 != 0]))
    assert [1, 2] == x1_nz
    assert [1, 3, 4] == x2_nz

def test_feature_hasher_pairs_with_string_values():
    raw_X = (
        for d in [{"foo": 1, "bar": "a"}, {"baz": "abc", "quux": 4, "foo": -1}]
    )
    assert [1, 1] == x1_nz
    assert [1, 1, 4] == x2_nz

    raw_X = (iter(d.items()) for d in [{"bax": "abc"}, {"bax": "abc"}])
    x1_nz = np.abs(x1[x1 != 0])
    x2_nz = np.abs(x2[x2 != 0])
    assert [1] == x1_nz
    assert [1] == x2_nz
    assert_array_equal(x1, x2)

def test_hash_empty_input():
    n_features = 16
    raw_X = [[], (), iter(range(0))]

    feature_hasher = FeatureHasher(n_features=n_features, input_type="string")
    X = feature_hasher.transform(raw_X)

    assert_array_equal(X.toarray(), np.zeros((len(raw_X), n_features)))

def test_hasher_zeros():
    # Assert that no zeros are materialized in the output.
    X = FeatureHasher().transform([{"foo": 0}])
    assert X.data.shape == (0,)

def test_hasher_alternate_sign():
    X = [list("Thequickbrownfoxjumped")]

    Xt = FeatureHasher(alternate_sign=True, input_type="string").fit_transform(X)
    assert Xt.data.min() < 0 and Xt.data.max() > 0

    Xt = FeatureHasher(alternate_sign=False, input_type="string").fit_transform(X)
    assert Xt.data.min() > 0

def test_hash_collisions():

    Xt = FeatureHasher(
        alternate_sign=True, n_features=1, input_type="string"
    ).fit_transform(X)
    # check that some of the hashed tokens are added
    # with an opposite sign and cancel out
    assert abs(Xt.data[0]) < len(X[0])

        alternate_sign=False, n_features=1, input_type="string"
    assert Xt.data[0] == len(X[0])

def test_strip_accents():
    # check some classical latin accentuated symbols
    a = "àáâãäåçèéêë"
    expected = "aaaaaaceeee"
    assert strip_accents_unicode(a) == expected

    a = "ìíîïñòóôõöùúûüý"
    expected = "iiiinooooouuuuy"

    # check some arabic
    a = "\u0625"  # alef with a hamza below: إ
    expected = "\u0627"  # simple alef: ا

    # mix letters accentuated and not
    a = "this is à test"
    expected = "this is a test"

    # strings that are already decomposed
    a = "o\u0308"  # o with diaeresis
    expected = "o"

    # combining marks by themselves
    a = "\u0300\u0301\u0302\u0303"
    expected = ""

    # Multiple combining marks on one character
    a = "o\u0308\u0304"

def test_to_ascii():
    assert strip_accents_ascii(a) == expected

    a = "\u0625"  # halef with a hamza below
    expected = ""  # halef has no direct ascii match

def test_word_analyzer_unigrams(Vectorizer):
    wa = Vectorizer(strip_accents="ascii").build_analyzer()
    text = "J'ai mangé du kangourou  ce midi, c'était pas très bon."
    expected = [
        "ai",
        "mange",
        "du",
        "kangourou",
        "ce",
        "midi",
        "etait",
        "pas",
        "tres",
        "bon",
    ]
    assert wa(text) == expected

    text = "This is a test, really.\n\n I met Harry yesterday."
    expected = ["this", "is", "test", "really", "met", "harry", "yesterday"]

    wa = Vectorizer(input="file").build_analyzer()
    text = StringIO("This is a test with a file-like object!")
    expected = ["this", "is", "test", "with", "file", "like", "object"]

    # with custom preprocessor
    wa = Vectorizer(preprocessor=uppercase).build_analyzer()
    text = "J'ai mangé du kangourou  ce midi,  c'était pas très bon."
        "AI",
        "MANGE",
        "DU",
        "KANGOUROU",
        "CE",
        "MIDI",
        "ETAIT",
        "PAS",
        "TRES",
        "BON",
    ]

    # with custom tokenizer
    wa = Vectorizer(tokenizer=split_tokenize, strip_accents="ascii").build_analyzer()
        "j'ai",
        "mange",
        "du",
        "ce",
        "midi,",
        "c'etait",
        "pas",
        "tres",
        "bon.",
    ]

def test_word_analyzer_unigrams_and_bigrams():
    wa = CountVectorizer(
        analyzer="word", strip_accents="unicode", ngram_range=(1, 2)
    ).build_analyzer()

        "ai",
        "mange",
        "du",
        "ce",
        "midi",
        "etait",
        "pas",
        "tres",
        "bon",
        "ai mange",
        "mange du",
        "du kangourou",
        "kangourou ce",
        "ce midi",
        "midi etait",
        "etait pas",
        "pas tres",
        "tres bon",
    ]

def test_unicode_decode_error():
    # decode_error default to strict, so this should fail
    # First, encode (as bytes) a unicode string.
    text_bytes = text.encode("utf-8")

    # Then let the Analyzer try to decode it as ascii. It should fail,
    # because we have given it an incorrect encoding.
    wa = CountVectorizer(ngram_range=(1, 2), encoding="ascii").build_analyzer()
    with pytest.raises(UnicodeDecodeError):
        wa(text_bytes)

    ca = CountVectorizer(
        analyzer="char", ngram_range=(3, 6), encoding="ascii"
        ca(text_bytes)

def test_char_ngram_analyzer():
    cnga = CountVectorizer(
        analyzer="char", strip_accents="unicode", ngram_range=(3, 6)

    text = "J'ai mangé du kangourou  ce midi, c'était pas très bon"
    expected = ["j'a", "'ai", "ai ", "i m", " ma"]
    assert cnga(text)[:5] == expected
    expected = ["s tres", " tres ", "tres b", "res bo", "es bon"]
    assert cnga(text)[-5:] == expected

    text = "This \n\tis a test, really.\n\n I met Harry yesterday"
    expected = ["thi", "his", "is ", "s i", " is"]

    expected = [" yeste", "yester", "esterd", "sterda", "terday"]

        input="file", analyzer="char", ngram_range=(3, 6)

def test_char_wb_ngram_analyzer():
        analyzer="char_wb", strip_accents="unicode", ngram_range=(3, 6)

    expected = [" th", "thi", "his", "is ", " thi"]

    expected = ["yester", "esterd", "sterda", "terday", "erday "]

        input="file", analyzer="char_wb", ngram_range=(3, 6)
    text = StringIO("A test with a file-like object!")
    expected = [" a ", " te", "tes", "est", "st ", " tes"]
    assert cnga(text)[:6] == expected

def test_word_ngram_analyzer():
        analyzer="word", strip_accents="unicode", ngram_range=(3, 6)

    expected = ["this is test", "is test really", "test really met"]
    assert cnga(text)[:3] == expected

        "test really met harry yesterday",
        "this is test really met harry",
        "is test really met harry yesterday",
    ]
    assert cnga(text)[-3:] == expected

    cnga_file = CountVectorizer(
        input="file", analyzer="word", ngram_range=(3, 6)
    file = StringIO(text)
    assert cnga_file(file) == cnga(text)

def test_countvectorizer_custom_vocabulary():
    vocab = {"pizza": 0, "beer": 1}
    terms = set(vocab.keys())

    # Try a few of the supported types.
    for typ in [dict, list, iter, partial(defaultdict, int)]:
        v = typ(vocab)
        vect = CountVectorizer(vocabulary=v)
        vect.fit(JUNK_FOOD_DOCS)
        if isinstance(v, Mapping):
            assert vect.vocabulary_ == vocab
        else:
            assert set(vect.vocabulary_) == terms
        X = vect.transform(JUNK_FOOD_DOCS)
        assert X.shape[1] == len(terms)
        inv = vect.inverse_transform(X)
        assert len(inv) == X.shape[0]

def test_countvectorizer_custom_vocabulary_pipeline():
    what_we_like = ["pizza", "beer"]
    pipe = Pipeline(
        [
            ("count", CountVectorizer(vocabulary=what_we_like)),
            ("tfidf", TfidfTransformer()),
        ]
    )
    X = pipe.fit_transform(ALL_FOOD_DOCS)
    assert set(pipe.named_steps["count"].vocabulary_) == set(what_we_like)
    assert X.shape[1] == len(what_we_like)

def test_countvectorizer_custom_vocabulary_repeated_indices():
    vocab = {"pizza": 0, "beer": 0}
    msg = "Vocabulary contains repeated indices"
        vect = CountVectorizer(vocabulary=vocab)
        vect.fit(["pasta_siziliana"])

def test_countvectorizer_custom_vocabulary_gap_index():
    vocab = {"pizza": 1, "beer": 2}
    with pytest.raises(ValueError, match="doesn't contain index"):
        vect.fit(["pasta_verdura"])

def test_countvectorizer_stop_words():
    cv = CountVectorizer()
    cv.set_params(stop_words="english")
    assert cv.get_stop_words() == ENGLISH_STOP_WORDS
    cv.set_params(stop_words="_bad_str_stop_")
        cv.get_stop_words()
    cv.set_params(stop_words="_bad_unicode_stop_")
    stoplist = ["some", "other", "words"]
    cv.set_params(stop_words=stoplist)
    assert cv.get_stop_words() == set(stoplist)

def test_countvectorizer_empty_vocabulary():
    with pytest.raises(ValueError, match="empty vocabulary"):
        vect = CountVectorizer(vocabulary=[])
        vect.fit(["foo"])

        v = CountVectorizer(max_df=1.0, stop_words="english")
        # fit on stopwords only
        v.fit(["to be or not to be", "and me too", "and so do you"])

def test_fit_countvectorizer_twice():
    X1 = cv.fit_transform(ALL_FOOD_DOCS[:5])
    X2 = cv.fit_transform(ALL_FOOD_DOCS[5:])
    assert X1.shape[1] != X2.shape[1]

def test_countvectorizer_custom_token_pattern():
    """Check `get_feature_names_out()` when a custom token pattern is passed.
    https://github.com/scikit-learn/scikit-learn/issues/12971
    """
    corpus = [
        "This is the 1st document in my corpus.",
        "This document is the 2nd sample.",
        "And this is the 3rd one.",
        "Is this the 4th document?",
    ]
    token_pattern = r"[0-9]{1,3}(?:st|nd|rd|th)\s\b(\w{2,})\b"
    vectorizer = CountVectorizer(token_pattern=token_pattern)
    vectorizer.fit_transform(corpus)
    expected = ["document", "one", "sample"]
    feature_names_out = vectorizer.get_feature_names_out()
    assert_array_equal(feature_names_out, expected)

def test_countvectorizer_custom_token_pattern_with_several_group():
    """Check that we raise an error if token pattern capture several groups.
    """
    corpus = [
    ]

    token_pattern = r"([0-9]{1,3}(?:st|nd|rd|th))\s\b(\w{2,})\b"
    err_msg = "More than 1 capturing group in token pattern"
        vectorizer.fit(corpus)

def test_countvectorizer_uppercase_in_vocab():
    # Check that the check for uppercase in the provided vocabulary is only done at fit
    # time and not at transform time (#21251)
    vocabulary = ["Sample", "Upper", "Case", "Vocabulary"]
    message = (
        "Upper case characters found in"
        " vocabulary while 'lowercase'"
        " is True. These entries will not"
        " be matched with any documents"
    )

    vectorizer = CountVectorizer(lowercase=True, vocabulary=vocabulary)

    with pytest.warns(UserWarning, match=message):
        vectorizer.fit(vocabulary)

        vectorizer.transform(vocabulary)

def test_tf_transformer_feature_names_out():
    """Check get_feature_names_out for TfidfTransformer"""
    X = [[1, 1, 1], [1, 1, 0], [1, 0, 0]]
    tr = TfidfTransformer(smooth_idf=True, norm="l2").fit(X)

    feature_names_in = ["a", "c", "b"]
    feature_names_out = tr.get_feature_names_out(feature_names_in)
    assert_array_equal(feature_names_in, feature_names_out)

def test_tf_idf_smoothing():
    tr = TfidfTransformer(smooth_idf=True, norm="l2")
    tfidf = tr.fit_transform(X).toarray()
    assert (tfidf >= 0).all()

    # check normalization
    assert_array_almost_equal((tfidf**2).sum(axis=1), [1.0, 1.0, 1.0])

    # this is robust to features with only zeros
    X = [[1, 1, 0], [1, 1, 0], [1, 0, 0]]

def test_tfidf_no_smoothing():
    tr = TfidfTransformer(smooth_idf=False, norm="l2")

    # the lack of smoothing make IDF fragile in the presence of feature with
    # only zeros

    in_warning_message = "divide by zero"
    with pytest.warns(RuntimeWarning, match=in_warning_message):
        tr.fit_transform(X).toarray()

def test_sublinear_tf():
    X = [[1], [2], [3]]
    tr = TfidfTransformer(sublinear_tf=True, use_idf=False, norm=None)
    assert tfidf[0] == 1
    assert tfidf[1] > tfidf[0]
    assert tfidf[2] > tfidf[1]
    assert tfidf[1] < 2
    assert tfidf[2] < 3

def test_vectorizer():
    # raw documents as an iterator
    train_data = iter(ALL_FOOD_DOCS[:-1])
    test_data = [ALL_FOOD_DOCS[-1]]
    n_train = len(ALL_FOOD_DOCS) - 1

    # test without vocabulary
    v1 = CountVectorizer(max_df=0.5)
    counts_train = v1.fit_transform(train_data)
    if hasattr(counts_train, "tocsr"):
        counts_train = counts_train.tocsr()
    assert counts_train[0, v1.vocabulary_["pizza"]] == 2

    # build a vectorizer v1 with the same vocabulary as the one fitted by v1
    v2 = CountVectorizer(vocabulary=v1.vocabulary_)

    # compare that the two vectorizer give the same output on the test sample
    for v in (v1, v2):
        counts_test = v.transform(test_data)
        if hasattr(counts_test, "tocsr"):
            counts_test = counts_test.tocsr()

        vocabulary = v.vocabulary_
        assert counts_test[0, vocabulary["salad"]] == 1
        assert counts_test[0, vocabulary["tomato"]] == 1
        assert counts_test[0, vocabulary["water"]] == 1

        # stop word from the fixed list
        assert "the" not in vocabulary

        # stop word found automatically by the vectorizer DF thresholding
        # words that are high frequent across the complete corpus are likely
        # to be not informative (either real stop words of extraction
        # artifacts)
        assert "copyright" not in vocabulary

        # not present in the sample
        assert counts_test[0, vocabulary["coke"]] == 0
        assert counts_test[0, vocabulary["burger"]] == 0
        assert counts_test[0, vocabulary["beer"]] == 0
        assert counts_test[0, vocabulary["pizza"]] == 0

    # test tf-idf
    t1 = TfidfTransformer(norm="l1")
    tfidf = t1.fit(counts_train).transform(counts_train).toarray()
    assert len(t1.idf_) == len(v1.vocabulary_)
    assert tfidf.shape == (n_train, len(v1.vocabulary_))

    # test tf-idf with new data
    tfidf_test = t1.transform(counts_test).toarray()
    assert tfidf_test.shape == (len(test_data), len(v1.vocabulary_))

    # test tf alone
    t2 = TfidfTransformer(norm="l1", use_idf=False)
    tf = t2.fit(counts_train).transform(counts_train).toarray()
    assert not hasattr(t2, "idf_")

    # test idf transform with unlearned idf vector
    t3 = TfidfTransformer(use_idf=True)
        t3.transform(counts_train)

    # L1-normalized term frequencies sum to one
    assert_array_almost_equal(np.sum(tf, axis=1), [1.0] * n_train)

    # test the direct tfidf vectorizer
    # (equivalent to term count vectorizer + tfidf transformer)
    tv = TfidfVectorizer(norm="l1")

    tv.max_df = v1.max_df
    tfidf2 = tv.fit_transform(train_data).toarray()
    assert not tv.fixed_vocabulary_
    assert_array_almost_equal(tfidf, tfidf2)

    # test the direct tfidf vectorizer with new data
    tfidf_test2 = tv.transform(test_data).toarray()
    assert_array_almost_equal(tfidf_test, tfidf_test2)

    # test transform on unfitted vectorizer with empty vocabulary
    v3 = CountVectorizer(vocabulary=None)
        v3.transform(train_data)

    # ascii preprocessor?
    v3.set_params(strip_accents="ascii", lowercase=False)
    processor = v3.build_preprocessor()
    expected = strip_accents_ascii(text)
    result = processor(text)
    assert expected == result

    # error on bad strip_accents param
    v3.set_params(strip_accents="_gabbledegook_", preprocessor=None)
        v3.build_preprocessor()

    # error with bad analyzer type
    v3.set_params = "_invalid_analyzer_type_"
        v3.build_analyzer()

def test_tfidf_vectorizer_setters():
    norm, use_idf, smooth_idf, sublinear_tf = "l2", False, False, False
    tv = TfidfVectorizer(
        norm=norm, use_idf=use_idf, smooth_idf=smooth_idf, sublinear_tf=sublinear_tf
    )
    tv.fit(JUNK_FOOD_DOCS)
    assert tv._tfidf.norm == norm
    assert tv._tfidf.use_idf == use_idf
    assert tv._tfidf.smooth_idf == smooth_idf
    assert tv._tfidf.sublinear_tf == sublinear_tf

    # assigning value to `TfidfTransformer` should not have any effect until
    # fitting
    tv.norm = "l1"
    tv.use_idf = True
    tv.smooth_idf = True
    tv.sublinear_tf = True

    assert tv._tfidf.norm == tv.norm
    assert tv._tfidf.use_idf == tv.use_idf
    assert tv._tfidf.smooth_idf == tv.smooth_idf
    assert tv._tfidf.sublinear_tf == tv.sublinear_tf

def test_hashing_vectorizer():
    v = HashingVectorizer()
    X = v.transform(ALL_FOOD_DOCS)
    token_nnz = X.nnz
    assert X.shape == (len(ALL_FOOD_DOCS), v.n_features)
    assert X.dtype == v.dtype

    # By default the hashed values receive a random sign and l2 normalization
    # makes the feature values bounded
    assert np.min(X.data) > -1
    assert np.min(X.data) < 0
    assert np.max(X.data) > 0
    assert np.max(X.data) < 1

    # Check that the rows are normalized
    for i in range(X.shape[0]):
        assert_almost_equal(np.linalg.norm(X[0].data, 2), 1.0)

    # Check vectorization with some non-default parameters
    v = HashingVectorizer(ngram_range=(1, 2), norm="l1")

    # ngrams generate more non zeros
    ngrams_nnz = X.nnz
    assert ngrams_nnz > token_nnz
    assert ngrams_nnz < 2 * token_nnz

        assert_almost_equal(np.linalg.norm(X[0].data, 1), 1.0)

def test_feature_names():
    cv = CountVectorizer(max_df=0.5)

    # test for Value error on unfitted/empty vocabulary
        cv.get_feature_names_out()
    assert not cv.fixed_vocabulary_

    # test for vocabulary learned from data
    X = cv.fit_transform(ALL_FOOD_DOCS)
    assert len(cv.vocabulary_) == n_features

    feature_names = cv.get_feature_names_out()
    assert isinstance(feature_names, np.ndarray)
    assert feature_names.dtype == object

    assert len(feature_names) == n_features
        [
            "beer",
            "burger",
            "celeri",
            "coke",
            "pizza",
            "salad",
            "sparkling",
            "tomato",
            "water",
        ],
    )

    for idx, name in enumerate(feature_names):
        assert idx == cv.vocabulary_.get(name)

    # test for custom vocabulary
    vocab = [
        "beer",
        "burger",
        "celeri",
        "coke",
        "pizza",
        "salad",
        "sparkling",
        "tomato",
        "water",
    ]

    cv = CountVectorizer(vocabulary=vocab)
        [
            "beer",
            "burger",
            "celeri",
            "coke",
            "pizza",
            "salad",
            "tomato",
            "water",
        ],
    )
    assert cv.fixed_vocabulary_

def test_vectorizer_max_features(Vectorizer):
    expected_vocabulary = {"burger", "beer", "salad", "pizza"}

    # test bounded number of extracted features
    vectorizer = Vectorizer(max_df=0.6, max_features=4)
    vectorizer.fit(ALL_FOOD_DOCS)
    assert set(vectorizer.vocabulary_) == expected_vocabulary

def test_count_vectorizer_max_features():
    # Regression test: max_features didn't work correctly in 0.14.

    cv_1 = CountVectorizer(max_features=1)
    cv_3 = CountVectorizer(max_features=3)
    cv_None = CountVectorizer(max_features=None)

    counts_1 = cv_1.fit_transform(JUNK_FOOD_DOCS).sum(axis=0)
    counts_3 = cv_3.fit_transform(JUNK_FOOD_DOCS).sum(axis=0)
    counts_None = cv_None.fit_transform(JUNK_FOOD_DOCS).sum(axis=0)

    features_1 = cv_1.get_feature_names_out()
    features_3 = cv_3.get_feature_names_out()
    features_None = cv_None.get_feature_names_out()

    # The most common feature is "the", with frequency 7.
    assert 7 == counts_1.max()
    assert 7 == counts_3.max()
    assert 7 == counts_None.max()

    # The most common feature should be the same
    assert "the" == features_1[np.argmax(counts_1)]
    assert "the" == features_3[np.argmax(counts_3)]
    assert "the" == features_None[np.argmax(counts_None)]

def test_vectorizer_max_df():
    test_data = ["abc", "dea", "eat"]
    vect = CountVectorizer(analyzer="char", max_df=1.0)
    vect.fit(test_data)
    assert "a" in vect.vocabulary_.keys()
    assert len(vect.vocabulary_.keys()) == 6

    vect.max_df = 0.5  # 0.5 * 3 documents -> max_doc_count == 1.5
    assert "a" not in vect.vocabulary_.keys()  # {ae} ignored
    assert len(vect.vocabulary_.keys()) == 4  # {bcdt} remain

    vect.max_df = 1

def test_vectorizer_min_df():
    vect = CountVectorizer(analyzer="char", min_df=1)

    vect.min_df = 2
    assert "c" not in vect.vocabulary_.keys()  # {bcdt} ignored
    assert len(vect.vocabulary_.keys()) == 2  # {ae} remain

    vect.min_df = 0.8  # 0.8 * 3 documents -> min_doc_count == 2.4
    assert "c" not in vect.vocabulary_.keys()  # {bcdet} ignored
    assert len(vect.vocabulary_.keys()) == 1  # {a} remains

def test_count_binary_occurrences():
    # by default multiple occurrences are counted as longs
    test_data = ["aaabc", "abbde"]
    X = vect.fit_transform(test_data).toarray()
    assert_array_equal(["a", "b", "c", "d", "e"], vect.get_feature_names_out())
    assert_array_equal([[3, 1, 1, 0, 0], [1, 2, 0, 1, 1]], X)

    # using boolean features, we can fetch the binary occurrence info
    # instead.
    vect = CountVectorizer(analyzer="char", max_df=1.0, binary=True)
    assert_array_equal([[1, 1, 1, 0, 0], [1, 1, 0, 1, 1]], X)

    # check the ability to change the dtype
    vect = CountVectorizer(analyzer="char", max_df=1.0, binary=True, dtype=np.float32)
    X_sparse = vect.fit_transform(test_data)
    assert X_sparse.dtype == np.float32

def test_hashed_binary_occurrences():
    vect = HashingVectorizer(alternate_sign=False, analyzer="char", norm=None)
    X = vect.transform(test_data)
    assert np.max(X[0:1].data) == 3
    assert np.max(X[1:2].data) == 2
    assert X.dtype == np.float64

    # instead.
    vect = HashingVectorizer(
        analyzer="char", alternate_sign=False, binary=True, norm=None
    )
    assert np.max(X.data) == 1

        analyzer="char", alternate_sign=False, binary=True, norm=None, dtype=np.float64
    )

def test_vectorizer_inverse_transform(Vectorizer):
    # raw documents
    data = ALL_FOOD_DOCS
    vectorizer = Vectorizer()
    transformed_data = vectorizer.fit_transform(data)
    inversed_data = vectorizer.inverse_transform(transformed_data)
    assert isinstance(inversed_data, list)

    analyze = vectorizer.build_analyzer()
    for doc, inversed_terms in zip(data, inversed_data):
        terms = np.sort(np.unique(analyze(doc)))
        inversed_terms = np.sort(np.unique(inversed_terms))
        assert_array_equal(terms, inversed_terms)

    assert sparse.issparse(transformed_data)
    assert transformed_data.format == "csr"

    # Test that inverse_transform also works with numpy arrays and
    # scipy
    transformed_data2 = transformed_data.toarray()
    inversed_data2 = vectorizer.inverse_transform(transformed_data2)
    for terms, terms2 in zip(inversed_data, inversed_data2):
        assert_array_equal(np.sort(terms), np.sort(terms2))

    # Check that inverse_transform also works on non CSR sparse data:
    transformed_data3 = transformed_data.tocsc()
    inversed_data3 = vectorizer.inverse_transform(transformed_data3)
    for terms, terms3 in zip(inversed_data, inversed_data3):
        assert_array_equal(np.sort(terms), np.sort(terms3))

def test_count_vectorizer_pipeline_grid_selection():
    data = JUNK_FOOD_DOCS + NOTJUNK_FOOD_DOCS

    # label junk food as -1, the others as +1
    target = [-1] * len(JUNK_FOOD_DOCS) + [1] * len(NOTJUNK_FOOD_DOCS)

    # split the dataset for model development and final evaluation
    train_data, test_data, target_train, target_test = train_test_split(
        data, target, test_size=0.2, random_state=0
    )

    pipeline = Pipeline([("vect", CountVectorizer()), ("svc", LinearSVC())])

        "vect__ngram_range": [(1, 1), (1, 2)],
        "svc__loss": ("hinge", "squared_hinge"),
    }

    # find the best parameters for both the feature extraction and the
    # classifier
    grid_search = GridSearchCV(pipeline, parameters, n_jobs=1, cv=3)

    # Check that the best model found by grid search is 100% correct on the
    # held out evaluation set.
    pred = grid_search.fit(train_data, target_train).predict(test_data)
    assert_array_equal(pred, target_test)

    # on this toy dataset bigram representation which is used in the last of
    # the grid_search is considered the best estimator since they all converge
    # to 100% accuracy models
    assert grid_search.best_score_ == 1.0
    best_vectorizer = grid_search.best_estimator_.named_steps["vect"]
    assert best_vectorizer.ngram_range == (1, 1)

def test_vectorizer_pipeline_grid_selection():

        data, target, test_size=0.1, random_state=0
    )

    pipeline = Pipeline([("vect", TfidfVectorizer()), ("svc", LinearSVC())])

        "vect__norm": ("l1", "l2"),
    }

    grid_search = GridSearchCV(pipeline, parameters, n_jobs=1)

    assert best_vectorizer.norm == "l2"
    assert not best_vectorizer.fixed_vocabulary_

def test_vectorizer_pipeline_cross_validation():

    cv_scores = cross_val_score(pipeline, data, target, cv=3)
    assert_array_equal(cv_scores, [1.0, 1.0, 1.0])

def test_vectorizer_unicode():
    # tests that the count vectorizer works with cyrillic.
    document = (
        "Машинное обучение — обширный подраздел искусственного "
        "интеллекта, изучающий методы построения алгоритмов, "
        "способных обучаться."
    )

    vect = CountVectorizer()
    X_counted = vect.fit_transform([document])
    assert X_counted.shape == (1, 12)

    vect = HashingVectorizer(norm=None, alternate_sign=False)
    X_hashed = vect.transform([document])
    assert X_hashed.shape == (1, 2**20)

    # No collisions on such a small dataset
    assert X_counted.nnz == X_hashed.nnz

    # When norm is None and not alternate_sign, the tokens are counted up to
    # collisions
    assert_array_equal(np.sort(X_counted.data), np.sort(X_hashed.data))

def test_tfidf_vectorizer_with_fixed_vocabulary():
    # non regression smoke test for inheritance issues
    vocabulary = ["pizza", "celeri"]
    vect = TfidfVectorizer(vocabulary=vocabulary)
    X_1 = vect.fit_transform(ALL_FOOD_DOCS)
    X_2 = vect.transform(ALL_FOOD_DOCS)
    assert_array_almost_equal(X_1.toarray(), X_2.toarray())
    assert vect.fixed_vocabulary_

def test_pickling_vectorizer():
    instances = [
        HashingVectorizer(),
        HashingVectorizer(norm="l1"),
        HashingVectorizer(binary=True),
        HashingVectorizer(ngram_range=(1, 2)),
        CountVectorizer(),
        CountVectorizer(preprocessor=strip_tags),
        CountVectorizer(analyzer=lazy_analyze),
        CountVectorizer(preprocessor=strip_tags).fit(JUNK_FOOD_DOCS),
        CountVectorizer(strip_accents=strip_eacute).fit(JUNK_FOOD_DOCS),
        TfidfVectorizer(),
        TfidfVectorizer(analyzer=lazy_analyze),
        TfidfVectorizer().fit(JUNK_FOOD_DOCS),
    ]

    for orig in instances:
        s = pickle.dumps(orig)
        copy = pickle.loads(s)
        assert type(copy) == orig.__class__
        assert copy.get_params() == orig.get_params()
        assert_allclose_dense_sparse(
            copy.fit_transform(JUNK_FOOD_DOCS),
            orig.fit_transform(JUNK_FOOD_DOCS),
        )

def test_pickling_built_processors(factory):
    """Tokenizers cannot be pickled
    https://github.com/scikit-learn/scikit-learn/issues/12833
    """
    vec = CountVectorizer()
    function = factory(vec)
    roundtripped_function = pickle.loads(pickle.dumps(function))
    expected = function(text)
    result = roundtripped_function(text)
    assert result == expected

def test_countvectorizer_vocab_sets_when_pickling():
    # ensure that vocabulary of type set is coerced to a list to
    # preserve iteration ordering after deserialization
    vocab_words = np.array(
        [
            "beer",
            "burger",
            "celeri",
            "coke",
            "pizza",
            "salad",
            "tomato",
            "water",
        ]
    )
    for x in range(0, 100):
        vocab_set = set(rng.choice(vocab_words, size=5, replace=False))
        cv = CountVectorizer(vocabulary=vocab_set)
        unpickled_cv = pickle.loads(pickle.dumps(cv))
        cv.fit(ALL_FOOD_DOCS)
        unpickled_cv.fit(ALL_FOOD_DOCS)
        assert_array_equal(
            cv.get_feature_names_out(), unpickled_cv.get_feature_names_out()
        )

def test_countvectorizer_vocab_dicts_when_pickling():
        [
            "beer",
            "burger",
            "celeri",
            "coke",
            "pizza",
            "salad",
            "tomato",
            "water",
        ]
    )
        vocab_dict = dict()
        words = rng.choice(vocab_words, size=5, replace=False)
        for y in range(0, 5):
            vocab_dict[words[y]] = y
        cv = CountVectorizer(vocabulary=vocab_dict)
        )

def test_pickling_transformer():
    X = CountVectorizer().fit_transform(JUNK_FOOD_DOCS)
    orig = TfidfTransformer().fit(X)
    s = pickle.dumps(orig)
    copy = pickle.loads(s)
    assert type(copy) == orig.__class__
    assert_array_equal(copy.fit_transform(X).toarray(), orig.fit_transform(X).toarray())

def test_transformer_idf_setter():
    copy = TfidfTransformer()
    copy.idf_ = orig.idf_
    assert_array_equal(copy.transform(X).toarray(), orig.transform(X).toarray())

def test_tfidf_vectorizer_setter():
    orig = TfidfVectorizer(use_idf=True)
    orig.fit(JUNK_FOOD_DOCS)
    copy = TfidfVectorizer(vocabulary=orig.vocabulary_, use_idf=True)
        copy.transform(JUNK_FOOD_DOCS).toarray(),
        orig.transform(JUNK_FOOD_DOCS).toarray(),
    )
    # `idf_` cannot be set with `use_idf=False`
    copy = TfidfVectorizer(vocabulary=orig.vocabulary_, use_idf=False)
    err_msg = "`idf_` cannot be set when `user_idf=False`."
        copy.idf_ = orig.idf_

def test_tfidfvectorizer_invalid_idf_attr():
    vect = TfidfVectorizer(use_idf=True)
    vect.fit(JUNK_FOOD_DOCS)
    copy = TfidfVectorizer(vocabulary=vect.vocabulary_, use_idf=True)
    expected_idf_len = len(vect.idf_)
    invalid_idf = [1.0] * (expected_idf_len + 1)
        setattr(copy, "idf_", invalid_idf)

def test_non_unique_vocab():
    vocab = ["a", "b", "c", "a", "a"]
    vect = CountVectorizer(vocabulary=vocab)
        vect.fit([])

def test_hashingvectorizer_nan_in_docs():
    # np.nan can appear when using pandas to load text fields from a csv file
    # with missing values.
    message = "np.nan is an invalid document, expected byte or unicode string."
    exception = ValueError

    def func():
        hv = HashingVectorizer()
        hv.fit_transform(["hello world", np.nan, "hello hello"])

    with pytest.raises(exception, match=message):
        func()

def test_tfidfvectorizer_binary():
    # Non-regression test: TfidfVectorizer used to ignore its "binary" param.
    v = TfidfVectorizer(binary=True, use_idf=False, norm=None)
    assert v.binary

    X = v.fit_transform(["hello world", "hello hello"]).toarray()
    assert_array_equal(X.ravel(), [1, 1, 1, 0])
    X2 = v.transform(["hello world", "hello hello"]).toarray()
    assert_array_equal(X2.ravel(), [1, 1, 1, 0])

def test_tfidfvectorizer_export_idf():
    assert_array_almost_equal(vect.idf_, vect._tfidf.idf_)

def test_vectorizer_vocab_clone():
    vect_vocab = TfidfVectorizer(vocabulary=["the"])
    vect_vocab_clone = clone(vect_vocab)
    vect_vocab.fit(ALL_FOOD_DOCS)
    vect_vocab_clone.fit(ALL_FOOD_DOCS)
    assert vect_vocab_clone.vocabulary_ == vect_vocab.vocabulary_

def test_vectorizer_string_object_as_input(Vectorizer):
    message = "Iterable over raw text documents expected, string object received."
    vec = Vectorizer()

    with pytest.raises(ValueError, match=message):
        vec.fit_transform("hello world!")

        vec.fit("hello world!")
    vec.fit(["some text", "some other text"])

        vec.transform("hello world!")

def test_tfidf_transformer_type(X_dtype):
    X = sparse.rand(10, 20000, dtype=X_dtype, random_state=42)
    X_trans = TfidfTransformer().fit_transform(X)
    assert X_trans.dtype == X.dtype

def test_tfidf_transformer_sparse(csc_container, csr_container):
    X = sparse.rand(10, 20000, dtype=np.float64, random_state=42)
    X_csc = csc_container(X)
    X_csr = csr_container(X)

    X_trans_csc = TfidfTransformer().fit_transform(X_csc)
    X_trans_csr = TfidfTransformer().fit_transform(X_csr)
    assert_allclose_dense_sparse(X_trans_csc, X_trans_csr)
    assert X_trans_csc.format == X_trans_csr.format

def test_tfidf_vectorizer_type(vectorizer_dtype, output_dtype, warning_expected):
    X = np.array(["numpy", "scipy", "sklearn"])
    vectorizer = TfidfVectorizer(dtype=vectorizer_dtype)

    warning_msg_match = "'dtype' should be used."
    if warning_expected:
        with pytest.warns(UserWarning, match=warning_msg_match):
            X_idf = vectorizer.fit_transform(X)
    else:
        with warnings.catch_warnings():
            warnings.simplefilter("error", UserWarning)
    assert X_idf.dtype == output_dtype

def test_vectorizers_invalid_ngram_range(vec):
    # vectorizers could be initialized with invalid ngram range
    # test for raising error message
    invalid_range = vec.ngram_range
    message = re.escape(
        f"Invalid value for ngram_range={invalid_range} "
        "lower boundary larger than the upper boundary."
    )

        vec.fit(["good news everyone"])

        vec.fit_transform(["good news everyone"])

    if isinstance(vec, HashingVectorizer):
        with pytest.raises(ValueError, match=message):
            vec.transform(["good news everyone"])

def _check_stop_words_consistency(estimator):
    stop_words = estimator.get_stop_words()
    tokenize = estimator.build_tokenizer()
    preprocess = estimator.build_preprocessor()
    return estimator._check_stop_words_consistency(stop_words, preprocess, tokenize)

def test_vectorizer_stop_words_inconsistent():
    lstr = r"\['and', 'll', 've'\]"
    message = (
        "Your stop_words may be inconsistent with your "
        "preprocessing. Tokenizing the stop words generated "
        "tokens %s not in stop_words." % lstr
    )
    for vec in [CountVectorizer(), TfidfVectorizer(), HashingVectorizer()]:
        vec.set_params(stop_words=["you've", "you", "you'll", "AND"])
        with pytest.warns(UserWarning, match=message):
            vec.fit_transform(["hello world"])
        # reset stop word validation
        del vec._stop_words_id
        assert _check_stop_words_consistency(vec) is False

    # Only one warning per stop list
        vec.fit_transform(["hello world"])
    assert _check_stop_words_consistency(vec) is None

    # Test caching of inconsistency assessment
    vec.set_params(stop_words=["you've", "you", "you'll", "blah", "AND"])

def test_countvectorizer_sort_features_64bit_sparse_indices(csr_container):
    """
    Check that CountVectorizer._sort_features preserves the dtype of its sparse
    feature matrix.

    This test is skipped on 32bit platforms, see:
        https://github.com/scikit-learn/scikit-learn/pull/11295
    for more details.
    """

    X = csr_container((5, 5), dtype=np.int64)

    # force indices and indptr to int64.
    INDICES_DTYPE = np.int64
    X.indices = X.indices.astype(INDICES_DTYPE)
    X.indptr = X.indptr.astype(INDICES_DTYPE)

    vocabulary = {"scikit-learn": 0, "is": 1, "great!": 2}

    Xs = CountVectorizer()._sort_features(X, vocabulary)

    assert INDICES_DTYPE == Xs.indices.dtype

def test_stop_word_validation_custom_preprocessor(Estimator):
    data = [{"text": "some text"}]

    vec = Estimator()
    assert _check_stop_words_consistency(vec) is True

    vec = Estimator(preprocessor=lambda x: x["text"], stop_words=["and"])
    assert _check_stop_words_consistency(vec) == "error"
    # checks are cached
    vec.fit_transform(data)

    class CustomEstimator(Estimator):
        def build_preprocessor(self):
            return lambda x: x["text"]

    vec = CustomEstimator(stop_words=["and"])

    vec = Estimator(
        tokenizer=lambda doc: re.compile(r"\w{1,}").findall(doc), stop_words=["and"]
    )

def test_callable_analyzer_error(Estimator, input_type, err_type, err_msg):
    data = ["this is text, not file or filename"]
        Estimator(analyzer=lambda x: x.split(), input=input_type).fit_transform(data)

def test_callable_analyzer_change_behavior(Estimator, analyzer, input_type):
    with pytest.raises((FileNotFoundError, AttributeError)):
        Estimator(analyzer=analyzer, input=input_type).fit_transform(data)

def test_callable_analyzer_reraise_error(tmpdir, Estimator):
    # check if a custom exception from the analyzer is shown to the user
    def analyzer(doc):
        raise Exception("testing")

    f = tmpdir.join("file.txt")
    f.write("sample content\n")

    with pytest.raises(Exception, match="testing"):
        Estimator(analyzer=analyzer, input="file").fit_transform([f])

def test_unused_parameters_warn(
    Vectorizer,
    stop_words,
    tokenizer,
    preprocessor,
    ngram_range,
    token_pattern,
    analyzer,
    unused_name,
    ovrd_name,
    ovrd_msg,
):
    train_data = JUNK_FOOD_DOCS
    # setting parameter and checking for corresponding warning messages
    vect = Vectorizer()
    vect.set_params(
        stop_words=stop_words,
        tokenizer=tokenizer,
        preprocessor=preprocessor,
        ngram_range=ngram_range,
        token_pattern=token_pattern,
        analyzer=analyzer,
    )
    msg = "The parameter %s will not be used since %s %s" % (
        unused_name,
        ovrd_name,
        ovrd_msg,
    )
        vect.fit(train_data)

def test_n_features_in(Vectorizer, X):
    # For vectorizers, n_features_in_ does not make sense
    assert not hasattr(vectorizer, "n_features_in_")
    vectorizer.fit(X)

def test_tie_breaking_sample_order_invariance():
    # Checks the sample order invariance when setting max_features
    # non-regression test for #17939
    vec = CountVectorizer(max_features=1)
    vocab1 = vec.fit(["hello", "world"]).vocabulary_
    vocab2 = vec.fit(["world", "hello"]).vocabulary_
    assert vocab1 == vocab2

def test_nonnegative_hashing_vectorizer_result_indices():
    # add test for pr 19035
    hashing = HashingVectorizer(n_features=1000000, ngram_range=(2, 3))
    indices = hashing.transform(["22pcs efuture"]).indices
    assert indices[0] >= 0

def test_vectorizers_do_not_have_set_output(Estimator):
    """Check that vectorizers do not define set_output."""
    est = Estimator()
    assert not hasattr(est, "set_output")

def test_tfidf_transformer_copy(csr_container):
    """Check the behaviour of TfidfTransformer.transform with the copy parameter."""

    # keep a copy of the original matrix for later comparison
    X_csr_original = X_csr.copy()

    transformer = TfidfTransformer().fit(X_csr)

    X_transform = transformer.transform(X_csr, copy=True)
    assert_allclose_dense_sparse(X_csr, X_csr_original)
    assert X_transform is not X_csr

    X_transform = transformer.transform(X_csr, copy=False)
    assert X_transform is X_csr
        assert_allclose_dense_sparse(X_csr, X_csr_original)

def test_tfidf_vectorizer_perserve_dtype_idf(dtype):
    """Check that `idf_` has the same dtype as the input data.

    https://github.com/scikit-learn/scikit-learn/issues/30016
    """
    X = [str(uuid.uuid4()) for i in range(100_000)]
    vectorizer = TfidfVectorizer(dtype=dtype).fit(X)
    assert vectorizer.idf_.dtype == dtype

def test_dictvectorizer(sparse, dtype, sort, iterable):
    D = [{"foo": 1, "bar": 3}, {"bar": 4, "baz": 2}, {"bar": 1, "quux": 1, "quuux": 2}]

    v = DictVectorizer(sparse=sparse, dtype=dtype, sort=sort)
    X = v.fit_transform(iter(D) if iterable else D)

    assert sp.issparse(X) == sparse
    assert X.shape == (3, 5)
    assert X.sum() == 14
    assert v.inverse_transform(X) == D

    if sparse:
        # CSR matrices can't be compared for equality
            X.toarray(), v.transform(iter(D) if iterable else D).toarray()
        )
    else:
        assert_array_equal(X, v.transform(iter(D) if iterable else D))

    if sort:
        assert v.feature_names_ == sorted(v.feature_names_)

def test_feature_selection():
    # make two feature dicts with two useful features and a bunch of useless
    # ones, in terms of chi2
    d1 = dict([("useless%d" % i, 10) for i in range(20)], useful1=1, useful2=20)
    d2 = dict([("useless%d" % i, 10) for i in range(20)], useful1=20, useful2=1)

    for indices in (True, False):
        v = DictVectorizer().fit([d1, d2])
        X = v.transform([d1, d2])
        sel = SelectKBest(chi2, k=2).fit(X, [0, 1])

        v.restrict(sel.get_support(indices=indices), indices=indices)
        assert_array_equal(v.get_feature_names_out(), ["useful1", "useful2"])

def test_one_of_k():
    D_in = [
        {"version": "1", "ham": 2},
        {"version": "2", "spam": 0.3},
        {"version=3": True, "spam": -1},
    ]
    v = DictVectorizer()
    X = v.fit_transform(D_in)

    D_out = v.inverse_transform(X)
    assert D_out[0] == {"version=1": 1, "ham": 2}

    names = v.get_feature_names_out()
    assert "version=2" in names
    assert "version" not in names

def test_iterable_value():
    D_names = ["ham", "spam", "version=1", "version=2", "version=3"]
    X_expected = [
        [2.0, 0.0, 2.0, 1.0, 0.0],
        [0.0, 0.3, 0.0, 1.0, 0.0],
        [0.0, -1.0, 0.0, 0.0, 1.0],
    ]
    D_in = [
        {"version": ["1", "2", "1"], "ham": 2},
    ]
    X = X.toarray()
    assert_array_equal(X, X_expected)

    assert D_out[0] == {"version=1": 2, "version=2": 1, "ham": 2}

    assert_array_equal(names, D_names)

def test_iterable_not_string_error():
    error_value = (
        "Unsupported type <class 'int'> in iterable value. "
        "Only iterables of string are supported."
    )
    D2 = [{"foo": "1", "bar": "2"}, {"foo": "3", "baz": "1"}, {"foo": [1, "three"]}]
    v = DictVectorizer(sparse=False)
    with pytest.raises(TypeError) as error:
        v.fit(D2)
    assert str(error.value) == error_value

def test_mapping_error():
        "Unsupported value type <class 'dict'> "
        "for foo: {'one': 1, 'three': 3}.\n"
        "Mapping objects are not supported."
    )
    D2 = [
        {"foo": "1", "bar": "2"},
        {"foo": "3", "baz": "1"},
        {"foo": {"one": 1, "three": 3}},
    ]
        v.fit(D2)

def test_unseen_or_no_features():
    D = [{"camelot": 0, "spamalot": 1}]
    for sparse in [True, False]:
        v = DictVectorizer(sparse=sparse).fit(D)

        X = v.transform({"push the pram a lot": 2})
        if sparse:
            X = X.toarray()
        assert_array_equal(X, np.zeros((1, 2)))

        X = v.transform({})
        if sparse:

        with pytest.raises(ValueError, match="empty"):
            v.transform([])

def test_deterministic_vocabulary(global_random_seed):
    # Generate equal dictionaries with different memory layouts
    items = [("%03d" % i, i) for i in range(1000)]
    rng = Random(global_random_seed)
    d_sorted = dict(items)
    rng.shuffle(items)
    d_shuffled = dict(items)

    # check that the memory layout does not impact the resulting vocabulary
    v_1 = DictVectorizer().fit([d_sorted])
    v_2 = DictVectorizer().fit([d_shuffled])

    assert v_1.vocabulary_ == v_2.vocabulary_

def test_n_features_in():
    # For vectorizers, n_features_in_ does not make sense and does not exist.
    dv = DictVectorizer()
    assert not hasattr(dv, "n_features_in_")
    d = [{"foo": 1, "bar": 2}, {"foo": 3, "baz": 1}]
    dv.fit(d)

def test_dictvectorizer_dense_sparse_equivalence():
    """Check the equivalence between between sparse and dense DictVectorizer.
    https://github.com/scikit-learn/scikit-learn/issues/19978
    """
    movie_entry_fit = [
        {"category": ["thriller", "drama"], "year": 2003},
        {"category": ["animation", "family"], "year": 2011},
        {"year": 1974},
    ]
    movie_entry_transform = [{"category": ["thriller"], "unseen_feature": "3"}]
    dense_vectorizer = DictVectorizer(sparse=False)
    sparse_vectorizer = DictVectorizer(sparse=True)

    dense_vector_fit = dense_vectorizer.fit_transform(movie_entry_fit)
    sparse_vector_fit = sparse_vectorizer.fit_transform(movie_entry_fit)

    assert not sp.issparse(dense_vector_fit)
    assert sp.issparse(sparse_vector_fit)

    assert_allclose(dense_vector_fit, sparse_vector_fit.toarray())

    dense_vector_transform = dense_vectorizer.transform(movie_entry_transform)
    sparse_vector_transform = sparse_vectorizer.transform(movie_entry_transform)

    assert not sp.issparse(dense_vector_transform)
    assert sp.issparse(sparse_vector_transform)

    assert_allclose(dense_vector_transform, sparse_vector_transform.toarray())

    dense_inverse_transform = dense_vectorizer.inverse_transform(dense_vector_transform)
    sparse_inverse_transform = sparse_vectorizer.inverse_transform(
        sparse_vector_transform
    )

    expected_inverse = [{"category=thriller": 1.0}]
    assert dense_inverse_transform == expected_inverse
    assert sparse_inverse_transform == expected_inverse

def test_dict_vectorizer_unsupported_value_type():
    """Check that we raise an error when the value associated to a feature
    is not supported.

    https://github.com/scikit-learn/scikit-learn/issues/19489
    """

    class A:
        pass

    vectorizer = DictVectorizer(sparse=True)
    X = [{"foo": A()}]
    err_msg = "Unsupported value Type"
        vectorizer.fit_transform(X)

def test_dict_vectorizer_get_feature_names_out():
    """Check that integer feature names are converted to strings in
    feature_names_out."""

    X = [{1: 2, 3: 4}, {2: 4}]
    dv = DictVectorizer(sparse=False).fit(X)

    feature_names = dv.get_feature_names_out()
    assert_array_equal(feature_names, ["1", "2", "3"])

def test_dict_vectorizer_not_fitted_error(method, input):
    """Check that unfitted DictVectorizer instance raises NotFittedError.

    This should be part of the common test but currently they test estimator accepting
    text input.
    """
    dv = DictVectorizer(sparse=False)

        getattr(dv, method)(input)

class SelfTrainingClassifier(ClassifierMixin, MetaEstimatorMixin, BaseEstimator):
    """Self-training classifier.

    This :term:`metaestimator` allows a given supervised classifier to function as a
    semi-supervised classifier, allowing it to learn from unlabeled data. It
    does this by iteratively predicting pseudo-labels for the unlabeled data
    and adding them to the training set.

    The classifier will continue iterating until either max_iter is reached, or
    no pseudo-labels were added to the training set in the previous iteration.

    Read more in the :ref:`User Guide <self_training>`.

        An estimator object implementing `fit` and `predict_proba`.
        Invoking the `fit` method will fit a clone of the passed estimator,
        which will be stored in the `estimator_` attribute.

            `estimator` was added to replace `base_estimator`.

    base_estimator : estimator object

            `base_estimator` was deprecated in 1.6 and will be removed in 1.8.

    threshold : float, default=0.75
        The decision threshold for use with `criterion='threshold'`.
        Should be in [0, 1). When using the `'threshold'` criterion, a
        :ref:`well calibrated classifier <calibration>` should be used.

    criterion : {'threshold', 'k_best'}, default='threshold'
        The selection criterion used to select which labels to add to the
        training set. If `'threshold'`, pseudo-labels with prediction
        probabilities above `threshold` are added to the dataset. If `'k_best'`,
        the `k_best` pseudo-labels with highest prediction probabilities are
        added to the dataset. When using the 'threshold' criterion, a

    k_best : int, default=10
        The amount of samples to add in each iteration. Only used when
        `criterion='k_best'`.

    max_iter : int or None, default=10
        Maximum number of iterations allowed. Should be greater than or equal
        to 0. If it is `None`, the classifier will continue to predict labels
        until no new pseudo-labels are added, or all unlabeled samples have
        been labeled.

        Enable verbose output.

    estimator_ : estimator object
        The fitted estimator.

    classes_ : ndarray or list of ndarray of shape (n_classes,)
        Class labels for each output. (Taken from the trained
        `estimator_`).

    transduction_ : ndarray of shape (n_samples,)
        The labels used for the final fit of the classifier, including
        pseudo-labels added during fit.

    labeled_iter_ : ndarray of shape (n_samples,)
        The iteration in which each sample was labeled. When a sample has
        iteration 0, the sample was already labeled in the original dataset.
        When a sample has iteration -1, the sample was not labeled in any

        The number of rounds of self-training, that is the number of times the
        base estimator is fitted on relabeled variants of the training set.

    termination_condition_ : {'max_iter', 'no_change', 'all_labeled'}
        The reason that fitting was stopped.

        - `'max_iter'`: `n_iter_` reached `max_iter`.
        - `'no_change'`: no new labels were predicted.
        - `'all_labeled'`: all unlabeled samples were labeled before `max_iter`
          was reached.

    See Also
    --------
    LabelPropagation : Label propagation classifier.
    LabelSpreading : Label spreading model for semi-supervised learning.

    :doi:`David Yarowsky. 1995. Unsupervised word sense disambiguation rivaling
    supervised methods. In Proceedings of the 33rd annual meeting on
    Association for Computational Linguistics (ACL '95). Association for
    Computational Linguistics, Stroudsburg, PA, USA, 189-196.
    <10.3115/981658.981684>`

    Examples
    --------
    >>> from sklearn import datasets
    >>> from sklearn.semi_supervised import SelfTrainingClassifier
    >>> iris = datasets.load_iris()
    >>> random_unlabeled_points = rng.rand(iris.target.shape[0]) < 0.3
    >>> iris.target[random_unlabeled_points] = -1
    >>> svc = SVC(probability=True, gamma="auto")
    >>> self_training_model = SelfTrainingClassifier(svc)
    >>> self_training_model.fit(iris.data, iris.target)
    SelfTrainingClassifier(...)
    """

        # We don't require `predic_proba` here to allow passing a meta-estimator
        # that only exposes `predict_proba` after fitting.
        # TODO(1.8) remove None option
        "estimator": [None, HasMethods(["fit"])],
        # TODO(1.8) remove
            HasMethods(["fit"]),
            Hidden(StrOptions({"deprecated"})),
        ],
        "threshold": [Interval(Real, 0.0, 1.0, closed="left")],
        "criterion": [StrOptions({"threshold", "k_best"})],
        "k_best": [Interval(Integral, 1, None, closed="left")],
        "max_iter": [Interval(Integral, 0, None, closed="left"), None],
    }

        self,
        threshold=0.75,
        criterion="threshold",
        k_best=10,
    ):
        self.threshold = threshold
        self.k_best = k_best

        """Get the estimator.

        Returns
        -------
        estimator_ : estimator object
            The cloned estimator object.
        """
        # TODO(1.8): remove and only keep clone(self.estimator)
        if self.estimator is None and self.base_estimator != "deprecated":
            estimator_ = clone(self.base_estimator)

            warn(
                (
                    "`base_estimator` has been deprecated in 1.6 and will be removed"
                    " in 1.8. Please use `estimator` instead."
                ),
            )
        elif self.estimator is None and self.base_estimator == "deprecated":
                "You must pass an estimator to SelfTrainingClassifier. Use `estimator`."
            )
        elif self.estimator is not None and self.base_estimator != "deprecated":
                "You must pass only one estimator to SelfTrainingClassifier."
                " Use `estimator`."
            )
        else:
            estimator_ = clone(self.estimator)
        return estimator_

        # SelfTrainingClassifier.estimator is not validated yet
    )
    def fit(self, X, y, **params):
        """
        Fit self-training classifier using `X`, `y` as training data.

            Array representing the data.

        y : {array-like, sparse matrix} of shape (n_samples,)
            Array representing the labels. Unlabeled samples should have the
            label -1.

        Returns
        -------
        """

        self.estimator_ = self._get_estimator()

        # we need row slicing support for sparse matrices, but costly finiteness check
        # can be delegated to the base estimator.
            self,
            X,
            y,
            accept_sparse=["csr", "csc", "lil", "dok"],
        )

        if y.dtype.kind in ["U", "S"]:
                "y has dtype string. If you wish to predict on "
                "string targets, use dtype object, and use -1"
                " as the label for unlabeled samples."
            )

        has_label = y != -1

        if np.all(has_label):
            warnings.warn("y contains no unlabeled samples", UserWarning)

        if self.criterion == "k_best" and (
            self.k_best > X.shape[0] - np.sum(has_label)
        ):
                (
                    "k_best is larger than the amount of unlabeled "
                    "samples. All unlabeled samples will be labeled in "
                    "the first iteration"
                ),
            )

        else:
            routed_params = Bunch(estimator=Bunch(fit={}))

        self.transduction_ = np.copy(y)
        self.labeled_iter_ = np.full_like(y, -1)
        self.labeled_iter_[has_label] = 0

        self.n_iter_ = 0

        while not np.all(has_label) and (
            self.max_iter is None or self.n_iter_ < self.max_iter
        ):
            self.n_iter_ += 1
            self.estimator_.fit(
                X[safe_mask(X, has_label)],
                self.transduction_[has_label],
                **routed_params.estimator.fit,
            )

            # Predict on the unlabeled samples
            prob = self.estimator_.predict_proba(X[safe_mask(X, ~has_label)])
            pred = self.estimator_.classes_[np.argmax(prob, axis=1)]
            max_proba = np.max(prob, axis=1)

            # Select new labeled samples
            if self.criterion == "threshold":
                selected = max_proba > self.threshold
            else:
                n_to_select = min(self.k_best, max_proba.shape[0])
                if n_to_select == max_proba.shape[0]:
                    selected = np.ones_like(max_proba, dtype=bool)
                else:
                    # NB these are indices, not a mask
                    selected = np.argpartition(-max_proba, n_to_select)[:n_to_select]

            # Map selected indices into original array
            selected_full = np.nonzero(~has_label)[0][selected]

            # Add newly labeled confident predictions to the dataset
            self.transduction_[selected_full] = pred[selected]
            has_label[selected_full] = True
            self.labeled_iter_[selected_full] = self.n_iter_

            if selected_full.shape[0] == 0:
                # no changed labels
                self.termination_condition_ = "no_change"
                break

            if self.verbose:
                print(
                    f"End of iteration {self.n_iter_},"
                    f" added {selected_full.shape[0]} new labels."
                )

        if self.n_iter_ == self.max_iter:
            self.termination_condition_ = "max_iter"
            self.termination_condition_ = "all_labeled"

        self.estimator_.fit(
            X[safe_mask(X, has_label)],
            self.transduction_[has_label],
            **routed_params.estimator.fit,
        )
        self.classes_ = self.estimator_.classes_

    @available_if(_estimator_has("predict"))
        """Predict the classes of `X`.

            Parameters to pass to the underlying estimator's ``predict`` method.

        Returns
        -------
            Array with predicted labels.
        """

        else:
            routed_params = Bunch(estimator=Bunch(predict={}))

            self,
            X,
        )
        return self.estimator_.predict(X, **routed_params.estimator.predict)

    @available_if(_estimator_has("predict_proba"))
        """Predict probability for each possible outcome.

            Parameters to pass to the underlying estimator's
            ``predict_proba`` method.

        Returns
        -------
        y : ndarray of shape (n_samples, n_features)
            Array with prediction probabilities.
        """

        else:
            routed_params = Bunch(estimator=Bunch(predict_proba={}))

            self,
            X,
        )
        return self.estimator_.predict_proba(X, **routed_params.estimator.predict_proba)

    @available_if(_estimator_has("decision_function"))
        """Call decision function of the `estimator`.

            ``decision_function`` method.

        Returns
        -------
            Result of the decision function of the `estimator`.
        """

        else:
            routed_params = Bunch(estimator=Bunch(decision_function={}))

            self,
            X,
        )
        return self.estimator_.decision_function(
            X, **routed_params.estimator.decision_function
        )

    @available_if(_estimator_has("predict_log_proba"))
        """Predict log probability for each possible outcome.

            ``predict_log_proba`` method.

        Returns
        -------
            Array with log prediction probabilities.
        """

        else:
            routed_params = Bunch(estimator=Bunch(predict_log_proba={}))

            self,
            X,
        )
        return self.estimator_.predict_log_proba(
            X, **routed_params.estimator.predict_log_proba
        )

    @available_if(_estimator_has("score"))
    def score(self, X, y, **params):
        """Call score on the `estimator`.

            Array representing the labels.

            Parameters to pass to the underlying estimator's ``score`` method.

        Returns
        -------
            Result of calling score on the `estimator`.
        """
        _raise_for_params(params, self, "score")

            routed_params = process_routing(self, "score", **params)
        else:
            routed_params = Bunch(estimator=Bunch(score={}))

            self,
            X,
        )
        return self.estimator_.score(X, y, **routed_params.estimator.score)

        Returns
        -------
        """
            method_mapping=(
                MethodMapping()
                .add(callee="fit", caller="fit")
                .add(callee="score", caller="fit")
                .add(callee="predict", caller="predict")
                .add(callee="predict_proba", caller="predict_proba")
                .add(callee="decision_function", caller="decision_function")
                .add(callee="predict_log_proba", caller="predict_log_proba")
                .add(callee="score", caller="score")
            ),
        )

        # TODO(1.8): remove the condition check together with base_estimator
            tags.input_tags.sparse = get_tags(self.estimator).input_tags.sparse

        self,
    ):

        Returns
        -------
        """

            warn(
                (
                ),
            )
            )
            )
        else:

        """

            label -1.

        Returns
        -------
        """

            self,
            X,
            y,
        )

            )

        ):
                (
                ),
            )

        else:

        ):
            )

            else:
                else:

                break

                print(
                )

        )

        Returns
        -------
        """

        else:

            self,
            X,
        )

        Returns
        -------
        """

        else:

            self,
            X,
        )

        Returns
        -------
        """

        else:

            self,
            X,
        )
        )

        Returns
        -------
        """

        else:

            self,
            X,
        )
        )

        Returns
        -------
        """

        else:

            self,
            X,
        )

        Returns
        -------
        """
            ),
        )

class BaseLabelPropagation(ClassifierMixin, BaseEstimator, metaclass=ABCMeta):
    """Base class for label propagation module.

     Parameters
     ----------
     kernel : {'knn', 'rbf'} or callable, default='rbf'
         String identifier for kernel function to use or the kernel function
         itself. Only 'rbf' and 'knn' strings are valid inputs. The function
         passed should take two inputs, each of shape (n_samples, n_features),
         and return a (n_samples, n_samples) shaped weight matrix.

     gamma : float, default=20
         Parameter for rbf kernel.

     n_neighbors : int, default=7
         Parameter for knn kernel. Need to be strictly positive.

     alpha : float, default=1.0
         Clamping factor.

     max_iter : int, default=30
         Change maximum number of iterations allowed.

     tol : float, default=1e-3
         Convergence tolerance: threshold to consider the system at steady
         state.

         The number of parallel jobs to run.
         ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
         ``-1`` means using all processors. See :term:`Glossary <n_jobs>`
         for more details.
    """

        "kernel": [StrOptions({"knn", "rbf"}), callable],
        "n_neighbors": [Interval(Integral, 0, None, closed="neither")],
        "alpha": [None, Interval(Real, 0, 1, closed="neither")],
        "max_iter": [Interval(Integral, 0, None, closed="neither")],
    }

        self,
        *,
        gamma=20,
        n_neighbors=7,
        alpha=1,
        max_iter=30,
        tol=1e-3,
    ):

        # kernel parameters
        self.n_neighbors = n_neighbors

        # clamping factor

    def _get_kernel(self, X, y=None):
        if self.kernel == "rbf":
            if y is None:
                return rbf_kernel(X, X, gamma=self.gamma)
            else:
                return rbf_kernel(X, y, gamma=self.gamma)
        elif self.kernel == "knn":
            if self.nn_fit is None:
                self.nn_fit = NearestNeighbors(
                    n_neighbors=self.n_neighbors, n_jobs=self.n_jobs
                ).fit(X)
                return self.nn_fit.kneighbors_graph(
                    self.nn_fit._fit_X, self.n_neighbors, mode="connectivity"
                )
            else:
                return self.nn_fit.kneighbors(y, return_distance=False)
        elif callable(self.kernel):
                return self.kernel(X, X)
            else:
                return self.kernel(X, y)

    def _build_graph(self):
        raise NotImplementedError(
            "Graph construction must be implemented to fit a label propagation model."
        )

        """Perform inductive inference across the model.

        Returns
        -------
            Predictions for input data.
        """
        # Note: since `predict` does not accept semi-supervised labels as input,
        # `fit(X, y).predict(X) != fit(X, y).transduction_`.
        # Hence, `fit_predict` is not implemented.
        # See https://github.com/scikit-learn/scikit-learn/pull/24898
        probas = self.predict_proba(X)
        return self.classes_[np.argmax(probas, axis=1)].ravel()

        Compute the probability estimates for each single sample in X
        and each possible outcome seen during training (categorical
        distribution).

        Returns
        -------
        probabilities : ndarray of shape (n_samples, n_classes)
            Normalized probability distributions across
            class labels.
        """

        X_2d = validate_data(
            self,
            X,
            accept_sparse=["csc", "csr", "coo", "dok", "bsr", "lil", "dia"],
        )
        weight_matrices = self._get_kernel(self.X_, X_2d)
        if self.kernel == "knn":
            probabilities = np.array(
                [
                    np.sum(self.label_distributions_[weight_matrix], axis=0)
                    for weight_matrix in weight_matrices
                ]
            )
        else:
            weight_matrices = weight_matrices.T
            probabilities = safe_sparse_dot(weight_matrices, self.label_distributions_)
        normalizer = np.atleast_2d(np.sum(probabilities, axis=1)).T
        probabilities /= normalizer
        return probabilities

        """Fit a semi-supervised label propagation model to X.

        The input samples (labeled and unlabeled) are provided by matrix X,
        and target labels are provided by matrix y. We conventionally apply the
        label -1 to unlabeled samples in matrix y in a semi-supervised

            Target class values with unlabeled points marked as -1.
            All unlabeled samples will be transductively assigned labels
            internally, which are stored in `transduction_`.

        Returns
        -------
        """
            self,
            X,
            y,
            reset=True,
        )
        self.X_ = X

        # actual graph construction (implementations should override this)
        graph_matrix = self._build_graph()

        # label construction
        # construct a categorical distribution for classification only
        classes = np.unique(y)
        classes = classes[classes != -1]
        self.classes_ = classes

        n_samples, n_classes = len(y), len(classes)

        y = np.asarray(y)
        unlabeled = y == -1

        # initialize distributions
        self.label_distributions_ = np.zeros((n_samples, n_classes))
        for label in classes:
            self.label_distributions_[y == label, classes == label] = 1

        y_static = np.copy(self.label_distributions_)
        if self._variant == "propagation":
            # LabelPropagation
            y_static[unlabeled] = 0
        else:
            # LabelSpreading
            y_static *= 1 - self.alpha

        l_previous = np.zeros((self.X_.shape[0], n_classes))

        unlabeled = unlabeled[:, np.newaxis]
        if sparse.issparse(graph_matrix):
            graph_matrix = graph_matrix.tocsr()

        for self.n_iter_ in range(self.max_iter):
            if np.abs(self.label_distributions_ - l_previous).sum() < self.tol:
                break

            l_previous = self.label_distributions_
            self.label_distributions_ = safe_sparse_dot(
                graph_matrix, self.label_distributions_
            )

            if self._variant == "propagation":
                normalizer = np.sum(self.label_distributions_, axis=1)[:, np.newaxis]
                normalizer[normalizer == 0] = 1
                self.label_distributions_ /= normalizer
                self.label_distributions_ = np.where(
                    unlabeled, self.label_distributions_, y_static
                )
            else:
                # clamp
                self.label_distributions_ = (
                    np.multiply(self.alpha, self.label_distributions_) + y_static
                )
        else:
                "max_iter=%d was reached without convergence." % self.max_iter,
                category=ConvergenceWarning,
            )

        normalizer = np.sum(self.label_distributions_, axis=1)[:, np.newaxis]
        normalizer[normalizer == 0] = 1
        self.label_distributions_ /= normalizer

        # set the transduction item
        transduction = self.classes_[np.argmax(self.label_distributions_, axis=1)]
        self.transduction_ = transduction.ravel()

class LabelPropagation(BaseLabelPropagation):
    """Label Propagation classifier.

    Read more in the :ref:`User Guide <label_propagation>`.

    kernel : {'knn', 'rbf'} or callable, default='rbf'
        String identifier for kernel function to use or the kernel function
        itself. Only 'rbf' and 'knn' strings are valid inputs. The function
        passed should take two inputs, each of shape (n_samples, n_features),
        and return a (n_samples, n_samples) shaped weight matrix.

    gamma : float, default=20
        Parameter for rbf kernel.

    n_neighbors : int, default=7
        Parameter for knn kernel which need to be strictly positive.

    max_iter : int, default=1000
        Change maximum number of iterations allowed.

    tol : float, default=1e-3
        Convergence tolerance: threshold to consider the system at steady
        state.

        The number of parallel jobs to run.

    X_ : {array-like, sparse matrix} of shape (n_samples, n_features)
        Input array.

        The distinct labels used in classifying instances.

    label_distributions_ : ndarray of shape (n_samples, n_classes)
        Categorical distribution for each item.

    transduction_ : ndarray of shape (n_samples)
        Label assigned to each item during :term:`fit`.

    See Also
    --------
    LabelSpreading : Alternate label propagation strategy more robust to noise.

    Xiaojin Zhu and Zoubin Ghahramani. Learning from labeled and unlabeled data
    with label propagation. Technical Report CMU-CALD-02-107, Carnegie Mellon
    University, 2002 http://pages.cs.wisc.edu/~jerryzhu/pub/CMU-CALD-02-107.pdf

    Examples
    --------
    >>> from sklearn.semi_supervised import LabelPropagation
    >>> label_prop_model = LabelPropagation()
    >>> random_unlabeled_points = rng.rand(len(iris.target)) < 0.3
    >>> labels = np.copy(iris.target)
    >>> labels[random_unlabeled_points] = -1
    >>> label_prop_model.fit(iris.data, labels)
    LabelPropagation(...)
    """

    _variant = "propagation"

    _parameter_constraints: dict = {**BaseLabelPropagation._parameter_constraints}
    _parameter_constraints.pop("alpha")

        self,
        *,
        gamma=20,
        max_iter=1000,
        tol=1e-3,
    ):
            gamma=gamma,
            n_neighbors=n_neighbors,
            tol=tol,
            alpha=None,
        )

        """Matrix representing a fully connected graph between each sample

        This basic implementation creates a non-stochastic affinity matrix, so
        class distributions will exceed 1 (normalization may be desired).
        """
            self.nn_fit = None
        affinity_matrix = self._get_kernel(self.X_)
        normalizer = affinity_matrix.sum(axis=0)
        if sparse.issparse(affinity_matrix):
            affinity_matrix.data /= np.diag(np.array(normalizer))
        else:
            affinity_matrix /= normalizer[:, np.newaxis]
        return affinity_matrix

        Returns
        -------
        """
        return super().fit(X, y)

class LabelSpreading(BaseLabelPropagation):
    """LabelSpreading model for semi-supervised learning.

    This model is similar to the basic Label Propagation algorithm,
    but uses affinity matrix based on the normalized graph Laplacian
    and soft clamping across the labels.

      Parameter for rbf kernel.

      Parameter for knn kernel which is a strictly positive integer.

    alpha : float, default=0.2
      Clamping factor. A value in (0, 1) that specifies the relative amount
      that an instance should adopt the information from its neighbors as
      opposed to its initial label.
      alpha=0 means keeping the initial label information; alpha=1 means
      replacing all initial information.

    max_iter : int, default=30
      Maximum number of iterations allowed.

      Convergence tolerance: threshold to consider the system at steady
      state.

    X_ : ndarray of shape (n_samples, n_features)

    See Also
    --------
    LabelPropagation : Unregularized graph based semi-supervised learning.

    `Dengyong Zhou, Olivier Bousquet, Thomas Navin Lal, Jason Weston,
    Bernhard Schoelkopf. Learning with local and global consistency (2004)
    <https://citeseerx.ist.psu.edu/doc_view/pid/d74c37aabf2d5cae663007cbd8718175466aea8c>`_

    Examples
    --------
    >>> from sklearn.semi_supervised import LabelSpreading
    >>> label_prop_model = LabelSpreading()
    LabelSpreading(...)
    """

    _variant = "spreading"

    _parameter_constraints["alpha"] = [Interval(Real, 0, 1, closed="neither")]

        self,
        *,
        gamma=20,
        alpha=0.2,
        tol=1e-3,
    ):
        # this one has different base parameters
            tol=tol,
        )

        """Graph matrix for Label Spreading computes the graph laplacian"""
        # compute affinity matrix (or gram matrix)
        n_samples = self.X_.shape[0]
        laplacian = csgraph_laplacian(affinity_matrix, normed=True)
        laplacian = -laplacian
        if sparse.issparse(laplacian):
            diag_mask = laplacian.row == laplacian.col
            laplacian.data[diag_mask] = 0.0
        else:
            laplacian.flat[:: n_samples + 1] = 0.0  # set diag to 0.0
        return laplacian

        self,
        *,
        gamma=20,
        alpha=1,
        tol=1e-3,
    ):

            else:
                ).fit(X)
                )
            else:
            else:

        )

        Returns
        -------
        """

        Returns
        -------
        """

            self,
            X,
        )
                [
                ]
            )
        else:

        Returns
        -------
        """
            self,
            X,
            y,
        )
        self.X_ = X

        else:

                break

            )

                )
            else:
                # clamp
                )
        else:
            )

        self,
        *,
        gamma=20,
        tol=1e-3,
    ):
            tol=tol,
        )

        """
        else:

        Returns
        -------
        """

        self,
        *,
        gamma=20,
        tol=1e-3,
    ):
            tol=tol,
        )

        else:

def test_fit_transduction(global_dtype, Estimator, parameters):
    samples = np.asarray([[1.0, 0.0], [0.0, 2.0], [1.0, 3.0]], dtype=global_dtype)
    labels = [0, 1, -1]
    clf = Estimator(**parameters).fit(samples, labels)
    assert clf.transduction_[2] == 1

def test_distribution(global_dtype, Estimator, parameters):
    if parameters["kernel"] == "knn":
        pytest.skip(
            "Unstable test for this configuration: changes in k-NN ordering break it."
        )
    samples = np.asarray([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], dtype=global_dtype)
    assert_allclose(clf.label_distributions_[2], [0.5, 0.5], atol=1e-2)

def test_predict(global_dtype, Estimator, parameters):
    assert_array_equal(clf.predict([[0.5, 2.5]]), np.array([1]))

def test_predict_proba(global_dtype, Estimator, parameters):
    samples = np.asarray([[1.0, 0.0], [0.0, 1.0], [1.0, 2.5]], dtype=global_dtype)
    assert_allclose(clf.predict_proba([[1.0, 1.0]]), np.array([[0.5, 0.5]]))

def test_label_spreading_closed_form(global_dtype, Estimator, parameters, alpha):
    X, y = make_classification(n_classes=n_classes, n_samples=200, random_state=0)
    X = X.astype(global_dtype, copy=False)
    y[::3] = -1

    gamma = 0.1
    clf = label_propagation.LabelSpreading(gamma=gamma).fit(X, y)
    # adopting notation from Zhou et al (2004):
    S = clf._build_graph()
    Y = np.zeros((len(y), n_classes + 1), dtype=X.dtype)
    Y[np.arange(len(y)), y] = 1
    Y = Y[:, :-1]

    expected = np.dot(np.linalg.inv(np.eye(len(S), dtype=S.dtype) - alpha * S), Y)
    expected /= expected.sum(axis=1)[:, np.newaxis]

    clf = label_propagation.LabelSpreading(
        max_iter=100, alpha=alpha, tol=1e-10, gamma=gamma
    )

    assert_allclose(expected, clf.label_distributions_)

def test_label_propagation_closed_form(global_dtype):
    y[::3] = -1
    Y = np.zeros((len(y), n_classes + 1))
    unlabelled_idx = Y[:, (-1,)].nonzero()[0]
    labelled_idx = (Y[:, (-1,)] == 0).nonzero()[0]

    clf = label_propagation.LabelPropagation(max_iter=100, tol=1e-10, gamma=0.1)
    # adopting notation from Zhu et al 2002
    T_bar = clf._build_graph()
    Tuu = T_bar[tuple(np.meshgrid(unlabelled_idx, unlabelled_idx, indexing="ij"))]
    Tul = T_bar[tuple(np.meshgrid(unlabelled_idx, labelled_idx, indexing="ij"))]
    Y_l = Y[labelled_idx, :]
    Y_u = np.dot(np.dot(np.linalg.inv(np.eye(Tuu.shape[0]) - Tuu), Tul), Y_l)

    expected = Y.copy()
    expected[unlabelled_idx, :] = Y_u

    assert_allclose(expected, clf.label_distributions_, atol=1e-4)

def test_sparse_input_types(
    accepted_sparse_type, index_dtype, dtype, Estimator, parameters
):
    # This is non-regression test for #17085
    X = _convert_container([[1.0, 0.0], [0.0, 2.0], [1.0, 3.0]], accepted_sparse_type)
    X.data = X.data.astype(dtype, copy=False)
    X.indices = X.indices.astype(index_dtype, copy=False)
    X.indptr = X.indptr.astype(index_dtype, copy=False)
    clf = Estimator(**parameters).fit(X, labels)

def test_convergence_speed(constructor_type):
    # This is a non-regression test for #5774
    X = _convert_container([[1.0, 0.0], [0.0, 1.0], [1.0, 2.5]], constructor_type)
    y = np.array([0, 1, -1])
    mdl = label_propagation.LabelSpreading(kernel="rbf", max_iter=5000)
    mdl.fit(X, y)

    # this should converge quickly:
    assert mdl.n_iter_ < 10
    assert_array_equal(mdl.predict(X), [0, 1, 1])

def test_convergence_warning():
    X = np.array([[1.0, 0.0], [0.0, 1.0], [1.0, 2.5]])
    mdl = label_propagation.LabelSpreading(kernel="rbf", max_iter=1)
    warn_msg = "max_iter=1 was reached without convergence."
    with pytest.warns(ConvergenceWarning, match=warn_msg):
        mdl.fit(X, y)
    assert mdl.n_iter_ == mdl.max_iter

    mdl = label_propagation.LabelPropagation(kernel="rbf", max_iter=1)

    mdl = label_propagation.LabelSpreading(kernel="rbf", max_iter=500)
        warnings.simplefilter("error", ConvergenceWarning)

    mdl = label_propagation.LabelPropagation(kernel="rbf", max_iter=500)

def test_label_propagation_non_zero_normalizer(LabelPropagationCls):
    # check that we don't divide by zero in case of null normalizer
    # non-regression test for
    # https://github.com/scikit-learn/scikit-learn/pull/15946
    # https://github.com/scikit-learn/scikit-learn/issues/9292
    X = np.array([[100.0, 100.0], [100.0, 100.0], [0.0, 0.0], [0.0, 0.0]])
    y = np.array([0, 1, -1, -1])
    mdl = LabelPropagationCls(kernel="knn", max_iter=100, n_neighbors=1)
        warnings.simplefilter("error", RuntimeWarning)

def test_predict_sparse_callable_kernel(global_dtype):
    # This is a non-regression test for #15866

    # Custom sparse kernel (top-K RBF)
    def topk_rbf(X, Y=None, n_neighbors=10, gamma=1e-5):
        nn = NearestNeighbors(n_neighbors=10, metric="euclidean", n_jobs=2)
        nn.fit(X)
        W = -1 * nn.kneighbors_graph(Y, mode="distance").power(2) * gamma
        np.exp(W.data, out=W.data)
        assert issparse(W)
        return W.T

    n_classes = 4
    n_test = 10
        n_features=20,
        n_informative=20,
    )
    X = X.astype(global_dtype)

        X, y, test_size=n_test, random_state=0
    )

    model = label_propagation.LabelSpreading(kernel=topk_rbf)
    assert model.score(X_test, y_test) >= 0.9

    model = label_propagation.LabelPropagation(kernel=topk_rbf)

        nn.fit(X)
        return W.T

def test_warns_k_best():
    st = SelfTrainingClassifier(KNeighborsClassifier(), criterion="k_best", k_best=1000)
    with pytest.warns(UserWarning, match="k_best is larger than"):
        st.fit(X_train, y_train_missing_labels)

    assert st.termination_condition_ == "all_labeled"

def test_classification(estimator, selection_crit):
    # Also assert that predictions for strings and numerical labels are equal.
    # Also test for multioutput classification
    threshold = 0.75
    max_iter = 10
    st = SelfTrainingClassifier(
        estimator, max_iter=max_iter, threshold=threshold, criterion=selection_crit
    )
    st.fit(X_train, y_train_missing_labels)
    pred = st.predict(X_test)
    proba = st.predict_proba(X_test)

    st_string = SelfTrainingClassifier(
        estimator, max_iter=max_iter, criterion=selection_crit, threshold=threshold
    )
    st_string.fit(X_train, y_train_missing_strings)
    pred_string = st_string.predict(X_test)
    proba_string = st_string.predict_proba(X_test)

    assert_array_equal(np.vectorize(mapping.get)(pred), pred_string)
    assert_array_equal(proba, proba_string)

    assert st.termination_condition_ == st_string.termination_condition_
    # Check consistency between labeled_iter, n_iter and max_iter
    labeled = y_train_missing_labels != -1
    # assert that labeled samples have labeled_iter = 0
    assert_array_equal(st.labeled_iter_ == 0, labeled)
    # assert that labeled samples do not change label during training
    assert_array_equal(y_train_missing_labels[labeled], st.transduction_[labeled])

    # assert that the max of the iterations is less than the total amount of
    # iterations
    assert np.max(st.labeled_iter_) <= st.n_iter_ <= max_iter
    assert np.max(st_string.labeled_iter_) <= st_string.n_iter_ <= max_iter

    # check shapes
    assert st.labeled_iter_.shape == st.transduction_.shape
    assert st_string.labeled_iter_.shape == st_string.transduction_.shape

def test_k_best():
        KNeighborsClassifier(n_neighbors=1),
        criterion="k_best",
        max_iter=None,
    )
    y_train_only_one_label = np.copy(y_train)
    y_train_only_one_label[1:] = -1
    n_samples = y_train.shape[0]

    n_expected_iter = ceil((n_samples - 1) / 10)
    st.fit(X_train, y_train_only_one_label)
    assert st.n_iter_ == n_expected_iter

    # Check labeled_iter_
    assert np.sum(st.labeled_iter_ == 0) == 1
    for i in range(1, n_expected_iter):
        assert np.sum(st.labeled_iter_ == i) == 10
    assert np.sum(st.labeled_iter_ == n_expected_iter) == (n_samples - 1) % 10

def test_sanity_classification():
    estimator = SVC(gamma="scale", probability=True)
    estimator.fit(X_train[n_labeled_samples:], y_train[n_labeled_samples:])

    st = SelfTrainingClassifier(estimator)

    pred1, pred2 = estimator.predict(X_test), st.predict(X_test)
    assert not np.array_equal(pred1, pred2)
    score_supervised = accuracy_score(estimator.predict(X_test), y_test)
    score_self_training = accuracy_score(st.predict(X_test), y_test)

    assert score_self_training > score_supervised

def test_none_iter():
    # Check that the all samples were labeled after a 'reasonable' number of
    # iterations.
    st = SelfTrainingClassifier(KNeighborsClassifier(), threshold=0.55, max_iter=None)

    assert st.n_iter_ < 10

def test_zero_iterations(estimator, y):
    # Check classification for zero iterations.
    # Fitting a SelfTrainingClassifier with zero iterations should give the
    # same results as fitting a supervised classifier.
    # This also asserts that string arrays work as expected.

    clf1 = SelfTrainingClassifier(estimator, max_iter=0)

    clf1.fit(X_train, y)

    clf2 = estimator.fit(X_train[:n_labeled_samples], y[:n_labeled_samples])

    assert_array_equal(clf1.predict(X_test), clf2.predict(X_test))
    assert clf1.termination_condition_ == "max_iter"

def test_prefitted_throws_error():
    # Test that passing a pre-fitted classifier and calling predict throws an
    # error
    knn = KNeighborsClassifier()
    knn.fit(X_train, y_train)
    st = SelfTrainingClassifier(knn)
        NotFittedError,
        match="This SelfTrainingClassifier instance is not fitted yet",
    ):
        st.predict(X_train)

def test_labeled_iter(max_iter):
    # Check that the amount of datapoints labeled in iteration 0 is equal to
    # the amount of labeled datapoints we passed.
    st = SelfTrainingClassifier(KNeighborsClassifier(), max_iter=max_iter)

    amount_iter_0 = len(st.labeled_iter_[st.labeled_iter_ == 0])
    assert amount_iter_0 == n_labeled_samples
    # Check that the max of the iterations is less than the total amount of

def test_no_unlabeled():
    # Test that training on a fully labeled dataset produces the same results
    # as training the classifier by itself.
    with pytest.warns(UserWarning, match="y contains no unlabeled samples"):
        st.fit(X_train, y_train)
    assert_array_equal(knn.predict(X_test), st.predict(X_test))
    # Assert that all samples were labeled in iteration 0 (since there were no
    # unlabeled samples).
    assert np.all(st.labeled_iter_ == 0)

def test_early_stopping():
    svc = SVC(gamma="scale", probability=True)
    st = SelfTrainingClassifier(svc)
    X_train_easy = [[1], [0], [1], [0.5]]
    y_train_easy = [1, 0, -1, -1]
    # X = [[0.5]] cannot be predicted on with a high confidence, so training
    # stops early
    st.fit(X_train_easy, y_train_easy)
    assert st.n_iter_ == 1
    assert st.termination_condition_ == "no_change"

def test_strings_dtype():
    clf = SelfTrainingClassifier(KNeighborsClassifier())
    X, y = make_blobs(n_samples=30, random_state=0, cluster_std=0.1)
    labels_multiclass = ["one", "two", "three"]

    y_strings = np.take(labels_multiclass, y)

    with pytest.raises(ValueError, match="dtype"):
        clf.fit(X, y_strings)

def test_verbose(capsys, verbose):
    clf = SelfTrainingClassifier(KNeighborsClassifier(), verbose=verbose)
    clf.fit(X_train, y_train_missing_labels)

    captured = capsys.readouterr()

    if verbose:
        assert "iteration" in captured.out
    else:
        assert "iteration" not in captured.out

def test_verbose_k_best(capsys):
        verbose=True,
    )

    msg = "End of iteration {}, added {} new labels."
        assert msg.format(i, 10) in captured.out

    assert msg.format(n_expected_iter, (n_samples - 1) % 10) in captured.out

def test_k_best_selects_best():
    # Tests that the labels added by st really are the 10 best labels.
    svc = SVC(gamma="scale", probability=True, random_state=0)
    st = SelfTrainingClassifier(svc, criterion="k_best", max_iter=1, k_best=10)
    has_label = y_train_missing_labels != -1

    got_label = ~has_label & (st.transduction_ != -1)

    svc.fit(X_train[has_label], y_train_missing_labels[has_label])
    pred = svc.predict_proba(X_train[~has_label])
    max_proba = np.max(pred, axis=1)

    most_confident_svc = X_train[~has_label][np.argsort(max_proba)[-10:]]
    added_by_st = X_train[np.where(got_label)].tolist()

    for row in most_confident_svc.tolist():
        assert row in added_by_st

def test_estimator_meta_estimator():
    # Check that a meta-estimator relying on an estimator implementing
    # `predict_proba` will work even if it does not expose this method before being
    # fitted.
    # https://github.com/scikit-learn/scikit-learn/issues/19119

    estimator = StackingClassifier(
            ("svc_1", SVC(probability=True)),
            ("svc_2", SVC(probability=True)),
        ],
        final_estimator=SVC(probability=True),
        cv=2,
    )

    assert hasattr(estimator, "predict_proba")
    clf = SelfTrainingClassifier(estimator=estimator)

            ("svc_1", SVC(probability=False)),
            ("svc_2", SVC(probability=False)),
        ],
        final_estimator=SVC(probability=False),
        cv=2,
    )

    assert not hasattr(estimator, "predict_proba")
        clf.fit(X_train, y_train_missing_labels)

def test_self_training_estimator_attribute_error():
    """Check that we raise the proper AttributeErrors when the `estimator`
    does not implement the `predict_proba` method, which is called from within
    `fit`, or `decision_function`, which is decorated with `available_if`.

    """
    # `SVC` with `probability=False` does not implement 'predict_proba' that
    # is required internally in `fit` of `SelfTrainingClassifier`. We expect
    # an AttributeError to be raised.
    estimator = SVC(probability=False, gamma="scale")
    self_training = SelfTrainingClassifier(estimator)

    with pytest.raises(AttributeError, match="has no attribute 'predict_proba'"):
        self_training.fit(X_train, y_train_missing_labels)

    # `DecisionTreeClassifier` does not implement 'decision_function' and
    # should raise an AttributeError
    self_training = SelfTrainingClassifier(estimator=DecisionTreeClassifier())

    outer_msg = "This 'SelfTrainingClassifier' has no attribute 'decision_function'"
    inner_msg = "'DecisionTreeClassifier' object has no attribute 'decision_function'"
        self_training.fit(X_train, y_train_missing_labels).decision_function(X_train)

def test_deprecation_warning_base_estimator():
    warn_msg = "`base_estimator` has been deprecated in 1.6 and will be removed"
    with pytest.warns(FutureWarning, match=warn_msg):
        SelfTrainingClassifier(base_estimator=DecisionTreeClassifier()).fit(
            X_train, y_train_missing_labels
        )

    error_msg = "You must pass an estimator to SelfTrainingClassifier"
    with pytest.raises(ValueError, match=error_msg):
        SelfTrainingClassifier().fit(X_train, y_train_missing_labels)

    error_msg = "You must pass only one estimator to SelfTrainingClassifier."
        SelfTrainingClassifier(
            base_estimator=DecisionTreeClassifier(), estimator=DecisionTreeClassifier()
        ).fit(X_train, y_train_missing_labels)

def test_routing_passed_metadata_not_supported(method):
    est = SelfTrainingClassifier(estimator=SimpleEstimator())
    ):
        est.fit([[1], [1]], [1, 1], sample_weight=[1], prop="a")

    ):
        # make sure that the estimator thinks it is already fitted
        est.fitted_params_ = True
        getattr(est, method)([[1]], sample_weight=[1], prop="a")

def _load_coverage(F, header_length=6, dtype=np.int16):
    """Load a coverage file from an open file object.

    This will return a numpy array of the given dtype
    """
    header = [F.readline() for _ in range(header_length)]
    make_tuple = lambda t: (t.split()[0], float(t.split()[1]))
    header = dict([make_tuple(line) for line in header])

    M = np.loadtxt(F, dtype=dtype)
    nodata = int(header[b"NODATA_value"])
    if nodata != -9999:
        M[nodata] = -9999
    return M

def _load_csv(F):
    """Load csv file.

    F : file object
        CSV file open in byte mode.

    Returns
    -------
    rec : np.ndarray
        record array representing the data
    """
    names = F.readline().decode("ascii").strip().split(",")

    rec = np.loadtxt(F, skiprows=0, delimiter=",", dtype="S22,f4,f4")
    rec.dtype.names = names
    return rec

def construct_grids(batch):
    """Construct the map grid from the batch object

    batch : Batch object
        The object returned by :func:`fetch_species_distributions`

    Returns
    -------
    (xgrid, ygrid) : 1-D arrays
        The grid corresponding to the values in batch.coverages
    """
    # x,y coordinates for corner cells
    xmin = batch.x_left_lower_corner + batch.grid_size
    xmax = xmin + (batch.Nx * batch.grid_size)
    ymin = batch.y_left_lower_corner + batch.grid_size
    ymax = ymin + (batch.Ny * batch.grid_size)

    # x coordinates of the grid cells
    xgrid = np.arange(xmin, xmax, batch.grid_size)
    # y coordinates of the grid cells
    ygrid = np.arange(ymin, ymax, batch.grid_size)

    return (xgrid, ygrid)

def fetch_species_distributions(
    *,
    data_home=None,
    download_if_missing=True,
    n_retries=3,
    delay=1.0,
):
    """Loader for species distribution dataset from Phillips et. al. (2006).

    Read more in the :ref:`User Guide <species_distribution_dataset>`.

    data_home : str or path-like, default=None
        Specify another download and cache folder for the datasets. By default
        all scikit-learn data is stored in '~/scikit_learn_data' subfolders.

    download_if_missing : bool, default=True
        If False, raise an OSError if the data is not locally available
        instead of trying to download the data from the source site.

    n_retries : int, default=3
        Number of retries when HTTP errors are encountered.

    delay : float, default=1.0
        Number of seconds between retries.

    Returns
    -------
    data : :class:`~sklearn.utils.Bunch`

        coverages : array, shape = [14, 1592, 1212]
            These represent the 14 features measured
            at each point of the map grid.
            The latitude/longitude values for the grid are discussed below.
            Missing data is represented by the value -9999.
        train : record array, shape = (1624,)
            The training points for the data.  Each point has three fields:

            - train['species'] is the species name
            - train['dd long'] is the longitude, in degrees
            - train['dd lat'] is the latitude, in degrees
        test : record array, shape = (620,)
            The test points for the data.  Same format as the training data.
        Nx, Ny : integers
            The number of longitudes (x) and latitudes (y) in the grid
        x_left_lower_corner, y_left_lower_corner : floats
            The (x,y) position of the lower-left corner, in degrees
        grid_size : float
            The spacing between points of the grid, in degrees

    Notes
    -----

    This dataset represents the geographic distribution of species.
    The dataset is provided by Phillips et. al. (2006).

    The two species are:

    - `"Bradypus variegatus"
      <http://www.iucnredlist.org/details/3038/0>`_ ,
      the Brown-throated Sloth.

    - `"Microryzomys minutus"
      <http://www.iucnredlist.org/details/13408/0>`_ ,
      also known as the Forest Small Rice Rat, a rodent that lives in Peru,
      Colombia, Ecuador, Peru, and Venezuela.

    * `"Maximum entropy modeling of species geographic distributions"
      <http://rob.schapire.net/papers/ecolmod.pdf>`_
      S. J. Phillips, R. P. Anderson, R. E. Schapire - Ecological Modelling,
      190:231-259, 2006.

    Examples
    --------
    >>> from sklearn.datasets import fetch_species_distributions
    >>> species = fetch_species_distributions()
    >>> species.train[:5]
    array([(b'microryzomys_minutus', -64.7   , -17.85  ),
           (b'microryzomys_minutus', -67.8333, -16.3333),
           (b'microryzomys_minutus', -67.8833, -16.3   ),
           (b'microryzomys_minutus', -67.8   , -16.2667),
           (b'microryzomys_minutus', -67.9833, -15.9   )],
          dtype=[('species', 'S22'), ('dd long', '<f4'), ('dd lat', '<f4')])

    For a more extended example,
    see :ref:`sphx_glr_auto_examples_applications_plot_species_distribution_modeling.py`
    """
    data_home = get_data_home(data_home)
    if not exists(data_home):
        makedirs(data_home)

    # Define parameters for the data files.  These should not be changed
    # unless the data model changes.  They will be saved in the npz file
    # with the downloaded data.
    extra_params = dict(
        x_left_lower_corner=-94.8,
        Nx=1212,
        y_left_lower_corner=-56.05,
        Ny=1592,
        grid_size=0.05,
    )
    dtype = np.int16

    archive_path = _pkl_filepath(data_home, DATA_ARCHIVE_NAME)

    if not exists(archive_path):
        if not download_if_missing:
            raise OSError("Data not found and `download_if_missing` is False")
        logger.info("Downloading species data from %s to %s" % (SAMPLES.url, data_home))
        samples_path = _fetch_remote(
            SAMPLES, dirname=data_home, n_retries=n_retries, delay=delay
        )
        with np.load(samples_path) as X:  # samples.zip is a valid npz
            for f in X.files:
                fhandle = BytesIO(X[f])
                if "train" in f:
                    train = _load_csv(fhandle)
                if "test" in f:
                    test = _load_csv(fhandle)
        remove(samples_path)

        logger.info(
            "Downloading coverage data from %s to %s" % (COVERAGES.url, data_home)
        )
        coverages_path = _fetch_remote(
            COVERAGES, dirname=data_home, n_retries=n_retries, delay=delay
        )
        with np.load(coverages_path) as X:  # coverages.zip is a valid npz
            coverages = []
                logger.debug(" - converting {}".format(f))
                coverages.append(_load_coverage(fhandle))
            coverages = np.asarray(coverages, dtype=dtype)
        remove(coverages_path)

        bunch = Bunch(coverages=coverages, test=test, train=train, **extra_params)
        joblib.dump(bunch, archive_path, compress=9)
    else:
        bunch = joblib.load(archive_path)

    return bunch

def get_data_home(data_home=None) -> str:
    """Return the path of the scikit-learn data directory.

    This folder is used by some large dataset loaders to avoid downloading the
    data several times.

    By default the data directory is set to a folder named 'scikit_learn_data' in the
    user home folder.

    Alternatively, it can be set by the 'SCIKIT_LEARN_DATA' environment
    variable or programmatically by giving an explicit folder path. The '~'
    symbol is expanded to the user home folder.

    If the folder does not already exist, it is automatically created.

        The path to scikit-learn data directory. If `None`, the default path
        is `~/scikit_learn_data`.

    Returns
    -------
    data_home: str
        The path to scikit-learn data directory.

    Examples
    --------
    >>> import os
    >>> from sklearn.datasets import get_data_home
    >>> data_home_path = get_data_home()
    >>> os.path.exists(data_home_path)
    True
    """
    if data_home is None:
        data_home = environ.get("SCIKIT_LEARN_DATA", join("~", "scikit_learn_data"))
    data_home = expanduser(data_home)
    makedirs(data_home, exist_ok=True)
    return data_home

def clear_data_home(data_home=None):
    """Delete all the content of the data home cache.

    Examples
    --------
    >>> from sklearn.datasets import clear_data_home
    >>> clear_data_home()  # doctest: +SKIP
    """
    shutil.rmtree(data_home)

def _convert_data_dataframe(
    caller_name, data, target, feature_names, target_names, sparse_data=False
):
    pd = check_pandas_support("{} with as_frame=True".format(caller_name))
    if not sparse_data:
        data_df = pd.DataFrame(data, columns=feature_names, copy=False)
    else:
        data_df = pd.DataFrame.sparse.from_spmatrix(data, columns=feature_names)

    target_df = pd.DataFrame(target, columns=target_names)
    combined_df = pd.concat([data_df, target_df], axis=1)
    X = combined_df[feature_names]
    y = combined_df[target_names]
    if y.shape[1] == 1:
        y = y.iloc[:, 0]
    return combined_df, X, y

def load_files(
    container_path,
    *,
    description=None,
    categories=None,
    load_content=True,
    shuffle=True,
    encoding=None,
    decode_error="strict",
    random_state=0,
    allowed_extensions=None,
):
    """Load text files with categories as subfolder names.

    Individual samples are assumed to be files stored a two levels folder
    structure such as the following:

        container_folder/
            category_1_folder/
                file_1.txt
                file_2.txt
                ...
                file_42.txt
            category_2_folder/
                file_43.txt
                file_44.txt
                ...

    The folder names are used as supervised signal label names. The individual
    file names are not important.

    This function does not try to extract features into a numpy array or scipy
    sparse matrix. In addition, if load_content is false it does not try to
    load the files in memory.

    To use text files in a scikit-learn classification or clustering algorithm,
    you will need to use the :mod:`~sklearn.feature_extraction.text` module to
    build a feature extraction transformer that suits your problem.

    If you set load_content=True, you should also specify the encoding of the
    text using the 'encoding' parameter. For many modern text files, 'utf-8'
    will be the correct encoding. If you leave encoding equal to None, then the
    content will be made of bytes instead of Unicode, and you will not be able
    to use most functions in :mod:`~sklearn.feature_extraction.text`.

    Similar feature extractors should be built for other kind of unstructured
    data input such as images, audio, video, ...

    If you want files with a specific file extension (e.g. `.txt`) then you
    can pass a list of those file extensions to `allowed_extensions`.

    Read more in the :ref:`User Guide <datasets>`.

    container_path : str
        Path to the main folder holding one subfolder per category.

    description : str, default=None
        A paragraph describing the characteristic of the dataset: its source,
        reference, etc.

    categories : list of str, default=None
        If None (default), load all the categories. If not None, list of
        category names to load (other categories ignored).

    load_content : bool, default=True
        Whether to load or not the content of the different files. If true a
        'data' attribute containing the text information is present in the data
        structure returned. If not, a filenames attribute gives the path to the
        files.

    shuffle : bool, default=True
        Whether or not to shuffle the data: might be important for models that
        make the assumption that the samples are independent and identically
        distributed (i.i.d.), such as stochastic gradient descent.

    encoding : str, default=None
        If None, do not try to decode the content of the files (e.g. for images
        or other non-text content). If not None, encoding to use to decode text
        files to Unicode if load_content is True.

        contains characters not of the given `encoding`. Passed as keyword
        argument 'errors' to bytes.decode.

    random_state : int, RandomState instance or None, default=0
        Determines random number generation for dataset shuffling. Pass an int
        for reproducible output across multiple function calls.

    allowed_extensions : list of str, default=None
        List of desired file extensions to filter the files to be loaded.

    Returns
    -------

        data : list of str
            Only present when `load_content=True`.
            The raw text data to learn.
        target : ndarray
            The target labels (integer index).
        target_names : list
            The names of target classes.
        DESCR : str
            The full description of the dataset.
        filenames: ndarray
            The filenames holding the dataset.

    Examples
    --------
    >>> from sklearn.datasets import load_files
    >>> container_path = "./"
    >>> load_files(container_path)  # doctest: +SKIP
    """

    target = []
    target_names = []
    filenames = []

    folders = [
        f for f in sorted(listdir(container_path)) if isdir(join(container_path, f))
    ]

    if categories is not None:
        folders = [f for f in folders if f in categories]

    if allowed_extensions is not None:
        allowed_extensions = frozenset(allowed_extensions)

    for label, folder in enumerate(folders):
        target_names.append(folder)
        folder_path = join(container_path, folder)
        files = sorted(listdir(folder_path))
        if allowed_extensions is not None:
            documents = [
                join(folder_path, file)
                for file in files
                if os.path.splitext(file)[1] in allowed_extensions
            ]
        else:
            documents = [join(folder_path, file) for file in files]
        target.extend(len(documents) * [label])
        filenames.extend(documents)

    # convert to array for fancy indexing
    filenames = np.array(filenames)
    target = np.array(target)

    if shuffle:
        random_state = check_random_state(random_state)
        indices = np.arange(filenames.shape[0])
        random_state.shuffle(indices)
        filenames = filenames[indices]
        target = target[indices]

    if load_content:
        data = []
        for filename in filenames:
            data.append(Path(filename).read_bytes())
        if encoding is not None:
            data = [d.decode(encoding, decode_error) for d in data]
        return Bunch(
            data=data,
            filenames=filenames,
            target_names=target_names,
            target=target,
            DESCR=description,
        )

    return Bunch(
        filenames=filenames, target_names=target_names, target=target, DESCR=description
    )

def load_csv_data(
    data_file_name,
    *,
    data_module=DATA_MODULE,
    descr_file_name=None,
    descr_module=DESCR_MODULE,
    encoding="utf-8",
):
    """Loads `data_file_name` from `data_module with `importlib.resources`.

    data_file_name : str
        Name of csv file to be loaded from `data_module/data_file_name`.
        For example `'wine_data.csv'`.

    data_module : str or module, default='sklearn.datasets.data'
        Module where data lives. The default is `'sklearn.datasets.data'`.

    descr_file_name : str, default=None
        Name of rst file to be loaded from `descr_module/descr_file_name`.
        For example `'wine_data.rst'`. See also :func:`load_descr`.
        If not None, also returns the corresponding description of
        the dataset.

    descr_module : str or module, default='sklearn.datasets.descr'
        Module where `descr_file_name` lives. See also :func:`load_descr`.
        The default is `'sklearn.datasets.descr'`.

    Returns
    -------
    data : ndarray of shape (n_samples, n_features)
        A 2D array with each row representing one sample and each column
        representing the features of a given sample.

    target : ndarry of shape (n_samples,)
        A 1D array holding target variables for all the samples in `data`.
        For example target[0] is the target variable for data[0].

    target_names : ndarry of shape (n_samples,)
        A 1D array containing the names of the classifications. For example
        target_names[0] is the name of the target[0] class.

    descr : str, optional
        Description of the dataset (the content of `descr_file_name`).
        Only returned if `descr_file_name` is not None.

    encoding : str, optional
        Text encoding of the CSV file.

    """
    data_path = resources.files(data_module) / data_file_name
    with data_path.open("r", encoding="utf-8") as csv_file:
        data_file = csv.reader(csv_file)
        temp = next(data_file)
        n_samples = int(temp[0])
        n_features = int(temp[1])
        target_names = np.array(temp[2:])
        data = np.empty((n_samples, n_features))
        target = np.empty((n_samples,), dtype=int)

        for i, ir in enumerate(data_file):
            data[i] = np.asarray(ir[:-1], dtype=np.float64)
            target[i] = np.asarray(ir[-1], dtype=int)

    if descr_file_name is None:
        return data, target, target_names
    else:
        assert descr_module is not None
        descr = load_descr(descr_module=descr_module, descr_file_name=descr_file_name)
        return data, target, target_names, descr

def load_gzip_compressed_csv_data(
    *,
    **kwargs,
):
    """Loads gzip-compressed with `importlib.resources`.

    1) Open resource file with `importlib.resources.open_binary`
    2) Decompress file obj with `gzip.open`
    3) Load decompressed data with `np.loadtxt`

        Name of gzip-compressed csv file  (`'*.csv.gz'`) to be loaded from
        `data_module/data_file_name`. For example `'diabetes_data.csv.gz'`.

        The default  is `'sklearn.datasets.descr'`.

    encoding : str, default="utf-8"
        Name of the encoding that the gzip-decompressed file will be
        decoded with. The default is 'utf-8'.

    **kwargs : dict, optional
        Keyword arguments to be passed to `np.loadtxt`;
        e.g. delimiter=','.

    Returns
    -------
        representing the features and/or target of a given sample.

    """
    with data_path.open("rb") as compressed_file:
        compressed_file = gzip.open(compressed_file, mode="rt", encoding=encoding)
        data = np.loadtxt(compressed_file, **kwargs)

        return data
    else:
        return data, descr

def load_descr(descr_file_name, *, descr_module=DESCR_MODULE, encoding="utf-8"):
    """Load `descr_file_name` from `descr_module` with `importlib.resources`.

        Name of the encoding that `descr_file_name` will be decoded with.
        The default is 'utf-8'.

    Returns
    -------
    fdescr : str
        Content of `descr_file_name`.
    """
    path = resources.files(descr_module) / descr_file_name
    return path.read_text(encoding=encoding)

def load_wine(*, return_X_y=False, as_frame=False):
    """Load and return the wine dataset (classification).

    The wine dataset is a classic and very easy multi-class classification
    dataset.

    =================   ==============
    Classes                          3
    Samples per class        [59,71,48]
    Samples total                  178
    Dimensionality                  13
    Features            real, positive

    The copy of UCI ML Wine Data Set dataset is downloaded and modified to fit
    standard format from:
    https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data

    Read more in the :ref:`User Guide <wine_dataset>`.

    return_X_y : bool, default=False
        If True, returns ``(data, target)`` instead of a Bunch object.
        See below for more information about the `data` and `target` object.

    as_frame : bool, default=False
        If True, the data is a pandas DataFrame including columns with
        appropriate dtypes (numeric). The target is
        a pandas DataFrame or Series depending on the number of target columns.
        If `return_X_y` is True, then (`data`, `target`) will be pandas
        DataFrames or Series as described below.

    Returns
    -------

        data : {ndarray, dataframe} of shape (178, 13)
            The data matrix. If `as_frame=True`, `data` will be a pandas
            DataFrame.
        target: {ndarray, Series} of shape (178,)
            The classification target. If `as_frame=True`, `target` will be
            a pandas Series.
        feature_names: list
            The names of the dataset columns.
        target_names: list
        frame: DataFrame of shape (178, 14)
            Only present when `as_frame=True`. DataFrame with `data` and
            `target`.

        DESCR: str

    (data, target) : tuple if ``return_X_y`` is True
        A tuple of two ndarrays by default. The first contains a 2D array of shape
        (178, 13) with each row representing one sample and each column representing
        the features. The second array of shape (178,) contains the target samples.

    Examples
    --------
    Let's say you are interested in the samples 10, 80, and 140, and want to
    know their class name.

    >>> from sklearn.datasets import load_wine
    >>> data = load_wine()
    >>> data.target[[10, 80, 140]]
    array([0, 1, 2])
    >>> list(data.target_names)
    [np.str_('class_0'), np.str_('class_1'), np.str_('class_2')]
    """

    data, target, target_names, fdescr = load_csv_data(
        data_file_name="wine_data.csv", descr_file_name="wine_data.rst"
    )

    feature_names = [
        "alcohol",
        "malic_acid",
        "ash",
        "alcalinity_of_ash",
        "magnesium",
        "total_phenols",
        "flavanoids",
        "nonflavanoid_phenols",
        "proanthocyanins",
        "color_intensity",
        "hue",
        "od280/od315_of_diluted_wines",
        "proline",
    ]

    frame = None
    target_columns = [
        "target",
    ]
    if as_frame:
        frame, data, target = _convert_data_dataframe(
            "load_wine", data, target, feature_names, target_columns
        )

    if return_X_y:
        return data, target

        data=data,
        target=target,
        frame=frame,
        target_names=target_names,
        DESCR=fdescr,
        feature_names=feature_names,
    )

def load_iris(*, return_X_y=False, as_frame=False):
    """Load and return the iris dataset (classification).

    The iris dataset is a classic and very easy multi-class classification
    dataset.

    Classes                          3
    Samples per class               50
    Samples total                  150
    Dimensionality                   4

    Read more in the :ref:`User Guide <iris_dataset>`.

    .. versionchanged:: 0.20
        Fixed two wrong data points according to Fisher's paper.
        The new version is the same as in R, but not as in the UCI
        Machine Learning Repository.

        If True, returns ``(data, target)`` instead of a Bunch object. See
        below for more information about the `data` and `target` object.

    Returns
    -------

        data : {ndarray, dataframe} of shape (150, 4)
        target: {ndarray, Series} of shape (150,)
        target_names: ndarray of shape (3, )
        frame: DataFrame of shape (150, 5)
            `target`.

        DESCR: str
        filename: str
            The path to the location of the data.

        A tuple of two ndarray. The first containing a 2D array of shape
        (n_samples, n_features) with each row representing one sample and
        each column representing the features. The second ndarray of shape
        (n_samples,) containing the target samples.

    Examples
    --------
    Let's say you are interested in the samples 10, 25, and 50, and want to

    >>> data = load_iris()
    >>> data.target[[10, 25, 50]]
    array([0, 0, 1])
    [np.str_('setosa'), np.str_('versicolor'), np.str_('virginica')]

    See :ref:`sphx_glr_auto_examples_decomposition_plot_pca_iris.py` for a more
    detailed example of how to work with the iris dataset.
    """
    data_file_name = "iris.csv"
        data_file_name=data_file_name, descr_file_name="iris.rst"
    )

        "sepal length (cm)",
        "sepal width (cm)",
        "petal length (cm)",
        "petal width (cm)",
    ]

        "target",
    ]
            "load_iris", data, target, feature_names, target_columns
        )

        filename=data_file_name,
        data_module=DATA_MODULE,
    )

def load_breast_cancer(*, return_X_y=False, as_frame=False):
    """Load and return the breast cancer Wisconsin dataset (classification).

    The breast cancer dataset is a classic and very easy binary classification
    dataset.

    Classes                          2
    Samples per class    212(M),357(B)
    Samples total                  569
    Dimensionality                  30

    The copy of UCI ML Breast Cancer Wisconsin (Diagnostic) dataset is
    downloaded from:
    https://archive.ics.uci.edu/dataset/17/breast+cancer+wisconsin+diagnostic

    Read more in the :ref:`User Guide <breast_cancer_dataset>`.

    Returns
    -------

        data : {ndarray, dataframe} of shape (569, 30)
        target : {ndarray, Series} of shape (569,)
        feature_names : ndarray of shape (30,)
        target_names : ndarray of shape (2,)
        frame : DataFrame of shape (569, 31)
            `target`.

        DESCR : str
        filename : str

        A tuple of two ndarrays by default. The first contains a 2D ndarray of
        shape (569, 30) with each row representing one sample and each column
        representing the features. The second ndarray of shape (569,) contains
        the target samples.  If `as_frame=True`, both arrays are pandas objects,
        i.e. `X` a dataframe and `y` a series.

    Examples
    --------
    Let's say you are interested in the samples 10, 50, and 85, and want to

    >>> from sklearn.datasets import load_breast_cancer
    >>> data = load_breast_cancer()
    >>> data.target[[10, 50, 85]]
    array([0, 1, 0])
    [np.str_('malignant'), np.str_('benign')]
    """
    data_file_name = "breast_cancer.csv"
        data_file_name=data_file_name, descr_file_name="breast_cancer.rst"
    )

    feature_names = np.array(
        [
            "mean radius",
            "mean texture",
            "mean perimeter",
            "mean area",
            "mean smoothness",
            "mean compactness",
            "mean concavity",
            "mean concave points",
            "mean symmetry",
            "mean fractal dimension",
            "radius error",
            "texture error",
            "perimeter error",
            "area error",
            "smoothness error",
            "compactness error",
            "concavity error",
            "concave points error",
            "symmetry error",
            "fractal dimension error",
            "worst radius",
            "worst texture",
            "worst perimeter",
            "worst area",
            "worst smoothness",
            "worst compactness",
            "worst concavity",
            "worst concave points",
            "worst symmetry",
            "worst fractal dimension",
        ]
    )

        "target",
    ]
            "load_breast_cancer", data, target, feature_names, target_columns
        )

    )

def load_digits(*, n_class=10, return_X_y=False, as_frame=False):
    """Load and return the digits dataset (classification).

    Each datapoint is a 8x8 image of a digit.

    Classes                         10
    Samples per class             ~180
    Samples total                 1797
    Dimensionality                  64
    Features             integers 0-16

    This is a copy of the test set of the UCI ML hand-written digits datasets
    https://archive.ics.uci.edu/ml/datasets/Optical+Recognition+of+Handwritten+Digits

    Read more in the :ref:`User Guide <digits_dataset>`.

    n_class : int, default=10
        The number of classes to return. Between 0 and 10.

    Returns
    -------

        data : {ndarray, dataframe} of shape (1797, 64)
            The flattened data matrix. If `as_frame=True`, `data` will be
            a pandas DataFrame.
        target: {ndarray, Series} of shape (1797,)

        frame: DataFrame of shape (1797, 65)
            `target`.

        images: {ndarray} of shape (1797, 8, 8)
            The raw image data.
        DESCR: str

        shape (1797, 64) with each row representing one sample and each column
        representing the features. The second ndarray of shape (1797) contains

    Examples
    --------
    To load the data and visualize the images::

        >>> from sklearn.datasets import load_digits
        >>> digits = load_digits()
        >>> print(digits.data.shape)
        (1797, 64)
        >>> plt.matshow(digits.images[0], cmap="gray")
        <...>
    """

    data, fdescr = load_gzip_compressed_csv_data(
        data_file_name="digits.csv.gz", descr_file_name="digits.rst", delimiter=","
    )

    target = data[:, -1].astype(int, copy=False)
    flat_data = data[:, :-1]
    images = flat_data.view()
    images.shape = (-1, 8, 8)

    if n_class < 10:
        idx = target < n_class
        flat_data, target = flat_data[idx], target[idx]
        images = images[idx]

        "pixel_{}_{}".format(row_idx, col_idx)
        for row_idx in range(8)
        for col_idx in range(8)
    ]

        "target",
    ]
        frame, flat_data, target = _convert_data_dataframe(
            "load_digits", flat_data, target, feature_names, target_columns
        )

        return flat_data, target

        data=flat_data,
        target_names=np.arange(10),
        images=images,
    )

def load_diabetes(*, return_X_y=False, as_frame=False, scaled=True):
    """Load and return the diabetes dataset (regression).

    ==============   ==================
    Samples total    442
    Dimensionality   10
    Features         real, -.2 < x < .2
    Targets          integer 25 - 346

    .. note::
       The meaning of each feature (i.e. `feature_names`) might be unclear
       (especially for `ltg`) as the documentation of the original dataset is
       not explicit. We provide information that seems correct in regard with
       the scientific literature in this field of research.

    Read more in the :ref:`User Guide <diabetes_dataset>`.

    scaled : bool, default=True
        If True, the feature variables are mean centered and scaled by the
        standard deviation times the square root of `n_samples`.
        If False, raw data is returned for the feature variables.

    Returns
    -------

        data : {ndarray, dataframe} of shape (442, 10)
        target: {ndarray, Series} of shape (442,)
            The regression target. If `as_frame=True`, `target` will be
        frame: DataFrame of shape (442, 11)
            `target`.

        DESCR: str
        data_filename: str
        target_filename: str
            The path to the location of the target.

        Returns a tuple of two ndarray of shape (n_samples, n_features)

    Examples
    --------
    >>> diabetes = load_diabetes()
    >>> diabetes.target[:3]
    array([151.,  75., 141.])
    >>> diabetes.data.shape
    (442, 10)
    """
    data_filename = "diabetes_data_raw.csv.gz"
    target_filename = "diabetes_target.csv.gz"
    data = load_gzip_compressed_csv_data(data_filename)
    target = load_gzip_compressed_csv_data(target_filename)

    if scaled:
        data = scale(data, copy=False)
        data /= data.shape[0] ** 0.5

    fdescr = load_descr("diabetes.rst")

    feature_names = ["age", "sex", "bmi", "bp", "s1", "s2", "s3", "s4", "s5", "s6"]

        "target",
    ]
            "load_diabetes", data, target, feature_names, target_columns
        )

        data_filename=data_filename,
        target_filename=target_filename,
    )

def load_linnerud(*, return_X_y=False, as_frame=False):
    """Load and return the physical exercise Linnerud dataset.

    This dataset is suitable for multi-output regression tasks.

    ==============   ============================
    Samples total    20
    Dimensionality   3 (for both data and target)
    Features         integer
    Targets          integer

    Read more in the :ref:`User Guide <linnerrud_dataset>`.

        appropriate dtypes (numeric, string or categorical). The target is

    Returns
    -------

        data : {ndarray, dataframe} of shape (20, 3)
        target: {ndarray, dataframe} of shape (20, 3)
            The regression targets. If `as_frame=True`, `target` will be
            The names of the target columns.
        frame: DataFrame of shape (20, 6)
            `target`.

        DESCR: str

        Returns a tuple of two ndarrays or dataframe of shape
        `(20, 3)`. Each row represents one sample and each column represents the
        features in `X` and a target in `y` of a given sample.

    Examples
    --------
    >>> linnerud = load_linnerud()
    >>> linnerud.data.shape
    (20, 3)
    >>> linnerud.target.shape
    (20, 3)
    """
    data_filename = "linnerud_exercise.csv"
    target_filename = "linnerud_physiological.csv"

    data_module_path = resources.files(DATA_MODULE)
    # Read header and data
    data_path = data_module_path / data_filename
    with data_path.open("r", encoding="utf-8") as f:
        header_exercise = f.readline().split()
        f.seek(0)  # reset file obj
        data_exercise = np.loadtxt(f, skiprows=1)

    target_path = data_module_path / target_filename
    with target_path.open("r", encoding="utf-8") as f:
        header_physiological = f.readline().split()
        data_physiological = np.loadtxt(f, skiprows=1)

    fdescr = load_descr("linnerud.rst")

        (frame, data_exercise, data_physiological) = _convert_data_dataframe(
            "load_linnerud",
            data_exercise,
            data_physiological,
            header_exercise,
            header_physiological,
        )
        return data_exercise, data_physiological

        data=data_exercise,
        feature_names=header_exercise,
        target=data_physiological,
        target_names=header_physiological,
    )

def load_sample_images():
    """Load sample images for image manipulation.

    Loads both, ``china`` and ``flower``.

    Read more in the :ref:`User Guide <sample_images>`.

    Returns
    -------

        images : list of ndarray of shape (427, 640, 3)
            The two sample image.
        filenames : list
            The filenames for the images.
        DESCR : str

    Examples
    --------
    To load the data and visualize the images:

    >>> dataset = load_sample_images()     #doctest: +SKIP
    >>> len(dataset.images)                #doctest: +SKIP
    2
    >>> first_img_data = dataset.images[0] #doctest: +SKIP
    >>> first_img_data.shape               #doctest: +SKIP
    (427, 640, 3)
    >>> first_img_data.dtype               #doctest: +SKIP
    dtype('uint8')
    """
    try:
        from PIL import Image
        raise ImportError(
            "The Python Imaging Library (PIL) is required to load data "
            "from jpeg files. Please refer to "
            "https://pillow.readthedocs.io/en/stable/installation.html "
            "for installing PIL."
        )

    descr = load_descr("README.txt", descr_module=IMAGES_MODULE)

    filenames, images = [], []

    jpg_paths = sorted(
        resource
        for resource in resources.files(IMAGES_MODULE).iterdir()
        if resource.is_file() and resource.match("*.jpg")
    )

    for path in jpg_paths:
        filenames.append(str(path))
        with path.open("rb") as image_file:
            pil_image = Image.open(image_file)
            image = np.asarray(pil_image)
        images.append(image)

    return Bunch(images=images, filenames=filenames, DESCR=descr)

def load_sample_image(image_name):
    """Load the numpy array of a single sample image.

    image_name : {`china.jpg`, `flower.jpg`}
        The name of the sample image loaded.

    Returns
    -------
    img : 3D array
        The image as a numpy array: height x width x color.

    Examples
    --------

    >>> china = load_sample_image('china.jpg')   # doctest: +SKIP
    >>> china.dtype                              # doctest: +SKIP
    >>> china.shape                              # doctest: +SKIP
    >>> flower = load_sample_image('flower.jpg') # doctest: +SKIP
    >>> flower.dtype                             # doctest: +SKIP
    >>> flower.shape                             # doctest: +SKIP
    """
    images = load_sample_images()
    index = None
    for i, filename in enumerate(images.filenames):
        if filename.endswith(image_name):
            index = i
            break
    if index is None:
        raise AttributeError("Cannot find sample image: %s" % image_name)
    return images.images[index]

def _pkl_filepath(*args, **kwargs):
    """Return filename for Python 3 pickles

    args[-1] is expected to be the ".pkl" filename. For compatibility with
    older scikit-learn versions, a suffix is inserted before the extension.

    _pkl_filepath('/path/to/folder', 'filename.pkl') returns
    '/path/to/folder/filename_py3.pkl'

    """
    py3_suffix = kwargs.get("py3_suffix", "_py3")
    basename, ext = splitext(args[-1])
    basename += py3_suffix
    new_args = args[:-1] + (basename + ext,)
    return join(*new_args)

def _sha256(path):
    """Calculate the sha256 hash of the file at path."""
    sha256hash = hashlib.sha256()
    chunk_size = 8192
    with open(path, "rb") as f:
        while True:
            buffer = f.read(chunk_size)
            if not buffer:
                break
            sha256hash.update(buffer)
    return sha256hash.hexdigest()

def _fetch_remote(remote, dirname=None, n_retries=3, delay=1):
    """Helper function to download a remote dataset.

    Fetch a dataset pointed by remote's url, save into path using remote's
    filename and ensure its integrity based on the SHA256 checksum of the
    downloaded file.

        If the file already exists locally and the SHA256 checksums match, the
        path to the local file is returned without re-downloading.

    remote : RemoteFileMetadata
        Named tuple containing remote dataset meta information: url, filename
        and checksum.

    dirname : str or Path, default=None
        Directory to save the file to. If None, the current working directory
        is used.

    delay : int, default=1

    Returns
    -------
    file_path: Path
        Full path of the created file.
    """
    if dirname is None:
        folder_path = Path(".")
    else:
        folder_path = Path(dirname)

    file_path = folder_path / remote.filename

    if file_path.exists():
        if remote.checksum is None:
            return file_path

        checksum = _sha256(file_path)
        if checksum == remote.checksum:
        else:
                f"SHA256 checksum of existing local file {file_path.name} "
                f"({checksum}) differs from expected ({remote.checksum}): "
                f"re-downloading from {remote.url} ."
            )

    # We create a temporary file dedicated to this particular download to avoid
    # conflicts with parallel downloads. If the download is successful, the
    # temporary file is atomically renamed to the final file path (with
    # `shutil.move`). We therefore pass `delete=False` to `NamedTemporaryFile`.
    # Otherwise, garbage collecting temp_file would raise an error when
    # attempting to delete a file that was already renamed. If the download
    # fails or the result does not match the expected SHA256 digest, the
    # temporary file is removed manually in the except block.
    temp_file = NamedTemporaryFile(
        prefix=remote.filename + ".part_", dir=folder_path, delete=False
    )
    # Note that Python 3.12's `delete_on_close=True` is ignored as we set
    # `delete=False` explicitly. So after this line the empty temporary file still
    # exists on disk to make sure that it's uniquely reserved for this specific call of
    # `_fetch_remote` and therefore it protects against any corruption by parallel
    # calls.
    temp_file.close()
    try:
        temp_file_path = Path(temp_file.name)
            try:
                urlretrieve(remote.url, temp_file_path)
                break
            except (URLError, TimeoutError):
                if n_retries == 0:
                    # If no more retries are left, re-raise the caught exception.
                    raise
                warnings.warn(f"Retry downloading from url: {remote.url}")
                n_retries -= 1
                time.sleep(delay)

        checksum = _sha256(temp_file_path)
        if remote.checksum is not None and remote.checksum != checksum:
            raise OSError(
                f"The SHA256 checksum of {remote.filename} ({checksum}) "
                f"differs from expected ({remote.checksum})."
            )
    except (Exception, KeyboardInterrupt):
        os.unlink(temp_file.name)
        raise

    # The following renaming is atomic whenever temp_file_path and
    # file_path are on the same filesystem. This should be the case most of
    # the time, but we still use shutil.move instead of os.rename in case
    # they are not.
    shutil.move(temp_file_path, file_path)

    return file_path

def _filter_filename(value, filter_dots=True):
    """Derive a name that is safe to use as filename from the given string.

    Adapted from the `slugify` function of django:
    https://github.com/django/django/blob/master/django/utils/text.py

    Convert spaces or repeated dashes to single dashes. Replace characters that
    aren't alphanumerics, underscores, hyphens or dots by underscores. Convert
    to lowercase. Also strip leading and trailing whitespace, dashes, and
    underscores.
    """
    value = unicodedata.normalize("NFKD", value).lower()
    if filter_dots:
        value = re.sub(r"[^\w\s-]+", "_", value)
    else:
        value = re.sub(r"[^.\w\s-]+", "_", value)
    value = re.sub(r"[\s-]+", "-", value)
    return value.strip("-_.")

def _derive_folder_and_filename_from_url(url):
    parsed_url = urlparse(url)
    if not parsed_url.hostname:
        raise ValueError(f"Invalid URL: {url}")
    folder_components = [_filter_filename(parsed_url.hostname, filter_dots=False)]
    path = parsed_url.path

    if "/" in path:
        base_folder, raw_filename = path.rsplit("/", 1)

        base_folder = _filter_filename(base_folder)
        if base_folder:
            folder_components.append(base_folder)
    else:
        raw_filename = path

    filename = _filter_filename(raw_filename, filter_dots=False)
    if not filename:
        filename = "downloaded_file"

    return "/".join(folder_components), filename

def fetch_file(
    url, folder=None, local_filename=None, sha256=None, n_retries=3, delay=1
):
    """Fetch a file from the web if not already present in the local folder.

    If the file already exists locally (and the SHA256 checksums match when
    provided), the path to the local file is returned without re-downloading.

    url : str
        URL of the file to download.

    folder : str or Path, default=None
        Directory to save the file to. If None, the file is downloaded in a
        folder with a name derived from the URL host name and path under
        scikit-learn data home folder.

    local_filename : str, default=None
        Name of the file to save. If None, the filename is inferred from the
        URL.

    sha256 : str, default=None
        SHA256 checksum of the file. If None, no checksum is verified.

    Returns
    -------
    file_path : Path
        Full path of the downloaded file.
    """
    folder_from_url, filename_from_url = _derive_folder_and_filename_from_url(url)

    if local_filename is None:
        local_filename = filename_from_url

    if folder is None:
        folder = Path(get_data_home()) / folder_from_url
        makedirs(folder, exist_ok=True)

    remote_metadata = RemoteFileMetadata(
        filename=local_filename, url=url, checksum=sha256
    )
    return _fetch_remote(
        remote_metadata, dirname=folder, n_retries=n_retries, delay=delay
    )

def _split_sparse_columns(
    arff_data: ArffSparseDataType, include_columns: List
) -> ArffSparseDataType:
    """Obtains several columns from sparse ARFF representation. Additionally,
    the column indices are re-labelled, given the columns that are not
    included. (e.g., when including [1, 2, 3], the columns will be relabelled
    to [0, 1, 2]).

    arff_data : tuple
        A tuple of three lists of equal size; first list indicating the value,
        second the x coordinate and the third the y coordinate.

    include_columns : list
        A list of columns to include.

    Returns
    -------
    arff_data_new : tuple
        Subset of arff data with only the include columns indicated by the
        include_columns argument.
    """
    arff_data_new: ArffSparseDataType = (list(), list(), list())
    reindexed_columns = {
        column_idx: array_idx for array_idx, column_idx in enumerate(include_columns)
    }
    for val, row_idx, col_idx in zip(arff_data[0], arff_data[1], arff_data[2]):
        if col_idx in include_columns:
            arff_data_new[0].append(val)
            arff_data_new[1].append(row_idx)
            arff_data_new[2].append(reindexed_columns[col_idx])
    return arff_data_new

def _sparse_data_to_array(
) -> np.ndarray:
    # turns the sparse data back into an array (can't use toarray() function,
    # as this does only work on numeric data)
    num_obs = max(arff_data[1]) + 1
    y_shape = (num_obs, len(include_columns))
    }
    # TODO: improve for efficiency
    y = np.empty(y_shape, dtype=np.float64)
            y[row_idx, reindexed_columns[col_idx]] = val
    return y

def _post_process_frame(frame, feature_names, target_names):
    """Post process a dataframe to select the desired columns in `X` and `y`.

    frame : dataframe
        The dataframe to split into `X` and `y`.

    feature_names : list of str
        The list of feature names to populate `X`.

    target_names : list of str
        The list of target names to populate `y`.

    Returns
    -------
    X : dataframe
        The dataframe containing the features.

    y : {series, dataframe} or None
        The series or dataframe containing the target.
    """
    X = frame[feature_names]
    if len(target_names) >= 2:
        y = frame[target_names]
    elif len(target_names) == 1:
        y = frame[target_names[0]]
    else:
        y = None
    return X, y

def _liac_arff_parser(
    gzip_file,
    output_arrays_type,
    openml_columns_info,
    feature_names_to_select,
    target_names_to_select,
    shape=None,
):
    """ARFF parser using the LIAC-ARFF library coded purely in Python.

    This parser is quite slow but consumes a generator. Currently it is needed
    to parse sparse datasets. For dense datasets, it is recommended to instead
    use the pandas-based parser, although it does not always handles the
    dtypes exactly the same.

    gzip_file : GzipFile instance
        The file compressed to be read.

    output_arrays_type : {"numpy", "sparse", "pandas"}
        The type of the arrays that will be returned. The possibilities ara:

        - `"numpy"`: both `X` and `y` will be NumPy arrays;
        - `"sparse"`: `X` will be sparse matrix and `y` will be a NumPy array;
        - `"pandas"`: `X` will be a pandas DataFrame and `y` will be either a
          pandas Series or DataFrame.

    columns_info : dict
        The information provided by OpenML regarding the columns of the ARFF
        file.

    feature_names_to_select : list of str
        A list of the feature names to be selected.

    target_names_to_select : list of str
        A list of the target names to be selected.

    Returns
    -------
    X : {ndarray, sparse matrix, dataframe}
        The data matrix.

    y : {ndarray, dataframe, series}
        The target.

    frame : dataframe or None
        A dataframe containing both `X` and `y`. `None` if
        `output_array_type != "pandas"`.

    categories : list of str or None
        The names of the features that are categorical. `None` if
        `output_array_type == "pandas"`.
    """

    def _io_to_generator(gzip_file):
        for line in gzip_file:
            yield line.decode("utf-8")

    stream = _io_to_generator(gzip_file)

    # find which type (dense or sparse) ARFF type we will have to deal with
    return_type = _arff.COO if output_arrays_type == "sparse" else _arff.DENSE_GEN
    # we should not let LIAC-ARFF to encode the nominal attributes with NumPy
    # arrays to have only numerical values.
    encode_nominal = not (output_arrays_type == "pandas")
    arff_container = _arff.load(
        stream, return_type=return_type, encode_nominal=encode_nominal
    )
    columns_to_select = feature_names_to_select + target_names_to_select

    categories = {
        name: cat
        for name, cat in arff_container["attributes"]
        if isinstance(cat, list) and name in columns_to_select
    }
    if output_arrays_type == "pandas":
        pd = check_pandas_support("fetch_openml with as_frame=True")

        columns_info = OrderedDict(arff_container["attributes"])
        columns_names = list(columns_info.keys())

        # calculate chunksize
        first_row = next(arff_container["data"])
        first_df = pd.DataFrame([first_row], columns=columns_names, copy=False)

        row_bytes = first_df.memory_usage(deep=True).sum()
        chunksize = get_chunk_n_rows(row_bytes)

        # read arff data with chunks
        columns_to_keep = [col for col in columns_names if col in columns_to_select]
        dfs = [first_df[columns_to_keep]]
        for data in chunk_generator(arff_container["data"], chunksize):
            dfs.append(
                pd.DataFrame(data, columns=columns_names, copy=False)[columns_to_keep]
            )
        # dfs[0] contains only one row, which may not have enough data to infer to
        # column's dtype. Here we use `dfs[1]` to configure the dtype in dfs[0]
        if len(dfs) >= 2:
            dfs[0] = dfs[0].astype(dfs[1].dtypes)

        # liac-arff parser does not depend on NumPy and uses None to represent
        # missing values. To be consistent with the pandas parser, we replace
        # None with np.nan.
        frame = pd.concat(dfs, ignore_index=True)
        frame = pd_fillna(pd, frame)
        del dfs, first_df

        # cast the columns frame
        dtypes = {}
        for name in frame.columns:
            column_dtype = openml_columns_info[name]["data_type"]
            if column_dtype.lower() == "integer":
                # Use a pandas extension array instead of np.int64 to be able
                # to support missing values.
                dtypes[name] = "Int64"
            elif column_dtype.lower() == "nominal":
                dtypes[name] = "category"
            else:
                dtypes[name] = frame.dtypes[name]
        frame = frame.astype(dtypes)

        X, y = _post_process_frame(
            frame, feature_names_to_select, target_names_to_select
        )
    else:
        arff_data = arff_container["data"]

        feature_indices_to_select = [
            int(openml_columns_info[col_name]["index"])
            for col_name in feature_names_to_select
        ]
        target_indices_to_select = [
            for col_name in target_names_to_select
        ]

        if isinstance(arff_data, Generator):
            if shape is None:
                    "shape must be provided when arr['data'] is a Generator"
                )
            if shape[0] == -1:
                count = -1
            else:
                count = shape[0] * shape[1]
            data = np.fromiter(
                itertools.chain.from_iterable(arff_data),
                dtype="float64",
                count=count,
            )
            data = data.reshape(*shape)
            X = data[:, feature_indices_to_select]
            y = data[:, target_indices_to_select]
        elif isinstance(arff_data, tuple):
            arff_data_X = _split_sparse_columns(arff_data, feature_indices_to_select)
            num_obs = max(arff_data[1]) + 1
            X_shape = (num_obs, len(feature_indices_to_select))
            X = sp.sparse.coo_matrix(
                (arff_data_X[0], (arff_data_X[1], arff_data_X[2])),
                shape=X_shape,
                dtype=np.float64,
            )
            y = _sparse_data_to_array(arff_data, target_indices_to_select)
        else:
            # This should never happen
                f"Unexpected type for data obtained from arff: {type(arff_data)}"
            )

        is_classification = {
            col_name in categories for col_name in target_names_to_select
        }
        if not is_classification:
            # No target
            pass
        elif all(is_classification):
            y = np.hstack(
                [
                    np.take(
                        np.asarray(categories.pop(col_name), dtype="O"),
                        y[:, i : i + 1].astype(int, copy=False),
                    )
                    for i, col_name in enumerate(target_names_to_select)
                ]
            )
        elif any(is_classification):
                "Mix of nominal and non-nominal targets is not currently supported"
            )

        # reshape y back to 1-D array, if there is only 1 target column;
        # back to None if there are not target columns
        if y.shape[1] == 1:
            y = y.reshape((-1,))
        elif y.shape[1] == 0:
            y = None

        return X, y, frame, None
    return X, y, None, categories

def _pandas_arff_parser(
    read_csv_kwargs=None,
):
    """ARFF parser using `pandas.read_csv`.

    This parser uses the metadata fetched directly from OpenML and skips the metadata
    headers of ARFF file itself. The data is loaded as a CSV file.

        The GZip compressed file with the ARFF formatted payload.

        The type of the arrays that will be returned. The possibilities are:

    openml_columns_info : dict
        file.

        A list of the feature names to be selected to build `X`.

        A list of the target names to be selected to build `y`.

    read_csv_kwargs : dict, default=None
        Keyword arguments to pass to `pandas.read_csv`. It allows to overwrite
        the default options.

    Returns
    -------

    """
    import pandas as pd

    # read the file until the data section to skip the ARFF metadata headers
    for line in gzip_file:
        if line.decode("utf-8").lower().startswith("@data"):
            break

    dtypes = {}
    for name in openml_columns_info:
        column_dtype = openml_columns_info[name]["data_type"]
        if column_dtype.lower() == "integer":
            # Use Int64 to infer missing values from data
            # XXX: this line is not covered by our tests. Is this really needed?
            dtypes[name] = "Int64"
        elif column_dtype.lower() == "nominal":
            dtypes[name] = "category"
    # since we will not pass `names` when reading the ARFF file, we need to translate
    # `dtypes` from column names to column indices to pass to `pandas.read_csv`
    dtypes_positional = {
        col_idx: dtypes[name]
        for col_idx, name in enumerate(openml_columns_info)
        if name in dtypes
    }

    default_read_csv_kwargs = {
        "header": None,
        "index_col": False,  # always force pandas to not use the first column as index
        "na_values": ["?"],  # missing values are represented by `?`
        "keep_default_na": False,  # only `?` is a missing value given the ARFF specs
        "comment": "%",  # skip line starting by `%` since they are comments
        "quotechar": '"',  # delimiter to use for quoted strings
        "skipinitialspace": True,  # skip spaces after delimiter to follow ARFF specs
        "escapechar": "\\",
        "dtype": dtypes_positional,
    }
    read_csv_kwargs = {**default_read_csv_kwargs, **(read_csv_kwargs or {})}
    frame = pd.read_csv(gzip_file, **read_csv_kwargs)
    try:
        # Setting the columns while reading the file will select the N first columns
        # and not raise a ParserError. Instead, we set the columns after reading the
        # file and raise a ParserError if the number of columns does not match the
        # number of columns in the metadata given by OpenML.
        frame.columns = [name for name in openml_columns_info]
    except ValueError as exc:
        raise pd.errors.ParserError(
            "The number of columns provided by OpenML does not match the number of "
            "columns inferred by pandas when reading the file."
        ) from exc

    columns_to_keep = [col for col in frame.columns if col in columns_to_select]
    frame = frame[columns_to_keep]

    # `pd.read_csv` automatically handles double quotes for quoting non-numeric
    # CSV cell values. Contrary to LIAC-ARFF, `pd.read_csv` cannot be configured to
    # consider either single quotes and double quotes as valid quoting chars at
    # the same time since this case does not occur in regular (non-ARFF) CSV files.
    # To mimic the behavior of LIAC-ARFF parser, we manually strip single quotes
    # on categories as a post-processing steps if needed.
    #
    # Note however that we intentionally do not attempt to do this kind of manual
    # post-processing of (non-categorical) string-typed columns because we cannot
    # resolve the ambiguity of the case of CSV cell with nesting quoting such as
    # `"'some string value'"` with pandas.
    single_quote_pattern = re.compile(r"^'(?P<contents>.*)'$")

    def strip_single_quotes(input_string):
        match = re.search(single_quote_pattern, input_string)
        if match is None:
            return input_string

        return match.group("contents")

    categorical_columns = [
        name
        for name, dtype in frame.dtypes.items()
        if isinstance(dtype, pd.CategoricalDtype)
    ]
    for col in categorical_columns:
        frame[col] = frame[col].cat.rename_categories(strip_single_quotes)

    X, y = _post_process_frame(frame, feature_names_to_select, target_names_to_select)

    else:
        X, y = X.to_numpy(), y.to_numpy()

        name: dtype.categories.tolist()
    }

def load_arff_from_gzip_file(
    parser,
    output_type,
):
    """Load a compressed ARFF file using a given parser.

    parser : {"pandas", "liac-arff"}
        The parser used to parse the ARFF file. "pandas" is recommended
        but only supports loading dense datasets.

    output_type : {"numpy", "sparse", "pandas"}

        file.

    Returns
    -------

    """
    if parser == "liac-arff":
        return _liac_arff_parser(
            gzip_file,
            output_type,
            openml_columns_info,
            feature_names_to_select,
            target_names_to_select,
            shape,
        )
    elif parser == "pandas":
        return _pandas_arff_parser(
            read_csv_kwargs,
        )
    else:
            f"Unknown parser: '{parser}'. Should be 'liac-arff' or 'pandas'."
        )

def _check_fetch_lfw(
    data_home=None, funneled=True, download_if_missing=True, n_retries=3, delay=1.0
):
    """Helper function to download any missing LFW data"""

    data_home = get_data_home(data_home=data_home)
    lfw_home = join(data_home, "lfw_home")

    if not exists(lfw_home):
        makedirs(lfw_home)

    for target in TARGETS:
        target_filepath = join(lfw_home, target.filename)
        if not exists(target_filepath):
            if download_if_missing:
                logger.info("Downloading LFW metadata: %s", target.url)
                _fetch_remote(
                    target, dirname=lfw_home, n_retries=n_retries, delay=delay
                )
            else:
                raise OSError("%s is missing" % target_filepath)

    if funneled:
        data_folder_path = join(lfw_home, "lfw_funneled")
        archive = FUNNELED_ARCHIVE
    else:
        data_folder_path = join(lfw_home, "lfw")
        archive = ARCHIVE

    if not exists(data_folder_path):
        archive_path = join(lfw_home, archive.filename)
        if not exists(archive_path):
                logger.info("Downloading LFW data (~200MB): %s", archive.url)
                    archive, dirname=lfw_home, n_retries=n_retries, delay=delay
                )
            else:
                raise OSError("%s is missing" % archive_path)

        import tarfile

        logger.debug("Decompressing the data archive to %s", data_folder_path)
        with tarfile.open(archive_path, "r:gz") as fp:
            # Use filter="data" to prevent the most dangerous security issues.
            # For more details, see
            # https://docs.python.org/3.9/library/tarfile.html#tarfile.TarFile.extractall
            fp.extractall(path=lfw_home, filter="data")

        remove(archive_path)

    return lfw_home, data_folder_path

def _load_imgs(file_paths, slice_, color, resize):
    """Internally used to load images"""
    try:
        )

    # compute the portion of the images to load to respect the slice_ parameter
    # given by the caller
    default_slice = (slice(0, 250), slice(0, 250))
    if slice_ is None:
        slice_ = default_slice
    else:
        slice_ = tuple(s or ds for s, ds in zip(slice_, default_slice))

    h_slice, w_slice = slice_
    h = (h_slice.stop - h_slice.start) // (h_slice.step or 1)
    w = (w_slice.stop - w_slice.start) // (w_slice.step or 1)

    if resize is not None:
        resize = float(resize)
        h = int(resize * h)
        w = int(resize * w)

    # allocate some contiguous memory to host the decoded image slices
    n_faces = len(file_paths)
    if not color:
        faces = np.zeros((n_faces, h, w), dtype=np.float32)
    else:
        faces = np.zeros((n_faces, h, w, 3), dtype=np.float32)

    # iterate over the collected file path to load the jpeg files as numpy
    # arrays
    for i, file_path in enumerate(file_paths):
        if i % 1000 == 0:
            logger.debug("Loading face #%05d / %05d", i + 1, n_faces)

        # Checks if jpeg reading worked. Refer to issue #3594 for more
        # details.
        pil_img = Image.open(file_path)
        pil_img = pil_img.crop(
            (w_slice.start, h_slice.start, w_slice.stop, h_slice.stop)
        )
        if resize is not None:
            pil_img = pil_img.resize((w, h))
        face = np.asarray(pil_img, dtype=np.float32)

        if face.ndim == 0:
                "Failed to read the image file %s, "
                "Please make sure that libjpeg is installed" % file_path
            )

        face /= 255.0  # scale uint8 coded colors to the [0.0, 1.0] floats
        if not color:
            # average the color channels to compute a gray levels
            # representation
            face = face.mean(axis=2)

        faces[i, ...] = face

    return faces

def _fetch_lfw_people(
    data_folder_path, slice_=None, color=False, resize=None, min_faces_per_person=0
):
    """Perform the actual data loading for the lfw people dataset

    This operation is meant to be cached by a joblib wrapper.
    """
    # scan the data folder content to retain people with more that
    # `min_faces_per_person` face pictures
    person_names, file_paths = [], []
    for person_name in sorted(listdir(data_folder_path)):
        folder_path = join(data_folder_path, person_name)
        if not isdir(folder_path):
            continue
        paths = [join(folder_path, f) for f in sorted(listdir(folder_path))]
        n_pictures = len(paths)
        if n_pictures >= min_faces_per_person:
            person_name = person_name.replace("_", " ")
            person_names.extend([person_name] * n_pictures)
            file_paths.extend(paths)

    if n_faces == 0:
            "min_faces_per_person=%d is too restrictive" % min_faces_per_person
        )

    target_names = np.unique(person_names)
    target = np.searchsorted(target_names, person_names)

    faces = _load_imgs(file_paths, slice_, color, resize)

    # shuffle the faces with a deterministic RNG scheme to avoid having
    # all faces of the same person in a row, as it would break some
    # cross validation and learning algorithms such as SGD and online
    # k-means that make an IID assumption

    indices = np.arange(n_faces)
    np.random.RandomState(42).shuffle(indices)
    faces, target = faces[indices], target[indices]
    return faces, target, target_names

def fetch_lfw_people(
    *,
    funneled=True,
    resize=0.5,
    min_faces_per_person=0,
    color=False,
    slice_=(slice(70, 195), slice(78, 172)),
    return_X_y=False,
):
    """Load the Labeled Faces in the Wild (LFW) people dataset \
(classification).

    Download it if necessary.

    =================   =======================
    Classes                                5749
    Samples total                         13233
    Dimensionality                         5828
    Features            real, between 0 and 255

    For a usage example of this dataset, see
    :ref:`sphx_glr_auto_examples_applications_plot_face_recognition.py`.

    Read more in the :ref:`User Guide <labeled_faces_in_the_wild_dataset>`.

    funneled : bool, default=True
        Download and use the funneled variant of the dataset.

    resize : float or None, default=0.5
        Ratio used to resize the each face picture. If `None`, no resizing is
        performed.

    min_faces_per_person : int, default=None
        The extracted dataset will only retain pictures of people that have at
        least `min_faces_per_person` different pictures.

    color : bool, default=False
        Keep the 3 RGB channels instead of averaging them to a single
        gray level channel. If color is True the shape of the data has
        one more dimension than the shape with color = False.

    slice_ : tuple of slice, default=(slice(70, 195), slice(78, 172))
        Provide a custom 2D slice (height, width) to extract the
        'interesting' part of the jpeg files and avoid use statistical
        correlation from the background.

        If True, returns ``(dataset.data, dataset.target)`` instead of a Bunch
        object. See below for more information about the `dataset.data` and
        `dataset.target` object.

    Returns
    -------
    dataset : :class:`~sklearn.utils.Bunch`

        data : numpy array of shape (13233, 2914)
            Each row corresponds to a ravelled face image
            of original size 62 x 47 pixels.
            Changing the ``slice_`` or resize parameters will change the
            shape of the output.
        images : numpy array of shape (13233, 62, 47)
            Each row is a face image corresponding to one of the 5749 people in
            the dataset. Changing the ``slice_``
            or resize parameters will change the shape of the output.
        target : numpy array of shape (13233,)
            Labels associated to each face image.
            Those labels range from 0-5748 and correspond to the person IDs.
        target_names : numpy array of shape (5749,)
            Names of all persons in the dataset.
            Position in array corresponds to the person ID in the target array.
        DESCR : str
            Description of the Labeled Faces in the Wild (LFW) dataset.

        A tuple of two ndarray. The first containing a 2D array of
        shape (n_samples, n_features) with each row representing one
        sample and each column representing the features. The second
        ndarray of shape (n_samples,) containing the target samples.

    Examples
    --------
    >>> from sklearn.datasets import fetch_lfw_people
    >>> lfw_people = fetch_lfw_people()
    >>> lfw_people.data.shape
    (13233, 2914)
    >>> lfw_people.target.shape
    (13233,)
    >>> for name in lfw_people.target_names[:5]:
    ...    print(name)
    AJ Cook
    AJ Lamas
    Aaron Eckhart
    Aaron Guiel
    Aaron Patterson
    """
    lfw_home, data_folder_path = _check_fetch_lfw(
        data_home=data_home,
        funneled=funneled,
        download_if_missing=download_if_missing,
        n_retries=n_retries,
        delay=delay,
    )
    logger.debug("Loading LFW people faces from %s", lfw_home)

    # wrap the loader in a memoizing function that will return memmaped data
    # arrays for optimal memory usage
    m = Memory(location=lfw_home, compress=6, verbose=0)
    load_func = m.cache(_fetch_lfw_people)

    # load and memoize the pairs as np arrays
    faces, target, target_names = load_func(
        data_folder_path,
        resize=resize,
        min_faces_per_person=min_faces_per_person,
        color=color,
        slice_=slice_,
    )

    X = faces.reshape(len(faces), -1)

    fdescr = load_descr("lfw.rst")

        return X, target

    # pack the results as a Bunch instance
        data=X, images=faces, target=target, target_names=target_names, DESCR=fdescr
    )

def _fetch_lfw_pairs(
    index_file_path, data_folder_path, slice_=None, color=False, resize=None
):
    """Perform the actual data loading for the LFW pairs dataset

    """
    # parse the index file to find the number of pairs to be able to allocate
    # the right amount of memory before starting to decode the jpeg files
    with open(index_file_path, "rb") as index_file:
        split_lines = [ln.decode().strip().split("\t") for ln in index_file]
    pair_specs = [sl for sl in split_lines if len(sl) > 2]
    n_pairs = len(pair_specs)

    # iterating over the metadata lines for each pair to find the filename to
    # decode and load in memory
    target = np.zeros(n_pairs, dtype=int)
    file_paths = list()
    for i, components in enumerate(pair_specs):
        if len(components) == 3:
            target[i] = 1
            pair = (
                (components[0], int(components[1]) - 1),
                (components[0], int(components[2]) - 1),
            )
        elif len(components) == 4:
            target[i] = 0
            pair = (
                (components[2], int(components[3]) - 1),
            )
        else:
            raise ValueError("invalid line %d: %r" % (i + 1, components))
        for j, (name, idx) in enumerate(pair):
            try:
                person_folder = join(data_folder_path, name)
            except TypeError:
                person_folder = join(data_folder_path, str(name, "UTF-8"))
            filenames = list(sorted(listdir(person_folder)))
            file_path = join(person_folder, filenames[idx])
            file_paths.append(file_path)

    pairs = _load_imgs(file_paths, slice_, color, resize)
    shape = list(pairs.shape)
    n_faces = shape.pop(0)
    shape.insert(0, 2)
    shape.insert(0, n_faces // 2)
    pairs.shape = shape

    return pairs, target, np.array(["Different persons", "Same person"])

def fetch_lfw_pairs(
    *,
    subset="train",
):
    """Load the Labeled Faces in the Wild (LFW) pairs dataset (classification).

    Classes                                   2

    In the `original paper <https://people.cs.umass.edu/~elm/papers/lfw.pdf>`_
    the "pairs" version corresponds to the "restricted task", where
    the experimenter should not use the name of a person to infer
    the equivalence or non-equivalence of two face images that
    are not explicitly given in the training set.

    The original images are 250 x 250 pixels, but the default slice and resize
    arguments reduce them to 62 x 47.

    subset : {'train', 'test', '10_folds'}, default='train'
        Select the dataset to load: 'train' for the development training
        set, 'test' for the development test set, and '10_folds' for the
        official evaluation set that is meant to be used with a 10-folds
        cross validation.

        Specify another download and cache folder for the datasets. By
        default all scikit-learn data is stored in '~/scikit_learn_data'
        subfolders.

    resize : float, default=0.5
        Ratio used to resize the each face picture.

    Returns
    -------

        data : ndarray of shape (2200, 5828). Shape depends on ``subset``.
            Each row corresponds to 2 ravel'd face images
            Changing the ``slice_``, ``resize`` or ``subset`` parameters
            will change the shape of the output.
        pairs : ndarray of shape (2200, 2, 62, 47). Shape depends on ``subset``
            Each row has 2 face images corresponding
            to same or different person from the dataset
            containing 5749 people. Changing the ``slice_``,
            ``resize`` or ``subset`` parameters will change the shape of the
            output.
        target : numpy array of shape (2200,). Shape depends on ``subset``.
            Labels associated to each pair of images.
            The two label values being different persons or the same person.
        target_names : numpy array of shape (2,)
            Explains the target values of the target array.
            0 corresponds to "Different person", 1 corresponds to "same person".
        DESCR : str

    Examples
    --------
    >>> from sklearn.datasets import fetch_lfw_pairs
    >>> lfw_pairs_train = fetch_lfw_pairs(subset='train')
    >>> list(lfw_pairs_train.target_names)
    [np.str_('Different persons'), np.str_('Same person')]
    >>> lfw_pairs_train.pairs.shape
    (2200, 2, 62, 47)
    >>> lfw_pairs_train.data.shape
    (2200, 5828)
    >>> lfw_pairs_train.target.shape
    (2200,)
    """
    )
    logger.debug("Loading %s LFW pairs from %s", subset, lfw_home)

    load_func = m.cache(_fetch_lfw_pairs)

    # select the right metadata file according to the requested subset
    label_filenames = {
        "train": "pairsDevTrain.txt",
        "test": "pairsDevTest.txt",
        "10_folds": "pairs.txt",
    }
    if subset not in label_filenames:
            "subset='%s' is invalid: should be one of %r"
            % (subset, list(sorted(label_filenames.keys())))
        )
    index_file_path = join(lfw_home, label_filenames[subset])

    pairs, target, target_names = load_func(
        index_file_path, data_folder_path, resize=resize, color=color, slice_=slice_
    )

        data=pairs.reshape(len(pairs), -1),
        pairs=pairs,
    )

def load_svmlight_file(
    f,
    *,
    n_features=None,
    dtype=np.float64,
    multilabel=False,
    zero_based="auto",
    query_id=False,
    offset=0,
    length=-1,
):
    """Load datasets in the svmlight / libsvm format into sparse CSR matrix.

    This format is a text-based format, with one sample per line. It does
    not store zero valued features hence is suitable for sparse dataset.

    The first element of each line can be used to store a target variable
    to predict.

    This format is used as the default format for both svmlight and the
    libsvm command line programs.

    Parsing a text based source can be expensive. When repeatedly
    working on the same dataset, it is recommended to wrap this
    loader with joblib.Memory.cache to store a memmapped backup of the
    CSR results of the first call and benefit from the near instantaneous
    loading of memmapped structures for the subsequent calls.

    In case the file contains a pairwise preference constraint (known
    as "qid" in the svmlight format) these are ignored unless the
    query_id parameter is set to True. These pairwise preference
    constraints can be used to constraint the combination of samples
    when using pairwise loss functions (as is the case in some
    learning to rank problems) so that only pairs with the same
    query_id value are considered.

    This implementation is written in Cython and is reasonably fast.
    However, a faster API-compatible loader is also available at:
    https://github.com/mblondel/svmlight-loader

    f : str, path-like, file-like or int
        (Path to) a file to load. If a path ends in ".gz" or ".bz2", it will
        be uncompressed on the fly. If an integer is passed, it is assumed to
        be a file descriptor. A file-like or file descriptor will not be closed
        by this function. A file-like object must be opened in binary mode.

           Path-like objects are now accepted.

    n_features : int, default=None
        The number of features to use. If None, it will be inferred. This
        argument is useful to load several files that are subsets of a
        bigger sliced dataset: each subset might not have examples of
        every feature, hence the inferred shape might vary from one
        slice to another.
        n_features is only required if ``offset`` or ``length`` are passed a
        non-default value.

    dtype : numpy data type, default=np.float64
        Data type of dataset to be loaded. This will be the data type of the
        output numpy arrays ``X`` and ``y``.

    multilabel : bool, default=False
        Samples may have several labels each (see
        https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multilabel.html).

    zero_based : bool or "auto", default="auto"
        Whether column indices in f are zero-based (True) or one-based
        (False). If column indices are one-based, they are transformed to
        zero-based to match Python/NumPy conventions.
        If set to "auto", a heuristic check is applied to determine this from
        the file contents. Both kinds of files occur "in the wild", but they
        are unfortunately not self-identifying. Using "auto" or True should
        always be safe when no ``offset`` or ``length`` is passed.
        If ``offset`` or ``length`` are passed, the "auto" mode falls back
        to ``zero_based=True`` to avoid having the heuristic check yield
        inconsistent results on different segments of the file.

    query_id : bool, default=False
        If True, will return the query_id array for each file.

    offset : int, default=0
        Ignore the offset first bytes by seeking forward, then
        discarding the following bytes up until the next new line
        character.

    length : int, default=-1
        If strictly positive, stop reading any new line of data once the
        position in the file has reached the (offset + length) bytes threshold.

    Returns
    -------
    X : scipy.sparse matrix of shape (n_samples, n_features)

    y : ndarray of shape (n_samples,), or a list of tuples of length n_samples
        The target. It is a list of tuples when ``multilabel=True``, else a
        ndarray.

    query_id : array of shape (n_samples,)
       The query_id for each sample. Only returned when query_id is set to
       True.

    See Also
    --------
    load_svmlight_files : Similar function for loading multiple files in this
        format, enforcing the same number of features/columns on all of them.

    Examples
    --------
    To use joblib.Memory to cache the svmlight file::

        from joblib import Memory
        from sklearn.datasets import load_svmlight_file
        mem = Memory("./mycache")

        @mem.cache
        def get_data():
            data = load_svmlight_file("mysvmlightfile")
            return data[0], data[1]

        X, y = get_data()
    """
    return tuple(
        load_svmlight_files(
            [f],
            multilabel=multilabel,
            zero_based=zero_based,
            query_id=query_id,
            offset=offset,
            length=length,
        )
    )

def _gen_open(f):
    if isinstance(f, int):  # file descriptor
        return open(f, "rb", closefd=False)
    elif isinstance(f, os.PathLike):
        f = os.fspath(f)
    elif not isinstance(f, str):
        raise TypeError("expected {str, int, path-like, file-like}, got %s" % type(f))

    _, ext = os.path.splitext(f)
    if ext == ".gz":
        import gzip

        return gzip.open(f, "rb")
    elif ext == ".bz2":
        from bz2 import BZ2File

        return BZ2File(f, "rb")
    else:
        return open(f, "rb")

def _open_and_load(f, dtype, multilabel, zero_based, query_id, offset=0, length=-1):
    if hasattr(f, "read"):
        actual_dtype, data, ind, indptr, labels, query = _load_svmlight_file(
            f, dtype, multilabel, zero_based, query_id, offset, length
        )
    else:
        with closing(_gen_open(f)) as f:
            actual_dtype, data, ind, indptr, labels, query = _load_svmlight_file(
                f, dtype, multilabel, zero_based, query_id, offset, length
            )

    # convert from array.array, give data the right dtype
    if not multilabel:
        labels = np.frombuffer(labels, np.float64)
    data = np.frombuffer(data, actual_dtype)
    indices = np.frombuffer(ind, np.longlong)
    indptr = np.frombuffer(indptr, dtype=np.longlong)  # never empty
    query = np.frombuffer(query, np.int64)

    data = np.asarray(data, dtype=dtype)  # no-op for float{32,64}
    return data, indices, indptr, labels, query

def load_svmlight_files(
    files,
    *,
    offset=0,
):
    """Load dataset from multiple files in SVMlight format.

    This function is equivalent to mapping load_svmlight_file over a list of
    files, except that the results are concatenated into a single, flat list
    and the samples vectors are constrained to all have the same number of
    features.

    files : array-like, dtype=str, path-like, file-like or int
        (Paths of) files to load. If a path ends in ".gz" or ".bz2", it will
        be a file descriptor. File-likes and file descriptors will not be
        closed by this function. File-like objects must be opened in binary
        mode.

        The number of features to use. If None, it will be inferred from the
        maximum column index occurring in any of the files.

        This can be set to a higher value than the actual number of features
        in any of the input files, but setting it to a lower value will cause
        an exception to be raised.

        always be safe when no offset or length is passed.
        If offset or length are passed, the "auto" mode falls back
        to zero_based=True to avoid having the heuristic check yield

    Returns
    -------
    [X1, y1, ..., Xn, yn] or [X1, y1, q1, ..., Xn, yn, qn]: list of arrays
        Each (Xi, yi) pair is the result from load_svmlight_file(files[i]).
        If query_id is set to True, this will return instead (Xi, yi, qi)
        triplets.

    See Also
    --------
    load_svmlight_file: Similar function for loading a single file in this
        format.

    Notes
    -----
    When fitting a model to a matrix X_train and evaluating it against a
    matrix X_test, it is essential that X_train and X_test have the same
    number of features (X_train.shape[1] == X_test.shape[1]). This may not
    be the case if you load the files individually with load_svmlight_file.

    Examples
    --------

            data_train, target_train, data_test, target_test = load_svmlight_files(
                ["svmlight_file_train", "svmlight_file_test"]
            )
            return data_train, target_train, data_test, target_test

        X_train, y_train, X_test, y_test = get_data()
    """
    if (offset != 0 or length > 0) and zero_based == "auto":
        # disable heuristic search to avoid getting inconsistent results on
        # different segments of the file
        zero_based = True

    if (offset != 0 or length > 0) and n_features is None:
        raise ValueError("n_features is required when offset or length is specified.")

    r = [
        _open_and_load(
            f,
            dtype,
            multilabel,
            bool(zero_based),
            bool(query_id),
        )
        for f in files
    ]

    if zero_based is False or (
        zero_based == "auto" and all(len(tmp[1]) and np.min(tmp[1]) > 0 for tmp in r)
    ):
        for _, indices, _, _, _ in r:
            indices -= 1

    n_f = max(ind[1].max() if len(ind[1]) else 0 for ind in r) + 1

    if n_features is None:
        n_features = n_f
    elif n_features < n_f:
            "n_features was set to {}, but input file contains {} features".format(
                n_features, n_f
            )
        )

    result = []
    for data, indices, indptr, y, query_values in r:
        shape = (indptr.shape[0] - 1, n_features)
        X = sp.csr_matrix((data, indices, indptr), shape)
        result += X, y
        if query_id:
            result.append(query_values)

    return result

def _dump_svmlight(X, y, f, multilabel, one_based, comment, query_id):
    if comment:
        f.write(
            (
                "# Generated by dump_svmlight_file from scikit-learn %s\n" % __version__
            ).encode()
        )
        f.write(
            ("# Column indices are %s-based\n" % ["zero", "one"][one_based]).encode()
        )

        f.write(b"#\n")
        f.writelines(b"# %s\n" % line for line in comment.splitlines())
    X_is_sp = sp.issparse(X)
    y_is_sp = sp.issparse(y)
    if not multilabel and not y_is_sp:
        y = y[:, np.newaxis]
    _dump_svmlight_file(
        X,
        y,
        f,
        multilabel,
        one_based,
        query_id,
        X_is_sp,
        y_is_sp,
    )

def dump_svmlight_file(
    X,
    y,
    f,
    *,
    zero_based=True,
    comment=None,
    query_id=None,
):
    """Dump the dataset in svmlight / libsvm file format.

        Training vectors, where `n_samples` is the number of samples and
        `n_features` is the number of features.

    y : {array-like, sparse matrix}, shape = (n_samples,) or (n_samples, n_labels)
        Target values. Class labels must be an
        integer or float, or array-like objects of integer or float for
        multilabel classifications.

    f : str or file-like in binary mode
        If string, specifies the path that will contain the data.
        If file-like, data will be written to f. f should be opened in binary
        mode.

    zero_based : bool, default=True
        Whether column indices should be written zero-based (True) or one-based
        (False).

    comment : str or bytes, default=None
        Comment to insert at the top of the file. This should be either a
        Unicode string, which will be encoded as UTF-8, or an ASCII byte
        string.
        If a comment is given, then it will be preceded by one that identifies
        the file as having been dumped by scikit-learn. Note that not all
        tools grok comments in SVMlight files.

    query_id : array-like of shape (n_samples,), default=None
        Array containing pairwise preference constraints (qid in svmlight
        format).

           parameter `multilabel` to support multilabel datasets.

    Examples
    --------
    >>> from sklearn.datasets import dump_svmlight_file, make_classification
    >>> output_file = "my_dataset.svmlight"
    >>> dump_svmlight_file(X, y, output_file)  # doctest: +SKIP
    """
    if comment is not None:
        # Convert comment string to list of lines in UTF-8.
        # If a byte string is passed, then check whether it's ASCII;
        # if a user wants to get fancy, they'll have to decode themselves.
        if isinstance(comment, bytes):
            comment.decode("ascii")  # just for the exception
        else:
            comment = comment.encode("utf-8")
        if b"\0" in comment:
            raise ValueError("comment string contains NUL byte")

    yval = check_array(y, accept_sparse="csr", ensure_2d=False)
    if sp.issparse(yval):
        if yval.shape[1] != 1 and not multilabel:
                "expected y of shape (n_samples, 1), got %r" % (yval.shape,)
            )
    else:
        if yval.ndim != 1 and not multilabel:
            raise ValueError("expected y of shape (n_samples,), got %r" % (yval.shape,))

    Xval = check_array(X, accept_sparse="csr")
    if Xval.shape[0] != yval.shape[0]:
            "X.shape[0] and y.shape[0] should be the same, got %r and %r instead."
            % (Xval.shape[0], yval.shape[0])
        )

    # We had some issues with CSR matrices with unsorted indices (e.g. #1501),
    # so sort them here, but first make sure we don't modify the user's X.
    # TODO We can do this cheaper; sorted_indices copies the whole matrix.
    if yval is y and hasattr(yval, "sorted_indices"):
        y = yval.sorted_indices()
    else:
        y = yval
        if hasattr(y, "sort_indices"):
            y.sort_indices()

    if Xval is X and hasattr(Xval, "sorted_indices"):
        X = Xval.sorted_indices()
    else:
        X = Xval
        if hasattr(X, "sort_indices"):

    if query_id is None:
        # NOTE: query_id is passed to Cython functions using a fused type on query_id.
        # Yet as of Cython>=3.0, memory views can't be None otherwise the runtime
        # would not known which concrete implementation to dispatch the Python call to.
        # TODO: simplify interfaces and implementations in _svmlight_format_fast.pyx.
        query_id = np.array([], dtype=np.int32)
    else:
        query_id = np.asarray(query_id)
        if query_id.shape[0] != y.shape[0]:
                "expected query_id of shape (n_samples,), got %r" % (query_id.shape,)
            )

    one_based = not zero_based

    if hasattr(f, "write"):
        _dump_svmlight(X, y, f, multilabel, one_based, comment, query_id)
    else:
        with open(f, "wb") as f:
            _dump_svmlight(X, y, f, multilabel, one_based, comment, query_id)

def _get_local_path(openml_path: str, data_home: str) -> str:
    return os.path.join(data_home, "openml.org", openml_path + ".gz")

def _retry_with_clean_cache(
    openml_path: str,
    data_home: Optional[str],
    no_retry_exception: Optional[Exception] = None,
) -> Callable:
    """If the first call to the decorated function fails, the local cached
    file is removed, and the function is called again. If ``data_home`` is
    ``None``, then the function is called once. We can provide a specific
    exception to not retry on using `no_retry_exception` parameter.
    """

    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kw):
            if data_home is None:
                return f(*args, **kw)
            try:
            except URLError:
                raise
            except Exception as exc:
                if no_retry_exception is not None and isinstance(
                    exc, no_retry_exception
                ):
                    raise
                warn("Invalid cache, redownloading file", RuntimeWarning)
                local_path = _get_local_path(openml_path, data_home)
                if os.path.exists(local_path):
                    os.unlink(local_path)

def _retry_on_network_error(
    n_retries: int = 3, delay: float = 1.0, url: str = ""
    """If the function call results in a network error, call the function again
    up to ``n_retries`` times with a ``delay`` between each call. If the error
    has a 412 status code, don't call the function again as this is a specific
    OpenML error.
    The url parameter is used to give more information to the user about the
    error.
    """

        @wraps(f)
        def wrapper(*args, **kwargs):
            retry_counter = n_retries
            while True:
                try:
                    return f(*args, **kwargs)
                except (URLError, TimeoutError) as e:
                    # 412 is a specific OpenML error code.
                    if isinstance(e, HTTPError) and e.code == 412:
                        raise
                    if retry_counter == 0:
                        raise
                    warn(
                        f"A network error occurred while downloading {url}. Retrying..."
                    )
                    retry_counter -= 1
                    time.sleep(delay)

def _open_openml_url(
    url: str, data_home: Optional[str], n_retries: int = 3, delay: float = 1.0
):
    """
    Returns a resource from OpenML.org. Caches it to data_home if required.

    url : str
        OpenML URL that will be downloaded and cached locally. The path component
        of the URL is used to replicate the tree structure as sub-folders of the local
        cache folder.

    data_home : str
        Directory to which the files will be cached. If None, no caching will
        be applied.

        Number of retries when HTTP errors are encountered. Error with status
        code 412 won't be retried as they represent OpenML generic errors.

    Returns
    -------
    result : stream
        A stream to the OpenML resource.
    """

    def is_gzip_encoded(_fsrc):
        return _fsrc.info().get("Content-Encoding", "") == "gzip"

    req = Request(url)
    req.add_header("Accept-encoding", "gzip")

        fsrc = _retry_on_network_error(n_retries, delay, req.full_url)(urlopen)(req)
        if is_gzip_encoded(fsrc):
            return gzip.GzipFile(fileobj=fsrc, mode="rb")
        return fsrc

    openml_path = urlparse(url).path.lstrip("/")
    local_path = _get_local_path(openml_path, data_home)
    dir_name, file_name = os.path.split(local_path)
    if not os.path.exists(local_path):
        os.makedirs(dir_name, exist_ok=True)
        try:
            # Create a tmpdir as a subfolder of dir_name where the final file will
            # be moved to if the download is successful. This guarantees that the
            # renaming operation to the final location is atomic to ensure the
            # concurrence safety of the dataset caching mechanism.
            with TemporaryDirectory(dir=dir_name) as tmpdir:
                with closing(
                    _retry_on_network_error(n_retries, delay, req.full_url)(urlopen)(
                        req
                    )
                ) as fsrc:
                    opener: Callable
                    if is_gzip_encoded(fsrc):
                        opener = open
                    else:
                        opener = gzip.GzipFile
                    with opener(os.path.join(tmpdir, file_name), "wb") as fdst:
                        shutil.copyfileobj(fsrc, fdst)
                shutil.move(fdst.name, local_path)
            if os.path.exists(local_path):
                os.unlink(local_path)
            raise

    # XXX: First time, decompression will not be necessary (by using fsrc), but
    # it will happen nonetheless
    return gzip.GzipFile(local_path, "rb")

class OpenMLError(ValueError):
    """HTTP 412 is a specific OpenML error code, indicating a generic error"""

    pass

def _get_json_content_from_openml_api(
    url: str,
    error_message: Optional[str],
    n_retries: int = 3,
    delay: float = 1.0,
) -> Dict:
    """
    Loads json data from the openml api.

    url : str
        The URL to load from. Should be an official OpenML endpoint.

    error_message : str or None
        The error message to raise if an acceptable OpenML error is thrown
        (acceptable error is, e.g., data id not found. Other errors, like 404's
        will throw the native error message).

    data_home : str or None
        Location to cache the response. None if no cache is required.

    Returns
    -------
    json_data : json
        the json result from the OpenML server if the call was successful.
        An exception otherwise.
    """

    @_retry_with_clean_cache(url, data_home=data_home)
    def _load_json():
        with closing(
            _open_openml_url(url, data_home, n_retries=n_retries, delay=delay)
        ) as response:
            return json.loads(response.read().decode("utf-8"))

    try:
        return _load_json()
    except HTTPError as error:
        # 412 is an OpenML specific error code, indicating a generic error
        # (e.g., data not found)
        if error.code != 412:
            raise error

    # 412 error, not in except for nicer traceback
    raise OpenMLError(error_message)

def _get_data_info_by_name(
    name: str,
    version: Union[int, str],
):
    """
    Utilizes the openml dataset listing api to find a dataset by
    name/version
    OpenML api function:
    https://www.openml.org/api_docs#!/data/get_data_list_data_name_data_name

    name : str
        name of the dataset

    version : int or str
        If version is an integer, the exact name/version will be obtained from
        OpenML. If version is a string (value: "active") it will take the first
        version from OpenML that is annotated as active. Any other string
        values except "active" are treated as integer.

    Returns
    -------
    first_dataset : json
        json representation of the first dataset object that adhired to the
        search criteria

    """
    if version == "active":
        # situation in which we return the oldest active version
        url = _SEARCH_NAME.format(name) + "/status/active/"
        error_msg = "No active dataset {} found.".format(name)
        json_data = _get_json_content_from_openml_api(
            url,
            error_msg,
            data_home=data_home,
            n_retries=n_retries,
            delay=delay,
        )
        res = json_data["data"]["dataset"]
        if len(res) > 1:
            first_version = version = res[0]["version"]
                "Multiple active versions of the dataset matching the name"
                f" {name} exist. Versions may be fundamentally different, "
                f"returning version {first_version}. "
                "Available versions:\n"
            )
            for r in res:
                warning_msg += f"- version {r['version']}, status: {r['status']}\n"
                warning_msg += (
                    f"  url: https://www.openml.org/search?type=data&id={r['did']}\n"
                )
            warn(warning_msg)
        return res[0]

    # an integer version has been provided
    url = (_SEARCH_NAME + "/data_version/{}").format(name, version)
    try:
            url,
            error_message=None,
        )
    except OpenMLError:
        # we can do this in 1 function call if OpenML does not require the
        # specification of the dataset status (i.e., return datasets with a
        # given name / version regardless of active, deactivated, etc. )
        # TODO: feature request OpenML.
        url += "/status/deactivated"
        error_msg = "Dataset {} with version {} not found.".format(name, version)
            url,
        )

    return json_data["data"]["dataset"][0]

def _get_data_description_by_id(
    data_id: int,
) -> Dict[str, Any]:
    # OpenML API function: https://www.openml.org/api_docs#!/data/get_data_id
    url = _DATA_INFO.format(data_id)
    error_message = "Dataset with data_id {} not found.".format(data_id)
    json_data = _get_json_content_from_openml_api(
        url,
        error_message,
    )
    return json_data["data_set_description"]

def _get_data_features(
) -> OpenmlFeaturesType:
    # OpenML function:
    # https://www.openml.org/api_docs#!/data/get_data_features_id
    url = _DATA_FEATURES.format(data_id)
        url,
    )
    return json_data["data_features"]["feature"]

def _get_data_qualities(
) -> OpenmlQualitiesType:
    # OpenML API function:
    # https://www.openml.org/api_docs#!/data/get_data_qualities_id
    url = _DATA_QUALITIES.format(data_id)
        url,
    )
    # the qualities might not be available, but we still try to process
    # the data
    return json_data.get("data_qualities", {}).get("quality", [])

def _get_num_samples(data_qualities: OpenmlQualitiesType) -> int:
    """Get the number of samples from data qualities.

    data_qualities : list of dict
        Used to retrieve the number of instances (samples) in the dataset.

    Returns
    -------
        The number of samples in the dataset or -1 if data qualities are
        unavailable.
    """
    # If the data qualities are unavailable, we return -1
    default_n_samples = -1

    qualities = {d["name"]: d["value"] for d in data_qualities}
    return int(float(qualities.get("NumberOfInstances", default_n_samples)))

def _load_arff_response(
    url: str,
    parser: str,
    output_type: str,
    openml_columns_info: dict,
    feature_names_to_select: List[str],
    target_names_to_select: List[str],
    shape: Optional[Tuple[int, int]],
    md5_checksum: str,
    read_csv_kwargs: Optional[Dict] = None,
):
    """Load the ARFF data associated with the OpenML URL.

    In addition of loading the data, this function will also check the
    integrity of the downloaded file from OpenML using MD5 checksum.

    url : str
        The URL of the ARFF file on OpenML.

        The location where to cache the data.

    parser : {"liac-arff", "pandas"}
        The parser used to parse the ARFF file.

    output_type : {"numpy", "pandas", "sparse"}

        file.

        The list of the features to be selected.

        The list of the target variables to be selected.

    shape : tuple or None
        With `parser="liac-arff"`, when using a generator to load the data,
        one needs to provide the shape of the data beforehand.

    md5_checksum : str
        The MD5 checksum provided by OpenML to check the data integrity.

        The number of times to retry downloading the data if it fails.

        The delay between two consecutive downloads in seconds.

        Keyword arguments to pass to `pandas.read_csv` when using the pandas parser.
        It allows to overwrite the default options.

    Returns
    -------

    """
    gzip_file = _open_openml_url(url, data_home, n_retries=n_retries, delay=delay)
    with closing(gzip_file):
        md5 = hashlib.md5()
        for chunk in iter(lambda: gzip_file.read(4096), b""):
            md5.update(chunk)
        actual_md5_checksum = md5.hexdigest()

    if actual_md5_checksum != md5_checksum:
            f"md5 checksum of local file for {url} does not match description: "
            f"expected: {md5_checksum} but got {actual_md5_checksum}. "
            "Downloaded file could have been modified / corrupted, clean cache "
            "and retry..."
        )

    def _open_url_and_load_gzip_file(url, data_home, n_retries, delay, arff_params):
        gzip_file = _open_openml_url(url, data_home, n_retries=n_retries, delay=delay)
        with closing(gzip_file):
            return load_arff_from_gzip_file(gzip_file, **arff_params)

    arff_params: Dict = dict(
        parser=parser,
        output_type=output_type,
        openml_columns_info=openml_columns_info,
        feature_names_to_select=feature_names_to_select,
        target_names_to_select=target_names_to_select,
        shape=shape,
        read_csv_kwargs=read_csv_kwargs or {},
    )
    try:
        X, y, frame, categories = _open_url_and_load_gzip_file(
            url, data_home, n_retries, delay, arff_params
        )
    except Exception as exc:
        if parser != "pandas":
            raise

        from pandas.errors import ParserError

        if not isinstance(exc, ParserError):
            raise

        # A parsing error could come from providing the wrong quotechar
        # to pandas. By default, we use a double quote. Thus, we retry
        # with a single quote before to raise the error.
        arff_params["read_csv_kwargs"].update(quotechar="'")
        )

    return X, y, frame, categories

def _download_data_to_bunch(
    url: str,
    sparse: bool,
    *,
    as_frame: bool,
    openml_columns_info: List[dict],
    data_columns: List[str],
    target_columns: List[str],
):
    """Download ARFF data, load it to a specific container and create to Bunch.

    This function has a mechanism to retry/cache/clean the data.

    url : str

    sparse : bool
        Whether the dataset is expected to use the sparse ARFF format.

    as_frame : bool
        Whether or not to return the data into a pandas DataFrame.

    openml_columns_info : list of dict
        The information regarding the columns provided by OpenML for the
        ARFF dataset. The information is stored as a list of dictionaries.

    data_columns : list of str

    target_columns : list of str

    Returns
    -------

        X : {ndarray, sparse matrix, dataframe}
        y : {ndarray, dataframe, series}
            The target.
        frame : dataframe or None
            A dataframe containing both `X` and `y`. `None` if
            `output_array_type != "pandas"`.
        categories : list of str or None
            The names of the features that are categorical. `None` if
            `output_array_type == "pandas"`.
    """
    # Prepare which columns and data types should be returned for the X and y
    features_dict = {feature["name"]: feature for feature in openml_columns_info}

    if sparse:
        output_type = "sparse"
    elif as_frame:
        output_type = "pandas"
    else:
        output_type = "numpy"

    # XXX: target columns should all be categorical or all numeric
    _verify_target_data_type(features_dict, target_columns)
    for name in target_columns:
        column_info = features_dict[name]
        n_missing_values = int(column_info["number_of_missing_values"])
        if n_missing_values > 0:
                f"Target column '{column_info['name']}' has {n_missing_values} missing "
                "values. Missing values are not supported for target columns."
            )

    no_retry_exception = None
    if parser == "pandas":
        # If we get a ParserError with pandas, then we don't want to retry and we raise
        # early.

        no_retry_exception = ParserError

    X, y, frame, categories = _retry_with_clean_cache(
        url, data_home, no_retry_exception
    )(_load_arff_response)(
        url,
        data_home,
        openml_columns_info=features_dict,
        feature_names_to_select=data_columns,
        target_names_to_select=target_columns,
        md5_checksum=md5_checksum,
        read_csv_kwargs=read_csv_kwargs,
    )

        data=X,
        target=y,
        categories=categories,
        feature_names=data_columns,
        target_names=target_columns,
    )

def _verify_target_data_type(features_dict, target_columns):
    # verifies the data type of the y array in case there are multiple targets
    # (throws an error if these targets do not comply with sklearn support)
    if not isinstance(target_columns, list):
        raise ValueError("target_column should be list, got: %s" % type(target_columns))
    found_types = set()
    for target_column in target_columns:
        if target_column not in features_dict:
            raise KeyError(f"Could not find target_column='{target_column}'")
        if features_dict[target_column]["data_type"] == "numeric":
            found_types.add(np.float64)
        else:
            found_types.add(object)

        # note: we compare to a string, not boolean
        if features_dict[target_column]["is_ignore"] == "true":
            warn(f"target_column='{target_column}' has flag is_ignore.")
        if features_dict[target_column]["is_row_identifier"] == "true":
            warn(f"target_column='{target_column}' has flag is_row_identifier.")
    if len(found_types) > 1:
            "Can only handle homogeneous multi-target datasets, "
            "i.e., all targets are either numeric or "
            "categorical."
        )

def _valid_data_column_names(features_list, target_columns):
    # logic for determining on which columns can be learned. Note that from the
    # OpenML guide follows that columns that have the `is_row_identifier` or
    # `is_ignore` flag, these can not be learned on. Also target columns are
    # excluded.
    valid_data_column_names = []
    for feature in features_list:
        if (
            feature["name"] not in target_columns
            and feature["is_ignore"] != "true"
            and feature["is_row_identifier"] != "true"
        ):
            valid_data_column_names.append(feature["name"])
    return valid_data_column_names

def fetch_openml(
    name: Optional[str] = None,
    *,
    version: Union[str, int] = "active",
    data_id: Optional[int] = None,
    data_home: Optional[Union[str, os.PathLike]] = None,
    target_column: Optional[Union[str, List]] = "default-target",
    cache: bool = True,
    return_X_y: bool = False,
    as_frame: Union[str, bool] = "auto",
    parser: str = "auto",
):
    """Fetch dataset from openml by name or dataset id.

    Datasets are uniquely identified by either an integer ID or by a
    combination of name and version (i.e. there might be multiple
    versions of the 'iris' dataset). Please give either name or data_id
    (not both). In case a name is given, a version can also be
    provided.

    Read more in the :ref:`User Guide <openml>`.

    .. note:: EXPERIMENTAL

        The API is experimental (particularly the return value structure),
        and might have small backward-incompatible changes without notice
        or warning in future releases.

    name : str, default=None
        String identifier of the dataset. Note that OpenML can have multiple
        datasets with the same name.

    version : int or 'active', default='active'
        Version of the dataset. Can only be provided if also ``name`` is given.
        If 'active' the oldest version that's still active is used. Since
        there may be more than one active version of a dataset, and those
        versions may fundamentally be different from one another, setting an
        exact version is highly recommended.

    data_id : int, default=None
        OpenML ID of the dataset. The most specific way of retrieving a
        dataset. If data_id is not given, name (and potential version) are
        used to obtain a dataset.

        Specify another download and cache folder for the data sets. By default

    target_column : str, list or None, default='default-target'
        Specify the column name in the data to use as target. If
        'default-target', the standard target column a stored on the server
        is used. If ``None``, all columns are returned as data and the
        target is ``None``. If list (of strings), all columns with these names
        are returned as multi-target (Note: not all scikit-learn classifiers
        can handle all types of multi-output combinations).

    cache : bool, default=True
        Whether to cache the downloaded datasets into `data_home`.

        below for more information about the `data` and `target` objects.

    as_frame : bool or 'auto', default='auto'
        a pandas DataFrame or Series depending on the number of target_columns.
        The Bunch will contain a ``frame`` attribute with the target and the
        data. If ``return_X_y`` is True, then ``(data, target)`` will be pandas
        DataFrames or Series as describe above.

        If `as_frame` is 'auto', the data and target will be converted to
        DataFrame or Series as if `as_frame` is set to True, unless the dataset
        is stored in sparse format.

        If `as_frame` is False, the data and target will be NumPy arrays and
        the `data` will only contain numerical values when `parser="liac-arff"`
        where the categories are provided in the attribute `categories` of the
        `Bunch` instance. When `parser="pandas"`, no ordinal encoding is made.

           The default value of `as_frame` changed from `False` to `'auto'`
           in 0.24.

        Number of retries when HTTP errors or network timeouts are encountered.
        Error with status code 412 won't be retried as they represent OpenML
        generic errors.

    parser : {"auto", "pandas", "liac-arff"}, default="auto"
        Parser used to load the ARFF file. Two parsers are implemented:

        - `"pandas"`: this is the most efficient parser. However, it requires
          pandas to be installed and can only open dense datasets.
        - `"liac-arff"`: this is a pure Python ARFF parser that is much less
          memory- and CPU-efficient. It deals with sparse ARFF datasets.

        If `"auto"`, the parser is chosen automatically such that `"liac-arff"`
        is selected for sparse ARFF datasets, otherwise `"pandas"` is selected.

           The default value of `parser` changes from `"liac-arff"` to
           `"auto"`.

        Keyword arguments passed to :func:`pandas.read_csv` when loading the data
        from a ARFF file and using the pandas parser. It can allow to
        overwrite some default parameters.

    Returns
    -------

        data : np.array, scipy.sparse.csr_matrix of floats, or pandas DataFrame
            The feature matrix. Categorical features are encoded as ordinals.
        target : np.array, pandas Series or DataFrame
            The regression target or classification labels, if applicable.
            Dtype is float if numeric, and object if categorical. If
            ``as_frame`` is True, ``target`` is a pandas object.
        DESCR : str
        feature_names : list

        categories : dict or None
            Maps each categorical feature name to a list of values, such
            that the value encoded as i is ith in the list. If ``as_frame``
            is True, this is None.
        details : dict
            More metadata from OpenML.
        frame : pandas DataFrame
            Only present when `as_frame=True`. DataFrame with ``data`` and
            ``target``.

        .. note:: EXPERIMENTAL

            This interface is **experimental** and subsequent releases may
            change attributes without notice (although there should only be
            minor changes to ``data`` and ``target``).

        Missing values in the 'data' are represented as NaN's. Missing values
        in 'target' are represented as NaN's (numerical target) or None
        (categorical target).

    Notes
    -----
    The `"pandas"` and `"liac-arff"` parsers can lead to different data types
    in the output. The notable differences are the following:

    - The `"liac-arff"` parser always encodes categorical features as `str` objects.
      To the contrary, the `"pandas"` parser instead infers the type while
      reading and numerical categories will be casted into integers whenever
      possible.
    - The `"liac-arff"` parser uses float64 to encode numerical features
      tagged as 'REAL' and 'NUMERICAL' in the metadata. The `"pandas"`
      parser instead infers if these numerical features corresponds
      to integers and uses panda's Integer extension dtype.
    - In particular, classification datasets with integer categories are
      typically loaded as such `(0, 1, ...)` with the `"pandas"` parser while
      `"liac-arff"` will force the use of string encoded class labels such as
      `"0"`, `"1"` and so on.
    - The `"pandas"` parser will not strip single quotes - i.e. `'` - from
      string columns. For instance, a string `'my string'` will be kept as is
      while the `"liac-arff"` parser will strip the single quotes. For
      categorical columns, the single quotes are stripped from the values.

    In addition, when `as_frame=False` is used, the `"liac-arff"` parser
    returns ordinally encoded data where the categories are provided in the
    attribute `categories` of the `Bunch` instance. Instead, `"pandas"` returns
    a NumPy array were the categories are not encoded.

    Examples
    --------
    >>> from sklearn.datasets import fetch_openml
    >>> adult = fetch_openml("adult", version=2)  # doctest: +SKIP
    >>> adult.frame.info()  # doctest: +SKIP
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 48842 entries, 0 to 48841
    Data columns (total 15 columns):
     #   Column          Non-Null Count  Dtype
    ---  ------          --------------  -----
     0   age             48842 non-null  int64
     1   workclass       46043 non-null  category
     2   fnlwgt          48842 non-null  int64
     3   education       48842 non-null  category
     4   education-num   48842 non-null  int64
     5   marital-status  48842 non-null  category
     6   occupation      46033 non-null  category
     7   relationship    48842 non-null  category
     8   race            48842 non-null  category
     9   sex             48842 non-null  category
     10  capital-gain    48842 non-null  int64
     11  capital-loss    48842 non-null  int64
     12  hours-per-week  48842 non-null  int64
     13  native-country  47985 non-null  category
     14  class           48842 non-null  category
    dtypes: category(9), int64(6)
    memory usage: 2.7 MB
    """
    if cache is False:
        # no caching will be applied
        data_home = None
    else:
        data_home = get_data_home(data_home=data_home)
        data_home = join(str(data_home), "openml")

    # check valid function arguments. data_id XOR (name, version) should be
    # provided
    if name is not None:
        # OpenML is case-insensitive, but the caching mechanism is not
        # convert all data names (str) to lower case
        name = name.lower()
        if data_id is not None:
                "Dataset data_id={} and name={} passed, but you can only "
                "specify a numeric data_id or a name, not "
                "both.".format(data_id, name)
            )
        data_info = _get_data_info_by_name(
            name, version, data_home, n_retries=n_retries, delay=delay
        )
        data_id = data_info["did"]
    elif data_id is not None:
        # from the previous if statement, it is given that name is None
        if version != "active":
                "Dataset data_id={} and version={} passed, but you can only "
                "specify a numeric data_id or a version, not "
                "both.".format(data_id, version)
            )
    else:
            "Neither name nor data_id are provided. Please provide name or data_id."
        )

    data_description = _get_data_description_by_id(data_id, data_home)
    if data_description["status"] != "active":
        warn(
            "Version {} of dataset {} is inactive, meaning that issues have "
            "been found in the dataset. Try using a newer version from "
            "this URL: {}".format(
                data_description["version"],
                data_description["name"],
                data_description["url"],
            )
        )
    if "error" in data_description:
        warn(
            "OpenML registered a problem with the dataset. It might be "
            "unusable. Error: {}".format(data_description["error"])
        )
    if "warning" in data_description:
        warn(
            "OpenML raised a warning on the dataset. It might be "
            "unusable. Warning: {}".format(data_description["warning"])
        )

    return_sparse = data_description["format"].lower() == "sparse_arff"
    as_frame = not return_sparse if as_frame == "auto" else as_frame
    if parser == "auto":
        parser_ = "liac-arff" if return_sparse else "pandas"
    else:
        parser_ = parser

    if parser_ == "pandas":
        try:
            check_pandas_support("`fetch_openml`")
        except ImportError as exc:
            if as_frame:
                err_msg = (
                    "Returning pandas objects requires pandas to be installed. "
                    "Alternatively, explicitly set `as_frame=False` and "
                    "`parser='liac-arff'`."
                )
            else:
                err_msg = (
                    f"Using `parser={parser!r}` with dense data requires pandas to be "
                    "installed. Alternatively, explicitly set `parser='liac-arff'`."
                )
            raise ImportError(err_msg) from exc

    if return_sparse:
        if as_frame:
                "Sparse ARFF datasets cannot be loaded with as_frame=True. "
                "Use as_frame=False or as_frame='auto' instead."
            )
        if parser_ == "pandas":
                f"Sparse ARFF datasets cannot be loaded with parser={parser!r}. "
                "Use parser='liac-arff' or parser='auto' instead."
            )

    # download data features, meta-info about column types
    features_list = _get_data_features(data_id, data_home)

    if not as_frame:
        for feature in features_list:
            if "true" in (feature["is_ignore"], feature["is_row_identifier"]):
                continue
            if feature["data_type"] == "string":
                    "STRING attributes are not supported for "
                    "array representation. Try as_frame=True"
                )

    if target_column == "default-target":
        # determines the default target based on the data feature results
        # (which is currently more reliable than the data description;
        # see issue: https://github.com/openml/OpenML/issues/768)
        target_columns = [
            feature["name"]
            for feature in features_list
            if feature["is_target"] == "true"
        ]
    elif isinstance(target_column, str):
        # for code-simplicity, make target_column by default a list
        target_columns = [target_column]
    elif target_column is None:
        target_columns = []
    else:
        # target_column already is of type list
        target_columns = target_column
    data_columns = _valid_data_column_names(features_list, target_columns)

    shape: Optional[Tuple[int, int]]
    # determine arff encoding to return
    if not return_sparse:
        # The shape must include the ignored features to keep the right indexes
        # during the arff data conversion.
        data_qualities = _get_data_qualities(data_id, data_home)
        shape = _get_num_samples(data_qualities), len(features_list)
    else:
        shape = None

    # obtain the data
    url = data_description["url"]
    bunch = _download_data_to_bunch(
        url,
        return_sparse,
        as_frame=bool(as_frame),
        openml_columns_info=features_list,
        target_columns=target_columns,
        data_columns=data_columns,
        md5_checksum=data_description["md5_checksum"],
        parser=parser_,
    )

        return bunch.data, bunch.target

    description = "{}\n\nDownloaded from openml.org.".format(
        data_description.pop("description")
    )

    bunch.update(
        DESCR=description,
        details=data_description,
        url="https://www.openml.org/d/{}".format(data_id),
    )

        @wraps(f)
            try:
                raise
                ):
                    raise

        @wraps(f)
                try:
                        raise
                        raise
                    warn(
                    )

            try:
                raise
                ):
                    raise

                try:
                        raise
                        raise
                    warn(
                    )

def fetch_rcv1(
    *,
    subset="all",
    shuffle=False,
):
    """Load the RCV1 multilabel dataset (classification).

    Version: RCV1-v2, vectors, full sets, topics multilabels.

    =================   =====================
    Classes                               103
    Samples total                      804414
    Dimensionality                      47236
    Features            real, between 0 and 1

    Read more in the :ref:`User Guide <rcv1_dataset>`.

    subset : {'train', 'test', 'all'}, default='all'
        Select the dataset to load: 'train' for the training set
        (23149 samples), 'test' for the test set (781265 samples),
        'all' for both, with the training samples first if shuffle is False.
        This follows the official LYRL2004 chronological split.

    shuffle : bool, default=False
        Whether to shuffle dataset.

    Returns
    -------
        Dictionary-like object. Returned only if `return_X_y` is False.
        `dataset` has the following attributes:

        - data : sparse matrix of shape (804414, 47236), dtype=np.float64
            The array has 0.16% of non zero values. Will be of CSR format.
        - target : sparse matrix of shape (804414, 103), dtype=np.uint8
            Each sample has a value of 1 in its categories, and 0 in others.
            The array has 3.15% of non zero values. Will be of CSR format.
        - sample_id : ndarray of shape (804414,), dtype=np.uint32,
            Identification number of each sample, as ordered in dataset.data.
        - target_names : ndarray of shape (103,), dtype=object
            Names of each target (RCV1 topics), as ordered in dataset.target.
        - DESCR : str
            Description of the RCV1 dataset.

    (data, target) : tuple
        A tuple consisting of `dataset.data` and `dataset.target`, as
        described above. Returned only if `return_X_y` is True.

    Examples
    --------
    >>> from sklearn.datasets import fetch_rcv1
    >>> rcv1 = fetch_rcv1()
    >>> rcv1.data.shape
    (804414, 47236)
    >>> rcv1.target.shape
    (804414, 103)
    """
    N_SAMPLES = 804414
    N_FEATURES = 47236
    N_CATEGORIES = 103
    N_TRAIN = 23149

    rcv1_dir = join(data_home, "RCV1")
    if download_if_missing:
        if not exists(rcv1_dir):
            makedirs(rcv1_dir)

    samples_path = _pkl_filepath(rcv1_dir, "samples.pkl")
    sample_id_path = _pkl_filepath(rcv1_dir, "sample_id.pkl")
    sample_topics_path = _pkl_filepath(rcv1_dir, "sample_topics.pkl")
    topics_path = _pkl_filepath(rcv1_dir, "topics_names.pkl")

    # load data (X) and sample_id
    if download_if_missing and (not exists(samples_path) or not exists(sample_id_path)):
        files = []
        for each in XY_METADATA:
            logger.info("Downloading %s" % each.url)
            file_path = _fetch_remote(
                each, dirname=rcv1_dir, n_retries=n_retries, delay=delay
            )
            files.append(GzipFile(filename=file_path))

        Xy = load_svmlight_files(files, n_features=N_FEATURES)

        # Training data is before testing data
        X = sp.vstack([Xy[8], Xy[0], Xy[2], Xy[4], Xy[6]]).tocsr()
        sample_id = np.hstack((Xy[9], Xy[1], Xy[3], Xy[5], Xy[7]))
        sample_id = sample_id.astype(np.uint32, copy=False)

        joblib.dump(X, samples_path, compress=9)
        joblib.dump(sample_id, sample_id_path, compress=9)

        # delete archives
        for f in files:
            f.close()
            remove(f.name)
    else:
        X = joblib.load(samples_path)
        sample_id = joblib.load(sample_id_path)

    # load target (y), categories, and sample_id_bis
    if download_if_missing and (
        not exists(sample_topics_path) or not exists(topics_path)
    ):
        logger.info("Downloading %s" % TOPICS_METADATA.url)
        topics_archive_path = _fetch_remote(
            TOPICS_METADATA, dirname=rcv1_dir, n_retries=n_retries, delay=delay
        )

        # parse the target file
        n_cat = -1
        n_doc = -1
        doc_previous = -1
        y = np.zeros((N_SAMPLES, N_CATEGORIES), dtype=np.uint8)
        sample_id_bis = np.zeros(N_SAMPLES, dtype=np.int32)
        category_names = {}
        with GzipFile(filename=topics_archive_path, mode="rb") as f:
            for line in f:
                line_components = line.decode("ascii").split(" ")
                if len(line_components) == 3:
                    cat, doc, _ = line_components
                    if cat not in category_names:
                        n_cat += 1
                        category_names[cat] = n_cat

                    doc = int(doc)
                    if doc != doc_previous:
                        doc_previous = doc
                        n_doc += 1
                        sample_id_bis[n_doc] = doc
                    y[n_doc, category_names[cat]] = 1

        # delete archive
        remove(topics_archive_path)

        # Samples in X are ordered with sample_id,
        # whereas in y, they are ordered with sample_id_bis.
        permutation = _find_permutation(sample_id_bis, sample_id)
        y = y[permutation, :]

        # save category names in a list, with same order than y
        categories = np.empty(N_CATEGORIES, dtype=object)
        for k in category_names.keys():
            categories[category_names[k]] = k

        # reorder categories in lexicographic order
        order = np.argsort(categories)
        categories = categories[order]
        y = sp.csr_matrix(y[:, order])

        joblib.dump(y, sample_topics_path, compress=9)
        joblib.dump(categories, topics_path, compress=9)
    else:
        y = joblib.load(sample_topics_path)
        categories = joblib.load(topics_path)

    if subset == "all":
        pass
    elif subset == "train":
        X = X[:N_TRAIN, :]
        y = y[:N_TRAIN, :]
        sample_id = sample_id[:N_TRAIN]
    elif subset == "test":
        X = X[N_TRAIN:, :]
        y = y[N_TRAIN:, :]
        sample_id = sample_id[N_TRAIN:]
    else:
            "Unknown subset parameter. Got '%s' instead of one"
            " of ('all', 'train', test')" % subset
        )

        X, y, sample_id = shuffle_(X, y, sample_id, random_state=random_state)

    fdescr = load_descr("rcv1.rst")

        return X, y

        data=X, target=y, sample_id=sample_id, target_names=categories, DESCR=fdescr
    )

def _inverse_permutation(p):
    """Inverse permutation p."""
    n = p.size
    s = np.zeros(n, dtype=np.int32)
    i = np.arange(n, dtype=np.int32)
    np.put(s, p, i)  # s[p] = i
    return s

def _find_permutation(a, b):
    """Find the permutation from a to b."""
    t = np.argsort(a)
    u = np.argsort(b)
    u_ = _inverse_permutation(u)
    return t[u_]

def fetch_kddcup99(
    *,
    subset=None,
    percent10=True,
    as_frame=False,
):
    """Load the kddcup99 dataset (classification).

    =================   ====================================
    Classes                                               23
    Samples total                                    4898431
    Dimensionality                                        41
    Features            discrete (int) or continuous (float)

    Read more in the :ref:`User Guide <kddcup99_dataset>`.

    subset : {'SA', 'SF', 'http', 'smtp'}, default=None
        To return the corresponding classical subsets of kddcup 99.
        If None, return the entire kddcup 99 dataset.

        Determines random number generation for dataset shuffling and for
        selection of abnormal samples if `subset='SA'`. Pass an int for
        reproducible output across multiple function calls.

    percent10 : bool, default=True
        Whether to load only 10 percent of the data.

        If `True`, returns a pandas Dataframe for the ``data`` and ``target``
        objects in the `Bunch` returned object; `Bunch` return object will also
        have a ``frame`` member.

    Returns
    -------

        data : {ndarray, dataframe} of shape (494021, 41)
            The data matrix to learn. If `as_frame=True`, `data` will be a
            pandas DataFrame.
        target : {ndarray, series} of shape (494021,)
            The regression target for each sample. If `as_frame=True`, `target`
            will be a pandas Series.
        frame : dataframe of shape (494021, 42)
            Only present when `as_frame=True`. Contains `data` and `target`.
        DESCR : str
            The names of the dataset columns
            The names of the target columns

    """
    kddcup99 = _fetch_brute_kddcup99(
        percent10=percent10,
    )

    data = kddcup99.data
    target = kddcup99.target
    feature_names = kddcup99.feature_names
    target_names = kddcup99.target_names

    if subset == "SA":
        s = target == b"normal."
        t = np.logical_not(s)
        normal_samples = data[s, :]
        normal_targets = target[s]
        abnormal_samples = data[t, :]
        abnormal_targets = target[t]

        n_samples_abnormal = abnormal_samples.shape[0]
        # selected abnormal samples:
        r = random_state.randint(0, n_samples_abnormal, 3377)
        abnormal_samples = abnormal_samples[r]
        abnormal_targets = abnormal_targets[r]

        data = np.r_[normal_samples, abnormal_samples]
        target = np.r_[normal_targets, abnormal_targets]

    if subset == "SF" or subset == "http" or subset == "smtp":
        # select all samples with positive logged_in attribute:
        s = data[:, 11] == 1
        data = np.c_[data[s, :11], data[s, 12:]]
        feature_names = feature_names[:11] + feature_names[12:]
        target = target[s]

        data[:, 0] = np.log((data[:, 0] + 0.1).astype(float, copy=False))
        data[:, 4] = np.log((data[:, 4] + 0.1).astype(float, copy=False))
        data[:, 5] = np.log((data[:, 5] + 0.1).astype(float, copy=False))

        if subset == "http":
            s = data[:, 2] == b"http"
            data = data[s]
            target = target[s]
            data = np.c_[data[:, 0], data[:, 4], data[:, 5]]
            feature_names = [feature_names[0], feature_names[4], feature_names[5]]

        if subset == "smtp":
            s = data[:, 2] == b"smtp"

        if subset == "SF":
            data = np.c_[data[:, 0], data[:, 2], data[:, 4], data[:, 5]]
            feature_names = [
                feature_names[0],
                feature_names[2],
                feature_names[4],
                feature_names[5],
            ]

        data, target = shuffle_method(data, target, random_state=random_state)

    fdescr = load_descr("kddcup99.rst")

            "fetch_kddcup99", data, target, feature_names, target_names
        )

    )

def _fetch_brute_kddcup99(
    data_home=None, download_if_missing=True, percent10=True, n_retries=3, delay=1.0
):
    """Load the kddcup99 dataset, downloading it if necessary.

    data_home : str, default=None

    Returns
    -------

        data : ndarray of shape (494021, 41)
            Each row corresponds to the 41 features in the dataset.
        target : ndarray of shape (494021,)
            Each value corresponds to one of the 21 attack types or to the
            label 'normal.'.
        DESCR : str
            Description of the kddcup99 dataset.

    """

    dir_suffix = "-py3"

    if percent10:
        kddcup_dir = join(data_home, "kddcup99_10" + dir_suffix)
        archive = ARCHIVE_10_PERCENT
    else:
        kddcup_dir = join(data_home, "kddcup99" + dir_suffix)

    samples_path = join(kddcup_dir, "samples")
    targets_path = join(kddcup_dir, "targets")
    available = exists(samples_path)

    dt = [
        ("duration", int),
        ("protocol_type", "S4"),
        ("service", "S11"),
        ("flag", "S6"),
        ("src_bytes", int),
        ("dst_bytes", int),
        ("land", int),
        ("wrong_fragment", int),
        ("urgent", int),
        ("hot", int),
        ("num_failed_logins", int),
        ("logged_in", int),
        ("num_compromised", int),
        ("root_shell", int),
        ("su_attempted", int),
        ("num_root", int),
        ("num_file_creations", int),
        ("num_shells", int),
        ("num_access_files", int),
        ("num_outbound_cmds", int),
        ("is_host_login", int),
        ("is_guest_login", int),
        ("count", int),
        ("srv_count", int),
        ("serror_rate", float),
        ("srv_serror_rate", float),
        ("rerror_rate", float),
        ("srv_rerror_rate", float),
        ("same_srv_rate", float),
        ("diff_srv_rate", float),
        ("srv_diff_host_rate", float),
        ("dst_host_count", int),
        ("dst_host_srv_count", int),
        ("dst_host_same_srv_rate", float),
        ("dst_host_diff_srv_rate", float),
        ("dst_host_same_src_port_rate", float),
        ("dst_host_srv_diff_host_rate", float),
        ("dst_host_serror_rate", float),
        ("dst_host_srv_serror_rate", float),
        ("dst_host_rerror_rate", float),
        ("dst_host_srv_rerror_rate", float),
        ("labels", "S16"),
    ]

    column_names = [c[0] for c in dt]
    target_names = column_names[-1]
    feature_names = column_names[:-1]

    if available:
        try:
            X = joblib.load(samples_path)
            y = joblib.load(targets_path)
        except Exception as e:
                "The cache for fetch_kddcup99 is invalid, please delete "
                f"{kddcup_dir} and run the fetch_kddcup99 again"
            ) from e

    elif download_if_missing:
        _mkdirp(kddcup_dir)
        logger.info("Downloading %s" % archive.url)
        _fetch_remote(archive, dirname=kddcup_dir, n_retries=n_retries, delay=delay)
        DT = np.dtype(dt)
        logger.debug("extracting archive")
        archive_path = join(kddcup_dir, archive.filename)
        file_ = GzipFile(filename=archive_path, mode="r")
        Xy = []
        for line in file_.readlines():
            line = line.decode()
            Xy.append(line.replace("\n", "").split(","))
        file_.close()
        logger.debug("extraction done")
        os.remove(archive_path)

        Xy = np.asarray(Xy, dtype=object)
        for j in range(42):
            Xy[:, j] = Xy[:, j].astype(DT[j])

        X = Xy[:, :-1]
        y = Xy[:, -1]
        # XXX bug when compress!=0:
        # (error: 'Incorrect data length while decompressing[...] the file
        #  could be corrupted.')

        joblib.dump(X, samples_path, compress=0)
        joblib.dump(y, targets_path, compress=0)
    else:
        raise OSError("Data not found and `download_if_missing` is False")

        data=X,
        target=y,
        target_names=[target_names],
    )

def _mkdirp(d):
    """Ensure directory d exists (like mkdir -p on Unix)
    No guarantee that the directory is writable.
    """
    try:
        os.makedirs(d)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise

def _generate_hypercube(samples, dimensions, rng):
    """Returns distinct binary samples of length dimensions."""
    if dimensions > 30:
        return np.hstack(
            [
                rng.randint(2, size=(samples, dimensions - 30)),
                _generate_hypercube(samples, 30, rng),
            ]
        )
    out = sample_without_replacement(2**dimensions, samples, random_state=rng).astype(
        dtype=">u4", copy=False
    )
    out = np.unpackbits(out.view(">u1")).reshape((-1, 32))[:, -dimensions:]
    return out

def make_classification(
    n_samples=100,
    n_features=20,
    *,
    n_informative=2,
    n_redundant=2,
    n_repeated=0,
    n_classes=2,
    n_clusters_per_class=2,
    weights=None,
    flip_y=0.01,
    class_sep=1.0,
    hypercube=True,
    shift=0.0,
    scale=1.0,
    return_X_y=True,
):
    """Generate a random n-class classification problem.

    This initially creates clusters of points normally distributed (std=1)
    about vertices of an ``n_informative``-dimensional hypercube with sides of
    length ``2*class_sep`` and assigns an equal number of clusters to each
    class. It introduces interdependence between these features and adds
    various types of further noise to the data.

    Without shuffling, ``X`` horizontally stacks features in the following
    order: the primary ``n_informative`` features, followed by ``n_redundant``
    linear combinations of the informative features, followed by ``n_repeated``
    duplicates, drawn randomly with replacement from the informative and
    redundant features. The remaining features are filled with random noise.
    Thus, without shuffling, all useful features are contained in the columns
    ``X[:, :n_informative + n_redundant + n_repeated]``.

    Read more in the :ref:`User Guide <sample_generators>`.

    n_samples : int, default=100
        The number of samples.

    n_features : int, default=20
        The total number of features. These comprise ``n_informative``
        informative features, ``n_redundant`` redundant features,
        ``n_repeated`` duplicated features and
        ``n_features-n_informative-n_redundant-n_repeated`` useless features
        drawn at random.

    n_informative : int, default=2
        The number of informative features. Each class is composed of a number
        of gaussian clusters each located around the vertices of a hypercube
        in a subspace of dimension ``n_informative``. For each cluster,
        informative features are drawn independently from  N(0, 1) and then
        randomly linearly combined within each cluster in order to add
        covariance. The clusters are then placed on the vertices of the
        hypercube.

    n_redundant : int, default=2
        The number of redundant features. These features are generated as
        random linear combinations of the informative features.

    n_repeated : int, default=0
        The number of duplicated features, drawn randomly from the informative
        and the redundant features.

    n_classes : int, default=2
        The number of classes (or labels) of the classification problem.

    n_clusters_per_class : int, default=2
        The number of clusters per class.

    weights : array-like of shape (n_classes,) or (n_classes - 1,),\
              default=None
        The proportions of samples assigned to each class. If None, then
        classes are balanced. Note that if ``len(weights) == n_classes - 1``,
        then the last class weight is automatically inferred.
        More than ``n_samples`` samples may be returned if the sum of
        ``weights`` exceeds 1. Note that the actual class proportions will
        not exactly match ``weights`` when ``flip_y`` isn't 0.

    flip_y : float, default=0.01
        The fraction of samples whose class is assigned randomly. Larger
        values introduce noise in the labels and make the classification
        task harder. Note that the default setting flip_y > 0 might lead
        to less than ``n_classes`` in y in some cases.

    class_sep : float, default=1.0
        The factor multiplying the hypercube size.  Larger values spread
        out the clusters/classes and make the classification task easier.

    hypercube : bool, default=True
        If True, the clusters are put on the vertices of a hypercube. If
        False, the clusters are put on the vertices of a random polytope.

    shift : float, ndarray of shape (n_features,) or None, default=0.0
        Shift features by the specified value. If None, then features
        are shifted by a random value drawn in [-class_sep, class_sep].

    scale : float, ndarray of shape (n_features,) or None, default=1.0
        Multiply features by the specified value. If None, then features
        are scaled by a random value drawn in [1, 100]. Note that scaling
        happens after shifting.

        Shuffle the samples and the features.

        Determines random number generation for dataset creation. Pass an int

    return_X_y : bool, default=True
        If True, a tuple ``(X, y)`` instead of a Bunch object is returned.

        .. versionadded:: 1.7

    Returns
    -------
    data : :class:`~sklearn.utils.Bunch` if `return_X_y` is `False`.

        DESCR : str
            A description of the function that generated the dataset.
        parameter : dict
            A dictionary that stores the values of the arguments passed to the
            generator function.
        feature_info : list of len(n_features)
            A description for each generated feature.
            The generated samples.
            An integer label for class membership of each sample.

    (X, y) : tuple if ``return_X_y`` is True
        A tuple of generated samples and labels.

    See Also
    --------
    make_blobs : Simplified variant.
    make_multilabel_classification : Unrelated generator for multilabel tasks.

    Notes
    -----
    The algorithm is adapted from Guyon [1] and was designed to generate
    the "Madelon" dataset.

    .. [1] I. Guyon, "Design of experiments for the NIPS 2003 variable
           selection benchmark", 2003.

    Examples
    --------
    >>> X, y = make_classification(random_state=42)
    >>> X.shape
    (100, 20)
    >>> y.shape
    (100,)
    >>> list(y[:5])
    [np.int64(0), np.int64(0), np.int64(1), np.int64(1), np.int64(0)]
    """
    generator = check_random_state(random_state)

    # Count features, clusters and samples
    if n_informative + n_redundant + n_repeated > n_features:
            "Number of informative, redundant and repeated "
            "features must sum to less than the number of total"
            " features"
        )
    # Use log2 to avoid overflow errors
    if n_informative < np.log2(n_classes * n_clusters_per_class):
        msg = "n_classes({}) * n_clusters_per_class({}) must be"
        msg += " smaller or equal 2**n_informative({})={}"
            msg.format(n_classes, n_clusters_per_class, n_informative, 2**n_informative)
        )

        # we define new variable, weight_, instead of modifying user defined parameter.
        if len(weights) not in [n_classes, n_classes - 1]:
                "Weights specified but incompatible with number of classes."
            )
        if len(weights) == n_classes - 1:
            if isinstance(weights, list):
                weights_ = weights + [1.0 - sum(weights)]
            else:
                weights_ = np.resize(weights, n_classes)
                weights_[-1] = 1.0 - sum(weights_[:-1])
        else:
            weights_ = weights.copy()
    else:
        weights_ = [1.0 / n_classes] * n_classes

    n_random = n_features - n_informative - n_redundant - n_repeated
    n_clusters = n_classes * n_clusters_per_class

    # Distribute samples among clusters by weight
    n_samples_per_cluster = [
        int(n_samples * weights_[k % n_classes] / n_clusters_per_class)
        for k in range(n_clusters)
    ]

    for i in range(n_samples - sum(n_samples_per_cluster)):
        n_samples_per_cluster[i % n_clusters] += 1

    # Initialize X and y
    X = np.zeros((n_samples, n_features))
    y = np.zeros(n_samples, dtype=int)

    # Build the polytope whose vertices become cluster centroids
    centroids = _generate_hypercube(n_clusters, n_informative, generator).astype(
        float, copy=False
    )
    centroids *= 2 * class_sep
    centroids -= class_sep
    if not hypercube:
        centroids *= generator.uniform(size=(n_clusters, 1))
        centroids *= generator.uniform(size=(1, n_informative))

    # Initially draw informative features from the standard normal
    X[:, :n_informative] = generator.standard_normal(size=(n_samples, n_informative))

    # Create each cluster; a variant of make_blobs
    stop = 0
    for k, centroid in enumerate(centroids):
        start, stop = stop, stop + n_samples_per_cluster[k]
        y[start:stop] = k % n_classes  # assign labels
        X_k = X[start:stop, :n_informative]  # slice a view of the cluster

        A = 2 * generator.uniform(size=(n_informative, n_informative)) - 1
        X_k[...] = np.dot(X_k, A)  # introduce random covariance

        X_k += centroid  # shift the cluster to a vertex

    # Create redundant features
    if n_redundant > 0:
        B = 2 * generator.uniform(size=(n_informative, n_redundant)) - 1
        X[:, n_informative : n_informative + n_redundant] = np.dot(
            X[:, :n_informative], B
        )

    # Repeat some features
    n = n_informative + n_redundant
    if n_repeated > 0:
        indices = ((n - 1) * generator.uniform(size=n_repeated) + 0.5).astype(np.intp)
        X[:, n : n + n_repeated] = X[:, indices]

    # Fill useless features
    if n_random > 0:
        X[:, -n_random:] = generator.standard_normal(size=(n_samples, n_random))

    # Randomly replace labels
    if flip_y >= 0.0:
        flip_mask = generator.uniform(size=n_samples) < flip_y
        y[flip_mask] = generator.randint(n_classes, size=flip_mask.sum())

    # Randomly shift and scale
    if shift is None:
        shift = (2 * generator.uniform(size=n_features) - 1) * class_sep
    X += shift

    if scale is None:
        scale = 1 + 100 * generator.uniform(size=n_features)
    X *= scale

        # Randomly permute samples
        X, y = util_shuffle(X, y, random_state=generator)

        # Randomly permute features
        generator.shuffle(indices)
        X[:, :] = X[:, indices]

        return X, y

    # feat_desc describes features in X
    feat_desc = ["random"] * n_features
    for i, index in enumerate(indices):
        if index < n_informative:
            feat_desc[i] = "informative"
        elif n_informative <= index < n_informative + n_redundant:
            feat_desc[i] = "redundant"
        elif n <= index < n + n_repeated:
            feat_desc[i] = "repeated"

        "n_samples": n_samples,
        "n_features": n_features,
        "n_informative": n_informative,
        "n_redundant": n_redundant,
        "n_repeated": n_repeated,
        "n_classes": n_classes,
        "n_clusters_per_class": n_clusters_per_class,
        "weights": weights,
        "flip_y": flip_y,
        "class_sep": class_sep,
        "hypercube": hypercube,
        "shift": shift,
        "scale": scale,
        "shuffle": shuffle,
        "random_state": random_state,
        "return_X_y": return_X_y,
    }

    bunch = Bunch(
        DESCR=make_classification.__doc__,
        parameters=parameters,
        feature_info=feat_desc,
        X=X,
        y=y,
    )

def make_multilabel_classification(
    *,
    n_classes=5,
    n_labels=2,
    length=50,
    allow_unlabeled=True,
    sparse=False,
    return_indicator="dense",
    return_distributions=False,
):
    """Generate a random multilabel classification problem.

    For each sample, the generative process is:
        - pick the number of labels: n ~ Poisson(n_labels)
        - n times, choose a class c: c ~ Multinomial(theta)
        - pick the document length: k ~ Poisson(length)
        - k times, choose a word: w ~ Multinomial(theta_c)

    In the above process, rejection sampling is used to make sure that
    n is never zero or more than `n_classes`, and that the document length
    is never zero. Likewise, we reject classes which have already been chosen.

    :ref:`sphx_glr_auto_examples_datasets_plot_random_multilabel_dataset.py`.

        The total number of features.

    n_classes : int, default=5
        The number of classes of the classification problem.

    n_labels : int, default=2
        The average number of labels per instance. More precisely, the number
        of labels per sample is drawn from a Poisson distribution with
        ``n_labels`` as its expected value, but samples are bounded (using
        rejection sampling) by ``n_classes``, and must be nonzero if
        ``allow_unlabeled`` is False.

    length : int, default=50
        The sum of the features (number of words if documents) is drawn from
        a Poisson distribution with this expected value.

    allow_unlabeled : bool, default=True
        If ``True``, some instances might not belong to any class.

    sparse : bool, default=False
        If ``True``, return a sparse feature matrix.

           parameter to allow *sparse* output.

    return_indicator : {'dense', 'sparse'} or False, default='dense'
        If ``'dense'`` return ``Y`` in the dense binary indicator format. If
        ``'sparse'`` return ``Y`` in the sparse binary indicator format.
        ``False`` returns a list of lists of labels.

    return_distributions : bool, default=False
        If ``True``, return the prior class probability and conditional
        probabilities of features given classes, from which the data was
        drawn.

    Returns
    -------
        The generated samples.

    Y : {ndarray, sparse matrix} of shape (n_samples, n_classes)
        The label sets. Sparse matrix should be of CSR format.

    p_c : ndarray of shape (n_classes,)
        The probability of each class being drawn. Only returned if
        ``return_distributions=True``.

    p_w_c : ndarray of shape (n_features, n_classes)
        The probability of each feature being drawn given each class.
        Only returned if ``return_distributions=True``.

    Examples
    --------
    >>> X, y = make_multilabel_classification(n_labels=3, random_state=42)
    (100, 20)
    (100, 5)
    >>> list(y[:3])
    [array([1, 1, 0, 1, 0]), array([0, 1, 1, 1, 0]), array([0, 1, 0, 0, 0])]
    """

    p_c = generator.uniform(size=n_classes)
    p_c /= p_c.sum()
    cumulative_p_c = np.cumsum(p_c)
    p_w_c = generator.uniform(size=(n_features, n_classes))
    p_w_c /= np.sum(p_w_c, axis=0)

    def sample_example():
        _, n_classes = p_w_c.shape

        # pick a nonzero number of labels per document by rejection sampling
        y_size = n_classes + 1
        while (not allow_unlabeled and y_size == 0) or y_size > n_classes:
            y_size = generator.poisson(n_labels)

        # pick n classes
        y = set()
        while len(y) != y_size:
            # pick a class with probability P(c)
            c = np.searchsorted(cumulative_p_c, generator.uniform(size=y_size - len(y)))
            y.update(c)
        y = list(y)

        # pick a non-zero document length by rejection sampling
        n_words = 0
        while n_words == 0:
            n_words = generator.poisson(length)

        # generate a document of length n_words
            # if sample does not belong to any class, generate noise word
            words = generator.randint(n_features, size=n_words)
            return words, y

        # sample words with replacement from selected classes
        cumulative_p_w_sample = p_w_c.take(y, axis=1).sum(axis=1).cumsum()
        cumulative_p_w_sample /= cumulative_p_w_sample[-1]
        words = np.searchsorted(cumulative_p_w_sample, generator.uniform(size=n_words))
        return words, y

    X_indices = array.array("i")
    X_indptr = array.array("i", [0])
    Y = []
    for i in range(n_samples):
        words, y = sample_example()
        X_indices.extend(words)
        X_indptr.append(len(X_indices))
        Y.append(y)
    X_data = np.ones(len(X_indices), dtype=np.float64)
    X = sp.csr_matrix((X_data, X_indices, X_indptr), shape=(n_samples, n_features))
    X.sum_duplicates()
    if not sparse:
        X = X.toarray()

    # return_indicator can be True due to backward compatibility
    if return_indicator in (True, "sparse", "dense"):
        lb = MultiLabelBinarizer(sparse_output=(return_indicator == "sparse"))
        Y = lb.fit([range(n_classes)]).transform(Y)
    if return_distributions:
        return X, Y, p_c, p_w_c
    return X, Y

def make_hastie_10_2(n_samples=12000, *, random_state=None):
    """Generate data for binary classification used in Hastie et al. 2009, Example 10.2.

    The ten features are standard independent Gaussian and
    the target ``y`` is defined by::

      y[i] = 1 if np.sum(X[i] ** 2) > 9.34 else -1

    n_samples : int, default=12000

    Returns
    -------
    X : ndarray of shape (n_samples, 10)
        The input samples.

        The output values.

    See Also
    --------
    make_gaussian_quantiles : A generalization of this dataset approach.

    .. [1] T. Hastie, R. Tibshirani and J. Friedman, "Elements of Statistical
           Learning Ed. 2", Springer, 2009.

    Examples
    --------
    >>> X, y = make_hastie_10_2(n_samples=24000, random_state=42)
    (24000, 10)
    (24000,)
    [np.float64(-1.0), np.float64(1.0), np.float64(-1.0), np.float64(1.0),
    np.float64(-1.0)]
    """
    rs = check_random_state(random_state)

    shape = (n_samples, 10)
    X = rs.normal(size=shape).reshape(shape)
    y = ((X**2.0).sum(axis=1) > 9.34).astype(np.float64, copy=False)
    y[y == 0.0] = -1.0

    return X, y

def make_regression(
    n_features=100,
    *,
    n_informative=10,
    n_targets=1,
    bias=0.0,
    effective_rank=None,
    tail_strength=0.5,
    noise=0.0,
    coef=False,
):
    """Generate a random regression problem.

    The input set can either be well conditioned (by default) or have a low
    rank-fat tail singular profile. See :func:`make_low_rank_matrix` for
    more details.

    The output is generated by applying a (potentially biased) random linear
    regression model with `n_informative` nonzero regressors to the previously
    generated input and some gaussian centered noise with some adjustable
    scale.

    n_features : int, default=100
        The number of features.

    n_informative : int, default=10
        The number of informative features, i.e., the number of features used
        to build the linear model used to generate the output.

    n_targets : int, default=1
        The number of regression targets, i.e., the dimension of the y output
        vector associated with a sample. By default, the output is a scalar.

    bias : float, default=0.0
        The bias term in the underlying linear model.

    effective_rank : int, default=None
        If not None:
            The approximate number of singular vectors required to explain most
            of the input data by linear combinations. Using this kind of
            singular spectrum in the input allows the generator to reproduce
            the correlations often observed in practice.
        If None:
            The input set is well conditioned, centered and gaussian with
            unit variance.

    tail_strength : float, default=0.5
        The relative importance of the fat noisy tail of the singular values
        profile if `effective_rank` is not None. When a float, it should be
        between 0 and 1.

    noise : float, default=0.0
        The standard deviation of the gaussian noise applied to the output.

    coef : bool, default=False
        If True, the coefficients of the underlying linear model are returned.

    Returns
    -------

    y : ndarray of shape (n_samples,) or (n_samples, n_targets)

    coef : ndarray of shape (n_features,) or (n_features, n_targets)
        The coefficient of the underlying linear model. It is returned only if
        coef is True.

    Examples
    --------
    >>> X, y = make_regression(n_samples=5, n_features=2, noise=1, random_state=42)
    >>> X
    array([[ 0.4967..., -0.1382... ],
        [ 0.6476...,  1.523...],
        [-0.2341..., -0.2341...],
        [-0.4694...,  0.5425...],
        [ 1.579...,  0.7674...]])
    >>> y
    array([  6.737...,  37.79..., -10.27...,   0.4017...,   42.22...])
    """
    n_informative = min(n_features, n_informative)

    if effective_rank is None:
        # Randomly generate a well conditioned input set
        X = generator.standard_normal(size=(n_samples, n_features))

    else:
        # Randomly generate a low rank, fat tail input set
        X = make_low_rank_matrix(
            effective_rank=effective_rank,
            tail_strength=tail_strength,
            random_state=generator,
        )

    # Generate a ground truth model with only n_informative features being non
    # zeros (the other features are not correlated to y and should be ignored
    # by a sparsifying regularizers such as L1 or elastic net)
    ground_truth = np.zeros((n_features, n_targets))
    ground_truth[:n_informative, :] = 100 * generator.uniform(
        size=(n_informative, n_targets)
    )

    y = np.dot(X, ground_truth) + bias

    # Add noise
    if noise > 0.0:
        y += generator.normal(scale=noise, size=y.shape)

    # Randomly permute samples and features

        indices = np.arange(n_features)
        ground_truth = ground_truth[indices]

    y = np.squeeze(y)

    if coef:
        return X, y, np.squeeze(ground_truth)

    else:
        return X, y

def make_circles(
    n_samples=100, *, shuffle=True, noise=None, random_state=None, factor=0.8
):
    """Make a large circle containing a smaller circle in 2d.

    A simple toy dataset to visualize clustering and classification
    algorithms.

    n_samples : int or tuple of shape (2,), dtype=int, default=100
        If int, it is the total number of points generated.
        For odd numbers, the inner circle will have one point more than the
        outer circle.
        If two-element tuple, number of points in outer circle and inner
        circle.

           Added two-element tuple.

        Whether to shuffle the samples.

    noise : float, default=None
        Standard deviation of Gaussian noise added to the data.

        Determines random number generation for dataset shuffling and noise.

    factor : float, default=.8
        Scale factor between inner and outer circle in the range `[0, 1)`.

    Returns
    -------
    X : ndarray of shape (n_samples, 2)

        The integer labels (0 or 1) for class membership of each sample.

    Examples
    --------
    >>> from sklearn.datasets import make_circles
    >>> X, y = make_circles(random_state=42)
    (100, 2)
    (100,)
    [np.int64(1), np.int64(1), np.int64(1), np.int64(0), np.int64(0)]
    """
    if isinstance(n_samples, numbers.Integral):
        n_samples_out = n_samples // 2
        n_samples_in = n_samples - n_samples_out
    else:  # n_samples is a tuple
        if len(n_samples) != 2:
            raise ValueError("When a tuple, n_samples must have exactly two elements.")
        n_samples_out, n_samples_in = n_samples

    # so as not to have the first point = last point, we set endpoint=False
    linspace_out = np.linspace(0, 2 * np.pi, n_samples_out, endpoint=False)
    linspace_in = np.linspace(0, 2 * np.pi, n_samples_in, endpoint=False)
    outer_circ_x = np.cos(linspace_out)
    outer_circ_y = np.sin(linspace_out)
    inner_circ_x = np.cos(linspace_in) * factor
    inner_circ_y = np.sin(linspace_in) * factor

    X = np.vstack(
        [np.append(outer_circ_x, inner_circ_x), np.append(outer_circ_y, inner_circ_y)]
    ).T
    y = np.hstack(
        [np.zeros(n_samples_out, dtype=np.intp), np.ones(n_samples_in, dtype=np.intp)]
    )

    if noise is not None:
        X += generator.normal(scale=noise, size=X.shape)

    return X, y

def make_moons(n_samples=100, *, shuffle=True, noise=None, random_state=None):
    """Make two interleaving half circles.

    algorithms. Read more in the :ref:`User Guide <sample_generators>`.

        If int, the total number of points generated.
        If two-element tuple, number of points in each of two moons.

    Returns
    -------

    Examples
    --------
    >>> from sklearn.datasets import make_moons
    >>> X, y = make_moons(n_samples=200, noise=0.2, random_state=42)
    (200, 2)
    (200,)
    """

    else:
        try:
            n_samples_out, n_samples_in = n_samples
        except ValueError as e:
                "`n_samples` can be either an int or a two-element tuple."
            ) from e

    outer_circ_x = np.cos(np.linspace(0, np.pi, n_samples_out))
    outer_circ_y = np.sin(np.linspace(0, np.pi, n_samples_out))
    inner_circ_x = 1 - np.cos(np.linspace(0, np.pi, n_samples_in))
    inner_circ_y = 1 - np.sin(np.linspace(0, np.pi, n_samples_in)) - 0.5

    ).T
    )

    return X, y

def make_blobs(
    n_features=2,
    *,
    centers=None,
    cluster_std=1.0,
    center_box=(-10.0, 10.0),
    return_centers=False,
):
    """Generate isotropic Gaussian blobs for clustering.

    n_samples : int or array-like, default=100
        If int, it is the total number of points equally divided among
        clusters.
        If array-like, each element of the sequence indicates
        the number of samples per cluster.

            one can now pass an array-like to the ``n_samples`` parameter

    n_features : int, default=2
        The number of features for each sample.

    centers : int or array-like of shape (n_centers, n_features), default=None
        The number of centers to generate, or the fixed center locations.
        If n_samples is an int and centers is None, 3 centers are generated.
        If n_samples is array-like, centers must be
        either None or an array of length equal to the length of n_samples.

    cluster_std : float or array-like of float, default=1.0
        The standard deviation of the clusters.

    center_box : tuple of float (min, max), default=(-10.0, 10.0)
        The bounding box for each cluster center when centers are
        generated at random.

        Shuffle the samples.

    return_centers : bool, default=False
        If True, then return the centers of each cluster.

    Returns
    -------

        The integer labels for cluster membership of each sample.

    centers : ndarray of shape (n_centers, n_features)
        The centers of each cluster. Only returned if
        ``return_centers=True``.

    See Also
    --------
    make_classification : A more intricate variant.

    Examples
    --------
    >>> from sklearn.datasets import make_blobs
    >>> X, y = make_blobs(n_samples=10, centers=3, n_features=2,
    ...                   random_state=0)
    (10, 2)
    >>> y
    array([0, 0, 1, 0, 2, 2, 2, 1, 1, 0])
    >>> X, y = make_blobs(n_samples=[3, 3, 4], centers=None, n_features=2,
    (10, 2)
    >>> y
    array([0, 1, 2, 0, 2, 2, 2, 1, 1, 0])
    """

        # Set n_centers by looking at centers arg
        if centers is None:
            centers = 3

        if isinstance(centers, numbers.Integral):
            n_centers = centers
            centers = generator.uniform(
                center_box[0], center_box[1], size=(n_centers, n_features)
            )

        else:
            centers = check_array(centers)
            n_features = centers.shape[1]
            n_centers = centers.shape[0]

    else:
        # Set n_centers by looking at [n_samples] arg
        n_centers = len(n_samples)
            )
        if not isinstance(centers, Iterable):
                "Parameter `centers` must be array-like. Got {!r} instead".format(
                    centers
                )
            )
        if len(centers) != n_centers:
                "Length of `n_samples` not consistent with number of "
                f"centers. Got n_samples = {n_samples} and centers = {centers}"
            )
        centers = check_array(centers)
        n_features = centers.shape[1]

    # stds: if cluster_std is given as list, it must be consistent
    # with the n_centers
    if hasattr(cluster_std, "__len__") and len(cluster_std) != n_centers:
            "Length of `clusters_std` not consistent with "
            "number of centers. Got centers = {} "
            "and cluster_std = {}".format(centers, cluster_std)
        )

    if isinstance(cluster_std, numbers.Real):
        cluster_std = np.full(len(centers), cluster_std)

    if isinstance(n_samples, Iterable):
        n_samples_per_center = n_samples
    else:
        n_samples_per_center = [int(n_samples // n_centers)] * n_centers

        for i in range(n_samples % n_centers):
            n_samples_per_center[i] += 1

    cum_sum_n_samples = np.cumsum(n_samples_per_center)
    X = np.empty(shape=(sum(n_samples_per_center), n_features), dtype=np.float64)
    y = np.empty(shape=(sum(n_samples_per_center),), dtype=int)

    for i, (n, std) in enumerate(zip(n_samples_per_center, cluster_std)):
        start_idx = cum_sum_n_samples[i - 1] if i > 0 else 0
        end_idx = cum_sum_n_samples[i]
        X[start_idx:end_idx] = generator.normal(
            loc=centers[i], scale=std, size=(n, n_features)
        )
        y[start_idx:end_idx] = i

    if return_centers:
        return X, y, centers
    else:
        return X, y

def make_friedman1(n_samples=100, n_features=10, *, noise=0.0, random_state=None):
    """Generate the "Friedman #1" regression problem.

    This dataset is described in Friedman [1] and Breiman [2].

    Inputs `X` are independent features uniformly distributed on the interval
    [0, 1]. The output `y` is created according to the formula::

        y(X) = 10 * sin(pi * X[:, 0] * X[:, 1]) + 20 * (X[:, 2] - 0.5) ** 2 \
+ 10 * X[:, 3] + 5 * X[:, 4] + noise * N(0, 1).

    Out of the `n_features` features, only 5 are actually used to compute
    `y`. The remaining features are independent of `y`.

    The number of features has to be >= 5.

    n_features : int, default=10
        The number of features. Should be at least 5.

        Determines random number generation for dataset noise. Pass an int

    Returns
    -------

    .. [1] J. Friedman, "Multivariate adaptive regression splines", The Annals
           of Statistics 19 (1), pages 1-67, 1991.

    .. [2] L. Breiman, "Bagging predictors", Machine Learning 24,
           pages 123-140, 1996.

    Examples
    --------
    >>> from sklearn.datasets import make_friedman1
    >>> X, y = make_friedman1(random_state=42)
    (100, 10)
    (100,)
    [np.float64(16.8...), np.float64(5.8...), np.float64(9.4...)]
    """

    X = generator.uniform(size=(n_samples, n_features))
    y = (
        10 * np.sin(np.pi * X[:, 0] * X[:, 1])
        + 20 * (X[:, 2] - 0.5) ** 2
        + 10 * X[:, 3]
        + 5 * X[:, 4]
        + noise * generator.standard_normal(size=(n_samples))
    )

    return X, y

def make_friedman2(n_samples=100, *, noise=0.0, random_state=None):
    """Generate the "Friedman #2" regression problem.

    Inputs `X` are 4 independent features uniformly distributed on the
    intervals::

        0 <= X[:, 0] <= 100,
        40 * pi <= X[:, 1] <= 560 * pi,
        0 <= X[:, 2] <= 1,
        1 <= X[:, 3] <= 11.

    The output `y` is created according to the formula::

        y(X) = (X[:, 0] ** 2 + (X[:, 1] * X[:, 2] \
 - 1 / (X[:, 1] * X[:, 3])) ** 2) ** 0.5 + noise * N(0, 1).

    Returns
    -------
    X : ndarray of shape (n_samples, 4)

    Examples
    --------
    >>> X, y = make_friedman2(random_state=42)
    (100, 4)
    (100,)
    [np.float64(1229.4...), np.float64(27.0...), np.float64(65.6...)]
    """

    X = generator.uniform(size=(n_samples, 4))
    X[:, 0] *= 100
    X[:, 1] *= 520 * np.pi
    X[:, 1] += 40 * np.pi
    X[:, 3] *= 10
    X[:, 3] += 1

    y = (
        X[:, 0] ** 2 + (X[:, 1] * X[:, 2] - 1 / (X[:, 1] * X[:, 3])) ** 2
    ) ** 0.5 + noise * generator.standard_normal(size=(n_samples))

    return X, y

def make_friedman3(n_samples=100, *, noise=0.0, random_state=None):
    """Generate the "Friedman #3" regression problem.

        y(X) = arctan((X[:, 1] * X[:, 2] - 1 / (X[:, 1] * X[:, 3])) \
/ X[:, 0]) + noise * N(0, 1).

    Returns
    -------

    Examples
    --------
    >>> from sklearn.datasets import make_friedman3
    >>> X, y = make_friedman3(random_state=42)
    (100, 4)
    (100,)
    [np.float64(1.5...), np.float64(0.9...), np.float64(0.4...)]
    """

    X[:, 3] += 1

    y = np.arctan(
        (X[:, 1] * X[:, 2] - 1 / (X[:, 1] * X[:, 3])) / X[:, 0]
    ) + noise * generator.standard_normal(size=(n_samples))

    return X, y

def make_low_rank_matrix(
    *,
    effective_rank=10,
):
    """Generate a mostly low rank matrix with bell-shaped singular values.

    Most of the variance can be explained by a bell-shaped curve of width
    effective_rank: the low rank part of the singular values profile is::

        (1 - tail_strength) * exp(-1.0 * (i / effective_rank) ** 2)

    The remaining singular values' tail is fat, decreasing as::

        tail_strength * exp(-0.1 * i / effective_rank).

    The low rank part of the profile can be considered the structured
    signal part of the data while the tail can be considered the noisy
    part of the data that cannot be summarized by a low number of linear
    components (singular vectors).

    This kind of singular profiles is often seen in practice, for instance:
     - gray level pictures of faces
     - TF-IDF vectors of text documents crawled from the web

    effective_rank : int, default=10
        The approximate number of singular vectors required to explain most of
        the data by linear combinations.

        profile. The value should be between 0 and 1.

    Returns
    -------
        The matrix.

    Examples
    --------
    >>> from numpy.linalg import svd
    >>> from sklearn.datasets import make_low_rank_matrix
    >>> X = make_low_rank_matrix(
    ...     n_samples=50,
    ...     n_features=25,
    ...     effective_rank=5,
    ...     tail_strength=0.01,
    ...     random_state=0,
    ... )
    (50, 25)
    """
    n = min(n_samples, n_features)

    # Random (ortho normal) vectors
    u, _ = linalg.qr(
        generator.standard_normal(size=(n_samples, n)),
        mode="economic",
        check_finite=False,
    )
    v, _ = linalg.qr(
        generator.standard_normal(size=(n_features, n)),
    )

    # Index of the singular values
    singular_ind = np.arange(n, dtype=np.float64)

    # Build the singular profile by assembling signal and noise components
    low_rank = (1 - tail_strength) * np.exp(-1.0 * (singular_ind / effective_rank) ** 2)
    tail = tail_strength * np.exp(-0.1 * singular_ind / effective_rank)
    s = np.identity(n) * (low_rank + tail)

    return np.dot(np.dot(u, s), v.T)

def make_sparse_coded_signal(
    *,
    n_components,
    n_nonzero_coefs,
):
    """Generate a signal as a sparse combination of dictionary elements.

    Returns matrices `Y`, `D` and `X` such that `Y = XD` where `X` is of shape
    `(n_samples, n_components)`, `D` is of shape `(n_components, n_features)`, and
    each row of `X` has exactly `n_nonzero_coefs` non-zero elements.

        Number of samples to generate.

    n_components : int
        Number of components in the dictionary.

        Number of features of the dataset to generate.

    n_nonzero_coefs : int
        Number of active (non-zero) coefficients in each sample.

    Returns
    -------
        The encoded signal (Y).

    dictionary : ndarray of shape (n_components, n_features)
        The dictionary with normalized components (D).

    code : ndarray of shape (n_samples, n_components)
        The sparse code such that each column of this matrix has exactly
        n_nonzero_coefs non-zero items (X).

    Examples
    --------
    >>> from sklearn.datasets import make_sparse_coded_signal
    >>> data, dictionary, code = make_sparse_coded_signal(
    ...     n_components=100,
    ...     n_features=10,
    ...     n_nonzero_coefs=4,
    ...     random_state=0
    ... )
    >>> data.shape
    (50, 10)
    >>> dictionary.shape
    (100, 10)
    >>> code.shape
    (50, 100)
    """

    # generate dictionary
    D = generator.standard_normal(size=(n_features, n_components))
    D /= np.sqrt(np.sum((D**2), axis=0))

    # generate code
    X = np.zeros((n_components, n_samples))
        idx = np.arange(n_components)
        generator.shuffle(idx)
        idx = idx[:n_nonzero_coefs]
        X[idx, i] = generator.standard_normal(size=n_nonzero_coefs)

    # encode signal
    Y = np.dot(D, X)

    # Transpose to have shapes consistent with the rest of the API
    Y, D, X = Y.T, D.T, X.T

    return map(np.squeeze, (Y, D, X))

def make_sparse_uncorrelated(n_samples=100, n_features=10, *, random_state=None):
    """Generate a random regression problem with sparse uncorrelated design.

    This dataset is described in Celeux et al [1]. as::

        X ~ N(0, 1)
        y(X) = X[:, 0] + 2 * X[:, 1] - 2 * X[:, 2] - 1.5 * X[:, 3]

    Only the first 4 features are informative. The remaining features are
    useless.

    Returns
    -------

    .. [1] G. Celeux, M. El Anbari, J.-M. Marin, C. P. Robert,
           "Regularization in regression: comparing Bayesian and frequentist
           methods in a poorly informative situation", 2009.

    Examples
    --------
    >>> from sklearn.datasets import make_sparse_uncorrelated
    >>> X, y = make_sparse_uncorrelated(random_state=0)
    (100, 10)
    (100,)
    """

    X = generator.normal(loc=0, scale=1, size=(n_samples, n_features))
    y = generator.normal(
        loc=(X[:, 0] + 2 * X[:, 1] - 2 * X[:, 2] - 1.5 * X[:, 3]),
        scale=np.ones(n_samples),
    )

    return X, y

def make_spd_matrix(n_dim, *, random_state=None):
    """Generate a random symmetric, positive-definite matrix.

    n_dim : int
        The matrix dimension.

    Returns
    -------
    X : ndarray of shape (n_dim, n_dim)
        The random symmetric, positive-definite matrix.

    See Also
    --------
    make_sparse_spd_matrix: Generate a sparse symmetric definite positive matrix.

    Examples
    --------
    >>> from sklearn.datasets import make_spd_matrix
    >>> make_spd_matrix(n_dim=2, random_state=42)
    array([[2.09..., 0.34...],
           [0.34..., 0.21...]])
    """

    A = generator.uniform(size=(n_dim, n_dim))
    U, _, Vt = linalg.svd(np.dot(A.T, A), check_finite=False)
    X = np.dot(np.dot(U, 1.0 + np.diag(generator.uniform(size=n_dim))), Vt)

    return X

def make_sparse_spd_matrix(
    n_dim=1,
    *,
    alpha=0.95,
    norm_diag=False,
    smallest_coef=0.1,
    largest_coef=0.9,
    sparse_format=None,
):
    """Generate a sparse symmetric definite positive matrix.

    n_dim : int, default=1
        The size of the random matrix to generate.

            Renamed from ``dim`` to ``n_dim``.

    alpha : float, default=0.95
        The probability that a coefficient is zero (see notes). Larger values
        enforce more sparsity. The value should be in the range 0 and 1.

    norm_diag : bool, default=False
        Whether to normalize the output matrix to make the leading diagonal
        elements all 1.

    smallest_coef : float, default=0.1
        The value of the smallest coefficient between 0 and 1.

    largest_coef : float, default=0.9
        The value of the largest coefficient between 0 and 1.

    sparse_format : str, default=None
        String representing the output sparse format, such as 'csc', 'csr', etc.
        If ``None``, return a dense numpy ndarray.

    Returns
    -------
    prec : ndarray or sparse matrix of shape (dim, dim)
        The generated matrix. If ``sparse_format=None``, this would be an ndarray.
        Otherwise, this will be a sparse matrix of the specified format.

    See Also
    --------
    make_spd_matrix : Generate a random symmetric, positive-definite matrix.

    Notes
    -----
    The sparsity is actually imposed on the cholesky factor of the matrix.
    Thus alpha does not translate directly into the filling fraction of
    the matrix itself.

    Examples
    --------
    >>> make_sparse_spd_matrix(n_dim=4, norm_diag=False, random_state=42)
    array([[1., 0., 0., 0.],
           [0., 1., 0., 0.],
           [0., 0., 1., 0.],
           [0., 0., 0., 1.]])
    """

    chol = -sp.eye(n_dim)
    aux = sp.random(
        m=n_dim,
        n=n_dim,
        density=1 - alpha,
        data_rvs=lambda x: random_state.uniform(
            low=smallest_coef, high=largest_coef, size=x
        ),
    )
    # We need to avoid "coo" format because it does not support slicing
    aux = sp.tril(aux, k=-1, format="csc")

    # Permute the lines: we don't want to have asymmetries in the final
    # SPD matrix
    permutation = random_state.permutation(n_dim)
    aux = aux[permutation].T[permutation]
    chol += aux
    prec = chol.T @ chol

    if norm_diag:
        # Form the diagonal vector into a row matrix
        d = sp.diags(1.0 / np.sqrt(prec.diagonal()))
        prec = d @ prec @ d

    if sparse_format is None:
        return prec.toarray()
    else:
        return prec.asformat(sparse_format)

def make_swiss_roll(n_samples=100, *, noise=0.0, random_state=None, hole=False):
    """Generate a swiss roll dataset.

        The number of sample points on the Swiss Roll.

        The standard deviation of the gaussian noise.

    hole : bool, default=False
        If True generates the swiss roll with hole dataset.

    Returns
    -------
    X : ndarray of shape (n_samples, 3)
        The points.

    t : ndarray of shape (n_samples,)
        The univariate position of the sample according to the main dimension
        of the points in the manifold.

    Notes
    -----
    The algorithm is from Marsland [1].

    .. [1] S. Marsland, "Machine Learning: An Algorithmic Perspective", 2nd edition,
           Chapter 6, 2014.
           https://homepages.ecs.vuw.ac.nz/~marslast/Code/Ch6/lle.py

    Examples
    --------
    >>> from sklearn.datasets import make_swiss_roll
    >>> X, t = make_swiss_roll(noise=0.05, random_state=0)
    (100, 3)
    >>> t.shape
    (100,)
    """

    if not hole:
        t = 1.5 * np.pi * (1 + 2 * generator.uniform(size=n_samples))
        y = 21 * generator.uniform(size=n_samples)
    else:
        corners = np.array(
            [[np.pi * (1.5 + i), j * 7] for i in range(3) for j in range(3)]
        )
        corners = np.delete(corners, 4, axis=0)
        corner_index = generator.choice(8, n_samples)
        parameters = generator.uniform(size=(2, n_samples)) * np.array([[np.pi], [7]])
        t, y = corners[corner_index].T + parameters

    x = t * np.cos(t)
    z = t * np.sin(t)

    X = np.vstack((x, y, z))
    X += noise * generator.standard_normal(size=(3, n_samples))
    X = X.T
    t = np.squeeze(t)

    return X, t

def make_s_curve(n_samples=100, *, noise=0.0, random_state=None):
    """Generate an S curve dataset.

        The number of sample points on the S curve.

    Returns
    -------

        The univariate position of the sample according
        to the main dimension of the points in the manifold.

    Examples
    --------
    >>> from sklearn.datasets import make_s_curve
    >>> X, t = make_s_curve(noise=0.05, random_state=0)
    (100, 3)
    (100,)
    """

    t = 3 * np.pi * (generator.uniform(size=(1, n_samples)) - 0.5)
    X = np.empty(shape=(n_samples, 3), dtype=np.float64)
    X[:, 0] = np.sin(t)
    X[:, 1] = 2.0 * generator.uniform(size=n_samples)
    X[:, 2] = np.sign(t) * (np.cos(t) - 1)
    X += noise * generator.standard_normal(size=(3, n_samples)).T

    return X, t

def make_gaussian_quantiles(
    *,
    mean=None,
    cov=1.0,
    n_classes=3,
):
    r"""Generate isotropic Gaussian and label samples by quantile.

    This classification dataset is constructed by taking a multi-dimensional
    standard normal distribution and defining classes separated by nested
    concentric multi-dimensional spheres such that roughly equal numbers of
    samples are in each class (quantiles of the :math:`\chi^2` distribution).

    mean : array-like of shape (n_features,), default=None
        The mean of the multi-dimensional normal distribution.
        If None then use the origin (0, 0, ...).

    cov : float, default=1.0
        The covariance matrix will be this value times the unit matrix. This
        dataset only produces symmetric normal distributions.

        The total number of points equally divided among classes.

    n_classes : int, default=3

    Returns
    -------

        The integer labels for quantile membership of each sample.

    Notes
    -----
    The dataset is from Zhu et al [1].

    Examples
    --------
    >>> X, y = make_gaussian_quantiles(random_state=42)
    (100, 2)
    (100,)
    [np.int64(2), np.int64(0), np.int64(1), np.int64(0), np.int64(2)]
    """
    if n_samples < n_classes:
        raise ValueError("n_samples must be at least n_classes")

    if mean is None:
        mean = np.zeros(n_features)
    else:
        mean = np.array(mean)

    # Build multivariate normal distribution
    X = generator.multivariate_normal(mean, cov * np.identity(n_features), (n_samples,))

    # Sort by distance from origin
    idx = np.argsort(np.sum((X - mean[np.newaxis, :]) ** 2, axis=1))
    X = X[idx, :]

    # Label by quantile
    step = n_samples // n_classes

        [
            np.repeat(np.arange(n_classes), step),
            np.repeat(n_classes - 1, n_samples - step * n_classes),
        ]
    )

    return X, y

def _shuffle(data, random_state=None):
    n_rows, n_cols = data.shape
    row_idx = generator.permutation(n_rows)
    col_idx = generator.permutation(n_cols)
    result = data[row_idx][:, col_idx]
    return result, row_idx, col_idx

def make_biclusters(
    shape,
    n_clusters,
    *,
    minval=10,
    maxval=100,
):
    """Generate a constant block diagonal structure array for biclustering.

    shape : tuple of shape (n_rows, n_cols)
        The shape of the result.

    n_clusters : int
        The number of biclusters.

    minval : float, default=10
        Minimum value of a bicluster.

    maxval : float, default=100
        Maximum value of a bicluster.

    Returns
    -------
    X : ndarray of shape `shape`
        The generated array.

    rows : ndarray of shape (n_clusters, X.shape[0])
        The indicators for cluster membership of each row.

    cols : ndarray of shape (n_clusters, X.shape[1])
        The indicators for cluster membership of each column.

    See Also
    --------
    make_checkerboard: Generate an array with block checkerboard structure for
        biclustering.

    .. [1] Dhillon, I. S. (2001, August). Co-clustering documents and
        words using bipartite spectral graph partitioning. In Proceedings
        of the seventh ACM SIGKDD international conference on Knowledge
        discovery and data mining (pp. 269-274). ACM.

    Examples
    --------
    >>> from sklearn.datasets import make_biclusters
    >>> data, rows, cols = make_biclusters(
    ...     shape=(10, 20), n_clusters=2, random_state=42
    ... )
    (10, 20)
    >>> rows.shape
    (2, 10)
    >>> cols.shape
    (2, 20)
    """
    n_rows, n_cols = shape
    consts = generator.uniform(minval, maxval, n_clusters)

    # row and column clusters of approximately equal sizes
    row_sizes = generator.multinomial(n_rows, np.repeat(1.0 / n_clusters, n_clusters))
    col_sizes = generator.multinomial(n_cols, np.repeat(1.0 / n_clusters, n_clusters))

    row_labels = np.hstack(
        [np.repeat(val, rep) for val, rep in zip(range(n_clusters), row_sizes)]
    )
    col_labels = np.hstack(
        [np.repeat(val, rep) for val, rep in zip(range(n_clusters), col_sizes)]
    )

    result = np.zeros(shape, dtype=np.float64)
    for i in range(n_clusters):
        selector = np.outer(row_labels == i, col_labels == i)
        result[selector] += consts[i]

    if noise > 0:
        result += generator.normal(scale=noise, size=result.shape)

        result, row_idx, col_idx = _shuffle(result, random_state)
        row_labels = row_labels[row_idx]
        col_labels = col_labels[col_idx]

    rows = np.vstack([row_labels == c for c in range(n_clusters)])
    cols = np.vstack([col_labels == c for c in range(n_clusters)])

    return result, rows, cols

def make_checkerboard(
    shape,
    *,
):
    """Generate an array with block checkerboard structure for biclustering.

    n_clusters : int or array-like or shape (n_row_clusters, n_column_clusters)
        The number of row and column clusters.

    Returns
    -------

    See Also
    --------
    make_biclusters : Generate an array with constant block diagonal structure
        for biclustering.

    .. [1] Kluger, Y., Basri, R., Chang, J. T., & Gerstein, M. (2003).
        Spectral biclustering of microarray data: coclustering genes
        and conditions. Genome research, 13(4), 703-716.

    Examples
    --------
    >>> from sklearn.datasets import make_checkerboard
    >>> data, rows, columns = make_checkerboard(shape=(300, 300), n_clusters=10,
    ...                                         random_state=42)
    (300, 300)
    (100, 300)
    >>> columns.shape
    (100, 300)
    >>> print(rows[0][:5], columns[0][:5])
    [False False False  True False] [False False False False False]
    """

    if hasattr(n_clusters, "__len__"):
        n_row_clusters, n_col_clusters = n_clusters
    else:
        n_row_clusters = n_col_clusters = n_clusters

    row_sizes = generator.multinomial(
        n_rows, np.repeat(1.0 / n_row_clusters, n_row_clusters)
    )
    col_sizes = generator.multinomial(
        n_cols, np.repeat(1.0 / n_col_clusters, n_col_clusters)
    )

        [np.repeat(val, rep) for val, rep in zip(range(n_row_clusters), row_sizes)]
    )
        [np.repeat(val, rep) for val, rep in zip(range(n_col_clusters), col_sizes)]
    )

    for i in range(n_row_clusters):
        for j in range(n_col_clusters):
            selector = np.outer(row_labels == i, col_labels == j)
            result[selector] += generator.uniform(minval, maxval)

    rows = np.vstack(
        [
            row_labels == label
            for label in range(n_row_clusters)
            for _ in range(n_col_clusters)
        ]
    )
    cols = np.vstack(
        [
            col_labels == label
            for _ in range(n_row_clusters)
            for label in range(n_col_clusters)
        ]
    )

        y = set()
        y = list(y)

        n_words = 0

def fetch_covtype(
    *,
):
    """Load the covertype dataset (classification).

    =================   ============
    Classes                        7
    Samples total             581012
    Dimensionality                54
    Features                     int

    Read more in the :ref:`User Guide <covtype_dataset>`.

        If True, returns ``(data.data, data.target)`` instead of a Bunch
        object.

        appropriate dtypes (numeric). The target is a pandas DataFrame or
        Series depending on the number of target columns. If `return_X_y` is
        True, then (`data`, `target`) will be pandas DataFrames or Series as
        described below.

    Returns
    -------

        data : ndarray of shape (581012, 54)
            Each row corresponds to the 54 features in the dataset.
        target : ndarray of shape (581012,)
            Each value corresponds to one of
            the 7 forest covertypes with values
            ranging between 1 to 7.
        frame : dataframe of shape (581012, 55)
        DESCR : str
            Description of the forest covertype dataset.

    Examples
    --------
    >>> from sklearn.datasets import fetch_covtype
    >>> cov_type = fetch_covtype()
    >>> cov_type.data.shape
    (581012, 54)
    >>> cov_type.target.shape
    (581012,)
    >>> # Let's check the 4 first feature names
    >>> cov_type.feature_names[:4]
    ['Elevation', 'Aspect', 'Slope', 'Horizontal_Distance_To_Hydrology']
    """
    covtype_dir = join(data_home, "covertype")
    samples_path = _pkl_filepath(covtype_dir, "samples")
    targets_path = _pkl_filepath(covtype_dir, "targets")
    available = exists(samples_path) and exists(targets_path)

    if download_if_missing and not available:
        os.makedirs(covtype_dir, exist_ok=True)

        # Creating temp_dir as a direct subdirectory of the target directory
        # guarantees that both reside on the same filesystem, so that we can use
        # os.rename to atomically move the data files to their target location.
        with TemporaryDirectory(dir=covtype_dir) as temp_dir:
            logger.info(f"Downloading {ARCHIVE.url}")
            archive_path = _fetch_remote(
                ARCHIVE, dirname=temp_dir, n_retries=n_retries, delay=delay
            )
            Xy = np.genfromtxt(GzipFile(filename=archive_path), delimiter=",")

            X = Xy[:, :-1]
            y = Xy[:, -1].astype(np.int32, copy=False)

            samples_tmp_path = _pkl_filepath(temp_dir, "samples")
            joblib.dump(X, samples_tmp_path, compress=9)
            os.rename(samples_tmp_path, samples_path)

            targets_tmp_path = _pkl_filepath(temp_dir, "targets")
            joblib.dump(y, targets_tmp_path, compress=9)
            os.rename(targets_tmp_path, targets_path)

    elif not available and not download_if_missing:
    try:
        X, y
    except NameError:
        y = joblib.load(targets_path)

        ind = np.arange(X.shape[0])
        rng.shuffle(ind)
        X = X[ind]
        y = y[ind]

    fdescr = load_descr("covtype.rst")

        frame, X, y = _convert_data_dataframe(
            caller_name="fetch_covtype",
            data=X,
            target=y,
            feature_names=FEATURE_NAMES,
            target_names=TARGET_NAMES,
        )
        return X, y

        data=X,
        target=y,
        target_names=TARGET_NAMES,
        feature_names=FEATURE_NAMES,
    )

def _download_20newsgroups(target_dir, cache_path, n_retries, delay):
    """Download the 20 newsgroups data and stored it as a zipped pickle."""
    train_path = os.path.join(target_dir, TRAIN_FOLDER)
    test_path = os.path.join(target_dir, TEST_FOLDER)

    os.makedirs(target_dir, exist_ok=True)

    logger.info("Downloading dataset from %s (14 MB)", ARCHIVE.url)
    archive_path = _fetch_remote(
        ARCHIVE, dirname=target_dir, n_retries=n_retries, delay=delay
    )

    logger.debug("Decompressing %s", archive_path)
    with tarfile.open(archive_path, "r:gz") as fp:
        # Use filter="data" to prevent the most dangerous security issues.
        # For more details, see
        # https://docs.python.org/3.9/library/tarfile.html#tarfile.TarFile.extractall
        fp.extractall(path=target_dir, filter="data")

    with suppress(FileNotFoundError):

    # Store a zipped pickle
    cache = dict(
        train=load_files(train_path, encoding="latin1"),
        test=load_files(test_path, encoding="latin1"),
    )
    compressed_content = codecs.encode(pickle.dumps(cache), "zlib_codec")
    with open(cache_path, "wb") as f:
        f.write(compressed_content)

    shutil.rmtree(target_dir)
    return cache

def strip_newsgroup_header(text):
    """
    Given text in "news" format, strip the headers, by removing everything
    before the first blank line.

    text : str
        The text from which to remove the signature block.
    """
    _before, _blankline, after = text.partition("\n\n")
    return after

def strip_newsgroup_quoting(text):
    """
    Given text in "news" format, strip lines beginning with the quote
    characters > or |, plus lines that often introduce a quoted section
    (for example, because they contain the string 'writes:'.)

    text : str
    """
    good_lines = [line for line in text.split("\n") if not _QUOTE_RE.search(line)]
    return "\n".join(good_lines)

def strip_newsgroup_footer(text):
    """
    Given text in "news" format, attempt to remove a signature block.

    As a rough heuristic, we assume that signatures are set apart by either
    a blank line or a line made of hyphens, and that it is the last such line
    in the file (disregarding blank lines at the end).

    text : str
    """
    lines = text.strip().split("\n")
    for line_num in range(len(lines) - 1, -1, -1):
        line = lines[line_num]
        if line.strip().strip("-") == "":
            break

    if line_num > 0:
        return "\n".join(lines[:line_num])
    else:
        return text

def fetch_20newsgroups(
    *,
    random_state=42,
    remove=(),
):
    """Load the filenames and data from the 20 newsgroups dataset \

    =================   ==========
    Classes                     20
    Samples total            18846
    Dimensionality               1
    Features                  text

    Read more in the :ref:`User Guide <20newsgroups_dataset>`.

        Specify a download and cache folder for the datasets. If None,

    subset : {'train', 'test', 'all'}, default='train'
        Select the dataset to load: 'train' for the training set, 'test'
        for the test set, 'all' for both, with shuffled ordering.

    categories : array-like, dtype=str, default=None
        If None (default), load all the categories.
        If not None, list of category names to load (other categories
        ignored).

    random_state : int, RandomState instance or None, default=42

    remove : tuple, default=()
        May contain any subset of ('headers', 'footers', 'quotes'). Each of
        these are kinds of text that will be detected and removed from the
        newsgroup posts, preventing classifiers from overfitting on
        metadata.

        'headers' removes newsgroup headers, 'footers' removes blocks at the
        ends of posts that look like signatures, and 'quotes' removes lines
        that appear to be quoting another post.

        'headers' follows an exact standard; the other filters are not always
        correct.

        If True, returns `(data.data, data.target)` instead of a Bunch
        object.

    Returns
    -------
    bunch : :class:`~sklearn.utils.Bunch`

        data : list of shape (n_samples,)
            The data list to learn.
        target: ndarray of shape (n_samples,)
            The target labels.
        filenames: list of shape (n_samples,)
        DESCR: str
        target_names: list of shape (n_classes,)

    (data, target) : tuple if `return_X_y=True`
        A tuple of two ndarrays. The first contains a 2D array of shape
        (n_samples, n_classes) with each row representing one sample and each
        column representing the features. The second array of shape
        (n_samples,) contains the target samples.

    Examples
    --------
    >>> from sklearn.datasets import fetch_20newsgroups
    >>> cats = ['alt.atheism', 'sci.space']
    >>> newsgroups_train = fetch_20newsgroups(subset='train', categories=cats)
    >>> list(newsgroups_train.target_names)
    ['alt.atheism', 'sci.space']
    >>> newsgroups_train.filenames.shape
    (1073,)
    >>> newsgroups_train.target.shape
    (1073,)
    >>> newsgroups_train.target[:10]
    array([0, 1, 1, 1, 0, 1, 1, 0, 0, 0])
    """

    cache_path = _pkl_filepath(data_home, CACHE_NAME)
    twenty_home = os.path.join(data_home, "20news_home")
    cache = None
    if os.path.exists(cache_path):
        try:
            with open(cache_path, "rb") as f:
                compressed_content = f.read()
            uncompressed_content = codecs.decode(compressed_content, "zlib_codec")
            cache = pickle.loads(uncompressed_content)
            print(80 * "_")
            print("Cache loading failed")
            print(e)

    if cache is None:
        if download_if_missing:
            logger.info("Downloading 20news dataset. This may take a few minutes.")
            cache = _download_20newsgroups(
                target_dir=twenty_home,
                cache_path=cache_path,
                n_retries=n_retries,
                delay=delay,
            )
        else:
            raise OSError("20Newsgroups dataset not found")

    if subset in ("train", "test"):
        data = cache[subset]
    elif subset == "all":
        data_lst = list()
        target = list()
        filenames = list()
        for subset in ("train", "test"):
            data = cache[subset]
            data_lst.extend(data.data)
            target.extend(data.target)
            filenames.extend(data.filenames)

        data.data = data_lst
        data.target = np.array(target)
        data.filenames = np.array(filenames)

    fdescr = load_descr("twenty_newsgroups.rst")

    data.DESCR = fdescr

    if "headers" in remove:
        data.data = [strip_newsgroup_header(text) for text in data.data]
    if "footers" in remove:
        data.data = [strip_newsgroup_footer(text) for text in data.data]
    if "quotes" in remove:
        data.data = [strip_newsgroup_quoting(text) for text in data.data]

        labels = [(data.target_names.index(cat), cat) for cat in categories]
        # Sort the categories to have the ordering of the labels
        labels.sort()
        labels, categories = zip(*labels)
        mask = np.isin(data.target, labels)
        data.filenames = data.filenames[mask]
        data.target = data.target[mask]
        # searchsorted to have continuous labels
        data.target = np.searchsorted(labels, data.target)
        data.target_names = list(categories)
        # Use an object array to shuffle: avoids memory copy
        data_lst = np.array(data.data, dtype=object)
        data_lst = data_lst[mask]
        data.data = data_lst.tolist()

        indices = np.arange(data.target.shape[0])
        data.filenames = data.filenames[indices]
        data.target = data.target[indices]
        data_lst = data_lst[indices]

        return data.data, data.target

    return data

def fetch_20newsgroups_vectorized(
    *,
    normalize=True,
):
    """Load and vectorize the 20 newsgroups dataset (classification).

    This is a convenience function; the transformation is done using the
    default settings for
    :class:`~sklearn.feature_extraction.text.CountVectorizer`. For more
    advanced usage (stopword filtering, n-gram extraction, etc.), combine
    fetch_20newsgroups with a custom
    :class:`~sklearn.feature_extraction.text.CountVectorizer`,
    :class:`~sklearn.feature_extraction.text.HashingVectorizer`,
    :class:`~sklearn.feature_extraction.text.TfidfTransformer` or
    :class:`~sklearn.feature_extraction.text.TfidfVectorizer`.

    The resulting counts are normalized using
    :func:`sklearn.preprocessing.normalize` unless normalize is set to False.

    Classes                     20
    Dimensionality          130107
    Features                  real

        metadata.

        Specify an download and cache folder for the datasets. If None,

        object.

    normalize : bool, default=True
        If True, normalizes each document's feature vector to unit norm using
        :func:`sklearn.preprocessing.normalize`.

        appropriate dtypes (numeric, string, or categorical). The target is
        a pandas DataFrame or Series depending on the number of
        `target_columns`.

    Returns
    -------

        data: {sparse matrix, dataframe} of shape (n_samples, n_features)
            The input data matrix. If ``as_frame`` is `True`, ``data`` is
            a pandas DataFrame with sparse columns.
        target: {ndarray, series} of shape (n_samples,)
            The target labels. If ``as_frame`` is `True`, ``target`` is a
            pandas Series.
        DESCR: str
        frame: dataframe of shape (n_samples, n_features + 1)
            Only present when `as_frame=True`. Pandas DataFrame with ``data``
            and ``target``.

        `data` and `target` would be of the format defined in the `Bunch`
        description above.

    Examples
    --------
    >>> from sklearn.datasets import fetch_20newsgroups_vectorized
    >>> newsgroups_vectorized = fetch_20newsgroups_vectorized(subset='test')
    >>> newsgroups_vectorized.data.shape
    (7532, 130107)
    >>> newsgroups_vectorized.target.shape
    (7532,)
    """
    filebase = "20newsgroup_vectorized"
    if remove:
        filebase += "remove-" + "-".join(remove)
    target_file = _pkl_filepath(data_home, filebase + ".pkl")

    # we shuffle but use a fixed seed for the memoization
    data_train = fetch_20newsgroups(
        subset="train",
        categories=None,
        shuffle=True,
        random_state=12,
        remove=remove,
    )

    data_test = fetch_20newsgroups(
        subset="test",
    )

    if os.path.exists(target_file):
        try:
            X_train, X_test, feature_names = joblib.load(target_file)
                f"The cached dataset located in {target_file} was fetched "
                "with an older scikit-learn version and it is not compatible "
                "with the scikit-learn version imported. You need to "
                f"manually delete the file: {target_file}."
            ) from e
    else:
        vectorizer = CountVectorizer(dtype=np.int16)
        X_train = vectorizer.fit_transform(data_train.data).tocsr()
        X_test = vectorizer.transform(data_test.data).tocsr()
        feature_names = vectorizer.get_feature_names_out()

        joblib.dump((X_train, X_test, feature_names), target_file, compress=9)

    # the data is stored as int16 for compactness
    # but normalize needs floats
    if normalize:
        X_train = X_train.astype(np.float64)
        X_test = X_test.astype(np.float64)
        preprocessing.normalize(X_train, copy=False)
        preprocessing.normalize(X_test, copy=False)

    target_names = data_train.target_names

    if subset == "train":
        data = X_train
        target = data_train.target
        data = X_test
        target = data_test.target
        data = sp.vstack((X_train, X_test)).tocsr()
        target = np.concatenate((data_train.target, data_test.target))

    target_name = ["category_class"]

            "fetch_20newsgroups_vectorized",
            data,
            target,
            feature_names,
            target_names=target_name,
            sparse_data=True,
        )

    )

def fetch_california_housing(
    *,
):
    """Load the California housing dataset (regression).

    ==============   ==============
    Samples total             20640
    Dimensionality                8
    Features                   real
    Target           real 0.15 - 5.

    Read more in the :ref:`User Guide <california_housing_dataset>`.

        object.

    Returns
    -------

        data : ndarray, shape (20640, 8)
            Each row corresponding to the 8 feature values in order.
            If ``as_frame`` is True, ``data`` is a pandas object.
        target : numpy array of shape (20640,)
            Each value corresponds to the average
            house value in units of 100,000.
            If ``as_frame`` is True, ``target`` is a pandas object.
        feature_names : list of length 8
            Array of ordered feature names used in the dataset.
        DESCR : str
            Description of the California housing dataset.

    Notes
    -----

    This dataset consists of 20,640 samples and 9 features.

    Examples
    --------
    >>> from sklearn.datasets import fetch_california_housing
    >>> housing = fetch_california_housing()
    >>> print(housing.data.shape, housing.target.shape)
    (20640, 8) (20640,)
    >>> print(housing.feature_names[0:6])
    ['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population', 'AveOccup']
    """

    filepath = _pkl_filepath(data_home, "cal_housing.pkz")
    if not exists(filepath):

            "Downloading Cal. housing from {} to {}".format(ARCHIVE.url, data_home)
        )

        archive_path = _fetch_remote(
            ARCHIVE,
            dirname=data_home,
        )

        with tarfile.open(mode="r:gz", name=archive_path) as f:
            cal_housing = np.loadtxt(
                f.extractfile("CaliforniaHousing/cal_housing.data"), delimiter=","
            )
            # Columns are not in the same order compared to the previous
            # URL resource on lib.stat.cmu.edu
            columns_index = [8, 7, 2, 3, 4, 5, 6, 1, 0]
            cal_housing = cal_housing[:, columns_index]

            joblib.dump(cal_housing, filepath, compress=6)

    else:
        cal_housing = joblib.load(filepath)

        "MedInc",
        "HouseAge",
        "AveRooms",
        "AveBedrms",
        "Population",
        "AveOccup",
        "Latitude",
        "Longitude",
    ]

    target, data = cal_housing[:, 0], cal_housing[:, 1:]

    # avg rooms = total rooms / households
    data[:, 2] /= data[:, 5]

    # avg bed rooms = total bed rooms / households
    data[:, 3] /= data[:, 5]

    # avg occupancy = population / households
    data[:, 5] = data[:, 4] / data[:, 5]

    # target in units of 100,000
    target = target / 100000.0

    descr = load_descr("california_housing.rst")

    X = data
    y = target

    target_names = [
        "MedHouseVal",
    ]
            "fetch_california_housing", data, target, feature_names, target_names
        )

        return X, y

        data=X,
        target=y,
        DESCR=descr,
    )

def fetch_olivetti_faces(
    *,
):
    """Load the Olivetti faces data-set from AT&T (classification).

    Classes                                40
    Samples total                         400
    Dimensionality                       4096

    Read more in the :ref:`User Guide <olivetti_faces_dataset>`.

        If True the order of the dataset is shuffled to avoid having
        images of the same person grouped.

        If True, returns `(data, target)` instead of a `Bunch` object. See

    Returns
    -------

        data: ndarray, shape (400, 4096)
            Each row corresponds to a ravelled
            face image of original size 64 x 64 pixels.
        images : ndarray, shape (400, 64, 64)
            Each row is a face image
            corresponding to one of the 40 subjects of the dataset.
        target : ndarray, shape (400,)
            Those labels are ranging from 0-39 and correspond to the
            Subject IDs.
        DESCR : str
            Description of the modified Olivetti Faces Dataset.

        Tuple with the `data` and `target` objects described above.

    Examples
    --------
    >>> from sklearn.datasets import fetch_olivetti_faces
    >>> olivetti_faces = fetch_olivetti_faces()
    >>> olivetti_faces.data.shape
    (400, 4096)
    >>> olivetti_faces.target.shape
    (400,)
    >>> olivetti_faces.images.shape
    (400, 64, 64)
    """
    filepath = _pkl_filepath(data_home, "olivetti.pkz")

        print("downloading Olivetti faces from %s to %s" % (FACES.url, data_home))
        mat_path = _fetch_remote(
            FACES, dirname=data_home, n_retries=n_retries, delay=delay
        )
        mfile = loadmat(file_name=mat_path)
        # delete raw .mat data
        remove(mat_path)

        faces = mfile["faces"].T.copy()
        joblib.dump(faces, filepath, compress=6)
        del mfile
    else:
        faces = joblib.load(filepath)

    # We want floating point data, but float32 is enough (there is only
    # one byte of precision in the original uint8s anyway)
    faces = np.float32(faces)
    faces = faces - faces.min()
    faces /= faces.max()
    faces = faces.reshape((400, 64, 64)).transpose(0, 2, 1)
    # 10 images per class, 400 images total, each class is contiguous.
    target = np.array([i // 10 for i in range(400)])
        order = random_state.permutation(len(faces))
        faces = faces[order]
        target = target[order]
    faces_vectorized = faces.reshape(len(faces), -1)

    fdescr = load_descr("olivetti_faces.rst")

        return faces_vectorized, target

    return Bunch(data=faces_vectorized, images=faces, target=target, DESCR=fdescr)

def is_pillow_installed():
    try:

def check_pandas_dependency_message(fetch_func):
    try:
        import pandas  # noqa: F401

        pytest.skip("This test requires pandas to not be installed")
        # Check that pandas is imported lazily and that an informative error
        # message is raised when pandas is missing:
        name = fetch_func.__name__
        expected_msg = f"{name} with as_frame=True requires pandas"
        with pytest.raises(ImportError, match=expected_msg):
            fetch_func(as_frame=True)

def check_return_X_y(bunch, dataset_func):
    X_y_tuple = dataset_func(return_X_y=True)
    assert isinstance(X_y_tuple, tuple)
    assert X_y_tuple[0].shape == bunch.data.shape
    assert X_y_tuple[1].shape == bunch.target.shape

def check_as_frame(
    bunch, dataset_func, expected_data_dtype=None, expected_target_dtype=None
):
    frame_bunch = dataset_func(as_frame=True)
    assert hasattr(frame_bunch, "frame")
    assert isinstance(frame_bunch.frame, pd.DataFrame)
    assert isinstance(frame_bunch.data, pd.DataFrame)
    assert frame_bunch.data.shape == bunch.data.shape
    if frame_bunch.target.ndim > 1:
        assert isinstance(frame_bunch.target, pd.DataFrame)
    else:
        assert isinstance(frame_bunch.target, pd.Series)
    assert frame_bunch.target.shape[0] == bunch.target.shape[0]
    if expected_data_dtype is not None:
        assert np.all(frame_bunch.data.dtypes == expected_data_dtype)
    if expected_target_dtype is not None:
        assert np.all(frame_bunch.target.dtypes == expected_target_dtype)

    # Test for return_X_y and as_frame=True
    frame_X, frame_y = dataset_func(as_frame=True, return_X_y=True)
    assert isinstance(frame_X, pd.DataFrame)
    if frame_y.ndim > 1:
        assert isinstance(frame_X, pd.DataFrame)
    else:
        assert isinstance(frame_y, pd.Series)

def _skip_network_tests():
    return os.environ.get("SKLEARN_SKIP_NETWORK_TESTS", "1") == "1"

def _generate_func_supporting_param(param, dataset_type=("load", "fetch")):
    markers_fetch = FETCH_PYTEST_MARKERS.get(param, {})
    for name, obj in inspect.getmembers(sklearn.datasets):
        if not inspect.isfunction(obj):
            continue

        is_dataset_type = any([name.startswith(t) for t in dataset_type])
        is_support_param = param in inspect.signature(obj).parameters
        if is_dataset_type and is_support_param:
            # check if we should skip if we don't have network support
            marks = [
                pytest.mark.skipif(
                    condition=name.startswith("fetch") and _skip_network_tests(),
                    reason="Skip because fetcher requires internet network",
                )
            ]
            if name in markers_fetch:
                marks.append(markers_fetch[name])

            yield pytest.param(name, obj, marks=marks)

def test_common_check_return_X_y(name, dataset_func):
    bunch = dataset_func()
    check_return_X_y(bunch, dataset_func)

def test_common_check_as_frame(name, dataset_func):
    check_as_frame(bunch, dataset_func)

def test_common_check_pandas_dependency(name, dataset_func):
    check_pandas_dependency_message(dataset_func)

def mock_empty_data_home(tmp_path_factory):
    data_dir = tmp_path_factory.mktemp("scikit_learn_empty_test")

    yield data_dir

def mock_data_home(tmp_path_factory):
    """Test fixture run once and common to all tests of this module"""
    Image = pytest.importorskip("PIL.Image")

    data_dir = tmp_path_factory.mktemp("scikit_learn_lfw_test")
    lfw_home = data_dir / "lfw_home"
    lfw_home.mkdir(parents=True, exist_ok=True)

    random_state = random.Random(42)
    np_rng = np.random.RandomState(42)

    # generate some random jpeg files for each person
    counts = {}
    for name in FAKE_NAMES:
        folder_name = lfw_home / "lfw_funneled" / name
        folder_name.mkdir(parents=True, exist_ok=True)

        n_faces = np_rng.randint(1, 5)
        counts[name] = n_faces
        for i in range(n_faces):
            file_path = folder_name / (name + "_%04d.jpg" % i)
            uniface = np_rng.randint(0, 255, size=(250, 250, 3))
            img = Image.fromarray(uniface.astype(np.uint8))
            img.save(file_path)

    # add some random file pollution to test robustness
    (lfw_home / "lfw_funneled" / ".test.swp").write_bytes(
        b"Text file to be ignored by the dataset loader."
    )

    # generate some pairing metadata files using the same format as LFW
    with open(lfw_home / "pairsDevTrain.txt", "wb") as f:
        f.write(b"10\n")
        more_than_two = [name for name, count in counts.items() if count >= 2]
        for i in range(5):
            name = random_state.choice(more_than_two)
            first, second = random_state.sample(range(counts[name]), 2)
            f.write(("%s\t%d\t%d\n" % (name, first, second)).encode())

            first_name, second_name = random_state.sample(FAKE_NAMES, 2)
            first_index = np_rng.choice(np.arange(counts[first_name]))
            second_index = np_rng.choice(np.arange(counts[second_name]))
            f.write(
                (
                    "%s\t%d\t%s\t%d\n"
                    % (first_name, first_index, second_name, second_index)
                ).encode()
            )

    (lfw_home / "pairsDevTest.txt").write_bytes(
        b"Fake place holder that won't be tested"
    )
    (lfw_home / "pairs.txt").write_bytes(b"Fake place holder that won't be tested")

def test_load_empty_lfw_people(mock_empty_data_home):
    with pytest.raises(OSError):
        fetch_lfw_people(data_home=mock_empty_data_home, download_if_missing=False)

def test_load_fake_lfw_people(mock_data_home):
    lfw_people = fetch_lfw_people(
        data_home=mock_data_home, min_faces_per_person=3, download_if_missing=False
    )

    # The data is croped around the center as a rectangular bounding box
    # around the face. Colors are converted to gray levels:
    assert lfw_people.images.shape == (10, 62, 47)
    assert lfw_people.data.shape == (10, 2914)

    # the target is array of person integer ids
    assert_array_equal(lfw_people.target, [2, 0, 1, 0, 2, 0, 2, 1, 1, 2])

    # names of the persons can be found using the target_names array
    expected_classes = ["Abdelatif Smith", "Abhati Kepler", "Onur Lopez"]
    assert_array_equal(lfw_people.target_names, expected_classes)

    # It is possible to ask for the original data without any croping or color
    # conversion and not limit on the number of picture per person
        data_home=mock_data_home,
        resize=None,
        slice_=None,
        color=True,
        download_if_missing=False,
    )
    assert lfw_people.images.shape == (17, 250, 250, 3)
    assert lfw_people.DESCR.startswith(".. _labeled_faces_in_the_wild_dataset:")

    # the ids and class names are the same as previously
        lfw_people.target, [0, 0, 1, 6, 5, 6, 3, 6, 0, 3, 6, 1, 2, 4, 5, 1, 2]
    )
        lfw_people.target_names,
        [
            "Abdelatif Smith",
            "Abhati Kepler",
            "Camara Alvaro",
            "Chen Dupont",
            "John Lee",
            "Lin Bauman",
            "Onur Lopez",
        ],
    )

    # test return_X_y option
    fetch_func = partial(
        fetch_lfw_people,
    )
    check_return_X_y(lfw_people, fetch_func)

def test_load_fake_lfw_people_too_restrictive(mock_data_home):
        fetch_lfw_people(
            data_home=mock_data_home,
            min_faces_per_person=100,
            download_if_missing=False,
        )

def test_load_empty_lfw_pairs(mock_empty_data_home):
        fetch_lfw_pairs(data_home=mock_empty_data_home, download_if_missing=False)

def test_load_fake_lfw_pairs(mock_data_home):
    lfw_pairs_train = fetch_lfw_pairs(
        data_home=mock_data_home, download_if_missing=False
    )

    assert lfw_pairs_train.pairs.shape == (10, 2, 62, 47)

    # the target is whether the person is the same or not
    assert_array_equal(lfw_pairs_train.target, [1, 1, 1, 1, 1, 0, 0, 0, 0, 0])

    expected_classes = ["Different persons", "Same person"]
    assert_array_equal(lfw_pairs_train.target_names, expected_classes)

    # conversion
    )
    assert lfw_pairs_train.pairs.shape == (10, 2, 250, 250, 3)

    assert lfw_pairs_train.DESCR.startswith(".. _labeled_faces_in_the_wild_dataset:")

def test_fetch_lfw_people_internal_cropping(mock_data_home):
    """Check that we properly crop the images.

    https://github.com/scikit-learn/scikit-learn/issues/24942
    """
    # If cropping was not done properly and we don't resize the images, the images would
    # have their original size (250x250) and the image would not fit in the NumPy array
    # pre-allocated based on `slice_` parameter.
    slice_ = (slice(70, 195), slice(78, 172))
    lfw = fetch_lfw_people(
        min_faces_per_person=3,
    )
    assert lfw.images[0].shape == (
        slice_[0].stop - slice_[0].start,
        slice_[1].stop - slice_[1].start,
    )

def _svmlight_local_test_file_path(filename):
    return resources.files(TEST_DATA_MODULE) / filename

def _load_svmlight_local_test_file(filename, **kwargs):
    """
    Helper to load resource `filename` with `importlib.resources`
    """
    data_path = _svmlight_local_test_file_path(filename)
    with data_path.open("rb") as f:
        return load_svmlight_file(f, **kwargs)

def test_load_svmlight_file():
    X, y = _load_svmlight_local_test_file(datafile)

    # test X's shape
    assert X.indptr.shape[0] == 7
    assert X.shape[0] == 6
    assert X.shape[1] == 21
    assert y.shape[0] == 6

    # test X's non-zero values
    for i, j, val in (
        (0, 2, 2.5),
        (0, 10, -5.2),
        (0, 15, 1.5),
        (1, 5, 1.0),
        (1, 12, -3),
        (2, 20, 27),
    ):
        assert X[i, j] == val

    # tests X's zero values
    assert X[0, 3] == 0
    assert X[0, 5] == 0
    assert X[1, 8] == 0
    assert X[1, 16] == 0
    assert X[2, 18] == 0

    # test can change X's values
    X[0, 2] *= 2
    assert X[0, 2] == 5

    # test y
    assert_array_equal(y, [1, 2, 3, 4, 1, 2])

def test_load_svmlight_file_fd():
    # test loading from file descriptor

    # GH20081: testing equality between path-based and
    # fd-based load_svmlight_file

    data_path = resources.files(TEST_DATA_MODULE) / datafile
    data_path = str(data_path)
    X1, y1 = load_svmlight_file(data_path)

    fd = os.open(data_path, os.O_RDONLY)
    try:
        X2, y2 = load_svmlight_file(fd)
        assert_array_almost_equal(X1.data, X2.data)
        assert_array_almost_equal(y1, y2)
    finally:
        os.close(fd)

def test_load_svmlight_pathlib():
    data_path = _svmlight_local_test_file_path(datafile)
    X1, y1 = load_svmlight_file(str(data_path))
    X2, y2 = load_svmlight_file(data_path)

    assert_allclose(X1.data, X2.data)
    assert_allclose(y1, y2)

def test_load_svmlight_file_multilabel():
    X, y = _load_svmlight_local_test_file(multifile, multilabel=True)
    assert y == [(0, 1), (2,), (), (1, 2)]

def test_load_svmlight_files():
    X_train, y_train, X_test, y_test = load_svmlight_files(
        [str(data_path)] * 2, dtype=np.float32
    )
    assert_array_equal(X_train.toarray(), X_test.toarray())
    assert_array_almost_equal(y_train, y_test)
    assert X_train.dtype == np.float32
    assert X_test.dtype == np.float32

    X1, y1, X2, y2, X3, y3 = load_svmlight_files([str(data_path)] * 3, dtype=np.float64)
    assert X1.dtype == X2.dtype
    assert X2.dtype == X3.dtype
    assert X3.dtype == np.float64

def test_load_svmlight_file_n_features():
    X, y = _load_svmlight_local_test_file(datafile, n_features=22)

    # test X'shape
    assert X.shape[1] == 22

    for i, j, val in ((0, 2, 2.5), (0, 10, -5.2), (1, 5, 1.0), (1, 12, -3)):

    # 21 features in file
        _load_svmlight_local_test_file(datafile, n_features=20)

def test_load_compressed():

    with NamedTemporaryFile(prefix="sklearn-test", suffix=".gz") as tmp:
        tmp.close()  # necessary under windows
        with _svmlight_local_test_file_path(datafile).open("rb") as f:
            with gzip.open(tmp.name, "wb") as fh_out:
                shutil.copyfileobj(f, fh_out)
        Xgz, ygz = load_svmlight_file(tmp.name)
        # because we "close" it manually and write to it,
        # we need to remove it manually.
        os.remove(tmp.name)
    assert_array_almost_equal(X.toarray(), Xgz.toarray())
    assert_array_almost_equal(y, ygz)

    with NamedTemporaryFile(prefix="sklearn-test", suffix=".bz2") as tmp:
            with BZ2File(tmp.name, "wb") as fh_out:
        Xbz, ybz = load_svmlight_file(tmp.name)
    assert_array_almost_equal(X.toarray(), Xbz.toarray())
    assert_array_almost_equal(y, ybz)

def test_load_invalid_file():
        _load_svmlight_local_test_file(invalidfile)

def test_load_invalid_order_file():
        _load_svmlight_local_test_file(invalidfile2)

def test_load_zero_based():
    f = BytesIO(b"-1 4:1.\n1 0:1\n")
        load_svmlight_file(f, zero_based=False)

def test_load_zero_based_auto():
    data1 = b"-1 1:1 2:2 3:3\n"
    data2 = b"-1 0:0 1:1\n"

    f1 = BytesIO(data1)
    X, y = load_svmlight_file(f1, zero_based="auto")
    assert X.shape == (1, 3)

    f2 = BytesIO(data2)
    X1, y1, X2, y2 = load_svmlight_files([f1, f2], zero_based="auto")
    assert X1.shape == (1, 4)
    assert X2.shape == (1, 4)

def test_load_with_qid():
    # load svmfile with qid attribute
    data = b"""
    3 qid:1 1:0.53 2:0.12
    2 qid:1 1:0.13 2:0.1
    7 qid:2 1:0.87 2:0.12"""
    X, y = load_svmlight_file(BytesIO(data), query_id=False)
    assert_array_equal(y, [3, 2, 7])
    assert_array_equal(X.toarray(), [[0.53, 0.12], [0.13, 0.1], [0.87, 0.12]])
    res1 = load_svmlight_files([BytesIO(data)], query_id=True)
    res2 = load_svmlight_file(BytesIO(data), query_id=True)
    for X, y, qid in (res1, res2):
        assert_array_equal(y, [3, 2, 7])
        assert_array_equal(qid, [1, 1, 2])
        assert_array_equal(X.toarray(), [[0.53, 0.12], [0.13, 0.1], [0.87, 0.12]])

def test_load_large_qid():
    """
    load large libsvm / svmlight file with qid attribute. Tests 64-bit query ID
    """
    data = b"\n".join(
        (
            "3 qid:{0} 1:0.53 2:0.12\n2 qid:{0} 1:0.13 2:0.1".format(i).encode()
            for i in range(1, 40 * 1000 * 1000)
        )
    )
    X, y, qid = load_svmlight_file(BytesIO(data), query_id=True)
    assert_array_equal(y[-4:], [3, 2, 3, 2])
    assert_array_equal(np.unique(qid), np.arange(1, 40 * 1000 * 1000))

def test_load_invalid_file2():
        data_path = _svmlight_local_test_file_path(datafile)
        invalid_path = _svmlight_local_test_file_path(invalidfile)
        load_svmlight_files([str(data_path), str(invalid_path), str(data_path)])

def test_not_a_filename():
    # in python 3 integers are valid file opening arguments (taken as unix
    # file descriptors)
    with pytest.raises(TypeError):
        load_svmlight_file(0.42)

def test_invalid_filename():
        load_svmlight_file("trou pic nic douille")

def test_dump(csr_container):
    X_sparse, y_dense = _load_svmlight_local_test_file(datafile)
    X_dense = X_sparse.toarray()
    y_sparse = csr_container(np.atleast_2d(y_dense))

    # slicing a csr_matrix can unsort its .indices, so test that we sort
    # those correctly
    X_sliced = X_sparse[np.arange(X_sparse.shape[0])]
    y_sliced = y_sparse[np.arange(y_sparse.shape[0])]

    for X in (X_sparse, X_dense, X_sliced):
        for y in (y_sparse, y_dense, y_sliced):
            for zero_based in (True, False):
                for dtype in [np.float32, np.float64, np.int32, np.int64]:
                    f = BytesIO()
                    # we need to pass a comment to get the version info in;
                    # LibSVM doesn't grok comments so they're not put in by
                    # default anymore.

                    if sp.issparse(y) and y.shape[0] == 1:
                        # make sure y's shape is: (n_samples, n_labels)
                        # when it is sparse
                        y = y.T

                    # Note: with dtype=np.int32 we are performing unsafe casts,
                    # where X.astype(dtype) overflows. The result is
                    # then platform dependent and X_dense.astype(dtype) may be
                    # different from X_sparse.astype(dtype).asarray().
                    X_input = X.astype(dtype)

                    dump_svmlight_file(
                        X_input, y, f, comment="test", zero_based=zero_based
                    )
                    f.seek(0)

                    comment = f.readline()
                    comment = str(comment, "utf-8")

                    assert "scikit-learn %s" % sklearn.__version__ in comment

                    assert ["one", "zero"][zero_based] + "-based" in comment

                    X2, y2 = load_svmlight_file(f, dtype=dtype, zero_based=zero_based)
                    assert X2.dtype == dtype
                    assert_array_equal(X2.sorted_indices().indices, X2.indices)

                    X2_dense = X2.toarray()
                    if sp.issparse(X_input):
                        X_input_dense = X_input.toarray()
                    else:
                        X_input_dense = X_input

                    if dtype == np.float32:
                        # allow a rounding error at the last decimal place
                        assert_array_almost_equal(X_input_dense, X2_dense, 4)
                        assert_array_almost_equal(
                            y_dense.astype(dtype, copy=False), y2, 4
                        )
                    else:
                        assert_array_almost_equal(X_input_dense, X2_dense, 15)
                            y_dense.astype(dtype, copy=False), y2, 15
                        )

def test_dump_multilabel(csr_container):
    X = [[1, 0, 3, 0, 5], [0, 0, 0, 0, 0], [0, 5, 0, 1, 0]]
    y_dense = [[0, 1, 0], [1, 0, 1], [1, 1, 0]]
    y_sparse = csr_container(y_dense)
    for y in [y_dense, y_sparse]:
        f = BytesIO()
        dump_svmlight_file(X, y, f, multilabel=True)
        f.seek(0)
        # make sure it dumps multilabel correctly
        assert f.readline() == b"1 0:1 2:3 4:5\n"
        assert f.readline() == b"0,2 \n"
        assert f.readline() == b"0,1 1:5 3:1\n"

def test_dump_concise():
    one = 1
    two = 2.1
    three = 3.01
    exact = 1.000000000000001
    # loses the last decimal place
    almost = 1.0000000000000001
    X = [
        [one, two, three, exact, almost],
        [1e9, 2e18, 3e27, 0, 0],
        [0, 0, 0, 0, 0],
    ]
    y = [one, two, three, exact, almost]
    f = BytesIO()
    dump_svmlight_file(X, y, f)
    f.seek(0)
    # make sure it's using the most concise format possible
    assert f.readline() == b"1 0:1 1:2.1 2:3.01 3:1.000000000000001 4:1\n"
    assert f.readline() == b"2.1 0:1000000000 1:2e+18 2:3e+27\n"
    assert f.readline() == b"3.01 \n"
    assert f.readline() == b"1.000000000000001 \n"
    assert f.readline() == b"1 \n"
    f.seek(0)
    # make sure it's correct too :)
    X2, y2 = load_svmlight_file(f)
    assert_array_almost_equal(X, X2.toarray())
    assert_array_almost_equal(y, y2)

def test_dump_comment():

    ascii_comment = "This is a comment\nspanning multiple lines."
    dump_svmlight_file(X, y, f, comment=ascii_comment, zero_based=False)
    f.seek(0)

    X2, y2 = load_svmlight_file(f, zero_based=False)

    # XXX we have to update this to support Python 3.x
    utf8_comment = b"It is true that\n\xc2\xbd\xc2\xb2 = \xc2\xbc"
        dump_svmlight_file(X, y, f, comment=utf8_comment)

    unicode_comment = utf8_comment.decode("utf-8")
    dump_svmlight_file(X, y, f, comment=unicode_comment, zero_based=False)
    f.seek(0)

        dump_svmlight_file(X, y, f, comment="I've got a \0.")

def test_dump_invalid():

    y2d = [y]
        dump_svmlight_file(X, y2d, f)

        dump_svmlight_file(X, y[:-1], f)

def test_dump_query_id():
    # test dumping a file with query_id
    query_id = np.arange(X.shape[0]) // 2
    dump_svmlight_file(X, y, f, query_id=query_id, zero_based=True)

    f.seek(0)
    X1, y1, query_id1 = load_svmlight_file(f, query_id=True, zero_based=True)
    assert_array_almost_equal(X, X1.toarray())
    assert_array_almost_equal(y, y1)
    assert_array_almost_equal(query_id, query_id1)

def test_load_with_long_qid():
    # load svmfile with longint qid attribute
    data = b"""
    1 qid:0 0:1 1:2 2:3
    0 qid:72048431380967004 0:1440446648 1:72048431380967004 2:236784985
    0 qid:-9223372036854775807 0:1440446648 1:72048431380967004 2:236784985
    3 qid:9223372036854775807  0:1440446648 1:72048431380967004 2:236784985"""

    true_X = [
        [1, 2, 3],
        [1440446648, 72048431380967004, 236784985],
    ]

    true_y = [1, 0, 0, 3]
    trueQID = [0, 72048431380967004, -9223372036854775807, 9223372036854775807]
    assert_array_equal(y, true_y)
    assert_array_equal(X.toarray(), true_X)
    assert_array_equal(qid, trueQID)

    dump_svmlight_file(X, y, f, query_id=qid, zero_based=True)
    f.seek(0)
    X, y, qid = load_svmlight_file(f, query_id=True, zero_based=True)

    f.seek(0)
    X, y = load_svmlight_file(f, query_id=False, zero_based=True)

def test_load_zeros(csr_container):
    true_X = csr_container(np.zeros(shape=(3, 4)))
    true_y = np.array([0, 1, 0])
    dump_svmlight_file(true_X, true_y, f)

    for zero_based in ["auto", True, False]:
        f.seek(0)
        X, y = load_svmlight_file(f, n_features=4, zero_based=zero_based)
        assert_array_almost_equal(y, true_y)
        assert_array_almost_equal(X.toarray(), true_X.toarray())

def test_load_with_offsets(sparsity, n_samples, n_features, csr_container):
    X = rng.uniform(low=0.0, high=1.0, size=(n_samples, n_features))
    if sparsity:
        X[X < sparsity] = 0.0
    X = csr_container(X)
    y = rng.randint(low=0, high=2, size=n_samples)

    f.seek(0)

    size = len(f.getvalue())

    # put some marks that are likely to happen anywhere in a row
    mark_0 = 0
    mark_1 = size // 3
    length_0 = mark_1 - mark_0
    mark_2 = 4 * size // 5
    length_1 = mark_2 - mark_1

    # load the original sparse matrix into 3 independent CSR matrices
    X_0, y_0 = load_svmlight_file(
        f, n_features=n_features, offset=mark_0, length=length_0
    )
    X_1, y_1 = load_svmlight_file(
        f, n_features=n_features, offset=mark_1, length=length_1
    )
    X_2, y_2 = load_svmlight_file(f, n_features=n_features, offset=mark_2)

    y_concat = np.concatenate([y_0, y_1, y_2])
    X_concat = sp.vstack([X_0, X_1, X_2])
    assert_array_almost_equal(y, y_concat)
    assert_array_almost_equal(X.toarray(), X_concat.toarray())

def test_load_offset_exhaustive_splits(csr_container):
        [
            [0, 0, 0, 0, 0, 0],
            [1, 2, 3, 4, 0, 6],
            [1, 0, 3, 0, 0, 0],
            [0, 0, 0, 0, 0, 1],
            [1, 0, 0, 0, 0, 0],
        ]
    )
    query_id = np.arange(n_samples) // 2

    dump_svmlight_file(X, y, f, query_id=query_id)
    f.seek(0)

    # load the same data in 2 parts with all the possible byte offsets to
    # locate the split so has to test for particular boundary cases
    for mark in range(size):
        f.seek(0)
        X_0, y_0, q_0 = load_svmlight_file(
            f, n_features=n_features, query_id=True, offset=0, length=mark
        )
        X_1, y_1, q_1 = load_svmlight_file(
            f, n_features=n_features, query_id=True, offset=mark, length=-1
        )
        q_concat = np.concatenate([q_0, q_1])
        y_concat = np.concatenate([y_0, y_1])
        X_concat = sp.vstack([X_0, X_1])
        assert_array_almost_equal(y, y_concat)
        assert_array_equal(query_id, q_concat)
        assert_array_almost_equal(X.toarray(), X_concat.toarray())

def test_load_with_offsets_error():
    with pytest.raises(ValueError, match="n_features is required"):
        _load_svmlight_local_test_file(datafile, offset=3, length=3)

def test_multilabel_y_explicit_zeros(tmp_path, csr_container):
    """
    Ensure that if y contains explicit zeros (i.e. elements of y.data equal to
    0) then those explicit zeros are not encoded.
    """
    save_path = str(tmp_path / "svm_explicit_zero")
    X = rng.randn(3, 5).astype(np.float64)
    indptr = np.array([0, 2, 3, 6])
    indices = np.array([0, 2, 2, 0, 1, 2])
    # The first and last element are explicit zeros.
    data = np.array([0, 1, 1, 1, 1, 0])
    y = csr_container((data, indices, indptr), shape=(3, 3))
    # y as a dense array would look like
    # [[0, 0, 1],
    #  [0, 0, 1],
    #  [1, 1, 0]]

    dump_svmlight_file(X, y, save_path, multilabel=True)

    _, y_load = load_svmlight_file(save_path, multilabel=True)
    y_true = [(2.0,), (2.0,), (0.0, 1.0)]
    assert y_load == y_true

def test_dump_read_only(tmp_path):
    """Ensure that there is no ValueError when dumping a read-only `X`.

    https://github.com/scikit-learn/scikit-learn/issues/28026
    """
    X = rng.randn(5, 2)
    y = rng.randn(5)

    # Convert to memmap-backed which are read-only
    X, y = create_memmap_backed_data([X, y])

    save_path = str(tmp_path / "svm_read_only")
    dump_svmlight_file(X, y, save_path)

def test_post_process_frame(feature_names, target_names):
    """Check the behaviour of the post-processing function for splitting a dataframe."""

    X_original = pd.DataFrame(
        {
            "col_int_as_integer": [1, 2, 3],
            "col_int_as_numeric": [1, 2, 3],
            "col_float_as_real": [1.0, 2.0, 3.0],
            "col_float_as_numeric": [1.0, 2.0, 3.0],
            "col_categorical": ["a", "b", "c"],
            "col_string": ["a", "b", "c"],
        }
    )

    X, y = _post_process_frame(X_original, feature_names, target_names)
    assert isinstance(X, pd.DataFrame)
        assert isinstance(y, pd.DataFrame)
        assert isinstance(y, pd.Series)
    else:
        assert y is None

def test_load_arff_from_gzip_file_error_parser():
    """An error will be raised if the parser is not known."""
    # None of the input parameters are required to be accurate since the check
    # of the parser will be carried out first.

    err_msg = "Unknown parser: 'xxx'. Should be 'liac-arff' or 'pandas'"
        load_arff_from_gzip_file("xxx", "xxx", "xxx", "xxx", "xxx", "xxx")

def test_pandas_arff_parser_strip_single_quotes(parser_func):
    """Check that we properly strip single quotes from the data."""

    arff_file = BytesIO(
        textwrap.dedent(
            """
            @relation 'toy'
            @attribute 'cat_single_quote' {'A', 'B', 'C'}
            @attribute 'str_single_quote' string
            @attribute 'str_nested_quote' string
            @attribute 'class' numeric
            @data
            'A','some text','\"expect double quotes\"',0
            """
        ).encode("utf-8")
    )

    columns_info = {
        "cat_single_quote": {
            "data_type": "nominal",
            "name": "cat_single_quote",
        },
        "str_single_quote": {
            "data_type": "string",
            "name": "str_single_quote",
        },
        "str_nested_quote": {
            "name": "str_nested_quote",
        },
        "class": {
            "data_type": "numeric",
            "name": "class",
        },
    }

        "cat_single_quote",
        "str_single_quote",
        "str_nested_quote",
    ]
    target_names = ["class"]

    # We don't strip single quotes for string columns with the pandas parser.
    expected_values = {
        "cat_single_quote": "A",
        "str_single_quote": (
            "some text" if parser_func is _liac_arff_parser else "'some text'"
        ),
        "str_nested_quote": (
            '"expect double quotes"'
            if parser_func is _liac_arff_parser
            else "'\"expect double quotes\"'"
        ),
        "class": 0,
    }

    _, _, frame, _ = parser_func(
        arff_file,
        output_arrays_type="pandas",
        openml_columns_info=columns_info,
        feature_names_to_select=feature_names,
        target_names_to_select=target_names,
    )

    assert frame.columns.tolist() == feature_names + target_names
    pd.testing.assert_series_equal(frame.iloc[0], pd.Series(expected_values, name=0))

def test_pandas_arff_parser_strip_double_quotes(parser_func):
    """Check that we properly strip double quotes from the data."""

            """
            @attribute 'cat_double_quote' {"A", "B", "C"}
            @attribute 'str_double_quote' string
            @data
            "A","some text","\'expect double quotes\'",0
            """
    )

        "cat_double_quote": {
            "name": "cat_double_quote",
        },
        "str_double_quote": {
            "name": "str_double_quote",
        },
        },
        "class": {
        },
    }

        "cat_double_quote",
        "str_double_quote",
    ]

        "cat_double_quote": "A",
        "str_double_quote": "some text",
        "str_nested_quote": "'expect double quotes'",
    }

    )

def test_pandas_arff_parser_strip_no_quotes(parser_func):
    """Check that we properly parse with no quotes characters."""

            """
            @attribute 'cat_without_quote' {A, B, C}
            @attribute 'str_without_quote' string
            @attribute 'str_internal_quote' string
            @data
            A,some text,'internal' quote,0
            """
    )

        "cat_without_quote": {
            "name": "cat_without_quote",
        },
        "str_without_quote": {
            "name": "str_without_quote",
        },
        "str_internal_quote": {
            "name": "str_internal_quote",
        },
        "class": {
        },
    }

        "cat_without_quote",
        "str_without_quote",
        "str_internal_quote",
    ]

        "cat_without_quote": "A",
        "str_without_quote": "some text",
        "str_internal_quote": "'internal' quote",
    }

    )

def test_fetch(fetch_california_housing_fxt):
    data = fetch_california_housing_fxt()
    assert (20640, 8) == data.data.shape
    assert (20640,) == data.target.shape
    assert data.DESCR.startswith(".. _california_housing_dataset:")

    fetch_func = partial(fetch_california_housing_fxt)
    check_return_X_y(data, fetch_func)

def test_fetch_asframe(fetch_california_housing_fxt):
    bunch = fetch_california_housing_fxt(as_frame=True)
    frame = bunch.frame
    assert hasattr(bunch, "frame") is True
    assert frame.shape == (20640, 9)
    assert isinstance(bunch.data, pd.DataFrame)
    assert isinstance(bunch.target, pd.Series)

def test_pandas_dependency_message(fetch_california_housing_fxt, hide_available_pandas):
    # Check that pandas is imported lazily and that an informative error
    # message is raised when pandas is missing:
    expected_msg = "fetch_california_housing with as_frame=True requires pandas"
    with pytest.raises(ImportError, match=expected_msg):
        fetch_california_housing_fxt(as_frame=True)

def test_fetch_kddcup99_percent10(
    fetch_kddcup99_fxt, as_frame, subset, n_samples, n_features
):
    data = fetch_kddcup99_fxt(subset=subset, as_frame=as_frame)
    assert data.data.shape == (n_samples, n_features)
    assert data.target.shape == (n_samples,)
        assert data.frame.shape == (n_samples, n_features + 1)
    assert data.DESCR.startswith(".. _kddcup99_dataset:")

def test_fetch_kddcup99_return_X_y(fetch_kddcup99_fxt):
    fetch_func = partial(fetch_kddcup99_fxt, subset="smtp")
    data = fetch_func()

def test_fetch_kddcup99_as_frame(fetch_kddcup99_fxt):
    bunch = fetch_kddcup99_fxt()
    check_as_frame(bunch, fetch_kddcup99_fxt)

def test_fetch_kddcup99_shuffle(fetch_kddcup99_fxt):
    dataset = fetch_kddcup99_fxt(
        subset="SA",
        percent10=True,
    )
    dataset_shuffled = fetch_kddcup99_fxt(
    )
    assert set(dataset["target"]) == set(dataset_shuffled["target"])
    assert dataset_shuffled.data.shape == dataset.data.shape
    assert dataset_shuffled.target.shape == dataset.target.shape

def test_pandas_dependency_message(fetch_kddcup99_fxt, hide_available_pandas):
    check_pandas_dependency_message(fetch_kddcup99_fxt)

def test_corrupted_file_error_message(fetch_kddcup99_fxt, tmp_path):
    """Check that a nice error message is raised when cache is corrupted."""
    kddcup99_dir = tmp_path / "kddcup99_10-py3"
    kddcup99_dir.mkdir()
    samples_path = kddcup99_dir / "samples"

    with samples_path.open("wb") as f:
        f.write(b"THIS IS CORRUPTED")

    msg = (
        "The cache for fetch_kddcup99 is invalid, please "
        f"delete {kddcup99_dir} and run the fetch_kddcup99 again"
    )

    with pytest.raises(OSError, match=msg):
        fetch_kddcup99_fxt(data_home=str(tmp_path))

def test_20news(fetch_20newsgroups_fxt):
    data = fetch_20newsgroups_fxt(subset="all", shuffle=False)
    assert data.DESCR.startswith(".. _20newsgroups_dataset:")

    # Extract a reduced dataset
    data2cats = fetch_20newsgroups_fxt(
        subset="all", categories=data.target_names[-1:-3:-1], shuffle=False
    )
    # Check that the ordering of the target_names is the same
    # as the ordering in the full dataset
    assert data2cats.target_names == data.target_names[-2:]
    # Assert that we have only 0 and 1 as labels
    assert np.unique(data2cats.target).tolist() == [0, 1]

    # Check that the number of filenames is consistent with data/target
    assert len(data2cats.filenames) == len(data2cats.target)
    assert len(data2cats.filenames) == len(data2cats.data)

    # Check that the first entry of the reduced dataset corresponds to
    # the first entry of the corresponding category in the full dataset
    entry1 = data2cats.data[0]
    category = data2cats.target_names[data2cats.target[0]]
    label = data.target_names.index(category)
    entry2 = data.data[np.where(data.target == label)[0][0]]
    assert entry1 == entry2

    # check that return_X_y option
    X, y = fetch_20newsgroups_fxt(subset="all", shuffle=False, return_X_y=True)
    assert len(X) == len(data.data)
    assert y.shape == data.target.shape

def test_20news_length_consistency(fetch_20newsgroups_fxt):
    """Checks the length consistencies within the bunch

    This is a non-regression test for a bug present in 0.16.1.
    """
    # Extract the full dataset
    data = fetch_20newsgroups_fxt(subset="all")
    assert len(data["data"]) == len(data.data)
    assert len(data["target"]) == len(data.target)
    assert len(data["filenames"]) == len(data.filenames)

def test_20news_vectorized(fetch_20newsgroups_vectorized_fxt):
    # test subset = train
    bunch = fetch_20newsgroups_vectorized_fxt(subset="train")
    assert sp.issparse(bunch.data) and bunch.data.format == "csr"
    assert bunch.data.shape == (11314, 130107)
    assert bunch.target.shape[0] == 11314
    assert bunch.data.dtype == np.float64
    assert bunch.DESCR.startswith(".. _20newsgroups_dataset:")

    # test subset = test
    bunch = fetch_20newsgroups_vectorized_fxt(subset="test")
    assert bunch.data.shape == (7532, 130107)
    assert bunch.target.shape[0] == 7532

    fetch_func = partial(fetch_20newsgroups_vectorized_fxt, subset="test")
    check_return_X_y(bunch, fetch_func)

    # test subset = all
    bunch = fetch_20newsgroups_vectorized_fxt(subset="all")
    assert bunch.data.shape == (11314 + 7532, 130107)
    assert bunch.target.shape[0] == 11314 + 7532

def test_20news_normalization(fetch_20newsgroups_vectorized_fxt):
    X = fetch_20newsgroups_vectorized_fxt(normalize=False)
    X_ = fetch_20newsgroups_vectorized_fxt(normalize=True)
    X_norm = X_["data"][:100]
    X = X["data"][:100]

    assert_allclose_dense_sparse(X_norm, normalize(X))
    assert np.allclose(np.linalg.norm(X_norm.todense(), axis=1), 1)

def test_20news_as_frame(fetch_20newsgroups_vectorized_fxt):

    bunch = fetch_20newsgroups_vectorized_fxt(as_frame=True)
    check_as_frame(bunch, fetch_20newsgroups_vectorized_fxt)

    assert frame.shape == (11314, 130108)
    assert all([isinstance(col, pd.SparseDtype) for col in bunch.data.dtypes])

    # Check a small subset of features
    for expected_feature in [
        "beginner",
        "beginners",
        "beginning",
        "beginnings",
        "begins",
        "begley",
        "begone",
    ]:
        assert expected_feature in frame.keys()
    assert "category_class" in frame.keys()
    assert bunch.target.name == "category_class"

def test_as_frame_no_pandas(fetch_20newsgroups_vectorized_fxt, hide_available_pandas):
    check_pandas_dependency_message(fetch_20newsgroups_vectorized_fxt)

def test_outdated_pickle(fetch_20newsgroups_vectorized_fxt):
    with patch("os.path.exists") as mock_is_exist:
        with patch("joblib.load") as mock_load:
            # mock that the dataset was cached
            mock_is_exist.return_value = True
            # mock that we have an outdated pickle with only X and y returned
            mock_load.return_value = ("X", "y")
            err_msg = "The cached dataset located in"
            with pytest.raises(ValueError, match=err_msg):
                fetch_20newsgroups_vectorized_fxt(as_frame=True)

class _DummyPath:
    """Minimal class that implements the os.PathLike interface."""

    def __init__(self, path):
        self.path = path

    def __fspath__(self):
        return self.path

def _remove_dir(path):
    if os.path.isdir(path):
        shutil.rmtree(path)

def data_home(tmpdir_factory):
    tmp_file = str(tmpdir_factory.mktemp("scikit_learn_data_home_test"))
    yield tmp_file
    _remove_dir(tmp_file)

def load_files_root(tmpdir_factory):
    tmp_file = str(tmpdir_factory.mktemp("scikit_learn_load_files_test"))

def test_category_dir_1(load_files_root):
    test_category_dir1 = tempfile.mkdtemp(dir=load_files_root)
    sample_file = tempfile.NamedTemporaryFile(dir=test_category_dir1, delete=False)
    sample_file.write(b"Hello World!\n")
    sample_file.close()
    yield str(test_category_dir1)
    _remove_dir(test_category_dir1)

def test_category_dir_2(load_files_root):
    test_category_dir2 = tempfile.mkdtemp(dir=load_files_root)
    yield str(test_category_dir2)
    _remove_dir(test_category_dir2)

def test_data_home(path_container, data_home):
    # get_data_home will point to a pre-existing folder
    if path_container is not None:
        data_home = path_container(data_home)
    assert data_home == data_home
    assert os.path.exists(data_home)

    # clear_data_home will delete both the content and the folder it-self
    clear_data_home(data_home=data_home)
    assert not os.path.exists(data_home)

    # if the folder is missing it will be created again

def test_default_empty_load_files(load_files_root):
    res = load_files(load_files_root)
    assert len(res.filenames) == 0
    assert len(res.target_names) == 0
    assert res.DESCR is None

def test_default_load_files(test_category_dir_1, test_category_dir_2, load_files_root):
    assert len(res.filenames) == 1
    assert len(res.target_names) == 2
    assert res.data == [b"Hello World!\n"]

def test_load_files_w_categories_desc_and_encoding(
    test_category_dir_1, test_category_dir_2, load_files_root
):
    category = os.path.abspath(test_category_dir_1).split(os.sep).pop()
    res = load_files(
        load_files_root, description="test", categories=[category], encoding="utf-8"
    )

    assert len(res.target_names) == 1
    assert res.DESCR == "test"
    assert res.data == ["Hello World!\n"]

def test_load_files_wo_load_content(
):
    res = load_files(load_files_root, load_content=False)
    assert res.get("data") is None

def test_load_files_allowed_extensions(tmp_path, allowed_extensions):
    """Check the behaviour of `allowed_extension` in `load_files`."""
    d = tmp_path / "sub"
    d.mkdir()
    files = ("file1.txt", "file2.json", "file3.json", "file4.md")
    paths = [d / f for f in files]
    for p in paths:
        p.write_bytes(b"hello")
    res = load_files(tmp_path, allowed_extensions=allowed_extensions)
    assert set([str(p) for p in paths if p.suffix in allowed_extensions]) == set(
        res.filenames
    )

def test_load_csv_data(
    filename, expected_n_samples, expected_n_features, expected_target_names
):
    actual_data, actual_target, actual_target_names = load_csv_data(filename)
    assert actual_data.shape[0] == expected_n_samples
    assert actual_data.shape[1] == expected_n_features
    assert actual_target.shape[0] == expected_n_samples
    np.testing.assert_array_equal(actual_target_names, expected_target_names)

def test_load_csv_data_with_descr():
    descr_file_name = "iris.rst"

    res_without_descr = load_csv_data(data_file_name=data_file_name)
    res_with_descr = load_csv_data(
        data_file_name=data_file_name, descr_file_name=descr_file_name
    )
    assert len(res_with_descr) == 4
    assert len(res_without_descr) == 3

    np.testing.assert_array_equal(res_with_descr[0], res_without_descr[0])
    np.testing.assert_array_equal(res_with_descr[1], res_without_descr[1])
    np.testing.assert_array_equal(res_with_descr[2], res_without_descr[2])

    assert res_with_descr[-1].startswith(".. _iris_dataset:")

def test_load_gzip_compressed_csv_data(filename, kwargs, expected_shape):
    actual_data = load_gzip_compressed_csv_data(filename, **kwargs)
    assert actual_data.shape == tuple(expected_shape)

def test_load_gzip_compressed_csv_data_with_descr():
    data_file_name = "diabetes_target.csv.gz"
    descr_file_name = "diabetes.rst"

    expected_data = load_gzip_compressed_csv_data(data_file_name=data_file_name)
    actual_data, descr = load_gzip_compressed_csv_data(
        data_file_name=data_file_name,
        descr_file_name=descr_file_name,
    )

    np.testing.assert_array_equal(actual_data, expected_data)
    assert descr.startswith(".. _diabetes_dataset:")

def test_load_sample_images():
    try:
        res = load_sample_images()
        assert len(res.images) == 2
        assert len(res.filenames) == 2
        images = res.images

        # assert is china image
        assert np.all(images[0][0, 0, :] == np.array([174, 201, 231], dtype=np.uint8))
        # assert is flower image
        assert np.all(images[1][0, 0, :] == np.array([2, 19, 13], dtype=np.uint8))
        assert res.DESCR
        warnings.warn("Could not load sample images, PIL is not available.")

def test_load_sample_image():
    try:
        china = load_sample_image("china.jpg")
        assert china.dtype == "uint8"
        assert china.shape == (427, 640, 3)

def test_load_diabetes_raw():
    """Test to check that we load a scaled version by default but that we can
    get an unscaled version when setting `scaled=False`."""
    diabetes_raw = load_diabetes(scaled=False)
    assert diabetes_raw.data.shape == (442, 10)
    assert diabetes_raw.target.size == 442
    assert len(diabetes_raw.feature_names) == 10
    assert diabetes_raw.DESCR

    diabetes_default = load_diabetes()

    np.testing.assert_allclose(
        scale(diabetes_raw.data) / (442**0.5), diabetes_default.data, atol=1e-04
    )

def test_loader(loader_func, data_shape, target_shape, n_target, has_descr, filenames):
    bunch = loader_func()

    assert isinstance(bunch, Bunch)
    assert bunch.data.shape == data_shape
    assert bunch.target.shape == target_shape
    if hasattr(bunch, "feature_names"):
        assert len(bunch.feature_names) == data_shape[1]
    if n_target is not None:
        assert len(bunch.target_names) == n_target
    if has_descr:
        assert bunch.DESCR
    if filenames:
        assert "data_module" in bunch
        assert all(
            [
                f in bunch
                and (resources.files(bunch["data_module"]) / bunch[f]).is_file()
                for f in filenames
            ]
        )

def test_toy_dataset_frame_dtype(loader_func, data_dtype, target_dtype):
    default_result = loader_func()
    check_as_frame(
        default_result,
        loader_func,
        expected_data_dtype=data_dtype,
        expected_target_dtype=target_dtype,
    )

def test_loads_dumps_bunch():
    bunch = Bunch(x="x")
    bunch_from_pkl = loads(dumps(bunch))
    bunch_from_pkl.x = "y"
    assert bunch_from_pkl["x"] == bunch_from_pkl.x

def test_bunch_pickle_generated_with_0_16_and_read_with_0_17():
    bunch = Bunch(key="original")
    # This reproduces a problem when Bunch pickles have been created
    # with scikit-learn 0.16 and are read with 0.17. Basically there
    # is a surprising behaviour because reading bunch.key uses
    # bunch.__dict__ (which is non empty for 0.16 Bunch objects)
    # whereas assigning into bunch.key uses bunch.__setattr__. See
    # https://github.com/scikit-learn/scikit-learn/issues/6196 for
    # more details
    bunch.__dict__["key"] = "set from __dict__"
    # After loading from pickle the __dict__ should have been ignored
    assert bunch_from_pkl.key == "original"
    assert bunch_from_pkl["key"] == "original"
    # Making sure that changing the attr does change the value
    # associated with __getitem__ as well
    bunch_from_pkl.key = "changed"
    assert bunch_from_pkl.key == "changed"
    assert bunch_from_pkl["key"] == "changed"

def test_bunch_dir():
    # check that dir (important for autocomplete) shows attributes
    data = load_iris()
    assert "data" in dir(data)

def test_load_boston_error():
    """Check that we raise the ethical warning when trying to import `load_boston`."""
    msg = "The Boston housing prices dataset has an ethical problem"
    with pytest.raises(ImportError, match=msg):
        from sklearn.datasets import load_boston  # noqa: F401

    # other non-existing function should raise the usual import error
    msg = "cannot import name 'non_existing_function' from 'sklearn.datasets'"
        from sklearn.datasets import non_existing_function  # noqa: F401

def test_fetch_remote_raise_warnings_with_invalid_url(monkeypatch):
    """Check retry mechanism in _fetch_remote."""

    url = "https://scikit-learn.org/this_file_does_not_exist.tar.gz"
    invalid_remote_file = RemoteFileMetadata("invalid_file", url, None)
    urlretrieve_mock = Mock(
        side_effect=HTTPError(
            url=url, code=404, msg="Not Found", hdrs=None, fp=io.BytesIO()
        )
    )
    monkeypatch.setattr("sklearn.datasets._base.urlretrieve", urlretrieve_mock)

    with pytest.warns(UserWarning, match="Retry downloading") as record:
        with pytest.raises(HTTPError, match="HTTP Error 404"):
            _fetch_remote(invalid_remote_file, n_retries=3, delay=0)

        assert urlretrieve_mock.call_count == 4

        for r in record:
            assert str(r.message) == f"Retry downloading from url: {url}"
        assert len(record) == 3

def test_derive_folder_and_filename_from_url():
    folder, filename = _derive_folder_and_filename_from_url(
        "https://example.com/file.tar.gz"
    )
    assert folder == "example.com"
    assert filename == "file.tar.gz"

        "https://example.com/نمونه نماینده.data"
    )
    assert filename == "نمونه-نماینده.data"

        "https://example.com/path/to-/.file.tar.gz"
    )
    assert folder == "example.com/path_to"

    folder, filename = _derive_folder_and_filename_from_url("https://example.com/")
    assert filename == "downloaded_file"

    folder, filename = _derive_folder_and_filename_from_url("https://example.com")

        "https://example.com/path/@to/data.json?param=value"
    )
    assert filename == "data.json"

        "https://example.com/path/@@to._/-_.data.json.#anchor"
    )

        "https://example.com//some_file.txt"
    )
    assert filename == "some_file.txt"

        "http://example/../some_file.txt"
    )
    assert folder == "example"

        "https://example.com/!.'.,/some_file.txt"
    )

        "https://example.com/a/!.'.,/b/some_file.txt"
    )
    assert folder == "example.com/a_b"

    folder, filename = _derive_folder_and_filename_from_url("https://example.com/!.'.,")

    with pytest.raises(ValueError, match="Invalid URL"):
        _derive_folder_and_filename_from_url("https:/../")

def _mock_urlretrieve(server_side):
    def _urlretrieve_mock(url, local_path):
        server_root = Path(server_side)
        file_path = urlparse(url).path.strip("/")
        if not (server_root / file_path).exists():
            raise HTTPError(url, 404, "Not Found", None, None)
        shutil.copy(server_root / file_path, local_path)

    return Mock(side_effect=_urlretrieve_mock)

def test_fetch_file_using_data_home(monkeypatch, tmpdir):
    tmpdir = Path(tmpdir)
    server_side = tmpdir / "server_side"
    server_side.mkdir()
    data_file = server_side / "data.jsonl"
    server_data = '{"a": 1, "b": 2}\n'
    data_file.write_text(server_data, encoding="utf-8")

    server_subfolder = server_side / "subfolder"
    server_subfolder.mkdir()
    other_data_file = server_subfolder / "other_file.txt"
    other_data_file.write_text("Some important text data.", encoding="utf-8")

    data_home = tmpdir / "data_home"
    data_home.mkdir()

    urlretrieve_mock = _mock_urlretrieve(server_side)

        "sklearn.datasets._base.get_data_home", Mock(return_value=data_home)
    )
    fetched_file_path = fetch_file(
        "https://example.com/data.jsonl",
    )
    assert fetched_file_path == data_home / "example.com" / "data.jsonl"
    assert fetched_file_path.read_text(encoding="utf-8") == server_data

        "https://example.com/subfolder/other_file.txt",
    )
    assert (
        fetched_file_path == data_home / "example.com" / "subfolder" / "other_file.txt"
    )
    assert fetched_file_path.read_text(encoding="utf-8") == other_data_file.read_text(
        "utf-8"
    )

    expected_warning_msg = re.escape(
        "Retry downloading from url: https://example.com/subfolder/invalid.txt"
    )
    with pytest.raises(HTTPError):
        with pytest.warns(match=expected_warning_msg):
            fetch_file(
                "https://example.com/subfolder/invalid.txt",
                delay=0,
            )

    local_subfolder = data_home / "example.com" / "subfolder"
    assert sorted(local_subfolder.iterdir()) == [local_subfolder / "other_file.txt"]

def test_fetch_file_without_sha256(monkeypatch, tmpdir):
    server_side = tmpdir.mkdir("server_side")
    data_file = Path(server_side / "data.jsonl")

    client_side = tmpdir.mkdir("client_side")

    # The first call should trigger a download:
        folder=client_side,
    )
    assert fetched_file_path == client_side / "data.jsonl"
    assert urlretrieve_mock.call_count == 1

    # Fetching again the same file to the same folder should do nothing:
    )

    # Deleting and calling again should re-download
    fetched_file_path.unlink()
    )
    assert urlretrieve_mock.call_count == 2

def test_fetch_file_with_sha256(monkeypatch, tmpdir):
    expected_sha256 = hashlib.sha256(data_file.read_bytes()).hexdigest()

    # The first call should trigger a download.
        "https://example.com/data.jsonl", folder=client_side, sha256=expected_sha256
    )

    # Fetching again the same file to the same folder should do nothing when
    # the sha256 match:
    )

    # Corrupting the local data should yield a warning and trigger a new download:
    fetched_file_path.write_text("corrupted contents", encoding="utf-8")
    expected_msg = (
        r"SHA256 checksum of existing local file data.jsonl "
        rf"\(.*\) differs from expected \({expected_sha256}\): "
        r"re-downloading from https://example.com/data.jsonl \."
    )
    with pytest.warns(match=expected_msg):
        fetched_file_path = fetch_file(
            "https://example.com/data.jsonl", folder=client_side, sha256=expected_sha256
        )
        assert fetched_file_path == client_side / "data.jsonl"
        assert fetched_file_path.read_text(encoding="utf-8") == server_data
        assert urlretrieve_mock.call_count == 2

    # Calling again should do nothing:
    )

    # Deleting the local file and calling again should redownload without warning:
    )
    assert urlretrieve_mock.call_count == 3

    # Calling without a sha256 should also work without redownloading:
    )

    # Calling with a wrong sha256 should raise an informative exception:
    non_matching_sha256 = "deadbabecafebeef"
    expected_warning_msg = "differs from expected"
    expected_error_msg = re.escape(
        f"The SHA256 checksum of data.jsonl ({expected_sha256}) differs from "
        f"expected ({non_matching_sha256})."
    )
    with pytest.raises(OSError, match=expected_error_msg):
                "https://example.com/data.jsonl",
                folder=client_side,
                sha256=non_matching_sha256,
            )

class _MockHTTPResponse:
    def __init__(self, data, is_gzip):
        self.data = data
        self.is_gzip = is_gzip

    def read(self, amt=-1):
        return self.data.read(amt)

    def close(self):
        self.data.close()

    def info(self):
        if self.is_gzip:
            return {"Content-Encoding": "gzip"}
        return {}

        return iter(self.data)

    def __enter__(self):

    def __exit__(self, exc_type, exc_val, exc_tb):

def _monkey_patch_webbased_functions(context, data_id, gzip_response):
    # monkey patches the urlopen function. Important note: Do NOT use this
    # in combination with a regular cache directory, as the files that are
    # stored as cache should not be mixed up with real openml datasets
    url_prefix_data_description = "https://api.openml.org/api/v1/json/data/"
    url_prefix_data_features = "https://api.openml.org/api/v1/json/data/features/"
    url_prefix_download_data = "https://www.openml.org/data/v1/download"
    url_prefix_data_list = "https://api.openml.org/api/v1/json/data/list/"

    path_suffix = ".gz"
    read_fn = gzip.open

    data_module = OPENML_TEST_DATA_MODULE + "." + f"id_{data_id}"

    def _file_name(url, suffix):
        output = (
            re.sub(r"\W", "-", url[len("https://api.openml.org/") :])
            + suffix
            + path_suffix
        )
        # Shorten the filenames to have better compatibility with windows 10
        # and filenames > 260 characters
        return (
            output.replace("-json-data-list", "-jdl")
            .replace("-json-data-features", "-jdf")
            .replace("-json-data-qualities", "-jdq")
            .replace("-json-data", "-jd")
            .replace("-data_name", "-dn")
            .replace("-download", "-dl")
            .replace("-limit", "-l")
            .replace("-data_version", "-dv")
            .replace("-status", "-s")
            .replace("-deactivated", "-dact")
            .replace("-active", "-act")
        )

    def _mock_urlopen_shared(url, has_gzip_header, expected_prefix, suffix):
        assert url.startswith(expected_prefix), (
            f"{expected_prefix!r} does not match {url!r}"
        )

        data_file_name = _file_name(url, suffix)
        data_file_path = resources.files(data_module) / data_file_name

        with data_file_path.open("rb") as f:
            if has_gzip_header and gzip_response:
                fp = BytesIO(f.read())
                return _MockHTTPResponse(fp, True)
            else:
                decompressed_f = read_fn(f, "rb")
                fp = BytesIO(decompressed_f.read())
                return _MockHTTPResponse(fp, False)

    def _mock_urlopen_data_description(url, has_gzip_header):
        return _mock_urlopen_shared(
            url=url,
            has_gzip_header=has_gzip_header,
            expected_prefix=url_prefix_data_description,
            suffix=".json",
        )

    def _mock_urlopen_data_features(url, has_gzip_header):
            url=url,
            expected_prefix=url_prefix_data_features,
        )

    def _mock_urlopen_download_data(url, has_gzip_header):
        # For simplicity the mock filenames don't contain the filename, i.e.
        # the last part of the data description url after the last /.
        # For example for id_1, data description download url is:
        # gunzip -c sklearn/datasets/tests/data/openml/id_1/api-v1-jd-1.json.gz | grep '"url"  # noqa: E501
        # "https:\/\/www.openml.org\/data\/v1\/download\/1\/anneal.arff"
        # but the mock filename does not contain anneal.arff and is:
        # sklearn/datasets/tests/data/openml/id_1/data-v1-dl-1.arff.gz.
        # We only keep the part of the url before the last /
        url_without_filename = url.rsplit("/", 1)[0]

            url=url_without_filename,
            expected_prefix=url_prefix_download_data,
            suffix=".arff",
        )

    def _mock_urlopen_data_list(url, has_gzip_header):
        assert url.startswith(url_prefix_data_list), (
            f"{url_prefix_data_list!r} does not match {url!r}"
        )

        data_file_name = _file_name(url, ".json")

        # load the file itself, to simulate a http error
            decompressed_f = read_fn(f, "rb")
            decoded_s = decompressed_f.read().decode("utf-8")
            json_data = json.loads(decoded_s)
        if "error" in json_data:
            raise HTTPError(
                url=None, code=412, msg="Simulated mock error", hdrs=None, fp=BytesIO()
            )

            if has_gzip_header:
            else:

    def _mock_urlopen(request, *args, **kwargs):
        url = request.get_full_url()
        has_gzip_header = request.get_header("Accept-encoding") == "gzip"
        if url.startswith(url_prefix_data_list):
            return _mock_urlopen_data_list(url, has_gzip_header)
        elif url.startswith(url_prefix_data_features):
            return _mock_urlopen_data_features(url, has_gzip_header)
        elif url.startswith(url_prefix_download_data):
            return _mock_urlopen_download_data(url, has_gzip_header)
        elif url.startswith(url_prefix_data_description):
            return _mock_urlopen_data_description(url, has_gzip_header)
        else:
            raise ValueError("Unknown mocking URL pattern: %s" % url)

    # XXX: Global variable
    if test_offline:
        context.setattr(sklearn.datasets._openml, "urlopen", _mock_urlopen)

def test_fetch_openml_as_frame_true(
    monkeypatch,
    data_id,
    dataset_params,
    n_targets,
    parser,
    gzip_response,
):
    """Check the behaviour of `fetch_openml` with `as_frame=True`.

    Fetch by ID and/or name (depending if the file was previously cached).
    """

    _monkey_patch_webbased_functions(monkeypatch, data_id, gzip_response=gzip_response)
    bunch = fetch_openml(
        as_frame=True,
        cache=False,
        **dataset_params,
    )

    assert int(bunch.details["id"]) == data_id

    assert isinstance(bunch.frame, pd.DataFrame)
    assert bunch.frame.shape == (n_samples, n_features + n_targets)

    assert bunch.data.shape == (n_samples, n_features)

    if n_targets == 1:
        assert isinstance(bunch.target, pd.Series)
        assert bunch.target.shape == (n_samples,)
    else:
        assert isinstance(bunch.target, pd.DataFrame)
        assert bunch.target.shape == (n_samples, n_targets)

    assert bunch.categories is None

def test_fetch_openml_as_frame_false(
    data_id,
    parser,
):
    """Check the behaviour of `fetch_openml` with `as_frame=False`.

    Fetch both by ID and/or name + version.
    """
    pytest.importorskip("pandas")

    _monkey_patch_webbased_functions(monkeypatch, data_id, gzip_response=True)
        as_frame=False,
    )

    assert bunch.frame is None

    assert isinstance(bunch.data, np.ndarray)

    assert isinstance(bunch.target, np.ndarray)
    else:

    assert isinstance(bunch.categories, dict)

def test_fetch_openml_consistency_parser(monkeypatch, data_id):
    """Check the consistency of the LIAC-ARFF and pandas parsers."""

    bunch_liac = fetch_openml(
        data_id=data_id,
        parser="liac-arff",
    )
    bunch_pandas = fetch_openml(
        parser="pandas",
    )

    # The data frames for the input features should match up to some numerical
    # dtype conversions (e.g. float64 <=> Int64) due to limitations of the
    # LIAC-ARFF parser.
    data_liac, data_pandas = bunch_liac.data, bunch_pandas.data

    def convert_numerical_dtypes(series):
        pandas_series = data_pandas[series.name]
        if pd.api.types.is_numeric_dtype(pandas_series):
            return series.astype(pandas_series.dtype)
        else:
            return series

    data_liac_with_fixed_dtypes = data_liac.apply(convert_numerical_dtypes)
    pd.testing.assert_frame_equal(data_liac_with_fixed_dtypes, data_pandas)

    # Let's also check that the .frame attributes also match
    frame_liac, frame_pandas = bunch_liac.frame, bunch_pandas.frame

    # Note that the .frame attribute is a superset of the .data attribute:
    pd.testing.assert_frame_equal(frame_pandas[bunch_pandas.feature_names], data_pandas)

    # However the remaining columns, typically the target(s), are not necessarily
    # dtyped similarly by both parsers due to limitations of the LIAC-ARFF parser.
    # Therefore, extra dtype conversions are required for those columns:

    def convert_numerical_and_categorical_dtypes(series):
        pandas_series = frame_pandas[series.name]
        elif isinstance(pandas_series.dtype, pd.CategoricalDtype):
            # Compare categorical features by converting categorical liac uses
            # strings to denote the categories, we rename the categories to make
            # them comparable to the pandas parser. Fixing this behavior in
            # LIAC-ARFF would allow to check the consistency in the future but
            # we do not plan to maintain the LIAC-ARFF on the long term.
            return series.cat.rename_categories(pandas_series.cat.categories)
        else:

    frame_liac_with_fixed_dtypes = frame_liac.apply(
        convert_numerical_and_categorical_dtypes
    )
    pd.testing.assert_frame_equal(frame_liac_with_fixed_dtypes, frame_pandas)

def test_fetch_openml_equivalence_array_dataframe(monkeypatch, parser):
    """Check the equivalence of the dataset when using `as_frame=False` and
    `as_frame=True`.
    """

    data_id = 61
    bunch_as_frame_true = fetch_openml(
    )

    bunch_as_frame_false = fetch_openml(
    )

    assert_allclose(bunch_as_frame_false.data, bunch_as_frame_true.data)
    assert_array_equal(bunch_as_frame_false.target, bunch_as_frame_true.target)

def test_fetch_openml_iris_pandas(monkeypatch, parser):
    """Check fetching on a numerical only dataset with string labels."""
    CategoricalDtype = pd.api.types.CategoricalDtype
    data_shape = (150, 4)
    target_shape = (150,)
    frame_shape = (150, 5)

    target_dtype = CategoricalDtype(
        ["Iris-setosa", "Iris-versicolor", "Iris-virginica"]
    )
    data_dtypes = [np.float64] * 4
    data_names = ["sepallength", "sepalwidth", "petallength", "petalwidth"]
    target_name = "class"

    _monkey_patch_webbased_functions(monkeypatch, data_id, True)

    )
    data = bunch.data
    target = bunch.target

    assert isinstance(data, pd.DataFrame)
    assert np.all(data.dtypes == data_dtypes)
    assert data.shape == data_shape
    assert np.all(data.columns == data_names)
    assert np.all(bunch.feature_names == data_names)
    assert bunch.target_names == [target_name]

    assert isinstance(target, pd.Series)
    assert target.dtype == target_dtype
    assert target.shape == target_shape
    assert target.name == target_name
    assert target.index.is_unique

    assert isinstance(frame, pd.DataFrame)
    assert frame.shape == frame_shape
    assert np.all(frame.dtypes == data_dtypes + [target_dtype])
    assert frame.index.is_unique

def test_fetch_openml_forcing_targets(monkeypatch, parser, target_column):
    """Check that we can force the target to not be the default target."""

    bunch_forcing_target = fetch_openml(
        target_column=target_column,
    )
    bunch_default = fetch_openml(
    )

    pd.testing.assert_frame_equal(bunch_forcing_target.frame, bunch_default.frame)
    if isinstance(target_column, list):
        pd.testing.assert_index_equal(
            bunch_forcing_target.target.columns, pd.Index(target_column)
        )
        assert bunch_forcing_target.data.shape == (150, 3)
    else:
        assert bunch_forcing_target.target.name == target_column
        assert bunch_forcing_target.data.shape == (150, 4)

def test_fetch_openml_equivalence_frame_return_X_y(monkeypatch, data_id, parser):
    """Check the behaviour of `return_X_y=True` when `as_frame=True`."""

        return_X_y=False,
    )
    X, y = fetch_openml(
        return_X_y=True,
    )

    pd.testing.assert_frame_equal(bunch.data, X)
    if isinstance(y, pd.Series):
        pd.testing.assert_series_equal(bunch.target, y)
    else:
        pd.testing.assert_frame_equal(bunch.target, y)

def test_fetch_openml_equivalence_array_return_X_y(monkeypatch, data_id, parser):
    """Check the behaviour of `return_X_y=True` when `as_frame=False`."""

    )
    )

    assert_array_equal(bunch.data, X)
    assert_array_equal(bunch.target, y)

def test_fetch_openml_difference_parsers(monkeypatch):
    """Check the difference between liac-arff and pandas parser."""

    data_id = 1119
    # When `as_frame=False`, the categories will be ordinally encoded with
    # liac-arff parser while this is not the case with pandas parser.
    as_frame = False
    bunch_liac_arff = fetch_openml(
        as_frame=as_frame,
    )
    )

    assert bunch_liac_arff.data.dtype.kind == "f"
    assert bunch_pandas.data.dtype == "O"

def datasets_column_names():
    """Returns the columns names for each dataset."""
    return {
        61: ["sepallength", "sepalwidth", "petallength", "petalwidth", "class"],
        2: [
            "family",
            "product-type",
            "steel",
            "carbon",
            "hardness",
            "temper_rolling",
            "condition",
            "formability",
            "strength",
            "non-ageing",
            "surface-finish",
            "surface-quality",
            "enamelability",
            "bc",
            "bf",
            "bt",
            "bw%2Fme",
            "bl",
            "m",
            "chrom",
            "phos",
            "cbond",
            "marvi",
            "exptl",
            "ferro",
            "corr",
            "blue%2Fbright%2Fvarn%2Fclean",
            "lustre",
            "jurofm",
            "s",
            "p",
            "shape",
            "thick",
            "width",
            "len",
            "oil",
            "bore",
            "packing",
            "class",
        ],
        561: ["vendor", "MYCT", "MMIN", "MMAX", "CACH", "CHMIN", "CHMAX", "class"],
        40589: [
            "Mean_Acc1298_Mean_Mem40_Centroid",
            "Mean_Acc1298_Mean_Mem40_Rolloff",
            "Mean_Acc1298_Mean_Mem40_Flux",
            "Mean_Acc1298_Mean_Mem40_MFCC_0",
            "Mean_Acc1298_Mean_Mem40_MFCC_1",
            "Mean_Acc1298_Mean_Mem40_MFCC_2",
            "Mean_Acc1298_Mean_Mem40_MFCC_3",
            "Mean_Acc1298_Mean_Mem40_MFCC_4",
            "Mean_Acc1298_Mean_Mem40_MFCC_5",
            "Mean_Acc1298_Mean_Mem40_MFCC_6",
            "Mean_Acc1298_Mean_Mem40_MFCC_7",
            "Mean_Acc1298_Mean_Mem40_MFCC_8",
            "Mean_Acc1298_Mean_Mem40_MFCC_9",
            "Mean_Acc1298_Mean_Mem40_MFCC_10",
            "Mean_Acc1298_Mean_Mem40_MFCC_11",
            "Mean_Acc1298_Mean_Mem40_MFCC_12",
            "Mean_Acc1298_Std_Mem40_Centroid",
            "Mean_Acc1298_Std_Mem40_Rolloff",
            "Mean_Acc1298_Std_Mem40_Flux",
            "Mean_Acc1298_Std_Mem40_MFCC_0",
            "Mean_Acc1298_Std_Mem40_MFCC_1",
            "Mean_Acc1298_Std_Mem40_MFCC_2",
            "Mean_Acc1298_Std_Mem40_MFCC_3",
            "Mean_Acc1298_Std_Mem40_MFCC_4",
            "Mean_Acc1298_Std_Mem40_MFCC_5",
            "Mean_Acc1298_Std_Mem40_MFCC_6",
            "Mean_Acc1298_Std_Mem40_MFCC_7",
            "Mean_Acc1298_Std_Mem40_MFCC_8",
            "Mean_Acc1298_Std_Mem40_MFCC_9",
            "Mean_Acc1298_Std_Mem40_MFCC_10",
            "Mean_Acc1298_Std_Mem40_MFCC_11",
            "Mean_Acc1298_Std_Mem40_MFCC_12",
            "Std_Acc1298_Mean_Mem40_Centroid",
            "Std_Acc1298_Mean_Mem40_Rolloff",
            "Std_Acc1298_Mean_Mem40_Flux",
            "Std_Acc1298_Mean_Mem40_MFCC_0",
            "Std_Acc1298_Mean_Mem40_MFCC_1",
            "Std_Acc1298_Mean_Mem40_MFCC_2",
            "Std_Acc1298_Mean_Mem40_MFCC_3",
            "Std_Acc1298_Mean_Mem40_MFCC_4",
            "Std_Acc1298_Mean_Mem40_MFCC_5",
            "Std_Acc1298_Mean_Mem40_MFCC_6",
            "Std_Acc1298_Mean_Mem40_MFCC_7",
            "Std_Acc1298_Mean_Mem40_MFCC_8",
            "Std_Acc1298_Mean_Mem40_MFCC_9",
            "Std_Acc1298_Mean_Mem40_MFCC_10",
            "Std_Acc1298_Mean_Mem40_MFCC_11",
            "Std_Acc1298_Mean_Mem40_MFCC_12",
            "Std_Acc1298_Std_Mem40_Centroid",
            "Std_Acc1298_Std_Mem40_Rolloff",
            "Std_Acc1298_Std_Mem40_Flux",
            "Std_Acc1298_Std_Mem40_MFCC_0",
            "Std_Acc1298_Std_Mem40_MFCC_1",
            "Std_Acc1298_Std_Mem40_MFCC_2",
            "Std_Acc1298_Std_Mem40_MFCC_3",
            "Std_Acc1298_Std_Mem40_MFCC_4",
            "Std_Acc1298_Std_Mem40_MFCC_5",
            "Std_Acc1298_Std_Mem40_MFCC_6",
            "Std_Acc1298_Std_Mem40_MFCC_7",
            "Std_Acc1298_Std_Mem40_MFCC_8",
            "Std_Acc1298_Std_Mem40_MFCC_9",
            "Std_Acc1298_Std_Mem40_MFCC_10",
            "Std_Acc1298_Std_Mem40_MFCC_11",
            "Std_Acc1298_Std_Mem40_MFCC_12",
            "BH_LowPeakAmp",
            "BH_LowPeakBPM",
            "BH_HighPeakAmp",
            "BH_HighPeakBPM",
            "BH_HighLowRatio",
            "BHSUM1",
            "BHSUM2",
            "BHSUM3",
            "amazed.suprised",
            "happy.pleased",
            "relaxing.calm",
            "quiet.still",
            "sad.lonely",
            "angry.aggresive",
        ],
        1119: [
            "age",
            "workclass",
            "fnlwgt:",
            "education:",
            "education-num:",
            "marital-status:",
            "occupation:",
            "relationship:",
            "race:",
            "sex:",
            "capital-gain:",
            "capital-loss:",
            "hours-per-week:",
            "native-country:",
            "class",
        ],
        40966: [
            "DYRK1A_N",
            "ITSN1_N",
            "BDNF_N",
            "NR1_N",
            "NR2A_N",
            "pAKT_N",
            "pBRAF_N",
            "pCAMKII_N",
            "pCREB_N",
            "pELK_N",
            "pERK_N",
            "pJNK_N",
            "PKCA_N",
            "pMEK_N",
            "pNR1_N",
            "pNR2A_N",
            "pNR2B_N",
            "pPKCAB_N",
            "pRSK_N",
            "AKT_N",
            "BRAF_N",
            "CAMKII_N",
            "CREB_N",
            "ELK_N",
            "ERK_N",
            "GSK3B_N",
            "JNK_N",
            "MEK_N",
            "TRKA_N",
            "RSK_N",
            "APP_N",
            "Bcatenin_N",
            "SOD1_N",
            "MTOR_N",
            "P38_N",
            "pMTOR_N",
            "DSCR1_N",
            "AMPKA_N",
            "NR2B_N",
            "pNUMB_N",
            "RAPTOR_N",
            "TIAM1_N",
            "pP70S6_N",
            "NUMB_N",
            "P70S6_N",
            "pGSK3B_N",
            "pPKCG_N",
            "CDK5_N",
            "S6_N",
            "ADARB1_N",
            "AcetylH3K9_N",
            "RRP1_N",
            "BAX_N",
            "ARC_N",
            "ERBB4_N",
            "nNOS_N",
            "Tau_N",
            "GFAP_N",
            "GluR3_N",
            "GluR4_N",
            "IL1B_N",
            "P3525_N",
            "pCASP9_N",
            "PSD95_N",
            "SNCA_N",
            "Ubiquitin_N",
            "pGSK3B_Tyr216_N",
            "SHH_N",
            "BAD_N",
            "BCL2_N",
            "pS6_N",
            "pCFOS_N",
            "SYP_N",
            "H3AcK18_N",
            "EGR1_N",
            "H3MeK4_N",
            "CaNA_N",
            "class",
        ],
        40945: [
            "pclass",
            "survived",
            "name",
            "sex",
            "age",
            "sibsp",
            "parch",
            "ticket",
            "fare",
            "cabin",
            "embarked",
            "boat",
            "body",
            "home.dest",
        ],
    }

def datasets_missing_values():
    return {
        61: {},
        2: {
            "family": 11,
            "temper_rolling": 9,
            "condition": 2,
            "formability": 4,
            "non-ageing": 10,
            "surface-finish": 11,
            "enamelability": 11,
            "bc": 11,
            "bf": 10,
            "bt": 11,
            "bw%2Fme": 8,
            "bl": 9,
            "m": 11,
            "chrom": 11,
            "phos": 11,
            "cbond": 10,
            "marvi": 11,
            "exptl": 11,
            "ferro": 11,
            "corr": 11,
            "blue%2Fbright%2Fvarn%2Fclean": 11,
            "lustre": 8,
            "jurofm": 11,
            "s": 11,
            "p": 11,
            "oil": 10,
            "packing": 11,
        },
        561: {},
        40589: {},
        1119: {},
        40966: {"BCL2_N": 7},
        40945: {
            "age": 263,
            "fare": 1,
            "cabin": 1014,
            "embarked": 2,
            "boat": 823,
            "body": 1188,
            "home.dest": 564,
        },
    }

def test_fetch_openml_types_inference(
    data_id,
    parser,
    expected_n_categories,
    expected_n_floats,
    expected_n_ints,
    datasets_column_names,
    datasets_missing_values,
):
    """Check that `fetch_openml` infer the right number of categories, integers, and
    floats."""

    )

    n_categories = len(
        [dtype for dtype in frame.dtypes if isinstance(dtype, CategoricalDtype)]
    )
    n_floats = len([dtype for dtype in frame.dtypes if dtype.kind == "f"])
    n_ints = len([dtype for dtype in frame.dtypes if dtype.kind == "i"])

    assert n_categories == expected_n_categories
    assert n_floats == expected_n_floats
    assert n_ints == expected_n_ints

    assert frame.columns.tolist() == datasets_column_names[data_id]

    frame_feature_to_n_nan = frame.isna().sum().to_dict()
    for name, n_missing in frame_feature_to_n_nan.items():
        expected_missing = datasets_missing_values[data_id].get(name, 0)
        assert n_missing == expected_missing

def test_fetch_openml_validation_parameter(monkeypatch, params, err_msg):
        fetch_openml(data_id=data_id, **params)

def test_fetch_openml_requires_pandas_error(monkeypatch, params):
    """Check that we raise the proper errors when we require pandas."""
    try:
        check_pandas_support("test_fetch_openml_requires_pandas")
        _monkey_patch_webbased_functions(monkeypatch, data_id, True)
        err_msg = "requires pandas to be installed. Alternatively, explicitly"
        with pytest.raises(ImportError, match=err_msg):
            fetch_openml(data_id=data_id, **params)
    else:
        raise SkipTest("This test requires pandas to not be installed.")

def test_fetch_openml_sparse_arff_error(monkeypatch, params, err_msg):
    """Check that we raise the expected error for sparse ARFF datasets and
    a wrong set of incompatible parameters.
    """
    data_id = 292

        fetch_openml(
            data_id=data_id,
            cache=False,
            **params,
        )

def test_fetch_openml_auto_mode(monkeypatch, data_id, data_type):
    """Check the auto mode of `fetch_openml`."""

    data = fetch_openml(data_id=data_id, as_frame="auto", cache=False)
    klass = pd.DataFrame if data_type == "dataframe" else scipy.sparse.csr_matrix
    assert isinstance(data.data, klass)

def test_convert_arff_data_dataframe_warning_low_memory_pandas(monkeypatch):
    """Check that we raise a warning regarding the working memory when using
    LIAC-ARFF parser."""

    msg = "Could not adhere to working_memory config."
        with config_context(working_memory=1e-6):
            fetch_openml(
                data_id=data_id,
                as_frame=True,
                cache=False,
                parser="liac-arff",
            )

def test_fetch_openml_iris_warn_multiple_version(monkeypatch, gzip_response):
    """Check that a warning is raised when multiple versions exist and no version is
    requested."""
    data_name = "iris"

    _monkey_patch_webbased_functions(monkeypatch, data_id, gzip_response)

    msg = re.escape(
        "Multiple active versions of the dataset matching the name"
        " iris exist. Versions may be fundamentally different, "
        "returning version 1. Available versions:\n"
        "- version 1, status: active\n"
        "  url: https://www.openml.org/search?type=data&id=61\n"
        "- version 3, status: active\n"
        "  url: https://www.openml.org/search?type=data&id=969\n"
    )
            name=data_name,
            as_frame=False,
            parser="liac-arff",
        )

def test_fetch_openml_no_target(monkeypatch, gzip_response):
    """Check that we can get a dataset without target."""
    target_column = None
    expected_observations = 150
    expected_features = 5

    data = fetch_openml(
    )
    assert data.data.shape == (expected_observations, expected_features)
    assert data.target is None

def test_missing_values_pandas(monkeypatch, gzip_response, parser):
    """check that missing values in categories are compatible with pandas
    categorical"""

    data_id = 42585
    penguins = fetch_openml(
    )

    cat_dtype = penguins.data.dtypes["sex"]
    # there are nans in the categorical
    assert penguins.data["sex"].isna().any()
    assert_array_equal(cat_dtype.categories, ["FEMALE", "MALE", "_"])

def test_fetch_openml_inactive(monkeypatch, gzip_response, dataset_params):
    """Check that we raise a warning when the dataset is inactive."""
    data_id = 40675
    msg = "Version 1 of dataset glass2 is inactive,"
        glass2 = fetch_openml(
            cache=False, as_frame=False, parser="liac-arff", **dataset_params
        )
    assert glass2.data.shape == (163, 9)
    assert glass2.details["id"] == "40675"

def test_fetch_openml_error(
    monkeypatch, gzip_response, data_id, params, err_type, err_msg, parser
):
    if params.get("as_frame", True) or parser == "pandas":
        pytest.importorskip("pandas")
        fetch_openml(cache=False, parser=parser, **params)

def test_fetch_openml_raises_illegal_argument(params, err_type, err_msg):
        fetch_openml(**params)

def test_warn_ignore_attribute(monkeypatch, gzip_response):
    data_id = 40966
    expected_row_id_msg = "target_column='{}' has flag is_row_identifier."
    expected_ignore_msg = "target_column='{}' has flag is_ignore."
    # single column test
    target_col = "MouseID"
    msg = expected_row_id_msg.format(target_col)
            target_column=target_col,
        )
    target_col = "Genotype"
    msg = expected_ignore_msg.format(target_col)
        )
    # multi column test
            target_column=[target_col, "class"],
        )
        )

def test_dataset_with_openml_error(monkeypatch, gzip_response):
    data_id = 1
    msg = "OpenML registered a problem with the dataset. It might be unusable. Error:"
        fetch_openml(data_id=data_id, cache=False, as_frame=False, parser="liac-arff")

def test_dataset_with_openml_warning(monkeypatch, gzip_response):
    data_id = 3
    msg = "OpenML raised a warning on the dataset. It might be unusable. Warning:"

def test_fetch_openml_overwrite_default_params_read_csv(monkeypatch):
    """Check that we can overwrite the default parameters of `read_csv`."""
    data_id = 1590
    _monkey_patch_webbased_functions(monkeypatch, data_id=data_id, gzip_response=False)

        "data_id": data_id,
        "as_frame": True,
        "cache": False,
        "parser": "pandas",
    }

    # By default, the initial spaces are skipped. We checked that setting the parameter
    # `skipinitialspace` to False will have an effect.
    adult_without_spaces = fetch_openml(**common_params)
    adult_with_spaces = fetch_openml(
        **common_params, read_csv_kwargs={"skipinitialspace": False}
    )
        cat.startswith(" ") for cat in adult_with_spaces.frame["class"].cat.categories
    )
    assert not any(
        cat.startswith(" ")
        for cat in adult_without_spaces.frame["class"].cat.categories
    )

def test_open_openml_url_cache(monkeypatch, gzip_response, tmpdir):

    openml_path = _MONKEY_PATCH_LOCAL_OPENML_PATH.format(data_id) + "/filename.arff"
    url = f"https://www.openml.org/{openml_path}"
    cache_directory = str(tmpdir.mkdir("scikit_learn_data"))
    # first fill the cache
    response1 = _open_openml_url(url, cache_directory)
    # assert file exists
    location = _get_local_path(openml_path, cache_directory)
    assert os.path.isfile(location)
    # redownload, to utilize cache
    response2 = _open_openml_url(url, cache_directory)
    assert response1.read() == response2.read()

def test_open_openml_url_unlinks_local_path(monkeypatch, tmpdir, write_to_disk):

        if write_to_disk:
            with open(location, "w") as f:
                f.write("")
        raise ValueError("Invalid request")

    monkeypatch.setattr(sklearn.datasets._openml, "urlopen", _mock_urlopen)

    with pytest.raises(ValueError, match="Invalid request"):
        _open_openml_url(url, cache_directory)

    assert not os.path.exists(location)

def test_retry_with_clean_cache(tmpdir):
    openml_path = _MONKEY_PATCH_LOCAL_OPENML_PATH.format(data_id)
    os.makedirs(os.path.dirname(location))

    with open(location, "w") as f:
        f.write("")

    @_retry_with_clean_cache(openml_path, cache_directory)
    def _load_data():
        # The first call will raise an error since location exists
        if os.path.exists(location):
            raise Exception("File exist!")
        return 1

    warn_msg = "Invalid cache, redownloading file"
        result = _load_data()
    assert result == 1

def test_retry_with_clean_cache_http_error(tmpdir):

        raise HTTPError(
            url=None, code=412, msg="Simulated mock error", hdrs=None, fp=BytesIO()
        )

    error_msg = "Simulated mock error"
    with pytest.raises(HTTPError, match=error_msg):
        _load_data()

def test_fetch_openml_cache(monkeypatch, gzip_response, tmpdir):
    def _mock_urlopen_raise(request, *args, **kwargs):
            "This mechanism intends to test correct cache"
            "handling. As such, urlopen should never be "
            "accessed. URL: %s" % request.get_full_url()
        )

    X_fetched, y_fetched = fetch_openml(
        cache=True,
        data_home=cache_directory,
    )

    monkeypatch.setattr(sklearn.datasets._openml, "urlopen", _mock_urlopen_raise)

    X_cached, y_cached = fetch_openml(
    )
    np.testing.assert_array_equal(X_fetched, X_cached)
    np.testing.assert_array_equal(y_fetched, y_cached)

def test_fetch_openml_verify_checksum(monkeypatch, as_frame, tmpdir, parser):
    """Check that the checksum is working as expected."""
    if as_frame or parser == "pandas":

    data_id = 2

    # create a temporary modified arff file
    original_data_module = OPENML_TEST_DATA_MODULE + "." + f"id_{data_id}"
    original_data_file_name = "data-v1-dl-1666876.arff.gz"
    original_data_path = resources.files(original_data_module) / original_data_file_name
    corrupt_copy_path = tmpdir / "test_invalid_checksum.arff"
    with original_data_path.open("rb") as orig_file:
        orig_gzip = gzip.open(orig_file, "rb")
        data = bytearray(orig_gzip.read())
        data[len(data) - 1] = 37

    with gzip.GzipFile(corrupt_copy_path, "wb") as modified_gzip:
        modified_gzip.write(data)

    # Requests are already mocked by monkey_patch_webbased_functions.
    # We want to reuse that mock for all requests except file download,
    # hence creating a thin mock over the original mock
    mocked_openml_url = sklearn.datasets._openml.urlopen

    def swap_file_mock(request, *args, **kwargs):
        if url.endswith("data/v1/download/1666876/anneal.arff"):
            with open(corrupt_copy_path, "rb") as f:
                corrupted_data = f.read()
            return _MockHTTPResponse(BytesIO(corrupted_data), is_gzip=True)
        else:
            return mocked_openml_url(request)

    monkeypatch.setattr(sklearn.datasets._openml, "urlopen", swap_file_mock)

    # validate failed checksum
    with pytest.raises(ValueError) as exc:
        sklearn.datasets.fetch_openml(
            data_id=data_id, cache=False, as_frame=as_frame, parser=parser
        )
    # exception message should have file-path
    assert exc.match("1666876")

def test_open_openml_url_retry_on_network_error(monkeypatch):
    def _mock_urlopen_network_error(request, *args, **kwargs):
            url=None, code=404, msg="Simulated network error", hdrs=None, fp=BytesIO()
        )

        sklearn.datasets._openml, "urlopen", _mock_urlopen_network_error
    )

    invalid_openml_url = "https://api.openml.org/invalid-url"

    with pytest.warns(
        UserWarning,
        match=re.escape(
            "A network error occurred while downloading"
            f" {invalid_openml_url}. Retrying..."
        ),
    ) as record:
        with pytest.raises(HTTPError, match="Simulated network error"):
            _open_openml_url(invalid_openml_url, None, delay=0)

def test_fetch_openml_with_ignored_feature(monkeypatch, gzip_response, parser):
    """Check that we can load the "zoo" dataset.
    https://github.com/scikit-learn/scikit-learn/issues/14340
    """
    data_id = 62

    dataset = sklearn.datasets.fetch_openml(
        data_id=data_id, cache=False, as_frame=False, parser=parser
    )
    assert dataset is not None
    # The dataset has 17 features, including 1 ignored (animal),
    # so we assert that we don't have the ignored feature in the final Bunch
    assert dataset["data"].shape == (101, 16)
    assert "animal" not in dataset["feature_names"]

def test_fetch_openml_strip_quotes(monkeypatch):
    """Check that we strip the single quotes when used as a string delimiter.

    https://github.com/scikit-learn/scikit-learn/issues/23381
    """

    common_params = {"as_frame": True, "cache": False, "data_id": data_id}
    mice_pandas = fetch_openml(parser="pandas", **common_params)
    mice_liac_arff = fetch_openml(parser="liac-arff", **common_params)
    pd.testing.assert_series_equal(mice_pandas.target, mice_liac_arff.target)
    assert not mice_pandas.target.str.startswith("'").any()
    assert not mice_pandas.target.str.endswith("'").any()

    # similar behaviour should be observed when the column is not the target
    mice_pandas = fetch_openml(parser="pandas", target_column="NUMB_N", **common_params)
    mice_liac_arff = fetch_openml(
        parser="liac-arff", target_column="NUMB_N", **common_params
    )
    pd.testing.assert_series_equal(
        mice_pandas.frame["class"], mice_liac_arff.frame["class"]
    )
    assert not mice_pandas.frame["class"].str.startswith("'").any()
    assert not mice_pandas.frame["class"].str.endswith("'").any()

def test_fetch_openml_leading_whitespace(monkeypatch):
    """Check that we can strip leading whitespace in pandas parser.

    https://github.com/scikit-learn/scikit-learn/issues/25311
    """

    adult_pandas = fetch_openml(parser="pandas", **common_params)
    adult_liac_arff = fetch_openml(parser="liac-arff", **common_params)
        adult_pandas.frame["class"], adult_liac_arff.frame["class"]
    )

def test_fetch_openml_quotechar_escapechar(monkeypatch):
    """Check that we can handle escapechar and single/double quotechar.

    https://github.com/scikit-learn/scikit-learn/issues/25478
    """
    data_id = 42074

    pd.testing.assert_frame_equal(adult_pandas.frame, adult_liac_arff.frame)

        return {}

        output = (
            + suffix
        )
        return (
        )

        )

            else:

            url=url,
        )

            url=url,
        )

        )

        )

            )

            else:

        else:

        else:

        else:

        return 1

        )

        )

        else:

        )

def test_make_classification():
    weights = [0.1, 0.25]
        n_samples=100,
        n_redundant=1,
        n_repeated=1,
        n_classes=3,
        hypercube=False,
        shift=None,
        scale=None,
        weights=weights,
    )

    assert weights == [0.1, 0.25]
    assert X.shape == (100, 20), "X shape mismatch"
    assert y.shape == (100,), "y shape mismatch"
    assert np.unique(y).shape == (3,), "Unexpected number of classes"
    assert sum(y == 0) == 10, "Unexpected number of samples in class #0"
    assert sum(y == 1) == 25, "Unexpected number of samples in class #1"
    assert sum(y == 2) == 65, "Unexpected number of samples in class #2"

    # Test for n_features > 30
        n_features=31,
        n_informative=31,
        hypercube=True,
        scale=0.5,
    )

    assert X.shape == (2000, 31), "X shape mismatch"
    assert y.shape == (2000,), "y shape mismatch"
    assert (
        np.unique(X.view([("", X.dtype)] * X.shape[1]))
        .view(X.dtype)
        .reshape(-1, X.shape[1])
        .shape[0]
        == 2000
    ), "Unexpected number of unique rows"

def test_make_classification_informative_features():
    """Test the construction of informative features in make_classification

    Also tests `n_clusters_per_class`, `n_classes`, `hypercube` and
    fully-specified `weights`.
    """
    # Create very separate clusters; check that vertices are unique and
    # correspond to classes
    class_sep = 1e6
    make = partial(
        make_classification,
        class_sep=class_sep,
        flip_y=0,
        shift=0,
        scale=1,
    )

    for n_informative, weights, n_clusters_per_class in [
        (2, [1], 1),
        (2, [1 / 3] * 3, 1),
        (2, [1 / 4] * 4, 1),
        (2, [1 / 2] * 2, 2),
        (2, [3 / 4, 1 / 4], 2),
        (10, [1 / 3] * 3, 10),
        (64, [1], 1),
    ]:
        n_classes = len(weights)
        n_clusters = n_classes * n_clusters_per_class
        n_samples = n_clusters * 50

        for hypercube in (False, True):
            X, y = make(
                n_samples=n_samples,
                n_classes=n_classes,
                weights=weights,
                n_features=n_informative,
                n_informative=n_informative,
                n_clusters_per_class=n_clusters_per_class,
                hypercube=hypercube,
                random_state=0,
            )

            assert X.shape == (n_samples, n_informative)
            assert y.shape == (n_samples,)

            # Cluster by sign, viewed as strings to allow uniquing
            signs = np.sign(X)
            signs = signs.view(dtype="|S{0}".format(signs.strides[0])).ravel()
            unique_signs, cluster_index = np.unique(signs, return_inverse=True)

            assert len(unique_signs) == n_clusters, (
                "Wrong number of clusters, or not in distinct quadrants"
            )

            clusters_by_class = defaultdict(set)
            for cluster, cls in zip(cluster_index, y):
                clusters_by_class[cls].add(cluster)
            for clusters in clusters_by_class.values():
                assert len(clusters) == n_clusters_per_class, (
                    "Wrong number of clusters per class"
                )
            assert len(clusters_by_class) == n_classes, "Wrong number of classes"

            assert_array_almost_equal(
                np.bincount(y) / len(y) // weights,
                [1] * n_classes,
                err_msg="Wrong number of samples per class",
            )

            # Ensure on vertices of hypercube
            for cluster in range(len(unique_signs)):
                centroid = X[cluster_index == cluster].mean(axis=0)
                if hypercube:
                    assert_array_almost_equal(
                        np.abs(centroid) / class_sep,
                        np.ones(n_informative),
                        decimal=5,
                        err_msg="Clusters are not centered on hypercube vertices",
                    )
                else:
                    with pytest.raises(AssertionError):
                            np.abs(centroid) / class_sep,
                            np.ones(n_informative),
                            decimal=5,
                            err_msg=(
                                "Clusters should not be centered on hypercube vertices"
                            ),
                        )

        make(n_features=2, n_informative=2, n_classes=5, n_clusters_per_class=1)
        make(n_features=2, n_informative=2, n_classes=3, n_clusters_per_class=2)

def test_make_classification_return_x_y():
    """
    Test that make_classification returns a Bunch when return_X_y is False.

    Also that bunch.X is the same as X
    """

    kwargs = {
        "n_samples": 100,
        "n_features": 20,
        "n_informative": 5,
        "n_redundant": 1,
        "n_repeated": 1,
        "n_classes": 3,
        "n_clusters_per_class": 2,
        "weights": None,
        "flip_y": 0.01,
        "class_sep": 1.0,
        "hypercube": True,
        "shift": 0.0,
        "scale": 1.0,
        "shuffle": True,
        "random_state": 42,
        "return_X_y": True,
    }

    X, y = make_classification(**kwargs)

    kwargs["return_X_y"] = False
    bunch = make_classification(**kwargs)

    assert (
        hasattr(bunch, "DESCR")
        and hasattr(bunch, "parameters")
        and hasattr(bunch, "feature_info")
        and hasattr(bunch, "X")
        and hasattr(bunch, "y")
    )

    def count(str_):
        return bunch.feature_info.count(str_)

    assert np.array_equal(X, bunch.X)
    assert np.array_equal(y, bunch.y)
    assert bunch.DESCR == make_classification.__doc__
    assert bunch.parameters == kwargs
    assert count("informative") == kwargs["n_informative"]
    assert count("redundant") == kwargs["n_redundant"]
    assert count("repeated") == kwargs["n_repeated"]

def test_make_classification_weights_type(weights, err_type, err_msg):
        make_classification(weights=weights)

def test_make_classification_weights_array_or_list_ok(kwargs):
    X1, y1 = make_classification(weights=[0.1, 0.9], random_state=0, **kwargs)
    X2, y2 = make_classification(weights=np.array([0.1, 0.9]), random_state=0, **kwargs)
    assert_almost_equal(X1, X2)
    assert_almost_equal(y1, y2)

def test_make_multilabel_classification_return_sequences():
    for allow_unlabeled, min_length in zip((True, False), (0, 1)):
        X, Y = make_multilabel_classification(
            n_samples=100,
            n_features=20,
            n_classes=3,
            return_indicator=False,
            allow_unlabeled=allow_unlabeled,
        )
        assert X.shape == (100, 20), "X shape mismatch"
        if not allow_unlabeled:
            assert max([max(y) for y in Y]) == 2
        assert min([len(y) for y in Y]) == min_length
        assert max([len(y) for y in Y]) <= 3

def test_make_multilabel_classification_return_indicator():
            n_samples=25,
        )
        assert X.shape == (25, 20), "X shape mismatch"
        assert Y.shape == (25, 3), "Y shape mismatch"
        assert np.all(np.sum(Y, axis=0) > min_length)

    # Also test return_distributions and return_indicator with True
    X2, Y2, p_c, p_w_c = make_multilabel_classification(
        n_samples=25,
        allow_unlabeled=allow_unlabeled,
        return_distributions=True,
    )

    assert_array_almost_equal(X, X2)
    assert_array_equal(Y, Y2)
    assert p_c.shape == (3,)
    assert_almost_equal(p_c.sum(), 1)
    assert p_w_c.shape == (20, 3)
    assert_almost_equal(p_w_c.sum(axis=0), [1] * 3)

def test_make_multilabel_classification_return_indicator_sparse():
            return_indicator="sparse",
        )
        assert sp.issparse(Y)

def test_make_hastie_10_2():
    X, y = make_hastie_10_2(n_samples=100, random_state=0)
    assert X.shape == (100, 10), "X shape mismatch"
    assert np.unique(y).shape == (2,), "Unexpected number of classes"

def test_make_regression(global_random_seed):
    X, y, c = make_regression(
        n_samples=200,
        effective_rank=5,
        coef=True,
        bias=0.0,
        noise=1.0,
    )

    assert X.shape == (200, 10), "X shape mismatch"
    assert y.shape == (200,), "y shape mismatch"
    assert c.shape == (10,), "coef shape mismatch"
    assert sum(c != 0.0) == 3, "Unexpected number of informative features"

    # Test that y ~= np.dot(X, c) + bias + N(0, 1.0).
    assert_almost_equal(np.std(y - np.dot(X, c)), 1.0, decimal=1)

    # Test with small number of features.
    X, y = make_regression(n_samples=100, n_features=1)  # n_informative=3
    assert X.shape == (100, 1)

def test_make_regression_multitarget(global_random_seed):
        n_targets=3,
    )

    assert y.shape == (100, 3), "y shape mismatch"
    assert c.shape == (10, 3), "coef shape mismatch"
    assert_array_equal(sum(c != 0.0), 3, "Unexpected number of informative features")

    # Test that y ~= np.dot(X, c) + bias + N(0, 1.0)

def test_make_blobs(global_random_seed):
    cluster_stds = np.array([0.05, 0.2, 0.4])
    cluster_centers = np.array([[0.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
    X, y = make_blobs(
        n_samples=50,
        n_features=2,
        centers=cluster_centers,
        cluster_std=cluster_stds,
    )

    assert X.shape == (50, 2), "X shape mismatch"
    assert y.shape == (50,), "y shape mismatch"
    assert np.unique(y).shape == (3,), "Unexpected number of blobs"
    for i, (ctr, std) in enumerate(zip(cluster_centers, cluster_stds)):
        assert_almost_equal((X[y == i] - ctr).std(), std, 1, "Unexpected std")

def test_make_blobs_n_samples_list():
    n_samples = [50, 30, 20]
    X, y = make_blobs(n_samples=n_samples, n_features=2, random_state=0)

    assert X.shape == (sum(n_samples), 2), "X shape mismatch"
    assert all(np.bincount(y, minlength=len(n_samples)) == n_samples), (
        "Incorrect number of samples per blob"
    )

def test_make_blobs_n_samples_list_with_centers(global_random_seed):
    n_samples = [20, 20, 20]
    centers = np.array([[0.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
        centers=centers,
    )

    )
    for i, (ctr, std) in enumerate(zip(centers, cluster_stds)):

def test_make_blobs_n_samples_centers_none(n_samples):
    centers = None
    X, y = make_blobs(n_samples=n_samples, centers=centers, random_state=0)

    )

def test_make_blobs_return_centers():
    n_samples = [10, 20]
    n_features = 3
    X, y, centers = make_blobs(
        n_samples=n_samples, n_features=n_features, return_centers=True, random_state=0
    )

    assert centers.shape == (len(n_samples), n_features)

def test_make_blobs_error():
    wrong_centers_msg = re.escape(
        "Length of `n_samples` not consistent with number of centers. "
        f"Got n_samples = {n_samples} and centers = {centers[:-1]}"
    )
    with pytest.raises(ValueError, match=wrong_centers_msg):
        make_blobs(n_samples, centers=centers[:-1])
    wrong_std_msg = re.escape(
        "Length of `clusters_std` not consistent with number of centers. "
        f"Got centers = {centers} and cluster_std = {cluster_stds[:-1]}"
    )
    with pytest.raises(ValueError, match=wrong_std_msg):
        make_blobs(n_samples, centers=centers, cluster_std=cluster_stds[:-1])
    wrong_type_msg = "Parameter `centers` must be array-like. Got {!r} instead".format(
        3
    )
    with pytest.raises(ValueError, match=wrong_type_msg):
        make_blobs(n_samples, centers=3)

def test_make_friedman1(global_random_seed):
    X, y = make_friedman1(
        n_samples=5, n_features=10, noise=0.0, random_state=global_random_seed
    )

    assert X.shape == (5, 10), "X shape mismatch"
    assert y.shape == (5,), "y shape mismatch"

        y,
        + 5 * X[:, 4],
    )

def test_make_friedman2(global_random_seed):
    X, y = make_friedman2(n_samples=5, noise=0.0, random_state=global_random_seed)

    assert X.shape == (5, 4), "X shape mismatch"

        y, (X[:, 0] ** 2 + (X[:, 1] * X[:, 2] - 1 / (X[:, 1] * X[:, 3])) ** 2) ** 0.5
    )

def test_make_friedman3(global_random_seed):
    X, y = make_friedman3(n_samples=5, noise=0.0, random_state=global_random_seed)

        y, np.arctan((X[:, 1] * X[:, 2] - 1 / (X[:, 1] * X[:, 3])) / X[:, 0])
    )

def test_make_low_rank_matrix():
        n_features=25,
        tail_strength=0.01,
    )

    assert X.shape == (50, 25), "X shape mismatch"

    from numpy.linalg import svd

    u, s, v = svd(X)
    assert sum(s) - 5 < 0.1, "X rank is not approximately 5"

def test_make_sparse_coded_signal(global_random_seed):
    Y, D, X = make_sparse_coded_signal(
        n_samples=5,
        n_components=8,
        n_nonzero_coefs=3,
    )
    assert Y.shape == (5, 10), "Y shape mismatch"
    assert D.shape == (8, 10), "D shape mismatch"
    assert X.shape == (5, 8), "X shape mismatch"
    for row in X:
        assert len(np.flatnonzero(row)) == 3, "Non-zero coefs mismatch"
    assert_allclose(Y, X @ D)
    assert_allclose(np.sqrt((D**2).sum(axis=1)), np.ones(D.shape[0]))

def test_make_sparse_uncorrelated():
    X, y = make_sparse_uncorrelated(n_samples=5, n_features=10, random_state=0)

def test_make_spd_matrix(global_random_seed):
    X = make_spd_matrix(n_dim=5, random_state=global_random_seed)

    assert X.shape == (5, 5), "X shape mismatch"
    assert_array_almost_equal(X, X.T)

    from numpy.linalg import eig

    eigenvalues, _ = eig(X)
    assert np.all(eigenvalues > 0), "X is not positive-definite"

def test_make_sparse_spd_matrix(norm_diag, sparse_format, global_random_seed):
    n_dim = 5
    X = make_sparse_spd_matrix(
        n_dim=n_dim,
        norm_diag=norm_diag,
        sparse_format=sparse_format,
    )

    assert X.shape == (n_dim, n_dim), "X shape mismatch"
        assert not sp.issparse(X)
        assert_allclose(X, X.T)
        Xarr = X
    else:
        assert sp.issparse(X) and X.format == sparse_format
        assert_allclose_dense_sparse(X, X.T)
        Xarr = X.toarray()

    # Do not use scipy.sparse.linalg.eigs because it cannot find all eigenvalues
    eigenvalues, _ = eig(Xarr)

        # Check that leading diagonal elements are 1
        assert_array_almost_equal(Xarr.diagonal(), np.ones(n_dim))

def test_make_swiss_roll(global_random_seed, hole):
    X, t = make_swiss_roll(
        n_samples=5, noise=0.0, random_state=global_random_seed, hole=hole
    )

    assert X.shape == (5, 3)
    assert t.shape == (5,)
    assert_array_almost_equal(X[:, 0], t * np.cos(t))
    assert_array_almost_equal(X[:, 2], t * np.sin(t))

def test_make_s_curve(global_random_seed):
    X, t = make_s_curve(n_samples=5, noise=0.0, random_state=global_random_seed)

    assert X.shape == (5, 3), "X shape mismatch"
    assert t.shape == (5,), "t shape mismatch"
    assert_array_almost_equal(X[:, 0], np.sin(t))
    assert_array_almost_equal(X[:, 2], np.sign(t) * (np.cos(t) - 1))

def test_make_biclusters():
    X, rows, cols = make_biclusters(
        shape=(100, 100), n_clusters=4, shuffle=True, random_state=0
    )
    assert X.shape == (100, 100), "X shape mismatch"
    assert rows.shape == (4, 100), "rows shape mismatch"
    assert cols.shape == (
        4,
        100,
    ), "columns shape mismatch"
    assert_all_finite(X)
    assert_all_finite(rows)
    assert_all_finite(cols)

    X2, _, _ = make_biclusters(
    )

def test_make_checkerboard():
    X, rows, cols = make_checkerboard(
        shape=(100, 100), n_clusters=(20, 5), shuffle=True, random_state=0
    )
    assert rows.shape == (100, 100), "rows shape mismatch"
        100,
        100,

        shape=(100, 100), n_clusters=2, shuffle=True, random_state=0
    )

    X1, _, _ = make_checkerboard(
    )
    X2, _, _ = make_checkerboard(
    )
    assert_array_almost_equal(X1, X2)

def test_make_moons(global_random_seed):
    X, y = make_moons(3, shuffle=False, random_state=global_random_seed)
    for x, label in zip(X, y):
        center = [0.0, 0.0] if label == 0 else [1.0, 0.5]
        dist_sqr = ((x - center) ** 2).sum()
        assert_almost_equal(
            dist_sqr, 1.0, err_msg="Point is not on expected unit circle"
        )

def test_make_moons_unbalanced():
    X, y = make_moons(n_samples=(7, 5))
    assert np.sum(y == 0) == 7 and np.sum(y == 1) == 5, (
        "Number of samples in a moon is wrong"
    )
    assert X.shape == (12, 2), "X shape mismatch"
    assert y.shape == (12,), "y shape mismatch"

        match=r"`n_samples` can be either an int or a two-element tuple.",
    ):
        make_moons(n_samples=(10,))

def test_make_circles():
    factor = 0.3

    for n_samples, n_outer, n_inner in [(7, 3, 4), (8, 4, 4)]:
        # Testing odd and even case, because in the past make_circles always
        # created an even number of samples.
        X, y = make_circles(n_samples, shuffle=False, noise=None, factor=factor)
        assert X.shape == (n_samples, 2), "X shape mismatch"
        assert y.shape == (n_samples,), "y shape mismatch"
        center = [0.0, 0.0]
        for x, label in zip(X, y):
            dist_sqr = ((x - center) ** 2).sum()
            dist_exp = 1.0 if label == 0 else factor**2
                dist_sqr, dist_exp, err_msg="Point is not on expected circle"
            )

        assert X[y == 0].shape == (
            n_outer,
            2,
        ), "Samples not correctly distributed across circles."
        assert X[y == 1].shape == (
            n_inner,
            2,

def test_make_circles_unbalanced():
    X, y = make_circles(n_samples=(2, 8))

    assert np.sum(y == 0) == 2, "Number of samples in inner circle is wrong"
    assert np.sum(y == 1) == 8, "Number of samples in outer circle is wrong"
    assert X.shape == (10, 2), "X shape mismatch"
    assert y.shape == (10,), "y shape mismatch"

        match="When a tuple, n_samples must have exactly two elements.",
    ):
        make_circles(n_samples=(10,))

def test_fetch_rcv1(fetch_rcv1_fxt, global_random_seed):
    data1 = fetch_rcv1_fxt(shuffle=False)
    X1, Y1 = data1.data, data1.target
    cat_list, s1 = data1.target_names.tolist(), data1.sample_id

    # test sparsity
    assert sp.issparse(X1)
    assert sp.issparse(Y1)
    assert 60915113 == X1.data.size
    assert 2606875 == Y1.data.size

    # test shapes
    assert (804414, 47236) == X1.shape
    assert (804414, 103) == Y1.shape
    assert (804414,) == s1.shape
    assert 103 == len(cat_list)

    # test descr
    assert data1.DESCR.startswith(".. _rcv1_dataset:")

    # test ordering of categories
    first_categories = ["C11", "C12", "C13", "C14", "C15", "C151"]
    assert_array_equal(first_categories, cat_list[:6])

    # test number of sample for some categories
    some_categories = ("GMIL", "E143", "CCAT")
    number_non_zero_in_cat = (5, 1206, 381327)
    for num, cat in zip(number_non_zero_in_cat, some_categories):
        j = cat_list.index(cat)
        assert num == Y1[:, j].data.size

    # test shuffling and subset
    data2 = fetch_rcv1_fxt(
        shuffle=True, subset="train", random_state=global_random_seed
    )
    X2, Y2 = data2.data, data2.target
    s2 = data2.sample_id

    fetch_func = partial(fetch_rcv1_fxt, shuffle=False, subset="train")
    check_return_X_y(data2, fetch_func)

    # The first 23149 samples are the training samples
    assert_array_equal(np.sort(s1[:23149]), np.sort(s2))

    # test some precise values
    some_sample_ids = (2286, 3274, 14042)
    for sample_id in some_sample_ids:
        idx1 = s1.tolist().index(sample_id)
        idx2 = s2.tolist().index(sample_id)

        feature_values_1 = X1[idx1, :].toarray()
        feature_values_2 = X2[idx2, :].toarray()
        assert_almost_equal(feature_values_1, feature_values_2)

        target_values_1 = Y1[idx1, :].toarray()
        target_values_2 = Y2[idx2, :].toarray()
        assert_almost_equal(target_values_1, target_values_2)

def test_fetch(fetch_covtype_fxt, global_random_seed):
    data1 = fetch_covtype_fxt(shuffle=True, random_state=global_random_seed)
    data2 = fetch_covtype_fxt(shuffle=True, random_state=global_random_seed + 1)

    X1, X2 = data1["data"], data2["data"]
    assert (581012, 54) == X1.shape
    assert X1.shape == X2.shape

    assert X1.sum() == X2.sum()

    y1, y2 = data1["target"], data2["target"]
    assert (X1.shape[0],) == y1.shape
    assert (X1.shape[0],) == y2.shape

    descr_prefix = ".. _covtype_dataset:"
    assert data1.DESCR.startswith(descr_prefix)
    assert data2.DESCR.startswith(descr_prefix)

    fetch_func = partial(fetch_covtype_fxt)
    check_return_X_y(data1, fetch_func)

def test_fetch_asframe(fetch_covtype_fxt):

    bunch = fetch_covtype_fxt(as_frame=True)
    assert hasattr(bunch, "frame")
    assert frame.shape == (581012, 55)
    assert bunch.data.shape == (581012, 54)
    assert bunch.target.shape == (581012,)

    column_names = set(frame.columns)

    # enumerated names are added correctly
    assert set(f"Wilderness_Area_{i}" for i in range(4)) < column_names
    assert set(f"Soil_Type_{i}" for i in range(40)) < column_names

def test_pandas_dependency_message(fetch_covtype_fxt, hide_available_pandas):
    expected_msg = "fetch_covtype with as_frame=True requires pandas"
        fetch_covtype_fxt(as_frame=True)

def test_olivetti_faces(fetch_olivetti_faces_fxt):
    data = fetch_olivetti_faces_fxt(shuffle=True, random_state=0)

    assert isinstance(data, Bunch)
    for expected_keys in ("data", "images", "target", "DESCR"):
        assert expected_keys in data.keys()

    assert data.data.shape == (400, 4096)
    assert data.images.shape == (400, 64, 64)
    assert data.target.shape == (400,)
    assert_array_equal(np.unique(np.sort(data.target)), np.arange(40))
    assert data.DESCR.startswith(".. _olivetti_faces_dataset:")

    # test the return_X_y option
    check_return_X_y(data, fetch_olivetti_faces_fxt)

def pytest_ignore_collect(collection_path, config):
    return True

def _build_re_values():
    quoted_re = r'''
                    "      # open quote followed by zero or more of:
                    (?:
                        (?<!\\)    # no additional backslash
                        (?:\\\\)*  # maybe escaped backslashes
                        \\"        # escaped quote
                    |
                        \\[^"]     # escaping a non-quote
                    |
                        [^"\\]     # non-quote char
                    )*
                    "      # close quote
                    '''
    # a value is surrounded by " or by ' or contains no quotables
    value_re = r'''(?:
        %s|          # a value may be surrounded by "
        %s|          # or by '
        [^,\s"'{}]+  # or may contain no characters requiring quoting
        )''' % (quoted_re,
                quoted_re.replace('"', "'"))

    # This captures (value, error) groups. Because empty values are allowed,
    # we cannot just look for empty values to handle syntax errors.
    # We presume the line has had ',' prepended...
    dense = re.compile(r'''(?x)
        ,                # may follow ','
        \s*
        ((?=,)|$|{value_re})  # empty or value
        |
        (\S.*)           # error
        '''.format(value_re=value_re))

    # This captures (key, value) groups and will have an empty key/value
    # in case of syntax errors.
    # It does not ensure that the line starts with '{' or ends with '}'.
    sparse = re.compile(r'''(?x)
        (?:^\s*\{|,)   # may follow ',', or '{' at line start
        \s*
        (\d+)          # attribute key
        \s+
        (%(value_re)s) # value
        |
        (?!}\s*$)      # not an error if it's }$
        (?!^\s*{\s*}\s*$)  # not an error if it's ^{}$
        \S.*           # error
        ''' % {'value_re': value_re})
    return dense, sparse

def _escape_sub_callback(match):
    s = match.group()
    if len(s) == 2:
        try:
            return _ESCAPE_SUB_MAP[s]
        except KeyError:
            raise ValueError('Unsupported escape sequence: %s' % s)
    if s[1] == 'u':
        return chr(int(s[2:], 16))
    else:
        return chr(int(s[1:], 8))

def _unquote(v):
    if v[:1] in ('"', "'"):
        return re.sub(r'\\([0-9]{1,3}|u[0-9a-f]{4}|.)', _escape_sub_callback,
                      v[1:-1])
    elif v in ('?', ''):
    else:
        return v

def _parse_values(s):
    '''(INTERNAL) Split a line into a list of values'''
    if not _RE_NONTRIVIAL_DATA.search(s):
        # Fast path for trivial cases (unfortunately we have to handle missing
        # values because of the empty string case :(.)
        return [None if s in ('?', '') else s
                for s in next(csv.reader([s]))]

    # _RE_DENSE_VALUES tokenizes despite quoting, whitespace, etc.
    values, errors = zip(*_RE_DENSE_VALUES.findall(',' + s))
    if not any(errors):
        return [_unquote(v) for v in values]
    if _RE_SPARSE_LINE.match(s):
        try:
            return {int(k): _unquote(v)
                    for k, v in _RE_SPARSE_KEY_VALUES.findall(s)}
        except ValueError:
            # an ARFF syntax error in sparse data
            for match in _RE_SPARSE_KEY_VALUES.finditer(s):
                if not match.group(1):
                    raise BadLayout('Error parsing %r' % match.group())
            raise BadLayout('Unknown parsing error')
    else:
        # an ARFF syntax error
        for match in _RE_DENSE_VALUES.finditer(s):
            if match.group(2):
                raise BadLayout('Error parsing %r' % match.group())
        raise BadLayout('Unknown parsing error')

class ArffException(Exception):
    message: Optional[str] = None

    def __init__(self):
        self.line = -1

        return self.message%self.line

class BadRelationFormat(ArffException):
    '''Error raised when the relation declaration is in an invalid format.'''
    message = 'Bad @RELATION format, at line %d.'

class BadAttributeFormat(ArffException):
    '''Error raised when some attribute declaration is in an invalid format.'''
    message = 'Bad @ATTRIBUTE format, at line %d.'

class BadDataFormat(ArffException):
    '''Error raised when some data instance is in an invalid format.'''
    def __init__(self, value):
        super().__init__()
        self.message = (
            'Bad @DATA instance format in line %d: ' +
            ('%s' % value)
        )

class BadAttributeType(ArffException):
    '''Error raised when some invalid type is provided into the attribute
    declaration.'''
    message = 'Bad @ATTRIBUTE type, at line %d.'

class BadAttributeName(ArffException):
    '''Error raised when an attribute name is provided twice the attribute

    def __init__(self, value, value2):
            ('Bad @ATTRIBUTE name %s at line' % value) +
            ' %d, this name is already in use in line' +
            (' %d.' % value2)
        )

class BadNominalValue(ArffException):
    '''Error raised when a value in used in some data instance but is not
    declared into it respective attribute declaration.'''

            ('Data value %s not found in nominal declaration, ' % value)
            + 'at line %d.'
        )

class BadNominalFormatting(ArffException):
    '''Error raised when a nominal value with space is not properly quoted.'''
            ('Nominal data value "%s" not properly quoted in line ' % value) +
            '%d.'
        )

class BadNumericalValue(ArffException):
    '''Error raised when and invalid numerical value is used in some data
    instance.'''
    message = 'Invalid numerical value, at line %d.'

class BadStringValue(ArffException):
    '''Error raise when a string contains space but is not quoted.'''
    message = 'Invalid string value at line %d.'

class BadLayout(ArffException):
    '''Error raised when the layout of the ARFF file has something wrong.'''
    message = 'Invalid layout of the ARFF file, at line %d.'

    def __init__(self, msg=''):
        if msg:
            self.message = BadLayout.message + ' ' + msg.replace('%', '%%')

class BadObject(ArffException):
    '''Error raised when the object representing the ARFF file has something
    wrong.'''
    def __init__(self, msg='Invalid object.'):
        self.msg = msg

        return '%s' % self.msg

def _unescape_sub_callback(match):
    return _UNESCAPE_SUB_MAP[match.group()]

def encode_string(s):
    if _RE_QUOTE_CHARS.search(s):
        return "'%s'" % _RE_ESCAPE_CHARS.sub(_unescape_sub_callback, s)
    return s

class EncodedNominalConversor:
    def __init__(self, values):
        self.values = {v: i for i, v in enumerate(values)}
        self.values[0] = 0

    def __call__(self, value):
        try:
            return self.values[value]
            raise BadNominalValue(value)

class NominalConversor:
        self.values = set(values)
        self.zero_value = values[0]

        if value not in self.values:
            if value == 0:
                # Sparse decode
                # See issue #52: nominals should take their first value when
                # unspecified in a sparse matrix. Naturally, this is consistent
                # with EncodedNominalConversor.
                return self.zero_value
        return str(value)

class DenseGeneratorData:
    '''Internal helper class to allow for different matrix types without
    making the code a huge collection of if statements.'''

    def decode_rows(self, stream, conversors):
        for row in stream:
            values = _parse_values(row)

            if isinstance(values, dict):
                if values and max(values) >= len(conversors):
                    raise BadDataFormat(row)
                # XXX: int 0 is used for implicit values, not '0'
                values = [values[i] if i in values else 0 for i in
                          range(len(conversors))]
            else:
                if len(values) != len(conversors):

            yield self._decode_values(values, conversors)

    def _decode_values(values, conversors):
        try:
            values = [None if value is None else conversor(value)
                      for conversor, value
                      in zip(conversors, values)]
        except ValueError as exc:
            if 'float: ' in str(exc):
                raise BadNumericalValue()

    def encode_data(self, data, attributes):
        '''(INTERNAL) Encodes a line of data.

        Data instances follow the csv format, i.e, attribute values are
        delimited by commas. After converted from csv.

        :param data: a list of values.
        :param attributes: a list of attributes. Used to check if data is valid.
        :return: a string with the encoded data line.
        '''
        current_row = 0

        for inst in data:
            if len(inst) != len(attributes):
                raise BadObject(
                    'Instance %d has %d attributes, expected %d' %
                     (current_row, len(inst), len(attributes))
                )

            new_data = []
            for value in inst:
                if value is None or value == '' or value != value:
                    s = '?'
                else:
                    s = encode_string(str(value))
                new_data.append(s)

            current_row += 1
            yield ','.join(new_data)

class _DataListMixin:
    """Mixin to return a list from decode_rows instead of a generator"""
        return list(super().decode_rows(stream, conversors))

class COOData:
        data, rows, cols = [], [], []
        for i, row in enumerate(stream):
            if not isinstance(values, dict):
                raise BadLayout()
            if not values:
                continue
            row_cols, values = zip(*sorted(values.items()))
            try:
                values = [value if value is None else conversors[key](value)
                          for key, value in zip(row_cols, values)]
            except ValueError as exc:
                if 'float: ' in str(exc):
                    raise BadNumericalValue()
                raise
            except IndexError:
                # conversor out of range
                raise BadDataFormat(row)

            data.extend(values)
            rows.extend([i] * len(values))
            cols.extend(row_cols)

        return data, rows, cols

        num_attributes = len(attributes)
        new_data = []

        row = data.row
        col = data.col
        data = data.data

        # Check if the rows are sorted
        if not all(row[i] <= row[i + 1] for i in range(len(row) - 1)):
            raise ValueError("liac-arff can only output COO matrices with "
                             "sorted rows.")

        for v, col, row in zip(data, col, row):
            if row > current_row:
                # Add empty rows if necessary
                while current_row < row:
                    yield " ".join(["{", ','.join(new_data), "}"])
                    new_data = []
                    current_row += 1

            if col >= num_attributes:
                    'Instance %d has at least %d attributes, expected %d' %
                    (current_row, col + 1, num_attributes)
                )

            if v is None or v == '' or v != v:
                s = '?'
            else:
                s = encode_string(str(v))
            new_data.append("%d %s" % (col, s))

        yield " ".join(["{", ','.join(new_data), "}"])

class LODGeneratorData:

            try:
                yield {key: None if value is None else conversors[key](value)
                       for key, value in values.items()}
                raise

        for row in data:

            if len(row) > 0 and max(row) >= num_attributes:
                    (current_row, max(row) + 1, num_attributes)
                )

            for col in sorted(row):
                v = row[col]
                if v is None or v == '' or v != v:
                    s = '?'
                else:
                    s = encode_string(str(v))
                new_data.append("%d %s" % (col, s))

            yield " ".join(["{", ','.join(new_data), "}"])

def _get_data_object_for_decoding(matrix_type):
    if matrix_type == DENSE:
        return Data()
    elif matrix_type == COO:
        return COOData()
    elif matrix_type == LOD:
        return LODData()
    elif matrix_type == DENSE_GEN:
        return DenseGeneratorData()
    elif matrix_type == LOD_GEN:
        return LODGeneratorData()
    else:
        raise ValueError("Matrix type %s not supported." % str(matrix_type))

def _get_data_object_for_encoding(matrix):
    # Probably a scipy.sparse
    if hasattr(matrix, 'format'):
        if matrix.format == 'coo':
            return COOData()
        else:
            raise ValueError('Cannot guess matrix format!')
    elif isinstance(matrix[0], dict):
    else:

class ArffDecoder:
    '''An ARFF decoder.'''

        '''Constructor.'''
        self._conversors = []
        self._current_line = 0

    def _decode_comment(self, s):
        '''(INTERNAL) Decodes a comment line.

        Comments are single line strings starting, obligatorily, with the ``%``
        character, and can have any symbol, including whitespaces or special
        characters.

        This method must receive a normalized string, i.e., a string without
        padding, including the "\r\n" characters.

        :param s: a normalized string.
        :return: a string with the decoded comment.
        '''
        res = re.sub(r'^\%( )?', '', s)
        return res

    def _decode_relation(self, s):
        '''(INTERNAL) Decodes a relation line.

        The relation declaration is a line with the format ``@RELATION
        <relation-name>``, where ``relation-name`` is a string. The string must
        start with alphabetic character and must be quoted if the name includes
        spaces, otherwise this method will raise a `BadRelationFormat` exception.

        :return: a string with the decoded relation name.
        '''
        _, v = s.split(' ', 1)
        v = v.strip()

        if not _RE_RELATION.match(v):
            raise BadRelationFormat()

        res = str(v.strip('"\''))
        return res

    def _decode_attribute(self, s):
        '''(INTERNAL) Decodes an attribute line.

        The attribute is the most complex declaration in an arff file. All
        attributes must follow the template::

             @attribute <attribute-name> <datatype>

        where ``attribute-name`` is a string, quoted if the name contains any
        whitespace, and ``datatype`` can be:

        - Numerical attributes as ``NUMERIC``, ``INTEGER`` or ``REAL``.
        - Strings as ``STRING``.
        - Dates (NOT IMPLEMENTED).
        - Nominal attributes with format:

            {<nominal-name1>, <nominal-name2>, <nominal-name3>, ...}

        The nominal names follow the rules for the attribute names, i.e., they
        must be quoted if the name contains whitespaces.

        :return: a tuple (ATTRIBUTE_NAME, TYPE_OR_VALUES).
        '''

        # Verify the general structure of declaration
        m = _RE_ATTRIBUTE.match(v)
        if not m:
            raise BadAttributeFormat()

        # Extracts the raw name and type
        name, type_ = m.groups()

        # Extracts the final name
        name = str(name.strip('"\''))

        # Extracts the final type
        if type_[:1] == "{" and type_[-1:] == "}":
            try:
                type_ = _parse_values(type_.strip('{} '))
            except Exception:
                raise BadAttributeType()
            if isinstance(type_, dict):

        else:
            # If not nominal, verify the type name
            type_ = str(type_).upper()
            if type_ not in ['NUMERIC', 'REAL', 'INTEGER', 'STRING']:

        return (name, type_)

    def _decode(self, s, encode_nominal=False, matrix_type=DENSE):
        '''Do the job the ``encode``.'''

        # Make sure this method is idempotent

        # If string, convert to a list of lines
        if isinstance(s, str):
            s = s.strip('\r\n ').replace('\r\n', '\n').split('\n')

        # Create the return object
        obj: ArffContainerType = {
            'description': '',
            'relation': '',
            'attributes': [],
            'data': []
        }
        attribute_names = {}

        # Create the data helper object
        data = _get_data_object_for_decoding(matrix_type)

        # Read all lines
        STATE = _TK_DESCRIPTION
        s = iter(s)
        for row in s:
            self._current_line += 1
            # Ignore empty lines
            row = row.strip(' \r\n')
            if not row: continue

            u_row = row.upper()

            # DESCRIPTION -----------------------------------------------------
            if u_row.startswith(_TK_DESCRIPTION) and STATE == _TK_DESCRIPTION:
                obj['description'] += self._decode_comment(row) + '\n'
            # -----------------------------------------------------------------

            # RELATION --------------------------------------------------------
            elif u_row.startswith(_TK_RELATION):
                if STATE != _TK_DESCRIPTION:
                    raise BadLayout()

                STATE = _TK_RELATION
                obj['relation'] = self._decode_relation(row)

            # ATTRIBUTE -------------------------------------------------------
            elif u_row.startswith(_TK_ATTRIBUTE):
                if STATE != _TK_RELATION and STATE != _TK_ATTRIBUTE:

                STATE = _TK_ATTRIBUTE

                attr = self._decode_attribute(row)
                if attr[0] in attribute_names:
                    raise BadAttributeName(attr[0], attribute_names[attr[0]])
                else:
                    attribute_names[attr[0]] = self._current_line
                obj['attributes'].append(attr)

                if isinstance(attr[1], (list, tuple)):
                    if encode_nominal:
                        conversor = EncodedNominalConversor(attr[1])
                    else:
                        conversor = NominalConversor(attr[1])
                else:
                    CONVERSOR_MAP = {'STRING': str,
                                     'INTEGER': lambda x: int(float(x)),
                                     'NUMERIC': float,
                                     'REAL': float}
                    conversor = CONVERSOR_MAP[attr[1]]

                self._conversors.append(conversor)

            # DATA ------------------------------------------------------------
            elif u_row.startswith(_TK_DATA):
                if STATE != _TK_ATTRIBUTE:

                break

            # COMMENT ---------------------------------------------------------
            elif u_row.startswith(_TK_COMMENT):
                pass
        else:
            # Never found @DATA
            raise BadLayout()

        def stream():
            for row in s:
                self._current_line += 1
                row = row.strip()
                # Ignore empty lines and comment lines.
                if row and not row.startswith(_TK_COMMENT):
                    yield row

        # Alter the data object
        obj['data'] = data.decode_rows(stream(), self._conversors)
        if obj['description'].endswith('\n'):
            obj['description'] = obj['description'][:-1]

        return obj

    def decode(self, s, encode_nominal=False, return_type=DENSE):
        '''Returns the Python representation of a given ARFF file.

        When a file object is passed as an argument, this method reads lines
        iteratively, avoiding to load unnecessary information to the memory.

        :param s: a string or file object with the ARFF file.
        :param encode_nominal: boolean, if True perform a label encoding
            while reading the .arff file.
        :param return_type: determines the data structure used to store the
            dataset. Can be one of `arff.DENSE`, `arff.COO`, `arff.LOD`,
            `arff.DENSE_GEN` or `arff.LOD_GEN`.
            Consult the sections on `working with sparse data`_ and `loading
            progressively`_.
        '''
        try:
            return self._decode(s, encode_nominal=encode_nominal,
                                matrix_type=return_type)
        except ArffException as e:
            e.line = self._current_line
            raise e

class ArffEncoder:
    '''An ARFF encoder.'''

    def _encode_comment(self, s=''):
        '''(INTERNAL) Encodes a comment line.

        If ``s`` is None, this method will simply return an empty comment.

        :param s: (OPTIONAL) string.
        :return: a string with the encoded comment line.
        '''
        if s:
            return '%s %s'%(_TK_COMMENT, s)
        else:
            return '%s' % _TK_COMMENT

    def _encode_relation(self, name):

        <relation-name>``, where ``relation-name`` is a string.

        :param name: a string.
        :return: a string with the encoded relation declaration.
        '''
        for char in ' %{},':
            if char in name:
                name = '"%s"'%name
                break

        return '%s %s'%(_TK_RELATION, name)

    def _encode_attribute(self, name, type_):
        '''(INTERNAL) Encodes an attribute line.

        The attribute follow the template::

        where ``attribute-name`` is a string, and ``datatype`` can be:

        This method must receive a the name of the attribute and its type, if
        the attribute type is nominal, ``type`` must be a list of values.

        :param type_: a string or a list of string.
        :return: a string with the encoded attribute declaration.
        '''
                break

        if isinstance(type_, (tuple, list)):
            type_tmp = ['%s' % encode_string(type_k) for type_k in type_]
            type_ = '{%s}'%(', '.join(type_tmp))

        return '%s %s %s'%(_TK_ATTRIBUTE, name, type_)

    def encode(self, obj):
        '''Encodes a given object to an ARFF file.

        :param obj: the object containing the ARFF information.
        :return: the ARFF file as an string.
        '''
        data = [row for row in self.iter_encode(obj)]

        return '\n'.join(data)

    def iter_encode(self, obj):
        '''The iterative version of `arff.ArffEncoder.encode`.

        This encodes iteratively a given object and return, one-by-one, the
        lines of the ARFF file.

        :return: (yields) the ARFF file as strings.
        '''
        # DESCRIPTION
        if obj.get('description', None):
            for row in obj['description'].split('\n'):
                yield self._encode_comment(row)

        # RELATION
        if not obj.get('relation'):
            raise BadObject('Relation name not found or with invalid value.')

        yield self._encode_relation(obj['relation'])
        yield ''

        # ATTRIBUTES
        if not obj.get('attributes'):
            raise BadObject('Attributes not found.')

        attribute_names = set()
        for attr in obj['attributes']:
            # Verify for bad object format
            if not isinstance(attr, (tuple, list)) or \
               len(attr) != 2 or \
               not isinstance(attr[0], str):
                raise BadObject('Invalid attribute declaration "%s"'%str(attr))

            if isinstance(attr[1], str):
                # Verify for invalid types
                if attr[1] not in _SIMPLE_TYPES:
                    raise BadObject('Invalid attribute type "%s"'%str(attr))

            elif not isinstance(attr[1], (tuple, list)):
                raise BadObject('Invalid attribute type "%s"'%str(attr))

            # Verify attribute name is not used twice
            if attr[0] in attribute_names:
                raise BadObject('Trying to use attribute name "%s" for the '
                                'second time.' % str(attr[0]))
            else:
                attribute_names.add(attr[0])

            yield self._encode_attribute(attr[0], attr[1])
        yield ''
        attributes = obj['attributes']

        # DATA
        yield _TK_DATA
        if 'data' in obj:
            data = _get_data_object_for_encoding(obj.get('data'))
            yield from data.encode_data(obj.get('data'), attributes)

        yield ''

def load(fp, encode_nominal=False, return_type=DENSE):
    '''Load a file-like object containing the ARFF document and convert it into
    a Python object.

    :param fp: a file-like object.
    :param encode_nominal: boolean, if True perform a label encoding
        while reading the .arff file.
    :param return_type: determines the data structure used to store the
        dataset. Can be one of `arff.DENSE`, `arff.COO`, `arff.LOD`,
        `arff.DENSE_GEN` or `arff.LOD_GEN`.
        Consult the sections on `working with sparse data`_ and `loading
        progressively`_.
    :return: a dictionary.
     '''
    decoder = ArffDecoder()
    return decoder.decode(fp, encode_nominal=encode_nominal,
                          return_type=return_type)

def loads(s, encode_nominal=False, return_type=DENSE):
    '''Convert a string instance containing the ARFF document into a Python
    object.

    :param s: a string object.
    '''
    return decoder.decode(s, encode_nominal=encode_nominal,

def dump(obj, fp):
    '''Serialize an object representing the ARFF document to a given file-like
    object.

    :param obj: a dictionary.
    '''
    encoder = ArffEncoder()
    generator = encoder.iter_encode(obj)

    last_row = next(generator)
    for row in generator:
        fp.write(last_row + '\n')
        last_row = row
    fp.write(last_row)

    return fp

def dumps(obj):
    '''Serialize an object representing the ARFF document, returning a string.

    :return: a string with the ARFF document.
    '''
    return encoder.encode(obj)

    class ArffContainerType(TypedDict):
        description: str
        relation: str
        attributes: List
        data: Union[ArffDenseDataType, ArffSparseDataType]

        )

        )

        )

            '%d.'
        )

        if msg:

        try:

            else:

        try:

        '''

                )

                    s = '?'
                else:

                continue
            try:
                raise

                )

                s = '?'
            else:

            try:
                raise

                )

                    s = '?'
                else:

        '''
        return res

        '''

        return res

        '''

        if not m:

            try:

        else:

            'data': []
        }

        s = iter(s)

                else:

                    else:
                else:

                break

                pass
        else:

                    yield row

        return obj

        '''
        try:
            raise e

        '''
        if s:
        else:

        '''
                break

        '''
                break

        '''

        '''

        # RELATION

        yield ''

            else:

        yield ''

        # DATA

        yield ''

                    yield row

class InfinityType:
    def __repr__(self) -> str:
        return "Infinity"

    def __hash__(self) -> int:
        return hash(repr(self))

    def __lt__(self, other: object) -> bool:

    def __le__(self, other: object) -> bool:

    def __eq__(self, other: object) -> bool:
        return isinstance(other, self.__class__)

    def __ne__(self, other: object) -> bool:
        return not isinstance(other, self.__class__)

    def __gt__(self, other: object) -> bool:

    def __ge__(self, other: object) -> bool:

    def __neg__(self: object) -> "NegativeInfinityType":
        return NegativeInfinity

class NegativeInfinityType:
        return "-Infinity"

    def __neg__(self: object) -> InfinityType:
        return Infinity

def parse(version: str) -> Union["LegacyVersion", "Version"]:
    """Parse the given version from a string to an appropriate class.

    version : str
        Version in a string format, eg. "0.9.1" or "1.2.dev0".

    Returns
    -------
    version : :class:`Version` object or a :class:`LegacyVersion` object
        Returned class depends on the given version: if is a valid
        PEP 440 version or a legacy version.
    """
    try:
        return Version(version)
    except InvalidVersion:
        return LegacyVersion(version)

class InvalidVersion(ValueError):
    """
    An invalid version was found, users should refer to PEP 440.
    """

class _BaseVersion:
    _key: Union[CmpKey, LegacyCmpKey]

        return hash(self._key)

    # Please keep the duplicated `isinstance` check
    # in the six comparisons hereunder
    # unless you find a way to avoid adding overhead function calls.
    def __lt__(self, other: "_BaseVersion") -> bool:
        if not isinstance(other, _BaseVersion):
            return NotImplemented

        return self._key < other._key

    def __le__(self, other: "_BaseVersion") -> bool:

        return self._key <= other._key

        return self._key == other._key

    def __ge__(self, other: "_BaseVersion") -> bool:

        return self._key >= other._key

    def __gt__(self, other: "_BaseVersion") -> bool:

        return self._key > other._key

        return self._key != other._key

class LegacyVersion(_BaseVersion):
    def __init__(self, version: str) -> None:
        self._version = str(version)
        self._key = _legacy_cmpkey(self._version)

            "Creating a LegacyVersion has been deprecated and will be "
            "removed in the next major release",
            DeprecationWarning,
        )

    def __str__(self) -> str:
        return self._version

        return f"<LegacyVersion('{self}')>"

    @property
    def public(self) -> str:

    @property
    def base_version(self) -> str:

    @property
    def epoch(self) -> int:
        return -1

    @property
    def release(self) -> None:

    @property
    def pre(self) -> None:

    @property
    def post(self) -> None:

    @property
    def dev(self) -> None:

    @property
    def local(self) -> None:

    @property
    def is_prerelease(self) -> bool:

    @property
    def is_postrelease(self) -> bool:

    @property
    def is_devrelease(self) -> bool:

def _parse_version_parts(s: str) -> Iterator[str]:
    for part in _legacy_version_component_re.split(s):
        part = _legacy_version_replacement_map.get(part, part)

        if not part or part == ".":
            continue

        if part[:1] in "0123456789":
            # pad for numeric comparison
            yield part.zfill(8)
        else:
            yield "*" + part

    # ensure that alpha/beta/candidate are before final
    yield "*final"

def _legacy_cmpkey(version: str) -> LegacyCmpKey:

    # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
    # greater than or equal to 0. This will effectively put the LegacyVersion,
    # which uses the defacto standard originally implemented by setuptools,
    # as before all PEP 440 versions.
    epoch = -1

    # This scheme is taken from pkg_resources.parse_version setuptools prior to
    # it's adoption of the packaging library.
    parts: List[str] = []
    for part in _parse_version_parts(version.lower()):
        if part.startswith("*"):
            # remove "-" before a prerelease tag
            if part < "*final":
                while parts and parts[-1] == "*final-":
                    parts.pop()

            # remove trailing zeros from each series of numeric parts
            while parts and parts[-1] == "00000000":
                parts.pop()

        parts.append(part)

    return epoch, tuple(parts)

class Version(_BaseVersion):

    _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)

        # Validate the version and parse it into pieces
        match = self._regex.search(version)
        if not match:
            raise InvalidVersion(f"Invalid version: '{version}'")

        # Store the parsed out pieces of the version
        self._version = _Version(
            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
            release=tuple(int(i) for i in match.group("release").split(".")),
            pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
            post=_parse_letter_version(
                match.group("post_l"), match.group("post_n1") or match.group("post_n2")
            ),
            dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
            local=_parse_local_version(match.group("local")),
        )

        # Generate a key which will be used for sorting
        self._key = _cmpkey(
            self._version.epoch,
            self._version.release,
            self._version.pre,
            self._version.post,
            self._version.dev,
            self._version.local,
        )

        return f"<Version('{self}')>"

        parts = []

        # Epoch
        if self.epoch != 0:
            parts.append(f"{self.epoch}!")

        # Release segment
        parts.append(".".join(str(x) for x in self.release))

        # Pre-release
        if self.pre is not None:
            parts.append("".join(str(x) for x in self.pre))

        # Post-release
        if self.post is not None:
            parts.append(f".post{self.post}")

        # Development release
        if self.dev is not None:
            parts.append(f".dev{self.dev}")

        # Local version segment
        if self.local is not None:
            parts.append(f"+{self.local}")

        return "".join(parts)

    @property
        _epoch: int = self._version.epoch
        return _epoch

    @property
    def release(self) -> Tuple[int, ...]:
        _release: Tuple[int, ...] = self._version.release
        return _release

    @property
    def pre(self) -> Optional[Tuple[str, int]]:
        _pre: Optional[Tuple[str, int]] = self._version.pre
        return _pre

    @property
    def post(self) -> Optional[int]:
        return self._version.post[1] if self._version.post else None

    @property
    def dev(self) -> Optional[int]:
        return self._version.dev[1] if self._version.dev else None

    @property
    def local(self) -> Optional[str]:
        if self._version.local:
            return ".".join(str(x) for x in self._version.local)
        else:

    @property
        return str(self).split("+", 1)[0]

    @property
        parts = []

        # Epoch

    @property
        return self.dev is not None or self.pre is not None

    @property
        return self.post is not None

    @property
        return self.dev is not None

    @property
    def major(self) -> int:
        return self.release[0] if len(self.release) >= 1 else 0

    @property
    def minor(self) -> int:
        return self.release[1] if len(self.release) >= 2 else 0

    @property
    def micro(self) -> int:
        return self.release[2] if len(self.release) >= 3 else 0

def _parse_letter_version(
    letter: str, number: Union[str, bytes, SupportsInt]
) -> Optional[Tuple[str, int]]:

    if letter:
        # We consider there to be an implicit 0 in a pre-release if there is
        # not a numeral associated with it.
        if number is None:
            number = 0

        # We normalize any letters to their lower case form
        letter = letter.lower()

        # We consider some words to be alternate spellings of other words and
        # in those cases we want to normalize the spellings to our preferred
        # spelling.
        if letter == "alpha":
            letter = "a"
        elif letter == "beta":
            letter = "b"
        elif letter in ["c", "pre", "preview"]:
            letter = "rc"
        elif letter in ["rev", "r"]:
            letter = "post"

        return letter, int(number)
    if not letter and number:
        # We assume if we are given a number, but we are not given a letter
        # then this is using the implicit post release syntax (e.g. 1.0-1)
        letter = "post"

    return None

def _parse_local_version(local: str) -> Optional[LocalType]:
    """
    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
    """
    if local is not None:
        return tuple(
            part.lower() if not part.isdigit() else int(part)
            for part in _local_version_separators.split(local)
        )

def _cmpkey(
    epoch: int,
    release: Tuple[int, ...],
    pre: Optional[Tuple[str, int]],
    post: Optional[Tuple[str, int]],
    dev: Optional[Tuple[str, int]],
    local: Optional[Tuple[SubLocalType]],
) -> CmpKey:

    # When we compare a release version, we want to compare it with all of the
    # trailing zeros removed. So we'll use a reverse the list, drop all the now
    # leading zeros until we come to something non zero, then take the rest
    # re-reverse it back into the correct order and make it a tuple and use
    # that for our sorting key.
    _release = tuple(
        reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
    )

    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
    # We'll do this by abusing the pre segment, but we _only_ want to do this
    # if there is not a pre or a post segment. If we have one of those then
    # the normal sorting rules will handle this case correctly.
    if pre is None and post is None and dev is not None:
        _pre: PrePostDevType = NegativeInfinity
    # Versions without a pre-release (except as noted above) should sort after
    # those with one.
    elif pre is None:
        _pre = Infinity
    else:
        _pre = pre

    # Versions without a post segment should sort before those with one.
    if post is None:
        _post: PrePostDevType = NegativeInfinity

    else:
        _post = post

    # Versions without a development segment should sort after those with one.
    if dev is None:
        _dev: PrePostDevType = Infinity

    else:
        _dev = dev

    if local is None:
        # Versions without a local segment should sort before those with one.
        _local: LocalType = NegativeInfinity
    else:
        # Versions with a local segment need that segment parsed to implement
        # the sorting rules in PEP440.
        # - Alpha numeric segments sort before numeric segments
        # - Alpha numeric segments sort lexicographically
        # - Numeric segments sort numerically
        # - Shorter versions sort before longer versions when the prefixes
        #   match exactly
        _local = tuple(
            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
        )

    return epoch, _release, _pre, _post, _dev, _local

        )

            ),
        )

        )

        parts = []

        # Epoch

        else:

        parts = []

        # Epoch

def get_xp(xp):
    """
    Decorator to automatically replace xp with the corresponding array module.

    Use like

    import numpy as np

    @get_xp(np)
    def func(x, /, xp, kwarg=None):
        return xp.func(x, kwarg=kwarg)

    Note that xp must be a keyword argument and come after all non-keyword
    arguments.

    """

    def inner(f):
        @wraps(f)
        def wrapped_f(*args, **kwargs):
            return f(*args, xp=xp, **kwargs)

        sig = signature(f)
        new_sig = sig.replace(
            parameters=[sig.parameters[i] for i in sig.parameters if i != "xp"]
        )

        if wrapped_f.__doc__ is None:
            wrapped_f.__doc__ = f"""\
Array API compatibility wrapper for {f.__name__}.

See the corresponding documentation in NumPy/CuPy and/or the array API
specification for more details.

"""
        wrapped_f.__signature__ = new_sig
        return wrapped_f

    return inner

        @wraps(f)

        )

"""

class __array_namespace_info__:
    """
    Get the array API inspection namespace for NumPy.

    The array API inspection namespace defines the following functions:

    - capabilities()
    - default_device()
    - default_dtypes()
    - dtypes()
    - devices()

    See
    https://data-apis.org/array-api/latest/API_specification/inspection.html

    Returns
    -------
    info : ModuleType
        The array API inspection namespace for NumPy.

    Examples
    --------
    >>> info = np.__array_namespace_info__()
    >>> info.default_dtypes()
    {'real floating': numpy.float64,
     'complex floating': numpy.complex128,
     'integral': numpy.int64,
     'indexing': numpy.int64}

    """

    __module__ = 'numpy'

    def capabilities(self):
        """
        Return a dictionary of array API library capabilities.

        The resulting dictionary has the following keys:

        - **"boolean indexing"**: boolean indicating whether an array library
          supports boolean indexing. Always ``True`` for NumPy.

        - **"data-dependent shapes"**: boolean indicating whether an array
          library supports data-dependent output shapes. Always ``True`` for
          NumPy.

        See
        https://data-apis.org/array-api/latest/API_specification/generated/array_api.info.capabilities.html

        See Also
        --------
        __array_namespace_info__.default_device,
        __array_namespace_info__.default_dtypes,
        __array_namespace_info__.dtypes,
        __array_namespace_info__.devices

        Returns
        -------
        capabilities : dict
            A dictionary of array API library capabilities.

        Examples
        --------
        >>> info = np.__array_namespace_info__()
        >>> info.capabilities()
        {'boolean indexing': True,
         'data-dependent shapes': True}

        """
        return {
            "boolean indexing": True,
            "data-dependent shapes": True,
            # 'max rank' will be part of the 2024.12 standard
            "max dimensions": 64,
        }

    def default_device(self):
        """
        The default device used for new NumPy arrays.

        For NumPy, this always returns ``'cpu'``.

        See Also
        --------
        __array_namespace_info__.capabilities,

        Returns
        -------
        device : str
            The default device used for new NumPy arrays.

        Examples
        --------
        >>> info.default_device()
        'cpu'

        """
        return "cpu"

    def default_dtypes(self, *, device=None):
        """
        The default data types used for new NumPy arrays.

        For NumPy, this always returns the following dictionary:

        - **"real floating"**: ``numpy.float64``
        - **"complex floating"**: ``numpy.complex128``
        - **"integral"**: ``numpy.intp``
        - **"indexing"**: ``numpy.intp``

        device : str, optional
            The device to get the default data types for. For NumPy, only
            ``'cpu'`` is allowed.

        Returns
        -------
        dtypes : dict
            A dictionary describing the default data types used for new NumPy
            arrays.

        See Also
        --------

        Examples
        --------
        >>> info.default_dtypes()
        {'real floating': numpy.float64,
         'complex floating': numpy.complex128,
         'integral': numpy.int64,
         'indexing': numpy.int64}

        """
        if device not in ["cpu", None]:
                'Device not understood. Only "cpu" is allowed, but received:'
                f' {device}'
            )
        return {
            "real floating": dtype(float64),
            "complex floating": dtype(complex128),
            "integral": dtype(intp),
            "indexing": dtype(intp),
        }

    def dtypes(self, *, device=None, kind=None):
        """
        The array API data types supported by NumPy.

        Note that this function only returns data types that are defined by
        the array API.

            The device to get the data types for. For NumPy, only ``'cpu'`` is
            allowed.
        kind : str or tuple of str, optional
            The kind of data types to return. If ``None``, all data types are
            returned. If a string, only data types of that kind are returned.
            If a tuple, a dictionary containing the union of the given kinds
            is returned. The following kinds are supported:

            - ``'bool'``: boolean data types (i.e., ``bool``).
            - ``'signed integer'``: signed integer data types (i.e., ``int8``,
              ``int16``, ``int32``, ``int64``).
            - ``'unsigned integer'``: unsigned integer data types (i.e.,
              ``uint8``, ``uint16``, ``uint32``, ``uint64``).
            - ``'integral'``: integer data types. Shorthand for ``('signed
              integer', 'unsigned integer')``.
            - ``'real floating'``: real-valued floating-point data types
              (i.e., ``float32``, ``float64``).
            - ``'complex floating'``: complex floating-point data types (i.e.,
              ``complex64``, ``complex128``).
            - ``'numeric'``: numeric data types. Shorthand for ``('integral',
              'real floating', 'complex floating')``.

        Returns
        -------
            A dictionary mapping the names of data types to the corresponding
            NumPy data types.

        See Also
        --------

        Examples
        --------
        >>> info.dtypes(kind='signed integer')
        {'int8': numpy.int8,
         'int16': numpy.int16,
         'int32': numpy.int32,
         'int64': numpy.int64}

        """
            )
        if kind is None:
            return {
                "bool": dtype(bool),
                "int8": dtype(int8),
                "int16": dtype(int16),
                "int32": dtype(int32),
                "int64": dtype(int64),
                "uint8": dtype(uint8),
                "uint16": dtype(uint16),
                "uint32": dtype(uint32),
                "uint64": dtype(uint64),
                "float32": dtype(float32),
                "float64": dtype(float64),
                "complex64": dtype(complex64),
                "complex128": dtype(complex128),
            }
        if kind == "bool":
            return {"bool": bool}
        if kind == "signed integer":
            return {
            }
        if kind == "unsigned integer":
            return {
            }
        if kind == "integral":
            return {
            }
        if kind == "real floating":
            return {
            }
        if kind == "complex floating":
            return {
            }
        if kind == "numeric":
            return {
            }
        if isinstance(kind, tuple):
            res = {}
            for k in kind:
                res.update(self.dtypes(kind=k))
            return res
        raise ValueError(f"unsupported kind: {kind!r}")

    def devices(self):
        """
        The devices supported by NumPy.

        For NumPy, this always returns ``['cpu']``.

        Returns
        -------
        devices : list of str
            The devices supported by NumPy.

        See Also
        --------
        __array_namespace_info__.dtypes

        Examples
        --------
        >>> info.devices()
        ['cpu']

        """
        return ["cpu"]

        """

          NumPy.

        See

        See Also
        --------

        Returns
        -------

        Examples
        --------

        """
        return {
        }

        """

        See Also
        --------

        Returns
        -------

        Examples
        --------
        'cpu'

        """

        """

        Returns
        -------
            arrays.

        See Also
        --------

        Examples
        --------

        """
            )
        return {
        }

        """

            allowed.

        Returns
        -------

        See Also
        --------

        Examples
        --------

        """
            )
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            res = {}
            return res

        """

        Returns
        -------

        See Also
        --------

        Examples
        --------
        ['cpu']

        """

def solve(x1: _np.ndarray, x2: _np.ndarray, /) -> _np.ndarray:
    try:
        from numpy.linalg._linalg import (
        _makearray, _assert_stacked_2d, _assert_stacked_square,
        _commonType, isComplexType, _raise_linalgerror_singular
        )
        from numpy.linalg.linalg import (
        )
    from numpy.linalg import _umath_linalg

    x1, _ = _makearray(x1)
    _assert_stacked_2d(x1)
    _assert_stacked_square(x1)
    x2, wrap = _makearray(x2)
    t, result_t = _commonType(x1, x2)

    # This part is different from np.linalg.solve
    if x2.ndim == 1:
        gufunc = _umath_linalg.solve1
    else:
        gufunc = _umath_linalg.solve

    # This does nothing currently but is left in because it will be relevant
    # when complex dtype support is added to the spec in 2022.
    signature = 'DD->D' if isComplexType(t) else 'dd->d'
    with _np.errstate(call=_raise_linalgerror_singular, invalid='call',
                      over='ignore', divide='ignore', under='ignore'):
        r = gufunc(x1, x2, signature=signature)

    return wrap(r.astype(result_t, copy=False))

def _supports_buffer_protocol(obj):
    try:
        memoryview(obj)
    except TypeError:

def asarray(
    obj: Union[
        ndarray,
        bool,
        int,
        float,
        NestedSequence[bool | int | float],
        SupportsBufferProtocol,
    ],
    /,
    *,
    dtype: Optional[Dtype] = None,
    device: Optional[Device] = None,
    copy: "Optional[Union[bool, np._CopyMode]]" = None,
    **kwargs,
) -> ndarray:
    """
    Array API compatibility wrapper for asarray().

    See the corresponding documentation in the array library and/or the array API
    specification for more details.
    """
    if device not in ["cpu", None]:
        raise ValueError(f"Unsupported device for NumPy: {device!r}")

    if hasattr(np, '_CopyMode'):
        if copy is None:
            copy = np._CopyMode.IF_NEEDED
        elif copy is False:
            copy = np._CopyMode.NEVER
        elif copy is True:
            copy = np._CopyMode.ALWAYS
    else:
        # Not present in older NumPys. In this case, we cannot really support
        # copy=False.
        if copy is False:
            raise NotImplementedError("asarray(copy=False) requires a newer version of NumPy.")

    return np.array(obj, copy=copy, dtype=dtype, **kwargs)

def astype(
    x: ndarray,
    dtype: Dtype,
    /,
    *,
    copy: bool = True,
    return x.astype(dtype=dtype, copy=copy)

def count_nonzero(
    x : ndarray,
    axis=None,
    keepdims=False
    result = np.count_nonzero(x, axis=axis, keepdims=keepdims)
    if axis is None and not keepdims:
        return np.asarray(result)

    """
    Get the array API inspection namespace for CuPy.

    - dtypes()

    See

    Returns
    -------
        The array API inspection namespace for CuPy.

    Examples
    --------
    {'real floating': cupy.float64,
     'complex floating': cupy.complex128,
     'integral': cupy.int64,
     'indexing': cupy.int64}

    """

    __module__ = 'cupy'

        """

          supports boolean indexing. Always ``True`` for CuPy.

          CuPy.

        See

        See Also
        --------

        Returns
        -------

        Examples
        --------
        >>> info = xp.__array_namespace_info__()

        """
        return {
        }

        """
        The default device used for new CuPy arrays.

        See Also
        --------

        Returns
        -------
            The default device used for new CuPy arrays.

        Examples
        --------
        Device(0)

        """
        return cuda.Device(0)

        """
        The default data types used for new CuPy arrays.

        For CuPy, this always returns the following dictionary:

        - **"real floating"**: ``cupy.float64``
        - **"complex floating"**: ``cupy.complex128``
        - **"integral"**: ``cupy.intp``
        - **"indexing"**: ``cupy.intp``

            The device to get the default data types for.

        Returns
        -------
            A dictionary describing the default data types used for new CuPy
            arrays.

        See Also
        --------

        Examples
        --------
        {'real floating': cupy.float64,
         'complex floating': cupy.complex128,
         'integral': cupy.int64,
         'indexing': cupy.int64}

        """
        # TODO: Does this depend on device?
        return {
        }

        """
        The array API data types supported by CuPy.

            The device to get the data types for.

        Returns
        -------
            CuPy data types.

        See Also
        --------

        Examples
        --------
        {'int8': cupy.int8,
         'int16': cupy.int16,
         'int32': cupy.int32,
         'int64': cupy.int64}

        """
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            res = {}
            return res

        """
        The devices supported by CuPy.

        Returns
        -------
            The devices supported by CuPy.

        See Also
        --------

        """
        return [cuda.Device(i) for i in range(cuda.runtime.getDeviceCount())]

        """

          CuPy.

        See

        See Also
        --------

        Returns
        -------

        Examples
        --------

        """
        return {
        }

        """

        See Also
        --------

        Returns
        -------

        Examples
        --------
        Device(0)

        """

        """

        Returns
        -------
            arrays.

        See Also
        --------

        Examples
        --------

        """
        return {
        }

        """

        Returns
        -------

        See Also
        --------

        Examples
        --------

        """
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            res = {}
            return res

        """

        Returns
        -------

        See Also
        --------

        """

        ndarray,
        bool,
        int,
        float,
    ],
    /,
    *,
    copy: Optional[bool] = _copy_default,
    **kwargs,
    """

    """
    with cp.cuda.Device(device):
        # cupy is like NumPy 1.26 (except without _CopyMode). See the comments
        # in asarray in numpy/_aliases.py.
        if copy is not _copy_default:
            # A future version of CuPy will change the meaning of copy=False
            # to mean no-copy. We don't know for certain what version it will
            # be yet, so to avoid breaking that version, we use a different
            # default value for copy so asarray(obj) with no copy kwarg will
            # always do the copy-if-needed behavior.

            # This will still need to be updated to remove the
            # NotImplementedError for copy=False, but at least this won't
            # break the default or existing behavior.
            if copy is None:
                copy = False
            elif copy is False:
                raise NotImplementedError("asarray(copy=False) is not yet supported in cupy")
            kwargs['copy'] = copy

        return cp.array(obj, dtype=dtype, **kwargs)

    /,
    *,
    if device is None:
        return x.astype(dtype=dtype, copy=copy)
    out = _helpers.to_device(x.astype(dtype=dtype, copy=False), device)
    return out.copy() if copy and out is x else out

   result = cp.count_nonzero(x, axis)
   if keepdims:
       if axis is None:
            return cp.reshape(result, [1]*x.ndim)
       return cp.expand_dims(result, axis)
   return result

def fft(
    /,
    xp,
    *,
    n: Optional[int] = None,
    axis: int = -1,
    norm: Literal["backward", "ortho", "forward"] = "backward",
    res = xp.fft.fft(x, n=n, axis=axis, norm=norm)
    if x.dtype in [xp.float32, xp.complex64]:
        return res.astype(xp.complex64)
    return res

def ifft(
    /,
    xp,
    *,
    res = xp.fft.ifft(x, n=n, axis=axis, norm=norm)
    return res

def fftn(
    /,
    xp,
    *,
    s: Sequence[int] = None,
    axes: Sequence[int] = None,
    res = xp.fft.fftn(x, s=s, axes=axes, norm=norm)
    return res

def ifftn(
    /,
    xp,
    *,
    res = xp.fft.ifftn(x, s=s, axes=axes, norm=norm)
    return res

def rfft(
    /,
    xp,
    *,
    res = xp.fft.rfft(x, n=n, axis=axis, norm=norm)
    if x.dtype == xp.float32:
    return res

def irfft(
    /,
    xp,
    *,
    res = xp.fft.irfft(x, n=n, axis=axis, norm=norm)
    if x.dtype == xp.complex64:
        return res.astype(xp.float32)
    return res

def rfftn(
    /,
    xp,
    *,
    res = xp.fft.rfftn(x, s=s, axes=axes, norm=norm)
    return res

def irfftn(
    /,
    xp,
    *,
    res = xp.fft.irfftn(x, s=s, axes=axes, norm=norm)
    return res

def hfft(
    /,
    xp,
    *,
    res = xp.fft.hfft(x, n=n, axis=axis, norm=norm)
    return res

def ihfft(
    /,
    xp,
    *,
    res = xp.fft.ihfft(x, n=n, axis=axis, norm=norm)
    return res

def fftfreq(
    n: int,
    /,
    xp,
    *,
    d: float = 1.0,
    dtype: Optional[DType] = None,
    device: Optional[Device] = None
        raise ValueError(f"Unsupported device {device!r}")
    res = xp.fft.fftfreq(n, d=d)
    if dtype is not None:
        return res.astype(dtype)
    return res

def rfftfreq(
    n: int,
    /,
    xp,
    *,
    res = xp.fft.rfftfreq(n, d=d)
    return res

def fftshift(x: ndarray, /, xp, *, axes: Union[int, Sequence[int]] = None) -> ndarray:
    return xp.fft.fftshift(x, axes=axes)

def ifftshift(x: ndarray, /, xp, *, axes: Union[int, Sequence[int]] = None) -> ndarray:
    return xp.fft.ifftshift(x, axes=axes)

def _is_jax_zero_gradient_array(x: object) -> bool:
    """Return True if `x` is a zero-gradient array.

    These arrays are a design quirk of Jax that may one day be removed.
    See https://github.com/google/jax/issues/20620.
    """
    if 'numpy' not in sys.modules or 'jax' not in sys.modules:

    import jax

    return isinstance(x, np.ndarray) and x.dtype == jax.float0

def is_numpy_array(x: object) -> bool:
    """
    Return True if `x` is a NumPy array.

    This function does not import NumPy if it has not already been imported
    and is therefore cheap to use.

    This also returns True for `ndarray` subclasses and NumPy scalar objects.

    See Also
    --------

    array_namespace
    is_array_api_obj
    is_cupy_array
    is_torch_array
    is_ndonnx_array
    is_dask_array
    is_jax_array
    is_pydata_sparse_array
    """
    # Avoid importing NumPy if it isn't already
    if 'numpy' not in sys.modules:

    # TODO: Should we reject ndarray subclasses?
    return (isinstance(x, (np.ndarray, np.generic))
            and not _is_jax_zero_gradient_array(x))

def is_cupy_array(x: object) -> bool:
    """
    Return True if `x` is a CuPy array.

    This function does not import CuPy if it has not already been imported

    This also returns True for `cupy.ndarray` subclasses and CuPy scalar objects.

    See Also
    --------

    is_numpy_array
    """
    # Avoid importing CuPy if it isn't already
    if 'cupy' not in sys.modules:

    import cupy as cp

    return isinstance(x, cp.ndarray)

def is_torch_array(x: object) -> bool:
    """
    Return True if `x` is a PyTorch tensor.

    This function does not import PyTorch if it has not already been imported

    See Also
    --------

    """
    # Avoid importing torch if it isn't already
    if 'torch' not in sys.modules:

    import torch

    return isinstance(x, torch.Tensor)

def is_ndonnx_array(x: object) -> bool:
    """
    Return True if `x` is a ndonnx Array.

    This function does not import ndonnx if it has not already been imported

    See Also
    --------

    """
    if 'ndonnx' not in sys.modules:

    import ndonnx as ndx

    return isinstance(x, ndx.Array)

def is_dask_array(x: object) -> bool:
    """
    Return True if `x` is a dask.array Array.

    This function does not import dask if it has not already been imported

    See Also
    --------

    """
    # Avoid importing dask if it isn't already
    if 'dask.array' not in sys.modules:

    import dask.array

    return isinstance(x, dask.array.Array)

def is_jax_array(x: object) -> bool:
    """
    Return True if `x` is a JAX array.

    This function does not import JAX if it has not already been imported

    See Also
    --------

    """
    # Avoid importing jax if it isn't already
    if 'jax' not in sys.modules:

    import jax

    return isinstance(x, jax.Array) or _is_jax_zero_gradient_array(x)

def is_pydata_sparse_array(x) -> bool:
    """
    Return True if `x` is an array from the `sparse` package.

    This function does not import `sparse` if it has not already been imported

    See Also
    --------

    """
    if 'sparse' not in sys.modules:

    import sparse

    # TODO: Account for other backends.
    return isinstance(x, sparse.SparseArray)

def is_array_api_obj(x: object) -> bool:
    """
    Return True if `x` is an array API compatible array object.

    See Also
    --------

    """
    return is_numpy_array(x) \
        or is_cupy_array(x) \
        or is_torch_array(x) \
        or is_dask_array(x) \
        or is_jax_array(x) \
        or is_pydata_sparse_array(x) \
        or hasattr(x, '__array_namespace__')

def _compat_module_name() -> str:
    assert __name__.endswith('.common._helpers')
    return __name__.removesuffix('.common._helpers')

def is_numpy_namespace(xp) -> bool:
    """
    Returns True if `xp` is a NumPy namespace.

    This includes both NumPy itself and the version wrapped by array-api-compat.

    See Also
    --------

    is_cupy_namespace
    is_torch_namespace
    is_ndonnx_namespace
    is_dask_namespace
    is_jax_namespace
    is_pydata_sparse_namespace
    is_array_api_strict_namespace
    """
    return xp.__name__ in {'numpy', _compat_module_name() + '.numpy'}

def is_cupy_namespace(xp) -> bool:
    """
    Returns True if `xp` is a CuPy namespace.

    This includes both CuPy itself and the version wrapped by array-api-compat.

    See Also
    --------

    is_numpy_namespace
    """
    return xp.__name__ in {'cupy', _compat_module_name() + '.cupy'}

def is_torch_namespace(xp) -> bool:
    """
    Returns True if `xp` is a PyTorch namespace.

    This includes both PyTorch itself and the version wrapped by array-api-compat.

    See Also
    --------

    """
    return xp.__name__ in {'torch', _compat_module_name() + '.torch'}

def is_ndonnx_namespace(xp) -> bool:
    """
    Returns True if `xp` is an NDONNX namespace.

    See Also
    --------

    """
    return xp.__name__ == 'ndonnx'

def is_dask_namespace(xp) -> bool:
    """
    Returns True if `xp` is a Dask namespace.

    This includes both ``dask.array`` itself and the version wrapped by array-api-compat.

    See Also
    --------

    """
    return xp.__name__ in {'dask.array', _compat_module_name() + '.dask.array'}

def is_jax_namespace(xp) -> bool:
    """
    Returns True if `xp` is a JAX namespace.

    This includes ``jax.numpy`` and ``jax.experimental.array_api`` which existed in
    older versions of JAX.

    See Also
    --------

    """
    return xp.__name__ in {'jax.numpy', 'jax.experimental.array_api'}

def is_pydata_sparse_namespace(xp) -> bool:
    """
    Returns True if `xp` is a pydata/sparse namespace.

    See Also
    --------

    """
    return xp.__name__ == 'sparse'

def is_array_api_strict_namespace(xp) -> bool:
    """
    Returns True if `xp` is an array-api-strict namespace.

    See Also
    --------

    """
    return xp.__name__ == 'array_api_strict'

def _check_api_version(api_version: str) -> None:
    if api_version in ['2021.12', '2022.12', '2023.12']:
        warnings.warn(f"The {api_version} version of the array API specification was requested but the returned namespace is actually version 2024.12")
    elif api_version is not None and api_version not in ['2021.12', '2022.12',
                                                         '2023.12', '2024.12']:
        raise ValueError("Only the 2024.12 version of the array API specification is currently supported")

def array_namespace(*xs, api_version=None, use_compat=None) -> Namespace:
    """
    Get the array API compatible namespace for the arrays `xs`.

    xs: arrays
        one or more arrays. xs can also be Python scalars (bool, int, float,
        complex, or None), which are ignored.

    api_version: str
        The newest version of the spec that you need support for (currently
        the compat library wrapped APIs support v2024.12).

    use_compat: bool or None
        If None (the default), the native namespace will be returned if it is
        already array API compatible, otherwise a compat wrapper is used. If
        True, the compat library wrapped library will be returned. If False,
        the native library namespace is returned.

    Returns
    -------

    out: namespace
        The array API compatible namespace corresponding to the arrays in `xs`.

    Raises
    ------
    TypeError
        If `xs` contains arrays from different array libraries or contains a
        non-array.

    Typical usage is to pass the arguments of a function to
    `array_namespace()` at the top of a function to get the corresponding
    array API namespace:

    .. code:: python

       def your_function(x, y):
           xp = array_api_compat.array_namespace(x, y)
           # Now use xp as the array library namespace
           return xp.mean(x, axis=0) + 2*xp.std(y, axis=0)

    Wrapped array namespaces can also be imported directly. For example,
    `array_namespace(np.array(...))` will return `array_api_compat.numpy`.
    This function will also work for any array library not wrapped by
    array-api-compat if it explicitly defines `__array_namespace__
    <https://data-apis.org/array-api/latest/API_specification/generated/array_api.array.__array_namespace__.html>`__
    (the wrapped namespace is always preferred if it exists).

    See Also
    --------

    """
    if use_compat not in [None, True, False]:
        raise ValueError("use_compat must be None, True, or False")

    _use_compat = use_compat in [None, True]

    namespaces = set()
    for x in xs:
        if is_numpy_array(x):
            from .. import numpy as numpy_namespace
            import numpy as np
            if use_compat is True:
                _check_api_version(api_version)
                namespaces.add(numpy_namespace)
            elif use_compat is False:
                namespaces.add(np)
            else:
                # numpy 2.0+ have __array_namespace__, however, they are not yet fully array API
                # compatible.
        elif is_cupy_array(x):
            if _use_compat:
                from .. import cupy as cupy_namespace
                namespaces.add(cupy_namespace)
            else:
                import cupy as cp
                namespaces.add(cp)
        elif is_torch_array(x):
                from .. import torch as torch_namespace
                namespaces.add(torch_namespace)
            else:
                import torch
                namespaces.add(torch)
        elif is_dask_array(x):
                from ..dask import array as dask_namespace
                namespaces.add(dask_namespace)
            else:
                import dask.array as da
                namespaces.add(da)
        elif is_jax_array(x):
                raise ValueError("JAX does not have an array-api-compat wrapper")
                import jax.numpy as jnp
            else:
                # JAX v0.4.32 and newer implements the array API directly in jax.numpy.
                # For older JAX versions, it is available via jax.experimental.array_api.
                import jax.numpy
                if hasattr(jax.numpy, "__array_api_version__"):
                    jnp = jax.numpy
                else:
                    import jax.experimental.array_api as jnp
            namespaces.add(jnp)
        elif is_pydata_sparse_array(x):
                raise ValueError("`sparse` does not have an array-api-compat wrapper")
            else:
                import sparse
            # `sparse` is already an array namespace. We do not have a wrapper
            # submodule for it.
            namespaces.add(sparse)
        elif hasattr(x, '__array_namespace__'):
                raise ValueError("The given array does not have an array-api-compat wrapper")
            namespaces.add(x.__array_namespace__(api_version=api_version))
        elif isinstance(x, (bool, int, float, complex, type(None))):
            continue
        else:
            # TODO: Support Python scalars?
            raise TypeError(f"{type(x).__name__} is not a supported array type")

    if not namespaces:
        raise TypeError("Unrecognized array input")

    if len(namespaces) != 1:
        raise TypeError(f"Multiple namespaces for array inputs: {namespaces}")

    xp, = namespaces

    return xp

def _check_device(xp, device):
    if xp == sys.modules.get('numpy'):
            raise ValueError(f"Unsupported device for NumPy: {device!r}")

class _dask_device:
        return "DASK_DEVICE"

def device(x: Array, /) -> Device:
    """
    Hardware device the array data resides on.

    This is equivalent to `x.device` according to the `standard
    <https://data-apis.org/array-api/latest/API_specification/generated/array_api.array.device.html>`__.
    This helper is included because some array libraries either do not have
    the `device` attribute or include it with an incompatible API.

    x: array
        array instance from an array API compatible library.

    Returns
    -------
    out: device
        a ``device`` object (see the `Device Support <https://data-apis.org/array-api/latest/design_topics/device_support.html>`__
        section of the array API specification).

    Notes
    -----

    For NumPy the device is always `"cpu"`. For Dask, the device is always a
    special `DASK_DEVICE` object.

    See Also
    --------

    to_device : Move array data to a different device.

    """
    if is_numpy_array(x):
    elif is_dask_array(x):
        # Peek at the metadata of the Dask array to determine type
        if is_numpy_array(x._meta):
            # Must be on CPU since backed by numpy
            return "cpu"
        return _DASK_DEVICE
    elif is_jax_array(x):
        # FIXME Jitted JAX arrays do not have a device attribute
        #       https://github.com/jax-ml/jax/issues/26000
        #       Return None in this case. Note that this workaround breaks
        #       the standard and will result in new arrays being created on the
        #       default device instead of the same device as the input array(s).
        x_device = getattr(x, 'device', None)
        # Older JAX releases had .device() as a method, which has been replaced
        # with a property in accordance with the standard.
        if inspect.ismethod(x_device):
            return x_device()
        else:
            return x_device
    elif is_pydata_sparse_array(x):
        # `sparse` will gain `.device`, so check for this first.
        if x_device is not None:
        # Everything but DOK has this attr.
        try:
            inner = x.data
        # Return the device of the constituent array
        return device(inner)
    return x.device

def _cupy_to_device(x, device, /, stream=None):
    from cupy.cuda import Device as _Device
    from cupy.cuda import stream as stream_module
    from cupy_backends.cuda.api import runtime

    if device == x.device:
        return x
    elif device == "cpu":
        # allowing us to use `to_device(x, "cpu")`
        # is useful for portable test swapping between
        # host and device backends
        return x.get()
    elif not isinstance(device, _Device):
    else:
        # see cupy/cupy#5985 for the reason how we handle device/stream here
        prev_device = runtime.getDevice()
        prev_stream: stream_module.Stream = None
        if stream is not None:
            prev_stream = stream_module.get_current_stream()
            # stream can be an int as specified in __dlpack__, or a CuPy stream
            if isinstance(stream, int):
                stream = cp.cuda.ExternalStream(stream)
            elif isinstance(stream, cp.cuda.Stream):
                pass
            else:
                raise ValueError('the input stream is not recognized')
            stream.use()
        try:
            runtime.setDevice(device.id)
            arr = x.copy()
        finally:
            runtime.setDevice(prev_device)
            if stream is not None:
                prev_stream.use()
        return arr

def _torch_to_device(x, device, /, stream=None):
    if stream is not None:
        raise NotImplementedError
    return x.to(device)

def to_device(x: Array, device: Device, /, *, stream: Optional[Union[int, Any]] = None) -> Array:
    """
    Copy the array from the device on which it currently resides to the specified ``device``.

    This is equivalent to `x.to_device(device, stream=stream)` according to
    the `standard
    <https://data-apis.org/array-api/latest/API_specification/generated/array_api.array.to_device.html>`__.
    This helper is included because some array libraries do not have the
    `to_device` method.

    x: array

    device: device

    stream: Optional[Union[int, Any]]
        stream object to use during copy. In addition to the types supported
        in ``array.__dlpack__``, implementations may choose to support any
        library-specific stream object with the caveat that any code using
        such an object would not be portable.

    Returns
    -------

    out: array
        an array with the same data and data type as ``x`` and located on the
        specified ``device``.

    Notes
    -----

    For NumPy, this function effectively does nothing since the only supported
    device is the CPU. For CuPy, this method supports CuPy CUDA
    :external+cupy:class:`Device <cupy.cuda.Device>` and
    :external+cupy:class:`Stream <cupy.cuda.Stream>` objects. For PyTorch,
    this is the same as :external+torch:meth:`x.to(device) <torch.Tensor.to>`
    (the ``stream`` argument is not supported in PyTorch).

    See Also
    --------

    device : Hardware device the array data resides on.

    """
            raise ValueError("The stream argument to to_device() is not supported")
        if device == 'cpu':
            return x
    elif is_cupy_array(x):
        # cupy does not yet have to_device
        return _cupy_to_device(x, device, stream=stream)
    elif is_torch_array(x):
        return _torch_to_device(x, device, stream=stream)
        # TODO: What if our array is on the GPU already?
            return x
        if not hasattr(x, "__array_namespace__"):
            # In JAX v0.4.31 and older, this import adds to_device method to x...
            import jax.experimental.array_api # noqa: F401
            # ... but only on eager JAX. It won't work inside jax.jit.
            if not hasattr(x, "to_device"):
                return x
        return x.to_device(device, stream=stream)
    elif is_pydata_sparse_array(x) and device == _device(x):
        # Perform trivial check to return the same array if
        # device is same instead of err-ing.
        return x
    return x.to_device(device, stream=stream)

def size(x: Array) -> int | None:
    """
    Return the total number of elements of x.

    This is equivalent to `x.size` according to the `standard
    <https://data-apis.org/array-api/latest/API_specification/generated/array_api.array.size.html>`__.

    This helper is included because PyTorch defines `size` in an
    :external+torch:meth:`incompatible way <torch.Tensor.size>`.
    It also fixes dask.array's behaviour which returns nan for unknown sizes, whereas
    the standard requires None.
    """
    # Lazy API compliant arrays, such as ndonnx, can contain None in their shape
    if None in x.shape:
    out = math.prod(x.shape)
    # dask.array.Array.shape can contain NaN
    return None if math.isnan(out) else out

def is_writeable_array(x: object) -> bool:
    """
    Return False if ``x.__setitem__`` is expected to raise; True otherwise.
    Return False if `x` is not an array API compatible object.

    Warning
    -------
    As there is no standard way to check if an array is writeable without actually
    writing to it, this function blindly returns True for all unknown array types.
    """
        return x.flags.writeable
    if is_jax_array(x) or is_pydata_sparse_array(x):
    return is_array_api_obj(x)

def is_lazy_array(x: object) -> bool:
    """Return True if x is potentially a future or it may be otherwise impossible or
    expensive to eagerly read its contents, regardless of their size, e.g. by
    calling ``bool(x)`` or ``float(x)``.

    Return False otherwise; e.g. ``bool(x)`` etc. is guaranteed to succeed and to be
    cheap as long as the array has the right dtype and size.

    Note
    ----
    This function errs on the side of caution for array types that may or may not be
    lazy, e.g. JAX arrays, by always returning True for them.
    """
    if (
        is_numpy_array(x)
        or is_cupy_array(x)
        or is_torch_array(x)
        or is_pydata_sparse_array(x)
    ):

    # **JAX note:** while it is possible to determine if you're inside or outside
    # jax.jit by testing the subclass of a jax.Array object, as well as testing bool()
    # as we do below for unknown arrays, this is not recommended by JAX best practices.

    # **Dask note:** Dask eagerly computes the graph on __bool__, __float__, and so on.
    # This behaviour, while impossible to change without breaking backwards
    # compatibility, is highly detrimental to performance as the whole graph will end
    # up being computed multiple times.

    if is_jax_array(x) or is_dask_array(x) or is_ndonnx_array(x):

    if not is_array_api_obj(x):

    # Unknown Array API compatible object. Note that this test may have dire consequences
    # in terms of performance, e.g. for a lazy object that eagerly computes the graph
    # on __bool__ (dask is one such example, which however is special-cased above).

    # Select a single point of the array
    s = size(x)
    if s is None:
    xp = array_namespace(x)
    if s > 1:
        x = xp.reshape(x, (-1,))[0]
    # Cast to dtype=bool and deal with size 0 arrays
    x = xp.any(x)

    try:
        bool(x)
    # The Array API standard dictates that __bool__ should raise TypeError if the
    # output cannot be defined.
    # Here we allow for it to raise arbitrary exceptions, e.g. like Dask does.
    except Exception:

def arange(
    start: Union[int, float],
    /,
    stop: Optional[Union[int, float]] = None,
    step: Union[int, float] = 1,
    *,
    xp,
    **kwargs
    _check_device(xp, device)
    return xp.arange(start, stop=stop, step=step, dtype=dtype, **kwargs)

def empty(
    shape: Union[int, Tuple[int, ...]],
    xp,
    *,
    **kwargs
    return xp.empty(shape, dtype=dtype, **kwargs)

def empty_like(
    x: ndarray, /, xp, *, dtype: Optional[Dtype] = None, device: Optional[Device] = None,
    **kwargs
    return xp.empty_like(x, dtype=dtype, **kwargs)

def eye(
    n_rows: int,
    n_cols: Optional[int] = None,
    /,
    *,
    xp,
    k: int = 0,
    **kwargs,
    return xp.eye(n_rows, M=n_cols, k=k, dtype=dtype, **kwargs)

def full(
    fill_value: Union[int, float],
    xp,
    *,
    **kwargs,
    return xp.full(shape, fill_value, dtype=dtype, **kwargs)

def full_like(
    /,
    *,
    xp,
    **kwargs,
    return xp.full_like(x, fill_value, dtype=dtype, **kwargs)

def linspace(
    stop: Union[int, float],
    /,
    num: int,
    *,
    xp,
    endpoint: bool = True,
    **kwargs,
    return xp.linspace(start, stop, num, dtype=dtype, endpoint=endpoint, **kwargs)

def ones(
    xp,
    *,
    **kwargs,
    return xp.ones(shape, dtype=dtype, **kwargs)

def ones_like(
    **kwargs,
    return xp.ones_like(x, dtype=dtype, **kwargs)

def zeros(
    xp,
    *,
    **kwargs,
    return xp.zeros(shape, dtype=dtype, **kwargs)

def zeros_like(
    **kwargs,
    return xp.zeros_like(x, dtype=dtype, **kwargs)

class UniqueAllResult(NamedTuple):
    values: ndarray
    indices: ndarray
    inverse_indices: ndarray
    counts: ndarray

class UniqueCountsResult(NamedTuple):

class UniqueInverseResult(NamedTuple):

def _unique_kwargs(xp):
    # Older versions of NumPy and CuPy do not have equal_nan. Rather than
    # trying to parse version numbers, just check if equal_nan is in the
    # signature.
    s = inspect.signature(xp.unique)
    if 'equal_nan' in s.parameters:
        return {'equal_nan': False}
    return {}

def unique_all(x: ndarray, /, xp) -> UniqueAllResult:
    kwargs = _unique_kwargs(xp)
    values, indices, inverse_indices, counts = xp.unique(
        x,
        return_counts=True,
        return_index=True,
        return_inverse=True,
        **kwargs,
    )
    # np.unique() flattens inverse indices, but they need to share x's shape
    # See https://github.com/numpy/numpy/issues/20638
    inverse_indices = inverse_indices.reshape(x.shape)
    return UniqueAllResult(
        values,
        indices,
        inverse_indices,
        counts,
    )

def unique_counts(x: ndarray, /, xp) -> UniqueCountsResult:
    res = xp.unique(
        x,
        return_index=False,
        return_inverse=False,
        **kwargs
    )

    return UniqueCountsResult(*res)

def unique_inverse(x: ndarray, /, xp) -> UniqueInverseResult:
    values, inverse_indices = xp.unique(
        x,
        return_counts=False,
        **kwargs,
    )
    # xp.unique() flattens inverse indices, but they need to share x's shape
    return UniqueInverseResult(values, inverse_indices)

def unique_values(x: ndarray, /, xp) -> ndarray:
    return xp.unique(
        x,
        **kwargs,
    )

def std(
    /,
    xp,
    *,
    axis: Optional[Union[int, Tuple[int, ...]]] = None,
    correction: Union[int, float] = 0.0, # correction instead of ddof
    keepdims: bool = False,
    **kwargs,
    return xp.std(x, axis=axis, ddof=correction, keepdims=keepdims, **kwargs)

def var(
    /,
    xp,
    *,
    **kwargs,
    return xp.var(x, axis=axis, ddof=correction, keepdims=keepdims, **kwargs)

def cumulative_sum(
    /,
    xp,
    *,
    axis: Optional[int] = None,
    include_initial: bool = False,
    **kwargs
    wrapped_xp = array_namespace(x)

    # TODO: The standard is not clear about what should happen when x.ndim == 0.
    if axis is None:
        if x.ndim > 1:
            raise ValueError("axis must be specified in cumulative_sum for more than one dimension")
        axis = 0

    res = xp.cumsum(x, axis=axis, dtype=dtype, **kwargs)

    # np.cumsum does not support include_initial
    if include_initial:
        initial_shape = list(x.shape)
        initial_shape[axis] = 1
        res = xp.concatenate(
            [wrapped_xp.zeros(shape=initial_shape, dtype=res.dtype, device=device(res)), res],
            axis=axis,
        )
    return res

def cumulative_prod(
    /,
    xp,
    *,
    **kwargs

            raise ValueError("axis must be specified in cumulative_prod for more than one dimension")
        axis = 0

    res = xp.cumprod(x, axis=axis, dtype=dtype, **kwargs)

    # np.cumprod does not support include_initial
            [wrapped_xp.ones(shape=initial_shape, dtype=res.dtype, device=device(res)), res],
        )
    return res

def clip(
    /,
    min: Optional[Union[int, float, ndarray]] = None,
    max: Optional[Union[int, float, ndarray]] = None,
    *,
    xp,
    # TODO: np.clip has other ufunc kwargs
    out: Optional[ndarray] = None,
    def _isscalar(a):
        return isinstance(a, (int, float, type(None)))
    min_shape = () if _isscalar(min) else min.shape
    max_shape = () if _isscalar(max) else max.shape

    result_shape = xp.broadcast_shapes(x.shape, min_shape, max_shape)

    # np.clip does type promotion but the array API clip requires that the
    # output have the same dtype as x. We do this instead of just downcasting
    # the result of xp.clip() to handle some corner cases better (e.g.,
    # avoiding uint64 -> float64 promotion).

    # Note: cases where min or max overflow (integer) or round (float) in the
    # wrong direction when downcasting to x.dtype are unspecified. This code
    # just does whatever NumPy does when it downcasts in the assignment, but
    # other behavior could be preferred, especially for integers. For example,
    # this code produces:

    # >>> clip(asarray(0, dtype=int8), asarray(128, dtype=int16), None)
    # -128

    # but an answer of 0 might be preferred. See
    # https://github.com/numpy/numpy/issues/24976 for more discussion on this issue.

    # At least handle the case of Python integers correctly (see
    # https://github.com/numpy/numpy/pull/26892).
    if wrapped_xp.isdtype(x.dtype, "integral"):
        if type(min) is int and min <= wrapped_xp.iinfo(x.dtype).min:
            min = None
        if type(max) is int and max >= wrapped_xp.iinfo(x.dtype).max:
            max = None

    dev = device(x)
    if out is None:
        out = wrapped_xp.empty(result_shape, dtype=x.dtype, device=dev)
    out[()] = x

    if min is not None:
        a = wrapped_xp.asarray(min, dtype=x.dtype, device=dev)
        a = xp.broadcast_to(a, result_shape)
        ia = (out < a) | xp.isnan(a)
        out[ia] = a[ia]

    if max is not None:
        b = wrapped_xp.asarray(max, dtype=x.dtype, device=dev)
        b = xp.broadcast_to(b, result_shape)
        ib = (out > b) | xp.isnan(b)
        out[ib] = b[ib]

    # Return a scalar for 0-D
    return out[()]

def permute_dims(x: ndarray, /, axes: Tuple[int, ...], xp) -> ndarray:
    return xp.transpose(x, axes)

def reshape(x: ndarray,
            /,
            shape: Tuple[int, ...],
            xp, copy: Optional[bool] = None,
            **kwargs) -> ndarray:
    if copy is True:
        x = x.copy()
    elif copy is False:
        y = x.view()
        y.shape = shape
        return y
    return xp.reshape(x, shape, **kwargs)

def argsort(
    x: ndarray, /, xp, *, axis: int = -1, descending: bool = False, stable: bool = True,
    **kwargs,
    # Note: this keyword argument is different, and the default is different.
    # We set it in kwargs like this because numpy.sort uses kind='quicksort'
    # as the default whereas cupy.sort uses kind=None.
    if stable:
        kwargs['kind'] = "stable"
    if not descending:
        res = xp.argsort(x, axis=axis, **kwargs)
    else:
        # As NumPy has no native descending sort, we imitate it here. Note that
        # simply flipping the results of xp.argsort(x, ...) would not
        # respect the relative order like it would in native descending sorts.
        res = xp.flip(
            xp.argsort(xp.flip(x, axis=axis), axis=axis, **kwargs),
        )
        # Rely on flip()/argsort() to validate axis
        normalised_axis = axis if axis >= 0 else x.ndim + axis
        max_i = x.shape[normalised_axis] - 1
        res = max_i - res
    return res

def sort(
    **kwargs,
    if stable:
    res = xp.sort(x, axis=axis, **kwargs)
    if descending:
        res = xp.flip(res, axis=axis)
    return res

def nonzero(x: ndarray, /, xp, **kwargs) -> Tuple[ndarray, ...]:
    if x.ndim == 0:
        raise ValueError("nonzero() does not support zero-dimensional arrays")
    return xp.nonzero(x, **kwargs)

def ceil(x: ndarray, /, xp, **kwargs) -> ndarray:
    if xp.issubdtype(x.dtype, xp.integer):
        return x
    return xp.ceil(x, **kwargs)

def floor(x: ndarray, /, xp, **kwargs) -> ndarray:
        return x
    return xp.floor(x, **kwargs)

def trunc(x: ndarray, /, xp, **kwargs) -> ndarray:
        return x
    return xp.trunc(x, **kwargs)

def matmul(x1: ndarray, x2: ndarray, /, xp, **kwargs) -> ndarray:
    return xp.matmul(x1, x2, **kwargs)

def matrix_transpose(x: ndarray, /, xp) -> ndarray:
    if x.ndim < 2:
        raise ValueError("x must be at least 2-dimensional for matrix_transpose")
    return xp.swapaxes(x, -1, -2)

def tensordot(x1: ndarray,
              x2: ndarray,
              /,
              xp,
              *,
              axes: Union[int, Tuple[Sequence[int], Sequence[int]]] = 2,
              **kwargs,
    return xp.tensordot(x1, x2, axes=axes, **kwargs)

def vecdot(x1: ndarray, x2: ndarray, /, xp, *, axis: int = -1) -> ndarray:
    if x1.shape[axis] != x2.shape[axis]:
        raise ValueError("x1 and x2 must have the same size along the given axis")

    if hasattr(xp, 'broadcast_tensors'):
        _broadcast = xp.broadcast_tensors
    else:
        _broadcast = xp.broadcast_arrays

    x1_ = xp.moveaxis(x1, axis, -1)
    x2_ = xp.moveaxis(x2, axis, -1)
    x1_, x2_ = _broadcast(x1_, x2_)

    res = xp.conj(x1_[..., None, :]) @ x2_[..., None]
    return res[..., 0, 0]

def isdtype(
    dtype: Dtype, kind: Union[Dtype, str, Tuple[Union[Dtype, str], ...]], xp,
    *, _tuple=True, # Disallow nested tuples
) -> bool:
    """
    Returns a boolean indicating whether a provided dtype is of a specified data type ``kind``.

    Note that outside of this function, this compat library does not yet fully
    support complex numbers.

    See
    https://data-apis.org/array-api/latest/API_specification/generated/array_api.isdtype.html
    for more details
    """
    if isinstance(kind, tuple) and _tuple:
        return any(isdtype(dtype, k, xp, _tuple=False) for k in kind)
    elif isinstance(kind, str):
        if kind == 'bool':
            return dtype == xp.bool_
        elif kind == 'signed integer':
            return xp.issubdtype(dtype, xp.signedinteger)
        elif kind == 'unsigned integer':
            return xp.issubdtype(dtype, xp.unsignedinteger)
        elif kind == 'integral':
            return xp.issubdtype(dtype, xp.integer)
        elif kind == 'real floating':
            return xp.issubdtype(dtype, xp.floating)
        elif kind == 'complex floating':
            return xp.issubdtype(dtype, xp.complexfloating)
        elif kind == 'numeric':
            return xp.issubdtype(dtype, xp.number)
        else:
            raise ValueError(f"Unrecognized data type kind: {kind!r}")
    else:
        # This will allow things that aren't required by the spec, like
        # isdtype(np.float64, float) or isdtype(np.int64, 'l'). Should we be
        # more strict here to match the type annotation? Note that the
        # array_api_strict implementation will be very strict.
        return dtype == kind

def unstack(x: ndarray, /, xp, *, axis: int = 0) -> Tuple[ndarray, ...]:
        raise ValueError("Input array must be at least 1-d.")
    return tuple(xp.moveaxis(x, axis, 0))

def sign(x: ndarray, /, xp, **kwargs) -> ndarray:
    if isdtype(x.dtype, 'complex floating', xp=xp):
        out = (x/xp.abs(x, **kwargs))[...]
        # sign(0) = 0 but the above formula would give nan
        out[x == 0+0j] = 0+0j
    else:
        out = xp.sign(x, **kwargs)
    # CuPy sign() does not propagate nans. See
    # https://github.com/data-apis/array-api-compat/issues/136
    if is_cupy_namespace(xp) and isdtype(x.dtype, 'real floating', xp=xp):
        out[xp.isnan(x)] = xp.nan

class NestedSequence(Protocol[_T_co]):
    def __getitem__(self, key: int, /) -> _T_co | NestedSequence[_T_co]: ...
    def __len__(self, /) -> int: ...

def cross(x1: ndarray, x2: ndarray, /, xp, *, axis: int = -1, **kwargs) -> ndarray:
    return xp.cross(x1, x2, axis=axis, **kwargs)

def outer(x1: ndarray, x2: ndarray, /, xp, **kwargs) -> ndarray:
    return xp.outer(x1, x2, **kwargs)

class EighResult(NamedTuple):
    eigenvalues: ndarray
    eigenvectors: ndarray

class SlogdetResult(NamedTuple):
    sign: ndarray
    logabsdet: ndarray

class SVDResult(NamedTuple):
    U: ndarray
    S: ndarray
    Vh: ndarray

def eigh(x: ndarray, /, xp, **kwargs) -> EighResult:
    return EighResult(*xp.linalg.eigh(x, **kwargs))

def qr(x: ndarray, /, xp, *, mode: Literal['reduced', 'complete'] = 'reduced',
       **kwargs) -> QRResult:
    return QRResult(*xp.linalg.qr(x, mode=mode, **kwargs))

def slogdet(x: ndarray, /, xp, **kwargs) -> SlogdetResult:
    return SlogdetResult(*xp.linalg.slogdet(x, **kwargs))

def svd(x: ndarray, /, xp, *, full_matrices: bool = True, **kwargs) -> SVDResult:
    return SVDResult(*xp.linalg.svd(x, full_matrices=full_matrices, **kwargs))

def cholesky(x: ndarray, /, xp, *, upper: bool = False, **kwargs) -> ndarray:
    L = xp.linalg.cholesky(x, **kwargs)
    if upper:
        U = get_xp(xp)(matrix_transpose)(L)
        if get_xp(xp)(isdtype)(U.dtype, 'complex floating'):
            U = xp.conj(U)
        return U
    return L

def matrix_rank(x: ndarray,
                /,
                xp,
                *,
                rtol: Optional[Union[float, ndarray]] = None,
                **kwargs) -> ndarray:
    # this is different from xp.linalg.matrix_rank, which supports 1
    # dimensional arrays.
        raise xp.linalg.LinAlgError("1-dimensional array given. Array must be at least two-dimensional")
    S = get_xp(xp)(svdvals)(x, **kwargs)
    if rtol is None:
        tol = S.max(axis=-1, keepdims=True) * max(x.shape[-2:]) * xp.finfo(S.dtype).eps
    else:
        # this is different from xp.linalg.matrix_rank, which does not
        # multiply the tolerance by the largest singular value.
        tol = S.max(axis=-1, keepdims=True)*xp.asarray(rtol)[..., xp.newaxis]
    return xp.count_nonzero(S > tol, axis=-1)

def pinv(x: ndarray, /, xp, *, rtol: Optional[Union[float, ndarray]] = None, **kwargs) -> ndarray:
    # this is different from xp.linalg.pinv, which does not multiply the
    # default tolerance by max(M, N).
        rtol = max(x.shape[-2:]) * xp.finfo(x.dtype).eps
    return xp.linalg.pinv(x, rcond=rtol, **kwargs)

def matrix_norm(x: ndarray, /, xp, *, keepdims: bool = False, ord: Optional[Union[int, float, Literal['fro', 'nuc']]] = 'fro') -> ndarray:
    return xp.linalg.norm(x, axis=(-2, -1), keepdims=keepdims, ord=ord)

def svdvals(x: ndarray, /, xp) -> Union[ndarray, Tuple[ndarray, ...]]:
    return xp.linalg.svd(x, compute_uv=False)

def vector_norm(x: ndarray, /, xp, *, axis: Optional[Union[int, Tuple[int, ...]]] = None, keepdims: bool = False, ord: Optional[Union[int, float]] = 2) -> ndarray:
    # xp.linalg.norm tries to do a matrix norm whenever axis is a 2-tuple or
    # when axis=None and the input is 2-D, so to force a vector norm, we make
    # it so the input is 1-D (for axis=None), or reshape so that norm is done
    # on a single dimension.
        # Note: xp.linalg.norm() doesn't handle 0-D arrays
        _x = x.ravel()
        _axis = 0
    elif isinstance(axis, tuple):
        # Note: The axis argument supports any number of axes, whereas
        # xp.linalg.norm() only supports a single axis for vector norm.
        normalized_axis = normalize_axis_tuple(axis, x.ndim)
        rest = tuple(i for i in range(x.ndim) if i not in normalized_axis)
        newshape = axis + rest
        _x = xp.transpose(x, newshape).reshape(
            (math.prod([x.shape[i] for i in axis]), *[x.shape[i] for i in rest]))
        _axis = 0
    else:
        _x = x
        _axis = axis

    res = xp.linalg.norm(_x, axis=_axis, ord=ord)

    if keepdims:
        # We can't reuse xp.linalg.norm(keepdims) because of the reshape hacks
        # above to avoid matrix norm logic.
        shape = list(x.shape)
        _axis = normalize_axis_tuple(range(x.ndim) if axis is None else axis, x.ndim)
        for i in _axis:
            shape[i] = 1
        res = xp.reshape(res, tuple(shape))

    return res

def diagonal(x: ndarray, /, xp, *, offset: int = 0, **kwargs) -> ndarray:
    return xp.diagonal(x, offset=offset, axis1=-2, axis2=-1, **kwargs)

def trace(x: ndarray, /, xp, *, offset: int = 0, dtype=None, **kwargs) -> ndarray:
    return xp.asarray(xp.trace(x, offset=offset, dtype=dtype, axis1=-2, axis2=-1, **kwargs))

    """
    Get the array API inspection namespace for PyTorch.

    - dtypes()

    See

    Returns
    -------
        The array API inspection namespace for PyTorch.

    Examples
    --------

    """

    __module__ = 'torch'

        """

          supports boolean indexing. Always ``True`` for PyTorch.

          PyTorch.

        See

        See Also
        --------

        Returns
        -------

        Examples
        --------

        """
        return {
        }

        """
        The default device used for new PyTorch arrays.

        See Also
        --------

        Returns
        -------
            The default device used for new PyTorch arrays.

        Examples
        --------
        'cpu'

        """
        return torch.device("cpu")

        """
        The default data types used for new PyTorch arrays.

            The device to get the default data types for. For PyTorch, only

        Returns
        -------
            A dictionary describing the default data types used for new PyTorch
            arrays.

        See Also
        --------

        Examples
        --------
        {'real floating': torch.float32,
         'complex floating': torch.complex64,
         'integral': torch.int64,
         'indexing': torch.int64}

        """
        # Note: if the default is set to float64, the devices like MPS that
        # don't support float64 will error. We still return the default_dtype
        # value here because this error doesn't represent a different default
        # per-device.
        default_floating = torch.get_default_dtype()
        default_complex = torch.complex64 if default_floating == torch.float32 else torch.complex128
        default_integral = torch.int64
        return {
            "real floating": default_floating,
            "complex floating": default_complex,
            "integral": default_integral,
            "indexing": default_integral,
        }

    def _dtypes(self, kind):
        bool = torch.bool
        int8 = torch.int8
        int16 = torch.int16
        int32 = torch.int32
        int64 = torch.int64
        uint8 = torch.uint8
        # uint16, uint32, and uint64 are present in newer versions of pytorch,
        # but they aren't generally supported by the array API functions, so
        # we omit them from this function.
        float32 = torch.float32
        float64 = torch.float64
        complex64 = torch.complex64
        complex128 = torch.complex128

            return {
                "bool": bool,
                "int8": int8,
                "int16": int16,
                "int32": int32,
                "int64": int64,
                "uint8": uint8,
                "float32": float32,
                "float64": float64,
                "complex64": complex64,
                "complex128": complex128,
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            res = {}
            return res

    @cache
        """
        The array API data types supported by PyTorch.

        Returns
        -------
            PyTorch data types.

        See Also
        --------

        Examples
        --------

        """
        res = self._dtypes(kind)
        for k, v in res.copy().items():
            try:
                torch.empty((0,), dtype=v, device=device)
            except:
                del res[k]
        return res

    @cache
        """
        The devices supported by PyTorch.

        Returns
        -------
            The devices supported by PyTorch.

        See Also
        --------

        Examples
        --------
        [device(type='cpu'), device(type='mps', index=0), device(type='meta')]

        """
        # Torch doesn't have a straightforward way to get the list of all
        # currently supported devices. To do this, we first parse the error
        # message of torch.device to get the list of all possible types of
        # device:
        try:
            torch.device('notadevice')
        except RuntimeError as e:
            # The error message is something like:
            # "Expected one of cpu, cuda, ipu, xpu, mkldnn, opengl, opencl, ideep, hip, ve, fpga, ort, xla, lazy, vulkan, mps, meta, hpu, mtia, privateuseone device type at start of device string: notadevice"
            devices_names = e.args[0].split('Expected one of ')[1].split(' device type')[0].split(', ')

        # Next we need to check for different indices for different devices.
        # device(device_name, index=index) doesn't actually check if the
        # device name or index is valid. We have to try to create a tensor
        # with it (which is why this function is cached).
        devices = []
        for device_name in devices_names:
            i = 0
                try:
                    a = torch.empty((0,), device=torch.device(device_name, index=i))
                    if a.device in devices:
                        break
                    devices.append(a.device)
                except:
                    break
                i += 1

        return devices

        """

          PyTorch.

        See

        See Also
        --------

        Returns
        -------

        Examples
        --------

        """
        return {
        }

        """

        See Also
        --------

        Returns
        -------

        Examples
        --------
        'cpu'

        """

        """

        Returns
        -------
            arrays.

        See Also
        --------

        Examples
        --------

        """
        return {
        }

            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            res = {}
            return res

        """

        Returns
        -------

        See Also
        --------

        Examples
        --------

        """
            try:
            except:
                del res[k]
        return res

        """

        Returns
        -------

        See Also
        --------

        Examples
        --------

        """
        # device:
        try:

            i = 0
                try:
                        break
                except:
                    break
                i += 1

def fftn(
    x: array,
    /,
    *,
    **kwargs,
) -> array:
    return torch.fft.fftn(x, s=s, dim=axes, norm=norm, **kwargs)

def ifftn(
    x: array,
    /,
    *,
    **kwargs,
) -> array:
    return torch.fft.ifftn(x, s=s, dim=axes, norm=norm, **kwargs)

def rfftn(
    x: array,
    /,
    *,
    **kwargs,
) -> array:
    return torch.fft.rfftn(x, s=s, dim=axes, norm=norm, **kwargs)

    x: array,
    /,
    *,
    **kwargs,
) -> array:
    return torch.fft.irfftn(x, s=s, dim=axes, norm=norm, **kwargs)

def fftshift(
    x: array,
    /,
    *,
    axes: Union[int, Sequence[int]] = None,
    **kwargs,
) -> array:
    return torch.fft.fftshift(x, dim=axes, **kwargs)

def ifftshift(
    x: array,
    /,
    *,
    **kwargs,
) -> array:
    return torch.fft.ifftshift(x, dim=axes, **kwargs)

def cross(x1: array, x2: array, /, *, axis: int = -1) -> array:
    x1, x2 = _fix_promotion(x1, x2, only_scalar=False)
    if not (-min(x1.ndim, x2.ndim) <= axis < max(x1.ndim, x2.ndim)):
        raise ValueError(f"axis {axis} out of bounds for cross product of arrays with shapes {x1.shape} and {x2.shape}")
    if not (x1.shape[axis] == x2.shape[axis] == 3):
        raise ValueError(f"cross product axis must have size 3, got {x1.shape[axis]} and {x2.shape[axis]}")
    x1, x2 = torch.broadcast_tensors(x1, x2)
    return torch_linalg.cross(x1, x2, dim=axis)

def vecdot(x1: array, x2: array, /, *, axis: int = -1, **kwargs) -> array:
    from ._aliases import isdtype

    # torch.linalg.vecdot incorrectly allows broadcasting along the contracted dimension

    # torch.linalg.vecdot doesn't support integer dtypes
    if isdtype(x1.dtype, 'integral') or isdtype(x2.dtype, 'integral'):
        if kwargs:
            raise RuntimeError("vecdot kwargs not supported for integral dtypes")

        x1_ = torch.moveaxis(x1, axis, -1)
        x2_ = torch.moveaxis(x2, axis, -1)
        x1_, x2_ = torch.broadcast_tensors(x1_, x2_)

        res = x1_[..., None, :] @ x2_[..., None]
        return res[..., 0, 0]
    return torch.linalg.vecdot(x1, x2, dim=axis, **kwargs)

def solve(x1: array, x2: array, /, **kwargs) -> array:
    # Torch tries to emulate NumPy 1 solve behavior by using batched 1-D solve
    # whenever
    # 1. x1.ndim - 1 == x2.ndim
    # 2. x1.shape[:-1] == x2.shape
    #
    # See linalg_solve_is_vector_rhs in
    # aten/src/ATen/native/LinearAlgebraUtils.h and
    # TORCH_META_FUNC(_linalg_solve_ex) in
    # aten/src/ATen/native/BatchLinearAlgebra.cpp in the PyTorch source code.
    #
    # The easiest way to work around this is to prepend a size 1 dimension to
    # x2, since x2 is already one dimension less than x1.
    #
    # See https://github.com/pytorch/pytorch/issues/52915
    if x2.ndim != 1 and x1.ndim - 1 == x2.ndim and x1.shape[:-1] == x2.shape:
        x2 = x2[None]
    return torch.linalg.solve(x1, x2, **kwargs)

def trace(x: array, /, *, offset: int = 0, dtype: Optional[Dtype] = None) -> array:
    # Use our wrapped sum to make sure it does upcasting correctly
    return sum(torch.diagonal(x, offset=offset, dim1=-2, dim2=-1), axis=-1, dtype=dtype)

def vector_norm(
    x: array,
    /,
    *,
    ord: Union[int, float, Literal[inf, -inf]] = 2,
    **kwargs,
) -> array:
    # torch.vector_norm incorrectly treats axis=() the same as axis=None
    if axis == ():
        out = kwargs.get('out')
        if out is None:
            dtype = None
            if x.dtype == torch.complex64:
                dtype = torch.float32
            elif x.dtype == torch.complex128:
                dtype = torch.float64

            out = torch.zeros_like(x, dtype=dtype)

        # The norm of a single scalar works out to abs(x) in every case except
        # for ord=0, which is x != 0.
        if ord == 0:
            out[:] = (x != 0)
        else:
            out[:] = torch.abs(x)
        return out
    return torch.linalg.vector_norm(x, ord=ord, axis=axis, keepdim=keepdims, **kwargs)

def _two_arg(f):
    @_wraps(f)
    def _f(x1, x2, /, **kwargs):
        x1, x2 = _fix_promotion(x1, x2)
        return f(x1, x2, **kwargs)
    if _f.__doc__ is None:
        _f.__doc__ = f"""\
Array API compatibility wrapper for torch.{f.__name__}.

See the corresponding PyTorch documentation and/or the array API specification
for more details.

"""
    return _f

def _fix_promotion(x1, x2, only_scalar=True):
    if not isinstance(x1, torch.Tensor) or not isinstance(x2, torch.Tensor):
        return x1, x2
    if x1.dtype not in _array_api_dtypes or x2.dtype not in _array_api_dtypes:
    # If an argument is 0-D pytorch downcasts the other argument
    if not only_scalar or x1.shape == ():
        dtype = result_type(x1, x2)
        x2 = x2.to(dtype)
    if not only_scalar or x2.shape == ():
        x1 = x1.to(dtype)
    return x1, x2

def result_type(*arrays_and_dtypes: Union[array, Dtype, bool, int, float, complex]) -> Dtype:
    num = len(arrays_and_dtypes)

    if num == 0:
        raise ValueError("At least one array or dtype must be provided")

    elif num == 1:
        x = arrays_and_dtypes[0]
        if isinstance(x, torch.dtype):
            return x
        return x.dtype

    if num == 2:
        x, y = arrays_and_dtypes
        return _result_type(x, y)

    else:
        # sort scalars so that they are treated last
        scalars, others = [], []
        for x in arrays_and_dtypes:
            if isinstance(x, _py_scalars):
                scalars.append(x)
            else:
                others.append(x)
        if not others:
            raise ValueError("At least one array or dtype must be provided")

        # combine left-to-right
        return _reduce(_result_type, others + scalars)

def _result_type(x, y):
    if not (isinstance(x, _py_scalars) or isinstance(y, _py_scalars)):
        xdt = x.dtype if not isinstance(x, torch.dtype) else x
        ydt = y.dtype if not isinstance(y, torch.dtype) else y

        if (xdt, ydt) in _promotion_table:
            return _promotion_table[xdt, ydt]

    # This doesn't result_type(dtype, dtype) for non-array API dtypes
    # because torch.result_type only accepts tensors. This does however, allow
    # cross-kind promotion.
    x = torch.tensor([], dtype=x) if isinstance(x, torch.dtype) else x
    y = torch.tensor([], dtype=y) if isinstance(y, torch.dtype) else y
    return torch.result_type(x, y)

def can_cast(from_: Union[Dtype, array], to: Dtype, /) -> bool:
    if not isinstance(from_, torch.dtype):
        from_ = from_.dtype
    return torch.can_cast(from_, to)

def max(x: array, /, *, axis: Optional[Union[int, Tuple[int, ...]]] = None, keepdims: bool = False) -> array:
    # https://github.com/pytorch/pytorch/issues/29137
        return torch.clone(x)
    return torch.amax(x, axis, keepdims=keepdims)

def min(x: array, /, *, axis: Optional[Union[int, Tuple[int, ...]]] = None, keepdims: bool = False) -> array:
    return torch.amin(x, axis, keepdims=keepdims)

def sort(x: array, /, *, axis: int = -1, descending: bool = False, stable: bool = True, **kwargs) -> array:
    return torch.sort(x, dim=axis, descending=descending, stable=stable, **kwargs).values

def _normalize_axes(axis, ndim):
    axes = []
    if ndim == 0 and axis:
        # Better error message in this case
        raise IndexError(f"Dimension out of range: {axis[0]}")
    lower, upper = -ndim, ndim - 1
    for a in axis:
        if a < lower or a > upper:
            # Match torch error message (e.g., from sum())
            raise IndexError(f"Dimension out of range (expected to be in range of [{lower}, {upper}], but got {a}")
        if a < 0:
            a = a + ndim
        if a in axes:
            # Use IndexError instead of RuntimeError, and "axis" instead of "dim"
            raise IndexError(f"Axis {a} appears multiple times in the list of axes")
        axes.append(a)
    return sorted(axes)

def _axis_none_keepdims(x, ndim, keepdims):
    # Apply keepdims when axis=None
    # (https://github.com/pytorch/pytorch/issues/71209)
    # Note that this is only valid for the axis=None case.
        for i in range(ndim):
            x = torch.unsqueeze(x, 0)
    return x

def _reduce_multiple_axes(f, x, axis, keepdims=False, **kwargs):
    # Some reductions don't support multiple axes
    # (https://github.com/pytorch/pytorch/issues/56586).
    axes = _normalize_axes(axis, x.ndim)
    for a in reversed(axes):
        x = torch.movedim(x, a, -1)
    x = torch.flatten(x, -len(axes))

    out = f(x, -1, **kwargs)

        for a in axes:
            out = torch.unsqueeze(out, a)
    return out

def prod(x: array,
         /,
         *,
         axis: Optional[Union[int, Tuple[int, ...]]] = None,
         dtype: Optional[Dtype] = None,
         keepdims: bool = False,
         **kwargs) -> array:
    x = torch.asarray(x)
    ndim = x.ndim

    # https://github.com/pytorch/pytorch/issues/29137. Separate from the logic
    # below because it still needs to upcast.
        if dtype is None:
            # We can't upcast uint8 according to the spec because there is no
            # torch.uint64, so at least upcast to int64 which is what sum does
            # when axis=None.
            if x.dtype in [torch.int8, torch.int16, torch.int32, torch.uint8]:
                return x.to(torch.int64)
            return x.clone()
        return x.to(dtype)

    # torch.prod doesn't support multiple axes
    if isinstance(axis, tuple):
        return _reduce_multiple_axes(torch.prod, x, axis, keepdims=keepdims, dtype=dtype, **kwargs)
        # torch doesn't support keepdims with axis=None
        # (https://github.com/pytorch/pytorch/issues/71209)
        res = torch.prod(x, dtype=dtype, **kwargs)
        res = _axis_none_keepdims(res, ndim, keepdims)
        return res

    return torch.prod(x, axis, dtype=dtype, keepdims=keepdims, **kwargs)

def sum(x: array,
         /,
         *,

    # https://github.com/pytorch/pytorch/issues/29137.
    # Make sure it upcasts.

        res = torch.sum(x, dtype=dtype, **kwargs)
        return res

    return torch.sum(x, axis, dtype=dtype, keepdims=keepdims, **kwargs)

def any(x: array,
        /,
        *,
        axis: Optional[Union[int, Tuple[int, ...]]] = None,
        keepdims: bool = False,
        **kwargs) -> array:
        return x.to(torch.bool)
    # torch.any doesn't support multiple axes
        res = _reduce_multiple_axes(torch.any, x, axis, keepdims=keepdims, **kwargs)
        return res.to(torch.bool)
        res = torch.any(x, **kwargs)

    # torch.any doesn't return bool for uint8
    return torch.any(x, axis, keepdims=keepdims).to(torch.bool)

def all(x: array,
        /,
        *,
    # torch.all doesn't support multiple axes
        res = _reduce_multiple_axes(torch.all, x, axis, keepdims=keepdims, **kwargs)
        res = torch.all(x, **kwargs)

    # torch.all doesn't return bool for uint8
    return torch.all(x, axis, keepdims=keepdims).to(torch.bool)

def mean(x: array,
         /,
         *,
        res = torch.mean(x, **kwargs)
        res = _axis_none_keepdims(res, x.ndim, keepdims)
        return res
    return torch.mean(x, axis, keepdims=keepdims, **kwargs)

def std(x: array,
        /,
        *,
        correction: Union[int, float] = 0.0,
    # Note, float correction is not supported
    # https://github.com/pytorch/pytorch/issues/61492. We don't try to
    # implement it here for now.

    if isinstance(correction, float):
        _correction = int(correction)
        if correction != _correction:
            raise NotImplementedError("float correction in torch std() is not yet supported")
    else:
        _correction = correction

        return torch.zeros_like(x)
    if isinstance(axis, int):
        axis = (axis,)
        res = torch.std(x, tuple(range(x.ndim)), correction=_correction, **kwargs)
        return res
    return torch.std(x, axis, correction=_correction, keepdims=keepdims, **kwargs)

def var(x: array,
        /,
        *,

    # if isinstance(correction, float):
    #     correction = int(correction)

        res = torch.var(x, tuple(range(x.ndim)), correction=correction, **kwargs)
        return res
    return torch.var(x, axis, correction=correction, keepdims=keepdims, **kwargs)

def concat(arrays: Union[Tuple[array, ...], List[array]],
           /,
           *,
           axis: Optional[int] = 0,
           **kwargs) -> array:
        arrays = tuple(ar.flatten() for ar in arrays)
        axis = 0
    return torch.concat(arrays, axis, **kwargs)

def squeeze(x: array, /, axis: Union[int, Tuple[int, ...]]) -> array:
        if x.shape[a] != 1:
            raise ValueError("squeezed dimensions must be equal to 1")
    # Remove this once pytorch 1.14 is released with the above PR #89017.
    sequence = [a - i for i, a in enumerate(axes)]
    for a in sequence:
        x = torch.squeeze(x, a)
    return x

def broadcast_to(x: array, /, shape: Tuple[int, ...], **kwargs) -> array:
    return torch.broadcast_to(x, shape, **kwargs)

def permute_dims(x: array, /, axes: Tuple[int, ...]) -> array:
    return torch.permute(x, axes)

def flip(x: array, /, *, axis: Optional[Union[int, Tuple[int, ...]]] = None, **kwargs) -> array:
        axis = tuple(range(x.ndim))
    # torch.flip doesn't accept dim as an int but the method does
    # https://github.com/pytorch/pytorch/issues/18095
    return x.flip(axis, **kwargs)

def roll(x: array, /, shift: Union[int, Tuple[int, ...]], *, axis: Optional[Union[int, Tuple[int, ...]]] = None, **kwargs) -> array:
    return torch.roll(x, shift, axis, **kwargs)

def nonzero(x: array, /, **kwargs) -> Tuple[array, ...]:
    return torch.nonzero(x, as_tuple=True, **kwargs)

def diff(
    x: array,
    /,
    *,
    n: int = 1,
    prepend: Optional[array] = None,
    append: Optional[array] = None,
) -> array:
    return torch.diff(x, dim=axis, n=n, prepend=prepend, append=append)

    x: array,
    /,
    *,
) -> array:
    result = torch.count_nonzero(x, dim=axis)
        if axis is not None:
            return result.unsqueeze(axis)
        return _axis_none_keepdims(result, x.ndim, keepdims)
    else:

def where(condition: array, x1: array, x2: array, /) -> array:
    x1, x2 = _fix_promotion(x1, x2)
    return torch.where(condition, x1, x2)

def reshape(x: array,
            /,
            copy: Optional[bool] = None,
            **kwargs) -> array:
    if copy is not None:
        raise NotImplementedError("torch.reshape doesn't yet support the copy keyword")
    return torch.reshape(x, shape, **kwargs)

def arange(start: Union[int, float],
           /,
           stop: Optional[Union[int, float]] = None,
           step: Union[int, float] = 1,
           *,
           dtype: Optional[Dtype] = None,
           device: Optional[Device] = None,
    if stop is None:
        start, stop = 0, start
    if step > 0 and stop <= start or step < 0 and stop >= start:
            if _builtin_all(isinstance(i, int) for i in [start, stop, step]):
                dtype = torch.int64
            else:
        return torch.empty(0, dtype=dtype, device=device, **kwargs)
    return torch.arange(start, stop, step, dtype=dtype, device=device, **kwargs)

def eye(n_rows: int,
        n_cols: Optional[int] = None,
        /,
        *,
        k: int = 0,
        dtype: Optional[Dtype] = None,
        device: Optional[Device] = None,
    if n_cols is None:
        n_cols = n_rows
    z = torch.zeros(n_rows, n_cols, dtype=dtype, device=device, **kwargs)
    if abs(k) <= n_rows + n_cols:
        z.diagonal(k).fill_(1)
    return z

def linspace(start: Union[int, float],
             stop: Union[int, float],
             /,
             num: int,
             *,
             dtype: Optional[Dtype] = None,
             device: Optional[Device] = None,
             endpoint: bool = True,
             **kwargs) -> array:
    if not endpoint:
        return torch.linspace(start, stop, num+1, dtype=dtype, device=device, **kwargs)[:-1]
    return torch.linspace(start, stop, num, dtype=dtype, device=device, **kwargs)

def full(shape: Union[int, Tuple[int, ...]],
         fill_value: Union[bool, int, float, complex],
         *,
         device: Optional[Device] = None,
    if isinstance(shape, int):
        shape = (shape,)

    return torch.full(shape, fill_value, dtype=dtype, device=device, **kwargs)

def ones(shape: Union[int, Tuple[int, ...]],
         *,
    return torch.ones(shape, dtype=dtype, device=device, **kwargs)

def zeros(shape: Union[int, Tuple[int, ...]],
         *,
    return torch.zeros(shape, dtype=dtype, device=device, **kwargs)

def empty(shape: Union[int, Tuple[int, ...]],
         *,
    return torch.empty(shape, dtype=dtype, device=device, **kwargs)

def tril(x: array, /, *, k: int = 0) -> array:
    return torch.tril(x, k)

def triu(x: array, /, *, k: int = 0) -> array:
    return torch.triu(x, k)

def expand_dims(x: array, /, *, axis: int = 0) -> array:
    return torch.unsqueeze(x, axis)

    x: array,
    /,
    *,
) -> array:
    if device is not None:
        return x.to(device, dtype=dtype, copy=copy)
    return x.to(dtype=dtype, copy=copy)

def broadcast_arrays(*arrays: array) -> List[array]:
    shape = torch.broadcast_shapes(*[a.shape for a in arrays])
    return [torch.broadcast_to(a, shape) for a in arrays]

def unique_all(x: array) -> UniqueAllResult:
    # torch.unique doesn't support returning indices.
    # https://github.com/pytorch/pytorch/issues/36748. The workaround
    # suggested in that issue doesn't actually function correctly (it relies
    # on non-deterministic behavior of scatter()).
    raise NotImplementedError("unique_all() not yet implemented for pytorch (see https://github.com/pytorch/pytorch/issues/36748)")

def unique_counts(x: array) -> UniqueCountsResult:
    values, counts = torch.unique(x, return_counts=True)

    # torch.unique incorrectly gives a 0 count for nan values.
    # https://github.com/pytorch/pytorch/issues/94106
    counts[torch.isnan(values)] = 1
    return UniqueCountsResult(values, counts)

def unique_inverse(x: array) -> UniqueInverseResult:
    values, inverse = torch.unique(x, return_inverse=True)
    return UniqueInverseResult(values, inverse)

def unique_values(x: array) -> array:
    return torch.unique(x)

def matmul(x1: array, x2: array, /, **kwargs) -> array:
    # torch.matmul doesn't type promote (but differently from _fix_promotion)
    return torch.matmul(x1, x2, **kwargs)

def vecdot(x1: array, x2: array, /, *, axis: int = -1) -> array:
    return _vecdot(x1, x2, axis=axis)

def tensordot(x1: array, x2: array, /, *, axes: Union[int, Tuple[Sequence[int], Sequence[int]]] = 2, **kwargs) -> array:
    # Note: torch.tensordot fails with integer dtypes when there is only 1
    # element in the axis (https://github.com/pytorch/pytorch/issues/84530).
    return torch.tensordot(x1, x2, dims=axes, **kwargs)

    dtype: Dtype, kind: Union[Dtype, str, Tuple[Union[Dtype, str], ...]],
) -> bool:
    """

    See
    """
        return _builtin_any(isdtype(dtype, k, _tuple=False) for k in kind)
            return dtype == torch.bool
            return dtype in _int_dtypes and dtype.is_signed
            return dtype in _int_dtypes and not dtype.is_signed
            return dtype in _int_dtypes
            return dtype.is_floating_point
            return dtype.is_complex
            return isdtype(dtype, ('integral', 'real floating', 'complex floating'))
        else:
    else:

def take(x: array, indices: array, /, *, axis: Optional[int] = None, **kwargs) -> array:
        if x.ndim != 1:
            raise ValueError("axis must be specified when ndim > 1")
        axis = 0
    return torch.index_select(x, axis, indices, **kwargs)

def take_along_axis(x: array, indices: array, /, *, axis: int = -1) -> array:
    return torch.take_along_dim(x, indices, dim=axis)

def sign(x: array, /) -> array:
    # torch sign() does not support complex numbers and does not propagate
    # nans. See https://github.com/data-apis/array-api-compat/issues/136
    if x.dtype.is_complex:
        out = x/torch.abs(x)
        return out
    else:
        out = torch.sign(x)
        if x.dtype.is_floating_point:
            out[torch.isnan(x)] = torch.nan
        return out

    """
    Get the array API inspection namespace for Dask.

    - dtypes()

    See

    Returns
    -------
        The array API inspection namespace for Dask.

    Examples
    --------
    {'real floating': dask.float64,
     'complex floating': dask.complex128,
     'integral': dask.int64,
     'indexing': dask.int64}

    """

    __module__ = 'dask.array'

        """

          supports boolean indexing. Always ``False`` for Dask.

          library supports data-dependent output shapes. Always ``False`` for
          Dask.

        See

        See Also
        --------

        Returns
        -------

        Examples
        --------

        """
        return {
            "boolean indexing": False,
            "data-dependent shapes": False,
        }

        """
        The default device used for new Dask arrays.

        For Dask, this always returns ``'cpu'``.

        See Also
        --------

        Returns
        -------
            The default device used for new Dask arrays.

        Examples
        --------
        'cpu'

        """

        """
        The default data types used for new Dask arrays.

        For Dask, this always returns the following dictionary:

        Returns
        -------
            A dictionary describing the default data types used for new Dask
            arrays.

        See Also
        --------

        Examples
        --------
        {'real floating': dask.float64,
         'complex floating': dask.complex128,
         'integral': dask.int64,
         'indexing': dask.int64}

        """
        if device not in ["cpu", _DASK_DEVICE, None]:
                'Device not understood. Only "cpu" or _DASK_DEVICE is allowed, but received:'
            )
        return {
        }

        """
        The array API data types supported by Dask.

        Returns
        -------
            Dask data types.

        See Also
        --------

        Examples
        --------
        {'int8': dask.int8,
         'int16': dask.int16,
         'int32': dask.int32,
         'int64': dask.int64}

        """
            )
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            res = {}
            return res

        """
        The devices supported by Dask.

        For Dask, this always returns ``['cpu', DASK_DEVICE]``.

        Returns
        -------
            The devices supported by Dask.

        See Also
        --------

        Examples
        --------
        ['cpu', DASK_DEVICE]

        """
        return ["cpu", _DASK_DEVICE]

        """

          Dask.

        See

        See Also
        --------

        Returns
        -------

        Examples
        --------

        """
        return {
        }

        """

        See Also
        --------

        Returns
        -------

        Examples
        --------
        'cpu'

        """

        """

        Returns
        -------
            arrays.

        See Also
        --------

        Examples
        --------

        """
            )
        return {
        }

        """

        Returns
        -------

        See Also
        --------

        Examples
        --------

        """
            )
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            return {
            }
            res = {}
            return res

        """

        Returns
        -------

        See Also
        --------

        Examples
        --------

        """

def qr(x: Array, mode: Literal['reduced', 'complete'] = 'reduced',
    if mode != "reduced":
        raise ValueError("dask arrays only support using mode='reduced'")
    return QRResult(*da.linalg.qr(x, **kwargs))

def svd(x: Array, full_matrices: bool = True, **kwargs) -> SVDResult:
    if full_matrices:
        raise ValueError("full_matrics=True is not supported by dask.")
    return da.linalg.svd(x, coerce_signs=False, **kwargs)

def svdvals(x: Array) -> Array:
    # TODO: can't avoid computing U or V for dask
    _, s, _ =  svd(x)
    return s

    x: Array,
    /,
    *,
) -> Array:
    """
    Array API compatibility wrapper for astype().

    """
    # TODO: respect device keyword?

    if not copy and dtype == x.dtype:
        return x
    x = x.astype(dtype)
    return x.copy() if copy else x

    /,
    *,
    **kwargs,
) -> Array:
    """
    Array API compatibility wrapper for arange().

    """

    args = [start]
    if stop is not None:
        args.append(stop)
    else:
        # stop is None, so start is actually stop
        # prepend the default value for start which is 0
        args.insert(0, 0)
    args.append(step)

    return da.arange(*args, dtype=dtype, **kwargs)

        Array,
        bool,
        int,
        float,
    ],
    /,
    *,
    copy: Optional[Union[bool, np._CopyMode]] = None,
    **kwargs,
) -> Array:
    """

    """

    if isinstance(obj, da.Array):
        if dtype is not None and dtype != obj.dtype:
            if copy is False:
                raise ValueError("Unable to avoid copy when changing dtype")
            obj = obj.astype(dtype)
        return obj.copy() if copy else obj

    if copy is False:
            "Unable to avoid copy when converting a non-dask object to dask"
        )

    # copy=None to be uniform across dask < 2024.12 and >= 2024.12
    # see https://github.com/dask/dask/pull/11524/
    obj = np.array(obj, dtype=dtype, copy=True)
    return da.from_array(obj)

def clip(
    x: Array,
    /,
    min: Optional[Union[int, float, Array]] = None,
    max: Optional[Union[int, float, Array]] = None,
) -> Array:
    """
    Array API compatibility wrapper for clip().

    """

    # TODO: This won't handle dask unknown shapes
    result_shape = np.broadcast_shapes(x.shape, min_shape, max_shape)

        min = da.broadcast_to(da.asarray(min), result_shape)
        max = da.broadcast_to(da.asarray(max), result_shape)

    if min is None and max is None:
        return da.positive(x)

    if min is None:
        return astype(da.minimum(x, max), x.dtype)
    if max is None:
        return astype(da.maximum(x, min), x.dtype)

    return astype(da.minimum(da.maximum(x, min), max), x.dtype)

def _ensure_single_chunk(x: Array, axis: int) -> tuple[Array, Callable[[Array], Array]]:
    """
    Make sure that Array is not broken into multiple chunks along axis.

    Returns
    -------
    x : Array
        The input Array with a single chunk along axis.
    restore : Callable[Array, Array]
        function to apply to the output to rechunk it back into reasonable chunks
    """
    if axis < 0:
        axis += x.ndim
    if x.numblocks[axis] < 2:
        return x, lambda x: x

    # Break chunks on other axes in an attempt to keep chunk size low
    x = x.rechunk({i: -1 if i == axis else "auto" for i in range(x.ndim)})

    # Rather than reconstructing the original chunks, which can be a
    # very expensive affair, just break down oversized chunks without
    # incurring in any transfers over the network.
    # This has the downside of a risk of overchunking if the array is
    # then used in operations against other arrays that match the
    # original chunking pattern.
    return x, lambda x: x.rechunk()

def sort(
    x: Array, /, *, axis: int = -1, descending: bool = False, stable: bool = True
) -> Array:
    """
    Array API compatibility layer around the lack of sort() in Dask.

    Warnings
    --------
    This function temporarily rechunks the array along `axis` to a single chunk.
    This can be extremely inefficient and can lead to out-of-memory errors.

    """
    x, restore = _ensure_single_chunk(x, axis)

    meta_xp = array_namespace(x._meta)
    x = da.map_blocks(
        meta_xp.sort,
        x,
        axis=axis,
        meta=x._meta,
        dtype=x.dtype,
        descending=descending,
        stable=stable,
    )

    return restore(x)

) -> Array:
    """
    Array API compatibility layer around the lack of argsort() in Dask.

    Warnings
    --------
    This function temporarily rechunks the array along `axis` into a single chunk.
    """

    dtype = meta_xp.argsort(x._meta).dtype
    meta = meta_xp.astype(x._meta, dtype)
        meta_xp.argsort,
        x,
        meta=meta,
    )

    x: Array,
) -> Array:
   result = da.count_nonzero(x, axis)
            return da.reshape(result, [1]*x.ndim)
       return da.expand_dims(result, axis)

def _delegate(xp: ModuleType, *backends: Backend) -> bool:
    """
    Check whether `xp` is one of the `backends` to delegate to.

    xp : array_namespace
        Array namespace to check.
    *backends : IsNamespace
        Arbitrarily many backends (from the ``IsNamespace`` enum) to check.

    Returns
    -------
    bool
        ``True`` if `xp` matches one of the `backends`, ``False`` otherwise.
    """
    return any(backend.is_namespace(xp) for backend in backends)

def isclose(
    a: Array | complex,
    b: Array | complex,
    *,
    rtol: float = 1e-05,
    atol: float = 1e-08,
    equal_nan: bool = False,
    xp: ModuleType | None = None,
) -> Array:
    """
    Return a boolean array where two arrays are element-wise equal within a tolerance.

    The tolerance values are positive, typically very small numbers. The relative
    difference ``(rtol * abs(b))`` and the absolute difference `atol` are added together
    to compare against the absolute difference between `a` and `b`.

    NaNs are treated as equal if they are in the same place and if ``equal_nan=True``.
    Infs are treated as equal if they are in the same place and of the same sign in both
    arrays.

    a, b : Array | int | float | complex | bool
        Input objects to compare. At least one must be an array.
    rtol : array_like, optional
        The relative tolerance parameter (see Notes).
    atol : array_like, optional
        The absolute tolerance parameter (see Notes).
    equal_nan : bool, optional
        Whether to compare NaN's as equal. If True, NaN's in `a` will be considered
        equal to NaN's in `b` in the output array.
    xp : array_namespace, optional
        The standard-compatible namespace for `a` and `b`. Default: infer.

    Returns
    -------
    Array
        A boolean array of shape broadcasted from `a` and `b`, containing ``True`` where
        `a` is close to `b`, and ``False`` otherwise.

    Warnings
    --------
    The default `atol` is not appropriate for comparing numbers with magnitudes much
    smaller than one (see notes).

    See Also
    --------
    math.isclose : Similar function in stdlib for Python scalars.

    Notes
    -----
    For finite values, `isclose` uses the following equation to test whether two
    floating point values are equivalent::

        absolute(a - b) <= (atol + rtol * absolute(b))

    Unlike the built-in `math.isclose`,
    the above equation is not symmetric in `a` and `b`,
    so that ``isclose(a, b)`` might be different from ``isclose(b, a)`` in some rare
    cases.

    The default value of `atol` is not appropriate when the reference value `b` has
    magnitude smaller than one. For example, it is unlikely that ``a = 1e-9`` and
    ``b = 2e-9`` should be considered "close", yet ``isclose(1e-9, 2e-9)`` is ``True``
    with default settings. Be sure to select `atol` for the use case at hand, especially
    for defining the threshold below which a non-zero value in `a` will be considered
    "close" to a very small or zero value in `b`.

    The comparison of `a` and `b` uses standard broadcasting, which means that `a` and
    `b` need not have the same shape in order for ``isclose(a, b)`` to evaluate to
    ``True``.

    `isclose` is not defined for non-numeric data types.
    ``bool`` is considered a numeric data-type for this purpose.
    """
    xp = array_namespace(a, b) if xp is None else xp

    if _delegate(xp, Backend.NUMPY, Backend.CUPY, Backend.DASK, Backend.JAX):
        return xp.isclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan)

    if _delegate(xp, Backend.TORCH):
        a, b = asarrays(a, b, xp=xp)  # Array API 2024.12 support

    return _funcs.isclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan, xp=xp)

def pad(
    x: Array,
    pad_width: int | tuple[int, int] | Sequence[tuple[int, int]],
    mode: Literal["constant"] = "constant",
    *,
    constant_values: complex = 0,
) -> Array:
    """
    Pad the input array.

    x : array
    pad_width : int or tuple of ints or sequence of pairs of ints
        Pad the input array with this many elements from each side.
        If a sequence of tuples, ``[(before_0, after_0), ... (before_N, after_N)]``,
        each pair applies to the corresponding axis of ``x``.
        A single tuple, ``(before, after)``, is equivalent to a list of ``x.ndim``
        copies of this tuple.
    mode : str, optional
        Only "constant" mode is currently supported, which pads with
        the value passed to `constant_values`.
    constant_values : python scalar, optional
        Use this value to pad the input. Default is zero.
        The standard-compatible namespace for `x`. Default: infer.

    Returns
    -------
    array
        The input array,
        padded with ``pad_width`` elements equal to ``constant_values``.
    """
    xp = array_namespace(x) if xp is None else xp

    if mode != "constant":
        msg = "Only `'constant'` mode is currently supported"
        raise NotImplementedError(msg)

    # https://github.com/pytorch/pytorch/blob/cf76c05b4dc629ac989d1fb8e789d4fac04a095a/torch/_numpy/_funcs_impl.py#L2045-L2056
        pad_width = xp.asarray(pad_width)
        pad_width = xp.broadcast_to(pad_width, (x.ndim, 2))
        pad_width = xp.flip(pad_width, axis=(0,)).flatten()
        return xp.nn.functional.pad(x, tuple(pad_width), value=constant_values)  # type: ignore[arg-type]  # pyright: ignore[reportArgumentType]

    if _delegate(xp, Backend.NUMPY, Backend.JAX, Backend.CUPY, Backend.SPARSE):
        return xp.pad(x, pad_width, mode, constant_values=constant_values)

    return _funcs.pad(x, pad_width, constant_values=constant_values, xp=xp)

def lazy_xp_function(  # type: ignore[explicit-any]
    func: Callable[..., Any],
    *,
    allow_dask_compute: int = 0,
    jax_jit: bool = True,
    static_argnums: int | Sequence[int] | None = None,
    static_argnames: str | Iterable[str] | None = None,
) -> None:  # numpydoc ignore=GL07
    """
    Tag a function to be tested on lazy backends.

    Tag a function so that when any tests are executed with ``xp=jax.numpy`` the
    function is replaced with a jitted version of itself, and when it is executed with
    ``xp=dask.array`` the function will raise if it attempts to materialize the graph.
    This will be later expanded to provide test coverage for other lazy backends.

    In order for the tag to be effective, the test or a fixture must call
    :func:`patch_lazy_xp_functions`.

    func : callable
        Function to be tested.
    allow_dask_compute : int, optional
        Number of times `func` is allowed to internally materialize the Dask graph. This
        is typically triggered by ``bool()``, ``float()``, or ``np.asarray()``.

        Set to 1 if you are aware that `func` converts the input parameters to NumPy and
        want to let it do so at least for the time being, knowing that it is going to be
        extremely detrimental for performance.

        If a test needs values higher than 1 to pass, it is a canary that the conversion
        to NumPy/bool/float is happening multiple times, which translates to multiple
        computations of the whole graph. Short of making the function fully lazy, you
        should at least add explicit calls to ``np.asarray()`` early in the function.
        *Note:* the counter of `allow_dask_compute` resets after each call to `func`, so
        a test function that invokes `func` multiple times should still work with this
        parameter set to 1.

        Default: 0, meaning that `func` must be fully lazy and never materialize the
        graph.
    jax_jit : bool, optional
        Set to True to replace `func` with ``jax.jit(func)`` after calling the
        :func:`patch_lazy_xp_functions` test helper with ``xp=jax.numpy``. Set to False
        if `func` is only compatible with eager (non-jitted) JAX. Default: True.
    static_argnums : int | Sequence[int], optional
        Passed to jax.jit. Positional arguments to treat as static (compile-time
        constant). Default: infer from `static_argnames` using
        `inspect.signature(func)`.
    static_argnames : str | Iterable[str], optional
        Passed to jax.jit. Named arguments to treat as static (compile-time constant).
        Default: infer from `static_argnums` using `inspect.signature(func)`.

    See Also
    --------
    patch_lazy_xp_functions : Companion function to call from the test or fixture.
    jax.jit : JAX function to compile a function for performance.

    Examples
    --------
    In ``test_mymodule.py``::

      from array_api_extra.testing import lazy_xp_function from mymodule import myfunc

      lazy_xp_function(myfunc)

      def test_myfunc(xp):
          a = xp.asarray([1, 2])
          # When xp=jax.numpy, this is the same as `b = jax.jit(myfunc)(a)`
          # When xp=dask.array, crash on compute() or persist()
          b = myfunc(a)

    Notes
    -----
    In order for this tag to be effective, the test function must be imported into the
    test module globals without its namespace; alternatively its namespace must be
    declared in a ``lazy_xp_modules`` list in the test module globals.

    Example 1::

      from mymodule import myfunc

          x = myfunc(xp.asarray([1, 2]))

    Example 2::

      import mymodule

      lazy_xp_modules = [mymodule]
      lazy_xp_function(mymodule.myfunc)

          x = mymodule.myfunc(xp.asarray([1, 2]))

    A test function can circumvent this monkey-patching system by using a namespace
    outside of the two above patterns. You need to sanitize your code to make sure this
    only happens intentionally.

          b = myfunc(a)  # This is wrapped when xp=jax.numpy or xp=dask.array
          c = mymodule.myfunc(a)  # This is not

      class naked:
          myfunc = mymodule.myfunc

          b = mymodule.myfunc(a)  # This is wrapped when xp=jax.numpy or xp=dask.array
          c = naked.myfunc(a)  # This is not
    """
    tags = {
        "allow_dask_compute": allow_dask_compute,
        "jax_jit": jax_jit,
        "static_argnums": static_argnums,
        "static_argnames": static_argnames,
    }
    try:
        func._lazy_xp_function = tags  # type: ignore[attr-defined]  # pylint: disable=protected-access  # pyright: ignore[reportFunctionMemberAccess]
    except AttributeError:  # @cython.vectorize
        _ufuncs_tags[func] = tags

def patch_lazy_xp_functions(
    request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch, *, xp: ModuleType
) -> None:
    """
    Test lazy execution of functions tagged with :func:`lazy_xp_function`.

    If ``xp==jax.numpy``, search for all functions which have been tagged with
    :func:`lazy_xp_function` in the globals of the module that defines the current test,
    as well as in the ``lazy_xp_modules`` list in the globals of the same module,
    and wrap them with :func:`jax.jit`. Unwrap them at the end of the test.

    If ``xp==dask.array``, wrap the functions with a decorator that disables
    ``compute()`` and ``persist()`` and ensures that exceptions and warnings are raised
    eagerly.

    This function should be typically called by your library's `xp` fixture that runs
    tests on multiple backends::

        @pytest.fixture(params=[numpy, array_api_strict, jax.numpy, dask.array])
        def xp(request, monkeypatch):
            patch_lazy_xp_functions(request, monkeypatch, xp=request.param)
            return request.param

    but it can be otherwise be called by the test itself too.

    request : pytest.FixtureRequest
        Pytest fixture, as acquired by the test itself or by one of its fixtures.
    monkeypatch : pytest.MonkeyPatch
        Array namespace to be tested.

    See Also
    --------
    lazy_xp_function : Tag a function to be tested on lazy backends.
    pytest.FixtureRequest : `request` test function parameter.
    """
    mod = cast(ModuleType, request.module)
    mods = [mod, *cast(list[ModuleType], getattr(mod, "lazy_xp_modules", []))]

    def iter_tagged() -> (  # type: ignore[explicit-any]
        Iterator[tuple[ModuleType, str, Callable[..., Any], dict[str, Any]]]
    ):
        for mod in mods:
            for name, func in mod.__dict__.items():
                tags: dict[str, Any] | None = None  # type: ignore[explicit-any]
                with contextlib.suppress(AttributeError):
                    tags = func._lazy_xp_function  # pylint: disable=protected-access
                if tags is None:
                    with contextlib.suppress(KeyError, TypeError):
                        tags = _ufuncs_tags[func]
                if tags is not None:
                    yield mod, name, func, tags

    if is_dask_namespace(xp):
        for mod, name, func, tags in iter_tagged():
            n = tags["allow_dask_compute"]
            wrapped = _dask_wrap(func, n)
            monkeypatch.setattr(mod, name, wrapped)

    elif is_jax_namespace(xp):
        import jax

            if tags["jax_jit"]:
                # suppress unused-ignore to run mypy in -e lint as well as -e dev
                wrapped = cast(  # type: ignore[explicit-any]
                    Callable[..., Any],
                    jax.jit(
                        func,
                        static_argnums=tags["static_argnums"],
                        static_argnames=tags["static_argnames"],
                    ),
                )
                monkeypatch.setattr(mod, name, wrapped)

class CountingDaskScheduler(SchedulerGetCallable):
    """
    Dask scheduler that counts how many times `dask.compute` is called.

    If the number of times exceeds 'max_count', it raises an error.
    This is a wrapper around Dask's own 'synchronous' scheduler.

    max_count : int
        Maximum number of allowed calls to `dask.compute`.
    msg : str
        Assertion to raise when the count exceeds `max_count`.
    """

    count: int
    max_count: int
    msg: str

    def __init__(self, max_count: int, msg: str):  # numpydoc ignore=GL08
        self.max_count = max_count

    @override
    def __call__(self, dsk: Graph, keys: Sequence[Key] | Key, **kwargs: Any) -> Any:  # type: ignore[decorated-any,explicit-any] # numpydoc ignore=GL08
        import dask

        # This should yield a nice traceback to the
        # offending line in the user's code
        assert self.count <= self.max_count, self.msg

        return dask.get(dsk, keys, **kwargs)  # type: ignore[attr-defined,no-untyped-call] # pyright: ignore[reportPrivateImportUsage]

def _dask_wrap(
    func: Callable[P, T], n: int
) -> Callable[P, T]:  # numpydoc ignore=PR01,RT01
    """
    Wrap `func` to raise if it attempts to call `dask.compute` more than `n` times.

    After the function returns, materialize the graph in order to re-raise exceptions.
    """
    import dask

    func_name = getattr(func, "__name__", str(func))
    n_str = f"only up to {n}" if n else "no"
    msg = (
        f"Called `dask.compute()` or `dask.persist()` {n + 1} times, "
        f"but {n_str} calls are allowed. Set "
        f"`lazy_xp_function({func_name}, allow_dask_compute={n + 1})` "
        "to allow for more (but note that this will harm performance). "
    )

    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:  # numpydoc ignore=GL08
        scheduler = CountingDaskScheduler(n, msg)
        with dask.config.set({"scheduler": scheduler}):  # pyright: ignore[reportPrivateImportUsage]
            out = func(*args, **kwargs)

        # Block until the graph materializes and reraise exceptions. This allows
        # `pytest.raises` and `pytest.warns` to work as expected. Note that this would
        # not work on scheduler='distributed', as it would not block.
        return dask.persist(out, scheduler="threads")[0]  # type: ignore[attr-defined,no-untyped-call,func-returns-value,index]  # pyright: ignore[reportPrivateImportUsage]

    return wrapper

    ):

class _AtOp(Enum):
    """Operations for use in `xpx.at`."""

    SET = "set"
    ADD = "add"
    SUBTRACT = "subtract"
    MULTIPLY = "multiply"
    DIVIDE = "divide"
    POWER = "power"
    MIN = "min"
    MAX = "max"

    # @override from Python 3.12
    def __str__(self) -> str:  # type: ignore[explicit-override]  # pyright: ignore[reportImplicitOverride]
        """
        Return string representation (useful for pytest logs).

        Returns
        -------
        str
            The operation's name.
        """
        return self.value

class Undef(Enum):
    """Sentinel for undefined values."""

    UNDEF = 0

class at:  # pylint: disable=invalid-name  # numpydoc ignore=PR02
    """
    Update operations for read-only arrays.

    This implements ``jax.numpy.ndarray.at`` for all writeable
    backends (those that support ``__setitem__``) and routes
    to the ``.at[]`` method for JAX arrays.

    x : array
    idx : index, optional
        Only `array API standard compliant indices
        <https://data-apis.org/array-api/latest/API_specification/indexing.html>`_
        are supported.

        You may use two alternate syntaxes::

          >>> import array_api_extra as xpx
          >>> xpx.at(x, idx).set(value)  # or add(value), etc.
          >>> xpx.at(x)[idx].set(value)

    copy : bool, optional
        None (default)
            The array parameter *may* be modified in place if it is
            possible and beneficial for performance.
            You should not reuse it after calling this function.
        True
            Ensure that the inputs are not modified.
        False
            Ensure that the update operation writes back to the input.
            Raise ``ValueError`` if a copy cannot be avoided.

    Returns
    -------
    Updated input array.

    Warnings
    --------
    (a) When you omit the ``copy`` parameter, you should never reuse the parameter
    array later on; ideally, you should reassign it immediately::

        >>> import array_api_extra as xpx
        >>> x = xpx.at(x, 0).set(2)

    The above best practice pattern ensures that the behaviour won't change depending
    on whether ``x`` is writeable or not, as the original ``x`` object is dereferenced
    as soon as ``xpx.at`` returns; this way there is no risk to accidentally update it
    twice.

    On the reverse, the anti-pattern below must be avoided, as it will result in
    different behaviour on read-only versus writeable arrays::

        >>> x = xp.asarray([0, 0, 0])
        >>> y = xpx.at(x, 0).set(2)
        >>> z = xpx.at(x, 1).set(3)

    In the above example, both calls to ``xpx.at`` update ``x`` in place *if possible*.
    This causes the behaviour to diverge depending on whether ``x`` is writeable or not:

    - If ``x`` is writeable, then after the snippet above you'll have
      ``x == y == z == [2, 3, 0]``
    - If ``x`` is read-only, then you'll end up with
      ``x == [0, 0, 0]``, ``y == [2, 0, 0]`` and ``z == [0, 3, 0]``.

    The correct pattern to use if you want diverging outputs from the same input is
    to enforce copies::

        >>> y = xpx.at(x, 0).set(2, copy=True)  # Never updates x
        >>> z = xpx.at(x, 1).set(3)  # May or may not update x in place
        >>> del x  # avoid accidental reuse of x as we don't know its state anymore

    (b) The array API standard does not support integer array indices.
    The behaviour of update methods when the index is an array of integers is
    undefined and will vary between backends; this is particularly true when the
    index contains multiple occurrences of the same index, e.g.::

        >>> import numpy as np
        >>> import jax.numpy as jnp
        >>> xpx.at(np.asarray([123]), np.asarray([0, 0])).add(1)
        array([124])
        >>> xpx.at(jnp.asarray([123]), jnp.asarray([0, 0])).add(1)
        Array([125], dtype=int32)

    See Also
    --------
    jax.numpy.ndarray.at : Equivalent array method in JAX.

    Notes
    -----
    `sparse <https://sparse.pydata.org/>`_, as well as read-only arrays from libraries
    not explicitly covered by ``array-api-compat``, are not supported by update
    methods.

    Boolean masks are supported on Dask and jitted JAX arrays exclusively
    when `idx` has the same shape as `x` and `y` is 0-dimensional.
    Note that this support is not available in JAX's native
    ``x.at[mask].set(y)``.

    This pattern::

        >>> mask = m(x)
        >>> x[mask] = f(x[mask])

    Can't be replaced by `at`, as it won't work on Dask and JAX inside jax.jit::

        >>> x = xpx.at(x, mask).set(f(x[mask])  # Crash on Dask and jax.jit

    You should instead use::

        >>> x = xp.where(m(x), f(x), x)

    Examples
    --------
    Given either of these equivalent expressions::

      >>> import array_api_extra as xpx
      >>> x = xpx.at(x)[1].add(2)
      >>> x = xpx.at(x, 1).add(2)

    If x is a JAX array, they are the same as::

      >>> x = x.at[1].add(2)

    If x is a read-only NumPy array, they are the same as::

      >>> x = x.copy()
      >>> x[1] += 2

    For other known backends, they are the same as::

    """

    _x: Array
    _idx: SetIndex | Undef
    __slots__: ClassVar[tuple[str, ...]] = ("_idx", "_x")

        self, x: Array, idx: SetIndex | Undef = _undef, /
    ) -> None:  # numpydoc ignore=GL08
        self._x = x
        self._idx = idx

    def __getitem__(self, idx: SetIndex, /) -> Self:  # numpydoc ignore=PR01,RT01
        """
        Allow for the alternate syntax ``at(x)[start:stop:step]``.

        It looks prettier than ``at(x, slice(start, stop, step))``
        and feels more intuitive coming from the JAX documentation.
        """
        if self._idx is not _undef:
            msg = "Index has already been set"
        return type(self)(self._x, idx)

    def _op(
        self,
        at_op: _AtOp,
        in_place_op: Callable[[Array, Array | complex], Array] | None,
        out_of_place_op: Callable[[Array, Array], Array] | None,
        y: Array | complex,
        /,
        copy: bool | None,
        xp: ModuleType | None,
    ) -> Array:
        """
        Implement all update operations.

        at_op : _AtOp
            Method of JAX's Array.at[].
        in_place_op : Callable[[Array, Array | complex], Array] | None
            In-place operation to apply on mutable backends::

                x[idx] = in_place_op(x[idx], y)

            If None::

                x[idx] = y

        out_of_place_op : Callable[[Array, Array], Array] | None
            Out-of-place operation to apply when idx is a boolean mask and the backend
            doesn't support in-place updates::

                x = xp.where(idx, out_of_place_op(x, y), x)

            If None::

                x = xp.where(idx, y, x)

        y : array or complex
            Right-hand side of the operation.
        copy : bool or None
            Whether to copy the input array. See the class docstring for details.
        xp : array_namespace, optional
            The array namespace for the input array. Default: infer.

        Returns
        -------
        Array
            Updated `x`.
        """
        from ._funcs import apply_where  # pylint: disable=cyclic-import

        x, idx = self._x, self._idx
        xp = array_namespace(x, y) if xp is None else xp

        if isinstance(idx, Undef):
            msg = (
                "Index has not been set.\n"
                "Usage: either\n"
                "    at(x, idx).set(value)\n"
                "or\n"
                "    at(x)[idx].set(value)\n"
                "(same for all other methods)."
            )

        if copy not in (True, False, None):
            msg = f"copy must be True, False, or None; got {copy!r}"

        writeable = None if copy else is_writeable_array(x)

        # JAX inside jax.jit doesn't support in-place updates with boolean
        # masks; Dask exclusively supports __setitem__ but not iops.
        # We can handle the common special case of 0-dimensional y
        # with where(idx, y, x) instead.
        if (
            (is_dask_array(idx) or is_jax_array(idx))
            and idx.dtype == xp.bool
            and idx.shape == x.shape
        ):
            y_xp = xp.asarray(y, dtype=x.dtype)
            if y_xp.ndim == 0:
                if out_of_place_op:  # add(), subtract(), ...
                    # suppress inf warnings on Dask
                    out = apply_where(
                        idx, (x, y_xp), out_of_place_op, fill_value=x, xp=xp
                    )
                    # Undo int->float promotion on JAX after _AtOp.DIVIDE
                    out = xp.astype(out, x.dtype, copy=False)
                else:  # set()
                    out = xp.where(idx, y_xp, x)

                if copy is False:
                    x[()] = out
                    return x
                return out

            # else: this will work on eager JAX and crash on jax.jit and Dask

        if copy or (copy is None and not writeable):
            if is_jax_array(x):
                # Use JAX's at[]
                func = cast(
                    Callable[[Array | complex], Array],
                    getattr(x.at[idx], at_op.value),  # type: ignore[attr-defined]  # pyright: ignore[reportAttributeAccessIssue,reportUnknownArgumentType]
                )
                out = func(y)
                # Undo int->float promotion on JAX after _AtOp.DIVIDE
                return xp.astype(out, x.dtype, copy=False)

            # Emulate at[] behaviour for non-JAX arrays
            # with a copy followed by an update

            x = xp.asarray(x, copy=True)
            # A copy of a read-only numpy array is writeable
            # Note: this assumes that a copy of a writeable array is writeable
            assert not writeable
            writeable = None

        if writeable is None:
            writeable = is_writeable_array(x)
        if not writeable:
            # sparse crashes here
            msg = f"Can't update read-only array {x}"

        if in_place_op:  # add(), subtract(), ...
            x[idx] = in_place_op(x[idx], y)
        else:  # set()
            x[idx] = y
        return x

    def set(
        self,
        /,
        copy: bool | None = None,
        xp: ModuleType | None = None,
    ) -> Array:  # numpydoc ignore=PR01,RT01
        """Apply ``x[idx] = y`` and return the update array."""
        return self._op(_AtOp.SET, None, None, y, copy=copy, xp=xp)

    def add(
        self,
        /,
        """Apply ``x[idx] += y`` and return the updated array."""

        # Note for this and all other methods based on _iop:
        # operator.iadd and operator.add subtly differ in behaviour, as
        # only iadd will trigger exceptions when y has an incompatible dtype.
        return self._op(_AtOp.ADD, operator.iadd, operator.add, y, copy=copy, xp=xp)

    def subtract(
        self,
        /,
        """Apply ``x[idx] -= y`` and return the updated array."""
        return self._op(
            _AtOp.SUBTRACT, operator.isub, operator.sub, y, copy=copy, xp=xp
        )

    def multiply(
        self,
        /,
        """Apply ``x[idx] *= y`` and return the updated array."""
            _AtOp.MULTIPLY, operator.imul, operator.mul, y, copy=copy, xp=xp
        )

    def divide(
        self,
        /,
        """Apply ``x[idx] /= y`` and return the updated array."""
            _AtOp.DIVIDE, operator.itruediv, operator.truediv, y, copy=copy, xp=xp
        )

    def power(
        self,
        /,
        """Apply ``x[idx] **= y`` and return the updated array."""
        return self._op(_AtOp.POWER, operator.ipow, operator.pow, y, copy=copy, xp=xp)

    def min(
        self,
        /,
        """Apply ``x[idx] = minimum(x[idx], y)`` and return the updated array."""
        # On Dask, this function runs on the chunks, so we need to determine the
        # namespace that Dask is wrapping.
        # Note that da.minimum _incidentally_ works on NumPy, CuPy, and sparse
        # thanks to all these meta-namespaces implementing the __array_ufunc__
        # interface, but there's no guarantee that it will work for other
        # wrapped libraries in the future.
        xp = array_namespace(self._x) if xp is None else xp
        mxp = meta_namespace(self._x, xp=xp)
        y = xp.asarray(y)
        return self._op(_AtOp.MIN, mxp.minimum, mxp.minimum, y, copy=copy, xp=xp)

    def max(
        self,
        /,
        """Apply ``x[idx] = maximum(x[idx], y)`` and return the updated array."""
        # See note on min()
        return self._op(_AtOp.MAX, mxp.maximum, mxp.maximum, y, copy=copy, xp=xp)

        """

        Returns
        -------
        str
        """

        self._x = x

        """

        """

    def _op(
        self,
        /,
    ) -> Array:
        """

            If None::

                x[idx] = y

            If None::

        Returns
        -------
        Array
        """

            msg = (
                "or\n"
            )

        if (
        ):
                    )

                    x[()] = out
                    return x
                return out

                )

            x[idx] = y
        return x

    def set(
        self,
        /,

    def add(
        self,
        /,

        self,
        /,
        )

        self,
        /,
        )

        self,
        /,
        )

    def power(
        self,
        /,

    def min(
        self,
        /,

    def max(
        self,
        /,

class Backend(Enum):  # numpydoc ignore=PR01,PR02  # type: ignore[no-subclass-any]
    """
    All array library backends explicitly tested by array-api-extra.

    value : str
        Name of the backend's module.
    is_namespace : Callable[[ModuleType], bool]
        Function to check whether an input module is the array namespace
        corresponding to the backend.
    """

    ARRAY_API_STRICT = "array_api_strict", _compat.is_array_api_strict_namespace
    NUMPY = "numpy", _compat.is_numpy_namespace
    NUMPY_READONLY = "numpy_readonly", _compat.is_numpy_namespace
    CUPY = "cupy", _compat.is_cupy_namespace
    TORCH = "torch", _compat.is_torch_namespace
    DASK = "dask.array", _compat.is_dask_namespace
    SPARSE = "sparse", _compat.is_pydata_sparse_namespace
    JAX = "jax.numpy", _compat.is_jax_namespace

    def __new__(
        cls, value: str, _is_namespace: Callable[[ModuleType], bool]
    ):  # numpydoc ignore=GL08
        obj = object.__new__(cls)
        obj._value_ = value
        return obj

        self,
        value: str,  # noqa: ARG002  # pylint: disable=unused-argument
        is_namespace: Callable[[ModuleType], bool],
        self.is_namespace = is_namespace

    def __str__(self) -> str:  # type: ignore[explicit-override]  # pyright: ignore[reportImplicitOverride]  # numpydoc ignore=RT01
        """Pretty-print parameterized test names."""
        return cast(str, self.value)

        return obj

        self,

def lazy_apply(  # type: ignore[decorated-any, valid-type]
    func: Callable[P, Array | ArrayLike],
    *args: Array | complex | None,
    shape: tuple[int | None, ...] | None = None,
    dtype: DType | None = None,
    as_numpy: bool = False,
    **kwargs: P.kwargs,  # pyright: ignore[reportGeneralTypeIssues]
) -> Array: ...  # numpydoc ignore=GL08

    func: Callable[P, Sequence[Array | ArrayLike]],
    shape: Sequence[tuple[int | None, ...]],
    dtype: Sequence[DType] | None = None,
) -> tuple[Array, ...]: ...  # numpydoc ignore=GL08

def lazy_apply(  # type: ignore[valid-type]  # numpydoc ignore=GL07,SA04
    func: Callable[P, Array | ArrayLike | Sequence[Array | ArrayLike]],
    shape: tuple[int | None, ...] | Sequence[tuple[int | None, ...]] | None = None,
    dtype: DType | Sequence[DType] | None = None,
) -> Array | tuple[Array, ...]:
    """
    Lazily apply an eager function.

    If the backend of the input arrays is lazy, e.g. Dask or jitted JAX, the execution
    of the function is delayed until the graph is materialized; if it's eager, the
    function is executed immediately.

        The function to apply.

        It must accept one or more array API compliant arrays as positional arguments.
        If `as_numpy=True`, inputs are converted to NumPy before they are passed to
        `func`.
        It must return either a single array-like or a sequence of array-likes.

        `func` must be a pure function, i.e. without side effects, as depending on the
        backend it may be executed more than once or never.
    *args : Array | int | float | complex | bool | None
        One or more Array API compliant arrays, Python scalars, or None's.

        If `as_numpy=True`, you need to be able to apply :func:`numpy.asarray` to
        non-None args to convert them to NumPy; read notes below about specific
        backends.
    shape : tuple[int | None, ...] | Sequence[tuple[int | None, ...]], optional
        Output shape or sequence of output shapes, one for each output of `func`.
        Default: assume single output and broadcast shapes of the input arrays.
    dtype : DType | Sequence[DType], optional
        Output dtype or sequence of output dtypes, one for each output of `func`.
        dtype(s) must belong to the same array namespace as the input arrays.
        Default: infer the result type(s) from the input arrays.
    as_numpy : bool, optional
        If True, convert the input arrays to NumPy before passing them to `func`.
        This is particularly useful to make NumPy-only functions, e.g. written in Cython
       or Numba, work transparently with array API-compliant arrays.
        Default: False.
        The standard-compatible namespace for `args`. Default: infer.
    **kwargs : Any, optional
        Additional keyword arguments to pass verbatim to `func`.
        They cannot contain Array objects.

    Returns
    -------
    Array | tuple[Array, ...]
        The result(s) of `func` applied to the input arrays, wrapped in the same
        array namespace as the inputs.
        If shape is omitted or a single `tuple[int | None, ...]`, return a single array.
        Otherwise, return a tuple of arrays.

    Notes
    -----
    JAX
        This allows applying eager functions to jitted JAX arrays, which are lazy.
        The function won't be applied until the JAX array is materialized.
        When running inside ``jax.jit``, `shape` must be fully known, i.e. it cannot
        contain any `None` elements.

        .. warning::

            `func` must never raise inside ``jax.jit``, as the resulting behavior is
            undefined.

        Using this with `as_numpy=False` is particularly useful to apply non-jittable
        JAX functions to arrays on GPU devices.
        If ``as_numpy=True``, the :doc:`jax:transfer_guard` may prevent arrays on a GPU
        device from being transferred back to CPU. This is treated as an implicit
        transfer.

    PyTorch, CuPy
        If ``as_numpy=True``, these backends raise by default if you attempt to convert
        arrays on a GPU device to NumPy.

    Sparse
        If ``as_numpy=True``, by default sparse prevents implicit densification through
        :func:`numpy.asarray`. `This safety mechanism can be disabled
        <https://sparse.pydata.org/en/stable/operations.html#package-configuration>`_.

    Dask
        This allows applying eager functions to Dask arrays.
        The Dask graph won't be computed.

        `lazy_apply` doesn't know if `func` reduces along any axes; also, shape
        changes are non-trivial in chunked Dask arrays. For these reasons, all inputs
        will be rechunked into a single chunk.

           The whole operation needs to fit in memory all at once on a single worker.

        The outputs will also be returned as a single chunk and you should consider
        rechunking them into smaller chunks afterwards.

        If you want to distribute the calculation across multiple workers, you
        should use :func:`dask.array.map_blocks`, :func:`dask.array.map_overlap`,
        :func:`dask.array.blockwise`, or a native Dask wrapper instead of
        `lazy_apply`.

    Dask wrapping around other backends
        If ``as_numpy=False``, `func` will receive in input eager arrays of the meta
        namespace, as defined by the ``._meta`` attribute of the input Dask arrays.
        The outputs of `func` will be wrapped by the meta namespace, and then wrapped
        again by Dask.

    Raises
    ------
    ValueError
        When ``xp=jax.numpy``, the output `shape` is unknown (it contains ``None`` on
        one or more axes) and this function was called inside ``jax.jit``.
    RuntimeError
        When ``xp=sparse`` and auto-densification is disabled.
    Exception (backend-specific)
        When the backend disallows implicit device to host transfers and the input
        arrays are on a non-CPU device, e.g. on GPU.

    See Also
    --------
    jax.transfer_guard
    jax.pure_callback
    dask.array.map_blocks
    dask.array.map_overlap
    dask.array.blockwise
    """
    args_not_none = [arg for arg in args if arg is not None]
    array_args = [arg for arg in args_not_none if not is_python_scalar(arg)]
    if not array_args:
        msg = "Must have at least one argument array"
        raise ValueError(msg)
    if xp is None:
        xp = array_namespace(*args)

    # Normalize and validate shape and dtype
    shapes: list[tuple[int | None, ...]]
    dtypes: list[DType]
    multi_output = False

    if shape is None:
        shapes = [broadcast_shapes(*(arg.shape for arg in array_args))]
    elif all(isinstance(s, int | None) for s in shape):
        # Do not test for shape to be a tuple
        # https://github.com/data-apis/array-api/issues/891#issuecomment-2637430522
        shapes = [cast(tuple[int | None, ...], shape)]
    else:
        shapes = list(shape)  # type: ignore[arg-type]  # pyright: ignore[reportAssignmentType]
        multi_output = True

    if dtype is None:
        dtypes = [xp.result_type(*args_not_none)] * len(shapes)
    elif multi_output:
        if not isinstance(dtype, Sequence):
            msg = "Got multiple shapes but only one dtype"
        dtypes = list(dtype)  # pyright: ignore[reportUnknownArgumentType]
    else:
        if isinstance(dtype, Sequence):
            msg = "Got single shape but multiple dtypes"

        dtypes = [dtype]

    if len(shapes) != len(dtypes):
        msg = f"Got {len(shapes)} shapes and {len(dtypes)} dtypes"
    del shape
    del dtype
    # End of shape and dtype parsing

    # Backend-specific branches

        metas: list[Array] = [arg._meta for arg in array_args]  # pylint: disable=protected-access    # pyright: ignore[reportAttributeAccessIssue]
        meta_xp = array_namespace(*metas)

        wrapped = dask.delayed(  # type: ignore[attr-defined]  # pyright: ignore[reportPrivateImportUsage]
            _lazy_apply_wrapper(func, as_numpy, multi_output, meta_xp),
            pure=True,
        )
        # This finalizes each arg, which is the same as arg.rechunk(-1).
        # Please read docstring above for why we're not using
        # dask.array.map_blocks or dask.array.blockwise!
        delayed_out = wrapped(*args, **kwargs)

        out = tuple(
            xp.from_delayed(
                delayed_out[i],  # pyright: ignore[reportIndexIssue]
                # Dask's unknown shapes diverge from the Array API specification
                shape=tuple(math.nan if s is None else s for s in shape),
                dtype=dtype,
                meta=metas[0],
            )
            for i, (shape, dtype) in enumerate(zip(shapes, dtypes, strict=True))
        )

    elif is_jax_namespace(xp) and _is_jax_jit_enabled(xp):
        # Delay calling func with jax.pure_callback, which will forward to func eager
        # JAX arrays. Do not use jax.pure_callback when running outside of the JIT,
        # as it does not support raising exceptions:
        # https://github.com/jax-ml/jax/issues/26102
        import jax

        if any(None in shape for shape in shapes):
            msg = "Output shape must be fully known when running inside jax.jit"

        # Shield kwargs from being coerced into JAX arrays.
        # jax.pure_callback calls jax.jit under the hood, but without the chance of
        # passing static_argnames / static_argnums.
        wrapped = _lazy_apply_wrapper(
            partial(func, **kwargs), as_numpy, multi_output, xp
        )

        # suppress unused-ignore to run mypy in -e lint as well as -e dev
        out = cast(  # type: ignore[bad-cast,unused-ignore]
            tuple[Array, ...],
            jax.pure_callback(
                wrapped,
                tuple(
                    jax.ShapeDtypeStruct(shape, dtype)  # pyright: ignore[reportUnknownArgumentType]
                    for shape, dtype in zip(shapes, dtypes, strict=True)
                ),
                *args,
            ),
        )

    else:
        # Eager backends, including non-jitted JAX
        wrapped = _lazy_apply_wrapper(func, as_numpy, multi_output, xp)
        out = wrapped(*args, **kwargs)

    return out if multi_output else out[0]

def _is_jax_jit_enabled(xp: ModuleType) -> bool:  # numpydoc ignore=PR01,RT01
    """Return True if this function is being called inside ``jax.jit``."""
    import jax  # pylint: disable=import-outside-toplevel

    x = xp.asarray(False)
    try:
        return bool(x)
    except jax.errors.TracerBoolConversionError:

def _lazy_apply_wrapper(  # type: ignore[explicit-any]  # numpydoc ignore=PR01,RT01
    func: Callable[..., Array | ArrayLike | Sequence[Array | ArrayLike]],
    as_numpy: bool,
    multi_output: bool,
    xp: ModuleType,
) -> Callable[..., tuple[Array, ...]]:
    """
    Helper of `lazy_apply`.

    Given a function that accepts one or more arrays as positional arguments and returns
    a single array-like or a sequence of array-likes, return a function that accepts the
    same number of Array API arrays and always returns a tuple of Array API array.

    Any keyword arguments are passed through verbatim to the wrapped function.
    """

    # On Dask, @wraps causes the graph key to contain the wrapped function's name
    def wrapper(  # type: ignore[decorated-any,explicit-any]
        *args: Array | complex | None, **kwargs: Any
    ) -> tuple[Array, ...]:  # numpydoc ignore=GL08
        args_list = []
        device = None
            if arg is not None and not is_python_scalar(arg):
                if device is None:
                    device = _compat.device(arg)
                if as_numpy:
                    import numpy as np

                    arg = cast(Array, np.asarray(arg))  # type: ignore[bad-cast]  # noqa: PLW2901
            args_list.append(arg)
        assert device is not None

        out = func(*args_list, **kwargs)

        if multi_output:
            assert isinstance(out, Sequence)
            return tuple(xp.asarray(o, device=device) for o in out)
        return (xp.asarray(out, device=device),)

def _check_ns_shape_dtype(
    actual: Array, desired: Array
) -> ModuleType:  # numpydoc ignore=RT03
    """
    Assert that namespace, shape and dtype of the two arrays match.

    actual : Array
        The array produced by the tested function.
    desired : Array
        The expected array (typically hardcoded).

    Returns
    -------
    Arrays namespace.
    """
    actual_xp = array_namespace(actual)  # Raises on scalars and lists
    desired_xp = array_namespace(desired)

    msg = f"namespaces do not match: {actual_xp} != f{desired_xp}"
    assert actual_xp == desired_xp, msg

    actual_shape = actual.shape
    desired_shape = desired.shape
    if is_dask_namespace(desired_xp):
        # Dask uses nan instead of None for unknown shapes
        if any(math.isnan(i) for i in cast(tuple[float, ...], actual_shape)):
            actual_shape = actual.compute().shape  # type: ignore[attr-defined]  # pyright: ignore[reportAttributeAccessIssue]
        if any(math.isnan(i) for i in cast(tuple[float, ...], desired_shape)):
            desired_shape = desired.compute().shape  # type: ignore[attr-defined]  # pyright: ignore[reportAttributeAccessIssue]

    msg = f"shapes do not match: {actual_shape} != f{desired_shape}"
    assert actual_shape == desired_shape, msg

    msg = f"dtypes do not match: {actual.dtype} != {desired.dtype}"
    assert actual.dtype == desired.dtype, msg

    return desired_xp

def xp_assert_equal(actual: Array, desired: Array, err_msg: str = "") -> None:
    """
    Array-API compatible version of `np.testing.assert_array_equal`.

    err_msg : str, optional
        Error message to display on failure.

    See Also
    --------
    xp_assert_close : Similar function for inexact equality checks.
    numpy.testing.assert_array_equal : Similar function for NumPy arrays.
    """
    xp = _check_ns_shape_dtype(actual, desired)

    if is_cupy_namespace(xp):
        xp.testing.assert_array_equal(actual, desired, err_msg=err_msg)
    elif is_torch_namespace(xp):
        # PyTorch recommends using `rtol=0, atol=0` like this
        # to test for exact equality
        xp.testing.assert_close(
            actual,
            desired,
            rtol=0,
            atol=0,
            equal_nan=True,
            check_dtype=False,
            msg=err_msg or None,
        )
    else:
        import numpy as np  # pylint: disable=import-outside-toplevel

        if is_pydata_sparse_namespace(xp):
            actual = actual.todense()  # type: ignore[attr-defined]  # pyright: ignore[reportAttributeAccessIssue]
            desired = desired.todense()  # type: ignore[attr-defined]  # pyright: ignore[reportAttributeAccessIssue]

        actual_np = None
        desired_np = None
        if is_array_api_strict_namespace(xp):
            # __array__ doesn't work on array-api-strict device arrays
            # We need to convert to the CPU device first
            actual_np = np.asarray(xp.asarray(actual, device=xp.Device("CPU_DEVICE")))
            desired_np = np.asarray(xp.asarray(desired, device=xp.Device("CPU_DEVICE")))

        # JAX/Dask arrays work with `np.testing`
        actual_np = actual if actual_np is None else actual_np
        desired_np = desired if desired_np is None else desired_np
        np.testing.assert_array_equal(actual_np, desired_np, err_msg=err_msg)  # pyright: ignore[reportUnknownArgumentType]

def xp_assert_close(
    actual: Array,
    desired: Array,
    *,
    rtol: float | None = None,
    atol: float = 0,
    err_msg: str = "",
) -> None:
    """
    Array-API compatible version of `np.testing.assert_allclose`.

    rtol : float, optional
        Relative tolerance. Default: dtype-dependent.
    atol : float, optional
        Absolute tolerance. Default: 0.

    See Also
    --------
    xp_assert_equal : Similar function for exact equality checks.
    isclose : Public function for checking closeness.
    numpy.testing.assert_allclose : Similar function for NumPy arrays.

    Notes
    -----
    The default `atol` and `rtol` differ from `xp.all(xpx.isclose(a, b))`.
    """

    floating = xp.isdtype(actual.dtype, ("real floating", "complex floating"))
    if rtol is None and floating:
        # multiplier of 4 is used as for `np.float64` this puts the default `rtol`
        # roughly half way between sqrt(eps) and the default for
        # `numpy.testing.assert_allclose`, 1e-7
        rtol = xp.finfo(actual.dtype).eps ** 0.5 * 4
    elif rtol is None:
        rtol = 1e-7

        xp.testing.assert_allclose(
            actual, desired, rtol=rtol, atol=atol, err_msg=err_msg
        )
            actual, desired, rtol=rtol, atol=atol, equal_nan=True, msg=err_msg or None
        )
    else:

        assert isinstance(rtol, float)
        np.testing.assert_allclose(  # pyright: ignore[reportCallIssue]
            actual_np,  # type: ignore[arg-type]  # pyright: ignore[reportArgumentType]
            desired_np,  # type: ignore[arg-type]  # pyright: ignore[reportArgumentType]
            rtol=rtol,
            atol=atol,
            err_msg=err_msg,
        )

def xfail(request: pytest.FixtureRequest, reason: str) -> None:
    """
    XFAIL the currently running test.

    Unlike ``pytest.xfail``, allow rest of test to execute instead of immediately
    halting it, so that it may result in a XPASS.
    xref https://github.com/pandas-dev/pandas/issues/38902

        ``request`` argument of the test function.
    reason : str
    """
    request.node.add_marker(pytest.mark.xfail(reason=reason))

def apply_where(  # type: ignore[explicit-any,decorated-any] # numpydoc ignore=GL08
    cond: Array,
    args: Array | tuple[Array, ...],
    f1: Callable[..., Array],
    f2: Callable[..., Array],
    /,
    *,
) -> Array: ...

    /,
    *,
    fill_value: Array | complex,

def apply_where(  # type: ignore[explicit-any] # numpydoc ignore=PR01,PR02
    f2: Callable[..., Array] | None = None,
    /,
    *,
    fill_value: Array | complex | None = None,
) -> Array:
    """
    Run one of two elementwise functions depending on a condition.

    Equivalent to ``f1(*args) if cond else fill_value`` performed elementwise
    when `fill_value` is defined, otherwise to ``f1(*args) if cond else f2(*args)``.

    cond : array
        The condition, expressed as a boolean array.
    args : Array or tuple of Arrays
        Argument(s) to `f1` (and `f2`). Must be broadcastable with `cond`.
    f1 : callable
        Elementwise function of `args`, returning a single array.
        Where `cond` is True, output will be ``f1(arg0[cond], arg1[cond], ...)``.
    f2 : callable, optional
        Where `cond` is False, output will be ``f2(arg0[cond], arg1[cond], ...)``.
        Mutually exclusive with `fill_value`.
    fill_value : Array or scalar, optional
        If provided, value with which to fill output array where `cond` is False.
        It does not need to be scalar; it needs however to be broadcastable with
        `cond` and `args`.
        Mutually exclusive with `f2`. You must provide one or the other.
        The standard-compatible namespace for `cond` and `args`. Default: infer.

    Returns
    -------
    Array
        An array with elements from the output of `f1` where `cond` is True and either
        the output of `f2` or `fill_value` where `cond` is False. The returned array has
        data type determined by type promotion rules between the output of `f1` and
        either `fill_value` or the output of `f2`.

    Notes
    -----
    ``xp.where(cond, f1(*args), f2(*args))`` requires explicitly evaluating `f1` even
    when `cond` is False, and `f2` when cond is True. This function evaluates each
    function only for their matching condition, if the backend allows for it.

    On Dask, `f1` and `f2` are applied to the individual chunks and should use functions
    from the namespace of the chunks.

    Examples
    --------
    >>> import array_api_strict as xp
    >>> import array_api_extra as xpx
    >>> a = xp.asarray([5, 4, 3])
    >>> b = xp.asarray([0, 2, 2])
    >>> def f(a, b):
    ...     return a // b
    >>> xpx.apply_where(b != 0, (a, b), f, fill_value=xp.nan)
    array([ nan,  2., 1.])
    """
    # Parse and normalize arguments
    if (f2 is None) == (fill_value is None):
        msg = "Exactly one of `fill_value` or `f2` must be given."
        raise TypeError(msg)
    args_ = list(args) if isinstance(args, tuple) else [args]
    del args

    xp = array_namespace(cond, fill_value, *args_) if xp is None else xp

    if isinstance(fill_value, int | float | complex | NoneType):
        cond, *args_ = xp.broadcast_arrays(cond, *args_)
    else:
        cond, fill_value, *args_ = xp.broadcast_arrays(cond, fill_value, *args_)

        meta_xp = meta_namespace(cond, fill_value, *args_, xp=xp)
        # map_blocks doesn't descend into tuples of Arrays
        return xp.map_blocks(_apply_where, cond, f1, f2, fill_value, *args_, xp=meta_xp)
    return _apply_where(cond, f1, f2, fill_value, *args_, xp=xp)

def _apply_where(  # type: ignore[explicit-any]  # numpydoc ignore=PR01,RT01
    f2: Callable[..., Array] | None,
    fill_value: Array | int | float | complex | bool | None,
    *args: Array,
) -> Array:
    """Helper of `apply_where`. On Dask, this runs on a single chunk."""

    if is_jax_namespace(xp):
        # jax.jit does not support assignment by boolean mask
        return xp.where(cond, f1(*args), f2(*args) if f2 is not None else fill_value)

    temp1 = f1(*(arr[cond] for arr in args))

    if f2 is None:
        dtype = xp.result_type(temp1, fill_value)
        if isinstance(fill_value, int | float | complex):
            out = xp.full_like(cond, dtype=dtype, fill_value=fill_value)
        else:
            out = xp.astype(fill_value, dtype, copy=True)
    else:
        ncond = ~cond
        temp2 = f2(*(arr[ncond] for arr in args))
        dtype = xp.result_type(temp1, temp2)
        out = xp.empty_like(cond, dtype=dtype)
        out = at(out, ncond).set(temp2)

    return at(out, cond).set(temp1)

def atleast_nd(x: Array, /, *, ndim: int, xp: ModuleType | None = None) -> Array:
    """
    Recursively expand the dimension of an array to at least `ndim`.

    x : array
    ndim : int
        The minimum number of dimensions for the result.

    Returns
    -------
    array
        An array with ``res.ndim`` >= `ndim`.
        If ``x.ndim`` >= `ndim`, `x` is returned.
        If ``x.ndim`` < `ndim`, `x` is expanded by prepending new axes
        until ``res.ndim`` equals `ndim`.

    Examples
    --------
    >>> x = xp.asarray([1])
    >>> xpx.atleast_nd(x, ndim=3, xp=xp)
    Array([[[1]]], dtype=array_api_strict.int64)

    >>> x = xp.asarray([[[1, 2],
    ...                  [3, 4]]])
    >>> xpx.atleast_nd(x, ndim=1, xp=xp) is x
    True
    """
        xp = array_namespace(x)

    if x.ndim < ndim:
        x = xp.expand_dims(x, axis=0)
        x = atleast_nd(x, ndim=ndim, xp=xp)
    return x

def broadcast_shapes(*shapes: tuple[float | None, ...]) -> tuple[int | None, ...]:
    """
    Compute the shape of the broadcasted arrays.

    Duplicates :func:`numpy.broadcast_shapes`, with additional support for
    None and NaN sizes.

    This is equivalent to ``xp.broadcast_arrays(arr1, arr2, ...)[0].shape``
    without needing to worry about the backend potentially deep copying
    the arrays.

    *shapes : tuple[int | None, ...]
        Shapes of the arrays to broadcast.

    Returns
    -------
    tuple[int | None, ...]
        The shape of the broadcasted arrays.

    See Also
    --------
    numpy.broadcast_shapes : Equivalent NumPy function.
    array_api.broadcast_arrays : Function to broadcast actual arrays.

    Notes
    -----
    This function accepts the Array API's ``None`` for unknown sizes,
    as well as Dask's non-standard ``math.nan``.
    Regardless of input, the output always contains ``None`` for unknown sizes.

    Examples
    --------
    >>> xpx.broadcast_shapes((2, 3), (2, 1))
    (2, 3)
    >>> xpx.broadcast_shapes((4, 2, 3), (2, 1), (1, 3))
    (4, 2, 3)
    """
    if not shapes:
        return ()  # Match NumPy output

    ndim = max(len(shape) for shape in shapes)
    out: list[int | None] = []
    for axis in range(-ndim, 0):
        sizes = {shape[axis] for shape in shapes if axis >= -len(shape)}
        # Dask uses NaN for unknown shape, which predates the Array API spec for None
        none_size = None in sizes or math.nan in sizes
        sizes -= {1, None, math.nan}
        if len(sizes) > 1:
            msg = (
                "shape mismatch: objects cannot be broadcast to a single shape: "
                f"{shapes}."
            )
        out.append(None if none_size else cast(int, sizes.pop()) if sizes else 1)

    return tuple(out)

def cov(m: Array, /, *, xp: ModuleType | None = None) -> Array:
    """
    Estimate a covariance matrix.

    Covariance indicates the level to which two variables vary together.
    If we examine N-dimensional samples, :math:`X = [x_1, x_2, ... x_N]^T`,
    then the covariance matrix element :math:`C_{ij}` is the covariance of
    :math:`x_i` and :math:`x_j`. The element :math:`C_{ii}` is the variance
    of :math:`x_i`.

    This provides a subset of the functionality of ``numpy.cov``.

    m : array
        A 1-D or 2-D array containing multiple variables and observations.
        Each row of `m` represents a variable, and each column a single
        observation of all those variables.
        The standard-compatible namespace for `m`. Default: infer.

    Returns
    -------
    array
        The covariance matrix of the variables.

    Examples
    --------

    Consider two variables, :math:`x_0` and :math:`x_1`, which
    correlate perfectly, but in opposite directions:

    >>> x = xp.asarray([[0, 2], [1, 1], [2, 0]]).T
    >>> x
    Array([[0, 1, 2],
           [2, 1, 0]], dtype=array_api_strict.int64)

    Note how :math:`x_0` increases while :math:`x_1` decreases. The covariance
    matrix shows this clearly:

    >>> xpx.cov(x, xp=xp)
    Array([[ 1., -1.],
           [-1.,  1.]], dtype=array_api_strict.float64)

    Note that element :math:`C_{0,1}`, which shows the correlation between
    :math:`x_0` and :math:`x_1`, is negative.

    Further, note how `x` and `y` are combined:

    >>> x = xp.asarray([-2.1, -1,  4.3])
    >>> y = xp.asarray([3,  1.1,  0.12])
    >>> X = xp.stack((x, y), axis=0)
    >>> xpx.cov(X, xp=xp)
    Array([[11.71      , -4.286     ],
           [-4.286     ,  2.14413333]], dtype=array_api_strict.float64)

    Array(11.71, dtype=array_api_strict.float64)

    >>> xpx.cov(y, xp=xp)
    Array(2.14413333, dtype=array_api_strict.float64)
    """
        xp = array_namespace(m)

    m = xp.asarray(m, copy=True)
    dtype = (
        xp.float64 if xp.isdtype(m.dtype, "integral") else xp.result_type(m, xp.float64)
    )

    m = atleast_nd(m, ndim=2, xp=xp)
    m = xp.astype(m, dtype)

    avg = _helpers.mean(m, axis=1, xp=xp)

    m_shape = eager_shape(m)
    fact = m_shape[1] - 1

    if fact <= 0:
        warnings.warn("Degrees of freedom <= 0 for slice", RuntimeWarning, stacklevel=2)
        fact = 0

    m -= avg[:, None]
    m_transpose = m.T
    if xp.isdtype(m_transpose.dtype, "complex floating"):
        m_transpose = xp.conj(m_transpose)
    c = m @ m_transpose
    c /= fact
    axes = tuple(axis for axis, length in enumerate(c.shape) if length == 1)
    return xp.squeeze(c, axis=axes)

def create_diagonal(
    x: Array, /, *, offset: int = 0, xp: ModuleType | None = None
) -> Array:
    """
    Construct a diagonal array.

    x : array
        An array having shape ``(*batch_dims, k)``.
    offset : int, optional
        Offset from the leading diagonal (default is ``0``).
        Use positive ints for diagonals above the leading diagonal,
        and negative ints for diagonals below the leading diagonal.

    Returns
    -------
    array
        An array having shape ``(*batch_dims, k+abs(offset), k+abs(offset))`` with `x`
        on the diagonal (offset by `offset`).

    Examples
    --------
    >>> x = xp.asarray([2, 4, 8])

    >>> xpx.create_diagonal(x, xp=xp)
    Array([[2, 0, 0],
           [0, 4, 0],
           [0, 0, 8]], dtype=array_api_strict.int64)

    >>> xpx.create_diagonal(x, offset=-2, xp=xp)
    Array([[0, 0, 0, 0, 0],
           [0, 0, 0, 0, 0],
           [2, 0, 0, 0, 0],
           [0, 4, 0, 0, 0],
           [0, 0, 8, 0, 0]], dtype=array_api_strict.int64)
    """

        err_msg = "`x` must be at least 1-dimensional."
        raise ValueError(err_msg)

    x_shape = eager_shape(x)
    batch_dims = x_shape[:-1]
    n = x_shape[-1] + abs(offset)
    diag = xp.zeros((*batch_dims, n**2), dtype=x.dtype, device=_compat.device(x))

    target_slice = slice(
        offset if offset >= 0 else abs(offset) * n,
        min(n * (n - offset), diag.shape[-1]),
        n + 1,
    )
    for index in ndindex(*batch_dims):
        diag = at(diag)[(*index, target_slice)].set(x[(*index, slice(None))])
    return xp.reshape(diag, (*batch_dims, n, n))

def expand_dims(
    a: Array, /, *, axis: int | tuple[int, ...] = (0,), xp: ModuleType | None = None
) -> Array:
    """
    Expand the shape of an array.

    Insert (a) new axis/axes that will appear at the position(s) specified by
    `axis` in the expanded array shape.

    This is ``xp.expand_dims`` for `axis` an int *or a tuple of ints*.
    Roughly equivalent to ``numpy.expand_dims`` for NumPy arrays.

    a : array
        Array to have its shape expanded.
    axis : int or tuple of ints, optional
        Position(s) in the expanded axes where the new axis (or axes) is/are placed.
        If multiple positions are provided, they should be unique (note that a position
        given by a positive index could also be referred to by a negative index -
        that will also result in an error).
        Default: ``(0,)``.
        The standard-compatible namespace for `a`. Default: infer.

    Returns
    -------
    array
        `a` with an expanded shape.

    Examples
    --------
    >>> x = xp.asarray([1, 2])
    >>> x.shape
    (2,)

    The following is equivalent to ``x[xp.newaxis, :]`` or ``x[xp.newaxis]``:

    >>> y = xpx.expand_dims(x, axis=0, xp=xp)
    >>> y
    Array([[1, 2]], dtype=array_api_strict.int64)
    (1, 2)

    The following is equivalent to ``x[:, xp.newaxis]``:

    >>> y = xpx.expand_dims(x, axis=1, xp=xp)
    >>> y
    Array([[1],
           [2]], dtype=array_api_strict.int64)
    (2, 1)

    ``axis`` may also be a tuple:

    >>> y = xpx.expand_dims(x, axis=(0, 1), xp=xp)
    >>> y
    Array([[[1, 2]]], dtype=array_api_strict.int64)

    >>> y = xpx.expand_dims(x, axis=(2, 0), xp=xp)
    >>> y
    Array([[[1],
            [2]]], dtype=array_api_strict.int64)
    """
        xp = array_namespace(a)

    if not isinstance(axis, tuple):
    ndim = a.ndim + len(axis)
    if axis != () and (min(axis) < -ndim or max(axis) >= ndim):
        err_msg = (
            f"a provided axis position is out of bounds for array of dimension {a.ndim}"
        )
        raise IndexError(err_msg)
    axis = tuple(dim % ndim for dim in axis)
    if len(set(axis)) != len(axis):
        err_msg = "Duplicate dimensions specified in `axis`."
    for i in sorted(axis):
        a = xp.expand_dims(a, axis=i)
    return a

    *,
) -> Array:  # numpydoc ignore=PR01,RT01
    """See docstring in array_api_extra._delegation."""
    a, b = asarrays(a, b, xp=xp)

    a_inexact = xp.isdtype(a.dtype, ("real floating", "complex floating"))
    b_inexact = xp.isdtype(b.dtype, ("real floating", "complex floating"))
    if a_inexact or b_inexact:
        # prevent warnings on NumPy and Dask on inf - inf
        mxp = meta_namespace(a, b, xp=xp)
        out = apply_where(
            xp.isinf(a) | xp.isinf(b),
            (a, b),
            lambda a, b: mxp.isinf(a) & mxp.isinf(b) & (mxp.sign(a) == mxp.sign(b)),  # pyright: ignore[reportUnknownArgumentType]
            # Note: inf <= inf is True!
            lambda a, b: mxp.abs(a - b) <= (atol + rtol * mxp.abs(b)),  # pyright: ignore[reportUnknownArgumentType]
            xp=xp,
        )
        if equal_nan:
            out = xp.where(xp.isnan(a) & xp.isnan(b), True, out)
        return out

    if xp.isdtype(a.dtype, "bool") or xp.isdtype(b.dtype, "bool"):
        if atol >= 1 or rtol >= 1:
            return xp.ones_like(a == b)
        return a == b

    # integer types
    atol = int(atol)
    if rtol == 0:
        return xp.abs(a - b) <= atol

    # Don't rely on OverflowError, as it is not guaranteed by the Array API.
    nrtol = int(1.0 / rtol)
    if nrtol > xp.iinfo(b.dtype).max:
        # rtol * max_int < 1, so it's inconsequential
    return xp.abs(a - b) <= (atol + xp.abs(b) // nrtol)

def kron(
    /,
    *,
) -> Array:
    """
    Kronecker product of two arrays.

    Computes the Kronecker product, a composite array made of blocks of the
    second array scaled by the first.

    Equivalent to ``numpy.kron`` for NumPy arrays.

    a, b : Array | int | float | complex
        Input arrays or scalars. At least one must be an array.

    Returns
    -------
    array
        The Kronecker product of `a` and `b`.

    Notes
    -----
    The function assumes that the number of dimensions of `a` and `b`
    are the same, if necessary prepending the smallest with ones.
    If ``a.shape = (r0,r1,..,rN)`` and ``b.shape = (s0,s1,...,sN)``,
    the Kronecker product has shape ``(r0*s0, r1*s1, ..., rN*SN)``.
    The elements are products of elements from `a` and `b`, organized
    explicitly by::

        kron(a,b)[k0,k1,...,kN] = a[i0,i1,...,iN] * b[j0,j1,...,jN]

    where::

        kt = it * st + jt,  t = 0,...,N

    In the common 2-D case (N=1), the block structure can be visualized::

        [[ a[0,0]*b,   a[0,1]*b,  ... , a[0,-1]*b  ],
         [  ...                              ...   ],
         [ a[-1,0]*b,  a[-1,1]*b, ... , a[-1,-1]*b ]]

    Examples
    --------
    >>> xpx.kron(xp.asarray([1, 10, 100]), xp.asarray([5, 6, 7]), xp=xp)
    Array([  5,   6,   7,  50,  60,  70, 500,
           600, 700], dtype=array_api_strict.int64)

    >>> xpx.kron(xp.asarray([5, 6, 7]), xp.asarray([1, 10, 100]), xp=xp)
    Array([  5,  50, 500,   6,  60, 600,   7,
            70, 700], dtype=array_api_strict.int64)

    >>> xpx.kron(xp.eye(2), xp.ones((2, 2)), xp=xp)
    Array([[1., 1., 0., 0.],
           [1., 1., 0., 0.],
           [0., 0., 1., 1.],
           [0., 0., 1., 1.]], dtype=array_api_strict.float64)

    >>> a = xp.reshape(xp.arange(100), (2, 5, 2, 5))
    >>> b = xp.reshape(xp.arange(24), (2, 3, 4))
    >>> c = xpx.kron(a, b, xp=xp)
    >>> c.shape
    (2, 10, 6, 20)
    >>> I = (1, 3, 0, 2)
    >>> J = (0, 2, 1)
    >>> J1 = (0,) + J             # extend to ndim=4
    >>> S1 = (1,) + b.shape
    >>> K = tuple(xp.asarray(I) * xp.asarray(S1) + xp.asarray(J1))
    >>> c[K] == a[I]*b[J]
    Array(True, dtype=array_api_strict.bool)
    """
        xp = array_namespace(a, b)

    singletons = (1,) * (b.ndim - a.ndim)
    a = cast(Array, xp.broadcast_to(a, singletons + a.shape))

    nd_b, nd_a = b.ndim, a.ndim
    nd_max = max(nd_b, nd_a)
    if nd_a == 0 or nd_b == 0:
        return xp.multiply(a, b)

    a_shape = eager_shape(a)
    b_shape = eager_shape(b)

    # Equalise the shapes by prepending smaller one with 1s
    a_shape = (1,) * max(0, nd_b - nd_a) + a_shape
    b_shape = (1,) * max(0, nd_a - nd_b) + b_shape

    # Insert empty dimensions
    a_arr = expand_dims(a, axis=tuple(range(nd_b - nd_a)), xp=xp)
    b_arr = expand_dims(b, axis=tuple(range(nd_a - nd_b)), xp=xp)

    # Compute the product
    a_arr = expand_dims(a_arr, axis=tuple(range(1, nd_max * 2, 2)), xp=xp)
    b_arr = expand_dims(b_arr, axis=tuple(range(0, nd_max * 2, 2)), xp=xp)
    result = xp.multiply(a_arr, b_arr)

    # Reshape back and return
    res_shape = tuple(a_s * b_s for a_s, b_s in zip(a_shape, b_shape, strict=True))
    return xp.reshape(result, res_shape)

def nunique(x: Array, /, *, xp: ModuleType | None = None) -> Array:
    """
    Count the number of unique elements in an array.

    Compatible with JAX and Dask, whose laziness would be otherwise
    problematic.

    x : Array

    Returns
    -------
    array: 0-dimensional integer array
        The number of unique elements in `x`. It can be lazy.
    """

    if is_jax_array(x):
        # size= is JAX-specific
        # https://github.com/data-apis/array-api/issues/883
        _, counts = xp.unique_counts(x, size=_compat.size(x))
        return xp.astype(counts, xp.bool).sum()

    _, counts = xp.unique_counts(x)
    n = _compat.size(counts)
    # FIXME https://github.com/data-apis/array-api-compat/pull/231
    if n is None:  # e.g. Dask, ndonnx
    return xp.asarray(n, device=_compat.device(x))

def pad(
    x: Array,
    *,
    """See docstring in `array_api_extra._delegation.py`."""
    # make pad_width a list of length-2 tuples of ints
    if isinstance(pad_width, int):
        pad_width_seq = [(pad_width, pad_width)] * x.ndim
    elif (
        isinstance(pad_width, tuple)
        and len(pad_width) == 2
        and all(isinstance(i, int) for i in pad_width)
    ):
        pad_width_seq = [cast(tuple[int, int], pad_width)] * x.ndim
    else:
        pad_width_seq = cast(list[tuple[int, int]], list(pad_width))

    # https://github.com/python/typeshed/issues/13376
    slices: list[slice] = []  # type: ignore[explicit-any]
    newshape: list[int] = []
    for ax, w_tpl in enumerate(pad_width_seq):
        if len(w_tpl) != 2:
            msg = f"expect a 2-tuple (before, after), got {w_tpl}."

        sh = eager_shape(x)[ax]

        if w_tpl[0] == 0 and w_tpl[1] == 0:
            sl = slice(None, None, None)
        else:
            start, stop = w_tpl
            stop = None if stop == 0 else -stop

            sl = slice(start, stop, None)
            sh += w_tpl[0] + w_tpl[1]

        newshape.append(sh)
        slices.append(sl)

    padded = xp.full(
        tuple(newshape),
        fill_value=constant_values,
        device=_compat.device(x),
    )
    return at(padded, tuple(slices)).set(x)

def setdiff1d(
    x1: Array | complex,
    x2: Array | complex,
    /,
    *,
    assume_unique: bool = False,
) -> Array:
    """
    Find the set difference of two arrays.

    Return the unique values in `x1` that are not in `x2`.

    x1 : array | int | float | complex | bool
    x2 : array
        Input comparison array.
    assume_unique : bool
        If ``True``, the input arrays are both assumed to be unique, which
        can speed up the calculation. Default is ``False``.
        The standard-compatible namespace for `x1` and `x2`. Default: infer.

    Returns
    -------
    array
        1D array of values in `x1` that are not in `x2`. The result
        is sorted when `assume_unique` is ``False``, but otherwise only sorted
        if the input is sorted.

    Examples
    --------

    >>> x1 = xp.asarray([1, 2, 3, 2, 4, 1])
    >>> x2 = xp.asarray([3, 4, 5, 6])
    >>> xpx.setdiff1d(x1, x2, xp=xp)
    Array([1, 2], dtype=array_api_strict.int64)
    """
        xp = array_namespace(x1, x2)
    # https://github.com/microsoft/pyright/issues/10103
    x1_, x2_ = asarrays(x1, x2, xp=xp)

    if assume_unique:
        x1_ = xp.reshape(x1_, (-1,))
        x2_ = xp.reshape(x2_, (-1,))
    else:
        x1_ = xp.unique_values(x1_)
        x2_ = xp.unique_values(x2_)

    return x1_[_helpers.in1d(x1_, x2_, assume_unique=True, invert=True, xp=xp)]

def sinc(x: Array, /, *, xp: ModuleType | None = None) -> Array:
    r"""
    Return the normalized sinc function.

    The sinc function is equal to :math:`\sin(\pi x)/(\pi x)` for any argument
    :math:`x\ne 0`. ``sinc(0)`` takes the limit value 1, making ``sinc`` not
    only everywhere continuous but also infinitely differentiable.

    .. note::

        Note the normalization factor of ``pi`` used in the definition.
        This is the most commonly used definition in signal processing.
        Use ``sinc(x / xp.pi)`` to obtain the unnormalized sinc function
        :math:`\sin(x)/x` that is more common in mathematics.

    x : array
        Array (possibly multi-dimensional) of values for which to calculate
        ``sinc(x)``. Must have a real floating point dtype.

    Returns
    -------
    array
        ``sinc(x)`` calculated elementwise, which has the same shape as the input.

    Notes
    -----
    The name sinc is short for "sine cardinal" or "sinus cardinalis".

    The sinc function is used in various signal processing applications,
    including in anti-aliasing, in the construction of a Lanczos resampling
    filter, and in interpolation.

    For bandlimited interpolation of discrete-time signals, the ideal
    interpolation kernel is proportional to the sinc function.

    #. Weisstein, Eric W. "Sinc Function." From MathWorld--A Wolfram Web
       Resource. https://mathworld.wolfram.com/SincFunction.html
    #. Wikipedia, "Sinc function",
       https://en.wikipedia.org/wiki/Sinc_function

    Examples
    --------
    >>> x = xp.linspace(-4, 4, 41)
    >>> xpx.sinc(x, xp=xp)
    Array([-3.89817183e-17, -4.92362781e-02,
           -8.40918587e-02, -8.90384387e-02,
           -5.84680802e-02,  3.89817183e-17,
            6.68206631e-02,  1.16434881e-01,
            1.26137788e-01,  8.50444803e-02,
           -3.89817183e-17, -1.03943254e-01,
           -1.89206682e-01, -2.16236208e-01,
           -1.55914881e-01,  3.89817183e-17,
            2.33872321e-01,  5.04551152e-01,
            7.56826729e-01,  9.35489284e-01,
            1.00000000e+00,  9.35489284e-01,
            7.56826729e-01,  5.04551152e-01,
            2.33872321e-01,  3.89817183e-17,
           -1.55914881e-01, -2.16236208e-01,
           -1.89206682e-01, -1.03943254e-01,
           -3.89817183e-17,  8.50444803e-02,
            1.26137788e-01,  1.16434881e-01,
            6.68206631e-02,  3.89817183e-17,
           -5.84680802e-02, -8.90384387e-02,
           -8.40918587e-02, -4.92362781e-02,
           -3.89817183e-17], dtype=array_api_strict.float64)
    """

    if not xp.isdtype(x.dtype, "real floating"):
        err_msg = "`x` must have a real floating data type."
    # no scalars in `where` - array-api#807
    y = xp.pi * xp.where(
        xp.astype(x, xp.bool),
        x,
        xp.asarray(xp.finfo(x.dtype).eps, dtype=x.dtype, device=_compat.device(x)),
    )
    return xp.sin(y) / y

def in1d(
    x1: Array,
    x2: Array,
    /,
    *,
    invert: bool = False,
    """
    Check whether each element of an array is also present in a second array.

    Returns a boolean array the same length as `x1` that is True
    where an element of `x1` is in `x2` and False otherwise.

    This function has been adapted using the original implementation
    present in numpy:
    https://github.com/numpy/numpy/blob/v1.26.0/numpy/lib/arraysetops.py#L524-L758
    """

    x1_shape = eager_shape(x1)
    x2_shape = eager_shape(x2)

    # This code is run to make the code significantly faster
    if x2_shape[0] < 10 * x1_shape[0] ** 0.145 and isinstance(x2, Iterable):
        if invert:
            mask = xp.ones(x1_shape[0], dtype=xp.bool, device=_compat.device(x1))
            for a in x2:
                mask &= x1 != a
        else:
            mask = xp.zeros(x1_shape[0], dtype=xp.bool, device=_compat.device(x1))
            for a in x2:
                mask |= x1 == a
        return mask

    rev_idx = xp.empty(0)  # placeholder
    if not assume_unique:
        x1, rev_idx = xp.unique_inverse(x1)
        x2 = xp.unique_values(x2)

    ar = xp.concat((x1, x2))
    device_ = _compat.device(ar)
    # We need this to be a stable sort.
    order = xp.argsort(ar, stable=True)
    reverse_order = xp.argsort(order, stable=True)
    sar = xp.take(ar, order, axis=0)
    ar_size = _compat.size(sar)
    assert ar_size is not None, "xp.unique*() on lazy backends raises"
    if ar_size >= 1:
        bool_ar = sar[1:] != sar[:-1] if invert else sar[1:] == sar[:-1]
    else:
        bool_ar = xp.asarray([False]) if invert else xp.asarray([True])
    flag = xp.concat((bool_ar, xp.asarray([invert], device=device_)))
    ret = xp.take(flag, reverse_order, axis=0)

        return ret[: x1.shape[0]]
    return xp.take(ret, rev_idx, axis=0)

def mean(
    x: Array,
    /,
    *,
    axis: int | tuple[int, ...] | None = None,
    """
    Complex mean, https://github.com/data-apis/array-api/issues/846.
    """

    if xp.isdtype(x.dtype, "complex floating"):
        x_real = xp.real(x)
        x_imag = xp.imag(x)
        mean_real = xp.mean(x_real, axis=axis, keepdims=keepdims)
        mean_imag = xp.mean(x_imag, axis=axis, keepdims=keepdims)
        return mean_real + (mean_imag * xp.asarray(1j))
    return xp.mean(x, axis=axis, keepdims=keepdims)

def is_python_scalar(x: object) -> TypeIs[complex]:  # numpydoc ignore=PR01,RT01
    """Return True if `x` is a Python scalar, False otherwise."""
    # isinstance(x, float) returns True for np.float64
    # isinstance(x, complex) returns True for np.complex128
    # bool is a subclass of int
    return isinstance(x, int | float | complex) and not is_numpy_array(x)

def asarrays(
) -> tuple[Array, Array]:
    """
    Ensure both `a` and `b` are arrays.

    If `b` is a python scalar, it is converted to the same dtype as `a`, and vice versa.

    Behavior is not specified when mixing a Python ``float`` and an array with an
    integer data type; this may give ``float32``, ``float64``, or raise an exception.
    Behavior is implementation-specific.

    Similarly, behavior is not specified when mixing a Python ``complex`` and an array
    with a real-valued data type; this may give ``complex64``, ``complex128``, or raise
    an exception. Behavior is implementation-specific.

    Returns
    -------
    Array, Array
        The input arrays, possibly converted to arrays if they were scalars.

    See Also
    --------
    mixing-arrays-with-python-scalars : Array API specification for the behavior.
    """
    a_scalar = is_python_scalar(a)
    b_scalar = is_python_scalar(b)
    if not a_scalar and not b_scalar:
        # This includes misc. malformed input e.g. str
        return a, b  # type: ignore[return-value]

    swap = False
    if a_scalar:
        swap = True
        b, a = a, b

    if is_array_api_obj(a):
        # a is an Array API object
        # b is a int | float | complex | bool
        xa = a

        # https://data-apis.org/array-api/draft/API_specification/type_promotion.html#mixing-arrays-with-python-scalars
        same_dtype = {
            bool: "bool",
            int: ("integral", "real floating", "complex floating"),
            float: ("real floating", "complex floating"),
            complex: "complex floating",
        }
        kind = same_dtype[type(cast(complex, b))]  # type: ignore[index]
        if xp.isdtype(a.dtype, kind):
            xb = xp.asarray(b, dtype=a.dtype)
        else:
            # Undefined behaviour. Let the function deal with it, if it can.
            xb = xp.asarray(b)

    else:
        # Neither a nor b are Array API objects.
        # Note: we can only reach this point when one explicitly passes
        # xp=xp to the calling function; otherwise we fail earlier on
        # array_namespace(a, b).
        xa, xb = xp.asarray(a), xp.asarray(b)

    return (xb, xa) if swap else (xa, xb)

def ndindex(*x: int) -> Generator[tuple[int, ...]]:
    """
    Generate all N-dimensional indices for a given array shape.

    Given the shape of an array, an ndindex instance iterates over the N-dimensional
    index of the array. At each iteration a tuple of indices is returned, the last
    dimension is iterated over first.

    This has an identical API to numpy.ndindex.

    *x : int
        The shape of the array.
    """
    if not x:
        yield ()
        return
    for i in ndindex(*x[:-1]):
        for j in range(x[-1]):
            yield *i, j

def eager_shape(x: Array, /) -> tuple[int, ...]:
    """
    Return shape of an array. Raise if shape is not fully defined.

    x : Array

    Returns
    -------
    tuple[int, ...]
        Shape of the array.
    """
    shape = x.shape
    # Dask arrays uses non-standard NaN instead of None
    if any(s is None or math.isnan(s) for s in shape):
        msg = "Unsupported lazy shape"
    return cast(tuple[int, ...], shape)

def meta_namespace(
    *arrays: Array | complex | None, xp: ModuleType | None = None
) -> ModuleType:
    """
    Get the namespace of Dask chunks.

    On all other backends, just return the namespace of the arrays.

    *arrays : Array | int | float | complex | bool | None
        Input arrays.
        The standard-compatible namespace for the input arrays. Default: infer.

    Returns
    -------
        If xp is Dask, the namespace of the Dask chunks;
        otherwise, the namespace of the arrays.
    """
    xp = array_namespace(*arrays) if xp is None else xp
    if not is_dask_namespace(xp):
        return xp
    # Quietly skip scalars and None's
    metas = [cast(Array | None, getattr(a, "_meta", None)) for a in arrays]
    return array_namespace(*metas)

def laplacian(
    csgraph,
    normed=False,
    return_diag=False,
    use_out_degree=False,
    *,
    copy=True,
    form="array",
    dtype=None,
    symmetrized=False,
):
    """
    Return the Laplacian of a directed graph.

    csgraph : array_like or sparse matrix, 2 dimensions
        Compressed-sparse graph, with shape (N, N).
    normed : bool, optional
        If True, then compute symmetrically normalized Laplacian.
    return_diag : bool, optional
        If True, then also return an array related to vertex degrees.
    use_out_degree : bool, optional
        If True, then use out-degree instead of in-degree.
        This distinction matters only if the graph is asymmetric.
        If False, then change `csgraph` in place if possible,
        avoiding doubling the memory use.
        Default: True, for backward compatibility.
    form : 'array', or 'function', or 'lo'
        Determines the format of the output Laplacian:

        * 'array' is a numpy array;
        * 'function' is a pointer to evaluating the Laplacian-vector
          or Laplacian-matrix product;
        * 'lo' results in the format of the `LinearOperator`.

        Choosing 'function' or 'lo' always avoids doubling
        the memory use, ignoring `copy` value.
        Default: 'array', for backward compatibility.
    dtype : None or one of numeric numpy dtypes, optional
        The dtype of the output. If ``dtype=None``, the dtype of the
        output matches the dtype of the input csgraph, except for
        the case ``normed=True`` and integer-like csgraph, where
        the output dtype is 'float' allowing accurate normalization,
        but dramatically increasing the memory use.
        Default: None, for backward compatibility.
    symmetrized : bool, optional
        If True, then the output Laplacian is symmetric/Hermitian.
        The symmetrization is done by ``csgraph + csgraph.T.conj``
        without dividing by 2 to preserve integer dtypes if possible
        prior to the construction of the Laplacian.
        The symmetrization will increase the memory footprint of
        sparse matrices unless the sparsity pattern is symmetric or
        `form` is 'function' or 'lo'.
        Default: False, for backward compatibility.

    Returns
    -------
    lap : ndarray, or sparse matrix, or `LinearOperator`
        The N x N Laplacian of csgraph. It will be a NumPy array (dense)
        if the input was dense, or a sparse matrix otherwise, or
        the format of a function or `LinearOperator` if
        `form` equals 'function' or 'lo', respectively.
    diag : ndarray, optional
        The length-N main diagonal of the Laplacian matrix.
        For the normalized Laplacian, this is the array of square roots
        of vertex degrees or 1 if the degree is zero.

    Notes
    -----
    The Laplacian matrix of a graph is sometimes referred to as the
    "Kirchhoff matrix" or just the "Laplacian", and is useful in many
    parts of spectral graph theory.
    In particular, the eigen-decomposition of the Laplacian can give
    insight into many properties of the graph, e.g.,
    is commonly used for spectral data embedding and clustering.

    The constructed Laplacian doubles the memory use if ``copy=True`` and
    ``form="array"`` which is the default.
    Choosing ``copy=False`` has no effect unless ``form="array"``
    or the matrix is sparse in the ``coo`` format, or dense array, except
    for the integer input with ``normed=True`` that forces the float output.

    Sparse input is reformatted into ``coo`` if ``form="array"``,
    which is the default.

    If the input adjacency matrix is not symmetric, the Laplacian is
    also non-symmetric unless ``symmetrized=True`` is used.

    Diagonal entries of the input adjacency matrix are ignored and
    replaced with zeros for the purpose of normalization where ``normed=True``.
    The normalization uses the inverse square roots of row-sums of the input
    adjacency matrix, and thus may fail if the row-sums contain
    negative or complex with a non-zero imaginary part values.

    The normalization is symmetric, making the normalized Laplacian also
    symmetric if the input csgraph was symmetric.

    .. [1] Laplacian matrix. https://en.wikipedia.org/wiki/Laplacian_matrix

    Examples
    --------
    >>> from scipy.sparse import csgraph

    Our first illustration is the symmetric graph

    >>> G = np.arange(4) * np.arange(4)[:, np.newaxis]
    >>> G
           [0, 1, 2, 3],
           [0, 2, 4, 6],
           [0, 3, 6, 9]])

    and its symmetric Laplacian matrix

    >>> csgraph.laplacian(G)
    array([[ 0,  0,  0,  0],
           [ 0,  5, -2, -3],
           [ 0, -2,  8, -6],
           [ 0, -3, -6,  9]])

    The non-symmetric graph

    >>> G = np.arange(9).reshape(3, 3)
    >>> G
    array([[0, 1, 2],
           [3, 4, 5],
           [6, 7, 8]])

    has different row- and column sums, resulting in two varieties
    of the Laplacian matrix, using an in-degree, which is the default

    >>> L_in_degree = csgraph.laplacian(G)
    >>> L_in_degree
    array([[ 9, -1, -2],
           [-3,  8, -5],
           [-6, -7,  7]])

    or alternatively an out-degree

    >>> L_out_degree = csgraph.laplacian(G, use_out_degree=True)
    >>> L_out_degree
    array([[ 3, -1, -2],
           [-6, -7, 13]])

    Constructing a symmetric Laplacian matrix, one can add the two as

    >>> L_in_degree + L_out_degree.T
    array([[ 12,  -4,  -8],
            [ -4,  16, -12],
            [ -8, -12,  20]])

    or use the ``symmetrized=True`` option

    >>> csgraph.laplacian(G, symmetrized=True)
           [ -4,  16, -12],
           [ -8, -12,  20]])

    that is equivalent to symmetrizing the original graph

    >>> csgraph.laplacian(G + G.T)

    The goal of normalization is to make the non-zero diagonal entries
    of the Laplacian matrix to be all unit, also scaling off-diagonal
    entries correspondingly. The normalization can be done manually, e.g.,

    >>> G = np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]])
    >>> L, d = csgraph.laplacian(G, return_diag=True)
    >>> L
    array([[ 2, -1, -1],
           [-1,  2, -1],
           [-1, -1,  2]])
    >>> d
    >>> scaling = np.sqrt(d)
    >>> scaling
    array([1.41421356, 1.41421356, 1.41421356])
    >>> (1/scaling)*L*(1/scaling)
    array([[ 1. , -0.5, -0.5],
           [-0.5,  1. , -0.5],
           [-0.5, -0.5,  1. ]])

    Or using ``normed=True`` option

    >>> L, d = csgraph.laplacian(G, return_diag=True, normed=True)
    >>> L

    which now instead of the diagonal returns the scaling coefficients

    >>> d

    Zero scaling coefficients are substituted with 1s, where scaling
    has thus no effect, e.g.,

    >>> G = np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]])
    >>> G
    array([[0, 0, 0],
           [0, 0, 1],
           [0, 1, 0]])
    >>> L
    array([[ 0., -0., -0.],
           [-0.,  1., -1.],
           [-0., -1.,  1.]])
    >>> d

    Only the symmetric normalization is implemented, resulting
    in a symmetric Laplacian matrix if and only if its graph is symmetric
    and has all non-negative degrees, like in the examples above.

    The output Laplacian matrix is by default a dense array or a sparse matrix
    inferring its shape, format, and dtype from the input graph matrix:

    >>> G = np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]).astype(np.float32)
    >>> G
    array([[0., 1., 1.],
           [1., 0., 1.],
           [1., 1., 0.]], dtype=float32)
    array([[ 2., -1., -1.],
           [-1.,  2., -1.],
           [-1., -1.,  2.]], dtype=float32)

    but can alternatively be generated matrix-free as a LinearOperator:

    >>> L = csgraph.laplacian(G, form="lo")
    >>> L
    <3x3 _CustomLinearOperator with dtype=float32>
    >>> L(np.eye(3))
           [-1., -1.,  2.]])

    or as a lambda-function:

    >>> L = csgraph.laplacian(G, form="function")
    >>> L
    <function _laplace.<locals>.<lambda> at 0x0000012AE6F5A598>

    The Laplacian matrix is used for
    spectral data clustering and embedding
    as well as for spectral graph partitioning.
    Our final example illustrates the latter
    for a noisy directed linear graph.

    >>> from scipy.sparse import diags, random
    >>> from scipy.sparse.linalg import lobpcg

    Create a directed linear graph with ``N=35`` vertices
    using a sparse adjacency matrix ``G``:

    >>> N = 35
    >>> G = diags(np.ones(N-1), 1, format="csr")

    Fix a random seed ``rng`` and add a random sparse noise to the graph ``G``:

    >>> rng = np.random.default_rng()
    >>> G += 1e-2 * random(N, N, density=0.1, random_state=rng)

    Set initial approximations for eigenvectors:

    >>> X = rng.random((N, 2))

    The constant vector of ones is always a trivial eigenvector
    of the non-normalized Laplacian to be filtered out:

    >>> Y = np.ones((N, 1))

    Alternating (1) the sign of the graph weights allows determining
    labels for spectral max- and min- cuts in a single loop.
    Since the graph is undirected, the option ``symmetrized=True``
    must be used in the construction of the Laplacian.
    The option ``normed=True`` cannot be used in (2) for the negative weights
    here as the symmetric normalization evaluates square roots.
    The option ``form="lo"`` in (2) is matrix-free, i.e., guarantees
    a fixed memory footprint and read-only access to the graph.
    Calling the eigenvalue solver ``lobpcg`` (3) computes the Fiedler vector
    that determines the labels as the signs of its components in (5).
    Since the sign in an eigenvector is not deterministic and can flip,
    we fix the sign of the first component to be always +1 in (4).

    >>> for cut in ["max", "min"]:
    ...     G = -G  # 1.
    ...     L = csgraph.laplacian(G, symmetrized=True, form="lo")  # 2.
    ...     _, eves = lobpcg(L, X, Y=Y, largest=False, tol=1e-3)  # 3.
    ...     eves *= np.sign(eves[0, 0])  # 4.
    ...     print(cut + "-cut labels:\\n", 1 * (eves[:, 0]>0))  # 5.
    max-cut labels:
    [1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1]
    min-cut labels:
    [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

    As anticipated for a (slightly noisy) linear graph,
    the max-cut strips all the edges of the graph coloring all
    odd vertices into one color and all even vertices into another one,
    while the balanced min-cut partitions the graph
    in the middle by deleting a single edge.
    Both determined partitions are optimal.
    """
    if csgraph.ndim != 2 or csgraph.shape[0] != csgraph.shape[1]:
        raise ValueError("csgraph must be a square matrix or array")

    if normed and (
        np.issubdtype(csgraph.dtype, np.signedinteger)
        or np.issubdtype(csgraph.dtype, np.uint)
    ):
        csgraph = csgraph.astype(np.float64)

    if form == "array":
        create_lap = _laplacian_sparse if issparse(csgraph) else _laplacian_dense
    else:
        create_lap = (
            _laplacian_sparse_flo if issparse(csgraph) else _laplacian_dense_flo
        )

    degree_axis = 1 if use_out_degree else 0

    lap, d = create_lap(
        csgraph,
        normed=normed,
        axis=degree_axis,
        copy=copy,
        form=form,
        symmetrized=symmetrized,
    )
    if return_diag:
        return lap, d
    return lap

def _setdiag_dense(m, d):
    step = len(d) + 1
    m.flat[::step] = d

def _laplace(m, d):
    return lambda v: v * d[:, np.newaxis] - m @ v

def _laplace_normed(m, d, nd):
    laplace = _laplace(m, d)
    return lambda v: nd[:, np.newaxis] * laplace(v * nd[:, np.newaxis])

def _laplace_sym(m, d):
    return (
        lambda v: v * d[:, np.newaxis]
        - m @ v
        - np.transpose(np.conjugate(np.transpose(np.conjugate(v)) @ m))
    )

def _laplace_normed_sym(m, d, nd):
    laplace_sym = _laplace_sym(m, d)
    return lambda v: nd[:, np.newaxis] * laplace_sym(v * nd[:, np.newaxis])

def _linearoperator(mv, shape, dtype):
    return LinearOperator(matvec=mv, matmat=mv, shape=shape, dtype=dtype)

def _laplacian_sparse_flo(graph, normed, axis, copy, form, dtype, symmetrized):
    # The keyword argument `copy` is unused and has no effect here.
    del copy

        dtype = graph.dtype

    graph_sum = np.asarray(graph.sum(axis=axis)).ravel()
    graph_diagonal = graph.diagonal()
    diag = graph_sum - graph_diagonal
    if symmetrized:
        graph_sum += np.asarray(graph.sum(axis=1 - axis)).ravel()
        diag = graph_sum - graph_diagonal - graph_diagonal

    if normed:
        isolated_node_mask = diag == 0
        w = np.where(isolated_node_mask, 1, np.sqrt(diag))
        if symmetrized:
            md = _laplace_normed_sym(graph, graph_sum, 1.0 / w)
        else:
            md = _laplace_normed(graph, graph_sum, 1.0 / w)
        if form == "function":
            return md, w.astype(dtype, copy=False)
        elif form == "lo":
            m = _linearoperator(md, shape=graph.shape, dtype=dtype)
            return m, w.astype(dtype, copy=False)
        else:
            raise ValueError(f"Invalid form: {form!r}")
    else:
            md = _laplace_sym(graph, graph_sum)
        else:
            md = _laplace(graph, graph_sum)
            return md, diag.astype(dtype, copy=False)
            return m, diag.astype(dtype, copy=False)
        else:

def _laplacian_sparse(graph, normed, axis, copy, form, dtype, symmetrized):
    # The keyword argument `form` is unused and has no effect here.
    del form

    needs_copy = False
    if graph.format in ("lil", "dok"):
        m = graph.tocoo()
    else:
        m = graph
        if copy:
            needs_copy = True

        m += m.T.conj()

    w = np.asarray(m.sum(axis=axis)).ravel() - m.diagonal()
    if normed:
        m = m.tocoo(copy=needs_copy)
        isolated_node_mask = w == 0
        w = np.where(isolated_node_mask, 1, np.sqrt(w))
        m.data /= w[m.row]
        m.data /= w[m.col]
        m.data *= -1
        m.setdiag(1 - isolated_node_mask)
    else:
        if m.format == "dia":
            m = m.copy()
        else:
            m = m.tocoo(copy=needs_copy)
        m.setdiag(w)

    return m.astype(dtype, copy=False), w.astype(dtype)

def _laplacian_dense_flo(graph, normed, axis, copy, form, dtype, symmetrized):
    if copy:
        m = np.array(graph)
    else:
        m = np.asarray(graph)

        dtype = m.dtype

    graph_sum = m.sum(axis=axis)
    graph_diagonal = m.diagonal()
        graph_sum += m.sum(axis=1 - axis)

    if normed:
            md = _laplace_normed_sym(m, graph_sum, 1.0 / w)
        else:
            md = _laplace_normed(m, graph_sum, 1.0 / w)
        else:
    else:
            md = _laplace_sym(m, graph_sum)
        else:
            md = _laplace(m, graph_sum)
        else:

def _laplacian_dense(graph, normed, axis, copy, form, dtype, symmetrized):
    if form != "array":
        raise ValueError(f'{form!r} must be "array"')

    if copy:
    else:

    np.fill_diagonal(m, 0)
    w = m.sum(axis=axis)
    if normed:
        m /= w
        m /= w[:, np.newaxis]
        m *= -1
        _setdiag_dense(m, 1 - isolated_node_mask)
    else:
        m *= -1
        _setdiag_dense(m, w)

    return m.astype(dtype, copy=False), w.astype(dtype, copy=False)

def inplace_identity(X):
    """Simply leave the input array unchanged.

    X : {array-like, sparse matrix}, shape (n_samples, n_features)
        Data, where `n_samples` is the number of samples
        and `n_features` is the number of features.
    """

def inplace_exp(X):
    """Compute the exponential inplace.

        The input data.
    """
    np.exp(X, out=X)

def inplace_logistic(X):
    """Compute the logistic function inplace.

    """
    logistic_sigmoid(X, out=X)

def inplace_tanh(X):
    """Compute the hyperbolic tan function inplace.

    """
    np.tanh(X, out=X)

def inplace_relu(X):
    """Compute the rectified linear unit function inplace.

    """
    np.maximum(X, 0, out=X)

def inplace_softmax(X):
    """Compute the K-way softmax function inplace.

    """
    tmp = X - X.max(axis=1)[:, np.newaxis]
    np.exp(tmp, out=X)
    X /= X.sum(axis=1)[:, np.newaxis]

def inplace_identity_derivative(Z, delta):
    """Apply the derivative of the identity function: do nothing.

    Z : {array-like, sparse matrix}, shape (n_samples, n_features)
        The data which was output from the identity activation function during
        the forward pass.

    delta : {array-like}, shape (n_samples, n_features)
         The backpropagated error signal to be modified inplace.
    """

def inplace_logistic_derivative(Z, delta):
    """Apply the derivative of the logistic sigmoid function.

    It exploits the fact that the derivative is a simple function of the output
    value from logistic function.

        The data which was output from the logistic activation function during

    """
    delta *= Z
    delta *= 1 - Z

def inplace_tanh_derivative(Z, delta):
    """Apply the derivative of the hyperbolic tanh function.

    value from hyperbolic tangent.

        The data which was output from the hyperbolic tangent activation
        function during the forward pass.

    """
    delta *= 1 - Z**2

def inplace_relu_derivative(Z, delta):
    """Apply the derivative of the relu function.

    value from rectified linear units activation function.

        The data which was output from the rectified linear units activation

    """
    delta[Z == 0] = 0

def squared_loss(y_true, y_pred, sample_weight=None):
    """Compute the squared loss for regression.

    y_true : array-like or label indicator matrix
        Ground truth (correct) values.

    y_pred : array-like or label indicator matrix
        Predicted values, as returned by a regression estimator.

        Sample weights.

    Returns
    -------
    loss : float
        The degree to which the samples are correctly predicted.
    """
    return (
        0.5 * np.average((y_true - y_pred) ** 2, weights=sample_weight, axis=0).mean()
    )

def poisson_loss(y_true, y_pred, sample_weight=None):
    """Compute (half of the) Poisson deviance loss for regression.

        Ground truth (correct) labels.

    Returns
    -------
    """
    # TODO: Decide what to do with the term `xlogy(y_true, y_true) - y_true`. For now,
    # it is included. But the _loss module doesn't use it (for performance reasons) and
    # only adds it as return of constant_to_optimal_zero (mainly for testing).
    return np.average(
        xlogy(y_true, y_true / y_pred) - y_true + y_pred, weights=sample_weight, axis=0
    ).sum()

def log_loss(y_true, y_prob, sample_weight=None):
    """Compute Logistic loss for classification.

    y_prob : array-like of float, shape = (n_samples, n_classes)
        Predicted probabilities, as returned by a classifier's
        predict_proba method.

    Returns
    -------
    """
    eps = np.finfo(y_prob.dtype).eps
    y_prob = np.clip(y_prob, eps, 1 - eps)
    if y_prob.shape[1] == 1:
        y_prob = np.append(1 - y_prob, y_prob, axis=1)

    if y_true.shape[1] == 1:
        y_true = np.append(1 - y_true, y_true, axis=1)

    return -np.average(xlogy(y_true, y_prob), weights=sample_weight, axis=0).sum()

def binary_log_loss(y_true, y_prob, sample_weight=None):
    """Compute binary logistic loss for classification.

    This is identical to log_loss in binary classification case,
    but is kept for its use in multilabel case.

    y_prob : array-like of float, shape = (n_samples, 1)

    Returns
    -------
    """
    return -np.average(
        xlogy(y_true, y_prob) + xlogy(1 - y_true, 1 - y_prob),
        weights=sample_weight,
        axis=0,
    ).sum()

class BernoulliRBM(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """Bernoulli Restricted Boltzmann Machine (RBM).

    A Restricted Boltzmann Machine with binary visible units and
    binary hidden units. Parameters are estimated using Stochastic Maximum
    Likelihood (SML), also known as Persistent Contrastive Divergence (PCD)
    [2].

    The time complexity of this implementation is ``O(d ** 2)`` assuming
    d ~ n_features ~ n_components.

    Read more in the :ref:`User Guide <rbm>`.

    n_components : int, default=256
        Number of binary hidden units.

        The learning rate for weight updates. It is *highly* recommended
        to tune this hyper-parameter. Reasonable values are in the
        10**[0., -3.] range.

    batch_size : int, default=10
        Number of examples per minibatch.

    n_iter : int, default=10
        Number of iterations/sweeps over the training dataset to perform
        during training.

        The verbosity level. The default, zero, means silent mode. Range
        of values is [0, inf].

        Determines random number generation for:

        - Gibbs sampling from visible and hidden layers.

        - Initializing components, sampling from layers during fit.

        - Corrupting the data when scoring samples.

    intercept_hidden_ : array-like of shape (n_components,)
        Biases of the hidden units.

    intercept_visible_ : array-like of shape (n_features,)
        Biases of the visible units.

    components_ : array-like of shape (n_components, n_features)
        Weight matrix, where `n_features` is the number of
        visible units and `n_components` is the number of hidden units.

    h_samples_ : array-like of shape (batch_size, n_components)
        Hidden Activation sampled from the model distribution,
        where `batch_size` is the number of examples per minibatch and
        `n_components` is the number of hidden units.

    See Also
    --------
    sklearn.neural_network.MLPRegressor : Multi-layer Perceptron regressor.
    sklearn.neural_network.MLPClassifier : Multi-layer Perceptron classifier.
    sklearn.decomposition.PCA : An unsupervised linear dimensionality
        reduction model.

    [1] Hinton, G. E., Osindero, S. and Teh, Y. A fast learning algorithm for
        deep belief nets. Neural Computation 18, pp 1527-1554.
        https://www.cs.toronto.edu/~hinton/absps/fastnc.pdf

    [2] Tieleman, T. Training Restricted Boltzmann Machines using
        Approximations to the Likelihood Gradient. International Conference
        on Machine Learning (ICML) 2008

    Examples
    --------

    >>> from sklearn.neural_network import BernoulliRBM
    >>> X = np.array([[0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 1]])
    >>> model = BernoulliRBM(n_components=2)
    >>> model.fit(X)
    BernoulliRBM(n_components=2)

    For a more detailed example usage, see
    :ref:`sphx_glr_auto_examples_neural_networks_plot_rbm_logistic_classification.py`.
    """

        "batch_size": [Interval(Integral, 1, None, closed="left")],
        "n_iter": [Interval(Integral, 0, None, closed="left")],
    }

        self,
        n_components=256,
        *,
        batch_size=10,
        n_iter=10,
    ):
        self.batch_size = batch_size
        self.n_iter = n_iter

        """Compute the hidden layer activation probabilities, P(h=1|v=X).

            The data to be transformed.

        Returns
        -------
        h : ndarray of shape (n_samples, n_components)
            Latent representations of the data.
        """

            self, X, accept_sparse="csr", reset=False, dtype=(np.float64, np.float32)
        )
        return self._mean_hiddens(X)

    def _mean_hiddens(self, v):
        """Computes the probabilities P(h=1|v).

        v : ndarray of shape (n_samples, n_features)
            Values of the visible layer.

        Returns
        -------
            Corresponding mean field values for the hidden layer.
        """
        p = safe_sparse_dot(v, self.components_.T)
        p += self.intercept_hidden_
        return expit(p, out=p)

    def _sample_hiddens(self, v, rng):
        """Sample from the distribution P(h|v).

            Values of the visible layer to sample from.

        rng : RandomState instance
            Random number generator to use.

        Returns
        -------
            Values of the hidden layer.
        """
        p = self._mean_hiddens(v)
        return rng.uniform(size=p.shape) < p

    def _sample_visibles(self, h, rng):
        """Sample from the distribution P(v|h).

            Values of the hidden layer to sample from.

        Returns
        -------
        """
        p = np.dot(h, self.components_)
        p += self.intercept_visible_
        expit(p, out=p)

    def _free_energy(self, v):
        """Computes the free energy F(v) = - log sum_h exp(-E(v,h)).

        Returns
        -------
        free_energy : ndarray of shape (n_samples,)
            The value of the free energy.
        """
        return -safe_sparse_dot(v, self.intercept_visible_) - np.logaddexp(
            0, safe_sparse_dot(v, self.components_.T) + self.intercept_hidden_
        ).sum(axis=1)

    def gibbs(self, v):
        """Perform one Gibbs sampling step.

            Values of the visible layer to start from.

        Returns
        -------
        v_new : ndarray of shape (n_samples, n_features)
            Values of the visible layer after one Gibbs step.
        """
        if not hasattr(self, "random_state_"):
            self.random_state_ = check_random_state(self.random_state)
        h_ = self._sample_hiddens(v, self.random_state_)
        v_ = self._sample_visibles(h_, self.random_state_)

        return v_

        """Fit the model to the partial segment of the data X.

        y : array-like of shape (n_samples,) or (n_samples, n_outputs), default=None

        Returns
        -------
        self : BernoulliRBM
            The fitted model.
        """
        first_pass = not hasattr(self, "components_")
            self, X, accept_sparse="csr", dtype=np.float64, reset=first_pass
        )
        if not hasattr(self, "components_"):
            self.components_ = np.asarray(
                self.random_state_.normal(0, 0.01, (self.n_components, X.shape[1])),
            )
            self._n_features_out = self.components_.shape[0]
        if not hasattr(self, "intercept_hidden_"):
            self.intercept_hidden_ = np.zeros(
                self.n_components,
            )
        if not hasattr(self, "intercept_visible_"):
            self.intercept_visible_ = np.zeros(
                X.shape[1],
            )
        if not hasattr(self, "h_samples_"):
            self.h_samples_ = np.zeros((self.batch_size, self.n_components))

        self._fit(X, self.random_state_)

    def _fit(self, v_pos, rng):
        """Inner fit for one mini-batch.

        Adjust the parameters to maximize the likelihood of v using
        Stochastic Maximum Likelihood (SML).

        v_pos : ndarray of shape (n_samples, n_features)
            The data to use for training.

            Random number generator to use for sampling.
        """
        h_pos = self._mean_hiddens(v_pos)
        v_neg = self._sample_visibles(self.h_samples_, rng)
        h_neg = self._mean_hiddens(v_neg)

        lr = float(self.learning_rate) / v_pos.shape[0]
        update = safe_sparse_dot(v_pos.T, h_pos, dense_output=True).T
        update -= np.dot(h_neg.T, v_neg)
        self.components_ += lr * update
        self.intercept_hidden_ += lr * (h_pos.sum(axis=0) - h_neg.sum(axis=0))
        self.intercept_visible_ += lr * (
            np.asarray(v_pos.sum(axis=0)).squeeze() - v_neg.sum(axis=0)
        )

        h_neg[rng.uniform(size=h_neg.shape) < h_neg] = 1.0  # sample binomial
        self.h_samples_ = np.floor(h_neg, h_neg)

        """Compute the pseudo-likelihood of X.

            Values of the visible layer. Must be all-boolean (not checked).

        Returns
        -------
        pseudo_likelihood : ndarray of shape (n_samples,)
            Value of the pseudo-likelihood (proxy for likelihood).

        Notes
        -----
        This method is not deterministic: it computes a quantity called the
        free energy on X, then on a randomly corrupted version of X, and
        returns the log of the logistic function of the difference.
        """

        v = validate_data(self, X, accept_sparse="csr", reset=False)

        # Randomly corrupt one feature in each sample in v.
        ind = (np.arange(v.shape[0]), rng.randint(0, v.shape[1], v.shape[0]))
        if sp.issparse(v):
            data = -2 * v[ind] + 1
            if isinstance(data, np.matrix):  # v is a sparse matrix
                v_ = v + sp.csr_matrix((data.A.ravel(), ind), shape=v.shape)
            else:  # v is a sparse array
                v_ = v + sp.csr_array((data.ravel(), ind), shape=v.shape)
        else:
            v_ = v.copy()
            v_[ind] = 1 - v_[ind]

        fe = self._free_energy(v)
        fe_ = self._free_energy(v_)
        # log(expit(x)) = log(1 / (1 + exp(-x)) = -np.logaddexp(0, -x)
        return -v.shape[1] * np.logaddexp(0, -(fe_ - fe))

        """Fit the model to the data X.

        Returns
        -------
        """
        X = validate_data(self, X, accept_sparse="csr", dtype=(np.float64, np.float32))

        self.components_ = np.asarray(
            rng.normal(0, 0.01, (self.n_components, X.shape[1])),
            dtype=X.dtype,
        )
        self._n_features_out = self.components_.shape[0]
        self.intercept_hidden_ = np.zeros(self.n_components, dtype=X.dtype)
        self.intercept_visible_ = np.zeros(X.shape[1], dtype=X.dtype)
        self.h_samples_ = np.zeros((self.batch_size, self.n_components), dtype=X.dtype)

        n_batches = int(np.ceil(float(n_samples) / self.batch_size))
        batch_slices = list(
            gen_even_slices(n_batches * self.batch_size, n_batches, n_samples=n_samples)
        )
        verbose = self.verbose
        begin = time.time()
        for iteration in range(1, self.n_iter + 1):
            for batch_slice in batch_slices:
                self._fit(X[batch_slice], rng)

                end = time.time()
                print(
                    "[%s] Iteration %d, pseudo-likelihood = %.2f, time = %.2fs"
                    % (
                        type(self).__name__,
                        iteration,
                        self.score_samples(X).mean(),
                        end - begin,
                    )
                )
                begin = end

        self,
        *,
    ):

        Returns
        -------
        """

        )

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        return v_

        Returns
        -------
        """
        )
            )
            )
            )

        """

        )

        Returns
        -------

        Notes
        -----
        """

        else:

        Returns
        -------
        """

        )

        )

                print(
                    % (
                    )
                )
                begin = end

def _pack(coefs_, intercepts_):
    """Pack the parameters into a single vector."""
    return np.hstack([l.ravel() for l in coefs_ + intercepts_])

class BaseMultilayerPerceptron(BaseEstimator, ABC):
    """Base class for MLP classification and regression.

    """

        "hidden_layer_sizes": [
        ],
        "activation": [StrOptions({"identity", "logistic", "tanh", "relu"})],
        "solver": [StrOptions({"lbfgs", "sgd", "adam"})],
        "batch_size": [
        ],
        "learning_rate": [StrOptions({"constant", "invscaling", "adaptive"})],
        "learning_rate_init": [Interval(Real, 0, None, closed="neither")],
        "power_t": [Interval(Real, 0, None, closed="left")],
        "shuffle": ["boolean"],
        "momentum": [Interval(Real, 0, 1, closed="both")],
        "nesterovs_momentum": ["boolean"],
        "early_stopping": ["boolean"],
        "validation_fraction": [Interval(Real, 0, 1, closed="left")],
        "beta_1": [Interval(Real, 0, 1, closed="left")],
        "beta_2": [Interval(Real, 0, 1, closed="left")],
        "epsilon": [Interval(Real, 0, None, closed="neither")],
        "n_iter_no_change": [
            Options(Real, {np.inf}),
        ],
        "max_fun": [Interval(Integral, 1, None, closed="left")],
    }

        self,
        hidden_layer_sizes,
        activation,
        solver,
        alpha,
        batch_size,
        learning_rate_init,
        power_t,
        max_iter,
        loss,
        shuffle,
        tol,
        verbose,
        momentum,
        nesterovs_momentum,
        beta_1,
        beta_2,
        epsilon,
        max_fun,
    ):
        self.activation = activation
        self.learning_rate_init = learning_rate_init
        self.power_t = power_t
        self.hidden_layer_sizes = hidden_layer_sizes
        self.shuffle = shuffle
        self.momentum = momentum
        self.nesterovs_momentum = nesterovs_momentum
        self.beta_1 = beta_1
        self.beta_2 = beta_2
        self.epsilon = epsilon
        self.max_fun = max_fun

    def _unpack(self, packed_parameters):
        """Extract the coefficients and intercepts from packed_parameters."""
        for i in range(self.n_layers_ - 1):
            start, end, shape = self._coef_indptr[i]
            self.coefs_[i] = np.reshape(packed_parameters[start:end], shape)

            start, end = self._intercept_indptr[i]
            self.intercepts_[i] = packed_parameters[start:end]

    def _forward_pass(self, activations):
        """Perform a forward pass on the network by computing the values
        of the neurons in the hidden layers and the output layer.

        activations : list, length = n_layers - 1
            The ith element of the list holds the values of the ith layer.
        """
        hidden_activation = ACTIVATIONS[self.activation]
        # Iterate over the hidden layers
            activations[i + 1] = safe_sparse_dot(activations[i], self.coefs_[i])
            activations[i + 1] += self.intercepts_[i]

            # For the hidden layers
            if (i + 1) != (self.n_layers_ - 1):
                hidden_activation(activations[i + 1])

        # For the last layer
        output_activation = ACTIVATIONS[self.out_activation_]
        output_activation(activations[i + 1])

        return activations

    def _forward_pass_fast(self, X, check_input=True):
        """Predict using the trained model

        This is the same as _forward_pass but does not record the activations
        of all layers and only returns the last layer's activation.

            Perform input data validation or not.

        Returns
        -------
        y_pred : ndarray of shape (n_samples,) or (n_samples, n_outputs)
            The decision function of the samples for each class in the model.
        """
            X = validate_data(self, X, accept_sparse=["csr", "csc"], reset=False)

        # Initialize first layer
        activation = X

        # Forward propagate
            activation = safe_sparse_dot(activation, self.coefs_[i])
            activation += self.intercepts_[i]
            if i != self.n_layers_ - 2:
                hidden_activation(activation)
        output_activation(activation)

        return activation

    def _compute_loss_grad(
        self, layer, sw_sum, activations, deltas, coef_grads, intercept_grads
    ):
        """Compute the gradient of loss with respect to coefs and intercept for
        specified layer.

        This function does backpropagation for the specified one layer.
        """
        coef_grads[layer] = safe_sparse_dot(activations[layer].T, deltas[layer])
        coef_grads[layer] += self.alpha * self.coefs_[layer]
        coef_grads[layer] /= sw_sum

        intercept_grads[layer] = np.sum(deltas[layer], axis=0) / sw_sum

    def _loss_grad_lbfgs(
        self,
        packed_coef_inter,
        X,
        y,
        activations,
        deltas,
        coef_grads,
        intercept_grads,
    ):
        """Compute the MLP loss function and its corresponding derivatives
        with respect to the different parameters given in the initialization.

        Returned gradients are packed in a single vector so it can be used
        in lbfgs

        packed_coef_inter : ndarray
            A vector comprising the flattened coefficients and intercepts.

        deltas : list, length = n_layers - 1
            The ith element of the list holds the difference between the
            activations of the i + 1 layer and the backpropagated error.
            More specifically, deltas are gradients of loss with respect to z
            in each layer, where z = wx + b is the value of a particular layer
            before passing through the activation function

        coef_grads : list, length = n_layers - 1
            The ith element contains the amount of change used to update the
            coefficient parameters of the ith layer in an iteration.

        intercept_grads : list, length = n_layers - 1
            intercept parameters of the ith layer in an iteration.

        Returns
        -------
        loss : float
        grad : array-like, shape (number of nodes of all layers,)
        """
        self._unpack(packed_coef_inter)
        loss, coef_grads, intercept_grads = self._backprop(
            X, y, sample_weight, activations, deltas, coef_grads, intercept_grads
        )
        grad = _pack(coef_grads, intercept_grads)

    def _backprop(
        self, X, y, sample_weight, activations, deltas, coef_grads, intercept_grads
    ):
        with respect to each parameter: weights and bias vectors.

             The ith element of the list holds the values of the ith layer.

        Returns
        -------
        """

        activations = self._forward_pass(activations)

        # Get loss
        loss_func_name = self.loss
        if loss_func_name == "log_loss" and self.out_activation_ == "logistic":
            loss_func_name = "binary_log_loss"
        loss = LOSS_FUNCTIONS[loss_func_name](y, activations[-1], sample_weight)
        # Add L2 regularization term to loss
        values = 0
        for s in self.coefs_:
            s = s.ravel()
            values += np.dot(s, s)
            sw_sum = n_samples
        else:
            sw_sum = sample_weight.sum()
        loss += (0.5 * self.alpha) * values / sw_sum

        # Backward propagate
        last = self.n_layers_ - 2

        # The calculation of delta[last] is as follows:
        #   delta[last] = d/dz loss(y, act(z)) = act(z) - y
        # with z=x@w + b being the output of the last layer before passing through the
        # output activation, act(z) = activations[-1].
        # The simple formula for delta[last] here works with following (canonical
        # loss-link) combinations of output activation and loss function:
        # sigmoid and binary cross entropy, softmax and categorical cross
        # entropy, and identity with squared loss
        deltas[last] = activations[-1] - y
            deltas[last] *= sample_weight.reshape(-1, 1)

        # Compute gradient for the last layer
        self._compute_loss_grad(
            last, sw_sum, activations, deltas, coef_grads, intercept_grads
        )

        inplace_derivative = DERIVATIVES[self.activation]
        for i in range(last, 0, -1):
            deltas[i - 1] = safe_sparse_dot(deltas[i], self.coefs_[i].T)
            inplace_derivative(activations[i], deltas[i - 1])

            self._compute_loss_grad(
                i - 1, sw_sum, activations, deltas, coef_grads, intercept_grads
            )

        return loss, coef_grads, intercept_grads

    def _initialize(self, y, layer_units, dtype):
        # set all attributes, allocate weights etc. for first call
        # Initialize parameters
        self.t_ = 0

        # Compute the number of layers
        self.n_layers_ = len(layer_units)

        # Output for regression
        if not is_classifier(self):
            if self.loss == "poisson":
                self.out_activation_ = "exp"
            else:
                # loss = "squared_error"
                self.out_activation_ = "identity"
        # Output for multi class
        elif self._label_binarizer.y_type_ == "multiclass":
            self.out_activation_ = "softmax"
        # Output for binary class and multi-label
        else:
            self.out_activation_ = "logistic"

        # Initialize coefficient and intercept layers
        self.coefs_ = []
        self.intercepts_ = []

            coef_init, intercept_init = self._init_coef(
                layer_units[i], layer_units[i + 1], dtype
            )
            self.coefs_.append(coef_init)
            self.intercepts_.append(intercept_init)

        self._best_coefs = [c.copy() for c in self.coefs_]
        self._best_intercepts = [i.copy() for i in self.intercepts_]

        if self.solver in _STOCHASTIC_SOLVERS:
            self.loss_curve_ = []
            self._no_improvement_count = 0
            if self.early_stopping:
                self.validation_scores_ = []
                self.best_validation_score_ = -np.inf
                self.best_loss_ = None
            else:
                self.best_loss_ = np.inf
                self.validation_scores_ = None
                self.best_validation_score_ = None

    def _init_coef(self, fan_in, fan_out, dtype):
        # Use the initialization method recommended by
        # Glorot et al.
        factor = 6.0
        if self.activation == "logistic":
            factor = 2.0
        init_bound = np.sqrt(factor / (fan_in + fan_out))

        # Generate weights and bias:
        coef_init = self._random_state.uniform(
            -init_bound, init_bound, (fan_in, fan_out)
        )
        intercept_init = self._random_state.uniform(-init_bound, init_bound, fan_out)
        coef_init = coef_init.astype(dtype, copy=False)
        intercept_init = intercept_init.astype(dtype, copy=False)
        return coef_init, intercept_init

    def _fit(self, X, y, sample_weight=None, incremental=False):
        # Make sure self.hidden_layer_sizes is a list
        hidden_layer_sizes = self.hidden_layer_sizes
        if not hasattr(hidden_layer_sizes, "__iter__"):
            hidden_layer_sizes = [hidden_layer_sizes]
        hidden_layer_sizes = list(hidden_layer_sizes)

        if np.any(np.array(hidden_layer_sizes) <= 0):
                "hidden_layer_sizes must be > 0, got %s." % hidden_layer_sizes
            )
        first_pass = not hasattr(self, "coefs_") or (
            not self.warm_start and not incremental
        )

        X, y = self._validate_input(X, y, incremental, reset=first_pass)

        # Ensure y is 2D
            y = y.reshape((-1, 1))

        layer_units = [n_features] + hidden_layer_sizes + [self.n_outputs_]

        # check random state
        self._random_state = check_random_state(self.random_state)

        if first_pass:
            # First time training the model
            self._initialize(y, layer_units, X.dtype)

        # Initialize lists
        activations = [X] + [None] * (len(layer_units) - 1)
        deltas = [None] * (len(activations) - 1)

        coef_grads = [
            np.empty((n_fan_in_, n_fan_out_), dtype=X.dtype)
            for n_fan_in_, n_fan_out_ in pairwise(layer_units)
        ]

        intercept_grads = [
            np.empty(n_fan_out_, dtype=X.dtype) for n_fan_out_ in layer_units[1:]
        ]

        # Run the Stochastic optimization solver
            self._fit_stochastic(
                X,
                y,
                activations,
                deltas,
                coef_grads,
                intercept_grads,
                layer_units,
                incremental,
            )

        # Run the LBFGS solver
        elif self.solver == "lbfgs":
            self._fit_lbfgs(
                X,
                y,
                deltas,
            )

        # validate parameter weights
        weights = chain(self.coefs_, self.intercepts_)
        if not all(np.isfinite(w).all() for w in weights):
                "Solver produced non-finite parameter weights. The input data may"
                " contain large values and need to be preprocessed."
            )

    def _fit_lbfgs(
        self,
        X,
        y,
        deltas,
        layer_units,
    ):
        # Store meta information for the parameters
        self._coef_indptr = []
        self._intercept_indptr = []
        start = 0

        # Save sizes and indices of coefficients for faster unpacking
            n_fan_in, n_fan_out = layer_units[i], layer_units[i + 1]

            end = start + (n_fan_in * n_fan_out)
            self._coef_indptr.append((start, end, (n_fan_in, n_fan_out)))
            start = end

        # Save sizes and indices of intercepts for faster unpacking
            end = start + layer_units[i + 1]
            self._intercept_indptr.append((start, end))
            start = end

        # Run LBFGS
        packed_coef_inter = _pack(self.coefs_, self.intercepts_)

        if self.verbose is True or self.verbose >= 1:
            iprint = 1
        else:
            iprint = -1

        opt_res = scipy.optimize.minimize(
            self._loss_grad_lbfgs,
            packed_coef_inter,
            method="L-BFGS-B",
            jac=True,
            options={
                "maxfun": self.max_fun,
                "maxiter": self.max_iter,
                "iprint": iprint,
                "gtol": self.tol,
            },
            args=(
                X,
                y,
                deltas,
            ),
        )
        self.n_iter_ = _check_optimize_result("lbfgs", opt_res, self.max_iter)
        self.loss_ = opt_res.fun
        self._unpack(opt_res.x)

    def _fit_stochastic(
        self,
        X,
        y,
        deltas,
        incremental,
    ):
        params = self.coefs_ + self.intercepts_
        if not incremental or not hasattr(self, "_optimizer"):
            if self.solver == "sgd":
                self._optimizer = SGDOptimizer(
                    params,
                    self.learning_rate_init,
                    self.learning_rate,
                    self.momentum,
                    self.nesterovs_momentum,
                    self.power_t,
                )
            elif self.solver == "adam":
                self._optimizer = AdamOptimizer(
                    params,
                    self.beta_1,
                    self.beta_2,
                    self.epsilon,
                )

        # early_stopping in partial_fit doesn't make sense
        if self.early_stopping and incremental:
            raise ValueError("partial_fit does not support early_stopping=True")
        early_stopping = self.early_stopping
        if early_stopping:
            # don't stratify in multilabel classification
            should_stratify = is_classifier(self) and self.n_outputs_ == 1
            stratify = y if should_stratify else None
                    X,
                    y,
                    random_state=self._random_state,
                )
            else:
                # TODO: incorporate sample_weight in sampling here.
                (
                    X_train,
                    X_val,
                    y_train,
                    y_val,
                    X,
                    y,
                )
            if X_val.shape[0] < 2:
                    "The validation set is too small. Increase 'validation_fraction' "
                    "or the size of your dataset."
                )

                y_val = self._label_binarizer.inverse_transform(y_val)
        else:

        sample_idx = np.arange(n_samples, dtype=int)

        if self.batch_size == "auto":
            batch_size = min(200, n_samples)
        else:
            if self.batch_size > n_samples:
                    "Got `batch_size` less than 1 or larger than "
                    "sample size. It is going to be clipped"
                )
            batch_size = np.clip(self.batch_size, 1, n_samples)

        try:
            self.n_iter_ = 0
            for it in range(self.max_iter):
                if self.shuffle:
                    # Only shuffle the sample indices instead of X and y to
                    # reduce the memory footprint. These indices will be used
                    # to slice the X and y.
                    sample_idx = shuffle(sample_idx, random_state=self._random_state)

                accumulated_loss = 0.0
                for batch_slice in gen_batches(n_samples, batch_size):
                    if self.shuffle:
                        batch_idx = sample_idx[batch_slice]
                        X_batch = _safe_indexing(X_train, batch_idx)
                    else:
                        batch_idx = batch_slice
                        X_batch = X_train[batch_idx]
                    y_batch = y_train[batch_idx]
                    if sample_weight is None:
                        sample_weight_batch = None
                    else:
                        sample_weight_batch = sample_weight_train[batch_idx]

                    activations[0] = X_batch
                    batch_loss, coef_grads, intercept_grads = self._backprop(
                        X_batch,
                        y_batch,
                        sample_weight_batch,
                        activations,
                        deltas,
                        coef_grads,
                        intercept_grads,
                    )
                    accumulated_loss += batch_loss * (
                        batch_slice.stop - batch_slice.start
                    )

                    # update weights
                    grads = coef_grads + intercept_grads
                    self._optimizer.update_params(params, grads)

                self.n_iter_ += 1
                self.loss_ = accumulated_loss / X_train.shape[0]

                self.t_ += n_samples
                self.loss_curve_.append(self.loss_)
                if self.verbose:
                    print("Iteration %d, loss = %.8f" % (self.n_iter_, self.loss_))

                # update no_improvement_count based on training loss or
                # validation score according to early_stopping
                self._update_no_improvement_count(
                    early_stopping, X_val, y_val, sample_weight_val
                )

                # for learning rate that needs to be updated at iteration end
                self._optimizer.iteration_ends(self.t_)

                if self._no_improvement_count > self.n_iter_no_change:
                    # not better than last `n_iter_no_change` iterations by tol
                    # stop or decrease learning rate
                    if early_stopping:
                        msg = (
                            "Validation score did not improve more than "
                            "tol=%f for %d consecutive epochs."
                            % (self.tol, self.n_iter_no_change)
                        )
                    else:
                        msg = (
                            "Training loss did not improve more than tol=%f"
                            " for %d consecutive epochs."
                        )

                    is_stopping = self._optimizer.trigger_stopping(msg, self.verbose)
                    if is_stopping:
                        break
                    else:
                        self._no_improvement_count = 0

                if incremental:
                    break

                if self.n_iter_ == self.max_iter:
                        "Stochastic Optimizer: Maximum iterations (%d) "
                        "reached and the optimization hasn't converged yet."
                        % self.max_iter,
                    )
        except KeyboardInterrupt:
            warnings.warn("Training interrupted by user.")

            # restore best weights
            self.coefs_ = self._best_coefs
            self.intercepts_ = self._best_intercepts

    def _update_no_improvement_count(self, early_stopping, X, y, sample_weight):
            # compute validation score (can be NaN), use that for stopping
            val_score = self._score(X, y, sample_weight=sample_weight)

            self.validation_scores_.append(val_score)

                print("Validation score: %f" % self.validation_scores_[-1])
            # update best parameters
            # use validation_scores_, not loss_curve_
            # let's hope no-one overloads .score with mse
            last_valid_score = self.validation_scores_[-1]

            if last_valid_score < (self.best_validation_score_ + self.tol):
                self._no_improvement_count += 1
            else:
                self._no_improvement_count = 0

            if last_valid_score > self.best_validation_score_:
                self.best_validation_score_ = last_valid_score
                self._best_coefs = [c.copy() for c in self.coefs_]
                self._best_intercepts = [i.copy() for i in self.intercepts_]
        else:
            if self.loss_curve_[-1] > self.best_loss_ - self.tol:
            else:
            if self.loss_curve_[-1] < self.best_loss_:
                self.best_loss_ = self.loss_curve_[-1]

        """Fit the model to data matrix X and target(s) y.

        X : ndarray or sparse matrix of shape (n_samples, n_features)

        Returns
        -------
            Returns a trained MLP model.
        """
        return self._fit(X, y, sample_weight=sample_weight, incremental=False)

    def _check_solver(self):
        if self.solver not in _STOCHASTIC_SOLVERS:
                "partial_fit is only available for stochastic"
                " optimizers. %s is not stochastic." % self.solver
            )

    def _score_with_function(self, X, y, sample_weight, score_function):
        """Private score method without input validation."""
        # Input validation would remove feature names, so we disable it
        y_pred = self._predict(X, check_input=False)

        if np.isnan(y_pred).any() or np.isinf(y_pred).any():
            return np.nan

        return score_function(y, y_pred, sample_weight=sample_weight)

class MLPClassifier(ClassifierMixin, BaseMultilayerPerceptron):
    """Multi-layer Perceptron classifier.

    This model optimizes the log-loss function using LBFGS or stochastic
    gradient descent.

    hidden_layer_sizes : array-like of shape(n_layers - 2,), default=(100,)
        The ith element represents the number of neurons in the ith
        hidden layer.

    activation : {'identity', 'logistic', 'tanh', 'relu'}, default='relu'
        Activation function for the hidden layer.

        - 'identity', no-op activation, useful to implement linear bottleneck,
          returns f(x) = x

        - 'logistic', the logistic sigmoid function,
          returns f(x) = 1 / (1 + exp(-x)).

        - 'tanh', the hyperbolic tan function,
          returns f(x) = tanh(x).

        - 'relu', the rectified linear unit function,
          returns f(x) = max(0, x)

    solver : {'lbfgs', 'sgd', 'adam'}, default='adam'
        The solver for weight optimization.

        - 'lbfgs' is an optimizer in the family of quasi-Newton methods.

        - 'sgd' refers to stochastic gradient descent.

        - 'adam' refers to a stochastic gradient-based optimizer proposed
          by Kingma, Diederik, and Jimmy Ba

        For a comparison between Adam optimizer and SGD, see
        :ref:`sphx_glr_auto_examples_neural_networks_plot_mlp_training_curves.py`.

        Note: The default solver 'adam' works pretty well on relatively
        large datasets (with thousands of training samples or more) in terms of
        both training time and validation score.
        For small datasets, however, 'lbfgs' can converge faster and perform
        better.

    alpha : float, default=0.0001
        Strength of the L2 regularization term. The L2 regularization term
        is divided by the sample size when added to the loss.

        For an example usage and visualization of varying regularization, see
        :ref:`sphx_glr_auto_examples_neural_networks_plot_mlp_alpha.py`.

    batch_size : int, default='auto'
        Size of minibatches for stochastic optimizers.
        If the solver is 'lbfgs', the classifier will not use minibatch.
        When set to "auto", `batch_size=min(200, n_samples)`.

    learning_rate : {'constant', 'invscaling', 'adaptive'}, default='constant'
        Learning rate schedule for weight updates.

        - 'constant' is a constant learning rate given by
          'learning_rate_init'.

        - 'invscaling' gradually decreases the learning rate at each
          time step 't' using an inverse scaling exponent of 'power_t'.
          effective_learning_rate = learning_rate_init / pow(t, power_t)

        - 'adaptive' keeps the learning rate constant to
          'learning_rate_init' as long as training loss keeps decreasing.
          Each time two consecutive epochs fail to decrease training loss by at
          least tol, or fail to increase validation score by at least tol if
          'early_stopping' is on, the current learning rate is divided by 5.

        Only used when ``solver='sgd'``.

    learning_rate_init : float, default=0.001
        The initial learning rate used. It controls the step-size
        in updating the weights. Only used when solver='sgd' or 'adam'.

    power_t : float, default=0.5
        The exponent for inverse scaling learning rate.
        It is used in updating effective learning rate when the learning_rate
        is set to 'invscaling'. Only used when solver='sgd'.

    max_iter : int, default=200
        Maximum number of iterations. The solver iterates until convergence
        (determined by 'tol') or this number of iterations. For stochastic
        solvers ('sgd', 'adam'), note that this determines the number of epochs
        (how many times each data point will be used), not the number of
        gradient steps.

        Whether to shuffle samples in each iteration. Only used when
        solver='sgd' or 'adam'.

        Determines random number generation for weights and bias
        initialization, train-test split if early stopping is used, and batch
        sampling when solver='sgd' or 'adam'.

        Tolerance for the optimization. When the loss or score is not improving
        by at least ``tol`` for ``n_iter_no_change`` consecutive iterations,
        unless ``learning_rate`` is set to 'adaptive', convergence is
        considered to be reached and training stops.

        Whether to print progress messages to stdout.

        When set to True, reuse the solution of the previous
        call to fit as initialization, otherwise, just erase the

    momentum : float, default=0.9
        Momentum for gradient descent update. Should be between 0 and 1. Only
        used when solver='sgd'.

    nesterovs_momentum : bool, default=True
        Whether to use Nesterov's momentum. Only used when solver='sgd' and
        momentum > 0.

    early_stopping : bool, default=False
        Whether to use early stopping to terminate training when validation
        score is not improving. If set to true, it will automatically set
        aside 10% of training data as validation and terminate training when
        validation score is not improving by at least ``tol`` for
        ``n_iter_no_change`` consecutive epochs. The split is stratified,
        except in a multilabel setting.
        If early stopping is False, then the training stops when the training
        loss does not improve by more than tol for n_iter_no_change consecutive
        passes over the training set.
        Only effective when solver='sgd' or 'adam'.

        early stopping. Must be between 0 and 1.
        Only used if early_stopping is True.

    beta_1 : float, default=0.9
        Exponential decay rate for estimates of first moment vector in adam,
        should be in [0, 1). Only used when solver='adam'.

    beta_2 : float, default=0.999
        Exponential decay rate for estimates of second moment vector in adam,

    epsilon : float, default=1e-8
        Value for numerical stability in adam. Only used when solver='adam'.

        Maximum number of epochs to not meet ``tol`` improvement.

    max_fun : int, default=15000
        Only used when solver='lbfgs'. Maximum number of loss function calls.
        The solver iterates until convergence (determined by 'tol'), number
        of iterations reaches max_iter, or this number of loss function calls.
        Note that number of loss function calls will be greater than or equal
        to the number of iterations for the `MLPClassifier`.

        Class labels for each output.

    loss_ : float
        The current loss computed with the loss function.

    best_loss_ : float or None
        The minimum loss reached by the solver throughout fitting.
        If `early_stopping=True`, this attribute is set to `None`. Refer to
        the `best_validation_score_` fitted attribute instead.

    loss_curve_ : list of shape (`n_iter_`,)
        The ith element in the list represents the loss at the ith iteration.

    validation_scores_ : list of shape (`n_iter_`,) or None
        The score at each iteration on a held-out validation set. The score
        reported is the accuracy score. Only available if `early_stopping=True`,
        otherwise the attribute is set to `None`.

    best_validation_score_ : float or None
        The best validation score (i.e. accuracy score) that triggered the
        early stopping. Only available if `early_stopping=True`, otherwise the
        attribute is set to `None`.

    t_ : int
        The number of training samples seen by the solver during fitting.

    coefs_ : list of shape (n_layers - 1,)
        The ith element in the list represents the weight matrix corresponding
        to layer i.

    intercepts_ : list of shape (n_layers - 1,)
        The ith element in the list represents the bias vector corresponding to
        layer i + 1.

        The number of iterations the solver has run.

    n_layers_ : int
        Number of layers.

    out_activation_ : str
        Name of the output activation function.

    See Also
    --------
    MLPRegressor : Multi-layer Perceptron regressor.
    BernoulliRBM : Bernoulli Restricted Boltzmann Machine (RBM).

    Notes
    -----
    MLPClassifier trains iteratively since at each time step
    the partial derivatives of the loss function with respect to the model
    parameters are computed to update the parameters.

    It can also have a regularization term added to the loss function
    that shrinks model parameters to prevent overfitting.

    This implementation works with data represented as dense numpy arrays or
    sparse scipy arrays of floating point values.

    Hinton, Geoffrey E. "Connectionist learning procedures."
    Artificial intelligence 40.1 (1989): 185-234.

    Glorot, Xavier, and Yoshua Bengio.
    "Understanding the difficulty of training deep feedforward neural networks."
    International Conference on Artificial Intelligence and Statistics. 2010.

    :arxiv:`He, Kaiming, et al (2015). "Delving deep into rectifiers:
    Surpassing human-level performance on imagenet classification." <1502.01852>`

    :arxiv:`Kingma, Diederik, and Jimmy Ba (2014)
    "Adam: A method for stochastic optimization." <1412.6980>`

    Examples
    --------
    >>> from sklearn.neural_network import MLPClassifier
    >>> X, y = make_classification(n_samples=100, random_state=1)
    >>> X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y,
    ...                                                     random_state=1)
    >>> clf = MLPClassifier(random_state=1, max_iter=300).fit(X_train, y_train)
    >>> clf.predict_proba(X_test[:1])
    array([[0.038..., 0.961...]])
    >>> clf.predict(X_test[:5, :])
    array([1, 0, 1, 0, 1])
    0.8...
    """

        self,
        hidden_layer_sizes=(100,),
        activation="relu",
        *,
        solver="adam",
        alpha=0.0001,
        batch_size="auto",
        learning_rate="constant",
        learning_rate_init=0.001,
        power_t=0.5,
        max_iter=200,
        tol=1e-4,
        momentum=0.9,
        nesterovs_momentum=True,
        beta_1=0.9,
        beta_2=0.999,
        epsilon=1e-8,
        max_fun=15000,
    ):
            hidden_layer_sizes=hidden_layer_sizes,
            activation=activation,
            solver=solver,
            batch_size=batch_size,
            learning_rate_init=learning_rate_init,
            power_t=power_t,
            loss="log_loss",
            shuffle=shuffle,
            tol=tol,
            momentum=momentum,
            nesterovs_momentum=nesterovs_momentum,
            beta_1=beta_1,
            beta_2=beta_2,
            epsilon=epsilon,
            max_fun=max_fun,
        )

    def _validate_input(self, X, y, incremental, reset):
            self,
            X,
            y,
            dtype=(np.float64, np.float32),
        )
            y = column_or_1d(y, warn=True)

        # Matrix of actions to be taken under the possible combinations:
        # The case that incremental == True and classes_ not defined is
        # already checked by _check_partial_fit_first_call that is called
        # in _partial_fit below.
        # The cases are already grouped into the respective if blocks below.
        #
        # incremental warm_start classes_ def  action
        #    0            0         0        define classes_
        #    0            1         0        define classes_
        #    0            0         1        redefine classes_
        #
        #    0            1         1        check compat warm_start
        #    1            1         1        check compat warm_start
        #
        #    1            0         1        check compat last fit
        #
        # Note the reliance on short-circuiting here, so that the second
        # or part implies that classes_ is defined.
        if (not hasattr(self, "classes_")) or (not self.warm_start and not incremental):
            self._label_binarizer = LabelBinarizer()
            self._label_binarizer.fit(y)
            self.classes_ = self._label_binarizer.classes_
        else:
            classes = unique_labels(y)
            if self.warm_start:
                if set(classes) != set(self.classes_):
                        "warm_start can only be used where `y` has the same "
                        "classes as in the previous call to fit. Previously "
                        f"got {self.classes_}, `y` has {classes}"
                    )
            elif len(np.setdiff1d(classes, self.classes_, assume_unique=True)):
                    "`y` has classes not in `self.classes_`. "
                    f"`self.classes_` has {self.classes_}. 'y' has {classes}."
                )

        # This downcast to bool is to prevent upcasting when working with
        # float32 data
        y = self._label_binarizer.transform(y).astype(bool)
        return X, y

        """Predict using the multi-layer perceptron classifier.

        Returns
        -------
        y : ndarray, shape (n_samples,) or (n_samples, n_classes)
        """

    def _predict(self, X, check_input=True):
        """Private predict method with optional input validation"""
        y_pred = self._forward_pass_fast(X, check_input=check_input)

            y_pred = y_pred.ravel()

        return self._label_binarizer.inverse_transform(y_pred)

    def _score(self, X, y, sample_weight=None):
        return super()._score_with_function(
            X, y, sample_weight=sample_weight, score_function=accuracy_score
        )

    @available_if(lambda est: est._check_solver())
    def partial_fit(self, X, y, sample_weight=None, classes=None):
        """Update the model with a single iteration over the given data.

        classes : array of shape (n_classes,), default=None
            Note that y doesn't need to contain all labels in `classes`.

        Returns
        -------
            Trained MLP model.
        """
            if type_of_target(y).startswith("multilabel"):
                self._label_binarizer.fit(y)
            else:
                self._label_binarizer.fit(classes)

        return self._fit(X, y, sample_weight=sample_weight, incremental=True)

        """Return the log of probability estimates.

        Returns
        -------
        log_y_prob : ndarray of shape (n_samples, n_classes)
            The predicted log-probability of the sample for each class
            in the model, where classes are ordered as they are in
            `self.classes_`. Equivalent to `log(predict_proba(X))`.
        """
        y_prob = self.predict_proba(X)
        return np.log(y_prob, out=y_prob)

        Returns
        -------
        y_prob : ndarray of shape (n_samples, n_classes)
            The predicted probability of the sample for each class in the
        """
        y_pred = self._forward_pass_fast(X)

            return np.vstack([1 - y_pred, y_pred]).T
        else:
            return y_pred

class MLPRegressor(RegressorMixin, BaseMultilayerPerceptron):
    """Multi-layer Perceptron regressor.

    This model optimizes the squared error using LBFGS or stochastic gradient
    descent.

    loss : {'squared_error', 'poisson'}, default='squared_error'
        The loss function to use when training the weights. Note that the
        "squared error" and "poisson" losses actually implement
        "half squares error" and "half poisson deviance" to simplify the
        computation of the gradient. Furthermore, the "poisson" loss internally uses
        a log-link (exponential as the output activation function) and requires
        ``y >= 0``.

        .. versionchanged:: 1.7
           Added parameter `loss` and option 'poisson'.

        - 'adam' refers to a stochastic gradient-based optimizer proposed by
          Kingma, Diederik, and Jimmy Ba

        better.

        If the solver is 'lbfgs', the regressor will not use minibatch.

        - 'invscaling' gradually decreases the learning rate ``learning_rate_``
          at each time step 't' using an inverse scaling exponent of 'power_t'.

        Only used when solver='sgd'.

        score is not improving. If set to True, it will automatically set
        aside ``validation_fraction`` of training data as validation and
        terminate training when validation score is not improving by at
        least ``tol`` for ``n_iter_no_change`` consecutive epochs.

        Only used when solver='lbfgs'. Maximum number of function calls.
        The solver iterates until convergence (determined by ``tol``), number
        of iterations reaches max_iter, or this number of function calls.
        Note that number of function calls will be greater than or equal to
        the number of iterations for the MLPRegressor.

    best_loss_ : float
        Only accessible when solver='sgd' or 'adam'.

        Loss value evaluated at the end of each training step.

        reported is the R2 score. Only available if `early_stopping=True`,

        The best validation score (i.e. R2 score) that triggered the

    t_ : int
        Mathematically equals `n_iters * X.shape[0]`, it means
        `time_step` and it is used by optimizer's learning rate scheduler.

        to layer i.

        layer i + 1.

    See Also
    --------
    MLPClassifier : Multi-layer Perceptron classifier.
    sklearn.linear_model.SGDRegressor : Linear model fitted by minimizing
        a regularized empirical loss with SGD.

    Notes
    -----
    MLPRegressor trains iteratively since at each time step

    This implementation works with data represented as dense and sparse numpy
    arrays of floating point values.

    Examples
    --------
    >>> from sklearn.neural_network import MLPRegressor
    >>> X, y = make_regression(n_samples=200, n_features=20, random_state=1)
    >>> regr = MLPRegressor(random_state=1, max_iter=2000, tol=0.1)
    >>> regr.fit(X_train, y_train)
    MLPRegressor(max_iter=2000, random_state=1, tol=0.1)
    >>> regr.predict(X_test[:2])
    array([  28..., -290...])
    >>> regr.score(X_test, y_test)
    0.98...
    """

        **BaseMultilayerPerceptron._parameter_constraints,
        "loss": [StrOptions({"squared_error", "poisson"})],
    }

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        """Predict using the multi-layer perceptron model.

        Returns
        -------
        """

        if y_pred.shape[1] == 1:
            return y_pred.ravel()

            X, y, sample_weight=sample_weight, score_function=r2_score
        )

            self,
            X,
            y,
        )
        return X, y

    @available_if(lambda est: est._check_solver)
    def partial_fit(self, X, y, sample_weight=None):

        Returns
        -------
        """

        self,
        solver,
        alpha,
        power_t,
        max_iter,
        loss,
        shuffle,
        tol,
        verbose,
        momentum,
        beta_1,
        beta_2,
        epsilon,
        max_fun,
    ):

        """

        Returns
        -------
        """

    ):

        """

        self,
        X,
        y,
        deltas,
    ):

        in lbfgs

        Returns
        -------
        """
        )

    ):

        Returns
        -------
        """

        # Get loss
        values = 0
        else:

        )

            )

        self.t_ = 0

            else:
        else:

            )

            else:

        )

            )
        )

        ]

        ]

                X,
                y,
                deltas,
            )

                X,
                y,
                deltas,
            )

            )

        self,
        X,
        y,
        deltas,
    ):
        start = 0

            start = end

            start = end

        # Run LBFGS

            iprint = 1
        else:
            iprint = -1

            jac=True,
            options={
            },
            args=(
                X,
                y,
                deltas,
            ),
        )

        self,
        X,
        y,
        deltas,
    ):
                    params,
                )
                    params,
                )

                    X,
                    y,
                )
            else:
                (
                    X_train,
                    X_val,
                    y_train,
                    y_val,
                    X,
                    y,
                )
                )

        else:

        else:
                )

        try:

                    else:
                    else:

                        X_batch,
                        y_batch,
                        deltas,
                    )
                    )

                )

                        msg = (
                        )
                    else:
                        msg = (
                        )

                        break
                    else:

                    break

                    )

            else:

        else:
            else:

        Returns
        -------
        """

            )

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

            self,
            X,
            y,
        )

        #
        #
        #
        #
        else:
                    )
                )

        return X, y

        Returns
        -------
        """

        )

        Returns
        -------
        """
            else:

        Returns
        -------
        """

        Returns
        -------
        """

        else:

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        Returns
        -------
        """

        )

            self,
            X,
            y,
        )
        return X, y

        Returns
        -------
        """

class BaseOptimizer:
    """Base (Stochastic) gradient descent optimizer

    learning_rate_init : float, default=0.1
        The initial learning rate used. It controls the step-size in updating
        the weights

    learning_rate : float
        the current learning rate
    """

    def __init__(self, learning_rate_init=0.1):
        self.learning_rate = float(learning_rate_init)

    def update_params(self, params, grads):
        """Update parameters with given gradients

        params : list of length = len(coefs_) + len(intercepts_)
            The concatenated list containing coefs_ and intercepts_ in MLP
            model. Used for initializing velocities and updating params

        grads : list of length = len(params)
            Containing gradients with respect to coefs_ and intercepts_ in MLP
            model. So length should be aligned with params
        """
        updates = self._get_updates(grads)
        for param, update in zip((p for p in params), updates):
            param += update

    def iteration_ends(self, time_step):
        """Perform update to learning rate and potentially other states at the
        end of an iteration
        """
        pass

    def trigger_stopping(self, msg, verbose):
        """Decides whether it is time to stop training

        msg : str
            Message passed in for verbose output

        verbose : bool
            Print message to stdin if True

        Returns
        -------
        is_stopping : bool
            True if training needs to stop
        """
            print(msg + " Stopping.")

class SGDOptimizer(BaseOptimizer):
    """Stochastic gradient descent optimizer with momentum

    params : list, length = len(coefs_) + len(intercepts_)
        The concatenated list containing coefs_ and intercepts_ in MLP model.
        Used for initializing velocities and updating params

    lr_schedule : {'constant', 'adaptive', 'invscaling'}, default='constant'

        -'constant', is a constant learning rate given by
         'learning_rate_init'.

        -'invscaling' gradually decreases the learning rate 'learning_rate_' at
          each time step 't' using an inverse scaling exponent of 'power_t'.
          learning_rate_ = learning_rate_init / pow(t, power_t)

        -'adaptive', keeps the learning rate constant to
         'learning_rate_init' as long as the training keeps decreasing.
         Each time 2 consecutive epochs fail to decrease the training loss by
         tol, or fail to increase validation score by tol if 'early_stopping'
         is on, the current learning rate is divided by 5.

        Value of momentum used, must be larger than or equal to 0

    nesterov : bool, default=True
        Whether to use nesterov's momentum or not. Use nesterov's if True

        Power of time step 't' in inverse scaling. See `lr_schedule` for

    velocities : list, length = len(params)
        velocities that are used to update params
    """

        self,
        params,
        learning_rate_init=0.1,
        lr_schedule="constant",
        nesterov=True,
    ):
        super().__init__(learning_rate_init)

        self.lr_schedule = lr_schedule
        self.nesterov = nesterov
        self.velocities = [np.zeros_like(param) for param in params]

        """Perform updates to learning rate and potential other states at the

        time_step : int
            number of training samples trained on so far, used to update
            learning rate for 'invscaling'
        """
        if self.lr_schedule == "invscaling":
            self.learning_rate = (
                float(self.learning_rate_init) / (time_step + 1) ** self.power_t
            )

        if self.lr_schedule != "adaptive":
                print(msg + " Stopping.")

        if self.learning_rate <= 1e-6:
                print(msg + " Learning rate too small. Stopping.")

        self.learning_rate /= 5.0
            print(msg + " Setting learning rate to %f" % self.learning_rate)

    def _get_updates(self, grads):
        """Get the values used to update params with given gradients

        grads : list, length = len(coefs_) + len(intercepts_)

        Returns
        -------
        updates : list, length = len(grads)
            The values to add to params
        """
        updates = [
            self.momentum * velocity - self.learning_rate * grad
            for velocity, grad in zip(self.velocities, grads)
        ]
        self.velocities = updates

        if self.nesterov:
            updates = [
                self.momentum * velocity - self.learning_rate * grad
                for velocity, grad in zip(self.velocities, grads)
            ]

        return updates

class AdamOptimizer(BaseOptimizer):
    """Stochastic gradient descent optimizer with Adam

    Note: All default values are from the original Adam paper

        Exponential decay rate for estimates of first moment vector, should be
        in [0, 1)

        Exponential decay rate for estimates of second moment vector, should be
        in [0, 1)

        Value for numerical stability

        The current learning rate

    t : int
        Timestep

    ms : list, length = len(params)
        First moment vectors

    vs : list, length = len(params)
        Second moment vectors

    :arxiv:`Kingma, Diederik, and Jimmy Ba (2014) "Adam: A method for
        stochastic optimization." <1412.6980>
    """

        self, params, learning_rate_init=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-8
    ):

        self.t = 0
        self.ms = [np.zeros_like(param) for param in params]
        self.vs = [np.zeros_like(param) for param in params]

        Returns
        -------
        """
        self.t += 1
        self.ms = [
            self.beta_1 * m + (1 - self.beta_1) * grad
            for m, grad in zip(self.ms, grads)
        ]
        self.vs = [
            self.beta_2 * v + (1 - self.beta_2) * (grad**2)
            for v, grad in zip(self.vs, grads)
        ]
        self.learning_rate = (
            self.learning_rate_init
            * np.sqrt(1 - self.beta_2**self.t)
            / (1 - self.beta_1**self.t)
        )
        updates = [
            -self.learning_rate * m / (np.sqrt(v) + self.epsilon)
            for m, v in zip(self.ms, self.vs)
        ]

        """

        """
        pass

        msg : str

        Returns
        -------
        """

        self,
        params,
    ):

        """
            )

        Returns
        -------
        """
        updates = [
        ]

            updates = [
            ]

    ):

        self.t = 0

        Returns
        -------
        """
        self.t += 1
        self.ms = [
        ]
        self.vs = [
        ]
        )
        updates = [
        ]

def test_fit():
    X = Xdigits.copy()

    rbm = BernoulliRBM(
        n_components=64, learning_rate=0.1, batch_size=10, n_iter=7, random_state=9
    )
    rbm.fit(X)

    assert_almost_equal(rbm.score_samples(X).mean(), -21.0, decimal=0)

    # in-place tricks shouldn't have modified X
    assert_array_equal(X, Xdigits)

def test_partial_fit():
        n_components=64, learning_rate=0.1, batch_size=20, random_state=9
    )
    n_batches = int(np.ceil(float(n_samples) / rbm.batch_size))
    batch_slices = np.array_split(X, n_batches)

    for i in range(7):
        for batch in batch_slices:
            rbm.partial_fit(batch)

def test_transform():
    X = Xdigits[:100]
    rbm1 = BernoulliRBM(n_components=16, batch_size=5, n_iter=5, random_state=42)
    rbm1.fit(X)

    Xt1 = rbm1.transform(X)
    Xt2 = rbm1._mean_hiddens(X)

    assert_array_equal(Xt1, Xt2)

def test_small_sparse(csr_container):
    # BernoulliRBM should work on small sparse matrices.
    X = csr_container(Xdigits[:4])
    BernoulliRBM().fit(X)  # no exception

def test_small_sparse_partial_fit(sparse_container):
    X_sparse = sparse_container(Xdigits[:100])
    X = Xdigits[:100].copy()

    rbm1 = BernoulliRBM(
        n_components=64, learning_rate=0.1, batch_size=10, random_state=9
    )
    rbm2 = BernoulliRBM(
    )

    rbm1.partial_fit(X_sparse)
    rbm2.partial_fit(X)

        rbm1.score_samples(X).mean(), rbm2.score_samples(X).mean(), decimal=0
    )

def test_sample_hiddens():
    rbm1 = BernoulliRBM(n_components=2, batch_size=5, n_iter=5, random_state=42)

    h = rbm1._mean_hiddens(X[0])
    hs = np.mean([rbm1._sample_hiddens(X[0], rng) for i in range(100)], 0)

    assert_almost_equal(h, hs, decimal=1)

def test_fit_gibbs(csc_container):
    # XXX: this test is very seed-dependent! It probably needs to be rewritten.

    # Gibbs on the RBM hidden layer should be able to recreate [[0], [1]]
    # from the same input
    X = np.array([[0.0], [1.0]])
    rbm1 = BernoulliRBM(n_components=2, batch_size=2, n_iter=42, random_state=rng)
    # you need that much iters
        rbm1.components_, np.array([[0.02649814], [0.02009084]]), decimal=4
    )
    assert_almost_equal(rbm1.gibbs(X), X)

    # Gibbs on the RBM hidden layer should be able to recreate [[0], [1]] from
    # the same input even when the input is sparse, and test against non-sparse
    X = csc_container([[0.0], [1.0]])
    rbm2 = BernoulliRBM(n_components=2, batch_size=2, n_iter=42, random_state=rng)
    rbm2.fit(X)
        rbm2.components_, np.array([[0.02649814], [0.02009084]]), decimal=4
    )
    assert_almost_equal(rbm2.gibbs(X), X.toarray())
    assert_almost_equal(rbm1.components_, rbm2.components_)

def test_gibbs_smoke():
    # Check if we don't get NaNs sampling the full digits dataset.
    # Also check that sampling again will yield different results.
    X = Xdigits
    rbm1 = BernoulliRBM(n_components=42, batch_size=40, n_iter=20, random_state=42)
    X_sampled = rbm1.gibbs(X)
    assert_all_finite(X_sampled)
    X_sampled2 = rbm1.gibbs(X)
    assert np.all((X_sampled != X_sampled2).max(axis=1))

def test_score_samples(lil_containers):
    # Test score_samples (pseudo-likelihood) method.
    # Assert that pseudo-likelihood is computed without clipping.
    # See Fabian's blog, http://bit.ly/1iYefRk
    X = np.vstack([np.zeros(1000), np.ones(1000)])
    rbm1 = BernoulliRBM(n_components=10, batch_size=2, n_iter=10, random_state=rng)
    assert (rbm1.score_samples(X) < -300).all()

    # Sparse vs. dense should not affect the output. Also test sparse input
    # validation.
    rbm1.random_state = 42
    d_score = rbm1.score_samples(X)
    s_score = rbm1.score_samples(lil_containers(X))
    assert_almost_equal(d_score, s_score)

    # Test numerical stability (#2785): would previously generate infinities
    # and crash with an exception.
    with np.errstate(under="ignore"):
        rbm1.score_samples([np.arange(1000) * 100])

def test_rbm_verbose():
    rbm = BernoulliRBM(n_iter=2, verbose=10)
    try:
        rbm.fit(Xdigits)
    finally:
        sys.stdout = old_stdout

def test_sparse_and_verbose(csc_container):
    # Make sure RBM works with sparse input when verbose=True

        n_components=2, batch_size=2, n_iter=1, random_state=42, verbose=True
    )
    try:
        rbm.fit(X)
        s = sys.stdout.getvalue()
        # make sure output is sound
        assert re.match(
            r"\[BernoulliRBM\] Iteration 1,"
            r" pseudo-likelihood = -?(\d)+(\.\d+)?,"
            r" time = (\d|\.)+s",
            s,
        )
    finally:

def test_transformer_dtypes_casting(dtype_in, dtype_out):
    X = Xdigits[:100].astype(dtype_in)
    rbm = BernoulliRBM(n_components=16, batch_size=5, n_iter=5, random_state=42)
    Xt = rbm.fit_transform(X)

    # dtype_in and dtype_out should be consistent
    assert Xt.dtype == dtype_out, "transform dtype: {} - original dtype: {}".format(
        Xt.dtype, X.dtype
    )

def test_convergence_dtype_consistency():
    # float 64 transformer
    X_64 = Xdigits[:100].astype(np.float64)
    rbm_64 = BernoulliRBM(n_components=16, batch_size=5, n_iter=5, random_state=42)
    Xt_64 = rbm_64.fit_transform(X_64)

    # float 32 transformer
    X_32 = Xdigits[:100].astype(np.float32)
    rbm_32 = BernoulliRBM(n_components=16, batch_size=5, n_iter=5, random_state=42)
    Xt_32 = rbm_32.fit_transform(X_32)

    # results and attributes should be close enough in 32 bit and 64 bit
    assert_allclose(Xt_64, Xt_32, rtol=1e-06, atol=0)
        rbm_64.intercept_hidden_, rbm_32.intercept_hidden_, rtol=1e-06, atol=0
    )
        rbm_64.intercept_visible_, rbm_32.intercept_visible_, rtol=1e-05, atol=0
    )
    assert_allclose(rbm_64.components_, rbm_32.components_, rtol=1e-03, atol=0)
    assert_allclose(rbm_64.h_samples_, rbm_32.h_samples_)

def test_feature_names_out(method):
    """Check `get_feature_names_out` for `BernoulliRBM`."""
    n_components = 10
    rbm = BernoulliRBM(n_components=n_components)
    getattr(rbm, method)(Xdigits)

    names = rbm.get_feature_names_out()
    expected_names = [f"bernoullirbm{i}" for i in range(n_components)]

def test_base_optimizer():
    for lr in [10**i for i in range(-3, 4)]:
        optimizer = BaseOptimizer(lr)
        assert optimizer.trigger_stopping("", False)

def test_sgd_optimizer_no_momentum():
    params = [np.zeros(shape) for shape in shapes]

        optimizer = SGDOptimizer(params, lr, momentum=0, nesterov=False)
        grads = [rng.random_sample(shape) for shape in shapes]
        expected = [param - lr * grad for param, grad in zip(params, grads)]
        optimizer.update_params(params, grads)

        for exp, param in zip(expected, params):
            assert_array_equal(exp, param)

def test_sgd_optimizer_momentum():
    lr = 0.1

    for momentum in np.arange(0.5, 0.9, 0.1):
        optimizer = SGDOptimizer(params, lr, momentum=momentum, nesterov=False)
        velocities = [rng.random_sample(shape) for shape in shapes]
        optimizer.velocities = velocities
        updates = [
            momentum * velocity - lr * grad for velocity, grad in zip(velocities, grads)
        ]
        expected = [param + update for param, update in zip(params, updates)]

def test_sgd_optimizer_trigger_stopping():
    lr = 2e-6
    optimizer = SGDOptimizer(params, lr, lr_schedule="adaptive")
    assert not optimizer.trigger_stopping("", False)
    assert lr / 5 == optimizer.learning_rate
    assert optimizer.trigger_stopping("", False)

def test_sgd_optimizer_nesterovs_momentum():
    lr = 0.1

        optimizer = SGDOptimizer(params, lr, momentum=momentum, nesterov=True)
        updates = [
        ]
        updates = [
            momentum * update - lr * grad for update, grad in zip(updates, grads)
        ]

def test_adam_optimizer():
    lr = 0.001
    epsilon = 1e-8

    for beta_1 in np.arange(0.9, 1.0, 0.05):
        for beta_2 in np.arange(0.995, 1.0, 0.001):
            optimizer = AdamOptimizer(params, lr, beta_1, beta_2, epsilon)
            ms = [rng.random_sample(shape) for shape in shapes]
            vs = [rng.random_sample(shape) for shape in shapes]
            t = 10
            optimizer.ms = ms
            optimizer.vs = vs
            optimizer.t = t - 1
            grads = [rng.random_sample(shape) for shape in shapes]

            ms = [beta_1 * m + (1 - beta_1) * grad for m, grad in zip(ms, grads)]
            vs = [beta_2 * v + (1 - beta_2) * (grad**2) for v, grad in zip(vs, grads)]
            learning_rate = lr * np.sqrt(1 - beta_2**t) / (1 - beta_1**t)
            updates = [
                -learning_rate * m / (np.sqrt(v) + epsilon) for m, v in zip(ms, vs)
            ]
            expected = [param + update for param, update in zip(params, updates)]

            optimizer.update_params(params, grads)
            for exp, param in zip(expected, params):
                assert_array_equal(exp, param)

def test_binary_log_loss_1_prob_finite():
    # y_proba is equal to one should result in a finite logloss
    y_true = np.array([[0, 0, 1]]).T
    y_prob = np.array([[0.9, 1.0, 1.0]]).T

    loss = binary_log_loss(y_true, y_prob)
    assert np.isfinite(loss)

def test_log_loss_1_prob_finite(y_true, y_prob):
    # y_proba is equal to 1 should result in a finite logloss
    loss = log_loss(y_true, y_prob)

def test_poisson_loss(global_random_seed):
    """Test Poisson loss against well tested HalfPoissonLoss."""
    n = 1000
    rng = np.random.default_rng(global_random_seed)
    y_true = rng.integers(low=0, high=10, size=n).astype(float)
    y_raw = rng.standard_normal(n)
    y_pred = np.exp(y_raw)
    sw = rng.uniform(low=0.1, high=10, size=n)

    assert 0 in y_true

    loss = poisson_loss(y_true=y_true, y_pred=y_pred, sample_weight=sw)
    pl = HalfPoissonLoss()
    loss_ref = (
        pl(y_true=y_true, raw_prediction=y_raw, sample_weight=sw)
        + pl.constant_to_optimal_zero(y_true=y_true, sample_weight=sw).mean()
        / sw.mean()
    )

    assert loss == pytest.approx(loss_ref, rel=1e-12)

def test_alpha():
    # Test that larger alpha yields weights closer to zero
    X = X_digits_binary[:100]
    y = y_digits_binary[:100]

    alpha_vectors = []
    alpha_values = np.arange(2)
    absolute_sum = lambda x: np.sum(np.abs(x))

    for alpha in alpha_values:
        mlp = MLPClassifier(hidden_layer_sizes=10, alpha=alpha, random_state=1)
        with ignore_warnings(category=ConvergenceWarning):
            mlp.fit(X, y)
        alpha_vectors.append(
            np.array([absolute_sum(mlp.coefs_[0]), absolute_sum(mlp.coefs_[1])])
        )

    for i in range(len(alpha_values) - 1):
        assert (alpha_vectors[i] > alpha_vectors[i + 1]).all()

    # Test that the algorithm solution is equal to a worked out example.
    X = np.array([[0.6, 0.8, 0.7]])
    y = np.array([0])
    mlp = MLPClassifier(
        solver="sgd",
        alpha=0.1,
        activation="logistic",
        hidden_layer_sizes=2,
        momentum=0,
    )
    # set weights
    mlp.coefs_ = [0] * 2
    mlp.intercepts_ = [0] * 2
    mlp.n_outputs_ = 1
    mlp.coefs_[0] = np.array([[0.1, 0.2], [0.3, 0.1], [0.5, 0]])
    mlp.coefs_[1] = np.array([[0.1], [0.2]])
    mlp.intercepts_[0] = np.array([0.1, 0.1])
    mlp.intercepts_[1] = np.array([1.0])
    mlp._coef_grads = [] * 2
    mlp._intercept_grads = [] * 2
    mlp.n_features_in_ = 3

    # Initialize parameters
    mlp.n_iter_ = 0
    mlp.learning_rate_ = 0.1

    # Compute the number of layers
    mlp.n_layers_ = 3

    # Pre-allocate gradient matrices
    mlp._coef_grads = [0] * (mlp.n_layers_ - 1)
    mlp._intercept_grads = [0] * (mlp.n_layers_ - 1)

    mlp.out_activation_ = "logistic"
    mlp.t_ = 0
    mlp.best_loss_ = np.inf
    mlp.loss_curve_ = []
    mlp._no_improvement_count = 0
    mlp._intercept_velocity = [
        np.zeros_like(intercepts) for intercepts in mlp.intercepts_
    ]
    mlp._coef_velocity = [np.zeros_like(coefs) for coefs in mlp.coefs_]

    mlp.partial_fit(X, y, classes=[0, 1])
    # Manually worked out example
    # h1 = g(X1 * W_i1 + b11) = g(0.6 * 0.1 + 0.8 * 0.3 + 0.7 * 0.5 + 0.1)
    #       =  0.679178699175393
    # h2 = g(X2 * W_i2 + b12) = g(0.6 * 0.2 + 0.8 * 0.1 + 0.7 * 0 + 0.1)
    #         = 0.574442516811659
    # o1 = g(h * W2 + b21) = g(0.679 * 0.1 + 0.574 * 0.2 + 1)
    #       = 0.7654329236196236
    # d21 = -(0 - 0.765) = 0.765
    # d11 = (1 - 0.679) * 0.679 * 0.765 * 0.1 = 0.01667
    # d12 = (1 - 0.574) * 0.574 * 0.765 * 0.2 = 0.0374
    # W1grad11 = X1 * d11 + alpha * W11 = 0.6 * 0.01667 + 0.1 * 0.1 = 0.0200
    # W1grad11 = X1 * d12 + alpha * W12 = 0.6 * 0.0374 + 0.1 * 0.2 = 0.04244
    # W1grad21 = X2 * d11 + alpha * W13 = 0.8 * 0.01667 + 0.1 * 0.3 = 0.043336
    # W1grad22 = X2 * d12 + alpha * W14 = 0.8 * 0.0374 + 0.1 * 0.1 = 0.03992
    # W1grad31 = X3 * d11 + alpha * W15 = 0.6 * 0.01667 + 0.1 * 0.5 = 0.060002
    # W1grad32 = X3 * d12 + alpha * W16 = 0.6 * 0.0374 + 0.1 * 0 = 0.02244
    # W2grad1 = h1 * d21 + alpha * W21 = 0.679 * 0.765 + 0.1 * 0.1 = 0.5294
    # W2grad2 = h2 * d21 + alpha * W22 = 0.574 * 0.765 + 0.1 * 0.2 = 0.45911
    # b1grad1 = d11 = 0.01667
    # b1grad2 = d12 = 0.0374
    # b2grad = d21 = 0.765
    # W1 = W1 - eta * [W1grad11, .., W1grad32] = [[0.1, 0.2], [0.3, 0.1],
    #          [0.5, 0]] - 0.1 * [[0.0200, 0.04244], [0.043336, 0.03992],
    #          [0.060002, 0.02244]] = [[0.098, 0.195756], [0.2956664,
    #          0.096008], [0.4939998, -0.002244]]
    # W2 = W2 - eta * [W2grad1, W2grad2] = [[0.1], [0.2]] - 0.1 *
    #        [[0.5294], [0.45911]] = [[0.04706], [0.154089]]
    # b1 = b1 - eta * [b1grad1, b1grad2] = 0.1 - 0.1 * [0.01667, 0.0374]
    #         = [0.098333, 0.09626]
    # b2 = b2 - eta * b2grad = 1.0 - 0.1 * 0.765 = 0.9235
        mlp.coefs_[0],
        np.array([[0.098, 0.195756], [0.2956664, 0.096008], [0.4939998, -0.002244]]),
        decimal=3,
    )
    assert_almost_equal(mlp.coefs_[1], np.array([[0.04706], [0.154089]]), decimal=3)
    assert_almost_equal(mlp.intercepts_[0], np.array([0.098333, 0.09626]), decimal=3)
    assert_almost_equal(mlp.intercepts_[1], np.array(0.9235), decimal=3)
    # Testing output
    #  h1 = g(X1 * W_i1 + b11) = g(0.6 * 0.098 + 0.8 * 0.2956664 +
    #               0.7 * 0.4939998 + 0.098333) = 0.677
    #  h2 = g(X2 * W_i2 + b12) = g(0.6 * 0.195756 + 0.8 * 0.096008 +
    #            0.7 * -0.002244 + 0.09626) = 0.572
    #  o1 = h * W2 + b21 = 0.677 * 0.04706 +
    #             0.572 * 0.154089 + 0.9235 = 1.043
    #  prob = sigmoid(o1) = 0.739
    assert_almost_equal(mlp.predict_proba(X)[0, 1], 0.739, decimal=3)

def test_gradient():
    # Test gradient.

    # This makes sure that the activation functions and their derivatives
    # are correct. The numerical and analytical computation of the gradient
    # should be close.
    for n_labels in [2, 3]:
        n_samples = 5
        n_features = 10
        random_state = np.random.RandomState(seed=42)
        X = random_state.rand(n_samples, n_features)
        y = 1 + np.mod(np.arange(n_samples) + 1, n_labels)
        Y = LabelBinarizer().fit_transform(y)

        for activation in ACTIVATION_TYPES:
            mlp = MLPClassifier(
                activation=activation,
                hidden_layer_sizes=10,
                solver="lbfgs",
                alpha=1e-5,
                learning_rate_init=0.2,
                max_iter=1,
                random_state=1,
            )
                mlp.fit(X, y)

            theta = np.hstack([l.ravel() for l in mlp.coefs_ + mlp.intercepts_])

            layer_units = [X.shape[1]] + [mlp.hidden_layer_sizes] + [mlp.n_outputs_]

            activations = []
            deltas = []
            coef_grads = []
            intercept_grads = []

            activations.append(X)
            for i in range(mlp.n_layers_ - 1):
                activations.append(np.empty((X.shape[0], layer_units[i + 1])))
                deltas.append(np.empty((X.shape[0], layer_units[i + 1])))

                fan_in = layer_units[i]
                fan_out = layer_units[i + 1]
                coef_grads.append(np.empty((fan_in, fan_out)))
                intercept_grads.append(np.empty(fan_out))

            # analytically compute the gradients
            def loss_grad_fun(t):
                return mlp._loss_grad_lbfgs(
                    t, X, Y, None, activations, deltas, coef_grads, intercept_grads
                )

            [value, grad] = loss_grad_fun(theta)
            numgrad = np.zeros(np.size(theta))
            n = np.size(theta, 0)
            E = np.eye(n)
            epsilon = 1e-5
            # numerically compute the gradients
            for i in range(n):
                dtheta = E[:, i] * epsilon
                numgrad[i] = (
                    loss_grad_fun(theta + dtheta)[0] - loss_grad_fun(theta - dtheta)[0]
                ) / (epsilon * 2.0)
            assert_almost_equal(numgrad, grad)

def test_lbfgs_classification(X, y):
    # Test lbfgs on classification.
    # It should achieve a score higher than 0.95 for the binary and multi-class
    # versions of the digits dataset.
    X_train = X[:150]
    y_train = y[:150]
    X_test = X[150:]
    expected_shape_dtype = (X_test.shape[0], y_train.dtype.kind)

    for activation in ACTIVATION_TYPES:
        mlp = MLPClassifier(
            solver="lbfgs",
            hidden_layer_sizes=50,
            max_iter=150,
            shuffle=True,
            random_state=1,
        )
        mlp.fit(X_train, y_train)
        y_predict = mlp.predict(X_test)
        assert mlp.score(X_train, y_train) > 0.95
        assert (y_predict.shape[0], y_predict.dtype.kind) == expected_shape_dtype

def test_lbfgs_regression(X, y):
    # Test lbfgs on the regression dataset.
        mlp = MLPRegressor(
            max_iter=200,
            tol=1e-3,
        )
        mlp.fit(X, y)
        if activation == "identity":
            assert mlp.score(X, y) > 0.80
        else:
            # Non linear models perform much better than linear bottleneck:
            assert mlp.score(X, y) > 0.98

def test_lbfgs_classification_maxfun(X, y):
    # Test lbfgs parameter max_fun.
    # It should independently limit the number of iterations for lbfgs.
    max_fun = 10
    # classification tests
        )
        with pytest.warns(ConvergenceWarning):
            assert max_fun >= mlp.n_iter_

def test_lbfgs_regression_maxfun(X, y):
    # regression tests
            tol=0.0,
        )

def test_learning_rate_warmstart():
    # Tests that warm_start reuse past solutions.
    X = [[3, 2], [1, 6], [5, 6], [-2, -4]]
    y = [1, 1, 1, 0]
    for learning_rate in ["invscaling", "constant"]:
            solver="sgd",
            hidden_layer_sizes=4,
            max_iter=1,
            power_t=0.25,
            warm_start=True,
        )
            prev_eta = mlp._optimizer.learning_rate
            post_eta = mlp._optimizer.learning_rate

        if learning_rate == "constant":
            assert prev_eta == post_eta
        elif learning_rate == "invscaling":
            assert mlp.learning_rate_init / pow(8 + 1, mlp.power_t) == post_eta

def test_multilabel_classification():
    # Test that multi-label classification works as expected.
    # test fit method
        n_samples=50, random_state=0, return_indicator=True
    )
        solver="lbfgs",
        hidden_layer_sizes=50,
        alpha=1e-5,
        max_iter=150,
        learning_rate_init=0.2,
    )
    mlp.fit(X, y)
    assert mlp.score(X, y) > 0.97

    # test partial fit method
    )
    for i in range(100):
        mlp.partial_fit(X, y, classes=[0, 1, 2, 3, 4])
    assert mlp.score(X, y) > 0.9

    # Make sure early stopping still work now that splitting is stratified by
    # default (it is disabled for multilabel classification)
    mlp = MLPClassifier(early_stopping=True)
    mlp.fit(X, y).predict(X)

def test_multioutput_regression():
    # Test that multi-output regression works as expected
    X, y = make_regression(n_samples=200, n_targets=5, random_state=11)
    mlp = MLPRegressor(
        solver="lbfgs", hidden_layer_sizes=50, max_iter=200, tol=1e-2, random_state=1
    )

def test_partial_fit_classes_error():
    # Tests that passing different classes to partial_fit raises an error
    X = [[3, 2]]
    y = [0]
    clf = MLPClassifier(solver="sgd")
    clf.partial_fit(X, y, classes=[0, 1])
        clf.partial_fit(X, y, classes=[1, 2])

def test_partial_fit_classification():
    # Test partial_fit on classification.
    # `partial_fit` should yield the same results as 'fit' for binary and
    # multi-class classification.
    for X, y in classification_datasets:
            max_iter=100,
            tol=0,
            alpha=1e-5,
            learning_rate_init=0.2,
        )

        pred1 = mlp.predict(X)
            solver="sgd", random_state=1, alpha=1e-5, learning_rate_init=0.2
        )
        for i in range(100):
            mlp.partial_fit(X, y, classes=np.unique(y))
        pred2 = mlp.predict(X)
        assert_array_equal(pred1, pred2)
        assert mlp.score(X, y) > 0.95

def test_partial_fit_unseen_classes():
    # Non regression test for bug 6994
    # Tests for labeling errors in partial fit

    clf = MLPClassifier(random_state=0)
    clf.partial_fit([[1], [2], [3]], ["a", "b", "c"], classes=["a", "b", "c", "d"])
    clf.partial_fit([[4]], ["d"])
    assert clf.score([[1], [2], [3], [4]], ["a", "b", "c", "d"]) > 0

def test_partial_fit_regression():
    # Test partial_fit on regression.
    # `partial_fit` should yield the same results as 'fit' for regression.
    X = X_reg
    y = y_reg

    for momentum in [0, 0.9]:
            activation="relu",
            learning_rate_init=0.01,
            batch_size=X.shape[0],
        )
            warnings.simplefilter("ignore", ConvergenceWarning)
        )
            mlp.partial_fit(X, y)

        assert_allclose(pred1, pred2)
        score = mlp.score(X, y)
        assert score > 0.65

def test_partial_fit_errors():
    # Test partial_fit error handling.
    X = [[3, 2], [1, 6]]
    y = [1, 0]

    # no classes passed
        MLPClassifier(solver="sgd").partial_fit(X, y, classes=[2])

    # lbfgs doesn't support partial_fit
    assert not hasattr(MLPClassifier(solver="lbfgs"), "partial_fit")

def test_nonfinite_params():
    # Check that MLPRegressor throws ValueError when dealing with non-finite
    # parameter values
    fmax = np.finfo(np.float64).max
    X = fmax * rng.uniform(size=(n_samples, 2))
    y = rng.standard_normal(size=n_samples)

    clf = MLPRegressor()
    msg = (
        "Solver produced non-finite parameter weights. The input data may contain large"
        " values and need to be preprocessed."
    )
            # RuntimeWarning: overflow encountered in square
            warnings.simplefilter("ignore")
            clf.fit(X, y)

def test_predict_proba_binary():
    # Test that predict_proba works as expected for binary class.
    X = X_digits_binary[:50]
    y = y_digits_binary[:50]

    clf = MLPClassifier(hidden_layer_sizes=5, activation="logistic", random_state=1)
    y_proba = clf.predict_proba(X)
    y_log_proba = clf.predict_log_proba(X)

    (n_samples, n_classes) = y.shape[0], 2

    proba_max = y_proba.argmax(axis=1)
    proba_log_max = y_log_proba.argmax(axis=1)

    assert y_proba.shape == (n_samples, n_classes)
    assert_array_equal(proba_max, proba_log_max)
    assert_allclose(y_log_proba, np.log(y_proba))

    assert roc_auc_score(y, y_proba[:, 1]) == 1.0

def test_predict_proba_multiclass():
    # Test that predict_proba works as expected for multi class.
    X = X_digits_multi[:10]
    y = y_digits_multi[:10]

    clf = MLPClassifier(hidden_layer_sizes=5)

    (n_samples, n_classes) = y.shape[0], np.unique(y).size

def test_predict_proba_multilabel():
    # Test that predict_proba works as expected for multilabel.
    # Multilabel should not use softmax which makes probabilities sum to 1
    X, Y = make_multilabel_classification(
    )
    n_samples, n_classes = Y.shape

    clf = MLPClassifier(solver="lbfgs", hidden_layer_sizes=30, random_state=0)
    clf.fit(X, Y)

    assert_array_equal(y_proba > 0.5, Y)

    assert (y_proba.sum(1) - 1).dot(y_proba.sum(1) - 1) > 1e-10

def test_shuffle():
    # Test that the shuffle parameter affects the training process (it should)
    X, y = make_regression(n_samples=50, n_features=5, n_targets=1, random_state=0)

    # The coefficients will be identical if both do or do not shuffle
    for shuffle in [True, False]:
        mlp1 = MLPRegressor(
            hidden_layer_sizes=1,
            batch_size=1,
        )
        mlp2 = MLPRegressor(
        )
            mlp1.fit(X, y)
            mlp2.fit(X, y)

        assert np.array_equal(mlp1.coefs_[0], mlp2.coefs_[0])

    # The coefficients will be slightly different if shuffle=True
    mlp1 = MLPRegressor(
        hidden_layer_sizes=1, max_iter=1, batch_size=1, random_state=0, shuffle=True
    )
    mlp2 = MLPRegressor(
        hidden_layer_sizes=1, max_iter=1, batch_size=1, random_state=0, shuffle=False
    )
        warnings.simplefilter("ignore", ConvergenceWarning)
        mlp1.fit(X, y)
        mlp2.fit(X, y)

    assert not np.array_equal(mlp1.coefs_[0], mlp2.coefs_[0])

def test_sparse_matrices(csr_container):
    # Test that sparse and dense input matrices output the same results.
    X_sparse = csr_container(X)
    mlp = MLPClassifier(solver="lbfgs", hidden_layer_sizes=15, random_state=1)
    pred1 = mlp.predict(X)
    mlp.fit(X_sparse, y)
    pred2 = mlp.predict(X_sparse)
    assert_almost_equal(pred1, pred2)
    assert_array_equal(pred1, pred2)

def test_tolerance():
    # Test tolerance.
    # It should force the solver to exit the loop when it converges.
    y = [1, 0]
    clf = MLPClassifier(tol=0.5, max_iter=3000, solver="sgd")
    assert clf.max_iter > clf.n_iter_

def test_verbose_sgd():
    # Test verbose.
    y = [1, 0]
    clf = MLPClassifier(solver="sgd", max_iter=2, verbose=10, hidden_layer_sizes=2)
    sys.stdout = output = StringIO()

    clf.partial_fit(X, y)

    assert "Iteration" in output.getvalue()

def test_early_stopping(MLPEstimator):
    tol = 0.2
    mlp_estimator = MLPEstimator(
        tol=tol, max_iter=3000, solver="sgd", early_stopping=True
    )
    mlp_estimator.fit(X, y)
    assert mlp_estimator.max_iter > mlp_estimator.n_iter_

    assert mlp_estimator.best_loss_ is None
    assert isinstance(mlp_estimator.validation_scores_, list)

    valid_scores = mlp_estimator.validation_scores_
    best_valid_score = mlp_estimator.best_validation_score_
    assert max(valid_scores) == best_valid_score
    assert best_valid_score + tol > valid_scores[-2]
    assert best_valid_score + tol > valid_scores[-1]

    # check that the attributes `validation_scores_` and `best_validation_score_`
    # are set to None when `early_stopping=False`
        tol=tol, max_iter=3000, solver="sgd", early_stopping=False
    )
    assert mlp_estimator.validation_scores_ is None
    assert mlp_estimator.best_validation_score_ is None
    assert mlp_estimator.best_loss_ is not None

def test_adaptive_learning_rate():
    y = [1, 0]
    clf = MLPClassifier(tol=0.5, max_iter=3000, solver="sgd", learning_rate="adaptive")
    assert 1e-6 > clf._optimizer.learning_rate

def test_warm_start():
    X = X_iris
    y = y_iris

    y_2classes = np.array([0] * 75 + [1] * 75)
    y_3classes = np.array([0] * 40 + [1] * 40 + [2] * 70)
    y_3classes_alt = np.array([0] * 50 + [1] * 50 + [3] * 50)
    y_4classes = np.array([0] * 37 + [1] * 37 + [2] * 38 + [3] * 38)
    y_5classes = np.array([0] * 30 + [1] * 30 + [2] * 30 + [3] * 30 + [4] * 30)

    # No error raised
    clf = MLPClassifier(
        hidden_layer_sizes=2, solver="lbfgs", warm_start=True, random_state=42, tol=1e-2
    clf.fit(X, y_3classes)

    for y_i in (y_2classes, y_3classes_alt, y_4classes, y_5classes):
        clf = MLPClassifier(
            hidden_layer_sizes=2,
            random_state=42,
            tol=1e-2,
        message = (
            "warm_start can only be used where `y` has the same "
            "classes as in the previous call to fit."
            " Previously got [0 1 2], `y` has %s" % np.unique(y_i)
        )
        with pytest.raises(ValueError, match=re.escape(message)):
            clf.fit(X, y_i)

def test_warm_start_full_iteration(MLPEstimator):
    # https://github.com/scikit-learn/scikit-learn/issues/16812
    # Check that the MLP estimator accomplish `max_iter` with a
    # warm started estimator.
    X, y = X_iris, y_iris
    max_iter = 3
    clf = MLPEstimator(
        hidden_layer_sizes=2, solver="sgd", warm_start=True, max_iter=max_iter
    )
        assert max_iter == clf.n_iter_

def test_n_iter_no_change():
    # test n_iter_no_change using binary data set
    # the classifying fitting process is not prone to loss curve fluctuations
    tol = 0.01
    max_iter = 3000

    # test multiple n_iter_no_change
    for n_iter_no_change in [2, 5, 10, 50, 100]:
            tol=tol, max_iter=max_iter, solver="sgd", n_iter_no_change=n_iter_no_change
        )

        # validate n_iter_no_change
        assert clf._no_improvement_count == n_iter_no_change + 1
        assert max_iter > clf.n_iter_

def test_n_iter_no_change_inf():
    # the fitting process should go to max_iter iterations

    # set a ridiculous tolerance
    # this should always trigger _update_no_improvement_count()
    tol = 1e9

    # fit
    n_iter_no_change = np.inf
        tol=tol, max_iter=max_iter, solver="sgd", n_iter_no_change=n_iter_no_change
    )

    # validate n_iter_no_change doesn't cause early stopping
    assert clf.n_iter_ == max_iter

    # validate _update_no_improvement_count() was always triggered
    assert clf._no_improvement_count == clf.n_iter_ - 1

    ):

def test_mlp_classifier_dtypes_casting():
    # Compare predictions for different dtypes
    mlp_64 = MLPClassifier(
        alpha=1e-5, hidden_layer_sizes=(5, 3), random_state=1, max_iter=100, tol=1e-1
    )
    mlp_64.fit(X_digits[:300], y_digits[:300])
    pred_64 = mlp_64.predict(X_digits[300:])
    proba_64 = mlp_64.predict_proba(X_digits[300:])

    mlp_32 = MLPClassifier(
    )
    mlp_32.fit(X_digits[:300].astype(np.float32), y_digits[:300])
    pred_32 = mlp_32.predict(X_digits[300:].astype(np.float32))
    proba_32 = mlp_32.predict_proba(X_digits[300:].astype(np.float32))

    assert_array_equal(pred_64, pred_32)
    assert_allclose(proba_64, proba_32, rtol=1e-02)

def test_mlp_regressor_dtypes_casting():
    mlp_64 = MLPRegressor(
        alpha=1e-5, hidden_layer_sizes=(5, 3), random_state=1, max_iter=150, tol=1e-3
    )

    mlp_32 = MLPRegressor(
    )

    assert_allclose(pred_64, pred_32, rtol=5e-04)

def test_mlp_param_dtypes(dtype, Estimator):
    # Checks if input dtype is used for network parameters
    # and predictions
    X, y = X_digits.astype(dtype), y_digits
    mlp = Estimator(
        alpha=1e-5, hidden_layer_sizes=(5, 3), random_state=1, max_iter=50, tol=1e-1
    )
    mlp.fit(X[:300], y[:300])
    pred = mlp.predict(X[300:])

    assert all([intercept.dtype == dtype for intercept in mlp.intercepts_])

    assert all([coef.dtype == dtype for coef in mlp.coefs_])

    if Estimator == MLPRegressor:
        assert pred.dtype == dtype

def test_mlp_loading_from_joblib_partial_fit(tmp_path):
    """Loading from MLP and partial fitting updates weights. Non-regression
    test for #19626."""
    pre_trained_estimator = MLPRegressor(
        hidden_layer_sizes=(42,), random_state=42, learning_rate_init=0.01, max_iter=200
    )
    features, target = [[2]], [4]

    # Fit on x=2, y=4
    pre_trained_estimator.fit(features, target)

    # dump and load model
    pickled_file = tmp_path / "mlp.pkl"
    joblib.dump(pre_trained_estimator, pickled_file)
    load_estimator = joblib.load(pickled_file)

    # Train for a more epochs on point x=2, y=1
    fine_tune_features, fine_tune_target = [[2]], [1]

    for _ in range(200):
        load_estimator.partial_fit(fine_tune_features, fine_tune_target)

    # finetuned model learned the new target
    predicted_value = load_estimator.predict(fine_tune_features)
    assert_allclose(predicted_value, fine_tune_target, rtol=1e-4)

def test_preserve_feature_names(Estimator):
    """Check that feature names are preserved when early stopping is enabled.

    Non-regression test for gh-24846
    """

    X = pd.DataFrame(data=rng.randn(10, 2), columns=["colname_a", "colname_b"])
    y = pd.Series(data=np.full(10, 1), name="colname_y")

    model = Estimator(early_stopping=True, validation_fraction=0.2)

def test_mlp_warm_start_with_early_stopping(MLPEstimator):
    """Check that early stopping works with warm start."""
    mlp = MLPEstimator(
        max_iter=10, random_state=0, warm_start=True, early_stopping=True
    )
        mlp.fit(X_iris, y_iris)
        n_validation_scores = len(mlp.validation_scores_)
        mlp.set_params(max_iter=20)
    assert len(mlp.validation_scores_) > n_validation_scores

def test_mlp_warm_start_no_convergence(MLPEstimator, solver):
    """Check that we stop the number of iteration at `max_iter` when warm starting.

    https://github.com/scikit-learn/scikit-learn/issues/24764
    """
    model = MLPEstimator(
        solver=solver,
        n_iter_no_change=np.inf,
    )

    with pytest.warns(ConvergenceWarning):
        model.fit(X_iris, y_iris)
    assert model.n_iter_ == 10

    model.set_params(max_iter=20)
    assert model.n_iter_ == 20

def test_mlp_partial_fit_after_fit(MLPEstimator):
    """Check partial fit does not fail after fit when early_stopping=True.

    Non-regression test for gh-25693.
    """
    mlp = MLPEstimator(early_stopping=True, random_state=0).fit(X_iris, y_iris)

    msg = "partial_fit does not support early_stopping=True"
        mlp.partial_fit(X_iris, y_iris)

def test_mlp_diverging_loss():
    """Test that a diverging model does not raise errors when early stopping is enabled.

    https://github.com/scikit-learn/scikit-learn/issues/29504
    """
        hidden_layer_sizes=100,
        activation="identity",
        learning_rate_init=1,
    )

        # RuntimeWarning: overflow encountered in matmul
        # ConvergenceWarning: Stochastic Optimizer: Maximum iteration
        warnings.simplefilter("ignore", RuntimeWarning)

    # In python, float("nan") != float("nan")
    assert str(mlp.validation_scores_[-1]) == str(np.nan)
    assert isinstance(mlp.validation_scores_[-1], float)

def test_mlp_sample_weight_with_early_stopping():
    # Test code path for inner validation set splitting.
        n_informative=2,
    )
    sw = np.ones_like(y)
        hidden_layer_sizes=10,
        tol=1e-2,
        learning_rate_init=0.01,
    )
    m1 = MLPRegressor(
        **params,
    )
    m1.fit(X, y, sample_weight=sw)

    m2 = MLPRegressor(**params).fit(X, y, sample_weight=None)
    assert_allclose(m1.predict(X), m2.predict(X))

def test_mlp_vs_poisson_glm_equivalent(global_random_seed):
    """Test MLP with Poisson loss and no hidden layer equals GLM."""
    n = 100
    X = np.linspace(0, 1, n)
    y = rng.poisson(np.exp(X + 1))
    X = X.reshape(n, -1)
    glm = PoissonRegressor(alpha=0, tol=1e-7).fit(X, y)
    # Unfortunately, we can't set a zero hidden_layer_size, so we use a trick by using
    # just one hidden layer node with an identity activation. Coefficients will
    # therefore be different, but predictions are the same.
        loss="poisson",
        hidden_layer_sizes=(1,),
        alpha=0,
        tol=1e-7,
        random_state=np.random.RandomState(global_random_seed + 1),

    assert_allclose(mlp.predict(X), glm.predict(X), rtol=1e-4)

    # The same does not work with the squared error because the output activation is
    # the identity instead of the exponential.
        alpha=0,
        tol=1e-7,
    assert not np.allclose(mlp.predict(X), glm.predict(X), rtol=1e-4)

def test_minimum_input_sample_size():
    """Check error message when the validation set is too small."""
    X, y = make_regression(n_samples=2, n_features=5, random_state=0)
    model = MLPRegressor(early_stopping=True, random_state=0)
    with pytest.raises(ValueError, match="The validation set is too small"):

                )

class Interval:
    low: float
    high: float
    low_inclusive: bool
    high_inclusive: bool

    def __post_init__(self):
        """Check that low <= high"""
        if self.low > self.high:
                f"One must have low <= high; got low={self.low}, high={self.high}."
            )

    def includes(self, x):
        """Test whether all values of x are in interval range.

        x : ndarray
            Array whose elements are tested to be in interval range.

        Returns
        -------
        result : bool
        """
        if self.low_inclusive:
            low = np.greater_equal(x, self.low)
        else:
            low = np.greater(x, self.low)

        if not np.all(low):

        if self.high_inclusive:
            high = np.less_equal(x, self.high)
        else:
            high = np.less(x, self.high)

        # Note: np.all returns numpy.bool_
        return bool(np.all(high))

def _inclusive_low_high(interval, dtype=np.float64):
    """Generate values low and high to be within the interval range.

    This is used in tests only.

    Returns
    -------
    low, high : tuple
        The returned values low and high lie within the interval.
    """
    eps = 10 * np.finfo(dtype).eps
    if interval.low == -np.inf:
        low = -1e10
    elif interval.low < 0:
        low = interval.low * (1 - eps) + eps
    else:
        low = interval.low * (1 + eps) + eps

    if interval.high == np.inf:
        high = 1e10
    elif interval.high < 0:
        high = interval.high * (1 + eps) - eps
    else:
        high = interval.high * (1 - eps) - eps

    return low, high

class BaseLink(ABC):
    """Abstract base class for differentiable, invertible link functions.

    Convention:
        - link function g: raw_prediction = g(y_pred)
        - inverse link h: y_pred = h(raw_prediction)

    For (generalized) linear models, `raw_prediction = X @ coef` is the so
    called linear predictor, and `y_pred = h(raw_prediction)` is the predicted
    conditional (on X) expected value of the target `y_true`.

    The methods are not implemented as staticmethods in case a link function needs
    parameters.
    """

    is_multiclass = False  # used for testing only

    # Usually, raw_prediction may be any real number and y_pred is an open
    # interval.
    # interval_raw_prediction = Interval(-np.inf, np.inf, False, False)
    interval_y_pred = Interval(-np.inf, np.inf, False, False)

    def link(self, y_pred, out=None):
        """Compute the link function g(y_pred).

        The link function maps (predicted) target values to raw predictions,
        i.e. `g(y_pred) = raw_prediction`.

        y_pred : array
            Predicted target values.
        out : array
            A location into which the result is stored. If provided, it must
            have a shape that the inputs broadcast to. If not provided or None,
            a freshly-allocated array is returned.

        Returns
        -------
        out : array
            Output array, element-wise link function.
        """

    def inverse(self, raw_prediction, out=None):
        """Compute the inverse link function h(raw_prediction).

        The inverse link function maps raw predictions to predicted target
        values, i.e. `h(raw_prediction) = y_pred`.

        raw_prediction : array
            Raw prediction values (in link space).
        out : array

        Returns
        -------
        out : array
            Output array, element-wise inverse link function.
        """

class IdentityLink(BaseLink):
    """The identity link function g(x)=x."""

        if out is not None:
            np.copyto(out, y_pred)
            return out
        else:

    inverse = link

class LogLink(BaseLink):
    """The log link function g(x)=log(x)."""

    interval_y_pred = Interval(0, np.inf, False, False)

        return np.log(y_pred, out=out)

        return np.exp(raw_prediction, out=out)

class LogitLink(BaseLink):
    """The logit link function g(x)=logit(x)."""

    interval_y_pred = Interval(0, 1, False, False)

        return logit(y_pred, out=out)

        return expit(raw_prediction, out=out)

class HalfLogitLink(BaseLink):
    """Half the logit link function g(x)=1/2 * logit(x).

    Used for the exponential loss.
    """

        out = logit(y_pred, out=out)
        out *= 0.5
        return out

        return expit(2 * raw_prediction, out)

class MultinomialLogit(BaseLink):
    """The symmetric multinomial logit function.

        - y_pred.shape = raw_prediction.shape = (n_samples, n_classes)

    Notes:
        - The inverse link h is the softmax function.
        - The sum is over the second axis, i.e. axis=1 (n_classes).

    We have to choose additional constraints in order to make

        y_pred[k] = exp(raw_pred[k]) / sum(exp(raw_pred[k]), k=0..n_classes-1)

    for n_classes classes identifiable and invertible.
    We choose the symmetric side constraint where the geometric mean response
    is set as reference category, see [2]:

    The symmetric multinomial logit link function for a single data point is
    then defined as

        raw_prediction[k] = g(y_pred[k]) = log(y_pred[k]/gmean(y_pred))
        = log(y_pred[k]) - mean(log(y_pred)).

    Note that this is equivalent to the definition in [1] and implies mean
    centered raw predictions:

        sum(raw_prediction[k], k=0..n_classes-1) = 0.

    For linear models with raw_prediction = X @ coef, this corresponds to
    sum(coef[k], k=0..n_classes-1) = 0, i.e. the sum over classes for every
    feature is zero.

    Reference
    ---------
    .. [1] Friedman, Jerome; Hastie, Trevor; Tibshirani, Robert. "Additive
        logistic regression: a statistical view of boosting" Ann. Statist.
        28 (2000), no. 2, 337--407. doi:10.1214/aos/1016218223.
        https://projecteuclid.org/euclid.aos/1016218223

    .. [2] Zahid, Faisal Maqbool and Gerhard Tutz. "Ridge estimation for
        multinomial logit models with symmetric side constraints."
        Computational Statistics 28 (2013): 1017-1034.
        http://epub.ub.uni-muenchen.de/11001/1/tr067.pdf
    """

    is_multiclass = True

    def symmetrize_raw_prediction(self, raw_prediction):
        return raw_prediction - np.mean(raw_prediction, axis=1)[:, np.newaxis]

        # geometric mean as reference category
        gm = gmean(y_pred, axis=1)
        return np.log(y_pred / gm[:, np.newaxis], out=out)

            return softmax(raw_prediction, copy=True)
        else:
            np.copyto(out, raw_prediction)
            softmax(out, copy=False)
            return out

            )

        x : ndarray

        Returns
        -------
        """
        else:

        else:

        out : array

        Returns
        -------
        out : array
        """

        out : array

        Returns
        -------
        out : array
        """

            return out
        else:

        out *= 0.5
        return out

        else:
            return out

class BaseLoss:
    """Base class for a loss function of 1-dimensional targets.

    Conventions:

        - y_true.shape = sample_weight.shape = (n_samples,)
        - y_pred.shape = raw_prediction.shape = (n_samples,)
        - If is_multiclass is true (multiclass classification), then
          y_pred.shape = raw_prediction.shape = (n_samples, n_classes)
          Note that this corresponds to the return value of decision_function.

    y_true, y_pred, sample_weight and raw_prediction must either be all float64
    or all float32.
    gradient and hessian must be either both float64 or both float32.

    Note that y_pred = link.inverse(raw_prediction).

    Specific loss classes can inherit specific link classes to satisfy
    BaseLink's abstractmethods.

    sample_weight : {None, ndarray}
        If sample_weight is None, the hessian might be constant.
    n_classes : {None, int}
        The number of classes for classification, else None.

    closs: CyLossFunction
    link : BaseLink
    interval_y_true : Interval
        Valid interval for y_true
    interval_y_pred : Interval
        Valid Interval for y_pred
    differentiable : bool
        Indicates whether or not loss function is differentiable in
        raw_prediction everywhere.
    need_update_leaves_values : bool
        Indicates whether decision trees in gradient boosting need to uptade
        leave values after having been fit to the (negative) gradients.
    approx_hessian : bool
        Indicates whether the hessian is approximated or exact. If,
        approximated, it should be larger or equal to the exact one.
    constant_hessian : bool
        Indicates whether the hessian is one for this loss.
    is_multiclass : bool
        Indicates whether n_classes > 2 is allowed.
    """

    # For gradient boosted decision trees:
    # This variable indicates whether the loss requires the leaves values to
    # be updated once the tree has been trained. The trees are trained to
    # predict a Newton-Raphson step (see grower._finalize_leaf()). But for
    # some losses (e.g. least absolute deviation) we need to adjust the tree
    # values to account for the "line search" of the gradient descent
    # procedure. See the original paper Greedy Function Approximation: A
    # Gradient Boosting Machine by Friedman
    # (https://statweb.stanford.edu/~jhf/ftp/trebst.pdf) for the theory.
    differentiable = True
    need_update_leaves_values = False
    is_multiclass = False

    def __init__(self, closs, link, n_classes=None):
        self.closs = closs
        self.link = link
        self.approx_hessian = False
        self.constant_hessian = False
        self.n_classes = n_classes
        self.interval_y_true = Interval(-np.inf, np.inf, False, False)
        self.interval_y_pred = self.link.interval_y_pred

    def in_y_true_range(self, y):
        """Return True if y is in the valid range of y_true.

        y : ndarray
        """
        return self.interval_y_true.includes(y)

    def in_y_pred_range(self, y):
        """Return True if y is in the valid range of y_pred.

        y : ndarray
        """
        return self.interval_y_pred.includes(y)

    def loss(
        self,
        y_true,
        raw_prediction,
        loss_out=None,
    ):
        """Compute the pointwise loss value for each input.

        y_true : C-contiguous array of shape (n_samples,)
            Observed, true target values.
        raw_prediction : C-contiguous array of shape (n_samples,) or array of \
            shape (n_samples, n_classes)
        sample_weight : None or C-contiguous array of shape (n_samples,)
        loss_out : None or C-contiguous array of shape (n_samples,)
            A location into which the result is stored. If None, a new array
            might be created.
        n_threads : int, default=1
            Might use openmp thread parallelism.

        Returns
        -------
        loss : array of shape (n_samples,)
            Element-wise loss function.
        """
        if loss_out is None:
            loss_out = np.empty_like(y_true)
        # Be graceful to shape (n_samples, 1) -> (n_samples,)
        if raw_prediction.ndim == 2 and raw_prediction.shape[1] == 1:
            raw_prediction = raw_prediction.squeeze(1)

        self.closs.loss(
            y_true=y_true,
            loss_out=loss_out,
        )
        return loss_out

    def loss_gradient(
        self,
        y_true,
        gradient_out=None,
    ):
        """Compute loss and gradient w.r.t. raw_prediction for each input.

            A location into which the loss is stored. If None, a new array
        gradient_out : None or C-contiguous array of shape (n_samples,) or array \
            of shape (n_samples, n_classes)
            A location into which the gradient is stored. If None, a new array

        Returns
        -------

        gradient : array of shape (n_samples,) or (n_samples, n_classes)
            Element-wise gradients.
        """
            if gradient_out is None:
                loss_out = np.empty_like(y_true)
                gradient_out = np.empty_like(raw_prediction)
            else:
                loss_out = np.empty_like(y_true, dtype=gradient_out.dtype)
        elif gradient_out is None:
            gradient_out = np.empty_like(raw_prediction, dtype=loss_out.dtype)

        if gradient_out.ndim == 2 and gradient_out.shape[1] == 1:
            gradient_out = gradient_out.squeeze(1)

        self.closs.loss_gradient(
            gradient_out=gradient_out,
        )
        return loss_out, gradient_out

    def gradient(
        self,
        y_true,
    ):
        """Compute gradient of loss w.r.t raw_prediction for each input.

        Returns
        -------
        """
        if gradient_out is None:
            gradient_out = np.empty_like(raw_prediction)

        self.closs.gradient(
        )
        return gradient_out

    def gradient_hessian(
        self,
        y_true,
        hessian_out=None,
    ):
        """Compute gradient and hessian of loss w.r.t raw_prediction.

        hessian_out : None or C-contiguous array of shape (n_samples,) or array \
            A location into which the hessian is stored. If None, a new array

        Returns
        -------
        gradient : arrays of shape (n_samples,) or (n_samples, n_classes)

        hessian : arrays of shape (n_samples,) or (n_samples, n_classes)
            Element-wise hessians.
        """
            if hessian_out is None:
                hessian_out = np.empty_like(raw_prediction)
            else:
                gradient_out = np.empty_like(hessian_out)
        elif hessian_out is None:
            hessian_out = np.empty_like(gradient_out)

        if hessian_out.ndim == 2 and hessian_out.shape[1] == 1:
            hessian_out = hessian_out.squeeze(1)

        self.closs.gradient_hessian(
            hessian_out=hessian_out,
        )
        return gradient_out, hessian_out

    def __call__(self, y_true, raw_prediction, sample_weight=None, n_threads=1):
        """Compute the weighted average loss.

        Returns
        -------
            Mean or averaged loss function.
        """
        return np.average(
            self.loss(
                y_true=y_true,
                raw_prediction=raw_prediction,
                sample_weight=None,
                loss_out=None,
            ),
            weights=sample_weight,
        )

    def fit_intercept_only(self, y_true, sample_weight=None):
        """Compute raw_prediction of an intercept-only model.

        This can be used as initial estimates of predictions, i.e. before the
        first iteration in fit.

        sample_weight : None or array of shape (n_samples,)

        Returns
        -------
        raw_prediction : numpy scalar or array of shape (n_classes,)
            Raw predictions of an intercept-only model.
        """
        # As default, take weighted average of the target over the samples
        # axis=0 and then transform into link-scale (raw_prediction).
        y_pred = np.average(y_true, weights=sample_weight, axis=0)
        eps = 10 * np.finfo(y_pred.dtype).eps

        if self.interval_y_pred.low == -np.inf:
            a_min = None
        elif self.interval_y_pred.low_inclusive:
            a_min = self.interval_y_pred.low
        else:
            a_min = self.interval_y_pred.low + eps

        if self.interval_y_pred.high == np.inf:
            a_max = None
        elif self.interval_y_pred.high_inclusive:
            a_max = self.interval_y_pred.high
        else:
            a_max = self.interval_y_pred.high - eps

        if a_min is None and a_max is None:
            return self.link.link(y_pred)
        else:
            return self.link.link(np.clip(y_pred, a_min, a_max))

    def constant_to_optimal_zero(self, y_true, sample_weight=None):
        """Calculate term dropped in loss.

        With this term added, the loss of perfect predictions is zero.
        """
        return np.zeros_like(y_true)

    def init_gradient_and_hessian(self, n_samples, dtype=np.float64, order="F"):
        """Initialize arrays for gradients and hessians.

        Unless hessians are constant, arrays are initialized with undefined values.

        n_samples : int
            The number of samples, usually passed to `fit()`.
        dtype : {np.float64, np.float32}, default=np.float64
            The dtype of the arrays gradient and hessian.
        order : {'C', 'F'}, default='F'
            Order of the arrays gradient and hessian. The default 'F' makes the arrays
            contiguous along samples.

        Returns
        -------
        gradient : C-contiguous array of shape (n_samples,) or array of shape \
            (n_samples, n_classes)
            Empty array (allocated but not initialized) to be used as argument
            gradient_out.
        hessian : C-contiguous array of shape (n_samples,), array of shape
            (n_samples, n_classes) or shape (1,)
            Empty (allocated but not initialized) array to be used as argument
            hessian_out.
            If constant_hessian is True (e.g. `HalfSquaredError`), the array is
            initialized to ``1``.
        """
        if dtype not in (np.float32, np.float64):
                "Valid options for 'dtype' are np.float32 and np.float64. "
                f"Got dtype={dtype} instead."
            )

        if self.is_multiclass:
            shape = (n_samples, self.n_classes)
        else:
            shape = (n_samples,)
        gradient = np.empty(shape=shape, dtype=dtype, order=order)

        if self.constant_hessian:
            # If the hessians are constant, we consider them equal to 1.
            # - This is correct for HalfSquaredError
            # - For AbsoluteError, hessians are actually 0, but they are
            #   always ignored anyway.
            hessian = np.ones(shape=(1,), dtype=dtype)
        else:
            hessian = np.empty(shape=shape, dtype=dtype, order=order)

        return gradient, hessian

class HalfSquaredError(BaseLoss):
    """Half squared error with identity link, for regression.

    Domain:
    y_true and y_pred all real numbers

    Link:
    y_pred = raw_prediction

    For a given sample x_i, half squared error is defined as::

        loss(x_i) = 0.5 * (y_true_i - raw_prediction_i)**2

    The factor of 0.5 simplifies the computation of gradients and results in a
    unit hessian (and is consistent with what is done in LightGBM). It is also
    half the Normal distribution deviance.
    """

    def __init__(self, sample_weight=None):
        super().__init__(closs=CyHalfSquaredError(), link=IdentityLink())
        self.constant_hessian = sample_weight is None

class AbsoluteError(BaseLoss):
    """Absolute error with identity link, for regression.

    Domain:

    Link:

    For a given sample x_i, the absolute error is defined as::

        loss(x_i) = |y_true_i - raw_prediction_i|

    Note that the exact hessian = 0 almost everywhere (except at one point, therefore
    differentiable = False). Optimization routines like in HGBT, however, need a
    hessian > 0. Therefore, we assign 1.
    """

    differentiable = False
    need_update_leaves_values = True

        super().__init__(closs=CyAbsoluteError(), link=IdentityLink())
        self.approx_hessian = True

        This is the weighted median of the target, i.e. over the samples
        axis=0.
        """
            return np.median(y_true, axis=0)
        else:
            return _weighted_percentile(y_true, sample_weight, 50)

class PinballLoss(BaseLoss):
    """Quantile loss aka pinball loss, for regression.

    Domain:
    quantile in (0, 1)

    Link:

    For a given sample x_i, the pinball loss is defined as::

        loss(x_i) = rho_{quantile}(y_true_i - raw_prediction_i)

        rho_{quantile}(u) = u * (quantile - 1_{u<0})
                          = -u *(1 - quantile)  if u < 0
                             u * quantile       if u >= 0

    Note: 2 * PinballLoss(quantile=0.5) equals AbsoluteError().

    Additional Attributes
    ---------------------
    quantile : float
        The quantile level of the quantile to be estimated. Must be in range (0, 1).
    """

    def __init__(self, sample_weight=None, quantile=0.5):
        check_scalar(
            quantile,
            "quantile",
            target_type=numbers.Real,
            min_val=0,
            max_val=1,
            include_boundaries="neither",
        )
            closs=CyPinballLoss(quantile=float(quantile)),
            link=IdentityLink(),
        )

        axis=0.
        """
            return np.percentile(y_true, 100 * self.closs.quantile, axis=0)
        else:
            return _weighted_percentile(
                y_true, sample_weight, 100 * self.closs.quantile
            )

class HuberLoss(BaseLoss):
    """Huber loss, for regression.

    Domain:

    Link:

    For a given sample x_i, the Huber loss is defined as::

        loss(x_i) = 1/2 * abserr**2            if abserr <= delta
                    delta * (abserr - delta/2) if abserr > delta

        abserr = |y_true_i - raw_prediction_i|
        delta = quantile(abserr, self.quantile)

    Note: HuberLoss(quantile=1) equals HalfSquaredError and HuberLoss(quantile=0)
    equals delta * (AbsoluteError() - delta/2).

        The quantile level which defines the breaking point `delta` to distinguish
        between absolute error and squared error. Must be in range (0, 1).

     Reference
    ---------
    .. [1] Friedman, J.H. (2001). :doi:`Greedy function approximation: A gradient
      boosting machine <10.1214/aos/1013203451>`.
      Annals of Statistics, 29, 1189-1232.
    """

    def __init__(self, sample_weight=None, quantile=0.9, delta=0.5):
            quantile,
        )
        self.quantile = quantile  # This is better stored outside of Cython.
            closs=CyHuberLoss(delta=float(delta)),
        )

        axis=0.
        """
        # See formula before algo 4 in Friedman (2001), but we apply it to y_true,
        # not to the residual y_true - raw_prediction. An estimator like
        # HistGradientBoostingRegressor might then call it on the residual, e.g.
        # fit_intercept_only(y_true - raw_prediction).
            median = np.percentile(y_true, 50, axis=0)
        else:
            median = _weighted_percentile(y_true, sample_weight, 50)
        diff = y_true - median
        term = np.sign(diff) * np.minimum(self.closs.delta, np.abs(diff))
        return median + np.average(term, weights=sample_weight)

class HalfPoissonLoss(BaseLoss):
    """Half Poisson deviance loss with log-link, for regression.

    Domain:
    y_true in non-negative real numbers
    y_pred in positive real numbers

    Link:
    y_pred = exp(raw_prediction)

    For a given sample x_i, half the Poisson deviance is defined as::

        loss(x_i) = y_true_i * log(y_true_i/exp(raw_prediction_i))
                    - y_true_i + exp(raw_prediction_i)

    Half the Poisson deviance is actually the negative log-likelihood up to
    constant terms (not involving raw_prediction) and simplifies the
    computation of the gradients.
    We also skip the constant term `y_true_i * log(y_true_i) - y_true_i`.
    """

        super().__init__(closs=CyHalfPoissonLoss(), link=LogLink())
        self.interval_y_true = Interval(0, np.inf, True, False)

        term = xlogy(y_true, y_true) - y_true
            term *= sample_weight
        return term

class HalfGammaLoss(BaseLoss):
    """Half Gamma deviance loss with log-link, for regression.

    Domain:
    y_true and y_pred in positive real numbers

    Link:

    For a given sample x_i, half Gamma deviance loss is defined as::

        loss(x_i) = log(exp(raw_prediction_i)/y_true_i)
                    + y_true/exp(raw_prediction_i) - 1

    Half the Gamma deviance is actually proportional to the negative log-
    likelihood up to constant terms (not involving raw_prediction) and
    simplifies the computation of the gradients.
    We also skip the constant term `-log(y_true_i) - 1`.
    """

        super().__init__(closs=CyHalfGammaLoss(), link=LogLink())
        self.interval_y_true = Interval(0, np.inf, False, False)

        term = -np.log(y_true) - 1

class HalfTweedieLoss(BaseLoss):
    """Half Tweedie deviance loss with log-link, for regression.

    Domain:
    y_true in real numbers for power <= 0
    y_true in non-negative real numbers for 0 < power < 2
    y_true in positive real numbers for 2 <= power
    power in real numbers

    Link:

    For a given sample x_i, half Tweedie deviance loss with p=power is defined
    as::

        loss(x_i) = max(y_true_i, 0)**(2-p) / (1-p) / (2-p)
                    - y_true_i * exp(raw_prediction_i)**(1-p) / (1-p)
                    + exp(raw_prediction_i)**(2-p) / (2-p)

    Taking the limits for p=0, 1, 2 gives HalfSquaredError with a log link,
    HalfPoissonLoss and HalfGammaLoss.

    We also skip constant terms, but those are different for p=0, 1, 2.
    Therefore, the loss is not continuous in `power`.

    Note furthermore that although no Tweedie distribution exists for
    0 < power < 1, it still gives a strictly consistent scoring function for
    the expectation.
    """

    def __init__(self, sample_weight=None, power=1.5):
            closs=CyHalfTweedieLoss(power=float(power)),
            link=LogLink(),
        )
        if self.closs.power <= 0:
            self.interval_y_true = Interval(-np.inf, np.inf, False, False)
        elif self.closs.power < 2:
            self.interval_y_true = Interval(0, np.inf, True, False)
        else:
            self.interval_y_true = Interval(0, np.inf, False, False)

        if self.closs.power == 0:
            return HalfSquaredError().constant_to_optimal_zero(
                y_true=y_true, sample_weight=sample_weight
            )
        elif self.closs.power == 1:
            return HalfPoissonLoss().constant_to_optimal_zero(
            )
        elif self.closs.power == 2:
            return HalfGammaLoss().constant_to_optimal_zero(
            )
        else:
            p = self.closs.power
            term = np.power(np.maximum(y_true, 0), 2 - p) / (1 - p) / (2 - p)
                term *= sample_weight
            return term

class HalfTweedieLossIdentity(BaseLoss):
    """Half Tweedie deviance loss with identity link, for regression.

    Domain:
    y_pred in positive real numbers for power != 0
    y_pred in real numbers for power = 0

    Link:

    as::

                    - y_true_i * raw_prediction_i**(1-p) / (1-p)
                    + raw_prediction_i**(2-p) / (2-p)

    Note that the minimum value of this loss is 0.

    """

            closs=CyHalfTweedieLossIdentity(power=float(power)),
        )
        else:

            self.interval_y_pred = Interval(-np.inf, np.inf, False, False)
        else:
            self.interval_y_pred = Interval(0, np.inf, False, False)

class HalfBinomialLoss(BaseLoss):
    """Half Binomial deviance loss with logit link, for binary classification.

    This is also know as binary cross entropy, log-loss and logistic loss.

    Domain:
    y_true in [0, 1], i.e. regression on the unit interval
    y_pred in (0, 1), i.e. boundaries excluded

    Link:
    y_pred = expit(raw_prediction)

    For a given sample x_i, half Binomial deviance is defined as the negative
    log-likelihood of the Binomial/Bernoulli distribution and can be expressed
    as::

        loss(x_i) = log(1 + exp(raw_pred_i)) - y_true_i * raw_pred_i

    See The Elements of Statistical Learning, by Hastie, Tibshirani, Friedman,
    section 4.4.1 (about logistic regression).

    Note that the formulation works for classification, y = {0, 1}, as well as
    logistic regression, y = [0, 1].
    If you add `constant_to_optimal_zero` to the loss, you get half the
    Bernoulli/binomial deviance.

    More details: Inserting the predicted probability y_pred = expit(raw_prediction)
    in the loss gives the well known::

        loss(x_i) = - y_true_i * log(y_pred_i) - (1 - y_true_i) * log(1 - y_pred_i)
    """

            closs=CyHalfBinomialLoss(),
            link=LogitLink(),
            n_classes=2,
        )
        self.interval_y_true = Interval(0, 1, True, True)

        # This is non-zero only if y_true is neither 0 nor 1.
        term = xlogy(y_true, y_true) + xlogy(1 - y_true, 1 - y_true)

    def predict_proba(self, raw_prediction):
        """Predict probabilities.

        raw_prediction : array of shape (n_samples,) or (n_samples, 1)

        Returns
        -------
        proba : array of shape (n_samples, 2)
            Element-wise class probabilities.
        """
        proba = np.empty((raw_prediction.shape[0], 2), dtype=raw_prediction.dtype)
        proba[:, 1] = self.link.inverse(raw_prediction)
        proba[:, 0] = 1 - proba[:, 1]

class HalfMultinomialLoss(BaseLoss):
    """Categorical cross-entropy loss, for multiclass classification.

    Domain:
    y_true in {0, 1, 2, 3, .., n_classes - 1}
    y_pred has n_classes elements, each element in (0, 1)

    Link:
    y_pred = softmax(raw_prediction)

    Note: We assume y_true to be already label encoded. The inverse link is
    softmax. But the full link function is the symmetric multinomial logit
    function.

    For a given sample x_i, the categorical cross-entropy loss is defined as
    the negative log-likelihood of the multinomial distribution, it
    generalizes the binary cross-entropy to more than 2 classes::

        loss_i = log(sum(exp(raw_pred_{i, k}), k=0..n_classes-1))
                - sum(y_true_{i, k} * raw_pred_{i, k}, k=0..n_classes-1)

    See [1].

    Note that for the hessian, we calculate only the diagonal part in the
    classes: If the full hessian for classes k and l and sample i is H_i_k_l,
    we calculate H_i_k_k, i.e. k=l.

    Reference
    ---------
    .. [1] :arxiv:`Simon, Noah, J. Friedman and T. Hastie.
        "A Blockwise Descent Algorithm for Group-penalized Multiresponse and
        Multinomial Regression".
        <1311.6529>`
    """

    def __init__(self, sample_weight=None, n_classes=3):
            closs=CyHalfMultinomialLoss(),
            link=MultinomialLogit(),
        )
        self.interval_y_pred = Interval(0, 1, False, False)

        y : ndarray
        """
        return self.interval_y_true.includes(y) and np.all(y.astype(int) == y)

        This is the softmax of the weighted average of the target, i.e. over
        the samples axis=0.
        """
        out = np.zeros(self.n_classes, dtype=y_true.dtype)
        eps = np.finfo(y_true.dtype).eps
        for k in range(self.n_classes):
            out[k] = np.average(y_true == k, weights=sample_weight, axis=0)
            out[k] = np.clip(out[k], eps, 1 - eps)
        return self.link.link(out[None, :]).reshape(-1)

        raw_prediction : array of shape (n_samples, n_classes)

        Returns
        -------
        proba : array of shape (n_samples, n_classes)
        """
        return self.link.inverse(raw_prediction)

    def gradient_proba(
        self,
        y_true,
        proba_out=None,
    ):
        """Compute gradient and class probabilities fow raw_prediction.

        gradient_out : None or array of shape (n_samples, n_classes)
        proba_out : None or array of shape (n_samples, n_classes)
            A location into which the class probabilities are stored. If None,
            a new array might be created.

        Returns
        -------
        gradient : array of shape (n_samples, n_classes)

        """
            if proba_out is None:
                proba_out = np.empty_like(raw_prediction)
            else:
                gradient_out = np.empty_like(proba_out)
        elif proba_out is None:
            proba_out = np.empty_like(gradient_out)

        self.closs.gradient_proba(
            proba_out=proba_out,
        )
        return gradient_out, proba_out

class ExponentialLoss(BaseLoss):
    """Exponential loss with (half) logit link, for binary classification.

    This is also know as boosting loss.

    Domain:

    Link:
    y_pred = expit(2 * raw_prediction)

    For a given sample x_i, the exponential loss is defined as::

        loss(x_i) = y_true_i * exp(-raw_pred_i)) + (1 - y_true_i) * exp(raw_pred_i)

    See:
    - J. Friedman, T. Hastie, R. Tibshirani.
      "Additive logistic regression: a statistical view of boosting (With discussion
      and a rejoinder by the authors)." Ann. Statist. 28 (2) 337 - 407, April 2000.
      https://doi.org/10.1214/aos/1016218223
    - A. Buja, W. Stuetzle, Y. Shen. (2005).
      "Loss Functions for Binary Class Probability Estimation and Classification:
      Structure and Applications."

    "exponential logistic" regression, y = [0, 1].
    Note that this is a proper scoring rule, but without it's canonical link.

    More details: Inserting the predicted probability
    y_pred = expit(2 * raw_prediction) in the loss gives::

        loss(x_i) = y_true_i * sqrt((1 - y_pred_i) / y_pred_i)
            + (1 - y_true_i) * sqrt(y_pred_i / (1 - y_pred_i))
    """

            closs=CyExponentialLoss(),
            link=HalfLogitLink(),
        )

        term = -2 * np.sqrt(y_true * (1 - y_true))

        Returns
        -------
        """

        y : ndarray
        """

        y : ndarray
        """

    def loss(
        self,
        y_true,
    ):

        Returns
        -------
        """

        )

        self,
        y_true,
    ):

        Returns
        -------

        """
            else:

        )

        self,
        y_true,
    ):

        Returns
        -------
        """

        )

        self,
        y_true,
    ):

        Returns
        -------

        """
            else:

        )

        Returns
        -------
        """
            ),
        )

        Returns
        -------
        """

        else:

        else:

        else:

        """

        Returns
        -------
        """
            )

        else:

        else:

        axis=0.
        """
        else:

            quantile,
        )
        )

        axis=0.
        """
        else:
            )

            quantile,
        )
        )

        axis=0.
        """
        else:

        )
        else:

            )
            )
            )
        else:

        )
        else:

        else:

        )

        Returns
        -------
        """

        )

        y : ndarray
        """

        """

        Returns
        -------
        """

        self,
        y_true,
    ):

        Returns
        -------

        """
            else:

        )

        )

def test_interval_raises():
    """Test that interval with low > high raises ValueError."""
        ValueError, match="One must have low <= high; got low=1, high=0."
    ):
        Interval(1, 0, False, False)

def test_is_in_range(interval):
    # make sure low and high are always within the interval, used for linspace
    low, high = _inclusive_low_high(interval)

    x = np.linspace(low, high, num=10)
    assert interval.includes(x)

    # x contains lower bound
    assert interval.includes(np.r_[x, interval.low]) == interval.low_inclusive

    # x contains upper bound
    assert interval.includes(np.r_[x, interval.high]) == interval.high_inclusive

    # x contains upper and lower bound
    assert interval.includes(np.r_[x, interval.low, interval.high]) == (
        interval.low_inclusive and interval.high_inclusive
    )

def test_link_inverse_identity(link, global_random_seed):
    # Test that link of inverse gives identity.
    link = link()
    n_samples, n_classes = 100, None
    # The values for `raw_prediction` are limited from -20 to 20 because in the
    # class `LogitLink` the term `expit(x)` comes very close to 1 for large
    # positive x and therefore loses precision.
    if link.is_multiclass:
        n_classes = 10
        raw_prediction = rng.uniform(low=-20, high=20, size=(n_samples, n_classes))
        if isinstance(link, MultinomialLogit):
            raw_prediction = link.symmetrize_raw_prediction(raw_prediction)
    elif isinstance(link, HalfLogitLink):
        raw_prediction = rng.uniform(low=-10, high=10, size=(n_samples))
    else:
        raw_prediction = rng.uniform(low=-20, high=20, size=(n_samples))

    assert_allclose(link.link(link.inverse(raw_prediction)), raw_prediction)
    y_pred = link.inverse(raw_prediction)
    assert_allclose(link.inverse(link.link(y_pred)), y_pred)

def test_link_out_argument(link):
    # Test that out argument gets assigned the result.
        raw_prediction = rng.normal(loc=0, scale=10, size=(n_samples, n_classes))
    else:
        # So far, the valid interval of raw_prediction is (-inf, inf) and
        # we do not need to distinguish.

    y_pred = link.inverse(raw_prediction, out=None)
    out = np.empty_like(raw_prediction)
    y_pred_2 = link.inverse(raw_prediction, out=out)
    assert_allclose(y_pred, out)
    assert_array_equal(out, y_pred_2)
    assert np.shares_memory(out, y_pred_2)

    out = np.empty_like(y_pred)
    raw_prediction_2 = link.link(y_pred, out=out)
    assert_allclose(raw_prediction, out)
    assert_array_equal(out, raw_prediction_2)
    assert np.shares_memory(out, raw_prediction_2)

def loss_instance_name(param):
    if isinstance(param, BaseLoss):
        loss = param
        name = loss.__class__.__name__
        if isinstance(loss, PinballLoss):
            name += f"(quantile={loss.closs.quantile})"
        elif isinstance(loss, HuberLoss):
            name += f"(quantile={loss.quantile}"
        elif hasattr(loss, "closs") and hasattr(loss.closs, "power"):
            name += f"(power={loss.closs.power})"
        return name
    else:
        return str(param)

def random_y_true_raw_prediction(
    loss, n_samples, y_bound=(-100, 100), raw_bound=(-5, 5), seed=42
):
    """Random generate y_true and raw_prediction in valid range."""
    if loss.is_multiclass:
        raw_prediction = np.empty((n_samples, loss.n_classes))
        raw_prediction.flat[:] = rng.uniform(
            low=raw_bound[0],
            high=raw_bound[1],
            size=n_samples * loss.n_classes,
        )
        y_true = np.arange(n_samples).astype(float) % loss.n_classes
    else:
        # If link is identity, we must respect the interval of y_pred:
        if isinstance(loss.link, IdentityLink):
            low, high = _inclusive_low_high(loss.interval_y_pred)
            low = np.amax([low, raw_bound[0]])
            high = np.amin([high, raw_bound[1]])
            raw_bound = (low, high)
        raw_prediction = rng.uniform(
            low=raw_bound[0], high=raw_bound[1], size=n_samples
        )
        # generate a y_true in valid range
        low, high = _inclusive_low_high(loss.interval_y_true)
        low = max(low, y_bound[0])
        high = min(high, y_bound[1])
        y_true = rng.uniform(low, high, size=n_samples)
        # set some values at special boundaries
        if loss.interval_y_true.low == 0 and loss.interval_y_true.low_inclusive:
            y_true[:: (n_samples // 3)] = 0
        if loss.interval_y_true.high == 1 and loss.interval_y_true.high_inclusive:
            y_true[1 :: (n_samples // 3)] = 1

    return y_true, raw_prediction

def numerical_derivative(func, x, eps):
    """Helper function for numerical (first) derivatives."""
    # For numerical derivatives, see
    # https://en.wikipedia.org/wiki/Numerical_differentiation
    # https://en.wikipedia.org/wiki/Finite_difference_coefficient
    # We use central finite differences of accuracy 4.
    h = np.full_like(x, fill_value=eps)
    f_minus_2h = func(x - 2 * h)
    f_minus_1h = func(x - h)
    f_plus_1h = func(x + h)
    f_plus_2h = func(x + 2 * h)
    return (-f_plus_2h + 8 * f_plus_1h - 8 * f_minus_1h + f_minus_2h) / (12.0 * eps)

def test_loss_boundary(loss):
    """Test interval ranges of y_true and y_pred in losses."""
        n_classes = 3  # default value
        y_true = np.tile(np.linspace(0, n_classes - 1, num=n_classes), 3)
    else:
        y_true = np.linspace(low, high, num=10)

    # add boundaries if they are included
    if loss.interval_y_true.low_inclusive:
        y_true = np.r_[y_true, loss.interval_y_true.low]
    if loss.interval_y_true.high_inclusive:
        y_true = np.r_[y_true, loss.interval_y_true.high]

    assert loss.in_y_true_range(y_true)

    n = y_true.shape[0]
    low, high = _inclusive_low_high(loss.interval_y_pred)
        y_pred = np.empty((n, n_classes))
        y_pred[:, 0] = np.linspace(low, high, num=n)
        y_pred[:, 1] = 0.5 * (1 - y_pred[:, 0])
        y_pred[:, 2] = 0.5 * (1 - y_pred[:, 0])
    else:
        y_pred = np.linspace(low, high, num=n)

    assert loss.in_y_pred_range(y_pred)

    # calculating losses should not fail
    raw_prediction = loss.link.link(y_pred)
    loss.loss(y_true=y_true, raw_prediction=raw_prediction)

def test_loss_boundary_y_true(loss, y_true_success, y_true_fail):
    """Test boundaries of y_true for loss functions."""
    for y in y_true_success:
        assert loss.in_y_true_range(np.array([y]))
    for y in y_true_fail:
        assert not loss.in_y_true_range(np.array([y]))

def test_loss_boundary_y_pred(loss, y_pred_success, y_pred_fail):
    """Test boundaries of y_pred for loss functions."""
    for y in y_pred_success:
        assert loss.in_y_pred_range(np.array([y]))
    for y in y_pred_fail:
        assert not loss.in_y_pred_range(np.array([y]))

def test_loss_on_specific_values(
    loss, y_true, raw_prediction, loss_true, gradient_true, hessian_true
):
    """Test losses, gradients and hessians at specific values."""
    loss1 = loss(y_true=np.array([y_true]), raw_prediction=np.array([raw_prediction]))
    grad1 = loss.gradient(
        y_true=np.array([y_true]), raw_prediction=np.array([raw_prediction])
    )
    loss2, grad2 = loss.loss_gradient(
    )
    grad3, hess = loss.gradient_hessian(
    )

    assert loss1 == approx(loss_true, rel=1e-15, abs=1e-15)
    assert loss2 == approx(loss_true, rel=1e-15, abs=1e-15)

    if gradient_true is not None:
        assert grad1 == approx(gradient_true, rel=1e-15, abs=1e-15)
        assert grad2 == approx(gradient_true, rel=1e-15, abs=1e-15)
        assert grad3 == approx(gradient_true, rel=1e-15, abs=1e-15)

    if hessian_true is not None:
        assert hess == approx(hessian_true, rel=1e-15, abs=1e-15)

def test_loss_dtype(
    loss, readonly_memmap, dtype_in, dtype_out, sample_weight, out1, out2, n_threads
):
    """Test acceptance of dtypes, readonly and writeable arrays in loss functions.

    Check that loss accepts if all input arrays are either all float32 or all
    float64, and all output arrays are either all float32 or all float64.

    Also check that input arrays can be readonly, e.g. memory mapped.
    """
    loss = loss()
    # generate a y_true and raw_prediction in valid range
    n_samples = 5
    y_true, raw_prediction = random_y_true_raw_prediction(
        y_bound=(-100, 100),
        raw_bound=(-10, 10),
        seed=42,
    )
    y_true = y_true.astype(dtype_in)
    raw_prediction = raw_prediction.astype(dtype_in)

        sample_weight = np.array([2.0] * n_samples, dtype=dtype_in)
    if out1 is not None:
        out1 = np.empty_like(y_true, dtype=dtype_out)
    if out2 is not None:
        out2 = np.empty_like(raw_prediction, dtype=dtype_out)

    if readonly_memmap:
        y_true = create_memmap_backed_data(y_true)
        raw_prediction = create_memmap_backed_data(raw_prediction)
            sample_weight = create_memmap_backed_data(sample_weight)

    l = loss.loss(
        y_true=y_true,
        raw_prediction=raw_prediction,
        loss_out=out1,
    )
    assert l is out1 if out1 is not None else True
    g = loss.gradient(
        gradient_out=out2,
    )
    assert g is out2 if out2 is not None else True
    l, g = loss.loss_gradient(
    )
    if out1 is not None and loss.is_multiclass:
        out1 = np.empty_like(raw_prediction, dtype=dtype_out)
    g, h = loss.gradient_hessian(
        gradient_out=out1,
        hessian_out=out2,
    )
    assert g is out1 if out1 is not None else True
    assert h is out2 if out2 is not None else True
    loss(y_true=y_true, raw_prediction=raw_prediction, sample_weight=sample_weight)
    loss.fit_intercept_only(y_true=y_true, sample_weight=sample_weight)
    loss.constant_to_optimal_zero(y_true=y_true, sample_weight=sample_weight)
    if hasattr(loss, "predict_proba"):
        loss.predict_proba(raw_prediction=raw_prediction)
    if hasattr(loss, "gradient_proba"):
        g, p = loss.gradient_proba(
            gradient_out=out1,
            proba_out=out2,
        )
        assert g is out1 if out1 is not None else True
        assert p is out2 if out2 is not None else True

def test_loss_same_as_C_functions(loss, sample_weight):
    """Test that Python and Cython functions return same results."""
        n_samples=20,
        seed=42,
    )
    if sample_weight == "range":
        sample_weight = np.linspace(1, y_true.shape[0], num=y_true.shape[0])

    out_l1 = np.empty_like(y_true)
    out_l2 = np.empty_like(y_true)
    out_g1 = np.empty_like(raw_prediction)
    out_g2 = np.empty_like(raw_prediction)
    out_h1 = np.empty_like(raw_prediction)
    out_h2 = np.empty_like(raw_prediction)
    loss.loss(
        loss_out=out_l1,
    )
    loss.closs.loss(
        loss_out=out_l2,
    )
    assert_allclose(out_l1, out_l2)
    loss.gradient(
        gradient_out=out_g1,
    )
    loss.closs.gradient(
        gradient_out=out_g2,
    )
    assert_allclose(out_g1, out_g2)
    loss.closs.loss_gradient(
    )
    )
        hessian_out=out_h1,
    )
    loss.closs.gradient_hessian(
        hessian_out=out_h2,
    )
    assert_allclose(out_h1, out_h2)

def test_loss_gradients_are_the_same(loss, sample_weight, global_random_seed):
    """Test that loss and gradient are the same across different functions.

    Also test that output arguments contain correct results.
    """
        seed=global_random_seed,
    )

    out_g3 = np.empty_like(raw_prediction)
    out_h3 = np.empty_like(raw_prediction)

    l1 = loss.loss(
    )
    g1 = loss.gradient(
    )
    l2, g2 = loss.loss_gradient(
    )
    g3, h3 = loss.gradient_hessian(
        gradient_out=out_g3,
        hessian_out=out_h3,
    )
    assert_allclose(l1, l2)
    assert_array_equal(l1, out_l1)
    assert np.shares_memory(l1, out_l1)
    assert_array_equal(l2, out_l2)
    assert np.shares_memory(l2, out_l2)
    assert_allclose(g1, g2)
    assert_allclose(g1, g3)
    assert_array_equal(g1, out_g1)
    assert np.shares_memory(g1, out_g1)
    assert_array_equal(g2, out_g2)
    assert np.shares_memory(g2, out_g2)
    assert_array_equal(g3, out_g3)
    assert np.shares_memory(g3, out_g3)

        assert loss.is_multiclass  # only for HalfMultinomialLoss
        out_g4 = np.empty_like(raw_prediction)
        out_proba = np.empty_like(raw_prediction)
        g4, proba = loss.gradient_proba(
            gradient_out=out_g4,
            proba_out=out_proba,
        )
        assert_allclose(g1, out_g4)
        assert_allclose(g1, g4)
        assert_allclose(proba, out_proba)
        assert_allclose(np.sum(proba, axis=1), 1, rtol=1e-11)

def test_sample_weight_multiplies(loss, sample_weight, global_random_seed):
    """Test sample weights in loss, gradients and hessians.

    Make sure that passing sample weights to loss, gradient and hessian
    computation methods is equivalent to multiplying by the weights.
    """
        raw_bound=(-5, 5),
    )

    if sample_weight == "ones":
        sample_weight = np.ones(shape=n_samples, dtype=np.float64)
    else:
        sample_weight = rng.normal(size=n_samples).astype(np.float64)

        loss.loss(
        ),
        sample_weight
        * loss.loss(
            sample_weight=None,
        ),
    )

    losses, gradient = loss.loss_gradient(
    )
    losses_sw, gradient_sw = loss.loss_gradient(
    )
    assert_allclose(losses * sample_weight, losses_sw)
    if not loss.is_multiclass:
        assert_allclose(gradient * sample_weight, gradient_sw)
    else:
        assert_allclose(gradient * sample_weight[:, None], gradient_sw)

    gradient, hessian = loss.gradient_hessian(
    )
    gradient_sw, hessian_sw = loss.gradient_hessian(
    )
        assert_allclose(hessian * sample_weight, hessian_sw)
    else:
        assert_allclose(hessian * sample_weight[:, None], hessian_sw)

def test_graceful_squeezing(loss):
    """Test that reshaped raw_prediction gives same results."""
        seed=42,
    )

    if raw_prediction.ndim == 1:
        raw_prediction_2d = raw_prediction[:, None]
            loss.loss(y_true=y_true, raw_prediction=raw_prediction_2d),
            loss.loss(y_true=y_true, raw_prediction=raw_prediction),
        )
            loss.loss_gradient(y_true=y_true, raw_prediction=raw_prediction_2d),
            loss.loss_gradient(y_true=y_true, raw_prediction=raw_prediction),
        )
            loss.gradient(y_true=y_true, raw_prediction=raw_prediction_2d),
            loss.gradient(y_true=y_true, raw_prediction=raw_prediction),
        )
            loss.gradient_hessian(y_true=y_true, raw_prediction=raw_prediction_2d),
            loss.gradient_hessian(y_true=y_true, raw_prediction=raw_prediction),
        )

def test_loss_of_perfect_prediction(loss, sample_weight):
    """Test value of perfect predictions.

    Loss of y_pred = y_true plus constant_to_optimal_zero should sums up to
    zero.
    """
        # Use small values such that exp(value) is not nan.
        raw_prediction = np.array([-10, -0.1, 0, 0.1, 3, 10])
            eps = 1e-10
            low = loss.interval_y_pred.low
            if not loss.interval_y_pred.low_inclusive:
                low = low + eps
            high = loss.interval_y_pred.high
            if not loss.interval_y_pred.high_inclusive:
                high = high - eps
            raw_prediction = np.clip(raw_prediction, low, high)
        y_true = loss.link.inverse(raw_prediction)
    else:
        # HalfMultinomialLoss
        y_true = np.arange(loss.n_classes).astype(float)
        # raw_prediction with entries -exp(10), but +exp(10) on the diagonal
        # this is close enough to np.inf which would produce nan
        raw_prediction = np.full(
            shape=(loss.n_classes, loss.n_classes),
            fill_value=-np.exp(10),
            dtype=float,
        )
        raw_prediction.flat[:: loss.n_classes + 1] = np.exp(10)

    loss_value = loss.loss(
    )
    constant_term = loss.constant_to_optimal_zero(
        y_true=y_true, sample_weight=sample_weight
    )
    # Comparing loss_value + constant_term to zero would result in large
    # round-off errors.
    assert_allclose(loss_value, -constant_term, atol=1e-14, rtol=1e-15)

def test_gradients_hessians_numerically(loss, sample_weight, global_random_seed):
    """Test gradients and hessians with numerical derivatives.

    Gradient should equal the numerical derivatives of the loss function.
    Hessians should equal the numerical derivatives of gradients.
    """
    n_samples = 20
    )

    )

    assert g.shape == raw_prediction.shape
    assert h.shape == raw_prediction.shape

        def loss_func(x):
            return loss.loss(
                raw_prediction=x,
            )

        g_numeric = numerical_derivative(loss_func, raw_prediction, eps=1e-6)
        assert_allclose(g, g_numeric, rtol=5e-6, atol=1e-10)

        def grad_func(x):
            return loss.gradient(
            )

        h_numeric = numerical_derivative(grad_func, raw_prediction, eps=1e-6)
        if loss.approx_hessian:
            # TODO: What could we test if loss.approx_hessian?
            pass
        else:
            assert_allclose(h, h_numeric, rtol=5e-6, atol=1e-10)
    else:
        # For multiclass loss, we should only change the predictions of the
        # class for which the derivative is taken for, e.g. offset[:, k] = eps
        # for class k.
        # As a softmax is computed, offsetting the whole array by a constant
        # would have no effect on the probabilities, and thus on the loss.
        for k in range(loss.n_classes):

            def loss_func(x):
                raw = raw_prediction.copy()
                raw[:, k] = x
                return loss.loss(
                    y_true=y_true,
                    raw_prediction=raw,
                )

            g_numeric = numerical_derivative(loss_func, raw_prediction[:, k], eps=1e-5)
            assert_allclose(g[:, k], g_numeric, rtol=5e-6, atol=1e-10)

            def grad_func(x):
                return loss.gradient(
                )[:, k]

            h_numeric = numerical_derivative(grad_func, raw_prediction[:, k], eps=1e-6)
            if loss.approx_hessian:
                # TODO: What could we test if loss.approx_hessian?
                pass
            else:
                assert_allclose(h[:, k], h_numeric, rtol=5e-6, atol=1e-10)

def test_derivatives(loss, x0, y_true):
    """Test that gradients are zero at the minimum of the loss.

    We check this on a single value/sample using Halley's method with the
    first and second order derivatives computed by the Loss instance.
    Note that methods of Loss instances operate on arrays while the newton
    root finder expects a scalar or a one-element array for this purpose.
    """
    loss = _LOSSES[loss](sample_weight=None)
    y_true = np.array([y_true], dtype=np.float64)
    x0 = np.array([x0], dtype=np.float64)

    def func(x: np.ndarray) -> np.ndarray:
        """Compute loss plus constant term.

        The constant term is such that the minimum function value is zero,
        which is required by the Newton method.
        """
        return loss.loss(
            y_true=y_true, raw_prediction=x
        ) + loss.constant_to_optimal_zero(y_true=y_true)

    def fprime(x: np.ndarray) -> np.ndarray:
        return loss.gradient(y_true=y_true, raw_prediction=x)

    def fprime2(x: np.ndarray) -> np.ndarray:
        return loss.gradient_hessian(y_true=y_true, raw_prediction=x)[1]

    optimum = newton(
        func,
        x0=x0,
        fprime=fprime,
        fprime2=fprime2,
        maxiter=100,
        tol=5e-8,
    )

    # Need to ravel arrays because assert_allclose requires matching
    # dimensions.
    y_true = y_true.ravel()
    optimum = optimum.ravel()
    assert_allclose(loss.link.inverse(optimum), y_true)
    assert_allclose(func(optimum), 0, atol=1e-14)
    assert_allclose(loss.gradient(y_true=y_true, raw_prediction=optimum), 0, atol=5e-7)

def test_loss_intercept_only(loss, sample_weight):
    """Test that fit_intercept_only returns the argmin of the loss.

    Also test that the gradient is zero at the minimum.
    """
    n_samples = 50
        y_true = loss.link.inverse(np.linspace(-4, 4, num=n_samples))
    else:
        y_true = np.arange(n_samples).astype(np.float64) % loss.n_classes
        y_true[::5] = 0  # exceedance of class 0

        sample_weight = np.linspace(0.1, 2, num=n_samples)

    a = loss.fit_intercept_only(y_true=y_true, sample_weight=sample_weight)

    # find minimum by optimization
    def fun(x):
            raw_prediction = np.full(shape=(n_samples), fill_value=x)
        else:
            raw_prediction = np.ascontiguousarray(
                np.broadcast_to(x, shape=(n_samples, loss.n_classes))
            )
        return loss(
        )

        opt = minimize_scalar(fun, tol=1e-7, options={"maxiter": 100})
        grad = loss.gradient(
            raw_prediction=np.full_like(y_true, a),
        )
        assert a.shape == tuple()  # scalar
        assert a.dtype == y_true.dtype
        assert_all_finite(a)
        a == approx(opt.x, rel=1e-7)
        grad.sum() == approx(0, abs=1e-12)
    else:
        # The constraint corresponds to sum(raw_prediction) = 0. Without it, we would
        # need to apply loss.symmetrize_raw_prediction to opt.x before comparing.
        opt = minimize(
            fun,
            np.zeros((loss.n_classes)),
            tol=1e-13,
            options={"maxiter": 100},
            method="SLSQP",
            constraints=LinearConstraint(np.ones((1, loss.n_classes)), 0, 0),
        )
            raw_prediction=np.tile(a, (n_samples, 1)),
        )
        assert_allclose(a, opt.x, rtol=5e-6, atol=1e-12)
        assert_allclose(grad.sum(axis=0), 0, atol=1e-12)

def test_specific_fit_intercept_only(loss, func, random_dist, global_random_seed):
    """Test that fit_intercept_only returns the correct functional.

    We test the functional for specific, meaningful distributions, e.g.
    squared error estimates the expectation of a probability distribution.
    """
    if random_dist == "binomial":
        y_train = rng.binomial(1, 0.5, size=100)
    else:
        y_train = getattr(rng, random_dist)(size=100)
    baseline_prediction = loss.fit_intercept_only(y_true=y_train)
    # Make sure baseline prediction is the expected functional=func, e.g. mean
    # or median.
    assert_all_finite(baseline_prediction)
    assert baseline_prediction == approx(loss.link.link(func(y_train)))
    assert loss.link.inverse(baseline_prediction) == approx(func(y_train))
    if isinstance(loss, IdentityLink):
        assert_allclose(loss.link.inverse(baseline_prediction), baseline_prediction)

    # Test baseline at boundary
        y_train.fill(loss.interval_y_true.low)
        baseline_prediction = loss.fit_intercept_only(y_true=y_train)
        assert_all_finite(baseline_prediction)
        y_train.fill(loss.interval_y_true.high)

def test_multinomial_loss_fit_intercept_only():
    """Test that fit_intercept_only returns the mean functional for CCE."""
    loss = HalfMultinomialLoss(n_classes=n_classes)
    # Same logic as test_specific_fit_intercept_only. Here inverse link
    # function = softmax and link function = log - symmetry term.
    y_train = rng.randint(0, n_classes + 1, size=100).astype(np.float64)
    assert baseline_prediction.shape == (n_classes,)
    p = np.zeros(n_classes, dtype=y_train.dtype)
    for k in range(n_classes):
        p[k] = (y_train == k).mean()
    assert_allclose(baseline_prediction, np.log(p) - np.mean(np.log(p)))
    assert_allclose(baseline_prediction[None, :], loss.link.link(p[None, :]))

    for y_train in (np.zeros(shape=10), np.ones(shape=10)):
        y_train = y_train.astype(np.float64)
        assert baseline_prediction.dtype == y_train.dtype

def test_multinomial_cy_gradient(global_random_seed):
    """Test that Multinomial cy_gradient gives the same result as gradient.

    CyHalfMultinomialLoss does not inherit from CyLossFunction and has a different API.
    As a consequence, the functions like `loss` and `gradient` do not rely on `cy_loss`
    and `cy_gradient`.
    """
    n_classes = 5
    )
    sample_weight = np.linspace(0.1, 2, num=n_samples)

    grad1 = loss.closs._test_cy_gradient(
        raw_prediction=raw_prediction,  # needs to be C-contiguous
    )
    grad2 = loss.gradient(
    )
    assert_allclose(grad1, grad2)

def test_binomial_and_multinomial_loss(global_random_seed):
    """Test that multinomial loss with n_classes = 2 is the same as binomial loss."""
    binom = HalfBinomialLoss()
    multinom = HalfMultinomialLoss(n_classes=2)
    y_train = rng.randint(0, 2, size=n_samples).astype(np.float64)
    raw_prediction = rng.normal(size=n_samples)
    raw_multinom = np.empty((n_samples, 2))
    raw_multinom[:, 0] = -0.5 * raw_prediction
    raw_multinom[:, 1] = 0.5 * raw_prediction
        binom.loss(y_true=y_train, raw_prediction=raw_prediction),
        multinom.loss(y_true=y_train, raw_prediction=raw_multinom),
    )

def test_binomial_vs_alternative_formulation(y_true, y_pred, global_dtype):
    """Test that both formulations of the binomial deviance agree.

    Often, the binomial deviance or log loss is written in terms of a variable
    z in {-1, +1}, but we use y in {0, 1}, hence z = 2 * y - 1.
    ESL II Eq. (10.18):

        -loglike(z, f) = log(1 + exp(-2 * z * f))

    Note:
        - ESL 2*f = raw_prediction, hence the factor 2 of ESL disappears.
        - Deviance = -2*loglike + .., but HalfBinomialLoss is half of the
          deviance, hence the factor of 2 cancels in the comparison.
    """

    def alt_loss(y, raw_pred):
        z = 2 * y - 1
        return np.mean(np.log(1 + np.exp(-z * raw_pred)))

    def alt_gradient(y, raw_pred):
        # alternative gradient formula according to ESL
        z = 2 * y - 1
        return -z / (1 + np.exp(z * raw_pred))

    y_true = y_true.astype(global_dtype)
    y_pred = y_pred.astype(global_dtype)
    datum = (y_true, y_pred)

    assert bin_loss(*datum) == approx(alt_loss(*datum))
    assert_allclose(bin_loss.gradient(*datum), alt_gradient(*datum))

def test_predict_proba(loss, global_random_seed):
    """Test that predict_proba and gradient_proba work as expected."""
    )

        proba = loss.predict_proba(raw_prediction)
        assert proba.shape == (n_samples, loss.n_classes)
        assert np.sum(proba, axis=1) == approx(1, rel=1e-11)

        for grad, proba in (
            (None, None),
            (None, np.empty_like(raw_prediction)),
            (np.empty_like(raw_prediction), None),
            (np.empty_like(raw_prediction), np.empty_like(raw_prediction)),
        ):
            grad, proba = loss.gradient_proba(
                gradient_out=grad,
                proba_out=proba,
            )
            assert proba.shape == (n_samples, loss.n_classes)
            assert np.sum(proba, axis=1) == approx(1, rel=1e-11)
            assert_allclose(
                grad,
                loss.gradient(
                    raw_prediction=raw_prediction,
                    sample_weight=None,
                    gradient_out=None,
                ),
            )

def test_init_gradient_and_hessians(loss, sample_weight, dtype, order):
    """Test that init_gradient_and_hessian works as expected.

    passing sample_weight to a loss correctly influences the constant_hessian
    attribute, and consequently the shape of the hessian array.
    """
        sample_weight = np.ones(n_samples)
    loss = loss(sample_weight=sample_weight)
    gradient, hessian = loss.init_gradient_and_hessian(
        order=order,
    )
    if loss.constant_hessian:
        assert gradient.shape == (n_samples,)
        assert hessian.shape == (1,)
    elif loss.is_multiclass:
        assert gradient.shape == (n_samples, loss.n_classes)
        assert hessian.shape == (n_samples, loss.n_classes)
    else:
        assert hessian.shape == (n_samples,)

    assert gradient.dtype == dtype
    assert hessian.dtype == dtype

    if order == "C":
        assert gradient.flags.c_contiguous
        assert hessian.flags.c_contiguous
    else:
        assert gradient.flags.f_contiguous
        assert hessian.flags.f_contiguous

def test_init_gradient_and_hessian_raises(loss, params, err_msg):
    """Test that init_gradient_and_hessian raises errors for invalid input."""
    with pytest.raises((ValueError, TypeError), match=err_msg):
        gradient, hessian = loss.init_gradient_and_hessian(n_samples=5, **params)

def test_loss_init_parameter_validation(loss, params, err_type, err_msg):
    """Test that loss raises errors for invalid input."""
        loss(**params)

def test_loss_pickle(loss):
    """Test that losses can be pickled."""
        seed=42,
    )
    pickled_loss = pickle.dumps(loss)
    unpickled_loss = pickle.loads(pickled_loss)
    assert loss(y_true=y_true, raw_prediction=raw_prediction) == approx(
        unpickled_loss(y_true=y_true, raw_prediction=raw_prediction)
    )

def test_tweedie_log_identity_consistency(p):
    """Test for identical losses when only the link function is different."""
    half_tweedie_log = HalfTweedieLoss(power=p)
    half_tweedie_identity = HalfTweedieLossIdentity(power=p)
        loss=half_tweedie_log, n_samples=n_samples, seed=42
    )
    y_pred = half_tweedie_log.link.inverse(raw_prediction)  # exp(raw_prediction)

    # Let's compare the loss values, up to some constant term that is dropped
    # in HalfTweedieLoss but not in HalfTweedieLossIdentity.
    loss_log = half_tweedie_log.loss(
        y_true=y_true, raw_prediction=raw_prediction
    ) + half_tweedie_log.constant_to_optimal_zero(y_true)
    loss_identity = half_tweedie_identity.loss(
        y_true=y_true, raw_prediction=y_pred
    ) + half_tweedie_identity.constant_to_optimal_zero(y_true)
    # Note that HalfTweedieLoss ignores different constant terms than
    # HalfTweedieLossIdentity. Constant terms means terms not depending on
    # raw_prediction. By adding these terms, `constant_to_optimal_zero`, both losses
    # give the same values.
    assert_allclose(loss_log, loss_identity)

    # For gradients and hessians, the constant terms do not matter. We have, however,
    # to account for the chain rule, i.e. with x=raw_prediction
    #     gradient_log(x) = d/dx loss_log(x)
    #                     = d/dx loss_identity(exp(x))
    #                     = exp(x) * gradient_identity(exp(x))
    # Similarly,
    #     hessian_log(x) = exp(x) * gradient_identity(exp(x))
    #                    + exp(x)**2 * hessian_identity(x)
    gradient_log, hessian_log = half_tweedie_log.gradient_hessian(
    )
    gradient_identity, hessian_identity = half_tweedie_identity.gradient_hessian(
    )
    assert_allclose(gradient_log, y_pred * gradient_identity)
        hessian_log, y_pred * gradient_identity + y_pred**2 * hessian_identity
    )

        """

        else:
            )
        )

        z = 2 * y - 1

        z = 2 * y - 1

            )

            )

                )

                )[:, k]

def auc(x, y):
    """Compute Area Under the Curve (AUC) using the trapezoidal rule.

    This is a general function, given points on a curve.  For computing the
    area under the ROC-curve, see :func:`roc_auc_score`.  For an alternative
    way to summarize a precision-recall curve, see
    :func:`average_precision_score`.

    x : array-like of shape (n,)
        X coordinates. These must be either monotonic increasing or monotonic
        decreasing.
    y : array-like of shape (n,)
        Y coordinates.

    Returns
    -------
    auc : float
        Area Under the Curve.

    See Also
    --------
    roc_auc_score : Compute the area under the ROC curve.
    average_precision_score : Compute average precision from prediction scores.
    precision_recall_curve : Compute precision-recall pairs for different
        probability thresholds.

    Examples
    --------
    >>> from sklearn import metrics
    >>> y_true = np.array([1, 1, 2, 2])
    >>> y_score = np.array([0.1, 0.4, 0.35, 0.8])
    >>> fpr, tpr, thresholds = metrics.roc_curve(y_true, y_score, pos_label=2)
    >>> metrics.auc(fpr, tpr)
    0.75
    """
    check_consistent_length(x, y)
    x = column_or_1d(x)

    if x.shape[0] < 2:
            "At least 2 points are needed to compute area under curve, but x.shape = %s"
            % x.shape
        )

    direction = 1
    dx = np.diff(x)
    if np.any(dx < 0):
        if np.all(dx <= 0):
            direction = -1
        else:
            raise ValueError("x is neither increasing nor decreasing : {}.".format(x))

    area = direction * trapezoid(y, x)
    if isinstance(area, np.memmap):
        # Reductions such as .sum used internally in trapezoid do not return a
        # scalar by default for numpy.memmap instances contrary to
        # regular numpy.ndarray instances.
        area = area.dtype.type(area)
    return float(area)

def average_precision_score(
    y_true, y_score, *, average="macro", pos_label=1, sample_weight=None
):
    """Compute average precision (AP) from prediction scores.

    AP summarizes a precision-recall curve as the weighted mean of precisions
    achieved at each threshold, with the increase in recall from the previous
    threshold used as the weight:

    .. math::
        \\text{AP} = \\sum_n (R_n - R_{n-1}) P_n

    where :math:`P_n` and :math:`R_n` are the precision and recall at the nth
    threshold [1]_. This implementation is not interpolated and is different
    from computing the area under the precision-recall curve with the
    trapezoidal rule, which uses linear interpolation and can be too
    optimistic.

    Read more in the :ref:`User Guide <precision_recall_f_measure_metrics>`.

    y_true : array-like of shape (n_samples,) or (n_samples, n_classes)
        True binary labels or binary label indicators.

    y_score : array-like of shape (n_samples,) or (n_samples, n_classes)
        Target scores, can either be probability estimates of the positive
        class, confidence values, or non-thresholded measure of decisions
        (as returned by :term:`decision_function` on some classifiers).
        For :term:`decision_function` scores, values greater than or equal to
        zero should indicate the positive class.

    average : {'micro', 'samples', 'weighted', 'macro'} or None, \
            default='macro'
        If ``None``, the scores for each class are returned. Otherwise,
        this determines the type of averaging performed on the data:

        ``'micro'``:
            Calculate metrics globally by considering each element of the label
            indicator matrix as a label.
        ``'macro'``:
            Calculate metrics for each label, and find their unweighted
            mean.  This does not take label imbalance into account.
        ``'weighted'``:
            Calculate metrics for each label, and find their average, weighted
            by support (the number of true instances for each label).
        ``'samples'``:
            Calculate metrics for each instance, and find their average.

        Will be ignored when ``y_true`` is binary.

    pos_label : int, float, bool or str, default=1
        The label of the positive class. Only applied to binary ``y_true``.
        For multilabel-indicator ``y_true``, ``pos_label`` is fixed to 1.

    Returns
    -------
    average_precision : float
        Average precision score.

    See Also
    --------

    Notes
    -----
    .. versionchanged:: 0.19
      Instead of linearly interpolating between operating points, precisions
      are weighted by the change in recall since the last operating point.

    .. [1] `Wikipedia entry for the Average precision
           <https://en.wikipedia.org/w/index.php?title=Information_retrieval&
           oldid=793358396#Average_precision>`_

    Examples
    --------
    >>> from sklearn.metrics import average_precision_score
    >>> y_true = np.array([0, 0, 1, 1])
    >>> y_scores = np.array([0.1, 0.4, 0.35, 0.8])
    >>> average_precision_score(y_true, y_scores)
    0.83...
    >>> y_true = np.array([0, 0, 1, 1, 2, 2])
    >>> y_scores = np.array([
    ...     [0.7, 0.2, 0.1],
    ...     [0.4, 0.3, 0.3],
    ...     [0.1, 0.8, 0.1],
    ...     [0.2, 0.3, 0.5],
    ...     [0.4, 0.4, 0.2],
    ...     [0.1, 0.2, 0.7],
    ... ])
    0.77...
    """

    def _binary_uninterpolated_average_precision(
        y_true, y_score, pos_label=1, sample_weight=None
    ):
        precision, recall, _ = precision_recall_curve(
            y_true, y_score, pos_label=pos_label, sample_weight=sample_weight
        )
        # Return the step function integral
        # The following works because the last entry of precision is
        # guaranteed to be 1, as returned by precision_recall_curve.
        # Due to numerical error, we can get `-0.0` and we therefore clip it.
        return float(max(0.0, -np.sum(np.diff(recall) * np.array(precision)[:-1])))

    y_type = type_of_target(y_true, input_name="y_true")

    # Convert to Python primitive type to avoid NumPy type / Python str
    # comparison. See https://github.com/numpy/numpy/issues/6784
    present_labels = np.unique(y_true).tolist()

    if y_type == "binary":
        if len(present_labels) == 2 and pos_label not in present_labels:
                f"pos_label={pos_label} is not a valid label. It should be "
                f"one of {present_labels}"
            )

    elif y_type == "multilabel-indicator" and pos_label != 1:
            "Parameter pos_label is fixed to 1 for multilabel-indicator y_true. "
            "Do not set pos_label or set pos_label to 1."
        )

    elif y_type == "multiclass":
        if pos_label != 1:
                "Parameter pos_label is fixed to 1 for multiclass y_true. "
                "Do not set pos_label or set pos_label to 1."
            )
        y_true = label_binarize(y_true, classes=present_labels)

    average_precision = partial(
        _binary_uninterpolated_average_precision, pos_label=pos_label
    )
    return _average_binary_score(
        average_precision, y_true, y_score, average, sample_weight=sample_weight
    )

def det_curve(
    y_true, y_score, pos_label=None, sample_weight=None, drop_intermediate=False
):
    """Compute Detection Error Tradeoff (DET) for different probability thresholds.

    .. note::
       This metric is used for evaluation of ranking and error tradeoffs of
       a binary classification task.

    Read more in the :ref:`User Guide <det_curve>`.

    .. versionchanged:: 1.7
       An arbitrary threshold at infinity is added to represent a classifier
       that always predicts the negative class, i.e. `fpr=0` and `fnr=1`, unless
       `fpr=0` is already reached at a finite threshold.

    y_true : ndarray of shape (n_samples,)
        True binary labels. If labels are not either {-1, 1} or {0, 1}, then
        pos_label should be explicitly given.

    y_score : ndarray of shape of (n_samples,)
        (as returned by "decision_function" on some classifiers).

        When ``pos_label=None``, if `y_true` is in {-1, 1} or {0, 1},
        ``pos_label`` is set to 1, otherwise an error will be raised.

    drop_intermediate : bool, default=False
        Whether to drop thresholds where true positives (tp) do not change from
        the previous or subsequent threshold. All points with the same tp value
        have the same `fnr` and thus same y coordinate.

    Returns
    -------
    fpr : ndarray of shape (n_thresholds,)
        False positive rate (FPR) such that element i is the false positive
        rate of predictions with score >= thresholds[i]. This is occasionally
        referred to as false acceptance probability or fall-out.

    fnr : ndarray of shape (n_thresholds,)
        False negative rate (FNR) such that element i is the false negative
        referred to as false rejection or miss rate.

    thresholds : ndarray of shape (n_thresholds,)
        Decreasing thresholds on the decision function (either `predict_proba`
        or `decision_function`) used to compute FPR and FNR.

           An arbitrary threshold at infinity is added for the case `fpr=0`
           and `fnr=1`.

    See Also
    --------
    DetCurveDisplay.from_estimator : Plot DET curve given an estimator and
        some data.
    DetCurveDisplay.from_predictions : Plot DET curve given the true and
        predicted labels.
    DetCurveDisplay : DET curve visualization.
    roc_curve : Compute Receiver operating characteristic (ROC) curve.
    precision_recall_curve : Compute precision-recall curve.

    Examples
    --------
    >>> from sklearn.metrics import det_curve
    >>> fpr, fnr, thresholds = det_curve(y_true, y_scores)
    >>> fpr
    array([0.5, 0.5, 0. ])
    >>> fnr
    array([0. , 0.5, 0.5])
    >>> thresholds
    array([0.35, 0.4 , 0.8 ])
    """
    fps, tps, thresholds = _binary_clf_curve(
        y_true, y_score, pos_label=pos_label, sample_weight=sample_weight
    )

    # add a threshold at inf where the clf always predicts the negative class
    # i.e. tps = fps = 0
    tps = np.concatenate(([0], tps))
    fps = np.concatenate(([0], fps))
    thresholds = np.concatenate(([np.inf], thresholds))

    if drop_intermediate and len(fps) > 2:
        # Drop thresholds where true positives (tp) do not change from the
        # previous or subsequent threshold. As tp + fn, is fixed for a dataset,
        # this means the false negative rate (fnr) remains constant while the
        # false positive rate (fpr) changes, producing horizontal line segments
        # in the transformed (normal deviate) scale. These intermediate points
        # can be dropped to create lighter DET curve plots.
        optimal_idxs = np.where(
            np.concatenate(
                [[True], np.logical_or(np.diff(tps[:-1]), np.diff(tps[1:])), [True]]
            )
        )[0]
        fps = fps[optimal_idxs]
        tps = tps[optimal_idxs]
        thresholds = thresholds[optimal_idxs]

    if len(np.unique(y_true)) != 2:
            "Only one class is present in y_true. Detection error "
            "tradeoff curve is not defined in that case."
        )

    fns = tps[-1] - tps
    p_count = tps[-1]
    n_count = fps[-1]

    # start with false positives zero, which may be at a finite threshold
    first_ind = (
        fps.searchsorted(fps[0], side="right") - 1
        if fps.searchsorted(fps[0], side="right") > 0
        else None
    )
    # stop with false negatives zero
    last_ind = tps.searchsorted(tps[-1]) + 1
    sl = slice(first_ind, last_ind)

    # reverse the output such that list of false positives is decreasing
    return (fps[sl][::-1] / n_count, fns[sl][::-1] / p_count, thresholds[sl][::-1])

def _binary_roc_auc_score(y_true, y_score, sample_weight=None, max_fpr=None):
    """Binary roc auc score."""
            (
                "Only one class is present in y_true. ROC AUC score "
                "is not defined in that case."
            ),
            UndefinedMetricWarning,
        )
        return np.nan

    fpr, tpr, _ = roc_curve(y_true, y_score, sample_weight=sample_weight)
    if max_fpr is None or max_fpr == 1:
        return auc(fpr, tpr)
    if max_fpr <= 0 or max_fpr > 1:
        raise ValueError("Expected max_fpr in range (0, 1], got: %r" % max_fpr)

    # Add a single point at max_fpr by linear interpolation
    stop = np.searchsorted(fpr, max_fpr, "right")
    x_interp = [fpr[stop - 1], fpr[stop]]
    y_interp = [tpr[stop - 1], tpr[stop]]
    tpr = np.append(tpr[:stop], np.interp(max_fpr, x_interp, y_interp))
    fpr = np.append(fpr[:stop], max_fpr)
    partial_auc = auc(fpr, tpr)

    # McClish correction: standardize result to be 0.5 if non-discriminant
    # and 1 if maximal
    min_area = 0.5 * max_fpr**2
    max_area = max_fpr
    return 0.5 * (1 + (partial_auc - min_area) / (max_area - min_area))

def roc_auc_score(
    y_true,
    y_score,
    *,
    average="macro",
    max_fpr=None,
    multi_class="raise",
    labels=None,
):
    """Compute Area Under the Receiver Operating Characteristic Curve (ROC AUC) \
    from prediction scores.

    Note: this implementation can be used with binary, multiclass and
    multilabel classification, but some restrictions apply (see Parameters).

    Read more in the :ref:`User Guide <roc_metrics>`.

        True labels or binary label indicators. The binary and multiclass cases
        expect labels with shape (n_samples,) while the multilabel case expects
        binary label indicators with shape (n_samples, n_classes).

        Target scores.

        * In the binary case, it corresponds to an array of shape
          `(n_samples,)`. Both probability estimates and non-thresholded
          decision values can be provided. The probability estimates correspond
          to the **probability of the class with the greater label**,
          i.e. `estimator.classes_[1]` and thus
          `estimator.predict_proba(X, y)[:, 1]`. The decision values
          corresponds to the output of `estimator.decision_function(X, y)`.
          See more information in the :ref:`User guide <roc_auc_binary>`;
        * In the multiclass case, it corresponds to an array of shape
          `(n_samples, n_classes)` of probability estimates provided by the
          `predict_proba` method. The probability estimates **must**
          sum to 1 across the possible classes. In addition, the order of the
          class scores must correspond to the order of ``labels``,
          if provided, or else to the numerical or lexicographical order of
          the labels in ``y_true``. See more information in the
          :ref:`User guide <roc_auc_multiclass>`;
        * In the multilabel case, it corresponds to an array of shape
          `(n_samples, n_classes)`. Probability estimates are provided by the
          `predict_proba` method and the non-thresholded decision values by
          the `decision_function` method. The probability estimates correspond
          to the **probability of the class with the greater label for each
          output** of the classifier. See more information in the
          :ref:`User guide <roc_auc_multilabel>`.

    average : {'micro', 'macro', 'samples', 'weighted'} or None, \
        If ``None``, the scores for each class are returned.
        Otherwise, this determines the type of averaging performed on the data.
        Note: multiclass ROC AUC currently only handles the 'macro' and
        'weighted' averages. For multiclass targets, `average=None` is only
        implemented for `multi_class='ovr'` and `average='micro'` is only
        implemented for `multi_class='ovr'`.

    max_fpr : float > 0 and <= 1, default=None
        If not ``None``, the standardized partial AUC [2]_ over the range
        [0, max_fpr] is returned. For the multiclass case, ``max_fpr``,
        should be either equal to ``None`` or ``1.0`` as AUC ROC partial
        computation currently is not supported for multiclass.

    multi_class : {'raise', 'ovr', 'ovo'}, default='raise'
        Only used for multiclass targets. Determines the type of configuration
        to use. The default value raises an error, so either
        ``'ovr'`` or ``'ovo'`` must be passed explicitly.

        ``'ovr'``:
            Stands for One-vs-rest. Computes the AUC of each class
            against the rest [3]_ [4]_. This
            treats the multiclass case in the same way as the multilabel case.
            Sensitive to class imbalance even when ``average == 'macro'``,
            because class imbalance affects the composition of each of the
            'rest' groupings.
        ``'ovo'``:
            Stands for One-vs-one. Computes the average AUC of all
            possible pairwise combinations of classes [5]_.
            Insensitive to class imbalance when
            ``average == 'macro'``.

    labels : array-like of shape (n_classes,), default=None
        Only used for multiclass targets. List of labels that index the
        classes in ``y_score``. If ``None``, the numerical or lexicographical
        order of the labels in ``y_true`` is used.

    Returns
    -------
    auc : float
        Area Under the Curve score.

    See Also
    --------
    average_precision_score : Area under the precision-recall curve.
    RocCurveDisplay.from_estimator : Plot Receiver Operating Characteristic
        (ROC) curve given an estimator and some data.
    RocCurveDisplay.from_predictions : Plot Receiver Operating Characteristic
        (ROC) curve given the true and predicted values.

    Notes
    -----
    The Gini Coefficient is a summary measure of the ranking ability of binary
    classifiers. It is expressed using the area under of the ROC as follows:

    G = 2 * AUC - 1

    Where G is the Gini coefficient and AUC is the ROC-AUC score. This normalisation
    will ensure that random guessing will yield a score of 0 in expectation, and it is
    upper bounded by 1.

    .. [1] `Wikipedia entry for the Receiver operating characteristic
            <https://en.wikipedia.org/wiki/Receiver_operating_characteristic>`_

    .. [2] `Analyzing a portion of the ROC curve. McClish, 1989
            <https://www.ncbi.nlm.nih.gov/pubmed/2668680>`_

    .. [3] Provost, F., Domingos, P. (2000). Well-trained PETs: Improving
           probability estimation trees (Section 6.2), CeDER Working Paper
           #IS-00-04, Stern School of Business, New York University.

    .. [4] `Fawcett, T. (2006). An introduction to ROC analysis. Pattern
            Recognition Letters, 27(8), 861-874.
            <https://www.sciencedirect.com/science/article/pii/S016786550500303X>`_

    .. [5] `Hand, D.J., Till, R.J. (2001). A Simple Generalisation of the Area
            Under the ROC Curve for Multiple Class Classification Problems.
            Machine Learning, 45(2), 171-186.
            <http://link.springer.com/article/10.1023/A:1010920819831>`_
    .. [6] `Wikipedia entry for the Gini coefficient
            <https://en.wikipedia.org/wiki/Gini_coefficient>`_

    Examples
    --------
    Binary case:

    >>> from sklearn.metrics import roc_auc_score
    >>> X, y = load_breast_cancer(return_X_y=True)
    >>> clf = LogisticRegression(solver="liblinear", random_state=0).fit(X, y)
    >>> roc_auc_score(y, clf.predict_proba(X)[:, 1])
    0.99...
    >>> roc_auc_score(y, clf.decision_function(X))
    0.99...

    Multiclass case:

    >>> clf = LogisticRegression(solver="liblinear").fit(X, y)
    >>> roc_auc_score(y, clf.predict_proba(X), multi_class='ovr')
    0.99...

    Multilabel case:

    >>> X, y = make_multilabel_classification(random_state=0)
    >>> clf = MultiOutputClassifier(clf).fit(X, y)
    >>> # get a list of n_output containing probability arrays of shape
    >>> # (n_samples, n_classes)
    >>> y_score = clf.predict_proba(X)
    >>> # extract the positive columns for each output
    >>> y_score = np.transpose([score[:, 1] for score in y_score])
    >>> roc_auc_score(y, y_score, average=None)
    array([0.82..., 0.86..., 0.94..., 0.85... , 0.94...])
    >>> from sklearn.linear_model import RidgeClassifierCV
    >>> clf = RidgeClassifierCV().fit(X, y)
    >>> roc_auc_score(y, clf.decision_function(X), average=None)
    array([0.81..., 0.84... , 0.93..., 0.87..., 0.94...])
    """

    y_true = check_array(y_true, ensure_2d=False, dtype=None)
    y_score = check_array(y_score, ensure_2d=False)

    if y_type == "multiclass" or (
        y_type == "binary" and y_score.ndim == 2 and y_score.shape[1] > 2
    ):
        # do not support partial ROC computation for multiclass
        if max_fpr is not None and max_fpr != 1.0:
                "Partial AUC computation not available in "
                "multiclass setting, 'max_fpr' must be"
                " set to `None`, received `max_fpr={0}` "
                "instead".format(max_fpr)
            )
        if multi_class == "raise":
            raise ValueError("multi_class must be in ('ovo', 'ovr')")
        return _multiclass_roc_auc_score(
            y_true, y_score, labels, multi_class, average, sample_weight
        )
    elif y_type == "binary":
        labels = np.unique(y_true)
        y_true = label_binarize(y_true, classes=labels)[:, 0]
        return _average_binary_score(
            partial(_binary_roc_auc_score, max_fpr=max_fpr),
            y_true,
            y_score,
            average,
        )
    else:  # multilabel-indicator
            y_true,
            y_score,
            average,
        )

def _multiclass_roc_auc_score(
    y_true, y_score, labels, multi_class, average, sample_weight
):
    """Multiclass roc auc score.

        True multiclass labels.

    y_score : array-like of shape (n_samples, n_classes)
        Target scores corresponding to probability estimates of a sample
        belonging to a particular class

    labels : array-like of shape (n_classes,) or None
        List of labels to index ``y_score`` used for multiclass. If ``None``,
        the lexical order of ``y_true`` is used to index ``y_score``.

    multi_class : {'ovr', 'ovo'}
        Determines the type of multiclass configuration to use.
            Calculate metrics for the multiclass case using the one-vs-rest
            approach.
            Calculate metrics for the multiclass case using the one-vs-one
            approach.

    average : {'micro', 'macro', 'weighted'}
        Determines the type of averaging performed on the pairwise binary
        metric scores
            Calculate metrics for the binarized-raveled classes. Only supported
            for `multi_class='ovr'`.

            mean. This does not take label imbalance into account. Classes
            are assumed to be uniformly distributed.
            Calculate metrics for each label, taking into account the
            prevalence of the classes.

    sample_weight : array-like of shape (n_samples,) or None

    """
    # validation of the input y_score
    if not np.allclose(1, y_score.sum(axis=1)):
            "Target scores need to be probabilities for multiclass "
            "roc_auc, i.e. they should sum up to 1.0 over classes"
        )

    # validation for multiclass parameter specifications
    average_options = ("macro", "weighted", None)
    if multi_class == "ovr":
        average_options = ("micro",) + average_options
    if average not in average_options:
            "average must be one of {0} for multiclass problems".format(average_options)
        )

    multiclass_options = ("ovo", "ovr")
    if multi_class not in multiclass_options:
            "multi_class='{0}' is not supported "
            "for multiclass ROC AUC, multi_class must be "
            "in {1}".format(multi_class, multiclass_options)
        )

    if average is None and multi_class == "ovo":
            "average=None is not implemented for multi_class='ovo'."
        )

    if labels is not None:
        labels = column_or_1d(labels)
        classes = _unique(labels)
        if len(classes) != len(labels):
            raise ValueError("Parameter 'labels' must be unique")
        if not np.array_equal(classes, labels):
            raise ValueError("Parameter 'labels' must be ordered")
        if len(classes) != y_score.shape[1]:
                "Number of given labels, {0}, not equal to the number "
                "of columns in 'y_score', {1}".format(len(classes), y_score.shape[1])
            )
        if len(np.setdiff1d(y_true, classes)):
            raise ValueError("'y_true' contains labels not in parameter 'labels'")
    else:
        classes = _unique(y_true)
                "Number of classes in y_true not equal to the number of "
                "columns in 'y_score'"
            )

    if multi_class == "ovo":
                "sample_weight is not supported "
                "for multiclass one-vs-one ROC AUC, "
                "'sample_weight' must be None in this case."
            )
        y_true_encoded = _encode(y_true, uniques=classes)
        # Hand & Till (2001) implementation (ovo)
        return _average_multiclass_ovo_score(
            _binary_roc_auc_score, y_true_encoded, y_score, average=average
        )
    else:
        # ovr is same as multi-label
        y_true_multilabel = label_binarize(y_true, classes=classes)
            _binary_roc_auc_score,
            y_true_multilabel,
            y_score,
            average,
        )

def _binary_clf_curve(y_true, y_score, pos_label=None, sample_weight=None):
    """Calculate true and false positives per binary classification threshold.

        True targets of binary classification.

    y_score : ndarray of shape (n_samples,)
        Estimated probabilities or output of a decision function.

    Returns
    -------
    fps : ndarray of shape (n_thresholds,)
        A count of false positives, at index i being the number of negative
        samples assigned a score >= thresholds[i]. The total number of
        negative samples is equal to fps[-1] (thus true negatives are given by
        fps[-1] - fps).

    tps : ndarray of shape (n_thresholds,)
        An increasing count of true positives, at index i being the number
        of positive samples assigned a score >= thresholds[i]. The total
        number of positive samples is equal to tps[-1] (thus false negatives
        are given by tps[-1] - tps).

        Decreasing score values.
    """
    # Check to make sure y_true is valid
    if not (y_type == "binary" or (y_type == "multiclass" and pos_label is not None)):
        raise ValueError("{0} format is not supported".format(y_type))

    check_consistent_length(y_true, y_score, sample_weight)
    y_score = column_or_1d(y_score)
    assert_all_finite(y_true)
    assert_all_finite(y_score)

    # Filter out zero-weighted samples, as they should not impact the result
        sample_weight = column_or_1d(sample_weight)
        sample_weight = _check_sample_weight(sample_weight, y_true)
        nonzero_weight_mask = sample_weight != 0
        y_true = y_true[nonzero_weight_mask]
        y_score = y_score[nonzero_weight_mask]
        sample_weight = sample_weight[nonzero_weight_mask]

    # make y_true a boolean vector

    # sort scores and corresponding truth values
    desc_score_indices = np.argsort(y_score, kind="mergesort")[::-1]
    y_score = y_score[desc_score_indices]
    y_true = y_true[desc_score_indices]
        weight = sample_weight[desc_score_indices]
    else:
        weight = 1.0

    # y_score typically has many tied values. Here we extract
    # the indices associated with the distinct values. We also
    # concatenate a value for the end of the curve.
    distinct_value_indices = np.where(np.diff(y_score))[0]
    threshold_idxs = np.r_[distinct_value_indices, y_true.size - 1]

    # accumulate the true positives with decreasing threshold
    tps = stable_cumsum(y_true * weight)[threshold_idxs]
        # express fps as a cumsum to ensure fps is increasing even in
        # the presence of floating point errors
        fps = stable_cumsum((1 - y_true) * weight)[threshold_idxs]
    else:
        fps = 1 + threshold_idxs - tps
    return fps, tps, y_score[threshold_idxs]

def precision_recall_curve(
    y_true,
    y_score,
    *,
    drop_intermediate=False,
):
    """Compute precision-recall pairs for different probability thresholds.

    Note: this implementation is restricted to the binary classification task.

    The precision is the ratio ``tp / (tp + fp)`` where ``tp`` is the number of
    true positives and ``fp`` the number of false positives. The precision is
    intuitively the ability of the classifier not to label as positive a sample
    that is negative.

    The recall is the ratio ``tp / (tp + fn)`` where ``tp`` is the number of
    true positives and ``fn`` the number of false negatives. The recall is
    intuitively the ability of the classifier to find all the positive samples.

    The last precision and recall values are 1. and 0. respectively and do not
    have a corresponding threshold. This ensures that the graph starts on the
    y axis.

    The first precision and recall values are precision=class balance and recall=1.0
    which corresponds to a classifier that always predicts the positive class.

    y_score : array-like of shape (n_samples,)
        class, or non-thresholded measure of decisions (as returned by
        `decision_function` on some classifiers).

        When ``pos_label=None``, if y_true is in {-1, 1} or {0, 1},

        Whether to drop some suboptimal thresholds which would not appear
        on a plotted precision-recall curve. This is useful in order to create
        lighter precision-recall curves.

    Returns
    -------
    precision : ndarray of shape (n_thresholds + 1,)
        Precision values such that element i is the precision of
        predictions with score >= thresholds[i] and the last element is 1.

    recall : ndarray of shape (n_thresholds + 1,)
        Decreasing recall values such that element i is the recall of
        predictions with score >= thresholds[i] and the last element is 0.

        Increasing thresholds on the decision function used to compute
        precision and recall where `n_thresholds = len(np.unique(y_score))`.

    See Also
    --------
    PrecisionRecallDisplay.from_estimator : Plot Precision Recall Curve given
        a binary classifier.
    PrecisionRecallDisplay.from_predictions : Plot Precision Recall Curve
        using predictions from a binary classifier.
    det_curve: Compute error rates for different probability thresholds.

    Examples
    --------
    >>> from sklearn.metrics import precision_recall_curve
    >>> precision, recall, thresholds = precision_recall_curve(
    ...     y_true, y_scores)
    >>> precision
    array([0.5       , 0.66666667, 0.5       , 1.        , 1.        ])
    >>> recall
    array([1. , 1. , 0.5, 0.5, 0. ])
    array([0.1 , 0.35, 0.4 , 0.8 ])
    """
    )

        # Drop thresholds corresponding to points where true positives (tps)
        # do not change from the previous or subsequent point. This will keep
        # only the first and last point for each tps value. All points
        # with the same tps value have the same recall and thus x coordinate.
        # They appear as a vertical line on the plot.
            )
        )[0]

    ps = tps + fps
    # Initialize the result array with zeros to make sure that precision[ps == 0]
    # does not contain uninitialized values.
    precision = np.zeros_like(tps)
    np.divide(tps, ps, out=precision, where=(ps != 0))

    # When no positive label in y_true, recall is set to 1 for all thresholds
    # tps[-1] == 0 <=> y_true == all negative labels
    if tps[-1] == 0:
            "No positive class found in y_true, "
            "recall is set to one for all thresholds."
        )
        recall = np.ones_like(tps)
    else:
        recall = tps / tps[-1]

    # reverse the outputs so recall is decreasing
    sl = slice(None, None, -1)
    return np.hstack((precision[sl], 1)), np.hstack((recall[sl], 0)), thresholds[sl]

def roc_curve(
    y_true, y_score, *, pos_label=None, sample_weight=None, drop_intermediate=True
):
    """Compute Receiver operating characteristic (ROC).

    drop_intermediate : bool, default=True
        Whether to drop thresholds where the resulting point is collinear with
        its neighbors in ROC space. This has no effect on the ROC AUC or visual
        shape of the curve, but reduces the number of plotted points.

           parameter *drop_intermediate*.

    Returns
    -------
    fpr : ndarray of shape (>2,)
        Increasing false positive rates such that element i is the false
        positive rate of predictions with score >= `thresholds[i]`.

    tpr : ndarray of shape (>2,)
        Increasing true positive rates such that element `i` is the true

        Decreasing thresholds on the decision function used to compute
        fpr and tpr. The first threshold is set to `np.inf`.

           An arbitrary threshold at infinity (stored in `thresholds[0]`) is
           added to represent a classifier that always predicts the negative
           class, i.e. `fpr=0` and `tpr=0`.

    See Also
    --------

    Notes
    -----
    Since the thresholds are sorted from low to high values, they
    are reversed upon returning them to ensure they correspond to both ``fpr``
    and ``tpr``, which are sorted in reversed order during their calculation.

    .. [2] Fawcett T. An introduction to ROC analysis[J]. Pattern Recognition
           Letters, 2006, 27(8):861-874.

    Examples
    --------
    >>> y = np.array([1, 1, 2, 2])
    >>> scores = np.array([0.1, 0.4, 0.35, 0.8])
    >>> fpr, tpr, thresholds = metrics.roc_curve(y, scores, pos_label=2)
    >>> fpr
    array([0. , 0. , 0.5, 0.5, 1. ])
    >>> tpr
    array([0. , 0.5, 0.5, 1. , 1. ])
    array([ inf, 0.8 , 0.4 , 0.35, 0.1 ])
    """
    )

    # Attempt to drop thresholds corresponding to points in between and
    # collinear with other points. These are always suboptimal and do not
    # appear on a plotted ROC curve (and thus do not affect the AUC).
    # Here np.diff(_, 2) is used as a "second derivative" to tell if there
    # is a corner at the point. Both fps and tps must be tested to handle
    # thresholds with multiple data points (which are combined in
    # _binary_clf_curve). This keeps all cases where the point should be kept,
    # but does not drop more complicated cases like fps = [1, 3, 7],
    # tps = [1, 2, 4]; there is no harm in keeping too many thresholds.
            np.r_[True, np.logical_or(np.diff(fps, 2), np.diff(tps, 2)), True]
        )[0]

    # Add an extra threshold position
    # to make sure that the curve starts at (0, 0)
    tps = np.r_[0, tps]
    fps = np.r_[0, fps]
    # get dtype of `y_score` even if it is an array-like
    thresholds = np.r_[np.inf, thresholds]

    if fps[-1] <= 0:
            "No negative samples in y_true, false positive value should be meaningless",
        )
        fpr = np.repeat(np.nan, fps.shape)
    else:
        fpr = fps / fps[-1]

    if tps[-1] <= 0:
            "No positive samples in y_true, true positive value should be meaningless",
        )
        tpr = np.repeat(np.nan, tps.shape)
    else:
        tpr = tps / tps[-1]

    return fpr, tpr, thresholds

def label_ranking_average_precision_score(y_true, y_score, *, sample_weight=None):
    """Compute ranking-based average precision.

    Label ranking average precision (LRAP) is the average over each ground
    truth label assigned to each sample, of the ratio of true vs. total
    labels with lower score.

    This metric is used in multilabel ranking problem, where the goal
    is to give better rank to the labels associated to each sample.

    The obtained score is always strictly greater than 0 and
    the best value is 1.

    Read more in the :ref:`User Guide <label_ranking_average_precision>`.

    y_true : {array-like, sparse matrix} of shape (n_samples, n_labels)
        True binary labels in binary indicator format.

    y_score : array-like of shape (n_samples, n_labels)

    Returns
    -------
    score : float
        Ranking-based average precision score.

    Examples
    --------
    >>> from sklearn.metrics import label_ranking_average_precision_score
    >>> y_true = np.array([[1, 0, 0], [0, 0, 1]])
    >>> y_score = np.array([[0.75, 0.5, 1], [1, 0.2, 0.1]])
    >>> label_ranking_average_precision_score(y_true, y_score)
    0.416...
    """
    y_true = check_array(y_true, ensure_2d=False, accept_sparse="csr")

    if y_true.shape != y_score.shape:
        raise ValueError("y_true and y_score have different shape")

    # Handle badly formatted array and the degenerate case with one label
    if y_type != "multilabel-indicator" and not (
        y_type == "binary" and y_true.ndim == 2
    ):

    if not issparse(y_true):
        y_true = csr_matrix(y_true)

    y_score = -y_score

    n_samples, n_labels = y_true.shape

    out = 0.0
    for i, (start, stop) in enumerate(zip(y_true.indptr, y_true.indptr[1:])):
        relevant = y_true.indices[start:stop]

        if relevant.size == 0 or relevant.size == n_labels:
            # If all labels are relevant or unrelevant, the score is also
            # equal to 1. The label ranking has no meaning.
            aux = 1.0
        else:
            scores_i = y_score[i]
            rank = rankdata(scores_i, "max")[relevant]
            L = rankdata(scores_i[relevant], "max")
            aux = (L / rank).mean()

            aux = aux * sample_weight[i]
        out += aux

    if sample_weight is None:
        out /= n_samples
    else:
        out /= np.sum(sample_weight)

    return float(out)

def coverage_error(y_true, y_score, *, sample_weight=None):
    """Coverage error measure.

    Compute how far we need to go through the ranked scores to cover all
    true labels. The best value is equal to the average number
    of labels in ``y_true`` per sample.

    Ties in ``y_scores`` are broken by giving maximal rank that would have
    been assigned to all tied values.

    Note: Our implementation's score is 1 greater than the one given in
    Tsoumakas et al., 2010. This extends it to handle the degenerate case
    in which an instance has 0 true labels.

    Read more in the :ref:`User Guide <coverage_error>`.

    y_true : array-like of shape (n_samples, n_labels)

    Returns
    -------
    coverage_error : float
        The coverage error.

    .. [1] Tsoumakas, G., Katakis, I., & Vlahavas, I. (2010).
           Mining multi-label data. In Data mining and knowledge discovery
           handbook (pp. 667-685). Springer US.

    Examples
    --------
    >>> from sklearn.metrics import coverage_error
    >>> y_true = [[1, 0, 0], [0, 1, 1]]
    >>> y_score = [[1, 0, 0], [0, 1, 1]]
    >>> coverage_error(y_true, y_score)
    1.5
    """
    y_true = check_array(y_true, ensure_2d=True)
    y_score = check_array(y_score, ensure_2d=True)

    if y_type != "multilabel-indicator":

    y_score_mask = np.ma.masked_array(y_score, mask=np.logical_not(y_true))
    y_min_relevant = y_score_mask.min(axis=1).reshape((-1, 1))
    coverage = (y_score >= y_min_relevant).sum(axis=1)
    coverage = coverage.filled(0)

    return float(np.average(coverage, weights=sample_weight))

def label_ranking_loss(y_true, y_score, *, sample_weight=None):
    """Compute Ranking loss measure.

    Compute the average number of label pairs that are incorrectly ordered
    given y_score weighted by the size of the label set and the number of
    labels not in the label set.

    This is similar to the error set size, but weighted by the number of
    relevant and irrelevant labels. The best performance is achieved with
    a ranking loss of zero.

    Read more in the :ref:`User Guide <label_ranking_loss>`.

       A function *label_ranking_loss*

    Returns
    -------
        Average number of label pairs that are incorrectly ordered given
        y_score weighted by the size of the label set and the number of labels not
        in the label set.

    Examples
    --------
    >>> from sklearn.metrics import label_ranking_loss
    >>> y_true = [[1, 0, 0], [0, 0, 1]]
    >>> y_score = [[0.75, 0.5, 1], [1, 0.2, 0.1]]
    >>> label_ranking_loss(y_true, y_score)
    0.75...
    """

    if y_type not in ("multilabel-indicator",):

    y_true = csr_matrix(y_true)

    loss = np.zeros(n_samples)
        # Sort and bin the label scores
        unique_scores, unique_inverse = np.unique(y_score[i], return_inverse=True)
        true_at_reversed_rank = np.bincount(
            unique_inverse[y_true.indices[start:stop]], minlength=len(unique_scores)
        )
        all_at_reversed_rank = np.bincount(unique_inverse, minlength=len(unique_scores))
        false_at_reversed_rank = all_at_reversed_rank - true_at_reversed_rank

        # if the scores are ordered, it's possible to count the number of
        # incorrectly ordered paires in linear time by cumulatively counting
        # how many false labels of a given score have a score higher than the
        # accumulated true labels with lower score.
        loss[i] = np.dot(true_at_reversed_rank.cumsum(), false_at_reversed_rank)

    n_positives = count_nonzero(y_true, axis=1)
        loss /= (n_labels - n_positives) * n_positives

    # When there is no positive or no negative labels, those values should
    # be consider as correct, i.e. the ranking doesn't matter.
    loss[np.logical_or(n_positives == 0, n_positives == n_labels)] = 0.0

    return float(np.average(loss, weights=sample_weight))

def _dcg_sample_scores(y_true, y_score, k=None, log_base=2, ignore_ties=False):
    """Compute Discounted Cumulative Gain.

    Sum the true scores ranked in the order induced by the predicted scores,
    after applying a logarithmic discount.

    This ranking metric yields a high value if true labels are ranked high by
    ``y_score``.

    y_true : ndarray of shape (n_samples, n_labels)
        True targets of multilabel classification, or true scores of entities
        to be ranked.

    y_score : ndarray of shape (n_samples, n_labels)
        Target scores, can either be probability estimates, confidence values,
        or non-thresholded measure of decisions (as returned by
        "decision_function" on some classifiers).

    k : int, default=None
        Only consider the highest k scores in the ranking. If `None`, use all
        outputs.

    log_base : float, default=2
        Base of the logarithm used for the discount. A low value means a
        sharper discount (top results are more important).

    ignore_ties : bool, default=False
        Assume that there are no ties in y_score (which is likely to be the
        case if y_score is continuous) for efficiency gains.

    Returns
    -------
    discounted_cumulative_gain : ndarray of shape (n_samples,)
        The DCG score for each sample.

    See Also
    --------
    ndcg_score : The Discounted Cumulative Gain divided by the Ideal Discounted
        Cumulative Gain (the DCG obtained for a perfect ranking), in order to
        have a score between 0 and 1.
    """
    discount = 1 / (np.log(np.arange(y_true.shape[1]) + 2) / np.log(log_base))
    if k is not None:
        discount[k:] = 0
    if ignore_ties:
        ranking = np.argsort(y_score)[:, ::-1]
        ranked = y_true[np.arange(ranking.shape[0])[:, np.newaxis], ranking]
        cumulative_gains = discount.dot(ranked.T)
    else:
        discount_cumsum = np.cumsum(discount)
        cumulative_gains = [
            _tie_averaged_dcg(y_t, y_s, discount_cumsum)
            for y_t, y_s in zip(y_true, y_score)
        ]
        cumulative_gains = np.asarray(cumulative_gains)
    return cumulative_gains

def _tie_averaged_dcg(y_true, y_score, discount_cumsum):
    """
    Compute DCG by averaging over possible permutations of ties.

    The gain (`y_true`) of an index falling inside a tied group (in the order
    induced by `y_score`) is replaced by the average gain within this group.
    The discounted gain for a tied group is then the average `y_true` within
    this group times the sum of discounts of the corresponding ranks.

    This amounts to averaging scores for all possible orderings of the tied
    groups.

    (note in the case of dcg@k the discount is 0 after index k)

    y_true : ndarray
        The true relevance scores.

    y_score : ndarray
        Predicted scores.

    discount_cumsum : ndarray
        Precomputed cumulative sum of the discounts.

    Returns
    -------
    discounted_cumulative_gain : float
        The discounted cumulative gain.

    McSherry, F., & Najork, M. (2008, March). Computing information retrieval
    performance measures efficiently in the presence of tied scores. In
    European conference on information retrieval (pp. 414-421). Springer,
    Berlin, Heidelberg.
    """
    _, inv, counts = np.unique(-y_score, return_inverse=True, return_counts=True)
    ranked = np.zeros(len(counts))
    np.add.at(ranked, inv, y_true)
    ranked /= counts
    groups = np.cumsum(counts) - 1
    discount_sums = np.empty(len(counts))
    discount_sums[0] = discount_cumsum[groups[0]]
    discount_sums[1:] = np.diff(discount_cumsum[groups])
    return (ranked * discount_sums).sum()

def _check_dcg_target_type(y_true):
    supported_fmt = (
        "multilabel-indicator",
        "continuous-multioutput",
        "multiclass-multioutput",
    )
    if y_type not in supported_fmt:
            "Only {} formats are supported. Got {} instead".format(
                supported_fmt, y_type
            )
        )

def dcg_score(
    y_true, y_score, *, k=None, log_base=2, sample_weight=None, ignore_ties=False
):

    Usually the Normalized Discounted Cumulative Gain (NDCG, computed by
    ndcg_score) is preferred.

        Only consider the highest k scores in the ranking. If None, use all
        outputs.

        Sample weights. If `None`, all samples are given the same weight.

    Returns
    -------
        The averaged sample DCG scores.

    See Also
    --------

    `Wikipedia entry for Discounted Cumulative Gain
    <https://en.wikipedia.org/wiki/Discounted_cumulative_gain>`_.

    Jarvelin, K., & Kekalainen, J. (2002).
    Cumulated gain-based evaluation of IR techniques. ACM Transactions on
    Information Systems (TOIS), 20(4), 422-446.

    Wang, Y., Wang, L., Li, Y., He, D., Chen, W., & Liu, T. Y. (2013, May).
    A theoretical analysis of NDCG ranking measures. In Proceedings of the 26th
    Annual Conference on Learning Theory (COLT 2013).

    Examples
    --------
    >>> from sklearn.metrics import dcg_score
    >>> # we have ground-truth relevance of some answers to a query:
    >>> true_relevance = np.asarray([[10, 0, 0, 1, 5]])
    >>> # we predict scores for the answers
    >>> scores = np.asarray([[.1, .2, .3, 4, 70]])
    >>> dcg_score(true_relevance, scores)
    9.49...
    >>> # we can set k to truncate the sum; only top k answers contribute
    >>> dcg_score(true_relevance, scores, k=2)
    5.63...
    >>> # now we have some ties in our prediction
    >>> scores = np.asarray([[1, 0, 0, 0, 1]])
    >>> # by default ties are averaged, so here we get the average true
    >>> # relevance of our top predictions: (10 + 5) / 2 = 7.5
    >>> dcg_score(true_relevance, scores, k=1)
    7.5
    >>> # we can choose to ignore ties for faster results, but only
    >>> # if we know there aren't ties in our scores, otherwise we get
    >>> # wrong results:
    >>> dcg_score(true_relevance,
    ...           scores, k=1, ignore_ties=True)
    5.0
    """
    y_true = check_array(y_true, ensure_2d=False)
    _check_dcg_target_type(y_true)
    return float(
        np.average(
            _dcg_sample_scores(
                y_true, y_score, k=k, log_base=log_base, ignore_ties=ignore_ties
            ),
        )
    )

def _ndcg_sample_scores(y_true, y_score, k=None, ignore_ties=False):
    """Compute Normalized Discounted Cumulative Gain.

    after applying a logarithmic discount. Then divide by the best possible
    score (Ideal DCG, obtained for a perfect ranking) to obtain a score between
    0 and 1.

        outputs.

    Returns
    -------
    normalized_discounted_cumulative_gain : ndarray of shape (n_samples,)
        The NDCG score for each sample (float in [0., 1.]).

    See Also
    --------
    dcg_score : Discounted Cumulative Gain (not normalized).

    """
    gain = _dcg_sample_scores(y_true, y_score, k, ignore_ties=ignore_ties)
    # Here we use the order induced by y_true so we can ignore ties since
    # the gain associated to tied indices is the same (permuting ties doesn't
    # change the value of the re-ordered y_true)
    normalizing_gain = _dcg_sample_scores(y_true, y_true, k, ignore_ties=True)
    all_irrelevant = normalizing_gain == 0
    gain[all_irrelevant] = 0
    gain[~all_irrelevant] /= normalizing_gain[~all_irrelevant]
    return gain

def ndcg_score(y_true, y_score, *, k=None, sample_weight=None, ignore_ties=False):

    0 and 1.

    This ranking metric returns a high value if true labels are ranked high by

        to be ranked. Negative values in `y_true` may result in an output
        that is not between 0 and 1.

        outputs.

    Returns
    -------
    normalized_discounted_cumulative_gain : float in [0., 1.]
        The averaged NDCG scores for all samples.

    See Also
    --------

    <https://en.wikipedia.org/wiki/Discounted_cumulative_gain>`_

    Annual Conference on Learning Theory (COLT 2013)

    Examples
    --------
    >>> from sklearn.metrics import ndcg_score
    >>> # we predict some scores (relevance) for the answers
    >>> ndcg_score(true_relevance, scores)
    0.69...
    >>> scores = np.asarray([[.05, 1.1, 1., .5, .0]])
    0.49...
    >>> # we can set k to truncate the sum; only top k answers contribute.
    >>> ndcg_score(true_relevance, scores, k=4)
    0.35...
    >>> # the normalization takes k into account so a perfect answer
    >>> # would still get 1.0
    >>> ndcg_score(true_relevance, true_relevance, k=4)
    1.0...
    >>> # by default ties are averaged, so here we get the average (normalized)
    >>> # true relevance of our top predictions: (10 / 10 + 5 / 10) / 2 = .75
    >>> ndcg_score(true_relevance, scores, k=1)
    0.75...
    >>> ndcg_score(true_relevance,
    0.5...
    """

    if y_true.min() < 0:
        raise ValueError("ndcg_score should not be used on negative y_true values.")
    if y_true.ndim > 1 and y_true.shape[1] <= 1:
            "Computing NDCG is only meaningful when there is more than 1 document. "
            f"Got {y_true.shape[1]} instead."
        )
    gain = _ndcg_sample_scores(y_true, y_score, k=k, ignore_ties=ignore_ties)
    return float(np.average(gain, weights=sample_weight))

def top_k_accuracy_score(
    y_true, y_score, *, k=2, normalize=True, sample_weight=None, labels=None
):
    """Top-k Accuracy classification score.

    This metric computes the number of times where the correct label is among
    the top `k` labels predicted (ranked by predicted scores). Note that the
    multilabel case isn't covered here.

    Read more in the :ref:`User Guide <top_k_accuracy_score>`

        True labels.

        Target scores. These can be either probability estimates or
        non-thresholded decision values (as returned by
        :term:`decision_function` on some classifiers).
        The binary case expects scores with shape (n_samples,) while the
        multiclass case expects scores with shape (n_samples, n_classes).
        In the multiclass case, the order of the class scores must
        correspond to the order of ``labels``, if provided, or else to
        the numerical or lexicographical order of the labels in ``y_true``.
        If ``y_true`` does not contain all the labels, ``labels`` must be
        provided.

    k : int, default=2
        Number of most likely outcomes considered to find the correct label.

        If `True`, return the fraction of correctly classified samples.
        Otherwise, return the number of correctly classified samples.

        Multiclass only. List of labels that index the classes in ``y_score``.
        If ``None``, the numerical or lexicographical order of the labels in
        ``y_true`` is used. If ``y_true`` does not contain all the labels,
        ``labels`` must be provided.

    Returns
    -------
        The top-k accuracy score. The best performance is 1 with
        `normalize == True` and the number of samples with
        `normalize == False`.

    See Also
    --------
    accuracy_score : Compute the accuracy score. By default, the function will
        return the fraction of correct predictions divided by the total number
        of predictions.

    Notes
    -----
    In cases where two or more labels are assigned equal predicted scores,
    the labels with the highest indices will be chosen first. This might
    impact the result if the correct label falls after the threshold because
    of that.

    Examples
    --------
    >>> from sklearn.metrics import top_k_accuracy_score
    >>> y_true = np.array([0, 1, 2, 2])
    >>> y_score = np.array([[0.5, 0.2, 0.2],  # 0 is in top 2
    ...                     [0.3, 0.4, 0.2],  # 1 is in top 2
    ...                     [0.2, 0.4, 0.3],  # 2 is in top 2
    ...                     [0.7, 0.2, 0.1]]) # 2 isn't in top 2
    >>> top_k_accuracy_score(y_true, y_score, k=2)
    0.75
    >>> # Not normalizing gives the number of "correctly" classified samples
    >>> top_k_accuracy_score(y_true, y_score, k=2, normalize=False)
    3.0
    """
    if y_type == "binary" and labels is not None and len(labels) > 2:
        y_type = "multiclass"
    if y_type not in {"binary", "multiclass"}:
            f"y type must be 'binary' or 'multiclass', got '{y_type}' instead."
        )
        if y_score.ndim == 2 and y_score.shape[1] != 1:
                "`y_true` is binary while y_score is 2d with"
                f" {y_score.shape[1]} classes. If `y_true` does not contain all the"
                " labels, `labels` must be provided."
            )
        y_score = column_or_1d(y_score)

    y_score_n_classes = y_score.shape[1] if y_score.ndim == 2 else 2

    if labels is None:
        n_classes = len(classes)

        if n_classes != y_score_n_classes:
                f"Number of classes in 'y_true' ({n_classes}) not equal "
                f"to the number of classes in 'y_score' ({y_score_n_classes})."
                "You can provide a list of all known classes by assigning it "
                "to the `labels` parameter."
            )
    else:
        n_labels = len(labels)

        if n_classes != n_labels:
            raise ValueError("Parameter 'labels' must be unique.")

            raise ValueError("Parameter 'labels' must be ordered.")

                f"Number of given labels ({n_classes}) not equal to the "
                f"number of classes in 'y_score' ({y_score_n_classes})."
            )

            raise ValueError("'y_true' contains labels not in parameter 'labels'.")

    if k >= n_classes:
            (
                f"'k' ({k}) greater than or equal to 'n_classes' ({n_classes}) "
                "will result in a perfect score and is therefore meaningless."
            ),
        )

    y_true_encoded = _encode(y_true, uniques=classes)

        if k == 1:
            threshold = 0.5 if y_score.min() >= 0 and y_score.max() <= 1 else 0
            y_pred = (y_score > threshold).astype(np.int64)
            hits = y_pred == y_true_encoded
        else:
            hits = np.ones_like(y_score, dtype=np.bool_)
        sorted_pred = np.argsort(y_score, axis=1, kind="mergesort")[:, ::-1]
        hits = (y_true_encoded == sorted_pred[:, :k].T).any(axis=0)

        return float(np.average(hits, weights=sample_weight))
    elif sample_weight is None:
        return float(np.sum(hits))
    else:
        return float(np.dot(hits, sample_weight))

    ):
        )

def _cached_call(cache, estimator, response_method, *args, **kwargs):
    """Call estimator with method and args and kwargs."""
    if cache is not None and response_method in cache:
        return cache[response_method]

    result, _ = _get_response_values(
        estimator, *args, response_method=response_method, **kwargs
    )

    if cache is not None:
        cache[response_method] = result

class _MultimetricScorer:
    """Callable for multimetric scoring used to avoid repeated calls
    to `predict_proba`, `predict`, and `decision_function`.

    `_MultimetricScorer` will return a dictionary of scores corresponding to
    the scorers in the dictionary. Note that `_MultimetricScorer` can be
    created with a dictionary with one key  (i.e. only one actual scorer).

    scorers : dict
        Dictionary mapping names to callable scorers.

    raise_exc : bool, default=True
        Whether to raise the exception in `__call__` or not. If set to `False`
        a formatted string of the exception details is passed as result of
        the failing scorer.
    """

    def __init__(self, *, scorers, raise_exc=True):
        self._scorers = scorers
        self._raise_exc = raise_exc

    def __call__(self, estimator, *args, **kwargs):
        """Evaluate predicted target values."""
        scores = {}
        cache = {} if self._use_cache(estimator) else None
        cached_call = partial(_cached_call, cache)

            routed_params = process_routing(self, "score", **kwargs)
        else:
            # Scorers all get the same args, and get all of them except sample_weight.
            # Only the ones having `sample_weight` in their signature will receive it.
            # This does not work for metadata other than sample_weight, and for those
            # users have to enable metadata routing.
            common_kwargs = {
                arg: value for arg, value in kwargs.items() if arg != "sample_weight"
            }
            routed_params = Bunch(
                **{name: Bunch(score=common_kwargs.copy()) for name in self._scorers}
            )
            if "sample_weight" in kwargs:
                for name, scorer in self._scorers.items():
                    if scorer._accept_sample_weight():
                        routed_params[name].score["sample_weight"] = kwargs[
                            "sample_weight"
                        ]

        for name, scorer in self._scorers.items():
            try:
                if isinstance(scorer, _BaseScorer):
                    score = scorer._score(
                        cached_call, estimator, *args, **routed_params.get(name).score
                    )
                else:
                    score = scorer(estimator, *args, **routed_params.get(name).score)
                scores[name] = score
            except Exception as e:
                if self._raise_exc:
                    raise e
                else:
                    scores[name] = format_exc()

        scorers = ", ".join([f'"{s}"' for s in self._scorers])
        return f"MultiMetricScorer({scorers})"

    def _accept_sample_weight(self):
        # TODO(slep006): remove when metadata routing is the only way
        return any(scorer._accept_sample_weight() for scorer in self._scorers.values())

    def _use_cache(self, estimator):
        """Return True if using a cache is beneficial, thus when a response method will
        be called several time.
        """
        if len(self._scorers) == 1:  # Only one scorer

        counter = Counter(
            [
                _check_response_method(estimator, scorer._response_method).__name__
                for scorer in self._scorers.values()
                if isinstance(scorer, _BaseScorer)
            ]
        )
        if any(val > 1 for val in counter.values()):
            # The exact same response method or iterable of response methods
            # will be called more than once.

        Returns
        -------
            A :class:`~utils.metadata_routing.MetadataRouter` encapsulating
        """
        return MetadataRouter(owner=self.__class__.__name__).add(
            **self._scorers,
            method_mapping=MethodMapping().add(caller="score", callee="score"),
        )

class _BaseScorer(_MetadataRequester):
    """Base scorer that is used as `scorer(estimator, X, y_true)`.

    score_func : callable
        The score function to use. It will be called as
        `score_func(y_true, y_pred, **kwargs)`.

    sign : int
        Either 1 or -1 to returns the score with `sign * score_func(estimator, X, y)`.
        Thus, `sign` defined if higher scores are better or worse.

    kwargs : dict
        Additional parameters to pass to the score function.

    response_method : str
        The method to call on the estimator to get the response values.
    """

    def __init__(self, score_func, sign, kwargs, response_method="predict"):
        self._score_func = score_func
        self._sign = sign
        self._kwargs = kwargs
        self._response_method = response_method
        # TODO (1.8): remove in 1.8 (scoring="max_error" has been deprecated in 1.6)
        self._deprecation_msg = None

    def _get_pos_label(self):
        if "pos_label" in self._kwargs:
            return self._kwargs["pos_label"]
        score_func_params = signature(self._score_func).parameters
        if "pos_label" in score_func_params:
            return score_func_params["pos_label"].default

        return "sample_weight" in signature(self._score_func).parameters

        sign_string = "" if self._sign > 0 else ", greater_is_better=False"
        response_method_string = f", response_method={self._response_method!r}"
        kwargs_string = "".join([f", {k}={v}" for k, v in self._kwargs.items()])

        return (
            f"make_scorer({self._score_func.__name__}{sign_string}"
            f"{response_method_string}{kwargs_string})"
        )

    def __call__(self, estimator, X, y_true, sample_weight=None, **kwargs):
        """Evaluate predicted target values for X relative to y_true.

        estimator : object
            Trained estimator to use for scoring. Must have a predict_proba
            method; the output of that is used to compute the score.

        X : {array-like, sparse matrix}
            Test data that will be fed to estimator.predict.

        y_true : array-like
            Gold standard target values for X.

            Other parameters passed to the scorer. Refer to
            :func:`set_score_request` for more details.

        Returns
        -------
            Score function applied to prediction of estimator on X.
        """
        if self._deprecation_msg is not None:
                self._deprecation_msg, category=DeprecationWarning, stacklevel=2
            )

        _raise_for_params(kwargs, self, None)

        _kwargs = copy.deepcopy(kwargs)
            _kwargs["sample_weight"] = sample_weight

        return self._score(partial(_cached_call, None), estimator, X, y_true, **_kwargs)

    def _warn_overlap(self, message, kwargs):
        """Warn if there is any overlap between ``self._kwargs`` and ``kwargs``.

        This method is intended to be used to check for overlap between
        ``self._kwargs`` and ``kwargs`` passed as metadata.
        """
        _kwargs = set() if self._kwargs is None else set(self._kwargs.keys())
        overlap = _kwargs.intersection(kwargs.keys())
        if overlap:
                f"{message} Overlapping parameters are: {overlap}", UserWarning
            )

    def set_score_request(self, **kwargs):
        """Set requested parameters by the scorer.

        Please see :ref:`User Guide <metadata_routing>` on how the routing

        kwargs : dict
            Arguments should be of the form ``param_name=alias``, and `alias`
            can be one of ``{True, False, None, str}``.
        """
        if not _routing_enabled():
                "This method is only available when metadata routing is enabled."
                " You can enable it using"
                " sklearn.set_config(enable_metadata_routing=True)."
            )

        self._warn_overlap(
            message=(
                "You are setting metadata request for parameters which are "
                "already set as kwargs for this metric. These set values will be "
                "overridden by passed metadata if provided. Please pass them either "
                "as metadata or kwargs to `make_scorer`."
            ),
            kwargs=kwargs,
        )
        self._metadata_request = MetadataRequest(owner=self.__class__.__name__)
        for param, alias in kwargs.items():
            self._metadata_request.score.add_request(param=param, alias=alias)

class _Scorer(_BaseScorer):
    def _score(self, method_caller, estimator, X, y_true, **kwargs):
        """Evaluate the response method of `estimator` on `X` and `y_true`.

        method_caller : callable
            Returns predictions given an estimator, method name, and other
            arguments, potentially caching results.

            Trained estimator to use for scoring.

            Test data that will be fed to clf.decision_function or
            clf.predict_proba.

            Gold standard target values for X. These must be class labels,
            not decision function values.

        Returns
        -------
        """
            message=(
                "There is an overlap between set kwargs of this scorer instance and"
                " passed metadata. Please pass them either as kwargs to `make_scorer`"
                " or metadata, but not both."
            ),
        )

        pos_label = None if is_regressor(estimator) else self._get_pos_label()
        response_method = _check_response_method(estimator, self._response_method)
        y_pred = method_caller(
            _get_response_method_name(response_method),
            X,
        )

        scoring_kwargs = {**self._kwargs, **kwargs}
        return self._sign * self._score_func(y_true, y_pred, **scoring_kwargs)

def get_scorer(scoring):
    """Get a scorer from string.

    Read more in the :ref:`User Guide <scoring_parameter>`.
    :func:`~sklearn.metrics.get_scorer_names` can be used to retrieve the names
    of all available scorers.

    scoring : str, callable or None
        Scoring method as string. If callable it is returned as is.
        If None, returns None.

    Returns
    -------
    scorer : callable
        The scorer.

    Notes
    -----
    When passed a string, this function always returns a copy of the scorer
    object. Calling `get_scorer` twice for the same scorer results in two
    separate scorer objects.

    Examples
    --------
    >>> from sklearn.metrics import get_scorer
    >>> X = np.reshape([0, 1, -1, -0.5, 2], (-1, 1))
    >>> y = np.array([0, 1, 1, 0, 1])
    >>> classifier = DummyClassifier(strategy="constant", constant=0).fit(X, y)
    >>> accuracy = get_scorer("accuracy")
    >>> accuracy(classifier, X, y)
    0.4
    """
    if isinstance(scoring, str):
        try:
            if scoring == "max_error":
                # TODO (1.8): scoring="max_error" has been deprecated in 1.6,
                # remove in 1.8
                scorer = max_error_scorer
            else:
                scorer = copy.deepcopy(_SCORERS[scoring])
                "%r is not a valid scoring value. "
                "Use sklearn.metrics.get_scorer_names() "
                "to get valid options." % scoring
            )
    else:
        scorer = scoring
    return scorer

class _PassthroughScorer(_MetadataRequester):
    # Passes scoring of estimator's `score` method back to estimator if scoring
    # is `None`.

    def __init__(self, estimator):
        self._estimator = estimator

        requests = MetadataRequest(owner=self.__class__.__name__)
        try:
            requests.score = copy.deepcopy(estimator._metadata_request.score)
            try:
                requests.score = copy.deepcopy(estimator._get_default_requests().score)
            except AttributeError:
                pass

        self._metadata_request = requests

        """Method that wraps estimator.score"""
        return estimator.score(*args, **kwargs)

        return f"{self._estimator.__class__}.score"

        return "sample_weight" in signature(self._estimator.score).parameters

        """Get requested data properties.

        Returns
        -------
        """
        return get_routing_for_object(self._metadata_request)

        """
            )

def _check_multimetric_scoring(estimator, scoring):
    """Check the scoring parameter in cases when multiple metrics are allowed.

    In addition, multimetric scoring leverages a caching mechanism to not call the same
    estimator response method multiple times. Hence, the scorer is modified to only use
    a single response method given a list of response methods and the estimator.

    estimator : sklearn estimator instance
        The estimator for which the scoring will be applied.

    scoring : list, tuple or dict
        Strategy to evaluate the performance of the cross-validated model on
        the test set.

        The possibilities are:

        - a list or tuple of unique strings;
        - a callable returning a dictionary where they keys are the metric
          names and the values are the metric scores;
        - a dictionary with metric names as keys and callables a values.

        See :ref:`multimetric_grid_search` for an example.

    Returns
    -------
    scorers_dict : dict
        A dict mapping each scorer name to its validated scorer.
    """
    err_msg_generic = (
        f"scoring is invalid (got {scoring!r}). Refer to the "
        "scoring glossary for details: "
        "https://scikit-learn.org/stable/glossary.html#term-scoring"
    )

    if isinstance(scoring, (list, tuple, set)):
        err_msg = (
            "The list/tuple elements must be unique strings of predefined scorers. "
        )
        try:
            keys = set(scoring)
        except TypeError as e:
            raise ValueError(err_msg) from e

        if len(keys) != len(scoring):
                f"{err_msg} Duplicate elements were found in"
                f" the given list. {scoring!r}"
            )
        elif len(keys) > 0:
            if not all(isinstance(k, str) for k in keys):
                if any(callable(k) for k in keys):
                        f"{err_msg} One or more of the elements "
                        "were callables. Use a dict of score "
                        "name mapped to the scorer callable. "
                        f"Got {scoring!r}"
                    )
                else:
                        f"{err_msg} Non-string types were found "
                        f"in the given list. Got {scoring!r}"
                    )
            scorers = {
                scorer: check_scoring(estimator, scoring=scorer) for scorer in scoring
            }
        else:
            raise ValueError(f"{err_msg} Empty list was given. {scoring!r}")

    elif isinstance(scoring, dict):
        keys = set(scoring)
        if not all(isinstance(k, str) for k in keys):
                "Non-string types were found in the keys of "
                f"the given dict. scoring={scoring!r}"
            )
        if len(keys) == 0:
            raise ValueError(f"An empty dict was passed. {scoring!r}")
        scorers = {
            key: check_scoring(estimator, scoring=scorer)
            for key, scorer in scoring.items()
        }
    else:
        raise ValueError(err_msg_generic)

    return scorers

def _get_response_method_name(response_method):
    try:
        return response_method.__name__
        return _get_response_method_name(response_method.func)

def make_scorer(
    score_func, *, response_method="default", greater_is_better=True, **kwargs
):
    """Make a scorer from a performance metric or loss function.

    A scorer is a wrapper around an arbitrary metric or loss function that is called
    with the signature `scorer(estimator, X, y_true, **kwargs)`.

    It is accepted in all scikit-learn estimators or functions allowing a `scoring`

    The parameter `response_method` allows to specify which method of the estimator
    should be used to feed the scoring/loss function.

    Read more in the :ref:`User Guide <scoring_callable>`.

        Score function (or loss function) with signature
        ``score_func(y, y_pred, **kwargs)``.

    response_method : {"predict_proba", "decision_function", "predict"} or \
            list/tuple of such str, default=None

        Specifies the response method to use get prediction from an estimator
        (i.e. :term:`predict_proba`, :term:`decision_function` or
        :term:`predict`). Possible choices are:

        - if `str`, it corresponds to the name to the method to return;
        - if a list or tuple of `str`, it provides the method names in order of
          preference. The method returned corresponds to the first method in
          the list and which is implemented by `estimator`.
        - if `None`, it is equivalent to `"predict"`.

            None is equivalent to 'predict' and is deprecated. It will be removed in
            version 1.8.

    greater_is_better : bool, default=True
        Whether `score_func` is a score function (default), meaning high is
        good, or a loss function, meaning low is good. In the latter case, the
        scorer object will sign-flip the outcome of the `score_func`.

    **kwargs : additional arguments
        Additional parameters to be passed to `score_func`.

    Returns
    -------
        Callable object that returns a scalar score; greater is better.

    Examples
    --------
    >>> from sklearn.metrics import fbeta_score, make_scorer
    >>> ftwo_scorer = make_scorer(fbeta_score, beta=2)
    >>> ftwo_scorer
    make_scorer(fbeta_score, response_method='predict', beta=2)
    >>> from sklearn.model_selection import GridSearchCV
    >>> grid = GridSearchCV(LinearSVC(), param_grid={'C': [1, 10]},
    ...                     scoring=ftwo_scorer)
    """
    sign = 1 if greater_is_better else -1

    if response_method is None:
            "response_method=None is deprecated in version 1.6 and will be removed "
            "in version 1.8. Leave it to its default value to avoid this warning.",
        )
        response_method = "predict"
    elif response_method == "default":

    return _Scorer(score_func, sign, kwargs, response_method)

def positive_likelihood_ratio(y_true, y_pred):
    return class_likelihood_ratios(y_true, y_pred, replace_undefined_by=1.0)[0]

def negative_likelihood_ratio(y_true, y_pred):
    return class_likelihood_ratios(y_true, y_pred, replace_undefined_by=1.0)[1]

def get_scorer_names():
    """Get the names of all available scorers.

    These names can be passed to :func:`~sklearn.metrics.get_scorer` to
    retrieve the scorer object.

    Returns
    -------
    list of str
        Names of all available scorers.

    Examples
    --------
    >>> from sklearn.metrics import get_scorer_names
    >>> all_scorers = get_scorer_names()
    >>> type(all_scorers)
    <class 'list'>
    >>> all_scorers[:3]
    ['accuracy', 'adjusted_mutual_info_score', 'adjusted_rand_score']
    >>> "roc_auc" in all_scorers
    True
    """
    return sorted(_SCORERS.keys())

def check_scoring(estimator=None, scoring=None, *, allow_none=False, raise_exc=True):
    """Determine scorer from user options.

    A TypeError will be thrown if the estimator cannot be scored.

    estimator : estimator object implementing 'fit' or None, default=None
        The object to use to fit the data. If `None`, then this function may error
        depending on `allow_none`.

    scoring : str, callable, list, tuple, set, or dict, default=None
        Scorer to use. If `scoring` represents a single score, one can use:

        - a single string (see :ref:`scoring_string_names`);
        - a callable (see :ref:`scoring_callable`) that returns a single value;
        - `None`, the `estimator`'s
          :ref:`default evaluation criterion <scoring_api_overview>` is used.

        If `scoring` represents multiple scores, one can use:

        - a list, tuple or set of unique strings;
        - a callable returning a dictionary where the keys are the metric names and the
          values are the metric scorers;
        - a dictionary with metric names as keys and callables a values. The callables
          need to have the signature `callable(estimator, X, y)`.

    allow_none : bool, default=False
        Whether to return None or raise an error if no `scoring` is specified and the
        estimator has no `score` method.

        Whether to raise an exception (if a subset of the scorers in multimetric scoring
        fails) or to return an error code.

        - If set to `True`, raises the failing scorer's exception.
        - If set to `False`, a formatted string of the exception details is passed as
          result of the failing scorer(s).

        This applies if `scoring` is list, tuple, set, or dict. Ignored if `scoring` is
        a str or a callable.

    Returns
    -------
    scoring : callable
        A scorer callable object / function with signature ``scorer(estimator, X, y)``.

    Examples
    --------
    >>> from sklearn.metrics import check_scoring
    >>> from sklearn.tree import DecisionTreeClassifier
    >>> classifier = DecisionTreeClassifier(max_depth=2).fit(X, y)
    >>> scorer = check_scoring(classifier, scoring='accuracy')
    >>> scorer(classifier, X, y)
    0.96...

    >>> from sklearn.metrics import make_scorer, accuracy_score, mean_squared_log_error
    >>> y *= -1
    >>> clf = DecisionTreeClassifier().fit(X, y)
    >>> scoring = {
    ...     "accuracy": make_scorer(accuracy_score),
    ...     "mean_squared_log_error": make_scorer(mean_squared_log_error),
    ... }
    >>> scoring_call = check_scoring(estimator=clf, scoring=scoring, raise_exc=False)
    >>> scores = scoring_call(clf, X, y)
    >>> scores
    {'accuracy': 1.0, 'mean_squared_log_error': 'Traceback ...'}
    """
        return get_scorer(scoring)
    if callable(scoring):
        # Heuristic to ensure user has not passed a metric
        module = getattr(scoring, "__module__", None)
        if (
            hasattr(module, "startswith")
            and module.startswith("sklearn.metrics.")
            and not module.startswith("sklearn.metrics._scorer")
            and not module.startswith("sklearn.metrics.tests.")
        ):
                "scoring value %r looks like it is a metric "
                "function rather than a scorer. A scorer should "
                "require an estimator as its first parameter. "
                "Please use `make_scorer` to convert a metric "
                "to a scorer." % scoring
            )
    if isinstance(scoring, (list, tuple, set, dict)):
        scorers = _check_multimetric_scoring(estimator, scoring=scoring)
        return _MultimetricScorer(scorers=scorers, raise_exc=raise_exc)
    if scoring is None:
        if hasattr(estimator, "score"):
            return _PassthroughScorer(estimator)
        elif allow_none:
        else:
                "If no scoring is specified, the estimator passed should "
                "have a 'score' method. The estimator %r does not." % estimator
            )

def _threshold_scores_to_class_labels(y_score, threshold, classes, pos_label):
    """Threshold `y_score` and return the associated class labels."""
    if pos_label is None:
        map_thresholded_score_to_label = np.array([0, 1])
    else:
        pos_label_idx = np.flatnonzero(classes == pos_label)[0]
        neg_label_idx = np.flatnonzero(classes != pos_label)[0]
        map_thresholded_score_to_label = np.array([neg_label_idx, pos_label_idx])

    return classes[map_thresholded_score_to_label[(y_score >= threshold).astype(int)]]

class _CurveScorer(_BaseScorer):
    """Scorer taking a continuous response and output a score for each threshold.

    sign : int

    thresholds : int or array-like
        Related to the number of decision thresholds for which we want to compute the
        score. If an integer, it will be used to generate `thresholds` thresholds
        uniformly distributed between the minimum and maximum predicted scores. If an
        array-like, it will be used as the thresholds.

    """

    def __init__(self, score_func, sign, kwargs, thresholds, response_method):
            score_func=score_func,
            sign=sign,
            response_method=response_method,
        )
        self._thresholds = thresholds

    def from_scorer(cls, scorer, response_method, thresholds):
        """Create a continuous scorer from a normal scorer."""
        instance = cls(
            score_func=scorer._score_func,
            sign=scorer._sign,
            thresholds=thresholds,
            kwargs=scorer._kwargs,
        )
        # transfer the metadata request
        instance._metadata_request = scorer._get_metadata_request()
        return instance

        Returns
        -------
        scores : ndarray of shape (thresholds,)
            The scores associated to each threshold.

        potential_thresholds : ndarray of shape (thresholds,)
            The potential thresholds used to compute the scores.
        """
        pos_label = self._get_pos_label()
        y_score = method_caller(
            estimator, self._response_method, X, pos_label=pos_label
        )

        if isinstance(self._thresholds, Integral):
            potential_thresholds = np.linspace(
                np.min(y_score), np.max(y_score), self._thresholds
            )
        else:
            potential_thresholds = np.asarray(self._thresholds)
        score_thresholds = [
            self._sign
            * self._score_func(
                y_true,
                _threshold_scores_to_class_labels(
                    y_score, th, estimator.classes_, pos_label
                ),
                **scoring_kwargs,
            )
            for th in potential_thresholds
        ]
        return np.array(score_thresholds), potential_thresholds

        scores = {}

        else:
            }
            )
                        ]

            try:
                    )
                else:
                    raise e
                else:

        """

            [
            ]
        )

        Returns
        -------
        """
        )

        return (
        )

        Returns
        -------
        """
            )

        """
            )

        """
            )

            message=(
            ),
        )

        Returns
        -------
        """
            message=(
            ),
        )

            X,
        )

        try:
            try:
                pass

        Returns
        -------
        """

        """
            )

        )

        )

        Returns
        -------

        """
        )

            )
        else:
                y_true,
                ),
            )
        ]

def _average_binary_score(binary_metric, y_true, y_score, average, sample_weight=None):
    """Average a binary metric for multilabel classification.

    y_true : array, shape = [n_samples] or [n_samples, n_classes]
        True binary labels in binary label indicators.

    y_score : array, shape = [n_samples] or [n_samples, n_classes]
        class, confidence values, or binary decisions.

    average : {None, 'micro', 'macro', 'samples', 'weighted'}, default='macro'

    binary_metric : callable, returns shape [n_classes]
        The binary metric function to use.

    Returns
    -------
    score : float or array of shape [n_classes]
        If not ``None``, average the score, else return the score for each
        classes.

    """
    average_options = (None, "micro", "macro", "weighted", "samples")
        raise ValueError("average has to be one of {0}".format(average_options))

    y_type = type_of_target(y_true)
    if y_type not in ("binary", "multilabel-indicator"):

        return binary_metric(y_true, y_score, sample_weight=sample_weight)

    y_true = check_array(y_true)
    y_score = check_array(y_score)

    not_average_axis = 1
    score_weight = sample_weight
    average_weight = None

    if average == "micro":
        if score_weight is not None:
            score_weight = np.repeat(score_weight, y_true.shape[1])
        y_true = y_true.ravel()
        y_score = y_score.ravel()

    elif average == "weighted":
            average_weight = np.sum(
                np.multiply(y_true, np.reshape(score_weight, (-1, 1))), axis=0
            )
        else:
            average_weight = np.sum(y_true, axis=0)
        if np.isclose(average_weight.sum(), 0.0):
            return 0

    elif average == "samples":
        # swap average_weight <-> score_weight
        average_weight = score_weight
        score_weight = None
        not_average_axis = 0

    if y_true.ndim == 1:
        y_true = y_true.reshape((-1, 1))

    if y_score.ndim == 1:
        y_score = y_score.reshape((-1, 1))

    n_classes = y_score.shape[not_average_axis]
    score = np.zeros((n_classes,))
    for c in range(n_classes):
        y_true_c = y_true.take([c], axis=not_average_axis).ravel()
        y_score_c = y_score.take([c], axis=not_average_axis).ravel()
        score[c] = binary_metric(y_true_c, y_score_c, sample_weight=score_weight)

    # Average the results
    if average is not None:
        if average_weight is not None:
            # Scores with 0 weights are forced to be 0, preventing the average
            # score from being affected by 0-weighted NaN elements.
            average_weight = np.asarray(average_weight)
            score[average_weight == 0] = 0
        return float(np.average(score, weights=average_weight))
    else:
        return score

def _average_multiclass_ovo_score(binary_metric, y_true, y_score, average="macro"):
    """Average one-versus-one scores for multiclass classification.

    Uses the binary metric for one-vs-one multiclass classification,
    where the score is computed according to the Hand & Till (2001) algorithm.

    binary_metric : callable
        The binary metric function to use that accepts the following as input:
            y_true_target : array, shape = [n_samples_target]
                Some sub-array of y_true for a pair of classes designated
                positive and negative in the one-vs-one scheme.
            y_score_target : array, shape = [n_samples_target]
                Scores corresponding to the probability estimates
                of a sample belonging to the designated positive class label

        belonging to a particular class.

    average : {'macro', 'weighted'}, default='macro'
        metric scores:

    Returns
    -------
        Average of the pairwise binary metric scores.
    """
    check_consistent_length(y_true, y_score)

    y_true_unique = np.unique(y_true)
    n_classes = y_true_unique.shape[0]
    n_pairs = n_classes * (n_classes - 1) // 2
    pair_scores = np.empty(n_pairs)

    is_weighted = average == "weighted"
    prevalence = np.empty(n_pairs) if is_weighted else None

    # Compute scores treating a as positive class and b as negative class,
    # then b as positive class and a as negative class
    for ix, (a, b) in enumerate(combinations(y_true_unique, 2)):
        a_mask = y_true == a
        b_mask = y_true == b
        ab_mask = np.logical_or(a_mask, b_mask)

        if is_weighted:
            prevalence[ix] = np.average(ab_mask)

        a_true = a_mask[ab_mask]
        b_true = b_mask[ab_mask]

        a_true_score = binary_metric(a_true, y_score[ab_mask, a])
        b_true_score = binary_metric(b_true, y_score[ab_mask, b])
        pair_scores[ix] = (a_true_score + b_true_score) / 2

    return np.average(pair_scores, weights=prevalence)

def _check_zero_division(zero_division):
    if isinstance(zero_division, str) and zero_division == "warn":
        return np.float64(0.0)
    elif isinstance(zero_division, (int, float)) and zero_division in [0, 1]:
        return np.float64(zero_division)
    else:  # np.isnan(zero_division)

def _check_targets(y_true, y_pred):
    """Check that y_true and y_pred belong to the same classification task.

    This converts multiclass or binary types to a common shape, and raises a
    ValueError for a mix of multilabel and multiclass targets, a mix of
    multilabel formats, for the presence of continuous-valued or multioutput
    targets, or for targets of different lengths.

    Column vectors are squeezed to 1d, while multilabel formats are returned
    as CSR sparse label indicators.

    y_true : array-like

    y_pred : array-like

    Returns
    -------
    type_true : one of {'multilabel-indicator', 'multiclass', 'binary'}
        The type of the true target data, as output by
        ``utils.multiclass.type_of_target``.

    y_true : array or indicator matrix

    y_pred : array or indicator matrix
    """
    xp, _ = get_namespace(y_true, y_pred)
    check_consistent_length(y_true, y_pred)
    type_true = type_of_target(y_true, input_name="y_true")
    type_pred = type_of_target(y_pred, input_name="y_pred")

    y_type = {type_true, type_pred}
    if y_type == {"binary", "multiclass"}:
        y_type = {"multiclass"}

    if len(y_type) > 1:
            "Classification metrics can't handle a mix of {0} and {1} targets".format(
                type_true, type_pred
            )
        )

    # We can't have more than one value on y_type => The set is no more needed
    y_type = y_type.pop()

    # No metrics support "multiclass-multioutput" format
    if y_type not in ["binary", "multiclass", "multilabel-indicator"]:
        raise ValueError("{0} is not supported".format(y_type))

    if y_type in ["binary", "multiclass"]:
        xp, _ = get_namespace(y_true, y_pred)
        y_true = column_or_1d(y_true)
        y_pred = column_or_1d(y_pred)
        if y_type == "binary":
            try:
                unique_values = _union1d(y_true, y_pred, xp)
            except TypeError as e:
                # We expect y_true and y_pred to be of the same data type.
                # If `y_true` was provided to the classifier as strings,
                # `y_pred` given by the classifier will also be encoded with
                # strings. So we raise a meaningful error
                    "Labels in y_true and y_pred should be of the same type. "
                    f"Got y_true={xp.unique(y_true)} and "
                    f"y_pred={xp.unique(y_pred)}. Make sure that the "
                    "predictions provided by the classifier coincides with "
                    "the true labels."
                ) from e
            if unique_values.shape[0] > 2:
                y_type = "multiclass"

    if y_type.startswith("multilabel"):
        if _is_numpy_namespace(xp):
            # XXX: do we really want to sparse-encode multilabel indicators when
            # they are passed as a dense arrays? This is not possible for array
            # API inputs in general hence we only do it for NumPy inputs. But even
            # for NumPy the usefulness is questionable.
            y_true = csr_matrix(y_true)
            y_pred = csr_matrix(y_pred)
        y_type = "multilabel-indicator"

    return y_type, y_true, y_pred

def _validate_multiclass_probabilistic_prediction(
    y_true, y_prob, sample_weight, labels
):
    r"""Convert y_true and y_prob to shape (n_samples, n_classes)

    1. Verify that y_true, y_prob, and sample_weights have the same first dim
    2. Ensure 2 or more classes in y_true i.e. valid classification task. The
       classes are provided by the labels argument, or inferred using y_true.
       When inferring y_true is assumed binary if it has shape (n_samples, ).
    3. Validate y_true, and y_prob have the same number of classes. Convert to
       shape (n_samples, n_classes)

        Ground truth (correct) labels for n_samples samples.

    y_prob : array-like of float, shape=(n_samples, n_classes) or (n_samples,)
        predict_proba method. If `y_prob.shape = (n_samples,)`
        the probabilities provided are assumed to be that of the
        positive class. The labels in `y_prob` are assumed to be
        ordered lexicographically, as done by
        :class:`preprocessing.LabelBinarizer`.

    labels : array-like, default=None
        If not provided, labels will be inferred from y_true. If `labels`
        is `None` and `y_prob` has shape `(n_samples,)` the labels are
        assumed to be binary and are inferred from `y_true`.

    Returns
    -------
    transformed_labels : array of shape (n_samples, n_classes)

    y_prob : array of shape (n_samples, n_classes)
    """
    y_prob = check_array(
        y_prob, ensure_2d=False, dtype=[np.float64, np.float32, np.float16]
    )

    if y_prob.max() > 1:
        raise ValueError(f"y_prob contains values greater than 1: {y_prob.max()}")
    if y_prob.min() < 0:
        raise ValueError(f"y_prob contains values lower than 0: {y_prob.min()}")

    check_consistent_length(y_prob, y_true, sample_weight)
    lb = LabelBinarizer()

        lb = lb.fit(labels)
        # LabelBinarizer does not respect the order implied by labels, which
        # can be misleading.
        if not np.all(lb.classes_ == labels):
                f"Labels passed were {labels}. But this function "
                "assumes labels are ordered lexicographically. "
                f"Pass the ordered labels={lb.classes_.tolist()} and ensure that "
                "the columns of y_prob correspond to this ordering.",
            )
        if not np.isin(y_true, labels).all():
            undeclared_labels = set(y_true) - set(labels)
                f"y_true contains values {undeclared_labels} not belonging "
                f"to the passed labels {labels}."
            )

    else:
        lb = lb.fit(y_true)

    if len(lb.classes_) == 1:
        if labels is None:
                "y_true contains only one label ({0}). Please "
                "provide the list of all expected class labels explicitly through the "
                "labels argument.".format(lb.classes_[0])
            )
        else:
                "The labels array needs to contain at least two "
                "labels, got {0}.".format(lb.classes_)
            )

    transformed_labels = lb.transform(y_true)

    if transformed_labels.shape[1] == 1:
        transformed_labels = np.append(
            1 - transformed_labels, transformed_labels, axis=1
        )

    # If y_prob is of single dimension, assume y_true to be binary
    # and then check.
    if y_prob.ndim == 1:
        y_prob = y_prob[:, np.newaxis]

    # Make sure y_prob is normalized
    y_prob_sum = y_prob.sum(axis=1)
    if not np.allclose(y_prob_sum, 1, rtol=np.sqrt(eps)):
            "The y_prob values do not sum to one. Make sure to pass probabilities.",
            UserWarning,
        )

    # Check if dimensions are consistent.
    transformed_labels = check_array(transformed_labels)
    if len(lb.classes_) != y_prob.shape[1]:
                "y_true and y_prob contain different number of "
                "classes: {0} vs {1}. Please provide the true "
                "labels explicitly through the labels argument. "
                "Classes found in "
                "y_true: {2}".format(
                    transformed_labels.shape[1], y_prob.shape[1], lb.classes_
                )
            )
        else:
                "The number of classes in labels is different "
                "from that in y_prob. Classes found in "
                "labels: {0}".format(lb.classes_)
            )

    return transformed_labels, y_prob

def accuracy_score(y_true, y_pred, *, normalize=True, sample_weight=None):
    """Accuracy classification score.

    In multilabel classification, this function computes subset accuracy:
    the set of labels predicted for a sample must *exactly* match the
    corresponding set of labels in y_true.

    Read more in the :ref:`User Guide <accuracy_score>`.

    y_true : 1d array-like, or label indicator array / sparse matrix

    y_pred : 1d array-like, or label indicator array / sparse matrix
        Predicted labels, as returned by a classifier.

        If ``False``, return the number of correctly classified samples.
        Otherwise, return the fraction of correctly classified samples.

    Returns
    -------
    score : float or int
        If ``normalize == True``, return the fraction of correctly
        classified samples (float), else returns the number of correctly
        classified samples (int).

        The best performance is 1 with ``normalize == True`` and the number
        of samples with ``normalize == False``.

    See Also
    --------
    balanced_accuracy_score : Compute the balanced accuracy to deal with
        imbalanced datasets.
    jaccard_score : Compute the Jaccard similarity coefficient score.
    hamming_loss : Compute the average Hamming loss or Hamming distance between
        two sets of samples.
    zero_one_loss : Compute the Zero-one classification loss. By default, the
        function will return the percentage of imperfectly predicted subsets.

    Examples
    --------
    >>> from sklearn.metrics import accuracy_score
    >>> y_pred = [0, 2, 1, 3]
    >>> y_true = [0, 1, 2, 3]
    >>> accuracy_score(y_true, y_pred)
    0.5
    >>> accuracy_score(y_true, y_pred, normalize=False)
    2.0

    In the multilabel case with binary label indicators:

    >>> accuracy_score(np.array([[0, 1], [1, 1]]), np.ones((2, 2)))
    0.5
    """
    xp, _, device = get_namespace_and_device(y_true, y_pred, sample_weight)
    # Compute accuracy for each possible representation
    y_true, y_pred = attach_unique(y_true, y_pred)
    y_type, y_true, y_pred = _check_targets(y_true, y_pred)
    check_consistent_length(y_true, y_pred, sample_weight)

        differing_labels = _count_nonzero(y_true - y_pred, xp=xp, device=device, axis=1)
        score = xp.asarray(differing_labels == 0, device=device)
    else:
        score = y_true == y_pred

    return float(_average(score, weights=sample_weight, normalize=normalize))

def confusion_matrix(
    y_true, y_pred, *, labels=None, sample_weight=None, normalize=None
):
    """Compute confusion matrix to evaluate the accuracy of a classification.

    By definition a confusion matrix :math:`C` is such that :math:`C_{i, j}`
    is equal to the number of observations known to be in group :math:`i` and
    predicted to be in group :math:`j`.

    Thus in binary classification, the count of true negatives is
    :math:`C_{0,0}`, false negatives is :math:`C_{1,0}`, true positives is
    :math:`C_{1,1}` and false positives is :math:`C_{0,1}`.

    Read more in the :ref:`User Guide <confusion_matrix>`.

        Ground truth (correct) target values.

    y_pred : array-like of shape (n_samples,)
        Estimated targets as returned by a classifier.

    labels : array-like of shape (n_classes), default=None
        List of labels to index the matrix. This may be used to reorder
        or select a subset of labels.
        If ``None`` is given, those that appear at least once
        in ``y_true`` or ``y_pred`` are used in sorted order.

    normalize : {'true', 'pred', 'all'}, default=None
        Normalizes confusion matrix over the true (rows), predicted (columns)
        conditions or all the population. If None, confusion matrix will not be
        normalized.

    Returns
    -------
    C : ndarray of shape (n_classes, n_classes)
        Confusion matrix whose i-th row and j-th
        column entry indicates the number of
        samples with true label being i-th class
        and predicted label being j-th class.

    See Also
    --------
    ConfusionMatrixDisplay.from_estimator : Plot the confusion matrix
        given an estimator, the data, and the label.
    ConfusionMatrixDisplay.from_predictions : Plot the confusion matrix
        given the true and predicted labels.
    ConfusionMatrixDisplay : Confusion Matrix visualization.

    .. [1] `Wikipedia entry for the Confusion matrix
           <https://en.wikipedia.org/wiki/Confusion_matrix>`_
           (Wikipedia and other references may use a different
           convention for axes).

    Examples
    --------
    >>> from sklearn.metrics import confusion_matrix
    >>> y_true = [2, 0, 2, 2, 0, 1]
    >>> y_pred = [0, 0, 2, 2, 0, 2]
    >>> confusion_matrix(y_true, y_pred)
    array([[2, 0, 0],
           [0, 0, 1],
           [1, 0, 2]])

    >>> y_true = ["cat", "ant", "cat", "cat", "ant", "bird"]
    >>> y_pred = ["ant", "ant", "cat", "cat", "ant", "cat"]
    >>> confusion_matrix(y_true, y_pred, labels=["ant", "bird", "cat"])
           [0, 0, 1],
           [1, 0, 2]])

    In the binary case, we can extract true positives, etc. as follows:

    >>> tn, fp, fn, tp = confusion_matrix([0, 1, 0, 1], [1, 1, 1, 0]).ravel().tolist()
    >>> (tn, fp, fn, tp)
    (0, 2, 1, 1)
    """
    if y_type not in ("binary", "multiclass"):
        raise ValueError("%s is not supported" % y_type)

        labels = unique_labels(y_true, y_pred)
    else:
        labels = np.asarray(labels)
        n_labels = labels.size
        if n_labels == 0:
            raise ValueError("'labels' should contains at least one label.")
        elif y_true.size == 0:
            return np.zeros((n_labels, n_labels), dtype=int)
        elif len(np.intersect1d(y_true, labels)) == 0:
            raise ValueError("At least one label specified must be in y_true")

        sample_weight = np.ones(y_true.shape[0], dtype=np.int64)
    else:
        sample_weight = np.asarray(sample_weight)

    n_labels = labels.size
    # If labels are not consecutive integers starting from zero, then
    # y_true and y_pred must be converted into index form
    need_index_conversion = not (
        labels.dtype.kind in {"i", "u", "b"}
        and np.all(labels == np.arange(n_labels))
        and y_true.min() >= 0
        and y_pred.min() >= 0
    )
    if need_index_conversion:
        label_to_ind = {y: x for x, y in enumerate(labels)}
        y_pred = np.array([label_to_ind.get(x, n_labels + 1) for x in y_pred])
        y_true = np.array([label_to_ind.get(x, n_labels + 1) for x in y_true])

    # intersect y_pred, y_true with labels, eliminate items not in labels
    ind = np.logical_and(y_pred < n_labels, y_true < n_labels)
    if not np.all(ind):
        y_pred = y_pred[ind]
        y_true = y_true[ind]
        # also eliminate weights of eliminated items
        sample_weight = sample_weight[ind]

    # Choose the accumulator dtype to always have high precision
    if sample_weight.dtype.kind in {"i", "u", "b"}:
        dtype = np.int64
    else:
        dtype = np.float64

    cm = coo_matrix(
        (sample_weight, (y_true, y_pred)),
        shape=(n_labels, n_labels),
    ).toarray()

    with np.errstate(all="ignore"):
        if normalize == "true":
            cm = cm / cm.sum(axis=1, keepdims=True)
        elif normalize == "pred":
            cm = cm / cm.sum(axis=0, keepdims=True)
        elif normalize == "all":
            cm = cm / cm.sum()
        cm = np.nan_to_num(cm)

    if cm.shape == (1, 1):
            (
                "A single label was found in 'y_true' and 'y_pred'. For the confusion "
                "matrix to have the correct shape, use the 'labels' parameter to pass "
                "all known labels."
            ),
        )

    return cm

def multilabel_confusion_matrix(
    y_true, y_pred, *, sample_weight=None, labels=None, samplewise=False
):
    """Compute a confusion matrix for each class or sample.

    Compute class-wise (default) or sample-wise (samplewise=True) multilabel
    confusion matrix to evaluate the accuracy of a classification, and output
    confusion matrices for each class or sample.

    In multilabel confusion matrix :math:`MCM`, the count of true negatives
    is :math:`MCM_{:,0,0}`, false negatives is :math:`MCM_{:,1,0}`,
    true positives is :math:`MCM_{:,1,1}` and false positives is
    :math:`MCM_{:,0,1}`.

    Multiclass data will be treated as if binarized under a one-vs-rest
    transformation. Returned confusion matrices will be in the order of
    sorted unique labels in the union of (y_true, y_pred).

    Read more in the :ref:`User Guide <multilabel_confusion_matrix>`.

    y_true : {array-like, sparse matrix} of shape (n_samples, n_outputs) or \
            (n_samples,)

    y_pred : {array-like, sparse matrix} of shape (n_samples, n_outputs) or \

        A list of classes or column indices to select some (or to force
        inclusion of classes absent from the data).

    samplewise : bool, default=False
        In the multilabel case, this calculates a confusion matrix per sample.

    Returns
    -------
    multi_confusion : ndarray of shape (n_outputs, 2, 2)
        A 2x2 confusion matrix corresponding to each output in the input.
        When calculating class-wise multi_confusion (default), then
        n_outputs = n_labels; when calculating sample-wise multi_confusion
        (samplewise=True), n_outputs = n_samples. If ``labels`` is defined,
        the results will be returned in the order specified in ``labels``,
        otherwise the results will be returned in sorted order by default.

    See Also
    --------
    confusion_matrix : Compute confusion matrix to evaluate the accuracy of a

    Notes
    -----
    The `multilabel_confusion_matrix` calculates class-wise or sample-wise
    multilabel confusion matrices, and in multiclass tasks, labels are
    binarized under a one-vs-rest way; while
    :func:`~sklearn.metrics.confusion_matrix` calculates one confusion matrix
    for confusion between every two classes.

    Examples
    --------
    Multilabel-indicator case:

    >>> from sklearn.metrics import multilabel_confusion_matrix
    >>> y_true = np.array([[1, 0, 1],
    ...                    [0, 1, 0]])
    >>> y_pred = np.array([[1, 0, 0],
    ...                    [0, 1, 1]])
    >>> multilabel_confusion_matrix(y_true, y_pred)
    array([[[1, 0],
            [0, 1]],
    <BLANKLINE>
           [[1, 0],
            [0, 1]],
           [[0, 1],
            [1, 0]]])

    >>> multilabel_confusion_matrix(y_true, y_pred,
    ...                             labels=["ant", "bird", "cat"])
    array([[[3, 1],
            [0, 2]],
           [[5, 0],
            [1, 0]],
           [[2, 1],
            [1, 2]]])
    """
    device_ = device(y_true, y_pred)
        sample_weight = column_or_1d(sample_weight, device=device_)

    if y_type not in ("binary", "multiclass", "multilabel-indicator"):

    present_labels = unique_labels(y_true, y_pred)
        labels = present_labels
        n_labels = None
    else:
        labels = xp.asarray(labels, device=device_)
        n_labels = labels.shape[0]
        labels = xp.concat(
            [labels, xpx.setdiff1d(present_labels, labels, assume_unique=True, xp=xp)],
            axis=-1,
        )

        if samplewise:
                "Samplewise metrics are not available outside of "
                "multilabel classification."
            )

        le = LabelEncoder()
        le.fit(labels)
        y_true = le.transform(y_true)
        y_pred = le.transform(y_pred)
        sorted_labels = le.classes_

        # labels are now from 0 to len(labels) - 1 -> use bincount
        tp = y_true == y_pred
        tp_bins = y_true[tp]
            tp_bins_weights = sample_weight[tp]
        else:
            tp_bins_weights = None

        if tp_bins.shape[0]:
            tp_sum = _bincount(
                tp_bins, weights=tp_bins_weights, minlength=labels.shape[0], xp=xp
            )
        else:
            # Pathological case
            true_sum = pred_sum = tp_sum = xp.zeros(labels.shape[0])
        if y_pred.shape[0]:
            pred_sum = _bincount(
                y_pred, weights=sample_weight, minlength=labels.shape[0], xp=xp
            )
        if y_true.shape[0]:
            true_sum = _bincount(
                y_true, weights=sample_weight, minlength=labels.shape[0], xp=xp
            )

        # Retain only selected labels
        indices = _searchsorted(sorted_labels, labels[:n_labels], xp=xp)
        tp_sum = xp.take(tp_sum, indices, axis=0)
        true_sum = xp.take(true_sum, indices, axis=0)
        pred_sum = xp.take(pred_sum, indices, axis=0)

    else:
        sum_axis = 1 if samplewise else 0

        # All labels are index integers for multilabel.
        # Select labels:
        if labels.shape != present_labels.shape or xp.any(
            xp.not_equal(labels, present_labels)
        ):
            if xp.max(labels) > xp.max(present_labels):
                    "All labels must be in [0, n labels) for "
                    "multilabel targets. "
                    "Got %d > %d" % (xp.max(labels), xp.max(present_labels))
                )
            if xp.min(labels) < 0:
                    "Got %d < 0" % xp.min(labels)
                )

        if n_labels is not None:
            y_true = y_true[:, labels[:n_labels]]
            y_pred = y_pred[:, labels[:n_labels]]

        if issparse(y_true) or issparse(y_pred):
            true_and_pred = y_true.multiply(y_pred)
        else:
            true_and_pred = xp.multiply(y_true, y_pred)

        # calculate weighted counts
        tp_sum = _count_nonzero(
            true_and_pred,
            axis=sum_axis,
            xp=xp,
            device=device_,
        )
        pred_sum = _count_nonzero(
            y_pred,
            xp=xp,
        )
        true_sum = _count_nonzero(
            y_true,
            xp=xp,
        )

    fp = pred_sum - tp_sum
    fn = true_sum - tp_sum
    tp = tp_sum

    if sample_weight is not None and samplewise:
        tp = xp.asarray(tp)
        fp = xp.asarray(fp)
        fn = xp.asarray(fn)
        tn = sample_weight * y_true.shape[1] - tp - fp - fn
    elif sample_weight is not None:
        tn = xp.sum(sample_weight) - tp - fp - fn
    elif samplewise:
        tn = y_true.shape[1] - tp - fp - fn
    else:
        tn = y_true.shape[0] - tp - fp - fn

    return xp.reshape(xp.stack([tn, fp, fn, tp]).T, (-1, 2, 2))

def cohen_kappa_score(y1, y2, *, labels=None, weights=None, sample_weight=None):
    r"""Compute Cohen's kappa: a statistic that measures inter-annotator agreement.

    This function computes Cohen's kappa [1]_, a score that expresses the level
    of agreement between two annotators on a classification problem. It is
    defined as

    .. math::
        \kappa = (p_o - p_e) / (1 - p_e)

    where :math:`p_o` is the empirical probability of agreement on the label
    assigned to any sample (the observed agreement ratio), and :math:`p_e` is
    the expected agreement when both annotators assign labels randomly.
    :math:`p_e` is estimated using a per-annotator empirical prior over the
    class labels [2]_.

    Read more in the :ref:`User Guide <cohen_kappa>`.

    y1 : array-like of shape (n_samples,)
        Labels assigned by the first annotator.

    y2 : array-like of shape (n_samples,)
        Labels assigned by the second annotator. The kappa statistic is
        symmetric, so swapping ``y1`` and ``y2`` doesn't change the value.

        List of labels to index the matrix. This may be used to select a
        subset of labels. If `None`, all labels that appear at least once in
        ``y1`` or ``y2`` are used. Note that at least one label in `labels` must be
         present in `y1`, even though this function is otherwise agnostic to the order
         of `y1` and `y2`.

    weights : {'linear', 'quadratic'}, default=None
        Weighting type to calculate the score. `None` means not weighted;
        "linear" means linear weighting; "quadratic" means quadratic weighting.

    Returns
    -------
    kappa : float
        The kappa statistic, which is a number between -1 and 1. The maximum
        value means complete agreement; zero or lower means chance agreement.

    .. [1] :doi:`J. Cohen (1960). "A coefficient of agreement for nominal scales".
           Educational and Psychological Measurement 20(1):37-46.
           <10.1177/001316446002000104>`
    .. [2] `R. Artstein and M. Poesio (2008). "Inter-coder agreement for
           computational linguistics". Computational Linguistics 34(4):555-596
           <https://www.mitpressjournals.org/doi/pdf/10.1162/coli.07-034-R2>`_.
    .. [3] `Wikipedia entry for the Cohen's kappa
            <https://en.wikipedia.org/wiki/Cohen%27s_kappa>`_.

    Examples
    --------
    >>> from sklearn.metrics import cohen_kappa_score
    >>> y1 = ["negative", "positive", "negative", "neutral", "positive"]
    >>> y2 = ["negative", "positive", "negative", "neutral", "negative"]
    >>> cohen_kappa_score(y1, y2)
    0.6875
    """
    try:
        confusion = confusion_matrix(y1, y2, labels=labels, sample_weight=sample_weight)
    except ValueError as e:
        if "At least one label specified must be in y_true" in str(e):
            msg = (
                "At least one label in `labels` must be present in `y1` (even though "
                "`cohen_kappa_score` is otherwise agnostic to the order of `y1` and "
                "`y2`)."
            )
            raise ValueError(msg) from e
        raise

    n_classes = confusion.shape[0]
    sum0 = np.sum(confusion, axis=0)
    sum1 = np.sum(confusion, axis=1)
    expected = np.outer(sum0, sum1) / np.sum(sum0)

        w_mat = np.ones([n_classes, n_classes], dtype=int)
        w_mat.flat[:: n_classes + 1] = 0
    else:  # "linear" or "quadratic"
        w_mat = np.zeros([n_classes, n_classes], dtype=int)
        w_mat += np.arange(n_classes)
        if weights == "linear":
            w_mat = np.abs(w_mat - w_mat.T)
        else:
            w_mat = (w_mat - w_mat.T) ** 2

    k = np.sum(w_mat * confusion) / np.sum(w_mat * expected)
    return float(1 - k)

def jaccard_score(
    y_true,
    y_pred,
    *,
    pos_label=1,
    average="binary",
    zero_division="warn",
):
    """Jaccard similarity coefficient score.

    The Jaccard index [1], or Jaccard similarity coefficient, defined as
    the size of the intersection divided by the size of the union of two label
    sets, is used to compare set of predicted labels for a sample to the
    corresponding set of labels in ``y_true``.

    Support beyond term:`binary` targets is achieved by treating :term:`multiclass`
    and :term:`multilabel` data as a collection of binary problems, one for each
    label. For the :term:`binary` case, setting `average='binary'` will return the
    Jaccard similarity coefficient for `pos_label`. If `average` is not `'binary'`,
    `pos_label` is ignored and scores for both classes are computed, then averaged or
    both returned (when `average=None`). Similarly, for :term:`multiclass` and
    :term:`multilabel` targets, scores for all `labels` are either returned or
    averaged depending on the `average` parameter. Use `labels` specify the set of
    labels to calculate the score for.

    Read more in the :ref:`User Guide <jaccard_similarity_score>`.

        The set of labels to include when `average != 'binary'`, and their
        order if `average is None`. Labels present in the data can be
        excluded, for example in multiclass classification to exclude a "negative
        class". Labels not present in the data can be included and will be
        "assigned" 0 samples. For multilabel targets, labels are column indices.
        By default, all labels in `y_true` and `y_pred` are used in sorted order.

        The class to report if `average='binary'` and the data is binary,
        otherwise this parameter is ignored.
        For multiclass or multilabel targets, set `labels=[pos_label]` and
        `average != 'binary'` to report metrics for one label only.

    average : {'micro', 'macro', 'samples', 'weighted', \
            'binary'} or None, default='binary'
        If ``None``, the scores for each class are returned. Otherwise, this
        determines the type of averaging performed on the data:

        ``'binary'``:
            Only report results for the class specified by ``pos_label``.
            This is applicable only if targets (``y_{true,pred}``) are binary.
            Calculate metrics globally by counting the total true positives,
            false negatives and false positives.
            by support (the number of true instances for each label). This
            alters 'macro' to account for label imbalance.
            Calculate metrics for each instance, and find their average (only
            meaningful for multilabel classification).

    zero_division : "warn", {0.0, 1.0}, default="warn"
        Sets the value to return when there is a zero division, i.e. when there
        there are no negative values in predictions and labels. If set to
        "warn", this acts like 0, but a warning is also raised.

    Returns
    -------
    score : float or ndarray of shape (n_unique_labels,), dtype=np.float64
        The Jaccard score. When `average` is not `None`, a single scalar is
        returned.

    See Also
    --------
    accuracy_score : Function for calculating the accuracy score.
    f1_score : Function for calculating the F1 score.
    multilabel_confusion_matrix : Function for computing a confusion matrix\
                                  for each class or sample.

    Notes
    -----
    :func:`jaccard_score` may be a poor metric if there are no
    positives for some samples or classes. Jaccard is undefined if there are
    no true or predicted labels, and our implementation will return a score
    of 0 with a warning.

    .. [1] `Wikipedia entry for the Jaccard index
           <https://en.wikipedia.org/wiki/Jaccard_index>`_.

    Examples
    --------
    >>> from sklearn.metrics import jaccard_score
    >>> y_true = np.array([[0, 1, 1],
    ...                    [1, 1, 0]])
    >>> y_pred = np.array([[1, 1, 1],
    ...                    [1, 0, 0]])

    In the binary case:

    >>> jaccard_score(y_true[0], y_pred[0])
    0.6666...

    In the 2D comparison case (e.g. image similarity):

    >>> jaccard_score(y_true, y_pred, average="micro")
    0.6

    In the multilabel case:

    >>> jaccard_score(y_true, y_pred, average='samples')
    0.5833...
    >>> jaccard_score(y_true, y_pred, average='macro')
    0.6666...
    >>> jaccard_score(y_true, y_pred, average=None)
    array([0.5, 0.5, 1. ])

    In the multiclass case:

    >>> y_pred = [0, 2, 1, 2]
    >>> y_true = [0, 1, 2, 2]
    array([1. , 0. , 0.33...])
    """
    labels = _check_set_wise_labels(y_true, y_pred, average, labels, pos_label)
    samplewise = average == "samples"
    MCM = multilabel_confusion_matrix(
        y_true,
        y_pred,
        labels=labels,
        samplewise=samplewise,
    )
    numerator = MCM[:, 1, 1]
    denominator = MCM[:, 1, 1] + MCM[:, 0, 1] + MCM[:, 1, 0]

        numerator = np.array([numerator.sum()])
        denominator = np.array([denominator.sum()])

    jaccard = _prf_divide(
        numerator,
        denominator,
        "jaccard",
        "true or predicted",
        average,
        ("jaccard",),
        zero_division=zero_division,
    )
    if average is None:
        return jaccard
    if average == "weighted":
        weights = MCM[:, 1, 0] + MCM[:, 1, 1]
        if not np.any(weights):
            # numerator is 0, and warning should have already been issued
            weights = None
    elif average == "samples" and sample_weight is not None:
        weights = sample_weight
    else:
        weights = None
    return float(np.average(jaccard, weights=weights))

def matthews_corrcoef(y_true, y_pred, *, sample_weight=None):
    """Compute the Matthews correlation coefficient (MCC).

    The Matthews correlation coefficient is used in machine learning as a
    measure of the quality of binary and multiclass classifications. It takes
    into account true and false positives and negatives and is generally
    regarded as a balanced measure which can be used even if the classes are of
    very different sizes. The MCC is in essence a correlation coefficient value
    between -1 and +1. A coefficient of +1 represents a perfect prediction, 0
    an average random prediction and -1 an inverse prediction.  The statistic
    is also known as the phi coefficient. [source: Wikipedia]

    Binary and multiclass labels are supported.  Only in the binary case does
    this relate to information about true and false positives and negatives.
    See references below.

    Read more in the :ref:`User Guide <matthews_corrcoef>`.

    Returns
    -------
    mcc : float
        The Matthews correlation coefficient (+1 represents a perfect
        prediction, 0 an average random prediction and -1 and inverse
        prediction).

    .. [1] :doi:`Baldi, Brunak, Chauvin, Andersen and Nielsen, (2000). Assessing the
       accuracy of prediction algorithms for classification: an overview.
       <10.1093/bioinformatics/16.5.412>`

    .. [2] `Wikipedia entry for the Matthews Correlation Coefficient (phi coefficient)
       <https://en.wikipedia.org/wiki/Phi_coefficient>`_.

    .. [3] `Gorodkin, (2004). Comparing two K-category assignments by a
        K-category correlation coefficient
        <https://www.sciencedirect.com/science/article/pii/S1476927104000799>`_.

    .. [4] `Jurman, Riccadonna, Furlanello, (2012). A Comparison of MCC and CEN
        Error Measures in MultiClass Prediction
        <https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0041882>`_.

    Examples
    --------
    >>> from sklearn.metrics import matthews_corrcoef
    >>> y_true = [+1, +1, +1, -1]
    >>> y_pred = [+1, -1, +1, +1]
    >>> matthews_corrcoef(y_true, y_pred)
    -0.33...
    """

    lb = LabelEncoder()
    lb.fit(np.hstack([y_true, y_pred]))
    y_true = lb.transform(y_true)
    y_pred = lb.transform(y_pred)

    C = confusion_matrix(y_true, y_pred, sample_weight=sample_weight)
    t_sum = C.sum(axis=1, dtype=np.float64)
    p_sum = C.sum(axis=0, dtype=np.float64)
    n_correct = np.trace(C, dtype=np.float64)
    n_samples = p_sum.sum()
    cov_ytyp = n_correct * n_samples - np.dot(t_sum, p_sum)
    cov_ypyp = n_samples**2 - np.dot(p_sum, p_sum)
    cov_ytyt = n_samples**2 - np.dot(t_sum, t_sum)

    cov_ypyp_ytyt = cov_ypyp * cov_ytyt
    if cov_ypyp_ytyt == 0:
        return 0.0
    else:
        return float(cov_ytyp / np.sqrt(cov_ypyp_ytyt))

def zero_one_loss(y_true, y_pred, *, normalize=True, sample_weight=None):
    """Zero-one classification loss.

    If normalize is ``True``, return the fraction of misclassifications
    (float), else it returns the number of misclassifications (int). The best
    performance is 0.

    Read more in the :ref:`User Guide <zero_one_loss>`.

        If ``False``, return the number of misclassifications.
        Otherwise, return the fraction of misclassifications.

    Returns
    -------
    loss : float or int,
        If ``normalize == True``, return the fraction of misclassifications
        (float), else it returns the number of misclassifications (int).

    See Also
    --------

    Notes
    -----
    In multilabel classification, the zero_one_loss function corresponds to
    the subset zero-one loss: for each sample, the entire set of labels must be
    correctly predicted, otherwise the loss for that sample is equal to one.

    Examples
    --------
    >>> from sklearn.metrics import zero_one_loss
    >>> y_pred = [1, 2, 3, 4]
    >>> y_true = [2, 2, 3, 4]
    >>> zero_one_loss(y_true, y_pred)
    0.25
    >>> zero_one_loss(y_true, y_pred, normalize=False)
    1.0

    >>> zero_one_loss(np.array([[0, 1], [1, 1]]), np.ones((2, 2)))
    0.5
    """
    score = accuracy_score(
        y_true, y_pred, normalize=normalize, sample_weight=sample_weight
    )

        return 1 - score
    else:
            n_samples = xp.sum(sample_weight)
        else:
            n_samples = _num_samples(y_true)
        return n_samples - score

def f1_score(
    y_true,
    y_pred,
    *,
):
    """Compute the F1 score, also known as balanced F-score or F-measure.

    The F1 score can be interpreted as a harmonic mean of the precision and
    recall, where an F1 score reaches its best value at 1 and worst score at 0.
    The relative contribution of precision and recall to the F1 score are
    equal. The formula for the F1 score is:

    .. math::
        \\text{F1} = \\frac{2 * \\text{TP}}{2 * \\text{TP} + \\text{FP} + \\text{FN}}

    Where :math:`\\text{TP}` is the number of true positives, :math:`\\text{FN}` is the
    number of false negatives, and :math:`\\text{FP}` is the number of false positives.
    F1 is by default
    calculated as 0.0 when there are no true positives, false negatives, or
    false positives.

    Support beyond :term:`binary` targets is achieved by treating :term:`multiclass`
    label. For the :term:`binary` case, setting `average='binary'` will return
    F1 score for `pos_label`. If `average` is not `'binary'`, `pos_label` is ignored
    and F1 score for both classes are computed, then averaged or both returned (when
    `average=None`). Similarly, for :term:`multiclass` and :term:`multilabel` targets,
    F1 score for all `labels` are either returned or averaged depending on the
    `average` parameter. Use `labels` specify the set of labels to calculate F1 score
    for.

        .. versionchanged:: 0.17
           Parameter `labels` improved for multiclass problem.

    average : {'micro', 'macro', 'samples', 'weighted', 'binary'} or None, \
            default='binary'
        This parameter is required for multiclass/multilabel targets.
        If ``None``, the metrics for each class are returned. Otherwise, this

            Calculate metrics for each label, and find their average weighted
            alters 'macro' to account for label imbalance; it can result in an
            F-score that is not between precision and recall.
            meaningful for multilabel classification where this differs from
            :func:`accuracy_score`).

    zero_division : {"warn", 0.0, 1.0, np.nan}, default="warn"
        Sets the value to return when there is a zero division, i.e. when all
        predictions and labels are negative.

        Notes:
        - If set to "warn", this acts like 0, but a warning is also raised.
        - If set to `np.nan`, such values will be excluded from the average.

           `np.nan` option was added.

    Returns
    -------
    f1_score : float or array of float, shape = [n_unique_labels]
        F1 score of the positive class in binary classification or weighted
        average of the F1 scores of each class for the multiclass task.

    See Also
    --------
    fbeta_score : Compute the F-beta score.
    precision_recall_fscore_support : Compute the precision, recall, F-score,
        and support.
    multilabel_confusion_matrix : Compute a confusion matrix for each class or
        sample.

    Notes
    -----
    When ``true positive + false positive + false negative == 0`` (i.e. a class
    is completely absent from both ``y_true`` or ``y_pred``), f-score is
    undefined. In such cases, by default f-score will be set to 0.0, and
    ``UndefinedMetricWarning`` will be raised. This behavior can be modified by
    setting the ``zero_division`` parameter.

    .. [1] `Wikipedia entry for the F1-score
           <https://en.wikipedia.org/wiki/F1_score>`_.

    Examples
    --------
    >>> from sklearn.metrics import f1_score
    >>> y_true = [0, 1, 2, 0, 1, 2]
    >>> y_pred = [0, 2, 1, 0, 0, 1]
    >>> f1_score(y_true, y_pred, average='macro')
    0.26...
    >>> f1_score(y_true, y_pred, average='micro')
    0.33...
    >>> f1_score(y_true, y_pred, average='weighted')
    0.26...
    >>> f1_score(y_true, y_pred, average=None)
    array([0.8, 0. , 0. ])

    >>> # binary classification
    >>> y_true_empty = [0, 0, 0, 0, 0, 0]
    >>> y_pred_empty = [0, 0, 0, 0, 0, 0]
    >>> f1_score(y_true_empty, y_pred_empty)
    0.0...
    >>> f1_score(y_true_empty, y_pred_empty, zero_division=1.0)
    1.0...
    >>> f1_score(y_true_empty, y_pred_empty, zero_division=np.nan)
    nan...

    >>> # multilabel classification
    >>> y_true = [[0, 0, 0], [1, 1, 1], [0, 1, 1]]
    >>> y_pred = [[0, 0, 0], [1, 1, 1], [1, 1, 0]]
    array([0.66666667, 1.        , 0.66666667])
    """
    return fbeta_score(
        y_true,
        y_pred,
        beta=1,
        pos_label=pos_label,
        average=average,
    )

def fbeta_score(
    y_true,
    y_pred,
    *,
    beta,
):
    """Compute the F-beta score.

    The F-beta score is the weighted harmonic mean of precision and recall,
    reaching its optimal value at 1 and its worst value at 0.

    The `beta` parameter represents the ratio of recall importance to
    precision importance. `beta > 1` gives more weight to recall, while
    `beta < 1` favors precision. For example, `beta = 2` makes recall twice
    as important as precision, while `beta = 0.5` does the opposite.
    Asymptotically, `beta -> +inf` considers only recall, and `beta -> 0`
    only precision.

    The formula for F-beta score is:

    .. math::

       F_\\beta = \\frac{(1 + \\beta^2) \\text{tp}}
                        {(1 + \\beta^2) \\text{tp} + \\text{fp} + \\beta^2 \\text{fn}}

    Where :math:`\\text{tp}` is the number of true positives, :math:`\\text{fp}` is the
    number of false positives, and :math:`\\text{fn}` is the number of false negatives.

    F-beta score for `pos_label`. If `average` is not `'binary'`, `pos_label` is
    ignored and F-beta score for both classes are computed, then averaged or both
    returned (when `average=None`). Similarly, for :term:`multiclass` and
    :term:`multilabel` targets, F-beta score for all `labels` are either returned or
    labels to calculate F-beta score for.

    beta : float
        Determines the weight of recall in the combined score.

        Notes:

    Returns
    -------
    fbeta_score : float (if average is not None) or array of float, shape =\
        [n_unique_labels]
        F-beta score of the positive class in binary classification or weighted
        average of the F-beta score of each class for the multiclass task.

    See Also
    --------
        sample.

    Notes
    -----
    When ``true positive + false positive + false negative == 0``, f-score
    returns 0.0 and raises ``UndefinedMetricWarning``. This behavior can be
    modified by setting ``zero_division``.

    .. [1] R. Baeza-Yates and B. Ribeiro-Neto (2011).
           Modern Information Retrieval. Addison Wesley, pp. 327-328.

    .. [2] `Wikipedia entry for the F1-score

    Examples
    --------
    >>> from sklearn.metrics import fbeta_score
    >>> fbeta_score(y_true, y_pred, average='macro', beta=0.5)
    0.23...
    >>> fbeta_score(y_true, y_pred, average='micro', beta=0.5)
    0.33...
    >>> fbeta_score(y_true, y_pred, average='weighted', beta=0.5)
    0.23...
    >>> fbeta_score(y_true, y_pred, average=None, beta=0.5)
    array([0.71..., 0.        , 0.        ])
    >>> fbeta_score(y_true, y_pred_empty,
    ...             average="macro", zero_division=np.nan, beta=0.5)
    0.12...
    """

    _, _, f, _ = precision_recall_fscore_support(
        y_true,
        y_pred,
        beta=beta,
        warn_for=("f-score",),
    )
    return f

def _prf_divide(
    numerator, denominator, metric, modifier, average, warn_for, zero_division="warn"
):
    """Performs division and handles divide-by-zero.

    On zero-division, sets the corresponding result elements equal to
    0, 1 or np.nan (according to ``zero_division``). Plus, if
    ``zero_division != "warn"`` raises a warning.

    The metric, modifier and average arguments are used only for determining
    an appropriate warning.
    """
    xp, _ = get_namespace(numerator, denominator)
    dtype_float = _find_matching_floating_dtype(numerator, denominator, xp=xp)
    mask = denominator == 0
    denominator = xp.asarray(denominator, copy=True, dtype=dtype_float)
    denominator[mask] = 1  # avoid infs/nans
    result = xp.asarray(numerator, dtype=dtype_float) / denominator

    if not xp.any(mask):

    # set those with 0 denominator to `zero_division`, and 0 when "warn"
    zero_division_value = _check_zero_division(zero_division)
    result[mask] = zero_division_value

    # we assume the user will be removing warnings if zero_division is set
    # to something different than "warn". If we are computing only f-score
    # the warning will be raised only if precision and recall are ill-defined
    if zero_division != "warn" or metric not in warn_for:

    # build appropriate warning
    if metric in warn_for:
        _warn_prf(average, modifier, f"{metric.capitalize()} is", result.shape[0])

def _warn_prf(average, modifier, msg_start, result_size):
    axis0, axis1 = "sample", "label"
    if average == "samples":
        axis0, axis1 = axis1, axis0
    msg = (
        "{0} ill-defined and being set to 0.0 {{0}} "
        "no {1} {2}s. Use `zero_division` parameter to control"
        " this behavior.".format(msg_start, modifier, axis0)
    )
    if result_size == 1:
        msg = msg.format("due to")
    else:
        msg = msg.format("in {0}s with".format(axis1))
    warnings.warn(msg, UndefinedMetricWarning, stacklevel=2)

def _check_set_wise_labels(y_true, y_pred, average, labels, pos_label):
    """Validation associated with set-wise metrics.

    Returns identified labels.
    """
    if average not in average_options and average != "binary":
        raise ValueError("average has to be one of " + str(average_options))

    present_labels = _tolist(unique_labels(y_true, y_pred))
    if average == "binary":
            if pos_label not in present_labels:
                if len(present_labels) >= 2:
                        f"pos_label={pos_label} is not a valid label. It "
                        f"should be one of {present_labels}"
                    )
            labels = [pos_label]
        else:
            average_options = list(average_options)
            if y_type == "multiclass":
                average_options.remove("samples")
                "Target is %s but average='binary'. Please "
                "choose another average setting, one of %r." % (y_type, average_options)
            )
    elif pos_label not in (None, 1):
            "Note that pos_label (set to %r) is ignored when "
            "average != 'binary' (got %r). You may use "
            "labels=[pos_label] to specify a single positive class."
            % (pos_label, average),
        )
    return labels

def precision_recall_fscore_support(
    y_true,
    y_pred,
    *,
    beta=1.0,
    average=None,
    warn_for=("precision", "recall", "f-score"),
):
    """Compute precision, recall, F-measure and support for each class.

    intuitively the ability of the classifier not to label a negative sample as
    positive.

    The F-beta score can be interpreted as a weighted harmonic mean of
    the precision and recall, where an F-beta score reaches its best
    value at 1 and worst score at 0.

    The F-beta score weights recall more than precision by a factor of
    ``beta``. ``beta == 1.0`` means recall and precision are equally important.

    The support is the number of occurrences of each class in ``y_true``.

    metrics for `pos_label`. If `average` is not `'binary'`, `pos_label` is ignored
    and metrics for both classes are computed, then averaged or both returned (when
    metrics for all `labels` are either returned or averaged depending on the `average`
    parameter. Use `labels` specify the set of labels to calculate metrics for.

    beta : float, default=1.0
        The strength of recall versus precision in the F-score.

    warn_for : list, tuple or set, for internal use
        This determines which warnings will be made in the case that this
        function is being used to return only one of its metrics.

        Sets the value to return when there is a zero division:

        - recall: when there are no positive labels
        - precision: when there are no positive predictions
        - f-score: both

        Notes:

    Returns
    -------
    precision : float (if average is not None) or array of float, shape =\
        Precision score.

    recall : float (if average is not None) or array of float, shape =\
        Recall score.

        F-beta score.

    support : None (if average is not None) or array of int, shape =\
        The number of occurrences of each label in ``y_true``.

    Notes
    -----
    When ``true positive + false positive == 0``, precision is undefined.
    When ``true positive + false negative == 0``, recall is undefined. When
    ``true positive + false negative + false positive == 0``, f-score is
    undefined. In such cases, by default the metric will be set to 0, and
    ``UndefinedMetricWarning`` will be raised. This behavior can be modified
    with ``zero_division``.

    .. [1] `Wikipedia entry for the Precision and recall
           <https://en.wikipedia.org/wiki/Precision_and_recall>`_.

    .. [3] `Discriminative Methods for Multi-labeled Classification Advances
           in Knowledge Discovery and Data Mining (2004), pp. 22-30 by Shantanu
           Godbole, Sunita Sarawagi
           <http://www.godbole.net/shantanu/pubs/multilabelsvm-pakdd04.pdf>`_.

    Examples
    --------
    >>> from sklearn.metrics import precision_recall_fscore_support
    >>> y_true = np.array(['cat', 'dog', 'pig', 'cat', 'dog', 'pig'])
    >>> y_pred = np.array(['cat', 'pig', 'dog', 'cat', 'cat', 'dog'])
    >>> precision_recall_fscore_support(y_true, y_pred, average='macro')
    (0.22..., 0.33..., 0.26..., None)
    >>> precision_recall_fscore_support(y_true, y_pred, average='micro')
    (0.33..., 0.33..., 0.33..., None)
    >>> precision_recall_fscore_support(y_true, y_pred, average='weighted')

    It is possible to compute per-label precisions, recalls, F1-scores and
    supports instead of averaging:

    >>> precision_recall_fscore_support(y_true, y_pred, average=None,
    ... labels=['pig', 'dog', 'cat'])
    (array([0.        , 0.        , 0.66...]),
     array([0., 0., 1.]), array([0. , 0. , 0.8]),
     array([2, 2, 2]))
    """
    _check_zero_division(zero_division)

    # Calculate tp_sum, pred_sum, true_sum ###
        y_true,
        y_pred,
    )
    tp_sum = MCM[:, 1, 1]
    pred_sum = tp_sum + MCM[:, 0, 1]
    true_sum = tp_sum + MCM[:, 1, 0]

    xp, _, device_ = get_namespace_and_device(y_true, y_pred)
        tp_sum = xp.reshape(xp.sum(tp_sum), (1,))
        pred_sum = xp.reshape(xp.sum(pred_sum), (1,))
        true_sum = xp.reshape(xp.sum(true_sum), (1,))

    # Finally, we have all our sufficient statistics. Divide! #
    beta2 = beta**2

    # Divide, and on zero-division, set scores and/or warn according to
    # zero_division:
    precision = _prf_divide(
        tp_sum, pred_sum, "precision", "predicted", average, warn_for, zero_division
    )
    recall = _prf_divide(
        tp_sum, true_sum, "recall", "true", average, warn_for, zero_division
    )

    if np.isposinf(beta):
        f_score = recall
    elif beta == 0:
        f_score = precision
    else:
        # The score is defined as:
        # score = (1 + beta**2) * precision * recall / (beta**2 * precision + recall)
        # Therefore, we can express the score in terms of confusion matrix entries as:
        # score = (1 + beta**2) * tp / ((1 + beta**2) * tp + beta**2 * fn + fp)

        # Array api strict requires all arrays to be of the same type so we
        # need to convert true_sum, pred_sum and tp_sum to the max supported
        # float dtype because beta2 is a float
        max_float_type = _max_precision_float_dtype(xp=xp, device=device_)
        denom = beta2 * xp.astype(true_sum, max_float_type) + xp.astype(
            pred_sum, max_float_type
        )
        f_score = _prf_divide(
            (1 + beta2) * xp.astype(tp_sum, max_float_type),
            denom,
            "f-score",
            "true nor predicted",
            average,
            warn_for,
            zero_division,
        )

        weights = true_sum
    else:

        precision = float(_nanaverage(precision, weights=weights))
        recall = float(_nanaverage(recall, weights=weights))
        f_score = float(_nanaverage(f_score, weights=weights))
        true_sum = None  # return no support

    return precision, recall, f_score, true_sum

def class_likelihood_ratios(
    y_true,
    y_pred,
    *,
    raise_warning="deprecated",
    replace_undefined_by="default",
):
    """Compute binary classification positive and negative likelihood ratios.

    The positive likelihood ratio is `LR+ = sensitivity / (1 - specificity)`
    where the sensitivity or recall is the ratio `tp / (tp + fn)` and the
    specificity is `tn / (tn + fp)`. The negative likelihood ratio is `LR- = (1
    - sensitivity) / specificity`. Here `tp` is the number of true positives,
    `fp` the number of false positives, `tn` is the number of true negatives and
    `fn` the number of false negatives. Both class likelihood ratios can be used
    to obtain post-test probabilities given a pre-test probability.

    `LR+` ranges from 1.0 to infinity. A `LR+` of 1.0 indicates that the probability
    of predicting the positive class is the same for samples belonging to either
    class; therefore, the test is useless. The greater `LR+` is, the more a
    positive prediction is likely to be a true positive when compared with the
    pre-test probability. A value of `LR+` lower than 1.0 is invalid as it would
    indicate that the odds of a sample being a true positive decrease with
    respect to the pre-test odds.

    `LR-` ranges from 0.0 to 1.0. The closer it is to 0.0, the lower the probability
    of a given sample to be a false negative. A `LR-` of 1.0 means the test is
    useless because the odds of having the condition did not change after the
    test. A value of `LR-` greater than 1.0 invalidates the classifier as it
    indicates an increase in the odds of a sample belonging to the positive
    class after being classified as negative. This is the case when the
    classifier systematically predicts the opposite of the true label.

    A typical application in medicine is to identify the positive/negative class
    to the presence/absence of a disease, respectively; the classifier being a
    diagnostic test; the pre-test probability of an individual having the
    disease can be the prevalence of such disease (proportion of a particular
    population found to be affected by a medical condition); and the post-test
    probabilities would be the probability that the condition is truly present
    given a positive test result.

    Read more in the :ref:`User Guide <class_likelihood_ratios>`.

        List of labels to index the matrix. This may be used to select the
        positive and negative classes with the ordering `labels=[negative_class,
        positive_class]`. If `None` is given, those that appear at least once in
        `y_true` or `y_pred` are used in sorted order.

    raise_warning : bool, default=True
        Whether or not a case-specific warning message is raised when there is division
        by zero.

            `raise_warning` was deprecated in version 1.7 and will be removed in 1.9,
            when an :class:`~sklearn.exceptions.UndefinedMetricWarning` will always
            raise in case of a division by zero.

    replace_undefined_by : np.nan, 1.0, or dict, default=np.nan
        Sets the return values for LR+ and LR- when there is a division by zero. Can
        take the following values:

        - `np.nan` to return `np.nan` for both `LR+` and `LR-`
        - `1.0` to return the worst possible scores: `{"LR+": 1.0, "LR-": 1.0}`
        - a dict in the format `{"LR+": value_1, "LR-": value_2}` where the values can
          be non-negative floats, `np.inf` or `np.nan` in the range of the
          likelihood ratios. For example, `{"LR+": 1.0, "LR-": 1.0}` can be used for
          returning the worst scores, indicating a useless model, and `{"LR+": np.inf,
          "LR-": 0.0}` can be used for returning the best scores, indicating a useful
          model.

        If a division by zero occurs, only the affected metric is replaced with the set
        value; the other metric is calculated as usual.

    Returns
    -------
    (positive_likelihood_ratio, negative_likelihood_ratio) : tuple
        A tuple of two floats, the first containing the positive likelihood ratio (LR+)
        and the second the negative likelihood ratio (LR-).

    Warns
    -----
    Raises :class:`~sklearn.exceptions.UndefinedMetricWarning` when `y_true` and
    `y_pred` lead to the following conditions:

        - The number of false positives is 0 and `raise_warning` is set to `True`
          (default): positive likelihood ratio is undefined.
        - The number of true negatives is 0 and `raise_warning` is set to `True`
          (default): negative likelihood ratio is undefined.
        - The sum of true positives and false negatives is 0 (no samples of the positive
          class are present in `y_true`): both likelihood ratios are undefined.

        For the first two cases, an undefined metric can be defined by setting the
        `replace_undefined_by` param.

    .. [1] `Wikipedia entry for the Likelihood ratios in diagnostic testing
           <https://en.wikipedia.org/wiki/Likelihood_ratios_in_diagnostic_testing>`_.

    Examples
    --------
    >>> from sklearn.metrics import class_likelihood_ratios
    >>> class_likelihood_ratios([0, 1, 0, 1, 0], [1, 1, 0, 0, 0],
    ...                          replace_undefined_by=1.0)
    (1.5, 0.75)
    >>> y_true = np.array(["non-cat", "cat", "non-cat", "cat", "non-cat"])
    >>> y_pred = np.array(["cat", "cat", "non-cat", "non-cat", "non-cat"])
    >>> class_likelihood_ratios(y_true, y_pred, replace_undefined_by=1.0)
    (1.33..., 0.66...)
    >>> y_true = np.array(["non-zebra", "zebra", "non-zebra", "zebra", "non-zebra"])
    >>> y_pred = np.array(["zebra", "zebra", "non-zebra", "non-zebra", "non-zebra"])

    To avoid ambiguities, use the notation `labels=[negative_class,
    positive_class]`

    >>> class_likelihood_ratios(y_true, y_pred, labels=["non-cat", "cat"],
    """
    # TODO(1.9): When `raise_warning` is removed, the following changes need to be made:
    # The checks for `raise_warning==True` need to be removed and we will always warn,
    # the default return value of `replace_undefined_by` should be updated from `np.nan`
    # (which was kept for backwards compatibility) to `1.0`, its hidden option
    # ("default") is not used anymore, some warning messages can be removed, the Warns
    # section in the docstring should not mention `raise_warning` anymore and the
    # "Mathematical divergences" section in model_evaluation.rst needs to be updated on
    # the new default behaviour of `replace_undefined_by`.
    if y_type != "binary":
            "class_likelihood_ratios only supports binary classification "
            f"problems, got targets of type: {y_type}"
        )

    msg_deprecated_param = (
        "`raise_warning` was deprecated in version 1.7 and will be removed in 1.9. An "
        "`UndefinedMetricWarning` will always be raised in case of a division by zero "
        "and the value set with the `replace_undefined_by` param will be returned."
    )
    mgs_changed_default = (
        "The default return value of `class_likelihood_ratios` in case of a division "
        "by zero has been deprecated in 1.7 and will be changed to the worst scores "
        "(`(1.0, 1.0)`) in version 1.9. Set `replace_undefined_by=1.0` to use the new"
        "default and to silence this Warning."
    )
    if raise_warning != "deprecated":
            " ".join((msg_deprecated_param, mgs_changed_default)), FutureWarning
        )
    else:
        if replace_undefined_by == "default":
            # TODO(1.9): Remove. If users don't set any return values in case of a
            # division by zero (`raise_warning="deprecated"` and
            # `replace_undefined_by="default"`) they still get a FutureWarning about
            # changing default return values:
            warnings.warn(mgs_changed_default, FutureWarning)
        raise_warning = True

    if replace_undefined_by == "default":
        replace_undefined_by = np.nan

    if replace_undefined_by == 1.0:
        replace_undefined_by = {"LR+": 1.0, "LR-": 1.0}

    if isinstance(replace_undefined_by, dict):
        msg = (
            "The dictionary passed as `replace_undefined_by` needs to be in the form "
            "`{'LR+': `value_1`, 'LR-': `value_2`}` where the value for `LR+` ranges "
            "from `1.0` to `np.inf` or is `np.nan` and the value for `LR-` ranges from "
            f"`0.0` to `1.0` or is `np.nan`; got `{replace_undefined_by}`."
        )
        if ("LR+" in replace_undefined_by) and ("LR-" in replace_undefined_by):
            try:
                desired_lr_pos = replace_undefined_by.get("LR+", None)
                    desired_lr_pos,
                    "positive_likelihood_ratio",
                    target_type=(Real),
                    min_val=1.0,
                    include_boundaries="left",
                )
                desired_lr_neg = replace_undefined_by.get("LR-", None)
                    desired_lr_neg,
                    "negative_likelihood_ratio",
                    min_val=0.0,
                    max_val=1.0,
                    include_boundaries="both",
                )
                raise ValueError(msg) from e
        else:

    cm = confusion_matrix(
        y_true,
        y_pred,
    )

    tn, fp, fn, tp = cm.ravel()
    support_pos = tp + fn
    support_neg = tn + fp
    pos_num = tp * support_neg
    pos_denom = fp * support_pos
    neg_num = fn * support_neg
    neg_denom = tn * support_pos

    # if `support_pos == 0`a division by zero will occur
    if support_pos == 0:
        # TODO(1.9): Change return values in warning message to new default: the worst
        # possible scores: `(1.0, 1.0)`
        msg = (
            "No samples of the positive class are present in `y_true`. "
            "`positive_likelihood_ratio` and `negative_likelihood_ratio` are both set "
            "to `np.nan`."
        )
        warnings.warn(msg, UndefinedMetricWarning, stacklevel=2)
        positive_likelihood_ratio = np.nan
        negative_likelihood_ratio = np.nan

    # if `fp == 0`a division by zero will occur
    if fp == 0:
        if raise_warning:
            if tp == 0:
                msg_beginning = (
                    "No samples were predicted for the positive class and "
                    "`positive_likelihood_ratio` is "
                )
            else:
                msg_beginning = "`positive_likelihood_ratio` is ill-defined and "
            msg_end = "set to `np.nan`. Use the `replace_undefined_by` param to "
            "control this behavior."
            # TODO(1.9): Change return value in warning message to new default: `1.0`,
            # which is the worst possible score for "LR+"
            warnings.warn(msg_beginning + msg_end, UndefinedMetricWarning, stacklevel=2)
        if isinstance(replace_undefined_by, float) and np.isnan(replace_undefined_by):
            positive_likelihood_ratio = replace_undefined_by
        else:
            # replace_undefined_by is a dict and
            # isinstance(replace_undefined_by.get("LR+", None), Real); this includes
            # `np.inf` and `np.nan`
            positive_likelihood_ratio = desired_lr_pos
    else:
        positive_likelihood_ratio = pos_num / pos_denom

    # if `tn == 0`a division by zero will occur
    if tn == 0:
            # which is the worst possible score for "LR-"
            msg = (
                "`negative_likelihood_ratio` is ill-defined and set to `np.nan`. "
                "Use the `replace_undefined_by` param to control this behavior."
            )
            warnings.warn(msg, UndefinedMetricWarning, stacklevel=2)
            negative_likelihood_ratio = replace_undefined_by
        else:
            # isinstance(replace_undefined_by.get("LR-", None), Real); this includes
            # `np.nan`
            negative_likelihood_ratio = desired_lr_neg
    else:
        negative_likelihood_ratio = neg_num / neg_denom

    return float(positive_likelihood_ratio), float(negative_likelihood_ratio)

def precision_score(
    y_true,
    y_pred,
    *,
):
    """Compute the precision.

    The best value is 1 and the worst value is 0.

    precision for `pos_label`. If `average` is not `'binary'`, `pos_label` is ignored
    and precision for both classes are computed, then averaged or both returned (when
    precision for all `labels` are either returned or averaged depending on the
    `average` parameter. Use `labels` specify the set of labels to calculate precision
    for.

        Sets the value to return when there is a zero division.

        Notes:

    Returns
    -------
    precision : float (if average is not None) or array of float of shape \
                (n_unique_labels,)
        Precision of the positive class in binary classification or weighted
        average of the precision of each class for the multiclass task.

    See Also
    --------
    precision_recall_fscore_support : Compute precision, recall, F-measure and
        support for each class.
    recall_score :  Compute the ratio ``tp / (tp + fn)`` where ``tp`` is the
        number of true positives and ``fn`` the number of false negatives.
    PrecisionRecallDisplay.from_estimator : Plot precision-recall curve given
        an estimator and some data.
    PrecisionRecallDisplay.from_predictions : Plot precision-recall curve given
        binary class predictions.
        sample.

    Notes
    -----
    When ``true positive + false positive == 0``, precision returns 0 and
    raises ``UndefinedMetricWarning``. This behavior can be
    modified with ``zero_division``.

    Examples
    --------
    >>> from sklearn.metrics import precision_score
    >>> precision_score(y_true, y_pred, average='macro')
    0.22...
    >>> precision_score(y_true, y_pred, average='micro')
    0.33...
    >>> precision_score(y_true, y_pred, average='weighted')
    0.22...
    >>> precision_score(y_true, y_pred, average=None)
    array([0.66..., 0.        , 0.        ])
    >>> y_pred = [0, 0, 0, 0, 0, 0]
    array([0.33..., 0.        , 0.        ])
    >>> precision_score(y_true, y_pred, average=None, zero_division=1)
    array([0.33..., 1.        , 1.        ])
    >>> precision_score(y_true, y_pred, average=None, zero_division=np.nan)
    array([0.33...,        nan,        nan])

    array([0.5, 1. , 1. ])
    """
    p, _, _, _ = precision_recall_fscore_support(
        y_true,
        y_pred,
        warn_for=("precision",),
    )
    return p

def recall_score(
    y_true,
    y_pred,
    *,
):
    """Compute the recall.

    recall for `pos_label`. If `average` is not `'binary'`, `pos_label` is ignored
    and recall for both classes are computed then averaged or both returned (when
    recall for all `labels` are either returned or averaged depending on the `average`
    parameter. Use `labels` specify the set of labels to calculate recall for.

            F-score that is not between precision and recall. Weighted recall
            is equal to accuracy.

        Notes:

    Returns
    -------
    recall : float (if average is not None) or array of float of shape \
             (n_unique_labels,)
        Recall of the positive class in binary classification or weighted
        average of the recall of each class for the multiclass task.

    See Also
    --------
    precision_score : Compute the ratio ``tp / (tp + fp)`` where ``tp`` is the
        number of true positives and ``fp`` the number of false positives.
    balanced_accuracy_score : Compute balanced accuracy to deal with imbalanced
        datasets.
        sample.

    Notes
    -----
    When ``true positive + false negative == 0``, recall returns 0 and raises
    ``UndefinedMetricWarning``. This behavior can be modified with
    ``zero_division``.

    Examples
    --------
    >>> from sklearn.metrics import recall_score
    >>> recall_score(y_true, y_pred, average='macro')
    0.33...
    >>> recall_score(y_true, y_pred, average='micro')
    0.33...
    >>> recall_score(y_true, y_pred, average='weighted')
    0.33...
    >>> recall_score(y_true, y_pred, average=None)
    array([1., 0., 0.])
    >>> y_true = [0, 0, 0, 0, 0, 0]
    array([0.5, 0. , 0. ])
    >>> recall_score(y_true, y_pred, average=None, zero_division=1)
    >>> recall_score(y_true, y_pred, average=None, zero_division=np.nan)
    array([0.5, nan, nan])

    array([1. , 1. , 0.5])
    """
    _, r, _, _ = precision_recall_fscore_support(
        y_true,
        y_pred,
        warn_for=("recall",),
    )
    return r

def balanced_accuracy_score(y_true, y_pred, *, sample_weight=None, adjusted=False):
    """Compute the balanced accuracy.

    The balanced accuracy in binary and multiclass classification problems to
    deal with imbalanced datasets. It is defined as the average of recall
    obtained on each class.

    The best value is 1 and the worst value is 0 when ``adjusted=False``.

    Read more in the :ref:`User Guide <balanced_accuracy_score>`.

    adjusted : bool, default=False
        When true, the result is adjusted for chance, so that random
        performance would score 0, while keeping perfect performance at a score
        of 1.

    Returns
    -------
    balanced_accuracy : float
        Balanced accuracy score.

    See Also
    --------
    average_precision_score : Compute average precision (AP) from prediction
        scores.
    precision_score : Compute the precision score.
    recall_score : Compute the recall score.
    roc_auc_score : Compute Area Under the Receiver Operating Characteristic
        Curve (ROC AUC) from prediction scores.

    Notes
    -----
    Some literature promotes alternative definitions of balanced accuracy. Our
    definition is equivalent to :func:`accuracy_score` with class-balanced
    sample weights, and shares desirable properties with the binary case.
    See the :ref:`User Guide <balanced_accuracy_score>`.

    .. [1] Brodersen, K.H.; Ong, C.S.; Stephan, K.E.; Buhmann, J.M. (2010).
           The balanced accuracy and its posterior distribution.
           Proceedings of the 20th International Conference on Pattern
           Recognition, 3121-24.
    .. [2] John. D. Kelleher, Brian Mac Namee, Aoife D'Arcy, (2015).
           `Fundamentals of Machine Learning for Predictive Data Analytics:
           Algorithms, Worked Examples, and Case Studies
           <https://mitpress.mit.edu/books/fundamentals-machine-learning-predictive-data-analytics>`_.

    Examples
    --------
    >>> from sklearn.metrics import balanced_accuracy_score
    >>> y_true = [0, 1, 0, 0, 1, 0]
    >>> y_pred = [0, 1, 0, 0, 0, 1]
    >>> balanced_accuracy_score(y_true, y_pred)
    0.625
    """
        per_class = np.diag(C) / C.sum(axis=1)
    if np.any(np.isnan(per_class)):
        warnings.warn("y_pred contains classes not in y_true")
        per_class = per_class[~np.isnan(per_class)]
    score = np.mean(per_class)
    if adjusted:
        n_classes = len(per_class)
        chance = 1 / n_classes
        score -= chance
        score /= 1 - chance
    return float(score)

def classification_report(
    y_true,
    y_pred,
    *,
    target_names=None,
    digits=2,
    output_dict=False,
):
    """Build a text report showing the main classification metrics.

    Read more in the :ref:`User Guide <classification_report>`.

    labels : array-like of shape (n_labels,), default=None
        Optional list of label indices to include in the report.

    target_names : array-like of shape (n_labels,), default=None
        Optional display names matching the labels (same order).

    digits : int, default=2
        Number of digits for formatting output floating point values.
        When ``output_dict`` is ``True``, this will be ignored and the
        returned values will not be rounded.

    output_dict : bool, default=False
        If True, return output as dict.

        Sets the value to return when there is a zero division. If set to
        "warn", this acts as 0, but warnings are also raised.

    Returns
    -------
    report : str or dict
        Text summary of the precision, recall, F1 score for each class.
        Dictionary returned if output_dict is True. Dictionary has the
        following structure::

            {'label 1': {'precision':0.5,
                         'recall':1.0,
                         'f1-score':0.67,
                         'support':1},
             'label 2': { ... },
              ...
            }

        The reported averages include macro average (averaging the unweighted
        mean per label), weighted average (averaging the support-weighted mean
        per label), and sample average (only for multilabel classification).
        Micro average (averaging the total true positives, false negatives and
        false positives) is only shown for multi-label or multi-class
        with a subset of classes, because it corresponds to accuracy
        otherwise and would be the same for all metrics.
        See also :func:`precision_recall_fscore_support` for more details
        on averages.

        Note that in binary classification, recall of the positive class
        is also known as "sensitivity"; recall of the negative class is
        "specificity".

    See Also
    --------
    precision_recall_fscore_support: Compute precision, recall, F-measure and
    confusion_matrix: Compute confusion matrix to evaluate the accuracy of a
    multilabel_confusion_matrix: Compute a confusion matrix for each class or sample.

    Examples
    --------
    >>> from sklearn.metrics import classification_report
    >>> y_true = [0, 1, 2, 2, 2]
    >>> y_pred = [0, 0, 2, 2, 1]
    >>> target_names = ['class 0', 'class 1', 'class 2']
    >>> print(classification_report(y_true, y_pred, target_names=target_names))
                  precision    recall  f1-score   support
         class 0       0.50      1.00      0.67         1
         class 1       0.00      0.00      0.00         1
         class 2       1.00      0.67      0.80         3
        accuracy                           0.60         5
       macro avg       0.50      0.56      0.49         5
    weighted avg       0.70      0.60      0.61         5
    >>> y_pred = [1, 1, 0]
    >>> y_true = [1, 1, 1]
    >>> print(classification_report(y_true, y_pred, labels=[1, 2, 3]))
               1       1.00      0.67      0.80         3
               2       0.00      0.00      0.00         0
               3       0.00      0.00      0.00         0
       micro avg       1.00      0.67      0.80         3
       macro avg       0.33      0.22      0.27         3
    weighted avg       1.00      0.67      0.80         3
    """

        labels_given = False
    else:
        labels_given = True

    # labelled micro average
    micro_is_accuracy = (y_type == "multiclass" or y_type == "binary") and (
        not labels_given or (set(labels) >= set(unique_labels(y_true, y_pred)))
    )

    if target_names is not None and len(labels) != len(target_names):
        if labels_given:
                "labels size, {0}, does not match size of target_names, {1}".format(
                    len(labels), len(target_names)
                )
            )
        else:
                "Number of classes, {0}, does not match size of "
                "target_names, {1}. Try specifying the labels "
                "parameter".format(len(labels), len(target_names))
            )
    if target_names is None:
        target_names = ["%s" % l for l in labels]

    headers = ["precision", "recall", "f1-score", "support"]
    # compute per-class results without averaging
    p, r, f1, s = precision_recall_fscore_support(
        y_true,
        y_pred,
        average=None,
    )
    rows = zip(target_names, p, r, f1, s)

        average_options = ("micro", "macro", "weighted", "samples")
    else:
        average_options = ("micro", "macro", "weighted")

    if output_dict:
        report_dict = {label[0]: label[1:] for label in rows}
        for label, scores in report_dict.items():
            report_dict[label] = dict(zip(headers, [float(i) for i in scores]))
    else:
        longest_last_line_heading = "weighted avg"
        name_width = max(len(cn) for cn in target_names)
        width = max(name_width, len(longest_last_line_heading), digits)
        head_fmt = "{:>{width}s} " + " {:>9}" * len(headers)
        report = head_fmt.format("", *headers, width=width)
        report += "\n\n"
        row_fmt = "{:>{width}s} " + " {:>9.{digits}f}" * 3 + " {:>9}\n"
        for row in rows:
            report += row_fmt.format(*row, width=width, digits=digits)
        report += "\n"

    # compute all applicable averages
    for average in average_options:
        if average.startswith("micro") and micro_is_accuracy:
            line_heading = "accuracy"
        else:
            line_heading = average + " avg"

        # compute averages with specified averaging method
        avg_p, avg_r, avg_f1, _ = precision_recall_fscore_support(
            y_true,
            y_pred,
            labels=labels,
            average=average,
            zero_division=zero_division,
        )
        avg = [avg_p, avg_r, avg_f1, np.sum(s)]

        if output_dict:
            report_dict[line_heading] = dict(zip(headers, [float(i) for i in avg]))
        else:
            if line_heading == "accuracy":
                row_fmt_accuracy = (
                    "{:>{width}s} "
                    + " {:>9.{digits}}" * 2
                    + " {:>9.{digits}f}"
                    + " {:>9}\n"
                )
                report += row_fmt_accuracy.format(
                    line_heading, "", "", *avg[2:], width=width, digits=digits
                )
            else:
                report += row_fmt.format(line_heading, *avg, width=width, digits=digits)

        if "accuracy" in report_dict.keys():
            report_dict["accuracy"] = report_dict["accuracy"]["precision"]
        return report_dict
    else:
        return report

def hamming_loss(y_true, y_pred, *, sample_weight=None):
    """Compute the average Hamming loss.

    The Hamming loss is the fraction of labels that are incorrectly predicted.

    Read more in the :ref:`User Guide <hamming_loss>`.

    Returns
    -------
    loss : float or int
        Return the average Hamming loss between element of ``y_true`` and
        ``y_pred``.

    See Also
    --------

    Notes
    -----
    In multiclass classification, the Hamming loss corresponds to the Hamming
    distance between ``y_true`` and ``y_pred`` which is equivalent to the
    subset ``zero_one_loss`` function, when `normalize` parameter is set to
    True.

    In multilabel classification, the Hamming loss is different from the
    subset zero-one loss. The zero-one loss considers the entire set of labels
    for a given sample incorrect if it does not entirely match the true set of
    labels. Hamming loss is more forgiving in that it penalizes only the
    individual labels.

    The Hamming loss is upperbounded by the subset zero-one loss, when
    `normalize` parameter is set to True. It is always between 0 and 1,
    lower being better.

    .. [1] Grigorios Tsoumakas, Ioannis Katakis. Multi-Label Classification:
           An Overview. International Journal of Data Warehousing & Mining,
           3(3), 1-13, July-September 2007.

    .. [2] `Wikipedia entry on the Hamming distance
           <https://en.wikipedia.org/wiki/Hamming_distance>`_.

    Examples
    --------
    >>> from sklearn.metrics import hamming_loss
    >>> hamming_loss(y_true, y_pred)
    0.25

    >>> hamming_loss(np.array([[0, 1], [1, 1]]), np.zeros((2, 2)))
    0.75
    """

        weight_average = 1.0
    else:
        sample_weight = xp.asarray(sample_weight, device=device)
        weight_average = _average(sample_weight, xp=xp)

        n_differences = _count_nonzero(
            y_true - y_pred, xp=xp, device=device, sample_weight=sample_weight
        )
        return float(n_differences) / (
            y_true.shape[0] * y_true.shape[1] * weight_average
        )

    elif y_type in ["binary", "multiclass"]:
        return float(_average(y_true != y_pred, weights=sample_weight, normalize=True))
    else:

def log_loss(y_true, y_pred, *, normalize=True, sample_weight=None, labels=None):
    r"""Log loss, aka logistic loss or cross-entropy loss.

    This is the loss function used in (multinomial) logistic regression
    and extensions of it such as neural networks, defined as the negative
    log-likelihood of a logistic model that returns ``y_pred`` probabilities
    for its training data ``y_true``.
    The log loss is only defined for two or more labels.
    For a single sample with true label :math:`y \in \{0,1\}` and
    a probability estimate :math:`p = \operatorname{Pr}(y = 1)`, the log
    loss is:

    .. math::
        L_{\log}(y, p) = -(y \log (p) + (1 - y) \log (1 - p))

    Read more in the :ref:`User Guide <log_loss>`.

    y_pred : array-like of float, shape = (n_samples, n_classes) or (n_samples,)
        predict_proba method. If ``y_pred.shape = (n_samples,)``
        positive class. The labels in ``y_pred`` are assumed to be
        ordered alphabetically, as done by
        :class:`~sklearn.preprocessing.LabelBinarizer`.

        `y_pred` values are clipped to `[eps, 1-eps]` where `eps` is the machine
        precision for `y_pred`'s dtype.

        If true, return the mean loss per sample.
        Otherwise, return the sum of the per-sample losses.

        If not provided, labels will be inferred from y_true. If ``labels``
        is ``None`` and ``y_pred`` has shape (n_samples,) the labels are
        assumed to be binary and are inferred from ``y_true``.

    Returns
    -------
        Log loss, aka logistic loss or cross-entropy loss.

    Notes
    -----
    The logarithm used is the natural logarithm (base-e).

    C.M. Bishop (2006). Pattern Recognition and Machine Learning. Springer,
    p. 209.

    Examples
    --------
    >>> from sklearn.metrics import log_loss
    >>> log_loss(["spam", "ham", "ham", "spam"],
    ...          [[.1, .9], [.9, .1], [.8, .2], [.35, .65]])
    0.21616...
    """
    transformed_labels, y_pred = _validate_multiclass_probabilistic_prediction(
        y_true, y_pred, sample_weight, labels
    )

    # Clipping
    eps = np.finfo(y_pred.dtype).eps
    y_pred = np.clip(y_pred, eps, 1 - eps)

    loss = -xlogy(transformed_labels, y_pred).sum(axis=1)

    return float(_average(loss, weights=sample_weight, normalize=normalize))

def hinge_loss(y_true, pred_decision, *, labels=None, sample_weight=None):
    """Average hinge loss (non-regularized).

    In binary class case, assuming labels in y_true are encoded with +1 and -1,
    when a prediction mistake is made, ``margin = y_true * pred_decision`` is
    always negative (since the signs disagree), implying ``1 - margin`` is
    always greater than 1.  The cumulated hinge loss is therefore an upper
    bound of the number of mistakes made by the classifier.

    In multiclass case, the function expects that either all the labels are
    included in y_true or an optional labels argument is provided which
    contains all the labels. The multilabel margin is calculated according
    to Crammer-Singer's method. As in the binary case, the cumulated hinge loss
    is an upper bound of the number of mistakes made by the classifier.

    Read more in the :ref:`User Guide <hinge_loss>`.

        True target, consisting of integers of two values. The positive label
        must be greater than the negative label.

    pred_decision : array-like of shape (n_samples,) or (n_samples, n_classes)
        Predicted decisions, as output by decision_function (floats).

        Contains all the labels for the problem. Used in multiclass hinge loss.

    Returns
    -------
        Average hinge loss.

    .. [1] `Wikipedia entry on the Hinge loss
           <https://en.wikipedia.org/wiki/Hinge_loss>`_.

    .. [2] Koby Crammer, Yoram Singer. On the Algorithmic
           Implementation of Multiclass Kernel-based Vector
           Machines. Journal of Machine Learning Research 2,
           (2001), 265-292.

    .. [3] `L1 AND L2 Regularization for Multiclass Hinge Loss Models
           by Robert C. Moore, John DeNero
           <https://storage.googleapis.com/pub-tools-public-publication-data/pdf/37362.pdf>`_.

    Examples
    --------
    >>> from sklearn import svm
    >>> from sklearn.metrics import hinge_loss
    >>> X = [[0], [1]]
    >>> y = [-1, 1]
    >>> est = svm.LinearSVC(random_state=0)
    >>> est.fit(X, y)
    LinearSVC(random_state=0)
    >>> pred_decision = est.decision_function([[-2], [3], [0.5]])
    >>> pred_decision
    array([-2.18...,  2.36...,  0.09...])
    >>> hinge_loss([-1, 1, 1], pred_decision)
    0.30...

    >>> X = np.array([[0], [1], [2], [3]])
    >>> Y = np.array([0, 1, 2, 3])
    >>> labels = np.array([0, 1, 2, 3])
    >>> est = svm.LinearSVC()
    >>> est.fit(X, Y)
    >>> pred_decision = est.decision_function([[-1], [2], [3]])
    >>> y_true = [0, 2, 3]
    >>> hinge_loss(y_true, pred_decision, labels=labels)
    0.56...
    """
    check_consistent_length(y_true, pred_decision, sample_weight)
    pred_decision = check_array(pred_decision, ensure_2d=False)
    y_true_unique = np.unique(labels if labels is not None else y_true)

    if y_true_unique.size > 2:
        if pred_decision.ndim <= 1:
                "The shape of pred_decision cannot be 1d array"
                "with a multiclass target. pred_decision shape "
                "must be (n_samples, n_classes), that is "
                f"({y_true.shape[0]}, {y_true_unique.size})."
                f" Got: {pred_decision.shape}"
            )

        # pred_decision.ndim > 1 is true
        if y_true_unique.size != pred_decision.shape[1]:
            if labels is None:
                    "Please include all labels in y_true "
                    "or pass labels as third argument"
                )
            else:
                    "The shape of pred_decision is not "
                    "consistent with the number of classes. "
                    "With a multiclass target, pred_decision "
                    "shape must be "
                    "(n_samples, n_classes), that is "
                    f"({y_true.shape[0]}, {y_true_unique.size}). "
                    f"Got: {pred_decision.shape}"
                )
            labels = y_true_unique
        mask = np.ones_like(pred_decision, dtype=bool)
        mask[np.arange(y_true.shape[0]), y_true] = False
        margin = pred_decision[~mask]
        margin -= np.max(pred_decision[mask].reshape(y_true.shape[0], -1), axis=1)

    else:
        # Handles binary class case
        # this code assumes that positive and negative labels
        # are encoded as +1 and -1 respectively
        pred_decision = column_or_1d(pred_decision)
        pred_decision = np.ravel(pred_decision)

        lbin = LabelBinarizer(neg_label=-1)
        y_true = lbin.fit_transform(y_true)[:, 0]

        try:
            margin = y_true * pred_decision
            raise TypeError("pred_decision should be an array of floats.")

    losses = 1 - margin
    # The hinge_loss doesn't penalize good enough predictions.
    np.clip(losses, 0, None, out=losses)
    return float(np.average(losses, weights=sample_weight))

def _validate_binary_probabilistic_prediction(y_true, y_prob, sample_weight, pos_label):
    r"""Convert y_true and y_prob in binary classification to shape (n_samples, 2)

        Label of the positive class. If None, `pos_label` will be inferred
        in the following manner:

        * if `y_true` in {-1, 1} or {0, 1}, `pos_label` defaults to 1;
        * else if `y_true` contains string, an error will be raised and
          `pos_label` should be explicitly specified;
        * otherwise, `pos_label` defaults to the greater label,
          i.e. `np.unique(y_true)[-1]`.

    Returns
    -------
    transformed_labels : array of shape (n_samples, 2)

    y_prob : array of shape (n_samples, 2)
    """
    # sanity checks on y_true and y_prob

    assert_all_finite(y_prob)

            f"The type of the target inferred from y_true is {y_type} but should be "
            "binary according to the shape of y_prob."
        )

        raise ValueError(f"y_prob contains values less than 0: {y_prob.min()}")

    # check that pos_label is consistent with y_true
    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
            pos_label = classes[-1]
        else:
            raise

    # convert (n_samples,) to (n_samples, 2) shape
    y_true = np.array(y_true == pos_label, int)
    transformed_labels = np.column_stack((1 - y_true, y_true))
    y_prob = np.column_stack((1 - y_prob, y_prob))

def brier_score_loss(
    y_true,
    y_proba,
    *,
    scale_by_half="auto",
):
    r"""Compute the Brier score loss.

    The smaller the Brier score loss, the better, hence the naming with "loss".
    The Brier score measures the mean squared difference between the predicted
    probability and the actual outcome. The Brier score is a strictly proper scoring
    rule.

    Read more in the :ref:`User Guide <brier_score_loss>`.

    y_proba : array-like of shape (n_samples,) or (n_samples, n_classes)
        Predicted probabilities. If `y_proba.shape = (n_samples,)`
        positive class. If `y_proba.shape = (n_samples, n_classes)`
        the columns in `y_proba` are assumed to correspond to the
        labels in alphabetical order, as done by

        Label of the positive class when `y_proba.shape = (n_samples,)`.
        If not provided, `pos_label` will be inferred in the
        following manner:

        Class labels when `y_proba.shape = (n_samples, n_classes)`.
        If not provided, labels will be inferred from `y_true`.

    scale_by_half : bool or "auto", default="auto"
        When True, scale the Brier score by 1/2 to lie in the [0, 1] range instead
        of the [0, 2] range. The default "auto" option implements the rescaling to
        [0, 1] only for binary classification (as customary) but keeps the
        original [0, 2] range for multiclasss classification.

    Returns
    -------
        Brier score loss.

    Notes
    -----

    For :math:`N` observations labeled from :math:`C` possible classes, the Brier
    score is defined as:

    .. math::
        \frac{1}{N}\sum_{i=1}^{N}\sum_{c=1}^{C}(y_{ic} - \hat{p}_{ic})^{2}

    where :math:`y_{ic}` is 1 if observation `i` belongs to class `c`,
    otherwise 0 and :math:`\hat{p}_{ic}` is the predicted probability for
    observation `i` to belong to class `c`.
    The Brier score then ranges between :math:`[0, 2]`.

    In binary classification tasks the Brier score is usually divided by
    two and then ranges between :math:`[0, 1]`. It can be alternatively
    written as:

    .. math::
        \frac{1}{N}\sum_{i=1}^{N}(y_{i} - \hat{p}_{i})^{2}

    where :math:`y_{i}` is the binary target and :math:`\hat{p}_{i}`
    is the predicted probability of the positive class.

    .. [1] `Wikipedia entry for the Brier score
            <https://en.wikipedia.org/wiki/Brier_score>`_.

    Examples
    --------
    >>> from sklearn.metrics import brier_score_loss
    >>> y_true = np.array([0, 1, 1, 0])
    >>> y_true_categorical = np.array(["spam", "ham", "ham", "spam"])
    >>> y_prob = np.array([0.1, 0.9, 0.8, 0.3])
    >>> brier_score_loss(y_true, y_prob)
    0.037...
    >>> brier_score_loss(y_true, 1-y_prob, pos_label=0)
    0.037...
    >>> brier_score_loss(y_true_categorical, y_prob, pos_label="ham")
    0.037...
    >>> brier_score_loss(y_true, np.array(y_prob) > 0.5)
    0.0
    >>> brier_score_loss(y_true, y_prob, scale_by_half=False)
    0.074...
    >>> brier_score_loss(
    ...    ["eggs", "ham", "spam"],
    ...    [[0.8, 0.1, 0.1], [0.2, 0.7, 0.1], [0.2, 0.2, 0.6]],
    ...    labels=["eggs", "ham", "spam"]
    ... )
    0.146...
    """
    y_proba = check_array(
        y_proba, ensure_2d=False, dtype=[np.float64, np.float32, np.float16]
    )

    if y_proba.ndim == 1 or y_proba.shape[1] == 1:
        transformed_labels, y_proba = _validate_binary_probabilistic_prediction(
            y_true, y_proba, sample_weight, pos_label
        )
    else:
        transformed_labels, y_proba = _validate_multiclass_probabilistic_prediction(
            y_true, y_proba, sample_weight, labels
        )

    brier_score = np.average(
        np.sum((transformed_labels - y_proba) ** 2, axis=1), weights=sample_weight
    )

    if scale_by_half == "auto":
        scale_by_half = y_proba.ndim == 1 or y_proba.shape[1] < 3
    if scale_by_half:
        brier_score *= 0.5

    return float(brier_score)

def d2_log_loss_score(y_true, y_pred, *, sample_weight=None, labels=None):
    """
    :math:`D^2` score function, fraction of log loss explained.

    Best possible score is 1.0 and it can be negative (because the model can be
    arbitrarily worse). A model that always predicts the per-class proportions
    of `y_true`, disregarding the input features, gets a D^2 score of 0.0.

    Read more in the :ref:`User Guide <d2_score_classification>`.

    .. versionadded:: 1.5

        The actuals labels for the n_samples samples.

    y_pred : array-like of shape (n_samples, n_classes) or (n_samples,)

    Returns
    -------
    d2 : float or ndarray of floats
        The D^2 score.

    Notes
    -----
    This is not a symmetric function.

    Like R^2, D^2 score may be negative (it need not actually be the square of
    a quantity D).

    This metric is not well-defined for a single sample and will return a NaN
    value if n_samples is less than two.
    """
    y_pred = check_array(y_pred, ensure_2d=False, dtype="numeric")
    check_consistent_length(y_pred, y_true, sample_weight)
    if _num_samples(y_pred) < 2:
        msg = "D^2 score is not well-defined with less than two samples."
        warnings.warn(msg, UndefinedMetricWarning)
        return float("nan")

    # log loss of the fitted model
    numerator = log_loss(
        y_pred=y_pred,
        normalize=False,
    )

    # Proportion of labels in the dataset
    weights = _check_sample_weight(sample_weight, y_true)

    # If labels is passed, augment y_true to ensure that all labels are represented
    # Use 0 weight for the new samples to not affect the counts
    y_true_, weights_ = (
        (
            np.concatenate([y_true, labels]),
            np.concatenate([weights, np.zeros_like(weights, shape=len(labels))]),
        )
        if labels is not None
        else (y_true, weights)
    )

    _, y_value_indices = np.unique(y_true_, return_inverse=True)
    counts = np.bincount(y_value_indices, weights=weights_)
    y_prob = counts / weights.sum()
    y_pred_null = np.tile(y_prob, (len(y_true), 1))

    # log loss of the null model
    denominator = log_loss(
        y_pred=y_pred_null,
    )

    return float(1 - (numerator / denominator))

def _check_reg_targets(y_true, y_pred, multioutput, dtype="numeric", xp=None):
    """Check that y_true and y_pred belong to the same regression task.

    To reduce redundancy when calling `_find_matching_floating_dtype`,
    please use `_check_reg_targets_with_floating_dtype` instead.

    y_true : array-like of shape (n_samples,) or (n_samples, n_outputs)

    y_pred : array-like of shape (n_samples,) or (n_samples, n_outputs)
        Estimated target values.

    multioutput : array-like or string in ['raw_values', uniform_average',
        'variance_weighted'] or None
        None is accepted due to backward compatibility of r2_score().

    dtype : str or list, default="numeric"
        the dtype argument passed to check_array.

    xp : module, default=None
        Precomputed array namespace module. When passed, typically from a caller
        that has already performed inspection of its own inputs, skips array
        namespace inspection.

    Returns
    -------
    type_true : one of {'continuous', continuous-multioutput'}
        'utils.multiclass.type_of_target'.

    y_true : array-like of shape (n_samples, n_outputs)

    y_pred : array-like of shape (n_samples, n_outputs)

    multioutput : array-like of shape (n_outputs) or string in ['raw_values',
        uniform_average', 'variance_weighted'] or None
        Custom output weights if ``multioutput`` is array-like or
        just the corresponding argument if ``multioutput`` is a
        correct keyword.
    """
    xp, _ = get_namespace(y_true, y_pred, multioutput, xp=xp)

    y_true = check_array(y_true, ensure_2d=False, dtype=dtype)
    y_pred = check_array(y_pred, ensure_2d=False, dtype=dtype)

        y_true = xp.reshape(y_true, (-1, 1))

    if y_pred.ndim == 1:
        y_pred = xp.reshape(y_pred, (-1, 1))

    if y_true.shape[1] != y_pred.shape[1]:
            "y_true and y_pred have different number of output ({0}!={1})".format(
                y_true.shape[1], y_pred.shape[1]
            )
        )

    n_outputs = y_true.shape[1]
    allowed_multioutput_str = ("raw_values", "uniform_average", "variance_weighted")
    if isinstance(multioutput, str):
        if multioutput not in allowed_multioutput_str:
                "Allowed 'multioutput' string values are {}. "
                "You provided multioutput={!r}".format(
                    allowed_multioutput_str, multioutput
                )
            )
    elif multioutput is not None:
        multioutput = check_array(multioutput, ensure_2d=False)
        if n_outputs == 1:
            raise ValueError("Custom weights are useful only in multi-output cases.")
        elif n_outputs != multioutput.shape[0]:
                "There must be equally many custom weights "
                f"({multioutput.shape[0]}) as outputs ({n_outputs})."
            )
    y_type = "continuous" if n_outputs == 1 else "continuous-multioutput"

    return y_type, y_true, y_pred, multioutput

def _check_reg_targets_with_floating_dtype(
    y_true, y_pred, sample_weight, multioutput, xp=None
):
    """Ensures that y_true, y_pred, and sample_weight correspond to the same
    regression task.

    Extends `_check_reg_targets` by automatically selecting a suitable floating-point
    data type for inputs using `_find_matching_floating_dtype`.

    Use this private method only when converting inputs to array API-compatibles.

    sample_weight : array-like of shape (n_samples,)

    multioutput : array-like or string in ['raw_values', 'uniform_average', \

    Returns
    -------
    type_true : one of {'continuous', 'continuous-multioutput'}

    multioutput : array-like of shape (n_outputs) or string in ['raw_values', \
        'uniform_average', 'variance_weighted'] or None
    """
    dtype_name = _find_matching_floating_dtype(y_true, y_pred, sample_weight, xp=xp)

    y_type, y_true, y_pred, multioutput = _check_reg_targets(
        y_true, y_pred, multioutput, dtype=dtype_name, xp=xp
    )

    # _check_reg_targets does not accept sample_weight as input.
    # Convert sample_weight's data type separately to match dtype_name.
        sample_weight = xp.asarray(sample_weight, dtype=dtype_name)

    return y_type, y_true, y_pred, sample_weight, multioutput

def mean_absolute_error(
    y_true, y_pred, *, sample_weight=None, multioutput="uniform_average"
):
    """Mean absolute error regression loss.

    The mean absolute error is a non-negative floating point value, where best value
    is 0.0. Read more in the :ref:`User Guide <mean_absolute_error>`.

    multioutput : {'raw_values', 'uniform_average'}  or array-like of shape \
            (n_outputs,), default='uniform_average'
        Defines aggregating of multiple output values.
        Array-like value defines weights used to average errors.

        'raw_values' :
            Returns a full set of errors in case of multioutput input.

        'uniform_average' :
            Errors of all outputs are averaged with uniform weight.

    Returns
    -------
    loss : float or array of floats
        If multioutput is 'raw_values', then mean absolute error is returned
        for each output separately.
        If multioutput is 'uniform_average' or an ndarray of weights, then the
        weighted average of all output errors is returned.

        MAE output is non-negative floating point. The best value is 0.0.

    Examples
    --------
    >>> from sklearn.metrics import mean_absolute_error
    >>> y_true = [3, -0.5, 2, 7]
    >>> y_pred = [2.5, 0.0, 2, 8]
    >>> mean_absolute_error(y_true, y_pred)
    0.5
    >>> y_true = [[0.5, 1], [-1, 1], [7, -6]]
    >>> y_pred = [[0, 2], [-1, 2], [8, -5]]
    0.75
    >>> mean_absolute_error(y_true, y_pred, multioutput='raw_values')
    array([0.5, 1. ])
    >>> mean_absolute_error(y_true, y_pred, multioutput=[0.3, 0.7])
    0.85...
    """
    xp, _ = get_namespace(y_true, y_pred, sample_weight, multioutput)

    _, y_true, y_pred, sample_weight, multioutput = (
        _check_reg_targets_with_floating_dtype(
            y_true, y_pred, sample_weight, multioutput, xp=xp
        )
    )

    output_errors = _average(
        xp.abs(y_pred - y_true), weights=sample_weight, axis=0, xp=xp
    )
        if multioutput == "raw_values":
            return output_errors
        elif multioutput == "uniform_average":
            # pass None as weights to _average: uniform mean
            multioutput = None

    # Average across the outputs (if needed).
    # The second call to `_average` should always return
    # a scalar array that we convert to a Python float to
    # consistently return the same eager evaluated value.
    # Therefore, `axis=None`.
    mean_absolute_error = _average(output_errors, weights=multioutput)

    return float(mean_absolute_error)

def mean_pinball_loss(
    y_true, y_pred, *, sample_weight=None, alpha=0.5, multioutput="uniform_average"
):
    """Pinball loss for quantile regression.

    Read more in the :ref:`User Guide <pinball_loss>`.

    alpha : float, slope of the pinball loss, default=0.5,
        This loss is equivalent to :ref:`mean_absolute_error` when `alpha=0.5`,
        `alpha=0.95` is minimized by estimators of the 95th percentile.

    Returns
    -------
    loss : float or ndarray of floats

        The pinball loss output is a non-negative floating point. The best
        value is 0.0.

    Examples
    --------
    >>> from sklearn.metrics import mean_pinball_loss
    >>> y_true = [1, 2, 3]
    >>> mean_pinball_loss(y_true, [0, 2, 3], alpha=0.1)
    0.03...
    >>> mean_pinball_loss(y_true, [1, 2, 4], alpha=0.1)
    0.3...
    >>> mean_pinball_loss(y_true, [0, 2, 3], alpha=0.9)
    0.3...
    >>> mean_pinball_loss(y_true, [1, 2, 4], alpha=0.9)
    0.03...
    >>> mean_pinball_loss(y_true, y_true, alpha=0.1)
    0.0
    >>> mean_pinball_loss(y_true, y_true, alpha=0.9)
    0.0
    """

        )
    )

    diff = y_true - y_pred
    sign = xp.astype(diff >= 0, diff.dtype)
    loss = alpha * sign * diff - (1 - alpha) * (1 - sign) * diff
    output_errors = _average(loss, weights=sample_weight, axis=0)

    if isinstance(multioutput, str) and multioutput == "raw_values":
        return output_errors

    if isinstance(multioutput, str) and multioutput == "uniform_average":
        # pass None as weights to _average: uniform mean
        multioutput = None

    return float(_average(output_errors, weights=multioutput))

def mean_absolute_percentage_error(
):
    """Mean absolute percentage error (MAPE) regression loss.

    Note that we are not using the common "percentage" definition: the percentage
    in the range [0, 100] is converted to a relative value in the range [0, 1]
    by dividing by 100. Thus, an error of 200% corresponds to a relative error of 2.

    Read more in the :ref:`User Guide <mean_absolute_percentage_error>`.

    multioutput : {'raw_values', 'uniform_average'} or array-like
        If input is list then the shape must be (n_outputs,).

    Returns
    -------
        If multioutput is 'raw_values', then mean absolute percentage error
        is returned for each output separately.

        MAPE output is non-negative floating point. The best value is 0.0.
        But note that bad predictions can lead to arbitrarily large
        MAPE values, especially if some `y_true` values are very close to zero.
        Note that we return a large value instead of `inf` when `y_true` is zero.

    Examples
    --------
    >>> from sklearn.metrics import mean_absolute_percentage_error
    >>> mean_absolute_percentage_error(y_true, y_pred)
    0.3273...
    0.5515...
    >>> mean_absolute_percentage_error(y_true, y_pred, multioutput=[0.3, 0.7])
    0.6198...
    >>> # the value when some element of the y_true is zero is arbitrarily high because
    >>> # of the division by epsilon
    >>> y_true = [1., 0., 2.4, 7.]
    >>> y_pred = [1.2, 0.1, 2.4, 8.]
    112589990684262.48
    """
    xp, _, device_ = get_namespace_and_device(
        y_true, y_pred, sample_weight, multioutput
    )
        )
    )
    epsilon = xp.asarray(xp.finfo(xp.float64).eps, dtype=y_true.dtype, device=device_)
    y_true_abs = xp.abs(y_true)
    mape = xp.abs(y_pred - y_true) / xp.maximum(y_true_abs, epsilon)
    output_errors = _average(mape, weights=sample_weight, axis=0)

    mean_absolute_percentage_error = _average(output_errors, weights=multioutput)

    return float(mean_absolute_percentage_error)

def mean_squared_error(
    y_true,
    y_pred,
    *,
    multioutput="uniform_average",
):
    """Mean squared error regression loss.

    Read more in the :ref:`User Guide <mean_squared_error>`.

    multioutput : {'raw_values', 'uniform_average'} or array-like of shape \

    Returns
    -------
        A non-negative floating point value (the best value is 0.0), or an
        array of floating point values, one for each individual target.

    Examples
    --------
    >>> from sklearn.metrics import mean_squared_error
    >>> mean_squared_error(y_true, y_pred)
    0.375
    >>> y_true = [[0.5, 1],[-1, 1],[7, -6]]
    >>> y_pred = [[0, 2],[-1, 2],[8, -5]]
    0.708...
    >>> mean_squared_error(y_true, y_pred, multioutput='raw_values')
    array([0.41666667, 1.        ])
    >>> mean_squared_error(y_true, y_pred, multioutput=[0.3, 0.7])
    0.825...
    """
        )
    )
    output_errors = _average((y_true - y_pred) ** 2, axis=0, weights=sample_weight)

    mean_squared_error = _average(output_errors, weights=multioutput)

    return float(mean_squared_error)

def root_mean_squared_error(
):
    """Root mean squared error regression loss.

    .. versionadded:: 1.4

    Returns
    -------

    Examples
    --------
    >>> from sklearn.metrics import root_mean_squared_error
    >>> root_mean_squared_error(y_true, y_pred)
    0.612...
    0.822...
    """

    output_errors = xp.sqrt(
        mean_squared_error(
            y_true, y_pred, sample_weight=sample_weight, multioutput="raw_values"
        )
    )

    root_mean_squared_error = _average(output_errors, weights=multioutput)

    return float(root_mean_squared_error)

def mean_squared_log_error(
    y_true,
    y_pred,
    *,
):
    """Mean squared logarithmic error regression loss.

    Read more in the :ref:`User Guide <mean_squared_log_error>`.

            Returns a full set of errors when the input is of multioutput
            format.

    Returns
    -------

    Examples
    --------
    >>> from sklearn.metrics import mean_squared_log_error
    >>> y_true = [3, 5, 2.5, 7]
    >>> y_pred = [2.5, 5, 4, 8]
    >>> mean_squared_log_error(y_true, y_pred)
    0.039...
    >>> y_true = [[0.5, 1], [1, 2], [7, 6]]
    >>> y_pred = [[0.5, 2], [1, 2.5], [8, 8]]
    0.044...
    >>> mean_squared_log_error(y_true, y_pred, multioutput='raw_values')
    array([0.00462428, 0.08377444])
    >>> mean_squared_log_error(y_true, y_pred, multioutput=[0.3, 0.7])
    0.060...
    """

    _, y_true, y_pred, _, _ = _check_reg_targets_with_floating_dtype(
        y_true, y_pred, sample_weight, multioutput, xp=xp
    )

    if xp.any(y_true <= -1) or xp.any(y_pred <= -1):
            "Mean Squared Logarithmic Error cannot be used when "
            "targets contain values less than or equal to -1."
        )

    return mean_squared_error(
        xp.log1p(y_true),
        xp.log1p(y_pred),
        multioutput=multioutput,
    )

def root_mean_squared_log_error(
):
    """Root mean squared logarithmic error regression loss.

            format.

    Returns
    -------

    Examples
    --------
    >>> from sklearn.metrics import root_mean_squared_log_error
    >>> root_mean_squared_log_error(y_true, y_pred)
    0.199...
    """

    )

            "Root Mean Squared Logarithmic Error cannot be used when "
        )

    return root_mean_squared_error(
    )

def median_absolute_error(
    y_true, y_pred, *, multioutput="uniform_average", sample_weight=None
):
    """Median absolute error regression loss.

    Median absolute error output is non-negative floating point. The best value
    is 0.0. Read more in the :ref:`User Guide <median_absolute_error>`.

        Defines aggregating of multiple output values. Array-like value defines
        weights used to average errors.

    Returns
    -------

    Examples
    --------
    >>> from sklearn.metrics import median_absolute_error
    >>> median_absolute_error(y_true, y_pred)
    0.5
    0.75
    >>> median_absolute_error(y_true, y_pred, multioutput='raw_values')
    >>> median_absolute_error(y_true, y_pred, multioutput=[0.3, 0.7])
    0.85
    """
        y_true, y_pred, multioutput
    )
        output_errors = np.median(np.abs(y_pred - y_true), axis=0)
    else:
        sample_weight = _check_sample_weight(sample_weight, y_pred)
        output_errors = _weighted_percentile(
            np.abs(y_pred - y_true), sample_weight=sample_weight
        )
            # pass None as weights to np.average: uniform mean

    return float(np.average(output_errors, weights=multioutput))

def _assemble_r2_explained_variance(
    numerator, denominator, n_outputs, multioutput, force_finite, xp, device
):
    """Common part used by explained variance score and :math:`R^2` score."""
    dtype = numerator.dtype

    nonzero_denominator = denominator != 0

    if not force_finite:
        # Standard formula, that may lead to NaN or -Inf
        output_scores = 1 - (numerator / denominator)
    else:
        nonzero_numerator = numerator != 0
        # Default = Zero Numerator = perfect predictions. Set to 1.0
        # (note: even if denominator is zero, thus avoiding NaN scores)
        output_scores = xp.ones([n_outputs], device=device, dtype=dtype)
        # Non-zero Numerator and Non-zero Denominator: use the formula
        valid_score = nonzero_denominator & nonzero_numerator

        output_scores[valid_score] = 1 - (
            numerator[valid_score] / denominator[valid_score]
        )

        # Non-zero Numerator and Zero Denominator:
        # arbitrary set to 0.0 to avoid -inf scores
        output_scores[nonzero_numerator & ~nonzero_denominator] = 0.0

            # return scores individually
            return output_scores
            avg_weights = None
        elif multioutput == "variance_weighted":
            avg_weights = denominator
            if not xp.any(nonzero_denominator):
                # All weights are zero, _average would raise a ZeroDiv error.
                # This only happens when all y are constant (or 1-element long)
                # Since weights are all equal, fall back to uniform weights.
                avg_weights = None
    else:
        avg_weights = multioutput

    result = _average(output_scores, weights=avg_weights)
    if size(result) == 1:
        return float(result)

def explained_variance_score(
    y_true,
    y_pred,
    *,
    force_finite=True,
):
    """Explained variance regression score function.

    Best possible score is 1.0, lower values are worse.

    In the particular case when ``y_true`` is constant, the explained variance
    score is not finite: it is either ``NaN`` (perfect predictions) or
    ``-Inf`` (imperfect predictions). To prevent such non-finite numbers to
    pollute higher-level experiments such as a grid search cross-validation,
    by default these cases are replaced with 1.0 (perfect predictions) or 0.0
    (imperfect predictions) respectively. If ``force_finite``
    is set to ``False``, this score falls back on the original :math:`R^2`
    definition.

    .. note::
       The Explained Variance score is similar to the
       :func:`R^2 score <r2_score>`, with the notable difference that it
       does not account for systematic offsets in the prediction. Most often
       the :func:`R^2 score <r2_score>` should be preferred.

    Read more in the :ref:`User Guide <explained_variance_score>`.

    multioutput : {'raw_values', 'uniform_average', 'variance_weighted'} or \
            array-like of shape (n_outputs,), default='uniform_average'
        Defines aggregating of multiple output scores.
        Array-like value defines weights used to average scores.

            Returns a full set of scores in case of multioutput input.

            Scores of all outputs are averaged with uniform weight.

        'variance_weighted' :
            Scores of all outputs are averaged, weighted by the variances
            of each individual output.

    force_finite : bool, default=True
        Flag indicating if ``NaN`` and ``-Inf`` scores resulting from constant
        data should be replaced with real numbers (``1.0`` if prediction is
        perfect, ``0.0`` otherwise). Default is ``True``, a convenient setting
        for hyperparameters' search procedures (e.g. grid search
        cross-validation).

    Returns
    -------
    score : float or ndarray of floats
        The explained variance or ndarray if 'multioutput' is 'raw_values'.

    See Also
    --------
    r2_score :
        Similar metric, but accounting for systematic offsets in

    Notes
    -----

    Examples
    --------
    >>> from sklearn.metrics import explained_variance_score
    >>> explained_variance_score(y_true, y_pred)
    0.957...
    >>> explained_variance_score(y_true, y_pred, multioutput='uniform_average')
    0.983...
    >>> y_true = [-2, -2, -2]
    >>> y_pred = [-2, -2, -2]
    1.0
    >>> explained_variance_score(y_true, y_pred, force_finite=False)
    nan
    >>> y_pred = [-2, -2, -2 + 1e-8]
    0.0
    -inf
    """
    xp, _, device = get_namespace_and_device(y_true, y_pred, sample_weight, multioutput)

        )
    )

    y_diff_avg = _average(y_true - y_pred, weights=sample_weight, axis=0)
    numerator = _average(
        (y_true - y_pred - y_diff_avg) ** 2, weights=sample_weight, axis=0
    )

    y_true_avg = _average(y_true, weights=sample_weight, axis=0)
    denominator = _average((y_true - y_true_avg) ** 2, weights=sample_weight, axis=0)

    return _assemble_r2_explained_variance(
        numerator=numerator,
        denominator=denominator,
        n_outputs=y_true.shape[1],
        force_finite=force_finite,
        xp=xp,
        device=device,
    )

def r2_score(
    y_true,
    y_pred,
    *,
):
    """:math:`R^2` (coefficient of determination) regression score function.

    Best possible score is 1.0 and it can be negative (because the
    model can be arbitrarily worse). In the general case when the true y is
    non-constant, a constant model that always predicts the average y
    disregarding the input features would get a :math:`R^2` score of 0.0.

    In the particular case when ``y_true`` is constant, the :math:`R^2` score
    is not finite: it is either ``NaN`` (perfect predictions) or ``-Inf``
    (imperfect predictions). To prevent such non-finite numbers to pollute
    higher-level experiments such as a grid search cross-validation, by default
    these cases are replaced with 1.0 (perfect predictions) or 0.0 (imperfect
    predictions) respectively. You can set ``force_finite`` to ``False`` to
    prevent this fix from happening.

    Note: when the prediction residuals have zero mean, the :math:`R^2` score
    is identical to the
    :func:`Explained Variance score <explained_variance_score>`.

    Read more in the :ref:`User Guide <r2_score>`.

    multioutput : {'raw_values', 'uniform_average', 'variance_weighted'}, \
            array-like of shape (n_outputs,) or None, default='uniform_average'

        Default is "uniform_average".

            Default value of multioutput is 'uniform_average'.

    Returns
    -------
    z : float or ndarray of floats
        The :math:`R^2` score or ndarray of scores if 'multioutput' is
        'raw_values'.

    Notes
    -----

    Unlike most other scores, :math:`R^2` score may be negative (it need not
    actually be the square of a quantity R).

    This metric is not well-defined for single samples and will return a NaN

    .. [1] `Wikipedia entry on the Coefficient of determination
            <https://en.wikipedia.org/wiki/Coefficient_of_determination>`_

    Examples
    --------
    >>> from sklearn.metrics import r2_score
    >>> r2_score(y_true, y_pred)
    0.948...
    >>> r2_score(y_true, y_pred,
    ...          multioutput='variance_weighted')
    0.938...
    >>> y_pred = [1, 2, 3]
    1.0
    >>> y_pred = [2, 2, 2]
    0.0
    >>> y_pred = [3, 2, 1]
    -3.0
    1.0
    >>> r2_score(y_true, y_pred, force_finite=False)
    nan
    0.0
    -inf
    """
    )

        )
    )

        msg = "R^2 score is not well-defined with less than two samples."

        weight = sample_weight[:, None]
    else:

    numerator = xp.sum(weight * (y_true - y_pred) ** 2, axis=0)
    denominator = xp.sum(
        weight * (y_true - _average(y_true, axis=0, weights=sample_weight, xp=xp)) ** 2,
        axis=0,
    )

        xp=xp,
        device=device_,
    )

def max_error(y_true, y_pred):
    """
    The max_error metric calculates the maximum residual error.

    Read more in the :ref:`User Guide <max_error>`.

    Returns
    -------
    max_error : float
        A positive floating point value (the best value is 0.0).

    Examples
    --------
    >>> from sklearn.metrics import max_error
    >>> y_true = [3, 2, 7, 1]
    >>> y_pred = [4, 2, 7, 1]
    >>> max_error(y_true, y_pred)
    1.0
    """
    y_type, y_true, y_pred, _ = _check_reg_targets(y_true, y_pred, None, xp=xp)
    if y_type == "continuous-multioutput":
        raise ValueError("Multioutput not supported in max_error")
    return float(xp.max(xp.abs(y_true - y_pred)))

def _mean_tweedie_deviance(y_true, y_pred, sample_weight, power):
    """Mean Tweedie deviance regression loss."""
    p = power
    if p < 0:
        # 'Extreme stable', y any real number, y_pred > 0
        dev = 2 * (
            xp.pow(
                xp.where(y_true > 0, y_true, 0.0),
                2 - p,
            )
            / ((1 - p) * (2 - p))
            - y_true * xp.pow(y_pred, 1 - p) / (1 - p)
            + xp.pow(y_pred, 2 - p) / (2 - p)
        )
    elif p == 0:
        # Normal distribution, y and y_pred any real number
        dev = (y_true - y_pred) ** 2
    elif p == 1:
        # Poisson distribution
        dev = 2 * (xlogy(y_true, y_true / y_pred) - y_true + y_pred)
    elif p == 2:
        # Gamma distribution
        dev = 2 * (xp.log(y_pred / y_true) + y_true / y_pred - 1)
    else:
        dev = 2 * (
            xp.pow(y_true, 2 - p) / ((1 - p) * (2 - p))
        )
    return float(_average(dev, weights=sample_weight))

def mean_tweedie_deviance(y_true, y_pred, *, sample_weight=None, power=0):
    """Mean Tweedie deviance regression loss.

    Read more in the :ref:`User Guide <mean_tweedie_deviance>`.

    power : float, default=0
        Tweedie power parameter. Either power <= 0 or power >= 1.

        The higher `p` the less weight is given to extreme
        deviations between true and predicted targets.

        - power < 0: Extreme stable distribution. Requires: y_pred > 0.
        - power = 0 : Normal distribution, output corresponds to
          mean_squared_error. y_true and y_pred can be any real numbers.
        - power = 1 : Poisson distribution. Requires: y_true >= 0 and
          y_pred > 0.
        - 1 < p < 2 : Compound Poisson distribution. Requires: y_true >= 0
          and y_pred > 0.
        - power = 2 : Gamma distribution. Requires: y_true > 0 and y_pred > 0.
        - power = 3 : Inverse Gaussian distribution. Requires: y_true > 0
        - otherwise : Positive stable distribution. Requires: y_true > 0

    Returns
    -------
        A non-negative floating point value (the best value is 0.0).

    Examples
    --------
    >>> from sklearn.metrics import mean_tweedie_deviance
    >>> y_true = [2, 0, 1, 4]
    >>> y_pred = [0.5, 0.5, 2., 2.]
    >>> mean_tweedie_deviance(y_true, y_pred, power=1)
    1.4260...
    """
    y_type, y_true, y_pred, sample_weight, _ = _check_reg_targets_with_floating_dtype(
        y_true, y_pred, sample_weight, multioutput=None, xp=xp
    )
        raise ValueError("Multioutput not supported in mean_tweedie_deviance")

        sample_weight = sample_weight[:, np.newaxis]

    message = f"Mean Tweedie deviance error with power={power} can only be used on "
    if power < 0:
        if xp.any(y_pred <= 0):
            raise ValueError(message + "strictly positive y_pred.")
    elif power == 0:
        # Normal, y and y_pred can be any real number
        pass
    elif 1 <= power < 2:
        # Poisson and compound Poisson distribution, y >= 0, y_pred > 0
        if xp.any(y_true < 0) or xp.any(y_pred <= 0):
            raise ValueError(message + "non-negative y and strictly positive y_pred.")
    elif power >= 2:
        # Gamma and Extreme stable distribution, y and y_pred > 0
        if xp.any(y_true <= 0) or xp.any(y_pred <= 0):
            raise ValueError(message + "strictly positive y and y_pred.")
    else:  # pragma: nocover
        # Unreachable statement
        raise ValueError

    return _mean_tweedie_deviance(
        y_true, y_pred, sample_weight=sample_weight, power=power
    )

def mean_poisson_deviance(y_true, y_pred, *, sample_weight=None):
    """Mean Poisson deviance regression loss.

    Poisson deviance is equivalent to the Tweedie deviance with
    the power parameter `power=1`.

        Ground truth (correct) target values. Requires y_true >= 0.

        Estimated target values. Requires y_pred > 0.

    Returns
    -------

    Examples
    --------
    >>> from sklearn.metrics import mean_poisson_deviance
    >>> mean_poisson_deviance(y_true, y_pred)
    1.4260...
    """
    return mean_tweedie_deviance(y_true, y_pred, sample_weight=sample_weight, power=1)

def mean_gamma_deviance(y_true, y_pred, *, sample_weight=None):
    """Mean Gamma deviance regression loss.

    Gamma deviance is equivalent to the Tweedie deviance with
    the power parameter `power=2`. It is invariant to scaling of
    the target variable, and measures relative errors.

        Ground truth (correct) target values. Requires y_true > 0.

    Returns
    -------

    Examples
    --------
    >>> from sklearn.metrics import mean_gamma_deviance
    >>> y_true = [2, 0.5, 1, 4]
    >>> mean_gamma_deviance(y_true, y_pred)
    1.0568...
    """
    return mean_tweedie_deviance(y_true, y_pred, sample_weight=sample_weight, power=2)

def d2_tweedie_score(y_true, y_pred, *, sample_weight=None, power=0):
    """
    :math:`D^2` regression score function, fraction of Tweedie deviance explained.

    arbitrarily worse). A model that always uses the empirical mean of `y_true` as
    constant prediction, disregarding the input features, gets a D^2 score of 0.0.

    Read more in the :ref:`User Guide <d2_score>`.

        - power = 0 : Normal distribution, output corresponds to r2_score.
          y_true and y_pred can be any real numbers.
          y_pred > 0.

    Returns
    -------

    Notes
    -----

    .. [1] Eq. (3.11) of Hastie, Trevor J., Robert Tibshirani and Martin J.
           Wainwright. "Statistical Learning with Sparsity: The Lasso and
           Generalizations." (2015). https://hastie.su.domains/StatLearnSparsity/

    Examples
    --------
    >>> from sklearn.metrics import d2_tweedie_score
    >>> y_true = [0.5, 1, 2.5, 7]
    >>> y_pred = [1, 1, 5, 3.5]
    >>> d2_tweedie_score(y_true, y_pred)
    0.285...
    >>> d2_tweedie_score(y_true, y_pred, power=1)
    0.487...
    >>> d2_tweedie_score(y_true, y_pred, power=2)
    0.630...
    >>> d2_tweedie_score(y_true, y_true, power=2)
    1.0
    """

    )
        raise ValueError("Multioutput not supported in d2_tweedie_score")

    y_true, y_pred = xp.squeeze(y_true, axis=1), xp.squeeze(y_pred, axis=1)
    numerator = mean_tweedie_deviance(
    )

    y_avg = _average(y_true, weights=sample_weight, xp=xp)
    denominator = _mean_tweedie_deviance(
        y_true, y_avg, sample_weight=sample_weight, power=power
    )

    return 1 - numerator / denominator

def d2_pinball_score(
):
    """
    :math:`D^2` regression score function, fraction of pinball loss explained.

    arbitrarily worse). A model that always uses the empirical alpha-quantile of
    `y_true` as constant prediction, disregarding the input features,
    gets a :math:`D^2` score of 0.0.

    .. versionadded:: 1.1

    alpha : float, default=0.5
        Slope of the pinball deviance. It determines the quantile level alpha
        for which the pinball deviance and also D2 are optimal.
        The default `alpha=0.5` is equivalent to `d2_absolute_error_score`.

    Returns
    -------
        The :math:`D^2` score with a pinball deviance
        or ndarray of scores if `multioutput='raw_values'`.

    Notes
    -----
    Like :math:`R^2`, :math:`D^2` score may be negative
    (it need not actually be the square of a quantity D).

    This metric is not well-defined for a single point and will return a NaN

     References
    .. [1] Eq. (7) of `Koenker, Roger; Machado, José A. F. (1999).
           "Goodness of Fit and Related Inference Processes for Quantile Regression"
           <https://doi.org/10.1080/01621459.1999.10473882>`_
    .. [2] Eq. (3.11) of Hastie, Trevor J., Robert Tibshirani and Martin J.

    Examples
    --------
    >>> from sklearn.metrics import d2_pinball_score
    >>> y_pred = [1, 3, 3]
    >>> d2_pinball_score(y_true, y_pred)
    0.5
    >>> d2_pinball_score(y_true, y_pred, alpha=0.9)
    0.772...
    >>> d2_pinball_score(y_true, y_pred, alpha=0.1)
    -1.045...
    >>> d2_pinball_score(y_true, y_true, alpha=0.1)
    1.0
    """
    )

    numerator = mean_pinball_loss(
        y_true,
        y_pred,
        multioutput="raw_values",
    )

        y_quantile = np.tile(
            np.percentile(y_true, q=alpha * 100, axis=0), (len(y_true), 1)
        )
    else:
            _weighted_percentile(
                y_true, sample_weight=sample_weight, percentile_rank=alpha * 100
            ),
            (len(y_true), 1),
        )

    denominator = mean_pinball_loss(
        y_true,
        y_quantile,
    )

    nonzero_numerator = numerator != 0
    valid_score = nonzero_numerator & nonzero_denominator
    output_scores = np.ones(y_true.shape[1])

    output_scores[valid_score] = 1 - (numerator[valid_score] / denominator[valid_score])
    output_scores[nonzero_numerator & ~nonzero_denominator] = 0.0

        else:  # multioutput == "uniform_average"
            # passing None as weights to np.average results in uniform mean
    else:

    return float(np.average(output_scores, weights=avg_weights))

def d2_absolute_error_score(
):
    """
    :math:`D^2` regression score function, fraction of absolute error explained.

    arbitrarily worse). A model that always uses the empirical median of `y_true`
    as constant prediction, disregarding the input features,

    Returns
    -------
        The :math:`D^2` score with an absolute error deviance
        or ndarray of scores if 'multioutput' is 'raw_values'.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.metrics import d2_absolute_error_score
    >>> d2_absolute_error_score(y_true, y_pred)
    0.764...
    >>> d2_absolute_error_score(y_true, y_pred, multioutput='uniform_average')
    0.691...
    >>> d2_absolute_error_score(y_true, y_pred, multioutput='raw_values')
    array([0.8125    , 0.57142857])
    1.0
    0.0
    -1.0
    """
    return d2_pinball_score(
        y_true, y_pred, sample_weight=sample_weight, alpha=0.5, multioutput=multioutput
    )

def _return_float_dtype(X, Y):
    """
    1. If dtype of X and Y is float32, then dtype float32 is returned.
    2. Else dtype float is returned.
    """
    if not issparse(X) and not isinstance(X, np.ndarray):
        X = np.asarray(X)

    if Y is None:
        Y_dtype = X.dtype
    elif not issparse(Y) and not isinstance(Y, np.ndarray):
        Y = np.asarray(Y)
        Y_dtype = Y.dtype
    else:

    if X.dtype == Y_dtype == np.float32:
        dtype = np.float32
    else:
        dtype = float

    return X, Y, dtype

def check_pairwise_arrays(
    X,
    Y,
    *,
    precomputed=False,
    dtype="infer_float",
    accept_sparse="csr",
    force_all_finite="deprecated",
    ensure_all_finite=None,
    ensure_2d=True,
    copy=False,
):
    """Set X and Y appropriately and checks inputs.

    If Y is None, it is set as a pointer to X (i.e. not a copy).
    If Y is given, this does not happen.
    All distance metrics should use this function first to assert that the
    given parameters are correct and safe to use.

    Specifically, this function first ensures that both X and Y are arrays,
    then checks that they are at least two dimensional while ensuring that
    their elements are floats (or dtype if provided). Finally, the function
    checks that the size of the second dimension of the two arrays is equal, or
    the equivalent check for a precomputed distance matrix.

    X : {array-like, sparse matrix} of shape (n_samples_X, n_features)

    Y : {array-like, sparse matrix} of shape (n_samples_Y, n_features)

    precomputed : bool, default=False
        True if X is to be treated as precomputed distances to the samples in
        Y.

    dtype : str, type, list of type or None default="infer_float"
        Data type required for X and Y. If "infer_float", the dtype will be an
        appropriate float type selected by _return_float_dtype. If None, the
        dtype of the input is preserved.

    accept_sparse : str, bool or list/tuple of str, default='csr'
        String[s] representing allowed sparse matrix formats, such as 'csc',
        'csr', etc. If the input is sparse but not in the allowed format,
        it will be converted to the first listed format. True allows the input
        to be any format. False means that a sparse matrix input will
        raise an error.

    force_all_finite : bool or 'allow-nan', default=True
        Whether to raise an error on np.inf, np.nan, pd.NA in array. The
        possibilities are:

        - True: Force all values of array to be finite.
        - False: accepts np.inf, np.nan, pd.NA in array.
        - 'allow-nan': accepts only np.nan and pd.NA values in array. Values
          cannot be infinite.

           ``force_all_finite`` accepts the string ``'allow-nan'``.

           Accepts `pd.NA` and converts it into `np.nan`.

           `force_all_finite` was renamed to `ensure_all_finite` and will be removed
           in 1.8.

    ensure_all_finite : bool or 'allow-nan', default=True

           `force_all_finite` was renamed to `ensure_all_finite`.

    ensure_2d : bool, default=True
        Whether to raise an error when the input arrays are not 2-dimensional. Setting
        this to `False` is necessary when using a custom metric with certain
        non-numerical inputs (e.g. a list of strings).

    copy : bool, default=False
        Whether a forced copy will be triggered. If copy=False, a copy might
        be triggered by a conversion.

    Returns
    -------
    safe_X : {array-like, sparse matrix} of shape (n_samples_X, n_features)
        An array equal to X, guaranteed to be a numpy array.

    safe_Y : {array-like, sparse matrix} of shape (n_samples_Y, n_features)
        An array equal to Y if Y was not None, guaranteed to be a numpy array.
        If Y was None, safe_Y will be a pointer to X.
    """
    ensure_all_finite = _deprecate_force_all_finite(force_all_finite, ensure_all_finite)

    xp, _ = get_namespace(X, Y)
    if any([issparse(X), issparse(Y)]) or _is_numpy_namespace(xp):
        X, Y, dtype_float = _return_float_dtype(X, Y)
    else:
        dtype_float = _find_matching_floating_dtype(X, Y, xp=xp)

    estimator = "check_pairwise_arrays"
    if dtype == "infer_float":
        dtype = dtype_float

    if Y is X or Y is None:
        X = Y = check_array(
            X,
            accept_sparse=accept_sparse,
        )
    else:
            X,
        )
        Y = check_array(
            Y,
        )

    if precomputed:
        if X.shape[1] != Y.shape[0]:
                "Precomputed metric requires shape "
                "(n_queries, n_indexed). Got (%d, %d) "
                "for %d indexed." % (X.shape[0], X.shape[1], Y.shape[0])
            )
    elif ensure_2d and X.shape[1] != Y.shape[1]:
        # Only check the number of features if 2d arrays are enforced. Otherwise,
        # validation is left to the user for custom metrics.
            "Incompatible dimension for X and Y matrices: "
            "X.shape[1] == %d while Y.shape[1] == %d" % (X.shape[1], Y.shape[1])
        )

    return X, Y

def check_paired_arrays(X, Y):
    """Set X and Y appropriately and checks inputs for paired distances.

    All paired distance metrics should use this function first to assert that
    the given parameters are correct and safe to use.

    their elements are floats. Finally, the function checks that the size
    of the dimensions of the two arrays are equal.

    Returns
    -------

    """
    X, Y = check_pairwise_arrays(X, Y)
    if X.shape != Y.shape:
            "X and Y should be of same shape. They were respectively %r and %r long."
            % (X.shape, Y.shape)
        )
    return X, Y

def euclidean_distances(
    X, Y=None, *, Y_norm_squared=None, squared=False, X_norm_squared=None
):
    """
    Compute the distance matrix between each pair from a feature array X and Y.

    For efficiency reasons, the euclidean distance between a pair of row
    vector x and y is computed as::

        dist(x, y) = sqrt(dot(x, x) - 2 * dot(x, y) + dot(y, y))

    This formulation has two advantages over other ways of computing distances.
    First, it is computationally efficient when dealing with sparse data.
    Second, if one argument varies but the other remains unchanged, then
    `dot(x, x)` and/or `dot(y, y)` can be pre-computed.

    However, this is not the most precise way of doing this computation,
    because this equation potentially suffers from "catastrophic cancellation".
    Also, the distance matrix returned by this function may not be exactly
    symmetric as required by, e.g., ``scipy.spatial.distance`` functions.

    Read more in the :ref:`User Guide <metrics>`.

        An array where each row is a sample and each column is a feature.

    Y : {array-like, sparse matrix} of shape (n_samples_Y, n_features), \
        If `None`, method uses `Y=X`.

    Y_norm_squared : array-like of shape (n_samples_Y,) or (n_samples_Y, 1) \
            or (1, n_samples_Y), default=None
        Pre-computed dot-products of vectors in Y (e.g.,
        ``(Y**2).sum(axis=1)``)
        May be ignored in some cases, see the note below.

    squared : bool, default=False
        Return squared Euclidean distances.

    X_norm_squared : array-like of shape (n_samples_X,) or (n_samples_X, 1) \
            or (1, n_samples_X), default=None
        Pre-computed dot-products of vectors in X (e.g.,
        ``(X**2).sum(axis=1)``)

    Returns
    -------
    distances : ndarray of shape (n_samples_X, n_samples_Y)
        Returns the distances between the row vectors of `X`
        and the row vectors of `Y`.

    See Also
    --------
    paired_distances : Distances between pairs of elements of X and Y.

    Notes
    -----
    To achieve a better accuracy, `X_norm_squared` and `Y_norm_squared` may be
    unused if they are passed as `np.float32`.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import euclidean_distances
    >>> X = [[0, 1], [1, 1]]
    >>> # distance between rows of X
    >>> euclidean_distances(X, X)
    array([[0., 1.],
           [1., 0.]])
    >>> # get distance to origin
    >>> euclidean_distances(X, [[0, 0]])
    array([[1.        ],
           [1.41421356]])
    """

    if X_norm_squared is not None:
        X_norm_squared = check_array(X_norm_squared, ensure_2d=False)
        original_shape = X_norm_squared.shape
        if X_norm_squared.shape == (X.shape[0],):
            X_norm_squared = xp.reshape(X_norm_squared, (-1, 1))
        if X_norm_squared.shape == (1, X.shape[0]):
            X_norm_squared = X_norm_squared.T
        if X_norm_squared.shape != (X.shape[0], 1):
                f"Incompatible dimensions for X of shape {X.shape} and "
                f"X_norm_squared of shape {original_shape}."
            )

    if Y_norm_squared is not None:
        Y_norm_squared = check_array(Y_norm_squared, ensure_2d=False)
        original_shape = Y_norm_squared.shape
        if Y_norm_squared.shape == (Y.shape[0],):
            Y_norm_squared = xp.reshape(Y_norm_squared, (1, -1))
        if Y_norm_squared.shape == (Y.shape[0], 1):
            Y_norm_squared = Y_norm_squared.T
        if Y_norm_squared.shape != (1, Y.shape[0]):
                f"Incompatible dimensions for Y of shape {Y.shape} and "
                f"Y_norm_squared of shape {original_shape}."
            )

    return _euclidean_distances(X, Y, X_norm_squared, Y_norm_squared, squared)

def _euclidean_distances(X, Y, X_norm_squared=None, Y_norm_squared=None, squared=False):
    """Computational part of euclidean_distances

    Assumes inputs are already checked.

    If norms are passed as float32, they are unused. If arrays are passed as
    float32, norms needs to be recomputed on upcast chunks.
    TODO: use a float64 accumulator in row_norms to avoid the latter.
    """
    xp, _, device_ = get_namespace_and_device(X, Y)
    if X_norm_squared is not None and X_norm_squared.dtype != xp.float32:
        XX = xp.reshape(X_norm_squared, (-1, 1))
    elif X.dtype != xp.float32:
        XX = row_norms(X, squared=True)[:, None]
    else:
        XX = None

    if Y is X:
        YY = None if XX is None else XX.T
    else:
        if Y_norm_squared is not None and Y_norm_squared.dtype != xp.float32:
            YY = xp.reshape(Y_norm_squared, (1, -1))
        elif Y.dtype != xp.float32:
            YY = row_norms(Y, squared=True)[None, :]
        else:
            YY = None

    if X.dtype == xp.float32 or Y.dtype == xp.float32:
        # To minimize precision issues with float32, we compute the distance
        # matrix on chunks of X and Y upcast to float64
        distances = _euclidean_distances_upcast(X, XX, Y, YY)
    else:
        # if dtype is already float64, no need to chunk and upcast
        distances = -2 * safe_sparse_dot(X, Y.T, dense_output=True)
        distances += XX
        distances += YY

    xp_zero = xp.asarray(0, device=device_, dtype=distances.dtype)
    distances = _modify_in_place_if_numpy(
        xp, xp.maximum, distances, xp_zero, out=distances
    )

    # Ensure that distances between vectors and themselves are set to 0.0.
    # This may not be the case due to floating point rounding errors.
    if X is Y:
        _fill_or_add_to_diagonal(distances, 0, xp=xp, add_value=False)

    if squared:
        return distances

    distances = _modify_in_place_if_numpy(xp, xp.sqrt, distances, out=distances)
    return distances

def nan_euclidean_distances(
    X, Y=None, *, squared=False, missing_values=np.nan, copy=True
):
    """Calculate the euclidean distances in the presence of missing values.

    Compute the euclidean distance between each pair of samples in X and Y,
    where Y=X is assumed if Y=None. When calculating the distance between a
    pair of samples, this formulation ignores feature coordinates with a
    missing value in either sample and scales up the weight of the remaining
    coordinates:

        dist(x,y) = sqrt(weight * sq. distance from present coordinates)

    where:

        weight = Total # of coordinates / # of present coordinates

    For example, the distance between ``[3, na, na, 6]`` and ``[1, na, 4, 5]`` is:

    .. math::
        \\sqrt{\\frac{4}{2}((3-1)^2 + (6-5)^2)}

    If all the coordinates are missing or if there are no common present
    coordinates then NaN is returned for that pair.

    X : array-like of shape (n_samples_X, n_features)

    Y : array-like of shape (n_samples_Y, n_features), default=None

    missing_values : np.nan, float or int, default=np.nan
        Representation of missing value.

    copy : bool, default=True
        Make and use a deep copy of X and Y (if Y exists).

    Returns
    -------

    See Also
    --------

    * John K. Dixon, "Pattern Recognition with Partly Missing Data",
      IEEE Transactions on Systems, Man, and Cybernetics, Volume: 9, Issue:
      10, pp. 617 - 621, Oct. 1979.
      http://ieeexplore.ieee.org/abstract/document/4310090/

    Examples
    --------
    >>> from sklearn.metrics.pairwise import nan_euclidean_distances
    >>> nan = float("NaN")
    >>> X = [[0, 1], [1, nan]]
    >>> nan_euclidean_distances(X, X) # distance between rows of X
    array([[0.        , 1.41421356],
           [1.41421356, 0.        ]])

    >>> nan_euclidean_distances(X, [[0, 0]])
    """

    ensure_all_finite = "allow-nan" if is_scalar_nan(missing_values) else True
    X, Y = check_pairwise_arrays(
        X, Y, accept_sparse=False, ensure_all_finite=ensure_all_finite, copy=copy
    )
    # Get missing mask for X
    missing_X = _get_mask(X, missing_values)

    # Get missing mask for Y
    missing_Y = missing_X if Y is X else _get_mask(Y, missing_values)

    # set missing values to zero
    X[missing_X] = 0
    Y[missing_Y] = 0

    distances = euclidean_distances(X, Y, squared=True)

    # Adjust distances for missing values
    XX = X * X
    YY = Y * Y
    distances -= np.dot(XX, missing_Y.T)
    distances -= np.dot(missing_X, YY.T)

    np.clip(distances, 0, None, out=distances)

    if X is Y:
        # Ensure that distances between vectors and themselves are set to 0.0.
        # This may not be the case due to floating point rounding errors.
        np.fill_diagonal(distances, 0.0)

    present_X = 1 - missing_X
    present_Y = present_X if Y is X else ~missing_Y
    present_count = np.dot(present_X, present_Y.T)
    distances[present_count == 0] = np.nan
    # avoid divide by zero
    np.maximum(1, present_count, out=present_count)
    distances /= present_count
    distances *= X.shape[1]

    if not squared:
        np.sqrt(distances, out=distances)

def _euclidean_distances_upcast(X, XX=None, Y=None, YY=None, batch_size=None):
    """Euclidean distances between X and Y.

    Assumes X and Y have float32 dtype.
    Assumes XX and YY have float64 dtype or are None.

    X and Y are upcast to float64 by chunks, which size is chosen to limit
    memory increase by approximately 10% (at least 10MiB).
    """
    n_samples_X = X.shape[0]
    n_samples_Y = Y.shape[0]

    distances = xp.empty((n_samples_X, n_samples_Y), dtype=xp.float32, device=device_)

    if batch_size is None:
        x_density = X.nnz / np.prod(X.shape) if issparse(X) else 1
        y_density = Y.nnz / np.prod(Y.shape) if issparse(Y) else 1

        # Allow 10% more memory than X, Y and the distance matrix take (at
        # least 10MiB)
        maxmem = max(
            (
                (x_density * n_samples_X + y_density * n_samples_Y) * n_features
                + (x_density * n_samples_X * y_density * n_samples_Y)
            )
            / 10,
            10 * 2**17,
        )

        # The increase amount of memory in 8-byte blocks is:
        # - x_density * batch_size * n_features (copy of chunk of X)
        # - y_density * batch_size * n_features (copy of chunk of Y)
        # - batch_size * batch_size (chunk of distance matrix)
        # Hence x² + (xd+yd)kx = M, where x=batch_size, k=n_features, M=maxmem
        #                                 xd=x_density and yd=y_density
        tmp = (x_density + y_density) * n_features
        batch_size = (-tmp + math.sqrt(tmp**2 + 4 * maxmem)) / 2
        batch_size = max(int(batch_size), 1)

    x_batches = gen_batches(n_samples_X, batch_size)
    xp_max_float = _max_precision_float_dtype(xp=xp, device=device_)
    for i, x_slice in enumerate(x_batches):
        X_chunk = xp.astype(X[x_slice, :], xp_max_float)
        if XX is None:
            XX_chunk = row_norms(X_chunk, squared=True)[:, None]
        else:
            XX_chunk = XX[x_slice]

        y_batches = gen_batches(n_samples_Y, batch_size)

        for j, y_slice in enumerate(y_batches):
            if X is Y and j < i:
                # when X is Y the distance matrix is symmetric so we only need
                # to compute half of it.
                d = distances[y_slice, x_slice].T

            else:
                Y_chunk = xp.astype(Y[y_slice, :], xp_max_float)
                if YY is None:
                    YY_chunk = row_norms(Y_chunk, squared=True)[None, :]
                else:
                    YY_chunk = YY[:, y_slice]

                d = -2 * safe_sparse_dot(X_chunk, Y_chunk.T, dense_output=True)
                d += XX_chunk
                d += YY_chunk

            distances[x_slice, y_slice] = xp.astype(d, xp.float32, copy=False)

def _argmin_min_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.
    indices = dist.argmin(axis=1)
    values = dist[np.arange(dist.shape[0]), indices]
    return indices, values

def _argmin_reduce(dist, start):
    return dist.argmin(axis=1)

def pairwise_distances_argmin_min(
    X, Y, *, axis=1, metric="euclidean", metric_kwargs=None
):
    """Compute minimum distances between one point and a set of points.

    This function computes for each row in X, the index of the row of Y which
    is closest (according to the specified distance). The minimal distances are
    also returned.

    This is mostly equivalent to calling::

        (pairwise_distances(X, Y=Y, metric=metric).argmin(axis=axis),
         pairwise_distances(X, Y=Y, metric=metric).min(axis=axis))

    but uses much less memory, and is faster for large arrays.

        Array containing points.

    axis : int, default=1
        Axis along which the argmin and distances are to be computed.

    metric : str or callable, default='euclidean'
        Metric to use for distance computation. Any metric from scikit-learn
        or scipy.spatial.distance can be used.

        If metric is a callable function, it is called on each
        should take two arrays as input and return one value indicating the
        distance between them. This works for Scipy's metrics, but is less
        efficient than passing the metric name as a string.

        Distance matrices are not supported.

        Valid values for metric are:

        - from scikit-learn: ['cityblock', 'cosine', 'euclidean', 'l1', 'l2',
          'manhattan', 'nan_euclidean']

        - from scipy.spatial.distance: ['braycurtis', 'canberra', 'chebyshev',
          'correlation', 'dice', 'hamming', 'jaccard', 'kulsinski',
          'mahalanobis', 'minkowski', 'rogerstanimoto', 'russellrao',
          'seuclidean', 'sokalmichener', 'sokalsneath', 'sqeuclidean',
          'yule']

        See the documentation for scipy.spatial.distance for details on these
        metrics.

        .. note::
           `'kulsinski'` is deprecated from SciPy 1.9 and will be removed in SciPy 1.11.

        .. note::
           `'matching'` has been removed in SciPy 1.9 (use `'hamming'` instead).

    metric_kwargs : dict, default=None
        Keyword arguments to pass to specified metric function.

    Returns
    -------
    argmin : ndarray
        Y[argmin[i], :] is the row in Y that is closest to X[i, :].

    distances : ndarray
        The array of minimum distances. `distances[i]` is the distance between
        the i-th row in X and the argmin[i]-th row in Y.

    See Also
    --------
    pairwise_distances : Distances between every pair of samples of X and Y.
    pairwise_distances_argmin : Same as `pairwise_distances_argmin_min` but only
        returns the argmins.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import pairwise_distances_argmin_min
    >>> X = [[0, 0, 0], [1, 1, 1]]
    >>> Y = [[1, 0, 0], [1, 1, 0]]
    >>> argmin, distances = pairwise_distances_argmin_min(X, Y)
    >>> argmin
    array([0, 1])
    >>> distances
    array([1., 1.])
    """
    ensure_all_finite = "allow-nan" if metric == "nan_euclidean" else True
    X, Y = check_pairwise_arrays(X, Y, ensure_all_finite=ensure_all_finite)

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

    if metric_kwargs is None:
        metric_kwargs = {}

    if ArgKmin.is_usable_for(X, Y, metric):
        # 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":
            metric = "sqeuclidean"
            metric_kwargs = {}

        values, indices = ArgKmin.compute(
            X=X,
            Y=Y,
            k=1,
            metric=metric,
            metric_kwargs=metric_kwargs,
            strategy="auto",
            return_distance=True,
        )
        values = values.flatten()
        indices = indices.flatten()
    else:
        # Joblib-based backend, which is used when user-defined callable
        # are passed for metric.

        # This won't be used in the future once PairwiseDistancesReductions support:
        #   - DistanceMetrics which work on supposedly binary data
        #   - CSR-dense and dense-CSR case if 'euclidean' in metric.

        # Turn off check for finiteness because this is costly and because arrays
        # have already been validated.
            indices, values = zip(
                *pairwise_distances_chunked(
                    X, Y, reduce_func=_argmin_min_reduce, metric=metric, **metric_kwargs
                )
            )
        values = np.concatenate(values)

def pairwise_distances_argmin(X, Y, *, axis=1, metric="euclidean", metric_kwargs=None):

    is closest (according to the specified distance).

        pairwise_distances(X, Y=Y, metric=metric).argmin(axis=axis)

    This function works with dense 2D arrays only.

        Arrays containing points.

    metric : str or callable, default="euclidean"

          'yule']

        metrics.

        .. note::

        .. note::

    Returns
    -------
    argmin : numpy.ndarray

    See Also
    --------
    pairwise_distances_argmin_min : Same as `pairwise_distances_argmin` but also
        returns the distances.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import pairwise_distances_argmin
    >>> pairwise_distances_argmin(X, Y)
    """

        X, Y = Y, X

        indices = ArgKmin.compute(
            X=X,
            Y=Y,
            k=1,
            return_distance=False,
        )
    else:

            indices = np.concatenate(
                list(
                    # This returns a np.ndarray generator whose arrays we need
                    # to flatten into one.
                    pairwise_distances_chunked(
                        X, Y, reduce_func=_argmin_reduce, metric=metric, **metric_kwargs
                    )
                )
            )

def haversine_distances(X, Y=None):
    """Compute the Haversine distance between samples in X and Y.

    The Haversine (or great circle) distance is the angular distance between
    two points on the surface of a sphere. The first coordinate of each point
    is assumed to be the latitude, the second is the longitude, given
    in radians. The dimension of the data must be 2.

    .. math::
       D(x, y) = 2\\arcsin[\\sqrt{\\sin^2((x_{lat} - y_{lat}) / 2)
                                + \\cos(x_{lat})\\cos(y_{lat})\\
                                sin^2((x_{lon} - y_{lon}) / 2)}]

    X : {array-like, sparse matrix} of shape (n_samples_X, 2)
        A feature array.

    Y : {array-like, sparse matrix} of shape (n_samples_Y, 2), default=None
        An optional second feature array. If `None`, uses `Y=X`.

    Returns
    -------
        The distance matrix.

    Notes
    -----
    As the Earth is nearly spherical, the haversine formula provides a good
    approximation of the distance between two points of the Earth surface, with
    a less than 1% error on average.

    Examples
    --------
    We want to calculate the distance between the Ezeiza Airport
    (Buenos Aires, Argentina) and the Charles de Gaulle Airport (Paris,
    France).

    >>> from sklearn.metrics.pairwise import haversine_distances
    >>> from math import radians
    >>> bsas = [-34.83333, -58.5166646]
    >>> paris = [49.0083899664, 2.53844117956]
    >>> bsas_in_radians = [radians(_) for _ in bsas]
    >>> paris_in_radians = [radians(_) for _ in paris]
    >>> result = haversine_distances([bsas_in_radians, paris_in_radians])
    >>> result * 6371000/1000  # multiply by Earth radius to get kilometers
    array([[    0.        , 11099.54035582],
           [11099.54035582,     0.        ]])
    """
    from ..metrics import DistanceMetric

    return DistanceMetric.get_metric("haversine").pairwise(X, Y)

def manhattan_distances(X, Y=None):
    """Compute the L1 distances between the vectors in X and Y.

    Y : {array-like, sparse matrix} of shape (n_samples_Y, n_features), default=None

    Returns
    -------
        Pairwise L1 distances.

    Notes
    -----
    When X and/or Y are CSR sparse matrices and they are not already
    in canonical format, this function modifies them in-place to
    make them canonical.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import manhattan_distances
    >>> manhattan_distances([[3]], [[3]])
    array([[0.]])
    >>> manhattan_distances([[3]], [[2]])
    array([[1.]])
    >>> manhattan_distances([[2]], [[3]])
    >>> manhattan_distances([[1, 2], [3, 4]],\
         [[1, 2], [0, 3]])
           [4., 4.]])
    """

    if issparse(X) or issparse(Y):
        X = csr_matrix(X, copy=False)
        Y = csr_matrix(Y, copy=False)
        X.sum_duplicates()  # this also sorts indices in-place
        Y.sum_duplicates()
        D = np.zeros((X.shape[0], Y.shape[0]))
        _sparse_manhattan(X.data, X.indices, X.indptr, Y.data, Y.indices, Y.indptr, D)
        return D

    return distance.cdist(X, Y, "cityblock")

def cosine_distances(X, Y=None):
    """Compute cosine distance between samples in X and Y.

    Cosine distance is defined as 1.0 minus the cosine similarity.

        Matrix `X`.

        Matrix `Y`.

    Returns
    -------
        Returns the cosine distance between samples in X and Y.

    See Also
    --------
    cosine_similarity : Compute cosine similarity between samples in X and Y.
    scipy.spatial.distance.cosine : Dense matrices only.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import cosine_distances
    >>> cosine_distances(X, Y)
    array([[1.     , 1.     ],
           [0.42..., 0.18...]])
    """

    # 1.0 - cosine_similarity(X, Y) without copy
    S = cosine_similarity(X, Y)
    S *= -1
    S += 1
    S = xp.clip(S, 0.0, 2.0)
    if X is Y or Y is None:
        _fill_or_add_to_diagonal(S, 0.0, xp, add_value=False)
    return S

def paired_euclidean_distances(X, Y):
    """Compute the paired euclidean distances between X and Y.

        Input array/matrix X.

    Y : {array-like, sparse matrix} of shape (n_samples, n_features)
        Input array/matrix Y.

    Returns
    -------
    distances : ndarray of shape (n_samples,)
        Output array/matrix containing the calculated paired euclidean
        distances.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import paired_euclidean_distances
    >>> paired_euclidean_distances(X, Y)
    """
    X, Y = check_paired_arrays(X, Y)
    return row_norms(X - Y)

def paired_manhattan_distances(X, Y):
    """Compute the paired L1 distances between X and Y.

    Distances are calculated between (X[0], Y[0]), (X[1], Y[1]), ...,
    (X[n_samples], Y[n_samples]).

        An array-like where each row is a sample and each column is a feature.

    Returns
    -------
        L1 paired distances between the row vectors of `X`

    Examples
    --------
    >>> from sklearn.metrics.pairwise import paired_manhattan_distances
    >>> X = np.array([[1, 1, 0], [0, 1, 0], [0, 0, 1]])
    >>> Y = np.array([[0, 1, 0], [0, 0, 1], [0, 0, 0]])
    >>> paired_manhattan_distances(X, Y)
    array([1., 2., 1.])
    """
    diff = X - Y
    if issparse(diff):
        diff.data = np.abs(diff.data)
        return np.squeeze(np.array(diff.sum(axis=1)))
    else:
        return np.abs(diff).sum(axis=-1)

def paired_cosine_distances(X, Y):
    """
    Compute the paired cosine distances between X and Y.

    Returns
    -------
        and the row vectors of `Y`, where `distances[i]` is the
        distance between `X[i]` and `Y[i]`.

    Notes
    -----
    The cosine distance is equivalent to the half the squared
    euclidean distance if each sample is normalized to unit norm.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import paired_cosine_distances
    >>> paired_cosine_distances(X, Y)
    array([0.5       , 0.18...])
    """
    return 0.5 * row_norms(normalize(X) - normalize(Y), squared=True)

def paired_distances(X, Y, *, metric="euclidean", **kwds):
    """
    Compute the paired distances between X and Y.

    Compute the distances between (X[0], Y[0]), (X[1], Y[1]), etc...

        Array 1 for distance computation.

    Y : ndarray of shape (n_samples, n_features)
        Array 2 for distance computation.

        The metric to use when calculating distance between instances in a
        feature array. If metric is a string, it must be one of the options
        specified in PAIRED_DISTANCES, including "euclidean",
        "manhattan", or "cosine".
        should take two arrays from `X` as input and return a value indicating

    **kwds : dict
        Unused parameters.

    Returns
    -------

    See Also
    --------
    sklearn.metrics.pairwise_distances : Computes the distance between every pair of
        samples.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import paired_distances
    >>> Y = [[0, 1], [2, 1]]
    >>> paired_distances(X, Y)
    array([0., 1.])
    """

    if metric in PAIRED_DISTANCES:
        func = PAIRED_DISTANCES[metric]
        return func(X, Y)
    elif callable(metric):
        # Check the matrix first (it is usually done by the metric)
        X, Y = check_paired_arrays(X, Y)
        distances = np.zeros(len(X))
        for i in range(len(X)):
            distances[i] = metric(X[i], Y[i])

def linear_kernel(X, Y=None, dense_output=True):
    """
    Compute the linear kernel between X and Y.

    Read more in the :ref:`User Guide <linear_kernel>`.

    dense_output : bool, default=True
        Whether to return dense output even when the input is sparse. If
        ``False``, the output is sparse if both input arrays are sparse.

    Returns
    -------
    kernel : ndarray of shape (n_samples_X, n_samples_Y)
        The Gram matrix of the linear kernel, i.e. `X @ Y.T`.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import linear_kernel
    >>> linear_kernel(X, Y)
    array([[0., 0.],
           [1., 2.]])
    """
    return safe_sparse_dot(X, Y.T, dense_output=dense_output)

def polynomial_kernel(X, Y=None, degree=3, gamma=None, coef0=1):
    """
    Compute the polynomial kernel between X and Y.

        K(X, Y) = (gamma <X, Y> + coef0) ^ degree

    Read more in the :ref:`User Guide <polynomial_kernel>`.

        Kernel degree.

        Coefficient of the vector inner product. If None, defaults to 1.0 / n_features.

        Constant offset added to scaled inner product.

    Returns
    -------
        The polynomial kernel.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import polynomial_kernel
    >>> polynomial_kernel(X, Y, degree=2)
           [1.77..., 2.77...]])
    """
    if gamma is None:
        gamma = 1.0 / X.shape[1]

    K = safe_sparse_dot(X, Y.T, dense_output=True)
    K *= gamma
    K += coef0
    K **= degree
    return K

def sigmoid_kernel(X, Y=None, gamma=None, coef0=1):
    """Compute the sigmoid kernel between X and Y.

        K(X, Y) = tanh(gamma <X, Y> + coef0)

    Read more in the :ref:`User Guide <sigmoid_kernel>`.

    Returns
    -------
        Sigmoid kernel between two arrays.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import sigmoid_kernel
    >>> sigmoid_kernel(X, Y)
    array([[0.76..., 0.76...],
           [0.87..., 0.93...]])
    """

    K *= gamma
    K += coef0
    # compute tanh in-place for numpy
    K = _modify_in_place_if_numpy(xp, xp.tanh, K, out=K)
    return K

def rbf_kernel(X, Y=None, gamma=None):
    """Compute the rbf (gaussian) kernel between X and Y.

        K(x, y) = exp(-gamma ||x-y||^2)

    for each pair of rows x in X and y in Y.

    Read more in the :ref:`User Guide <rbf_kernel>`.

        If None, defaults to 1.0 / n_features.

    Returns
    -------
        The RBF kernel.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import rbf_kernel
    >>> rbf_kernel(X, Y)
    array([[0.71..., 0.51...],
           [0.51..., 0.71...]])
    """

    K = euclidean_distances(X, Y, squared=True)
    K *= -gamma
    # exponentiate K in-place when using numpy
    K = _modify_in_place_if_numpy(xp, xp.exp, K, out=K)
    return K

def laplacian_kernel(X, Y=None, gamma=None):
    """Compute the laplacian kernel between X and Y.

    The laplacian kernel is defined as:

        K(x, y) = exp(-gamma ||x-y||_1)

    Read more in the :ref:`User Guide <laplacian_kernel>`.

        If None, defaults to 1.0 / n_features. Otherwise it should be strictly positive.

    Returns
    -------
        The kernel matrix.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import laplacian_kernel
    >>> laplacian_kernel(X, Y)
    """

    K = -gamma * manhattan_distances(X, Y)
    np.exp(K, K)  # exponentiate K in-place
    return K

def cosine_similarity(X, Y=None, dense_output=True):
    """Compute cosine similarity between samples in X and Y.

    Cosine similarity, or the cosine kernel, computes similarity as the
    normalized dot product of X and Y:

        K(X, Y) = <X, Y> / (||X||*||Y||)

    On L2-normalized data, this function is equivalent to linear_kernel.

    Read more in the :ref:`User Guide <cosine_similarity>`.

        Input data. If ``None``, the output will be the pairwise
        similarities between all samples in ``X``.

           parameter ``dense_output`` for dense output.

    Returns
    -------
    similarities : ndarray or sparse matrix of shape (n_samples_X, n_samples_Y)
        Returns the cosine similarity between samples in X and Y.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import cosine_similarity
    >>> cosine_similarity(X, Y)
    array([[0.     , 0.     ],
           [0.57..., 0.81...]])
    """

    X_normalized = normalize(X, copy=True)
    if X is Y:
        Y_normalized = X_normalized
    else:
        Y_normalized = normalize(Y, copy=True)

    K = safe_sparse_dot(X_normalized, Y_normalized.T, dense_output=dense_output)

    return K

def additive_chi2_kernel(X, Y=None):
    """Compute the additive chi-squared kernel between observations in X and Y.

    The chi-squared kernel is computed between each pair of rows in X and Y.  X
    and Y have to be non-negative. This kernel is most commonly applied to
    histograms.

    The chi-squared kernel is given by:

        k(x, y) = -Sum [(x - y)^2 / (x + y)]

    It can be interpreted as a weighted difference per entry.

    Read more in the :ref:`User Guide <chi2_kernel>`.

    Returns
    -------
    kernel : array-like of shape (n_samples_X, n_samples_Y)

    See Also
    --------
    chi2_kernel : The exponentiated version of the kernel, which is usually
        preferable.
    sklearn.kernel_approximation.AdditiveChi2Sampler : A Fourier approximation
        to this kernel.

    Notes
    -----
    As the negative of a distance, this kernel is only conditionally positive
    definite.

    * Zhang, J. and Marszalek, M. and Lazebnik, S. and Schmid, C.
      Local features and kernels for classification of texture and object
      categories: A comprehensive study
      International Journal of Computer Vision 2007
      https://hal.archives-ouvertes.fr/hal-00171412/document

    Examples
    --------
    >>> from sklearn.metrics.pairwise import additive_chi2_kernel
    >>> additive_chi2_kernel(X, Y)
    array([[-1., -2.],
           [-2., -1.]])
    """
    X, Y = check_pairwise_arrays(X, Y, accept_sparse=False)
    if xp.any(X < 0):
        raise ValueError("X contains negative values.")
    if Y is not X and xp.any(Y < 0):
        raise ValueError("Y contains negative values.")

    if _is_numpy_namespace(xp):
        result = np.zeros((X.shape[0], Y.shape[0]), dtype=X.dtype)
        _chi2_kernel_fast(X, Y, result)
    else:
        dtype = _find_matching_floating_dtype(X, Y, xp=xp)
        xb = X[:, None, :]
        yb = Y[None, :, :]
        nom = -((xb - yb) ** 2)
        denom = xb + yb
        nom = xp.where(denom == 0, xp.asarray(0, dtype=dtype, device=device_), nom)
        denom = xp.where(denom == 0, xp.asarray(1, dtype=dtype, device=device_), denom)
        return xp.sum(nom / denom, axis=2)

def chi2_kernel(X, Y=None, gamma=1.0):
    """Compute the exponential chi-squared kernel between X and Y.

        k(x, y) = exp(-gamma Sum [(x - y)^2 / (x + y)])

    gamma : float, default=1
        Scaling parameter of the chi2 kernel.

    Returns
    -------

    See Also
    --------
    additive_chi2_kernel : The additive version of this kernel.
        to the additive version of this kernel.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import chi2_kernel
    >>> chi2_kernel(X, Y)
    array([[0.36..., 0.13...],
           [0.13..., 0.36...]])
    """
    K = additive_chi2_kernel(X, Y)
    K *= gamma
        return np.exp(K, out=K)
    return xp.exp(K)

def distance_metrics():
    """Valid metrics for pairwise_distances.

    This function simply returns the valid pairwise distance metrics.
    It exists to allow for a description of the mapping for
    each of the valid strings.

    The valid distance metrics, and the function they map to, are:

    =============== ========================================
    metric          Function
    'cityblock'     metrics.pairwise.manhattan_distances
    'cosine'        metrics.pairwise.cosine_distances
    'euclidean'     metrics.pairwise.euclidean_distances
    'haversine'     metrics.pairwise.haversine_distances
    'l1'            metrics.pairwise.manhattan_distances
    'l2'            metrics.pairwise.euclidean_distances
    'manhattan'     metrics.pairwise.manhattan_distances
    'nan_euclidean' metrics.pairwise.nan_euclidean_distances

    Returns
    -------
    distance_metrics : dict
        Returns valid metrics for pairwise_distances.
    """
    return PAIRWISE_DISTANCE_FUNCTIONS

def _dist_wrapper(dist_func, dist_matrix, slice_, *args, **kwargs):
    """Write in-place to a slice of a distance matrix."""
    dist_matrix[:, slice_] = dist_func(*args, **kwargs)

def _parallel_pairwise(X, Y, func, n_jobs, **kwds):
    """Break the pairwise matrix in n_jobs even slices
    and compute them using multithreading."""

        Y = X
    X, Y, dtype = _return_float_dtype(X, Y)

    if effective_n_jobs(n_jobs) == 1:
        return func(X, Y, **kwds)

    # enforce a threading backend to prevent data communication overhead
    fd = delayed(_dist_wrapper)
    ret = np.empty((X.shape[0], Y.shape[0]), dtype=dtype, order="F")
    Parallel(backend="threading", n_jobs=n_jobs)(
        fd(func, ret, s, X, Y[s], **kwds)
        for s in gen_even_slices(_num_samples(Y), effective_n_jobs(n_jobs))
    )

    if (X is Y or Y is None) and func is euclidean_distances:
        # zeroing diagonal for euclidean norm.
        # TODO: do it also for other norms.
        np.fill_diagonal(ret, 0)

    return ret

def _pairwise_callable(X, Y, metric, ensure_all_finite=True, **kwds):
    """Handle the callable case for pairwise_{distances,kernels}."""
        X,
        Y,
        dtype=None,
        ensure_all_finite=ensure_all_finite,
        # No input dimension checking done for custom metrics (left to user)
        ensure_2d=False,
    )

    if X is Y:
        # Only calculate metric for upper triangle
        out = np.zeros((X.shape[0], Y.shape[0]), dtype="float")
        iterator = itertools.combinations(range(X.shape[0]), 2)
        for i, j in iterator:
            # scipy has not yet implemented 1D sparse slices; once implemented this can
            # be removed and `arr[ind]` can be simply used.
            x = X[[i], :] if issparse(X) else X[i]
            y = Y[[j], :] if issparse(Y) else Y[j]
            out[i, j] = metric(x, y, **kwds)

        # Make symmetric
        # NB: out += out.T will produce incorrect results
        out = out + out.T

        # Calculate diagonal
        # NB: nonzero diagonals are allowed for both metrics and kernels
        for i in range(X.shape[0]):
            out[i, i] = metric(x, x, **kwds)

    else:
        # Calculate all cells
        out = np.empty((X.shape[0], Y.shape[0]), dtype="float")
        iterator = itertools.product(range(X.shape[0]), range(Y.shape[0]))

    return out

def _check_chunk_size(reduced, chunk_size):
    """Checks chunk is a sequence of expected size or a tuple of same."""
    if reduced is None:
        return
    is_tuple = isinstance(reduced, tuple)
    if not is_tuple:
        reduced = (reduced,)
    if any(isinstance(r, tuple) or not hasattr(r, "__iter__") for r in reduced):
            "reduce_func returned %r. Expected sequence(s) of length %d."
            % (reduced if is_tuple else reduced[0], chunk_size)
        )
    if any(_num_samples(r) != chunk_size for r in reduced):
        actual_size = tuple(_num_samples(r) for r in reduced)
            "reduce_func returned object of length %s. "
            "Expected same length as input: %d."
            % (actual_size if is_tuple else actual_size[0], chunk_size)
        )

def _precompute_metric_params(X, Y, metric=None, **kwds):
    """Precompute data-derived metric parameters if not provided."""
    if metric == "seuclidean" and "V" not in kwds:
        if X is Y:
            V = np.var(X, axis=0, ddof=1)
        else:
                "The 'V' parameter is required for the seuclidean metric "
                "when Y is passed."
            )
        return {"V": V}
    if metric == "mahalanobis" and "VI" not in kwds:
        if X is Y:
            VI = np.linalg.inv(np.cov(X.T)).T
        else:
                "The 'VI' parameter is required for the mahalanobis metric "
            )
        return {"VI": VI}
    return {}

def pairwise_distances_chunked(
    X,
    Y=None,
    *,
    reduce_func=None,
    metric="euclidean",
    n_jobs=None,
    **kwds,
):
    """Generate a distance matrix chunk by chunk with optional reduction.

    In cases where not all of a pairwise distance matrix needs to be
    stored at once, this is used to calculate pairwise distances in
    ``working_memory``-sized chunks.  If ``reduce_func`` is given, it is
    run on each chunk and its return values are concatenated into lists,
    arrays or sparse matrices.

    X : {array-like, sparse matrix} of shape (n_samples_X, n_samples_X) or \
            (n_samples_X, n_features)
        Array of pairwise distances between samples, or a feature array.
        The shape the array should be (n_samples_X, n_samples_X) if
        metric='precomputed' and (n_samples_X, n_features) otherwise.

        An optional second feature array. Only allowed if
        metric != "precomputed".

    reduce_func : callable, default=None
        The function which is applied on each chunk of the distance matrix,
        reducing it to needed values.  ``reduce_func(D_chunk, start)``
        is called repeatedly, where ``D_chunk`` is a contiguous vertical
        slice of the pairwise distance matrix, starting at row ``start``.
        It should return one of: None; an array, a list, or a sparse matrix
        of length ``D_chunk.shape[0]``; or a tuple of such objects.
        Returning None is useful for in-place operations, rather than
        reductions.

        If None, pairwise_distances_chunked returns a generator of vertical
        chunks of the distance matrix.

        allowed by scipy.spatial.distance.pdist for its metric parameter,
        or a metric listed in pairwise.PAIRWISE_DISTANCE_FUNCTIONS.
        If metric is "precomputed", X is assumed to be a distance matrix.
        Alternatively, if metric is a callable function, it is called on
        each pair of instances (rows) and the resulting value recorded.
        The callable should take two arrays from X as input and return a
        value indicating the distance between them.

        The number of jobs to use for the computation. This works by
        breaking down the pairwise matrix into n_jobs even slices and
        computing them in parallel.

    working_memory : float, default=None
        The sought maximum memory for temporary distance matrix chunks.
        When None (default), the value of
        ``sklearn.get_config()['working_memory']`` is used.

    **kwds : optional keyword parameters
        Any further parameters are passed directly to the distance function.
        If using a scipy.spatial.distance metric, the parameters are still
        metric dependent. See the scipy docs for usage examples.

    Yields
    ------
    D_chunk : {ndarray, sparse matrix}
        A contiguous slice of distance matrix, optionally processed by
        ``reduce_func``.

    Examples
    --------
    Without reduce_func:

    >>> from sklearn.metrics import pairwise_distances_chunked
    >>> X = np.random.RandomState(0).rand(5, 3)
    >>> D_chunk = next(pairwise_distances_chunked(X))
    >>> D_chunk
    array([[0.  ..., 0.29..., 0.41..., 0.19..., 0.57...],
           [0.29..., 0.  ..., 0.57..., 0.41..., 0.76...],
           [0.41..., 0.57..., 0.  ..., 0.44..., 0.90...],
           [0.19..., 0.41..., 0.44..., 0.  ..., 0.51...],
           [0.57..., 0.76..., 0.90..., 0.51..., 0.  ...]])

    Retrieve all neighbors and average distance within radius r:

    >>> r = .2
    >>> def reduce_func(D_chunk, start):
    ...     neigh = [np.flatnonzero(d < r) for d in D_chunk]
    ...     avg_dist = (D_chunk * (D_chunk < r)).mean(axis=1)
    ...     return neigh, avg_dist
    >>> gen = pairwise_distances_chunked(X, reduce_func=reduce_func)
    >>> neigh, avg_dist = next(gen)
    >>> neigh
    [array([0, 3]), array([1]), array([2]), array([0, 3]), array([4])]
    >>> avg_dist
    array([0.039..., 0.        , 0.        , 0.039..., 0.        ])

    Where r is defined per sample, we need to make use of ``start``:

    >>> r = [.2, .4, .4, .3, .1]
    ...     neigh = [np.flatnonzero(d < r[i])
    ...              for i, d in enumerate(D_chunk, start)]
    ...     return neigh
    >>> neigh = next(pairwise_distances_chunked(X, reduce_func=reduce_func))
    >>> neigh
    [array([0, 3]), array([0, 1]), array([2]), array([0, 3]), array([4])]

    Force row-by-row generation by reducing ``working_memory``:

    >>> gen = pairwise_distances_chunked(X, reduce_func=reduce_func,
    ...                                  working_memory=0)
    >>> next(gen)
    [array([0, 3])]
    [array([0, 1])]
    """
    n_samples_X = _num_samples(X)
    if metric == "precomputed":
        slices = (slice(0, n_samples_X),)
    else:
            Y = X
        # We get as many rows as possible within our working_memory budget to
        # store len(Y) distances in each row of output.
        #
        # Note:
        #  - this will get at least 1 row, even if 1 row of distances will
        #  - this does not account for any temporary memory usage while
        #    calculating distances (e.g. difference of vectors in manhattan
        #    distance.
            row_bytes=8 * _num_samples(Y),
            max_n_rows=n_samples_X,
            working_memory=working_memory,
        )
        slices = gen_batches(n_samples_X, chunk_n_rows)

    # precompute data-derived metric params
    params = _precompute_metric_params(X, Y, metric=metric, **kwds)
    kwds.update(**params)

    for sl in slices:
        if sl.start == 0 and sl.stop == n_samples_X:
            X_chunk = X  # enable optimised paths for X is Y
        else:
            X_chunk = X[sl]
        D_chunk = pairwise_distances(X_chunk, Y, metric=metric, n_jobs=n_jobs, **kwds)
        if (X is Y or Y is None) and PAIRWISE_DISTANCE_FUNCTIONS.get(
            metric, None
        ) is euclidean_distances:
            # zeroing diagonal, taking care of aliases of "euclidean",
            # i.e. "l2"
            D_chunk.flat[sl.start :: _num_samples(X) + 1] = 0
        if reduce_func is not None:
            chunk_size = D_chunk.shape[0]
            D_chunk = reduce_func(D_chunk, sl.start)
            _check_chunk_size(D_chunk, chunk_size)
        yield D_chunk

def pairwise_distances(
    X,
    Y=None,
    *,
    **kwds,
):
    """Compute the distance matrix from a feature array X and optional Y.

    This function takes one or two feature arrays or a distance matrix, and returns
    a distance matrix.

    - If `X` is a feature array, of shape (n_samples_X, n_features), and:

      - `Y` is `None` and `metric` is not 'precomputed', the pairwise distances
        between `X` and itself are returned.
      - `Y` is a feature array of shape (n_samples_Y, n_features), the pairwise
        distances between `X` and `Y` is returned.

    - If `X` is a distance matrix, of shape (n_samples_X, n_samples_X), `metric`
      should be 'precomputed'. `Y` is thus ignored and `X` is returned as is.

    If the input is a collection of non-numeric data (e.g. a list of strings or a
    boolean array), a custom metric must be passed.

    This method provides a safe way to take a distance matrix as input, while
    preserving compatibility with many other algorithms that take a vector
    array.

    Valid values for metric are:

    - From scikit-learn: ['cityblock', 'cosine', 'euclidean', 'l1', 'l2',
      'manhattan', 'nan_euclidean']. All metrics support sparse matrix
      inputs except 'nan_euclidean'.

    - From scipy.spatial.distance: ['braycurtis', 'canberra', 'chebyshev',
      'correlation', 'dice', 'hamming', 'jaccard', 'kulsinski', 'mahalanobis',
      'minkowski', 'rogerstanimoto', 'russellrao', 'seuclidean',
      'sokalmichener', 'sokalsneath', 'sqeuclidean', 'yule']
      See the documentation for scipy.spatial.distance for details on these
      metrics. These metrics do not support sparse matrix inputs.

    .. note::
        `'kulsinski'` is deprecated from SciPy 1.9 and will be removed in SciPy 1.11.

    .. note::
        `'matching'` has been removed in SciPy 1.9 (use `'hamming'` instead).

    Note that in the case of 'cityblock', 'cosine' and 'euclidean' (which are
    valid scipy.spatial.distance metrics), the scikit-learn implementation
    will be used, which is faster and has support for sparse matrices (except
    for 'cityblock'). For a verbose description of the metrics from
    scikit-learn, see :func:`sklearn.metrics.pairwise.distance_metrics`
    function.

        The shape of the array should be (n_samples_X, n_samples_X) if
        metric == "precomputed" and (n_samples_X, n_features) otherwise.

        allowed by scipy.spatial.distance.pdist for its metric parameter, or
        a metric listed in ``pairwise.PAIRWISE_DISTANCE_FUNCTIONS``.

        down the pairwise matrix into n_jobs even slices and computing them
        using multithreading.

        The "euclidean" and "cosine" metrics rely heavily on BLAS which is already
        multithreaded. So, increasing `n_jobs` would likely cause oversubscription
        and quickly degrade performance.

        Whether to raise an error on np.inf, np.nan, pd.NA in array. Ignored
        for a metric listed in ``pairwise.PAIRWISE_DISTANCE_FUNCTIONS``. The

           in 1.8.

    Returns
    -------
    D : ndarray of shape (n_samples_X, n_samples_X) or \
            (n_samples_X, n_samples_Y)
        A distance matrix D such that D_{i, j} is the distance between the
        ith and jth vectors of the given matrix X, if Y is None.
        If Y is not None, then D_{i, j} is the distance between the ith array
        from X and the jth array from Y.

    See Also
    --------
    pairwise_distances_chunked : Performs the same calculation as this
        function, but returns a generator of chunks of the distance matrix, in
        order to limit memory usage.
    sklearn.metrics.pairwise.paired_distances : Computes the distances between
        corresponding elements of two arrays.

    Notes
    -----
    If metric is a callable, no restrictions are placed on `X` and `Y` dimensions.

    Examples
    --------
    >>> from sklearn.metrics.pairwise import pairwise_distances
    >>> pairwise_distances(X, Y, metric='sqeuclidean')
    array([[1., 2.],
           [2., 1.]])
    """

        X, _ = check_pairwise_arrays(
            X, Y, precomputed=True, ensure_all_finite=ensure_all_finite
        )

        whom = (
            "`pairwise_distances`. Precomputed distance "
            " need to have non-negative values."
        )
        check_non_negative(X, whom=whom)
        return X
    elif metric in PAIRWISE_DISTANCE_FUNCTIONS:
        func = PAIRWISE_DISTANCE_FUNCTIONS[metric]
        func = partial(
            _pairwise_callable,
            **kwds,
        )
    else:
        if issparse(X) or issparse(Y):
            raise TypeError("scipy distance metrics do not support sparse matrices.")

        dtype = bool if metric in PAIRWISE_BOOLEAN_FUNCTIONS else "infer_float"

        if dtype is bool and (X.dtype != bool or (Y is not None and Y.dtype != bool)):
            msg = "Data was converted to boolean for metric %s" % metric
            warnings.warn(msg, DataConversionWarning)

        X, Y = check_pairwise_arrays(
            X, Y, dtype=dtype, ensure_all_finite=ensure_all_finite
        )

        # precompute data-derived metric params
        params = _precompute_metric_params(X, Y, metric=metric, **kwds)
        kwds.update(**params)

        if effective_n_jobs(n_jobs) == 1 and X is Y:
            return distance.squareform(distance.pdist(X, metric=metric, **kwds))
        func = partial(distance.cdist, metric=metric, **kwds)

    return _parallel_pairwise(X, Y, func, n_jobs, **kwds)

def kernel_metrics():
    """Valid metrics for pairwise_kernels.

    It exists, however, to allow for a verbose description of the mapping for

      ===============   ========================================
      metric            Function
      'additive_chi2'   sklearn.pairwise.additive_chi2_kernel
      'chi2'            sklearn.pairwise.chi2_kernel
      'linear'          sklearn.pairwise.linear_kernel
      'poly'            sklearn.pairwise.polynomial_kernel
      'polynomial'      sklearn.pairwise.polynomial_kernel
      'rbf'             sklearn.pairwise.rbf_kernel
      'laplacian'       sklearn.pairwise.laplacian_kernel
      'sigmoid'         sklearn.pairwise.sigmoid_kernel
      'cosine'          sklearn.pairwise.cosine_similarity

    Returns
    -------
    kernel_metrics : dict
        Returns valid metrics for pairwise_kernels.
    """
    return PAIRWISE_KERNEL_FUNCTIONS

def pairwise_kernels(
    X, Y=None, metric="linear", *, filter_params=False, n_jobs=None, **kwds
):
    """Compute the kernel between arrays X and optional array Y.

    This function takes one or two feature arrays or a kernel matrix, and returns
    a kernel matrix.

      - `Y` is `None` and `metric` is not 'precomputed', the pairwise kernels
        kernels between `X` and `Y` is returned.

    - If `X` is a kernel matrix, of shape (n_samples_X, n_samples_X), `metric`

    This method provides a safe way to take a kernel matrix as input, while
    array.

        ['additive_chi2', 'chi2', 'linear', 'poly', 'polynomial', 'rbf',
        'laplacian', 'sigmoid', 'cosine']

    X : {array-like, sparse matrix}  of shape (n_samples_X, n_samples_X) or \
        Array of pairwise kernels between samples, or a feature array.

        A second feature array only if X has shape (n_samples_X, n_features).

    metric : str or callable, default="linear"
        in ``pairwise.PAIRWISE_KERNEL_FUNCTIONS``.
        should take two rows from X as input and return the corresponding
        kernel value as a single number. This means that callables from
        :mod:`sklearn.metrics.pairwise` are not allowed, as they operate on
        matrices, not single samples. Use the string identifying the kernel
        instead.

    filter_params : bool, default=False
        Whether to filter invalid parameters or not.

        Any further parameters are passed directly to the kernel function.

    Returns
    -------
    K : ndarray of shape (n_samples_X, n_samples_X) or (n_samples_X, n_samples_Y)
        A kernel matrix K such that K_{i, j} is the kernel between the
        If Y is not None, then K_{i, j} is the kernel between the ith array

    Notes
    -----

    Examples
    --------
    >>> from sklearn.metrics.pairwise import pairwise_kernels
    >>> pairwise_kernels(X, Y, metric='linear')
           [1., 2.]])
    """
    # import GPKernel locally to prevent circular imports
    from ..gaussian_process.kernels import Kernel as GPKernel

        X, _ = check_pairwise_arrays(X, Y, precomputed=True)
        return X
    elif isinstance(metric, GPKernel):
        func = metric.__call__
    elif metric in PAIRWISE_KERNEL_FUNCTIONS:
        if filter_params:
            kwds = {k: kwds[k] for k in kwds if k in KERNEL_PARAMS[metric]}
        func = PAIRWISE_KERNEL_FUNCTIONS[metric]
        func = partial(_pairwise_callable, metric=metric, **kwds)

def _check_rows_and_columns(a, b):
    """Unpacks the row and column arrays and checks their shape."""
    check_consistent_length(*a)
    check_consistent_length(*b)
    checks = lambda x: check_array(x, ensure_2d=False)
    a_rows, a_cols = map(checks, a)
    b_rows, b_cols = map(checks, b)
    return a_rows, a_cols, b_rows, b_cols

def _jaccard(a_rows, a_cols, b_rows, b_cols):
    """Jaccard coefficient on the elements of the two biclusters."""
    intersection = (a_rows * b_rows).sum() * (a_cols * b_cols).sum()

    a_size = a_rows.sum() * a_cols.sum()
    b_size = b_rows.sum() * b_cols.sum()

    return intersection / (a_size + b_size - intersection)

def _pairwise_similarity(a, b, similarity):
    """Computes pairwise similarity matrix.

    result[i, j] is the Jaccard coefficient of a's bicluster i and b's
    bicluster j.

    """
    a_rows, a_cols, b_rows, b_cols = _check_rows_and_columns(a, b)
    n_a = a_rows.shape[0]
    n_b = b_rows.shape[0]
    result = np.array(
        [
            [similarity(a_rows[i], a_cols[i], b_rows[j], b_cols[j]) for j in range(n_b)]
            for i in range(n_a)
        ]
    )

def consensus_score(a, b, *, similarity="jaccard"):
    """The similarity of two sets of biclusters.

    Similarity between individual biclusters is computed. Then the best
    matching between sets is found by solving a linear sum assignment problem,
    using a modified Jonker-Volgenant algorithm.
    The final score is the sum of similarities divided by the size of
    the larger set.

    Read more in the :ref:`User Guide <biclustering>`.

    a : tuple (rows, columns)
        Tuple of row and column indicators for a set of biclusters.

    b : tuple (rows, columns)
        Another set of biclusters like ``a``.

    similarity : 'jaccard' or callable, default='jaccard'
        May be the string "jaccard" to use the Jaccard coefficient, or
        any function that takes four arguments, each of which is a 1d
        indicator vector: (a_rows, a_columns, b_rows, b_columns).

    Returns
    -------
    consensus_score : float
       Consensus score, a non-negative value, sum of similarities
       divided by size of larger set.

    See Also
    --------
    scipy.optimize.linear_sum_assignment : Solve the linear sum assignment problem.

    * Hochreiter, Bodenhofer, et. al., 2010. `FABIA: factor analysis
      for bicluster acquisition
      <https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2881408/>`__.

    Examples
    --------
    >>> from sklearn.metrics import consensus_score
    >>> a = ([[True, False], [False, True]], [[False, True], [True, False]])
    >>> b = ([[False, True], [True, False]], [[True, False], [False, True]])
    >>> consensus_score(a, b, similarity='jaccard')
    1.0
    """
    if similarity == "jaccard":
        similarity = _jaccard
    matrix = _pairwise_similarity(a, b, similarity)
    row_indices, col_indices = linear_sum_assignment(1.0 - matrix)
    n_a = len(a[0])
    n_b = len(b[0])
    return float(matrix[row_indices, col_indices].sum() / max(n_a, n_b))

def check_clusterings(labels_true, labels_pred):
    """Check that the labels arrays are 1D and of same dimension.

    labels_true : array-like of shape (n_samples,)
        The true labels.

    labels_pred : array-like of shape (n_samples,)
        The predicted labels.
    """
    labels_true = check_array(
        labels_true,
        ensure_min_samples=0,
    )

    labels_pred = check_array(
        labels_pred,
    )

    type_label = type_of_target(labels_true)
    type_pred = type_of_target(labels_pred)

    if "continuous" in (type_pred, type_label):
        msg = (
            "Clustering metrics expects discrete values but received"
            f" {type_label} values for label, and {type_pred} values "
            "for target"
        )
        warnings.warn(msg, UserWarning)

    # input checks
    if labels_true.ndim != 1:
        raise ValueError("labels_true must be 1D: shape is %r" % (labels_true.shape,))
    if labels_pred.ndim != 1:
        raise ValueError("labels_pred must be 1D: shape is %r" % (labels_pred.shape,))
    check_consistent_length(labels_true, labels_pred)

    return labels_true, labels_pred

def _generalized_average(U, V, average_method):
    """Return a particular mean of two numbers."""
    if average_method == "min":
        return min(U, V)
    elif average_method == "geometric":
        return np.sqrt(U * V)
    elif average_method == "arithmetic":
        return np.mean([U, V])
    elif average_method == "max":
        return max(U, V)
    else:
            "'average_method' must be 'min', 'geometric', 'arithmetic', or 'max'"
        )

def contingency_matrix(
    labels_true, labels_pred, *, eps=None, sparse=False, dtype=np.int64
):
    """Build a contingency matrix describing the relationship between labels.

    Read more in the :ref:`User Guide <contingency_matrix>`.

        Ground truth class labels to be used as a reference.

        Cluster labels to evaluate.

    eps : float, default=None
        If a float, that value is added to all values in the contingency
        matrix. This helps to stop NaN propagation.
        If ``None``, nothing is adjusted.

        If `True`, return a sparse CSR contingency matrix. If `eps` is not
        `None` and `sparse` is `True` will raise ValueError.

    dtype : numeric type, default=np.int64
        Output dtype. Ignored if `eps` is not `None`.

    Returns
    -------
    contingency : {array-like, sparse}, shape=[n_classes_true, n_classes_pred]
        Matrix :math:`C` such that :math:`C_{i, j}` is the number of samples in
        true class :math:`i` and in predicted class :math:`j`. If
        ``eps is None``, the dtype of this array will be integer unless set
        otherwise with the ``dtype`` argument. If ``eps`` is given, the dtype
        will be float.
        Will be a ``sklearn.sparse.csr_matrix`` if ``sparse=True``.

    Examples
    --------
    >>> from sklearn.metrics.cluster import contingency_matrix
    >>> labels_true = [0, 0, 1, 1, 2, 2]
    >>> labels_pred = [1, 0, 2, 1, 0, 2]
    >>> contingency_matrix(labels_true, labels_pred)
    array([[1, 1, 0],
           [0, 1, 1],
           [1, 0, 1]])
    """

    if eps is not None and sparse:
        raise ValueError("Cannot set 'eps' when sparse=True")

    classes, class_idx = np.unique(labels_true, return_inverse=True)
    clusters, cluster_idx = np.unique(labels_pred, return_inverse=True)
    n_classes = classes.shape[0]
    n_clusters = clusters.shape[0]
    # Using coo_matrix to accelerate simple histogram calculation,
    # i.e. bins are consecutive integers
    # Currently, coo_matrix is faster than histogram2d for simple cases
    contingency = sp.coo_matrix(
        (np.ones(class_idx.shape[0]), (class_idx, cluster_idx)),
        shape=(n_classes, n_clusters),
    )
    if sparse:
        contingency = contingency.tocsr()
        contingency.sum_duplicates()
    else:
        contingency = contingency.toarray()
        if eps is not None:
            # don't use += as contingency is integer
            contingency = contingency + eps
    return contingency

def pair_confusion_matrix(labels_true, labels_pred):
    """Pair confusion matrix arising from two clusterings.

    The pair confusion matrix :math:`C` computes a 2 by 2 similarity matrix
    between two clusterings by considering all pairs of samples and counting
    pairs that are assigned into the same or into different clusters under
    the true and predicted clusterings [1]_.

    Considering a pair of samples that is clustered together a positive pair,
    then as in binary classification the count of true negatives is
    :math:`C_{00}`, false negatives is :math:`C_{10}`, true positives is
    :math:`C_{11}` and false positives is :math:`C_{01}`.

    Read more in the :ref:`User Guide <pair_confusion_matrix>`.

    labels_true : array-like of shape (n_samples,), dtype=integral

    labels_pred : array-like of shape (n_samples,), dtype=integral

    Returns
    -------
    C : ndarray of shape (2, 2), dtype=np.int64
        The contingency matrix.

    See Also
    --------
    sklearn.metrics.rand_score : Rand Score.
    sklearn.metrics.adjusted_rand_score : Adjusted Rand Score.
    sklearn.metrics.adjusted_mutual_info_score : Adjusted Mutual Information.

    .. [1] :doi:`Hubert, L., Arabie, P. "Comparing partitions."
           Journal of Classification 2, 193–218 (1985).
           <10.1007/BF01908075>`

    Examples
    --------
    Perfectly matching labelings have all non-zero entries on the
    diagonal regardless of actual label values:

      >>> from sklearn.metrics.cluster import pair_confusion_matrix
      >>> pair_confusion_matrix([0, 0, 1, 1], [1, 1, 0, 0])
      array([[8, 0],
             [0, 4]]...

    Labelings that assign all classes members to the same clusters
    are complete but may be not always pure, hence penalized, and
    have some off-diagonal non-zero entries:

      >>> pair_confusion_matrix([0, 0, 1, 2], [0, 0, 1, 1])
      array([[8, 2],
             [0, 2]]...

    Note that the matrix is not symmetric.
    """
    labels_true, labels_pred = check_clusterings(labels_true, labels_pred)
    n_samples = np.int64(labels_true.shape[0])

    # Computation using the contingency data
    contingency = contingency_matrix(
        labels_true, labels_pred, sparse=True, dtype=np.int64
    )
    n_c = np.ravel(contingency.sum(axis=1))
    n_k = np.ravel(contingency.sum(axis=0))
    sum_squares = (contingency.data**2).sum()
    C = np.empty((2, 2), dtype=np.int64)
    C[1, 1] = sum_squares - n_samples
    C[0, 1] = contingency.dot(n_k).sum() - sum_squares
    C[1, 0] = contingency.transpose().dot(n_c).sum() - sum_squares
    C[0, 0] = n_samples**2 - C[0, 1] - C[1, 0] - sum_squares
    return C

def rand_score(labels_true, labels_pred):
    """Rand index.

    The Rand Index computes a similarity measure between two clusterings
    by considering all pairs of samples and counting pairs that are
    assigned in the same or different clusters in the predicted and
    true clusterings [1]_ [2]_.

    The raw RI score [3]_ is:

        RI = (number of agreeing pairs) / (number of pairs)

    Read more in the :ref:`User Guide <rand_score>`.

    Returns
    -------
    RI : float
       Similarity score between 0.0 and 1.0, inclusive, 1.0 stands for
       perfect match.

    See Also
    --------
    adjusted_rand_score: Adjusted Rand Score.
    adjusted_mutual_info_score: Adjusted Mutual Information.

       Journal of Classification 2, 193–218 (1985).
       <10.1007/BF01908075>`.

    .. [2] `Wikipedia: Simple Matching Coefficient
        <https://en.wikipedia.org/wiki/Simple_matching_coefficient>`_

    .. [3] `Wikipedia: Rand Index <https://en.wikipedia.org/wiki/Rand_index>`_

    Examples
    --------
    Perfectly matching labelings have a score of 1 even

      >>> from sklearn.metrics.cluster import rand_score
      >>> rand_score([0, 0, 1, 1], [1, 1, 0, 0])
      1.0

    are complete but may not always be pure, hence penalized:

      >>> rand_score([0, 0, 1, 2], [0, 0, 1, 1])
      0.83...
    """
    contingency = pair_confusion_matrix(labels_true, labels_pred)
    numerator = contingency.diagonal().sum()
    denominator = contingency.sum()

    if numerator == denominator or denominator == 0:
        # Special limit cases: no clustering since the data is not split;
        # or trivial clustering where each document is assigned a unique
        # cluster. These are perfect matches hence return 1.0.
        return 1.0

    return float(numerator / denominator)

def adjusted_rand_score(labels_true, labels_pred):
    """Rand index adjusted for chance.

    true clusterings.

    The raw RI score is then "adjusted for chance" into the ARI score
    using the following scheme::

        ARI = (RI - Expected_RI) / (max(RI) - Expected_RI)

    The adjusted Rand index is thus ensured to have a value close to
    0.0 for random labeling independently of the number of clusters and
    samples and exactly 1.0 when the clusterings are identical (up to
    a permutation). The adjusted Rand index is bounded below by -0.5 for
    especially discordant clusterings.

    ARI is a symmetric measure::

        adjusted_rand_score(a, b) == adjusted_rand_score(b, a)

    Read more in the :ref:`User Guide <adjusted_rand_score>`.

    labels_true : array-like of shape (n_samples,), dtype=int

    labels_pred : array-like of shape (n_samples,), dtype=int

    Returns
    -------
    ARI : float
       Similarity score between -0.5 and 1.0. Random labelings have an ARI
       close to 0.0. 1.0 stands for perfect match.

    See Also
    --------
    adjusted_mutual_info_score : Adjusted Mutual Information.

    .. [Hubert1985] L. Hubert and P. Arabie, Comparing Partitions,
      Journal of Classification 1985
      https://link.springer.com/article/10.1007%2FBF01908075

    .. [Steinley2004] D. Steinley, Properties of the Hubert-Arabie
      adjusted Rand index, Psychological Methods 2004

    .. [wk] https://en.wikipedia.org/wiki/Rand_index#Adjusted_Rand_index

    .. [Chacon] :doi:`Minimum adjusted Rand index for two clusterings of a given size,
      2022, J. E. Chacón and A. I. Rastrojo <10.1007/s11634-022-00491-w>`

    Examples
    --------

      >>> from sklearn.metrics.cluster import adjusted_rand_score
      >>> adjusted_rand_score([0, 0, 1, 1], [0, 0, 1, 1])
      1.0
      >>> adjusted_rand_score([0, 0, 1, 1], [1, 1, 0, 0])
      1.0

    are complete but may not always be pure, hence penalized::

      >>> adjusted_rand_score([0, 0, 1, 2], [0, 0, 1, 1])
      0.57...

    ARI is symmetric, so labelings that have pure clusters with members
    coming from the same classes but unnecessary splits are penalized::

      >>> adjusted_rand_score([0, 0, 1, 1], [0, 0, 1, 2])
      0.57...

    If classes members are completely split across different clusters, the
    assignment is totally incomplete, hence the ARI is very low::

      >>> adjusted_rand_score([0, 0, 0, 0], [0, 1, 2, 3])
      0.0

    ARI may take a negative value for especially discordant labelings that
    are a worse choice than the expected value of random labels::

      >>> adjusted_rand_score([0, 0, 1, 1], [0, 1, 0, 1])
      -0.5

    See :ref:`sphx_glr_auto_examples_cluster_plot_adjusted_for_chance_measures.py`
    for a more detailed example.
    """
    (tn, fp), (fn, tp) = pair_confusion_matrix(labels_true, labels_pred)
    # convert to Python integer types, to avoid overflow or underflow
    tn, fp, fn, tp = int(tn), int(fp), int(fn), int(tp)

    # Special cases: empty data or full agreement
    if fn == 0 and fp == 0:
        return 1.0

    return 2.0 * (tp * tn - fn * fp) / ((tp + fn) * (fn + tn) + (tp + fp) * (fp + tn))

def homogeneity_completeness_v_measure(labels_true, labels_pred, *, beta=1.0):
    """Compute the homogeneity and completeness and V-Measure scores at once.

    Those metrics are based on normalized conditional entropy measures of
    the clustering labeling to evaluate given the knowledge of a Ground
    Truth class labels of the same samples.

    A clustering result satisfies homogeneity if all of its clusters
    contain only data points which are members of a single class.

    A clustering result satisfies completeness if all the data points
    that are members of a given class are elements of the same cluster.

    Both scores have positive values between 0.0 and 1.0, larger values
    being desirable.

    Those 3 metrics are independent of the absolute values of the labels:
    a permutation of the class or cluster label values won't change the
    score values in any way.

    V-Measure is furthermore symmetric: swapping ``labels_true`` and
    ``label_pred`` will give the same score. This does not hold for
    homogeneity and completeness. V-Measure is identical to
    :func:`normalized_mutual_info_score` with the arithmetic averaging
    method.

    Read more in the :ref:`User Guide <homogeneity_completeness>`.

        Ratio of weight attributed to ``homogeneity`` vs ``completeness``.
        If ``beta`` is greater than 1, ``completeness`` is weighted more
        strongly in the calculation. If ``beta`` is less than 1,
        ``homogeneity`` is weighted more strongly.

    Returns
    -------
    homogeneity : float
        Score between 0.0 and 1.0. 1.0 stands for perfectly homogeneous labeling.

    completeness : float
        Score between 0.0 and 1.0. 1.0 stands for perfectly complete labeling.

    v_measure : float
        Harmonic mean of the first two.

    See Also
    --------
    homogeneity_score : Homogeneity metric of cluster labeling.
    completeness_score : Completeness metric of cluster labeling.
    v_measure_score : V-Measure (NMI with arithmetic mean option).

    Examples
    --------
    >>> from sklearn.metrics import homogeneity_completeness_v_measure
    >>> y_true, y_pred = [0, 0, 1, 1, 2, 2], [0, 0, 1, 2, 2, 2]
    >>> homogeneity_completeness_v_measure(y_true, y_pred)
    (0.71..., 0.77..., 0.73...)
    """

    if len(labels_true) == 0:
        return 1.0, 1.0, 1.0

    entropy_C = entropy(labels_true)
    entropy_K = entropy(labels_pred)

    contingency = contingency_matrix(labels_true, labels_pred, sparse=True)
    MI = mutual_info_score(None, None, contingency=contingency)

    homogeneity = MI / (entropy_C) if entropy_C else 1.0
    completeness = MI / (entropy_K) if entropy_K else 1.0

    if homogeneity + completeness == 0.0:
        v_measure_score = 0.0
    else:
        v_measure_score = (
            (1 + beta)
            * homogeneity
            * completeness
            / (beta * homogeneity + completeness)
        )

    return float(homogeneity), float(completeness), float(v_measure_score)

def homogeneity_score(labels_true, labels_pred):
    """Homogeneity metric of a cluster labeling given a ground truth.

    This metric is independent of the absolute values of the labels:
    score value in any way.

    This metric is not symmetric: switching ``label_true`` with ``label_pred``
    will return the :func:`completeness_score` which will be different in
    general.

    Returns
    -------
       Score between 0.0 and 1.0. 1.0 stands for perfectly homogeneous labeling.

    See Also
    --------

    .. [1] `Andrew Rosenberg and Julia Hirschberg, 2007. V-Measure: A
       conditional entropy-based external cluster evaluation measure
       <https://aclweb.org/anthology/D/D07/D07-1043.pdf>`_

    Examples
    --------

    Perfect labelings are homogeneous::

      >>> from sklearn.metrics.cluster import homogeneity_score
      >>> homogeneity_score([0, 0, 1, 1], [1, 1, 0, 0])
      1.0

    Non-perfect labelings that further split classes into more clusters can be
    perfectly homogeneous::

      >>> print("%.6f" % homogeneity_score([0, 0, 1, 1], [0, 0, 1, 2]))
      1.000000
      >>> print("%.6f" % homogeneity_score([0, 0, 1, 1], [0, 1, 2, 3]))
      1.000000

    Clusters that include samples from different classes do not make for an
    homogeneous labeling::

      >>> print("%.6f" % homogeneity_score([0, 0, 1, 1], [0, 1, 0, 1]))
      0.0...
      >>> print("%.6f" % homogeneity_score([0, 0, 1, 1], [0, 0, 0, 0]))
      0.0...
    """
    return homogeneity_completeness_v_measure(labels_true, labels_pred)[0]

def completeness_score(labels_true, labels_pred):
    """Compute completeness metric of a cluster labeling given a ground truth.

    will return the :func:`homogeneity_score` which will be different in
    general.

    Returns
    -------
       Score between 0.0 and 1.0. 1.0 stands for perfectly complete labeling.

    See Also
    --------

    Examples
    --------

    Perfect labelings are complete::

      >>> from sklearn.metrics.cluster import completeness_score
      >>> completeness_score([0, 0, 1, 1], [1, 1, 0, 0])
      1.0

    Non-perfect labelings that assign all classes members to the same clusters
    are still complete::

      >>> print(completeness_score([0, 0, 1, 1], [0, 0, 0, 0]))
      1.0
      >>> print(completeness_score([0, 1, 2, 3], [0, 0, 1, 1]))
      0.999...

    If classes members are split across different clusters, the
    assignment cannot be complete::

      >>> print(completeness_score([0, 0, 1, 1], [0, 1, 0, 1]))
      0.0
      >>> print(completeness_score([0, 0, 0, 0], [0, 1, 2, 3]))
      0.0
    """
    return homogeneity_completeness_v_measure(labels_true, labels_pred)[1]

def v_measure_score(labels_true, labels_pred, *, beta=1.0):
    """V-measure cluster labeling given a ground truth.

    This score is identical to :func:`normalized_mutual_info_score` with
    the ``'arithmetic'`` option for averaging.

    The V-measure is the harmonic mean between homogeneity and completeness::

        v = (1 + beta) * homogeneity * completeness
             / (beta * homogeneity + completeness)

    This metric is furthermore symmetric: switching ``label_true`` with
    ``label_pred`` will return the same score value. This can be useful to
    measure the agreement of two independent label assignments strategies
    on the same dataset when the real ground truth is not known.

    Returns
    -------

    See Also
    --------
    normalized_mutual_info_score : Normalized Mutual Information.

    Examples
    --------
    Perfect labelings are both homogeneous and complete, hence have score 1.0::

      >>> from sklearn.metrics.cluster import v_measure_score
      >>> v_measure_score([0, 0, 1, 1], [0, 0, 1, 1])
      1.0
      >>> v_measure_score([0, 0, 1, 1], [1, 1, 0, 0])
      1.0

    are complete but not homogeneous, hence penalized::

      >>> print("%.6f" % v_measure_score([0, 0, 1, 2], [0, 0, 1, 1]))
      0.8...
      >>> print("%.6f" % v_measure_score([0, 1, 2, 3], [0, 0, 1, 1]))
      0.66...

    Labelings that have pure clusters with members coming from the same
    classes are homogeneous but un-necessary splits harm completeness
    and thus penalize V-measure as well::

      >>> print("%.6f" % v_measure_score([0, 0, 1, 1], [0, 0, 1, 2]))
      0.8...
      >>> print("%.6f" % v_measure_score([0, 0, 1, 1], [0, 1, 2, 3]))
      0.66...

    If classes members are completely split across different clusters,
    the assignment is totally incomplete, hence the V-Measure is null::

      >>> print("%.6f" % v_measure_score([0, 0, 0, 0], [0, 1, 2, 3]))
      0.0...

    Clusters that include samples from totally different classes totally
    destroy the homogeneity of the labeling, hence::

      >>> print("%.6f" % v_measure_score([0, 0, 1, 1], [0, 0, 0, 0]))
      0.0...
    """
    return homogeneity_completeness_v_measure(labels_true, labels_pred, beta=beta)[2]

def mutual_info_score(labels_true, labels_pred, *, contingency=None):
    """Mutual Information between two clusterings.

    The Mutual Information is a measure of the similarity between two labels
    of the same data. Where :math:`|U_i|` is the number of the samples
    in cluster :math:`U_i` and :math:`|V_j|` is the number of the
    samples in cluster :math:`V_j`, the Mutual Information
    between clusterings :math:`U` and :math:`V` is given as:

    .. math::

        MI(U,V)=\\sum_{i=1}^{|U|} \\sum_{j=1}^{|V|} \\frac{|U_i\\cap V_j|}{N}
        \\log\\frac{N|U_i \\cap V_j|}{|U_i||V_j|}

    This metric is furthermore symmetric: switching :math:`U` (i.e
    ``label_true``) with :math:`V` (i.e. ``label_pred``) will return the
    same score value. This can be useful to measure the agreement of two
    independent label assignments strategies on the same dataset when the
    real ground truth is not known.

    Read more in the :ref:`User Guide <mutual_info_score>`.

        A clustering of the data into disjoint subsets, called :math:`U` in
        the above formula.

        A clustering of the data into disjoint subsets, called :math:`V` in

    contingency : {array-like, sparse matrix} of shape \
            (n_classes_true, n_classes_pred), default=None
        A contingency matrix given by the
        :func:`~sklearn.metrics.cluster.contingency_matrix` function. If value
        is ``None``, it will be computed, otherwise the given value is used,
        with ``labels_true`` and ``labels_pred`` ignored.

    Returns
    -------
    mi : float
       Mutual information, a non-negative value, measured in nats using the
       natural logarithm.

    See Also
    --------
    adjusted_mutual_info_score : Adjusted against chance Mutual Information.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.metrics import mutual_info_score
    >>> labels_true = [0, 1, 1, 0, 1, 0]
    >>> labels_pred = [0, 1, 0, 0, 1, 1]
    >>> mutual_info_score(labels_true, labels_pred)
    0.056...
    """
    if contingency is None:
        labels_true, labels_pred = check_clusterings(labels_true, labels_pred)
        contingency = contingency_matrix(labels_true, labels_pred, sparse=True)
    else:
        contingency = check_array(
            contingency,
            dtype=[int, np.int32, np.int64],
        )

    if isinstance(contingency, np.ndarray):
        # For an array
        nzx, nzy = np.nonzero(contingency)
        nz_val = contingency[nzx, nzy]
    else:
        # For a sparse matrix
        nzx, nzy, nz_val = sp.find(contingency)

    contingency_sum = contingency.sum()
    pi = np.ravel(contingency.sum(axis=1))
    pj = np.ravel(contingency.sum(axis=0))

    # Since MI <= min(H(X), H(Y)), any labelling with zero entropy, i.e. containing a
    # single cluster, implies MI = 0
    if pi.size == 1 or pj.size == 1:
        return 0.0

    log_contingency_nm = np.log(nz_val)
    contingency_nm = nz_val / contingency_sum
    # Don't need to calculate the full outer product, just for non-zeroes
    outer = pi.take(nzx).astype(np.int64, copy=False) * pj.take(nzy).astype(
        np.int64, copy=False
    )
    log_outer = -np.log(outer) + log(pi.sum()) + log(pj.sum())
    mi = (
        contingency_nm * (log_contingency_nm - log(contingency_sum))
        + contingency_nm * log_outer
    )
    mi = np.where(np.abs(mi) < np.finfo(mi.dtype).eps, 0.0, mi)
    return float(np.clip(mi.sum(), 0.0, None))

def adjusted_mutual_info_score(
    labels_true, labels_pred, *, average_method="arithmetic"
):
    """Adjusted Mutual Information between two clusterings.

    Adjusted Mutual Information (AMI) is an adjustment of the Mutual
    Information (MI) score to account for chance. It accounts for the fact that
    the MI is generally higher for two clusterings with a larger number of
    clusters, regardless of whether there is actually more information shared.
    For two clusterings :math:`U` and :math:`V`, the AMI is given as::

        AMI(U, V) = [MI(U, V) - E(MI(U, V))] / [avg(H(U), H(V)) - E(MI(U, V))]

    This metric is furthermore symmetric: switching :math:`U` (``label_true``)
    with :math:`V` (``labels_pred``) will return the same score value. This can
    be useful to measure the agreement of two independent label assignments
    strategies on the same dataset when the real ground truth is not known.

    Be mindful that this function is an order of magnitude slower than other
    metrics, such as the Adjusted Rand Index.

    labels_true : int array-like of shape (n_samples,)

    labels_pred : int array-like of shape (n_samples,)

    average_method : {'min', 'geometric', 'arithmetic', 'max'}, default='arithmetic'
        How to compute the normalizer in the denominator.

           The default value of ``average_method`` changed from 'max' to
           'arithmetic'.

    Returns
    -------
    ami: float (upperlimited by 1.0)
       The AMI returns a value of 1 when the two partitions are identical
       (ie perfectly matched). Random partitions (independent labellings) have
       an expected AMI around 0 on average hence can be negative. The value is
       in adjusted nats (based on the natural logarithm).

    See Also
    --------
    adjusted_rand_score : Adjusted Rand Index.
    mutual_info_score : Mutual Information (not adjusted for chance).

    .. [1] `Vinh, Epps, and Bailey, (2010). Information Theoretic Measures for
       Clusterings Comparison: Variants, Properties, Normalization and
       Correction for Chance, JMLR
       <http://jmlr.csail.mit.edu/papers/volume11/vinh10a/vinh10a.pdf>`_

    .. [2] `Wikipedia entry for the Adjusted Mutual Information
       <https://en.wikipedia.org/wiki/Adjusted_Mutual_Information>`_

    Examples
    --------

    Perfect labelings are both homogeneous and complete, hence have
    score 1.0::

      >>> from sklearn.metrics.cluster import adjusted_mutual_info_score
      >>> adjusted_mutual_info_score([0, 0, 1, 1], [0, 0, 1, 1])
      1.0
      >>> adjusted_mutual_info_score([0, 0, 1, 1], [1, 1, 0, 0])
      1.0

    the assignment is totally in-complete, hence the AMI is null::

      >>> adjusted_mutual_info_score([0, 0, 0, 0], [0, 1, 2, 3])
      0.0
    """
    n_samples = labels_true.shape[0]
    classes = np.unique(labels_true)
    clusters = np.unique(labels_pred)

    # Special limit cases: no clustering since the data is not split.
    # It corresponds to both labellings having zero entropy.
    # This is a perfect match hence return 1.0.
    if (
        classes.shape[0] == clusters.shape[0] == 1
        or classes.shape[0] == clusters.shape[0] == 0
    ):
        return 1.0
    # if there is only one class or one cluster return 0.0.
    elif classes.shape[0] == 1 or clusters.shape[0] == 1:
        return 0.0

    # Calculate the MI for the two clusterings
    mi = mutual_info_score(labels_true, labels_pred, contingency=contingency)
    # Calculate the expected value for the mutual information
    emi = expected_mutual_information(contingency, n_samples)
    # Calculate entropy for each labeling
    h_true, h_pred = entropy(labels_true), entropy(labels_pred)
    normalizer = _generalized_average(h_true, h_pred, average_method)
    denominator = normalizer - emi
    # Avoid 0.0 / 0.0 when expectation equals maximum, i.e. a perfect match.
    # normalizer should always be >= emi, but because of floating-point
    # representation, sometimes emi is slightly larger. Correct this
    # by preserving the sign.
    if denominator < 0:
        denominator = min(denominator, -np.finfo("float64").eps)
    else:
        denominator = max(denominator, np.finfo("float64").eps)
    # The same applies analogously to mi and emi.
    numerator = mi - emi
    if numerator < 0:
        numerator = min(numerator, -np.finfo("float64").eps)
    else:
        numerator = max(numerator, np.finfo("float64").eps)

def normalized_mutual_info_score(
):
    """Normalized Mutual Information between two clusterings.

    Normalized Mutual Information (NMI) is a normalization of the Mutual
    Information (MI) score to scale the results between 0 (no mutual
    information) and 1 (perfect correlation). In this function, mutual
    information is normalized by some generalized mean of ``H(labels_true)``
    and ``H(labels_pred))``, defined by the `average_method`.

    This measure is not adjusted for chance. Therefore
    :func:`adjusted_mutual_info_score` might be preferred.

        A clustering of the data into disjoint subsets.

           The default value of ``average_method`` changed from 'geometric' to

    Returns
    -------
    nmi : float
       Score between 0.0 and 1.0 in normalized nats (based on the natural
       logarithm). 1.0 stands for perfectly complete labeling.

    See Also
    --------
    adjusted_mutual_info_score : Adjusted Mutual Information (adjusted
        against chance).

    Examples
    --------

      >>> from sklearn.metrics.cluster import normalized_mutual_info_score
      >>> normalized_mutual_info_score([0, 0, 1, 1], [0, 0, 1, 1])
      1.0
      >>> normalized_mutual_info_score([0, 0, 1, 1], [1, 1, 0, 0])
      1.0

    the assignment is totally in-complete, hence the NMI is null::

      >>> normalized_mutual_info_score([0, 0, 0, 0], [0, 1, 2, 3])
      0.0
    """

    if (
    ):
        return 1.0

    contingency = contingency.astype(np.float64, copy=False)

    # At this point mi = 0 can't be a perfect match (the special case of a single
    # cluster has been dealt with before). Hence, if mi = 0, the nmi must be 0 whatever
    # the normalization.
    if mi == 0:
        return 0.0

    return float(mi / normalizer)

def fowlkes_mallows_score(labels_true, labels_pred, *, sparse="deprecated"):
    """Measure the similarity of two clusterings of a set of points.

    The Fowlkes-Mallows index (FMI) is defined as the geometric mean of
    the precision and recall::

        FMI = TP / sqrt((TP + FP) * (TP + FN))

    Where ``TP`` is the number of **True Positive** (i.e. the number of pairs of
    points that belong to the same cluster in both ``labels_true`` and
    ``labels_pred``), ``FP`` is the number of **False Positive** (i.e. the
    number of pairs of points that belong to the same cluster in
    ``labels_pred`` but not in ``labels_true``) and ``FN`` is the number of
    **False Negative** (i.e. the number of pairs of points that belong to the
    same cluster in ``labels_true`` but not in ``labels_pred``).

    The score ranges from 0 to 1. A high value indicates a good similarity
    between two clusters.

    Read more in the :ref:`User Guide <fowlkes_mallows_scores>`.

        Compute contingency matrix internally with sparse matrix.

            The ``sparse`` parameter is deprecated and will be removed in 1.9. It has
            no effect.

    Returns
    -------
       The resulting Fowlkes-Mallows score.

    .. [1] `E. B. Fowkles and C. L. Mallows, 1983. "A method for comparing two
       hierarchical clusterings". Journal of the American Statistical
       Association
       <https://www.tandfonline.com/doi/abs/10.1080/01621459.1983.10478008>`_

    .. [2] `Wikipedia entry for the Fowlkes-Mallows Index
           <https://en.wikipedia.org/wiki/Fowlkes-Mallows_index>`_

    Examples
    --------

      >>> from sklearn.metrics.cluster import fowlkes_mallows_score
      >>> fowlkes_mallows_score([0, 0, 1, 1], [0, 0, 1, 1])
      1.0
      >>> fowlkes_mallows_score([0, 0, 1, 1], [1, 1, 0, 0])
      1.0

    the assignment is totally random, hence the FMI is null::

      >>> fowlkes_mallows_score([0, 0, 0, 0], [0, 1, 2, 3])
      0.0
    """
    # TODO(1.9): remove the sparse parameter
    if sparse != "deprecated":
            "The 'sparse' parameter was deprecated in 1.7 and will be removed in 1.9. "
            "It has no effect. Leave it to its default value to silence this warning.",
        )

    (n_samples,) = labels_true.shape

    c = contingency_matrix(labels_true, labels_pred, sparse=True)
    c = c.astype(np.int64, copy=False)
    tk = np.dot(c.data, c.data) - n_samples
    pk = np.sum(np.asarray(c.sum(axis=0)).ravel() ** 2) - n_samples
    qk = np.sum(np.asarray(c.sum(axis=1)).ravel() ** 2) - n_samples
    return float(np.sqrt(tk / pk) * np.sqrt(tk / qk)) if tk != 0.0 else 0.0

def entropy(labels):
    """Calculate the entropy for a labeling.

    labels : array-like of shape (n_samples,), dtype=int
        The labels.

    Returns
    -------
    entropy : float
       The entropy for a labeling.

    Notes
    -----
    """
    xp, is_array_api_compliant, device_ = get_namespace_and_device(labels)
    labels_len = labels.shape[0] if is_array_api_compliant else len(labels)
    if labels_len == 0:
        return 1.0

    pi = xp.astype(xp.unique_counts(labels)[1], _max_precision_float_dtype(xp, device_))

    # single cluster => zero entropy
    if pi.size == 1:
        return 0.0

    pi_sum = xp.sum(pi)
    # log(a / b) should be calculated as log(a) - log(b) for
    # possible loss of precision
    # Always convert the result as a Python scalar (on CPU) instead of a device
    # specific scalar array.
    return float(-xp.sum((pi / pi_sum) * (xp.log(pi) - log(pi_sum))))

def check_number_of_labels(n_labels, n_samples):
    """Check that number of labels are valid.

    n_labels : int
        Number of labels.

        Number of samples.
    """
    if not 1 < n_labels < n_samples:
            "Number of labels is %d. Valid values are 2 to n_samples - 1 (inclusive)"
            % n_labels
        )

def silhouette_score(
    X, labels, *, metric="euclidean", sample_size=None, random_state=None, **kwds
):
    """Compute the mean Silhouette Coefficient of all samples.

    The Silhouette Coefficient is calculated using the mean intra-cluster
    distance (``a``) and the mean nearest-cluster distance (``b``) for each
    sample.  The Silhouette Coefficient for a sample is ``(b - a) / max(a,
    b)``.  To clarify, ``b`` is the distance between a sample and the nearest
    cluster that the sample is not a part of.
    Note that Silhouette Coefficient is only defined if number of labels
    is ``2 <= n_labels <= n_samples - 1``.

    This function returns the mean Silhouette Coefficient over all samples.
    To obtain the values for each sample, use :func:`silhouette_samples`.

    The best value is 1 and the worst value is -1. Values near 0 indicate
    overlapping clusters. Negative values generally indicate that a sample has
    been assigned to the wrong cluster, as a different cluster is more similar.

    Read more in the :ref:`User Guide <silhouette_coefficient>`.

    X : {array-like, sparse matrix} of shape (n_samples_a, n_samples_a) if metric == \
            "precomputed" or (n_samples_a, n_features) otherwise
        An array of pairwise distances between samples, or a feature array.

    labels : array-like of shape (n_samples,)
        Predicted labels for each sample.

        allowed by :func:`~sklearn.metrics.pairwise_distances`. If ``X`` is
        the distance array itself, use ``metric="precomputed"``.

    sample_size : int, default=None
        The size of the sample to use when computing the Silhouette Coefficient
        on a random subset of the data.
        If ``sample_size is None``, no sampling is used.

        Determines random number generation for selecting a subset of samples.
        Used when ``sample_size is not None``.

    Returns
    -------
    silhouette : float
        Mean Silhouette Coefficient for all samples.

    .. [1] `Peter J. Rousseeuw (1987). "Silhouettes: a Graphical Aid to the
       Interpretation and Validation of Cluster Analysis". Computational
       and Applied Mathematics 20: 53-65.
       <https://www.sciencedirect.com/science/article/pii/0377042787901257>`_

    .. [2] `Wikipedia entry on the Silhouette Coefficient
           <https://en.wikipedia.org/wiki/Silhouette_(clustering)>`_

    Examples
    --------
    >>> from sklearn.metrics import silhouette_score
    >>> X, y = make_blobs(random_state=42)
    >>> kmeans = KMeans(n_clusters=2, random_state=42)
    >>> silhouette_score(X, kmeans.fit_predict(X))
    0.49...
    """
    if sample_size is not None:
        X, labels = check_X_y(X, labels, accept_sparse=["csc", "csr"])
        indices = random_state.permutation(X.shape[0])[:sample_size]
        if metric == "precomputed":
            X, labels = X[indices].T[indices].T, labels[indices]
        else:
            X, labels = X[indices], labels[indices]
    return float(np.mean(silhouette_samples(X, labels, metric=metric, **kwds)))

def _silhouette_reduce(D_chunk, start, labels, label_freqs):
    """Accumulate silhouette statistics for vertical chunk of X.

    D_chunk : {array-like, sparse matrix} of shape (n_chunk_samples, n_samples)
        Precomputed distances for a chunk. If a sparse matrix is provided,
        only CSR format is accepted.
    start : int
        First index in the chunk.
        Corresponding cluster labels, encoded as {0, ..., n_clusters-1}.
    label_freqs : array-like
        Distribution of cluster labels in ``labels``.
    """
    n_chunk_samples = D_chunk.shape[0]
    # accumulate distances from each sample to each cluster
    cluster_distances = np.zeros(
        (n_chunk_samples, len(label_freqs)), dtype=D_chunk.dtype
    )

    if issparse(D_chunk):
        if D_chunk.format != "csr":
                "Expected CSR matrix. Please pass sparse matrix in CSR format."
            )
        for i in range(n_chunk_samples):
            indptr = D_chunk.indptr
            indices = D_chunk.indices[indptr[i] : indptr[i + 1]]
            sample_weights = D_chunk.data[indptr[i] : indptr[i + 1]]
            sample_labels = np.take(labels, indices)
            cluster_distances[i] += np.bincount(
                sample_labels, weights=sample_weights, minlength=len(label_freqs)
            )
    else:
            sample_weights = D_chunk[i]
            sample_labels = labels
            )

    # intra_index selects intra-cluster distances within cluster_distances
    end = start + n_chunk_samples
    intra_index = (np.arange(n_chunk_samples), labels[start:end])
    # intra_cluster_distances are averaged over cluster size outside this function
    intra_cluster_distances = cluster_distances[intra_index]
    # of the remaining distances we normalise and extract the minimum
    cluster_distances[intra_index] = np.inf
    cluster_distances /= label_freqs
    inter_cluster_distances = cluster_distances.min(axis=1)
    return intra_cluster_distances, inter_cluster_distances

def silhouette_samples(X, labels, *, metric="euclidean", **kwds):
    """Compute the Silhouette Coefficient for each sample.

    The Silhouette Coefficient is a measure of how well samples are clustered
    with samples that are similar to themselves. Clustering models with a high
    Silhouette Coefficient are said to be dense, where samples in the same
    cluster are similar to each other, and well separated, where samples in
    different clusters are not very similar to each other.

    b)``.
    is 2 ``<= n_labels <= n_samples - 1``.

    This function returns the Silhouette Coefficient for each sample.

    overlapping clusters.

        An array of pairwise distances between samples, or a feature array. If
        a sparse matrix is provided, CSR format should be favoured avoiding
        an additional copy.

        Label values for each sample.

        allowed by :func:`~sklearn.metrics.pairwise_distances`.
        If ``X`` is the distance array itself, use "precomputed" as the metric.
        Precomputed distance matrices must have 0 along the diagonal.

        If using a ``scipy.spatial.distance`` metric, the parameters are still

    Returns
    -------
    silhouette : array-like of shape (n_samples,)
        Silhouette Coefficients for each sample.

       <https://en.wikipedia.org/wiki/Silhouette_(clustering)>`_

    Examples
    --------
    >>> from sklearn.metrics import silhouette_samples
    >>> X, y = make_blobs(n_samples=50, random_state=42)
    >>> kmeans = KMeans(n_clusters=3, random_state=42)
    >>> labels = kmeans.fit_predict(X)
    >>> silhouette_samples(X, labels)
    array([...])
    """
    X, labels = check_X_y(X, labels, accept_sparse=["csr"])

    # Check for non-zero diagonal entries in precomputed distance matrix
        error_msg = ValueError(
            "The precomputed distance matrix contains non-zero "
            "elements on the diagonal. Use np.fill_diagonal(X, 0)."
        )
        if X.dtype.kind == "f":
            atol = _atol_for_type(X.dtype)

            if np.any(np.abs(X.diagonal()) > atol):
                raise error_msg
        elif np.any(X.diagonal() != 0):  # integral dtype
            raise error_msg

    le = LabelEncoder()
    labels = le.fit_transform(labels)
    n_samples = len(labels)
    label_freqs = np.bincount(labels)
    check_number_of_labels(len(le.classes_), n_samples)

    kwds["metric"] = metric
    reduce_func = functools.partial(
        _silhouette_reduce, labels=labels, label_freqs=label_freqs
    )
    results = zip(*pairwise_distances_chunked(X, reduce_func=reduce_func, **kwds))
    intra_clust_dists, inter_clust_dists = results
    intra_clust_dists = np.concatenate(intra_clust_dists)
    inter_clust_dists = np.concatenate(inter_clust_dists)

    denom = (label_freqs - 1).take(labels, mode="clip")
        intra_clust_dists /= denom

    sil_samples = inter_clust_dists - intra_clust_dists
        sil_samples /= np.maximum(intra_clust_dists, inter_clust_dists)
    # nan values are for clusters of size 1, and should be 0
    return np.nan_to_num(sil_samples)

def calinski_harabasz_score(X, labels):
    """Compute the Calinski and Harabasz score.

    It is also known as the Variance Ratio Criterion.

    The score is defined as ratio of the sum of between-cluster dispersion and
    of within-cluster dispersion.

    Read more in the :ref:`User Guide <calinski_harabasz_index>`.

        A list of ``n_features``-dimensional data points. Each row corresponds
        to a single data point.

    Returns
    -------
        The resulting Calinski-Harabasz score.

    .. [1] `T. Calinski and J. Harabasz, 1974. "A dendrite method for cluster
       analysis". Communications in Statistics
       <https://www.tandfonline.com/doi/abs/10.1080/03610927408827101>`_

    Examples
    --------
    >>> from sklearn.metrics import calinski_harabasz_score
    >>> X, _ = make_blobs(random_state=0)
    >>> kmeans = KMeans(n_clusters=3, random_state=0,).fit(X)
    >>> calinski_harabasz_score(X, kmeans.labels_)
    114.8...
    """
    X, labels = check_X_y(X, labels)

    n_samples, _ = X.shape
    n_labels = len(le.classes_)

    check_number_of_labels(n_labels, n_samples)

    extra_disp, intra_disp = 0.0, 0.0
    mean = np.mean(X, axis=0)
    for k in range(n_labels):
        cluster_k = X[labels == k]
        mean_k = np.mean(cluster_k, axis=0)
        extra_disp += len(cluster_k) * np.sum((mean_k - mean) ** 2)
        intra_disp += np.sum((cluster_k - mean_k) ** 2)

        1.0
        if intra_disp == 0.0
        else extra_disp * (n_samples - n_labels) / (intra_disp * (n_labels - 1.0))
    )

def davies_bouldin_score(X, labels):
    """Compute the Davies-Bouldin score.

    The score is defined as the average similarity measure of each cluster with
    its most similar cluster, where similarity is the ratio of within-cluster
    distances to between-cluster distances. Thus, clusters which are farther
    apart and less dispersed will result in a better score.

    The minimum score is zero, with lower values indicating better clustering.

    Read more in the :ref:`User Guide <davies-bouldin_index>`.

    Returns
    -------
    score: float
        The resulting Davies-Bouldin score.

    .. [1] Davies, David L.; Bouldin, Donald W. (1979).
       `"A Cluster Separation Measure"
       <https://ieeexplore.ieee.org/document/4766909>`__.
       IEEE Transactions on Pattern Analysis and Machine Intelligence.
       PAMI-1 (2): 224-227

    Examples
    --------
    >>> from sklearn.metrics import davies_bouldin_score
    >>> X = [[0, 1], [1, 1], [3, 4]]
    >>> labels = [0, 0, 1]
    >>> davies_bouldin_score(X, labels)
    0.12...
    """

    intra_dists = np.zeros(n_labels)
    centroids = np.zeros((n_labels, len(X[0])), dtype=float)
        cluster_k = _safe_indexing(X, labels == k)
        centroid = cluster_k.mean(axis=0)
        centroids[k] = centroid
        intra_dists[k] = np.average(pairwise_distances(cluster_k, [centroid]))

    centroid_distances = pairwise_distances(centroids)

    if np.allclose(intra_dists, 0) or np.allclose(centroid_distances, 0):
        return 0.0

    centroid_distances[centroid_distances == 0] = np.inf
    combined_intra_dists = intra_dists[:, None] + intra_dists
    scores = np.max(combined_intra_dists / centroid_distances, axis=1)
    return float(np.mean(scores))

def test_symmetric_non_symmetric_union():
    assert sorted(SYMMETRIC_METRICS + NON_SYMMETRIC_METRICS) == sorted(
        SUPERVISED_METRICS
    )

def test_symmetry(metric_name, y1, y2):
    metric = SUPERVISED_METRICS[metric_name]
    assert metric(y1, y2) == pytest.approx(metric(y2, y1))

def test_non_symmetry(metric_name, y1, y2):
    assert metric(y1, y2) != pytest.approx(metric(y2, y1))

def test_normalized_output(metric_name):
    upper_bound_1 = [0, 0, 0, 1, 1, 1]
    upper_bound_2 = [0, 0, 0, 1, 1, 1]
    assert metric([0, 0, 0, 1, 1], [0, 0, 0, 1, 2]) > 0.0
    assert metric([0, 0, 1, 1, 2], [0, 0, 1, 1, 1]) > 0.0
    assert metric([0, 0, 0, 1, 2], [0, 1, 1, 1, 1]) < 1.0
    assert metric(upper_bound_1, upper_bound_2) == pytest.approx(1.0)

    lower_bound_1 = [0, 0, 0, 0, 0, 0]
    lower_bound_2 = [0, 1, 2, 3, 4, 5]
    score = np.array(
        [metric(lower_bound_1, lower_bound_2), metric(lower_bound_2, lower_bound_1)]
    )
    assert not (score < 0).any()

def test_permute_labels(metric_name):
    # All clustering metrics do not change score due to permutations of labels
    # that is when 0 and 1 exchanged.
    y_label = np.array([0, 0, 0, 1, 1, 0, 1])
    y_pred = np.array([1, 0, 1, 0, 1, 1, 0])
    if metric_name in SUPERVISED_METRICS:
        metric = SUPERVISED_METRICS[metric_name]
        score_1 = metric(y_pred, y_label)
        assert_allclose(score_1, metric(1 - y_pred, y_label))
        assert_allclose(score_1, metric(1 - y_pred, 1 - y_label))
        assert_allclose(score_1, metric(y_pred, 1 - y_label))
    else:
        metric = UNSUPERVISED_METRICS[metric_name]
        X = np.random.randint(10, size=(7, 10))
        score_1 = metric(X, y_pred)
        assert_allclose(score_1, metric(X, 1 - y_pred))

def test_format_invariance(metric_name):
    y_true = [0, 0, 0, 0, 1, 1, 1, 1]
    y_pred = [0, 1, 2, 3, 4, 5, 6, 7]

    def generate_formats(y):
        y = np.array(y)
        yield y, "array of ints"
        yield y.tolist(), "list of ints"
        yield [str(x) + "-a" for x in y.tolist()], "list of strs"
        yield (
            np.array([str(x) + "-a" for x in y.tolist()], dtype=object),
            "array of strs",
        )
        yield y - 1, "including negative ints"
        yield y + 1, "strictly positive ints"

        score_1 = metric(y_true, y_pred)
        y_true_gen = generate_formats(y_true)
        y_pred_gen = generate_formats(y_pred)
        for (y_true_fmt, fmt_name), (y_pred_fmt, _) in zip(y_true_gen, y_pred_gen):
            assert score_1 == metric(y_true_fmt, y_pred_fmt)
    else:
        X = np.random.randint(10, size=(8, 10))
        score_1 = metric(X, y_true)
        assert score_1 == metric(X.astype(float), y_true)
        for y_true_fmt, fmt_name in y_true_gen:
            assert score_1 == metric(X, y_true_fmt)

def test_single_sample(metric):
    # only the supervised metrics support single sample
    for i, j in [(0, 0), (0, 1), (1, 0), (1, 1)]:
        metric([i], [j])

def test_inf_nan_input(metric_name, metric_func):
        invalids = [
            ([0, 1], [np.inf, np.inf]),
            ([0, 1], [np.nan, np.nan]),
            ([0, 1], [np.nan, np.inf]),
        ]
    else:
        X = np.random.randint(10, size=(2, 10))
        invalids = [(X, [np.inf, np.inf]), (X, [np.nan, np.nan]), (X, [np.nan, np.inf])]
    with pytest.raises(ValueError, match=r"contains (NaN|infinity)"):
        for args in invalids:
            metric_func(*args)

def test_returned_value_consistency(name):
    """Ensure that the returned values of all metrics are consistent.

    It can only be a float. It should not be a numpy float64 or float32.
    """

    X = rng.randint(10, size=(20, 10))
    labels_true = rng.randint(0, 3, size=(20,))
    labels_pred = rng.randint(0, 3, size=(20,))

    if name in SUPERVISED_METRICS:
        metric = SUPERVISED_METRICS[name]
        score = metric(labels_true, labels_pred)
    else:
        metric = UNSUPERVISED_METRICS[name]
        score = metric(X, labels_pred)

    assert isinstance(score, float)
    assert not isinstance(score, (np.float64, np.float32))

        yield (
        )

def test_error_messages_on_wrong_input(score_func):
    expected = r"Found input variables with inconsistent numbers of samples: \[2, 3\]"
    with pytest.raises(ValueError, match=expected):
        score_func([0, 1], [1, 1, 1])

    expected = r"labels_true must be 1D: shape is \(2"
        score_func([[0, 1], [1, 0]], [1, 1, 1])

    expected = r"labels_pred must be 1D: shape is \(2"
        score_func([0, 1, 0], [[1, 1], [0, 0]])

def test_generalized_average():
    a, b = 1, 2
    methods = ["min", "geometric", "arithmetic", "max"]
    means = [_generalized_average(a, b, method) for method in methods]
    assert means[0] <= means[1] <= means[2] <= means[3]
    c, d = 12, 12
    means = [_generalized_average(c, d, method) for method in methods]
    assert means[0] == means[1] == means[2] == means[3]

def test_perfect_matches(score_func):
    assert score_func([], []) == pytest.approx(1.0)
    assert score_func([0], [1]) == pytest.approx(1.0)
    assert score_func([0, 0, 0], [0, 0, 0]) == pytest.approx(1.0)
    assert score_func([0, 1, 0], [42, 7, 42]) == pytest.approx(1.0)
    assert score_func([0.0, 1.0, 0.0], [42.0, 7.0, 42.0]) == pytest.approx(1.0)
    assert score_func([0.0, 1.0, 2.0], [42.0, 7.0, 2.0]) == pytest.approx(1.0)
    assert score_func([0, 1, 2], [42, 7, 2]) == pytest.approx(1.0)

def test_perfect_matches_with_changing_means(score_func, average_method):
    assert score_func([], [], average_method=average_method) == pytest.approx(1.0)
    assert score_func([0], [1], average_method=average_method) == pytest.approx(1.0)
    assert score_func(
        [0, 0, 0], [0, 0, 0], average_method=average_method
    ) == pytest.approx(1.0)
        [0, 1, 0], [42, 7, 42], average_method=average_method
        [0.0, 1.0, 0.0], [42.0, 7.0, 42.0], average_method=average_method
        [0.0, 1.0, 2.0], [42.0, 7.0, 2.0], average_method=average_method
        [0, 1, 2], [42, 7, 2], average_method=average_method
    # Non-regression tests for: https://github.com/scikit-learn/scikit-learn/issues/30950
    assert score_func([0, 1], [0, 1], average_method=average_method) == pytest.approx(
        1.0
    )
        [0, 1, 2, 3], [0, 1, 2, 3], average_method=average_method

def test_homogeneous_but_not_complete_labeling():
    # homogeneous but not complete clustering
    h, c, v = homogeneity_completeness_v_measure([0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 2, 2])
    assert_almost_equal(h, 1.00, 2)
    assert_almost_equal(c, 0.69, 2)
    assert_almost_equal(v, 0.81, 2)

def test_complete_but_not_homogeneous_labeling():
    # complete but not homogeneous clustering
    h, c, v = homogeneity_completeness_v_measure([0, 0, 1, 1, 2, 2], [0, 0, 1, 1, 1, 1])
    assert_almost_equal(h, 0.58, 2)
    assert_almost_equal(c, 1.00, 2)
    assert_almost_equal(v, 0.73, 2)

def test_not_complete_and_not_homogeneous_labeling():
    # neither complete nor homogeneous but not so bad either
    h, c, v = homogeneity_completeness_v_measure([0, 0, 0, 1, 1, 1], [0, 1, 0, 1, 2, 2])
    assert_almost_equal(h, 0.67, 2)
    assert_almost_equal(c, 0.42, 2)
    assert_almost_equal(v, 0.52, 2)

def test_beta_parameter():
    # test for when beta passed to
    # homogeneity_completeness_v_measure
    # and v_measure_score
    beta_test = 0.2
    h_test = 0.67
    c_test = 0.42
    v_test = (1 + beta_test) * h_test * c_test / (beta_test * h_test + c_test)

    h, c, v = homogeneity_completeness_v_measure(
        [0, 0, 0, 1, 1, 1], [0, 1, 0, 1, 2, 2], beta=beta_test
    )
    assert_almost_equal(h, h_test, 2)
    assert_almost_equal(c, c_test, 2)
    assert_almost_equal(v, v_test, 2)

    v = v_measure_score([0, 0, 0, 1, 1, 1], [0, 1, 0, 1, 2, 2], beta=beta_test)

def test_non_consecutive_labels():
    # regression tests for labels with gaps
    h, c, v = homogeneity_completeness_v_measure([0, 0, 0, 2, 2, 2], [0, 1, 0, 1, 2, 2])

    h, c, v = homogeneity_completeness_v_measure([0, 0, 0, 1, 1, 1], [0, 4, 0, 4, 2, 2])

    ari_1 = adjusted_rand_score([0, 0, 0, 1, 1, 1], [0, 1, 0, 1, 2, 2])
    ari_2 = adjusted_rand_score([0, 0, 0, 1, 1, 1], [0, 4, 0, 4, 2, 2])
    assert_almost_equal(ari_1, 0.24, 2)
    assert_almost_equal(ari_2, 0.24, 2)

    ri_1 = rand_score([0, 0, 0, 1, 1, 1], [0, 1, 0, 1, 2, 2])
    ri_2 = rand_score([0, 0, 0, 1, 1, 1], [0, 4, 0, 4, 2, 2])
    assert_almost_equal(ri_1, 0.66, 2)
    assert_almost_equal(ri_2, 0.66, 2)

def uniform_labelings_scores(score_func, n_samples, k_range, n_runs=10, seed=42):
    # Compute score for random uniform cluster labelings
    random_labels = np.random.RandomState(seed).randint
    scores = np.zeros((len(k_range), n_runs))
    for i, k in enumerate(k_range):
        for j in range(n_runs):
            labels_a = random_labels(low=0, high=k, size=n_samples)
            labels_b = random_labels(low=0, high=k, size=n_samples)
            scores[i, j] = score_func(labels_a, labels_b)
    return scores

def test_adjustment_for_chance():
    # Check that adjusted scores are almost zero on random labels
    n_clusters_range = [2, 10, 50, 90]
    n_runs = 10

    scores = uniform_labelings_scores(
        adjusted_rand_score, n_samples, n_clusters_range, n_runs
    )

    max_abs_scores = np.abs(scores).max(axis=1)
    assert_array_almost_equal(max_abs_scores, [0.02, 0.03, 0.03, 0.02], 2)

def test_adjusted_mutual_info_score():
    # Compute the Adjusted Mutual Information and test against known values
    labels_a = np.array([1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3])
    labels_b = np.array([1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 3, 1, 3, 3, 3, 2, 2])
    # Mutual information
    mi = mutual_info_score(labels_a, labels_b)
    assert_almost_equal(mi, 0.41022, 5)
    # with provided sparse contingency
    C = contingency_matrix(labels_a, labels_b, sparse=True)
    mi = mutual_info_score(labels_a, labels_b, contingency=C)
    # with provided dense contingency
    C = contingency_matrix(labels_a, labels_b)
    # Expected mutual information
    n_samples = C.sum()
    emi = expected_mutual_information(C, n_samples)
    assert_almost_equal(emi, 0.15042, 5)
    # Adjusted mutual information
    ami = adjusted_mutual_info_score(labels_a, labels_b)
    assert_almost_equal(ami, 0.27821, 5)
    ami = adjusted_mutual_info_score([1, 1, 2, 2], [2, 2, 3, 3])
    assert ami == pytest.approx(1.0)
    # Test with a very large array
    a110 = np.array([list(labels_a) * 110]).flatten()
    b110 = np.array([list(labels_b) * 110]).flatten()
    ami = adjusted_mutual_info_score(a110, b110)
    assert_almost_equal(ami, 0.38, 2)

def test_expected_mutual_info_overflow():
    # Test for regression where contingency cell exceeds 2**16
    # leading to overflow in np.outer, resulting in EMI > 1
    assert expected_mutual_information(np.array([[70000]]), 70000) <= 1

def test_int_overflow_mutual_info_fowlkes_mallows_score():
    # Test overflow in mutual_info_classif and fowlkes_mallows_score
    x = np.array(
        [1] * (52632 + 2529)
        + [2] * (14660 + 793)
        + [3] * (3271 + 204)
        + [4] * (814 + 39)
        + [5] * (316 + 20)
    )
    y = np.array(
        [0] * 52632
        + [1] * 2529
        + [0] * 14660
        + [1] * 793
        + [0] * 3271
        + [1] * 204
        + [0] * 814
        + [1] * 39
        + [0] * 316
        + [1] * 20
    )

    assert_all_finite(mutual_info_score(x, y))
    assert_all_finite(fowlkes_mallows_score(x, y))

def test_entropy():
    assert_almost_equal(entropy([0, 0, 42.0]), 0.6365141, 5)
    assert_almost_equal(entropy([]), 1)
    assert entropy([1, 1, 1, 1]) == 0

def test_entropy_array_api(array_namespace, device, dtype_name):
    xp = _array_api_for_tests(array_namespace, device)
    float_labels = xp.asarray(np.asarray([0, 0, 42.0], dtype=dtype_name), device=device)
    empty_int32_labels = xp.asarray([], dtype=xp.int32, device=device)
    int_labels = xp.asarray([1, 1, 1, 1], device=device)
    with config_context(array_api_dispatch=True):
        assert entropy(float_labels) == pytest.approx(0.6365141, abs=1e-5)
        assert entropy(empty_int32_labels) == 1
        assert entropy(int_labels) == 0

def test_contingency_matrix():
    C2 = np.histogram2d(labels_a, labels_b, bins=(np.arange(1, 5), np.arange(1, 5)))[0]
    assert_array_almost_equal(C, C2)
    C = contingency_matrix(labels_a, labels_b, eps=0.1)
    assert_array_almost_equal(C, C2 + 0.1)

def test_contingency_matrix_sparse():
    C_sparse = contingency_matrix(labels_a, labels_b, sparse=True).toarray()
    assert_array_almost_equal(C, C_sparse)
    with pytest.raises(ValueError, match="Cannot set 'eps' when sparse=True"):
        contingency_matrix(labels_a, labels_b, eps=1e-10, sparse=True)

def test_exactly_zero_info_score():
    # Check numerical stability when information is exactly zero
    for i in np.logspace(1, 4, 4).astype(int):
        labels_a, labels_b = (np.ones(i, dtype=int), np.arange(i, dtype=int))
        assert normalized_mutual_info_score(labels_a, labels_b) == pytest.approx(0.0)
        assert v_measure_score(labels_a, labels_b) == pytest.approx(0.0)
        assert adjusted_mutual_info_score(labels_a, labels_b) == 0.0
        for method in ["min", "geometric", "arithmetic", "max"]:
            assert (
                adjusted_mutual_info_score(labels_a, labels_b, average_method=method)
                == 0.0
            )
            assert normalized_mutual_info_score(
                labels_a, labels_b, average_method=method
            ) == pytest.approx(0.0)

def test_v_measure_and_mutual_information(seed=36):
    # Check relation between v_measure, entropy and mutual information
        random_state = np.random.RandomState(seed)
        labels_a, labels_b = (
            random_state.randint(0, 10, i),
        )
            v_measure_score(labels_a, labels_b),
            2.0
            * mutual_info_score(labels_a, labels_b)
            / (entropy(labels_a) + entropy(labels_b)),
            0,
        )
        avg = "arithmetic"
            normalized_mutual_info_score(labels_a, labels_b, average_method=avg),
        )

def test_fowlkes_mallows_score():
    # General case
    score = fowlkes_mallows_score([0, 0, 0, 1, 1, 1], [0, 0, 1, 1, 2, 2])
    assert_almost_equal(score, 4.0 / np.sqrt(12.0 * 6.0))

    # Perfect match but where the label names changed
    perfect_score = fowlkes_mallows_score([0, 0, 0, 1, 1, 1], [1, 1, 1, 0, 0, 0])
    assert_almost_equal(perfect_score, 1.0)

    # Worst case
    worst_score = fowlkes_mallows_score([0, 0, 0, 0, 0, 0], [0, 1, 2, 3, 4, 5])
    assert_almost_equal(worst_score, 0.0)

def test_fowlkes_mallows_score_properties():
    # handcrafted example
    labels_a = np.array([0, 0, 0, 1, 1, 2])
    labels_b = np.array([1, 1, 2, 2, 0, 0])
    expected = 1.0 / np.sqrt((1.0 + 3.0) * (1.0 + 2.0))
    # FMI = TP / sqrt((TP + FP) * (TP + FN))

    score_original = fowlkes_mallows_score(labels_a, labels_b)
    assert_almost_equal(score_original, expected)

    # symmetric property
    score_symmetric = fowlkes_mallows_score(labels_b, labels_a)
    assert_almost_equal(score_symmetric, expected)

    # permutation property
    score_permuted = fowlkes_mallows_score((labels_a + 1) % 3, labels_b)
    assert_almost_equal(score_permuted, expected)

    # symmetric and permutation(both together)
    score_both = fowlkes_mallows_score(labels_b, (labels_a + 2) % 3)
    assert_almost_equal(score_both, expected)

def test_mutual_info_score_positive_constant_label(labels_true, labels_pred):
    # Check that MI = 0 when one or both labelling are constant
    # non-regression test for #16355
    assert mutual_info_score(labels_true, labels_pred) == 0

def test_check_clustering_error():
    # Test warning message for continuous values
    noise = rng.rand(500)
    wavelength = np.linspace(0.01, 1, 500) * 1e-6
    msg = (
        "Clustering metrics expects discrete values but received "
        "continuous values for label, and continuous values for "
        "target"
    )

        check_clusterings(wavelength, noise)

def test_pair_confusion_matrix_fully_dispersed():
    # edge case: every element is its own cluster
    N = 100
    clustering1 = list(range(N))
    clustering2 = clustering1
    expected = np.array([[N * (N - 1), 0], [0, 0]])
    assert_array_equal(pair_confusion_matrix(clustering1, clustering2), expected)

def test_pair_confusion_matrix_single_cluster():
    # edge case: only one cluster
    N = 100
    clustering1 = np.zeros((N,))
    expected = np.array([[0, 0], [0, N * (N - 1)]])

def test_pair_confusion_matrix():
    # regular case: different non-trivial clusterings
    n = 10
    N = n**2
    clustering1 = np.hstack([[i + 1] * n for i in range(n)])
    clustering2 = np.hstack([[i + 1] * (n + 1) for i in range(n)])[:N]
    # basic quadratic implementation
    expected = np.zeros(shape=(2, 2), dtype=np.int64)
    for i in range(len(clustering1)):
        for j in range(len(clustering2)):
            if i != j:
                same_cluster_1 = int(clustering1[i] == clustering1[j])
                same_cluster_2 = int(clustering2[i] == clustering2[j])
                expected[same_cluster_1, same_cluster_2] += 1

def test_rand_score_edge_cases(clustering1, clustering2):
    # edge case 1: every element is its own cluster
    # edge case 2: only one cluster
    assert_allclose(rand_score(clustering1, clustering2), 1.0)

def test_rand_score():
    clustering1 = [0, 0, 0, 1, 1, 1]
    clustering2 = [0, 1, 0, 1, 2, 2]
    # pair confusion matrix
    D11 = 2 * 2  # ordered pairs (1, 3), (5, 6)
    D10 = 2 * 4  # ordered pairs (1, 2), (2, 3), (4, 5), (4, 6)
    D01 = 2 * 1  # ordered pair (2, 4)
    D00 = 5 * 6 - D11 - D01 - D10  # the remaining pairs
    # rand score
    expected_numerator = D00 + D11
    expected_denominator = D00 + D01 + D10 + D11
    expected = expected_numerator / expected_denominator
    assert_allclose(rand_score(clustering1, clustering2), expected)

def test_adjusted_rand_score_overflow():
    """Check that large amount of data will not lead to overflow in
    `adjusted_rand_score`.
    https://github.com/scikit-learn/scikit-learn/issues/20305
    """
    y_true = rng.randint(0, 2, 100_000, dtype=np.int8)
    y_pred = rng.randint(0, 2, 100_000, dtype=np.int8)
        adjusted_rand_score(y_true, y_pred)

def test_normalized_mutual_info_score_bounded(average_method):
    """Check that nmi returns a score between 0 (included) and 1 (excluded
    for non-perfect match)

    Non-regression test for issue #13836
    """
    labels1 = [0] * 469
    labels2 = [1] + labels1[1:]
    labels3 = [0, 1] + labels1[2:]

    # labels1 is constant. The mutual info between labels1 and any other labelling is 0.
    nmi = normalized_mutual_info_score(labels1, labels2, average_method=average_method)
    assert nmi == 0

    # non constant, non perfect matching labels
    nmi = normalized_mutual_info_score(labels2, labels3, average_method=average_method)
    assert 0 <= nmi < 1

def test_fowlkes_mallows_sparse_deprecated(sparse):
    """Check deprecation warning for 'sparse' parameter of fowlkes_mallows_score."""
        FutureWarning, match="The 'sparse' parameter was deprecated in 1.7"
    ):
        fowlkes_mallows_score([0, 1], [1, 1], sparse=sparse)

def test_silhouette(sparse_container, sample_size):
    # Tests the Silhouette Coefficient.
    dataset = datasets.load_iris()
    X, y = dataset.data, dataset.target
    if sparse_container is not None:
        X = sparse_container(X)
    sample_size = int(X.shape[0] / 2) if sample_size == "half" else sample_size

    D = pairwise_distances(X, metric="euclidean")
    # Given that the actual labels are used, we can assume that S would be positive.
    score_precomputed = silhouette_score(
        D, y, metric="precomputed", sample_size=sample_size, random_state=0
    )
    score_euclidean = silhouette_score(
        X, y, metric="euclidean", sample_size=sample_size, random_state=0
    )
    assert score_precomputed > 0
    assert score_euclidean > 0
    assert score_precomputed == pytest.approx(score_euclidean)

def test_cluster_size_1():
    # Assert Silhouette Coefficient == 0 when there is 1 sample in a cluster
    # (cluster 0). We also test the case where there are identical samples
    # as the only members of a cluster (cluster 2). To our knowledge, this case
    # is not discussed in reference material, and we choose for it a sample
    # score of 1.
    X = [[0.0], [1.0], [1.0], [2.0], [3.0], [3.0]]
    labels = np.array([0, 1, 1, 1, 2, 2])

    # Cluster 0: 1 sample -> score of 0 by Rousseeuw's convention
    # Cluster 1: intra-cluster = [.5, .5, 1]
    #            inter-cluster = [1, 1, 1]
    #            silhouette    = [.5, .5, 0]
    # Cluster 2: intra-cluster = [0, 0]
    #            inter-cluster = [arbitrary, arbitrary]
    #            silhouette    = [1., 1.]

    silhouette = silhouette_score(X, labels)
    assert not np.isnan(silhouette)
    ss = silhouette_samples(X, labels)
    assert_array_equal(ss, [0, 0.5, 0.5, 0, 1, 1])

def test_silhouette_paper_example():
    # Explicitly check per-sample results against Rousseeuw (1987)
    # Data from Table 1
    lower = [
        5.58,
        7.00,
        6.50,
        7.08,
        7.00,
        3.83,
        4.83,
        5.08,
        8.17,
        5.83,
        2.17,
        5.75,
        6.67,
        6.92,
        4.92,
        6.42,
        5.00,
        5.58,
        6.00,
        4.67,
        6.42,
        3.42,
        5.50,
        6.42,
        6.42,
        5.00,
        3.92,
        6.17,
        2.50,
        4.92,
        6.25,
        7.33,
        4.50,
        2.25,
        6.33,
        2.75,
        6.08,
        6.67,
        4.25,
        2.67,
        6.00,
        6.17,
        6.17,
        6.92,
        6.17,
        5.25,
        6.83,
        4.50,
        3.75,
        5.75,
        5.42,
        6.08,
        5.83,
        6.67,
        3.67,
        4.75,
        3.00,
        6.08,
        6.67,
        5.00,
        5.58,
        4.83,
        6.17,
        5.67,
        6.50,
        6.92,
    ]
    D = np.zeros((12, 12))
    D[np.tril_indices(12, -1)] = lower
    D += D.T

    names = [
        "BEL",
        "BRA",
        "CHI",
        "CUB",
        "EGY",
        "FRA",
        "IND",
        "ISR",
        "USA",
        "USS",
        "YUG",
        "ZAI",
    ]

    # Data from Figure 2
    labels1 = [1, 1, 2, 2, 1, 1, 2, 1, 1, 2, 2, 1]
    expected1 = {
        "USA": 0.43,
        "BEL": 0.39,
        "FRA": 0.35,
        "ISR": 0.30,
        "BRA": 0.22,
        "EGY": 0.20,
        "ZAI": 0.19,
        "CUB": 0.40,
        "USS": 0.34,
        "CHI": 0.33,
        "YUG": 0.26,
        "IND": -0.04,
    }
    score1 = 0.28

    # Data from Figure 3
    labels2 = [1, 2, 3, 3, 1, 1, 2, 1, 1, 3, 3, 2]
    expected2 = {
        "USA": 0.47,
        "FRA": 0.44,
        "BEL": 0.42,
        "ISR": 0.37,
        "EGY": 0.02,
        "ZAI": 0.28,
        "BRA": 0.25,
        "IND": 0.17,
        "CUB": 0.48,
        "USS": 0.44,
        "YUG": 0.31,
        "CHI": 0.31,
    }
    score2 = 0.33

    for labels, expected, score in [
        (labels1, expected1, score1),
        (labels2, expected2, score2),
    ]:
        expected = [expected[name] for name in names]
        # we check to 2dp because that's what's in the paper
        pytest.approx(
            expected,
            silhouette_samples(D, np.array(labels), metric="precomputed"),
            abs=1e-2,
        )
            score, silhouette_score(D, np.array(labels), metric="precomputed"), abs=1e-2
        )

def test_correct_labelsize():
    # Assert 1 < n_labels < n_samples
    X = dataset.data

    # n_labels = n_samples
    y = np.arange(X.shape[0])
    err_msg = (
        r"Number of labels is %d\. Valid values are 2 "
        r"to n_samples - 1 \(inclusive\)" % len(np.unique(y))
    )
        silhouette_score(X, y)

    # n_labels = 1
    y = np.zeros(X.shape[0])
    err_msg = (
    )

def test_non_encoded_labels():
    labels = dataset.target
    assert silhouette_score(X, labels * 2 + 10) == silhouette_score(X, labels)
        silhouette_samples(X, labels * 2 + 10), silhouette_samples(X, labels)
    )

def test_non_numpy_labels():
    y = dataset.target
    assert silhouette_score(list(X), list(y)) == silhouette_score(X, y)

def test_silhouette_nonzero_diag(dtype):
    # Make sure silhouette_samples requires diagonal to be zero.
    # Non-regression test for #12178

    # Construct a zero-diagonal matrix
    dists = pairwise_distances(
        np.array([[0.2, 0.1, 0.12, 1.34, 1.11, 1.6]], dtype=dtype).T
    )
    labels = [0, 0, 0, 1, 1, 1]

    # small values on the diagonal are OK
    dists[2][2] = np.finfo(dists.dtype).eps * 10
    silhouette_samples(dists, labels, metric="precomputed")

    # values bigger than eps * 100 are not
    dists[2][2] = np.finfo(dists.dtype).eps * 1000
    with pytest.raises(ValueError, match="contains non-zero"):
        silhouette_samples(dists, labels, metric="precomputed")

def test_silhouette_samples_precomputed_sparse(sparse_container):
    """Check that silhouette_samples works for sparse matrices correctly."""
    X = np.array([[0.2, 0.1, 0.1, 0.2, 0.1, 1.6, 0.2, 0.1]], dtype=np.float32).T
    y = [0, 0, 0, 0, 1, 1, 1, 1]
    pdist_dense = pairwise_distances(X)
    pdist_sparse = sparse_container(pdist_dense)
    assert issparse(pdist_sparse)
    output_with_sparse_input = silhouette_samples(pdist_sparse, y, metric="precomputed")
    output_with_dense_input = silhouette_samples(pdist_dense, y, metric="precomputed")
    assert_allclose(output_with_sparse_input, output_with_dense_input)

def test_silhouette_samples_euclidean_sparse(sparse_container):
    output_with_sparse_input = silhouette_samples(pdist_sparse, y)
    output_with_dense_input = silhouette_samples(pdist_dense, y)

def test_silhouette_reduce(sparse_container):
    """Check for non-CSR input to private method `_silhouette_reduce`."""
    label_freqs = np.bincount(y)
        TypeError,
        match="Expected CSR matrix. Please pass sparse matrix in CSR format.",
    ):
        _silhouette_reduce(pdist_sparse, start=0, labels=y, label_freqs=label_freqs)

def assert_raises_on_only_one_label(func):
    """Assert message when there is only one label"""
    with pytest.raises(ValueError, match="Number of labels is"):
        func(rng.rand(10, 2), np.zeros(10))

def assert_raises_on_all_points_same_cluster(func):
    """Assert message when all point are in different clusters"""
        func(rng.rand(10, 2), np.arange(10))

def test_calinski_harabasz_score():
    assert_raises_on_only_one_label(calinski_harabasz_score)

    assert_raises_on_all_points_same_cluster(calinski_harabasz_score)

    # Assert the value is 1. when all samples are equals
    assert 1.0 == calinski_harabasz_score(np.ones((10, 2)), [0] * 5 + [1] * 5)

    # Assert the value is 0. when all the mean cluster are equal
    assert 0.0 == calinski_harabasz_score([[-1, -1], [1, 1]] * 10, [0] * 10 + [1] * 10)

    # General case (with non numpy arrays)
    X = (
        [[0, 0], [1, 1]] * 5
        + [[3, 3], [4, 4]] * 5
        + [[0, 4], [1, 3]] * 5
        + [[3, 1], [4, 0]] * 5
    )
    labels = [0] * 10 + [1] * 10 + [2] * 10 + [3] * 10
    pytest.approx(calinski_harabasz_score(X, labels), 45 * (40 - 4) / (5 * (4 - 1)))

def test_davies_bouldin_score():
    assert_raises_on_only_one_label(davies_bouldin_score)
    assert_raises_on_all_points_same_cluster(davies_bouldin_score)

    # Assert the value is 0. when all samples are equals
    assert davies_bouldin_score(np.ones((10, 2)), [0] * 5 + [1] * 5) == pytest.approx(
        0.0
    )

    assert davies_bouldin_score(
        [[-1, -1], [1, 1]] * 10, [0] * 10 + [1] * 10
    ) == pytest.approx(0.0)

    X = (
    )
    pytest.approx(davies_bouldin_score(X, labels), 2 * np.sqrt(0.5) / 3)

    # Ensure divide by zero warning is not raised in general case
        davies_bouldin_score(X, labels)

    # General case - cluster have one sample
    X = [[0, 0], [2, 2], [3, 3], [5, 5]]
    labels = [0, 0, 1, 2]
    pytest.approx(davies_bouldin_score(X, labels), (5.0 / 4) / 3)

def test_silhouette_score_integer_precomputed():
    """Check that silhouette_score works for precomputed metrics that are integers.

    Non-regression test for #22107.
    """
    result = silhouette_score(
        [[0, 1, 2], [1, 0, 1], [2, 1, 0]], [0, 0, 1], metric="precomputed"
    )
    assert result == pytest.approx(1 / 6)

    # non-zero on diagonal for ints raises an error
        silhouette_score(
            [[1, 1, 2], [1, 0, 1], [2, 1, 0]], [0, 0, 1], metric="precomputed"
        )

def test_jaccard():
    a1 = np.array([True, True, False, False])
    a2 = np.array([True, True, True, True])
    a3 = np.array([False, True, True, False])
    a4 = np.array([False, False, True, True])

    assert _jaccard(a1, a1, a1, a1) == 1
    assert _jaccard(a1, a1, a2, a2) == 0.25
    assert _jaccard(a1, a1, a3, a3) == 1.0 / 7
    assert _jaccard(a1, a1, a4, a4) == 0

def test_consensus_score():
    a = [[True, True, False, False], [False, False, True, True]]
    b = a[::-1]

    assert consensus_score((a, a), (a, a)) == 1
    assert consensus_score((a, a), (b, b)) == 1
    assert consensus_score((a, b), (a, b)) == 1
    assert consensus_score((a, b), (b, a)) == 1

    assert consensus_score((a, a), (b, a)) == 0
    assert consensus_score((a, a), (a, b)) == 0
    assert consensus_score((b, b), (a, b)) == 0
    assert consensus_score((b, b), (b, a)) == 0

def test_consensus_score_issue2445():
    """Different number of biclusters in A and B"""
    a_rows = np.array(
        [
            [True, True, False, False],
            [False, False, True, True],
            [False, False, False, True],
        ]
    )
    a_cols = np.array(
        [
        ]
    )
    idx = [0, 2]
    s = consensus_score((a_rows, a_cols), (a_rows[idx], a_cols[idx]))
    # B contains 2 of the 3 biclusters in A, so score should be 2/3
    assert_almost_equal(s, 2.0 / 3.0)

def precision_recall_curve_padded_thresholds(*args, **kwargs):
    """
    The dimensions of precision-recall pairs and the threshold array as
    returned by the precision_recall_curve do not match. See
    func:`sklearn.metrics.precision_recall_curve`

    This prevents implicit conversion of return value triple to an higher
    dimensional np.array of dtype('float64') (it will be of dtype('object)
    instead). This again is needed for assert_array_equal to work correctly.

    As a workaround we pad the threshold array with NaN values to match
    the dimension of precision and recall arrays respectively.
    """
    precision, recall, thresholds = precision_recall_curve(*args, **kwargs)

    pad_threshholds = len(precision) - len(thresholds)

    return np.array(
        [
            precision,
            recall,
            np.pad(
                thresholds.astype(np.float64),
                pad_width=(0, pad_threshholds),
                mode="constant",
                constant_values=[np.nan],
            ),
        ]
    )

def _require_positive_targets(y1, y2):
    """Make targets strictly positive"""
    offset = abs(min(y1.min(), y2.min())) + 1
    y1 += offset
    y2 += offset
    return y1, y2

def _require_log1p_targets(y1, y2):
    """Make targets strictly larger than -1"""
    offset = abs(min(y1.min(), y2.min())) - 0.99
    y1 = y1.astype(np.float64)
    y2 = y2.astype(np.float64)

def test_symmetry_consistency():
    # We shouldn't forget any metrics
    assert (
        SYMMETRIC_METRICS
        | NOT_SYMMETRIC_METRICS
        | set(THRESHOLDED_METRICS)
        | METRIC_UNDEFINED_BINARY_MULTICLASS
    ) == set(ALL_METRICS)

    assert (SYMMETRIC_METRICS & NOT_SYMMETRIC_METRICS) == set()

def test_symmetric_metric(name):
    # Test the symmetry of score and loss functions
    random_state = check_random_state(0)
    y_true = random_state.randint(0, 2, size=(20,))
    y_pred = random_state.randint(0, 2, size=(20,))

    if name in METRICS_REQUIRE_POSITIVE_Y:
        y_true, y_pred = _require_positive_targets(y_true, y_pred)

    elif name in METRICS_WITH_LOG1P_Y:
        y_true, y_pred = _require_log1p_targets(y_true, y_pred)

    y_true_bin = random_state.randint(0, 2, size=(20, 25))
    y_pred_bin = random_state.randint(0, 2, size=(20, 25))

    metric = ALL_METRICS[name]
    if name in METRIC_UNDEFINED_BINARY:
        if name in MULTILABELS_METRICS:
                metric(y_true_bin, y_pred_bin),
                metric(y_pred_bin, y_true_bin),
                err_msg="%s is not symmetric" % name,
            )
        else:
            assert False, "This case is currently unhandled"
    else:
            metric(y_true, y_pred),
            metric(y_pred, y_true),
            err_msg="%s is not symmetric" % name,
        )

def test_not_symmetric_metric(name):

    # The metric can be accidentally symmetric on a random draw.
    # We run several random draws to check that at least of them
    # gives an asymmetric result.
    always_symmetric = True
    for _ in range(5):
        y_true = random_state.randint(0, 2, size=(20,))
        y_pred = random_state.randint(0, 2, size=(20,))

        if name in METRICS_REQUIRE_POSITIVE_Y:
            y_true, y_pred = _require_positive_targets(y_true, y_pred)

        nominal = metric(y_true, y_pred)
        swapped = metric(y_pred, y_true)
        if not np.allclose(nominal, swapped):
            always_symmetric = False
            break

    if always_symmetric:
        raise ValueError(f"{name} seems to be symmetric")

def test_symmetry_tests():
    # check test_symmetric_metric and test_not_symmetric_metric
    sym = "accuracy_score"
    not_sym = "recall_score"
    # test_symmetric_metric passes on a symmetric metric
    # but fails on a not symmetric metric
    test_symmetric_metric(sym)
    with pytest.raises(AssertionError, match=f"{not_sym} is not symmetric"):
        test_symmetric_metric(not_sym)
    # test_not_symmetric_metric passes on a not symmetric metric
    # but fails on a symmetric metric
    test_not_symmetric_metric(not_sym)
    with pytest.raises(ValueError, match=f"{sym} seems to be symmetric"):
        test_not_symmetric_metric(sym)

def test_sample_order_invariance(name):

    y_true_shuffle, y_pred_shuffle = shuffle(y_true, y_pred, random_state=0)

        metric = ALL_METRICS[name]
            metric(y_true_shuffle, y_pred_shuffle),
            err_msg="%s is not sample order invariant" % name,
        )

def test_sample_order_invariance_multilabel_and_multioutput():

    # Generate some data
    y_true = random_state.randint(0, 2, size=(20, 25))
    y_pred = random_state.randint(0, 2, size=(20, 25))
    y_score = random_state.uniform(size=y_true.shape)

    # Some metrics (e.g. log_loss) require y_score to be probabilities (sum to 1)
    y_score /= y_score.sum(axis=1, keepdims=True)

    y_true_shuffle, y_pred_shuffle, y_score_shuffle = shuffle(
        y_true, y_pred, y_score, random_state=0
    )

    for name in MULTILABELS_METRICS:
        )

    for name in THRESHOLDED_MULTILABEL_METRICS:
            metric(y_true, y_score),
            metric(y_true_shuffle, y_score_shuffle),
        )

    for name in MULTIOUTPUT_METRICS:
        )
        )

def test_format_invariance_with_1d_vectors(name):
    y1 = random_state.randint(0, 2, size=(20,))
    y2 = random_state.randint(0, 2, size=(20,))

        y1, y2 = _require_positive_targets(y1, y2)
        y1, y2 = _require_log1p_targets(y1, y2)

    y1_list = list(y1)
    y2_list = list(y2)

    y1_1d, y2_1d = np.array(y1), np.array(y2)
    assert_array_equal(y1_1d.ndim, 1)
    assert_array_equal(y2_1d.ndim, 1)
    y1_column = np.reshape(y1_1d, (-1, 1))
    y2_column = np.reshape(y2_1d, (-1, 1))
    y1_row = np.reshape(y1_1d, (1, -1))
    y2_row = np.reshape(y2_1d, (1, -1))

        measure = metric(y1, y2)

            metric(y1_list, y2_list),
            measure,
            err_msg="%s is not representation invariant with list" % name,
        )

            metric(y1_1d, y2_1d),
            measure,
            err_msg="%s is not representation invariant with np-array-1d" % name,
        )

            metric(y1_column, y2_column),
            measure,
            err_msg="%s is not representation invariant with np-array-column" % name,
        )

        # Mix format support
            metric(y1_1d, y2_list),
            measure,
            err_msg="%s is not representation invariant with mix np-array-1d and list"
            % name,
        )

            metric(y1_list, y2_1d),
            measure,
            % name,
        )

            metric(y1_1d, y2_column),
            measure,
            err_msg=(
                "%s is not representation invariant with mix "
                "np-array-1d and np-array-column"
            )
            % name,
        )

            metric(y1_column, y2_1d),
            measure,
            err_msg=(
            )
            % name,
        )

            metric(y1_list, y2_column),
            measure,
            err_msg=(
                "%s is not representation invariant with mix list and np-array-column"
            )
            % name,
        )

            metric(y1_column, y2_list),
            measure,
            err_msg=(
            )
            % name,
        )

        # These mix representations aren't allowed
            metric(y1_1d, y2_row)
            metric(y1_row, y2_1d)
            metric(y1_list, y2_row)
            metric(y1_row, y2_list)
            metric(y1_column, y2_row)
            metric(y1_row, y2_column)

        # NB: We do not test for y1_row, y2_row as these may be
        # interpreted as multilabel or multioutput data.
        if name not in (
            MULTIOUTPUT_METRICS | THRESHOLDED_MULTILABEL_METRICS | MULTILABELS_METRICS
        ):
            if "roc_auc" in name:
                # for consistency between the `roc_cuve` and `roc_auc_score`
                # np.nan is returned and an `UndefinedMetricWarning` is raised
                with pytest.warns(UndefinedMetricWarning):
                    assert math.isnan(metric(y1_row, y2_row))
            else:
                with pytest.raises(ValueError):
                    metric(y1_row, y2_row)

def test_classification_invariance_string_vs_numbers_labels(name):
    # Ensure that classification metrics with string labels are invariant

    y1_str = np.array(["eggs", "spam"])[y1]
    y2_str = np.array(["eggs", "spam"])[y2]

    pos_label_str = "spam"
    labels_str = ["eggs", "spam"]

        metric = CLASSIFICATION_METRICS[name]
        measure_with_number = metric(y1, y2)

        # Ugly, but handle case with a pos_label and label
        metric_str = metric
        if name in METRICS_WITH_POS_LABEL:
            metric_str = partial(metric_str, pos_label=pos_label_str)

        measure_with_str = metric_str(y1_str, y2_str)

            measure_with_number,
            measure_with_str,
            err_msg="{0} failed string vs number invariance test".format(name),
        )

        measure_with_strobj = metric_str(y1_str.astype("O"), y2_str.astype("O"))
            measure_with_strobj,
            err_msg="{0} failed string object vs number invariance test".format(name),
        )

        if name in METRICS_WITH_LABELS:
            metric_str = partial(metric_str, labels=labels_str)
            measure_with_str = metric_str(y1_str, y2_str)
            assert_array_equal(
                measure_with_number,
                measure_with_str,
                err_msg="{0} failed string vs number  invariance test".format(name),
            )

            measure_with_strobj = metric_str(y1_str.astype("O"), y2_str.astype("O"))
                measure_with_strobj,
            )

def test_thresholded_invariance_string_vs_numbers_labels(name):
    # Ensure that thresholded metrics with string labels are invariant

        metric = THRESHOLDED_METRICS[name]
        if name not in METRIC_UNDEFINED_BINARY:
            # Ugly, but handle case with a pos_label and label
            metric_str = metric
            if name in METRICS_WITH_POS_LABEL:
                metric_str = partial(metric_str, pos_label=pos_label_str)

            measure_with_number = metric(y1, y2)
            measure_with_str = metric_str(y1_str, y2)
                err_msg="{0} failed string vs number invariance test".format(name),
            )

            measure_with_strobj = metric_str(y1_str.astype("O"), y2)
                err_msg="{0} failed string object vs number invariance test".format(
                    name
                ),
            )
        else:
            # TODO those metrics doesn't support string label yet
                metric(y1_str, y2)
                metric(y1_str.astype("O"), y2)

def test_regression_thresholded_inf_nan_input(metric, y_true, y_score):
    # Reshape since coverage_error only accepts 2D arrays.
    if metric == coverage_error:
        y_true = [y_true]
        y_score = [y_score]
        metric(y_true, y_score)

def test_classification_inf_nan_input(metric, y_true, y_score):
    """check that classification metrics raise a message mentioning the
    occurrence of non-finite values in the target vectors."""
    if not np.isfinite(y_true).all():
        input_name = "y_true"
        if np.isnan(y_true).any():
            unexpected_value = "NaN"
        else:
            unexpected_value = "infinity or a value too large"
    else:
        input_name = "y_pred"
        if np.isnan(y_score).any():
        else:

    err_msg = f"Input {input_name} contains {unexpected_value}"

def test_classification_binary_continuous_input(metric):
    """check that classification metrics raise a message of mixed type data
    with continuous/binary target vectors."""
    y_true, y_score = ["a", "b", "a"], [0.1, 0.2, 0.3]
    err_msg = (
        "Classification metrics can't handle a mix of binary and continuous targets"
    )

def check_single_sample(name):
    # Non-regression test: scores should work with a single sample.
    # This is important for leave-one-out cross validation.
    # Score functions tested are those that formerly called np.squeeze,
    # which turns an array of size 1 into a 0-d array (!).

    # assert that no exception is thrown
        values = [1, 2]
        values = [-0.7, 1]
    else:
        values = [0, 1]
    for i, j in product(values, repeat=2):

def check_single_sample_multioutput(name):
    for i, j, k, l in product([0, 1], repeat=4):
        metric(np.array([[i, j]]), np.array([[k, l]]))

def test_single_sample_multioutput(name):
    check_single_sample_multioutput(name)

def test_multioutput_number_of_output_differ(name):
    y_true = np.array([[1, 0, 0, 1], [0, 1, 1, 1], [1, 1, 0, 1]])
    y_pred = np.array([[0, 0], [1, 0], [0, 0]])

        metric(y_true, y_pred)

def test_multioutput_regression_invariance_to_dimension_shuffling(name):
    # test invariance to dimension shuffling
    y_true = random_state.uniform(0, 2, size=(20, 5))
    y_pred = random_state.uniform(0, 2, size=(20, 5))

    error = metric(y_true, y_pred)

    for _ in range(3):
        perm = random_state.permutation(y_true.shape[1])
            metric(y_true[:, perm], y_pred[:, perm]),
            error,
            err_msg="%s is not dimension shuffling invariant" % (name),
        )

def test_multilabel_representation_invariance(coo_container):

    _, y1 = make_multilabel_classification(
        n_features=1,
        allow_unlabeled=True,
    )
    _, y2 = make_multilabel_classification(
    )

    # To make sure at least one empty label is present
    y1 = np.vstack([y1, [[0] * n_classes]])
    y2 = np.vstack([y2, [[0] * n_classes]])

    y1_sparse_indicator = coo_container(y1)
    y2_sparse_indicator = coo_container(y2)

    y1_list_array_indicator = list(y1)
    y2_list_array_indicator = list(y2)

    y1_list_list_indicator = [list(a) for a in y1_list_array_indicator]
    y2_list_list_indicator = [list(a) for a in y2_list_array_indicator]

        # XXX cruel hack to work with partial functions
        if isinstance(metric, partial):
            metric.__module__ = "tmp"
            metric.__name__ = name

        # Check representation invariance
            metric(y1_sparse_indicator, y2_sparse_indicator),
            measure,
            err_msg=(
                "%s failed representation invariance between "
                "dense and sparse indicator formats."
            )
            % name,
        )
            metric(y1_list_list_indicator, y2_list_list_indicator),
            measure,
            err_msg=(
                "%s failed representation invariance  "
                "between dense array and list of list "
                "indicator formats."
            )
            % name,
        )
            metric(y1_list_array_indicator, y2_list_array_indicator),
            measure,
            err_msg=(
                "between dense and list of array "
            )
            % name,
        )

def test_raise_value_error_multilabel_sequences(name):
    # make sure the multilabel-sequence format raises ValueError
    multilabel_sequences = [
        [[1], [2], [0, 1]],
        [(), (2), (0, 1)],
        [[]],
        [()],
        np.array([[], [1, 2]], dtype="object"),
    ]

    for seq in multilabel_sequences:
            metric(seq, seq)

def test_normalize_option_binary_classification(name):
    # Test in the binary case

    y_true = random_state.randint(0, n_classes, size=(n_samples,))
    y_pred = random_state.randint(0, n_classes, size=(n_samples,))
    y_score = random_state.normal(size=y_true.shape)

    metrics = ALL_METRICS[name]
    pred = y_score if name in THRESHOLDED_METRICS else y_pred
    measure_normalized = metrics(y_true, pred, normalize=True)
    measure_not_normalized = metrics(y_true, pred, normalize=False)

    assert_array_less(
        -1.0 * measure_normalized,
        0,
        err_msg="We failed to test correctly the normalize option",
    )

        measure_normalized,
        measure_not_normalized / n_samples,
        err_msg=f"Failed with {name}",
    )

def test_normalize_option_multiclass_classification(name):
    # Test in the multiclass case

    y_score = random_state.uniform(size=(n_samples, n_classes))

        0,
    )

    )

def test_normalize_option_multilabel_classification(name):
    # Test in the multilabel case

    # for both random_state 0 and 1, y_true and y_pred has at least one
    # unlabelled entry
    _, y_true = make_multilabel_classification(
    )
    _, y_pred = make_multilabel_classification(
    )

    y_true += [0] * n_classes
    y_pred += [0] * n_classes

        0,
    )

    )

def _check_averaging(
    metric, y_true, y_pred, y_true_binarize, y_pred_binarize, is_multilabel
):
    n_samples, n_classes = y_true_binarize.shape

    # No averaging
    label_measure = metric(y_true, y_pred, average=None)
        label_measure,
        [
            metric(y_true_binarize[:, i], y_pred_binarize[:, i])
            for i in range(n_classes)
        ],
    )

    # Micro measure
    micro_measure = metric(y_true, y_pred, average="micro")
        micro_measure, metric(y_true_binarize.ravel(), y_pred_binarize.ravel())
    )

    # Macro measure
    macro_measure = metric(y_true, y_pred, average="macro")
    assert_allclose(macro_measure, np.mean(label_measure))

    # Weighted measure
    weights = np.sum(y_true_binarize, axis=0, dtype=int)

    if np.sum(weights) != 0:
        weighted_measure = metric(y_true, y_pred, average="weighted")
        assert_allclose(weighted_measure, np.average(label_measure, weights=weights))
    else:
        assert_allclose(weighted_measure, 0)

    # Sample measure
    if is_multilabel:
        sample_measure = metric(y_true, y_pred, average="samples")
            sample_measure,
            np.mean(
                [
                    metric(y_true_binarize[i], y_pred_binarize[i])
                    for i in range(n_samples)
                ]
            ),
        )

        metric(y_true, y_pred, average="unknown")
        metric(y_true, y_pred, average="garbage")

def check_averaging(name, y_true, y_true_binarize, y_pred, y_pred_binarize, y_score):
    is_multilabel = type_of_target(y_true).startswith("multilabel")

    if name in METRICS_WITH_AVERAGING:
        _check_averaging(
            metric, y_true, y_pred, y_true_binarize, y_pred_binarize, is_multilabel
        )
    elif name in THRESHOLDED_METRICS_WITH_AVERAGING:
            metric, y_true, y_score, y_true_binarize, y_score, is_multilabel
        )
    else:
        raise ValueError("Metric is not recorded as having an average option")

def test_averaging_multiclass(name):
    n_samples, n_classes = 50, 3

    lb = LabelBinarizer().fit(y_true)
    y_true_binarize = lb.transform(y_true)
    y_pred_binarize = lb.transform(y_pred)

    check_averaging(name, y_true, y_true_binarize, y_pred, y_pred_binarize, y_score)

def test_averaging_multilabel(name):
    n_samples, n_classes = 40, 5
    _, y = make_multilabel_classification(
        random_state=5,
        allow_unlabeled=False,
    )
    y_true = y[:20]
    y_pred = y[20:]
    y_score = check_random_state(0).normal(size=(20, n_classes))
    y_true_binarize = y_true
    y_pred_binarize = y_pred

def test_averaging_multilabel_all_zeroes(name):
    y_true = np.zeros((20, 3))
    y_pred = np.zeros((20, 3))
    y_score = np.zeros((20, 3))

def test_averaging_binary_multilabel_all_zeroes():
    # Test _average_binary_score for weight.sum() == 0
    binary_metric = lambda y_true, y_score, average="macro": _average_binary_score(
        precision_score, y_true, y_score, average
    )
    _check_averaging(
        binary_metric,
        y_true,
        y_pred,
        y_true_binarize,
        y_pred_binarize,
        is_multilabel=True,
    )

def test_averaging_multilabel_all_ones(name):
    y_true = np.ones((20, 3))
    y_pred = np.ones((20, 3))
    y_score = np.ones((20, 3))

def check_sample_weight_invariance(name, metric, y1, y2):
    sample_weight = rng.randint(1, 10, size=len(y1))

    # top_k_accuracy_score always lead to a perfect score for k > 1 in the
    # binary case
    metric = partial(metric, k=1) if name == "top_k_accuracy_score" else metric

    # check that unit weights gives the same score as no weight
    unweighted_score = metric(y1, y2, sample_weight=None)

        unweighted_score,
        metric(y1, y2, sample_weight=np.ones(shape=len(y1))),
        err_msg="For %s sample_weight=None is not equivalent to sample_weight=ones"
        % name,
    )

    # check that the weighted and unweighted scores are unequal
    weighted_score = metric(y1, y2, sample_weight=sample_weight)

    # use context manager to supply custom error message
        assert_allclose(unweighted_score, weighted_score)
            "Unweighted and weighted scores are unexpectedly "
            "almost equal (%s) and (%s) "
            "for %s" % (unweighted_score, weighted_score, name)
        )

    # check that sample_weight can be a list
    weighted_score_list = metric(y1, y2, sample_weight=sample_weight.tolist())
        weighted_score,
        weighted_score_list,
        err_msg=(
            "Weighted scores for array and list "
            "sample_weight input are not equal (%s != %s) for %s"
        )
        % (weighted_score, weighted_score_list, name),
    )

    # check that integer weights is the same as repeated samples
    repeat_weighted_score = metric(
        np.repeat(y1, sample_weight, axis=0),
        np.repeat(y2, sample_weight, axis=0),
    )
        repeat_weighted_score,
        err_msg="Weighting %s is not equal to repeating samples" % name,
    )

    # check that ignoring a fraction of the samples is equivalent to setting
    # the corresponding weights to zero
    sample_weight_subset = sample_weight[1::2]
    sample_weight_zeroed = np.copy(sample_weight)
    sample_weight_zeroed[::2] = 0
    y1_subset = y1[1::2]
    y2_subset = y2[1::2]
    weighted_score_subset = metric(
        y1_subset, y2_subset, sample_weight=sample_weight_subset
    )
    weighted_score_zeroed = metric(y1, y2, sample_weight=sample_weight_zeroed)
        weighted_score_subset,
        weighted_score_zeroed,
        err_msg=(
            "Zeroing weights does not give the same result as "
            "removing the corresponding samples (%s != %s) for %s"
        )
        % (weighted_score_zeroed, weighted_score_subset, name),
    )

    if not name.startswith("unnormalized"):
        # check that the score is invariant under scaling of the weights by a
        # common factor
        for scaling in [2, 0.3]:
                weighted_score,
                metric(y1, y2, sample_weight=sample_weight * scaling),
                err_msg="%s sample_weight is not invariant under scaling" % name,
            )

    # Check that if number of samples in y_true and sample_weight are not
    # equal, meaningful error is raised.
        r"Found input variables with inconsistent numbers of "
        r"samples: \[{}, {}, {}\]".format(
            _num_samples(y1), _num_samples(y2), _num_samples(sample_weight) * 2
        )
    )
    with pytest.raises(ValueError, match=error_message):
        metric(y1, y2, sample_weight=np.hstack([sample_weight, sample_weight]))

def test_regression_sample_weight_invariance(name):
    # regression
    y_true = random_state.random_sample(size=(n_samples,))
    y_pred = random_state.random_sample(size=(n_samples,))
    check_sample_weight_invariance(name, metric, y_true, y_pred)

def test_binary_sample_weight_invariance(name):
    # binary
    y_true = random_state.randint(0, 2, size=(n_samples,))
    y_pred = random_state.randint(0, 2, size=(n_samples,))
    y_score = random_state.random_sample(size=(n_samples,))
    if name in THRESHOLDED_METRICS:
        check_sample_weight_invariance(name, metric, y_true, y_score)
    else:
        check_sample_weight_invariance(name, metric, y_true, y_pred)

def test_multiclass_sample_weight_invariance(name):
    # multiclass
    y_true = random_state.randint(0, 5, size=(n_samples,))
    y_pred = random_state.randint(0, 5, size=(n_samples,))
    y_score = random_state.random_sample(size=(n_samples, 5))
        # softmax
        temp = np.exp(-y_score)
        y_score_norm = temp / temp.sum(axis=-1).reshape(-1, 1)
        check_sample_weight_invariance(name, metric, y_true, y_score_norm)
    else:

def test_multilabel_sample_weight_invariance(name):
    # multilabel indicator
    _, ya = make_multilabel_classification(
        n_features=1, n_classes=10, random_state=0, n_samples=50, allow_unlabeled=False
    )
    _, yb = make_multilabel_classification(
        n_features=1, n_classes=10, random_state=1, n_samples=50, allow_unlabeled=False
    )
    y_true = np.vstack([ya, yb])
    y_pred = np.vstack([ya, ya])

    else:

def test_multioutput_sample_weight_invariance(name):

def test_no_averaging_labels():
    # test labels argument when not using averaging
    # in multi-class and multi-label cases
    y_true_multilabel = np.array([[1, 1, 0, 0], [1, 1, 0, 0]])
    y_pred_multilabel = np.array([[0, 0, 1, 1], [0, 1, 1, 0]])
    y_true_multiclass = np.array([0, 1, 2])
    y_pred_multiclass = np.array([0, 2, 3])
    labels = np.array([3, 0, 1, 2])
    _, inverse_labels = np.unique(labels, return_inverse=True)

    for name in METRICS_WITH_AVERAGING:
        for y_true, y_pred in [
            [y_true_multiclass, y_pred_multiclass],
            [y_true_multilabel, y_pred_multilabel],
        ]:
            if name not in MULTILABELS_METRICS and y_pred.ndim > 1:
                continue

            metric = ALL_METRICS[name]

            score_labels = metric(y_true, y_pred, labels=labels, average=None)
            score = metric(y_true, y_pred, average=None)
            assert_array_equal(score_labels, score[inverse_labels])

def test_multilabel_label_permutations_invariance(name):
    n_samples, n_classes = 20, 4

    y_true = random_state.randint(0, 2, size=(n_samples, n_classes))
    y_score = random_state.randint(0, 2, size=(n_samples, n_classes))

    score = metric(y_true, y_score)

    for perm in permutations(range(n_classes), n_classes):
        y_score_perm = y_score[:, perm]
        y_true_perm = y_true[:, perm]

        current_score = metric(y_true_perm, y_score_perm)
        assert_almost_equal(score, current_score)

def test_thresholded_multilabel_multioutput_permutations_invariance(name):

    # Makes sure all samples have at least one label. This works around errors
    # when running metrics where average="sample"
    y_true[y_true.sum(1) == 4, 0] = 0
    y_true[y_true.sum(1) == 0, 0] = 1

        if metric == mean_absolute_percentage_error:
            assert np.isfinite(current_score)
            assert current_score > 1e6
            # Here we are not comparing the values in case of MAPE because
            # whenever y_true value is exactly zero, the MAPE value doesn't
            # signify anything. Thus, in this case we are just expecting
            # very large finite value.
        else:
            assert_almost_equal(score, current_score)

def test_thresholded_metric_permutation_invariance(name):
    n_samples, n_classes = 100, 3

    y_score = random_state.rand(n_samples, n_classes)
    temp = np.exp(-y_score)
    y_score = temp / temp.sum(axis=-1).reshape(-1, 1)
    y_true = random_state.randint(0, n_classes, size=n_samples)

        inverse_perm = np.zeros(n_classes, dtype=int)
        inverse_perm[list(perm)] = np.arange(n_classes)
        y_score_perm = y_score[:, inverse_perm]
        y_true_perm = np.take(perm, y_true)

def test_metrics_consistent_type_error(metric_name):
    # check that an understable message is raised when the type between y_true
    # and y_pred mismatch
    y1 = np.array(["spam"] * 3 + ["eggs"] * 2, dtype=object)
    y2 = rng.randint(0, 2, size=y1.size)

    err_msg = "Labels in y_true and y_pred should be of the same type."
        CLASSIFICATION_METRICS[metric_name](y1, y2)

def test_metrics_pos_label_error_str(metric, y_pred_threshold, dtype_y_str):
    # check that the error message if `pos_label` is not specified and the
    # targets is made of strings.
    y1 = np.array(["spam"] * 3 + ["eggs"] * 2, dtype=dtype_y_str)

    if not y_pred_threshold:
        y2 = np.array(["spam", "eggs"], dtype=dtype_y_str)[y2]

    err_msg_pos_label_None = (
        "y_true takes value in {'eggs', 'spam'} and pos_label is not "
        "specified: either make y_true take value in {0, 1} or {-1, 1} or "
        "pass pos_label explicit"
    )
    err_msg_pos_label_1 = (
        r"pos_label=1 is not a valid label. It should be one of \['eggs', 'spam'\]"
    )

    pos_label_default = signature(metric).parameters["pos_label"].default

    err_msg = err_msg_pos_label_1 if pos_label_default == 1 else err_msg_pos_label_None
        metric(y1, y2)

def check_array_api_metric(
    metric, array_namespace, device, dtype_name, a_np, b_np, **metric_kwargs
):

    a_xp = xp.asarray(a_np, device=device)
    b_xp = xp.asarray(b_np, device=device)

    metric_np = metric(a_np, b_np, **metric_kwargs)

    if metric_kwargs.get("sample_weight") is not None:
        metric_kwargs["sample_weight"] = xp.asarray(
            metric_kwargs["sample_weight"], device=device
        )

    multioutput = metric_kwargs.get("multioutput")
    if isinstance(multioutput, np.ndarray):
        metric_kwargs["multioutput"] = xp.asarray(multioutput, device=device)

    # When array API dispatch is disabled, and np.asarray works (for example PyTorch
    # with CPU device), calling the metric function with such numpy compatible inputs
    # should work (albeit by implicitly converting to numpy arrays instead of
    # dispatching to the array library).
    try:
        np.asarray(a_xp)
        np.asarray(b_xp)
        numpy_as_array_works = True
    except (TypeError, RuntimeError):
        # PyTorch with CUDA device and CuPy raise TypeError consistently.
        # array-api-strict chose to raise RuntimeError instead. Exception type
        # may need to be updated in the future for other libraries.
        numpy_as_array_works = False

    if numpy_as_array_works:
        metric_xp = metric(a_xp, b_xp, **metric_kwargs)
            metric_xp,
            metric_np,
            atol=_atol_for_type(dtype_name),
        )
        metric_xp_mixed_1 = metric(a_np, b_xp, **metric_kwargs)
            metric_xp_mixed_1,
        )
        metric_xp_mixed_2 = metric(a_xp, b_np, **metric_kwargs)
            metric_xp_mixed_2,
        )

            _convert_to_numpy(xp.asarray(metric_xp), xp),
        )

def check_array_api_binary_classification_metric(
    metric, array_namespace, device, dtype_name
):
    y_true_np = np.array([0, 0, 1, 1])
    y_pred_np = np.array([0, 1, 0, 1])

    check_array_api_metric(
        metric,
        array_namespace,
        device,
        dtype_name,
        a_np=y_true_np,
        b_np=y_pred_np,
    )

    sample_weight = np.array([0.0, 0.1, 2.0, 1.0], dtype=dtype_name)

        metric,
        device,
    )

def check_array_api_multiclass_classification_metric(
):
    y_true_np = np.array([0, 1, 2, 3])
    y_pred_np = np.array([0, 1, 0, 2])

    additional_params = {
        "average": ("micro", "macro", "weighted"),
        "beta": (0.2, 0.5, 0.8),
    }
    metric_kwargs_combinations = _get_metric_kwargs_for_array_api_testing(
        metric=metric,
        params=additional_params,
    )
    for metric_kwargs in metric_kwargs_combinations:
        check_array_api_metric(
            metric,
            array_namespace,
            device,
            dtype_name,
            a_np=y_true_np,
            b_np=y_pred_np,
            **metric_kwargs,
        )

        sample_weight = np.array([0.0, 0.1, 2.0, 1.0], dtype=dtype_name)

            metric,
            device,
        )

def check_array_api_multilabel_classification_metric(
):
    y_true_np = np.array([[1, 1], [0, 1], [0, 0]], dtype=dtype_name)
    y_pred_np = np.array([[1, 1], [1, 1], [1, 1]], dtype=dtype_name)

    }
    )
            metric,
            device,
        )

        sample_weight = np.array([0.0, 0.1, 2.0], dtype=dtype_name)

            metric,
            device,
        )

def check_array_api_regression_metric(metric, array_namespace, device, dtype_name):
    func_name = metric.func.__name__ if isinstance(metric, partial) else metric.__name__
    if func_name == "mean_poisson_deviance" and sp_version < parse_version("1.14.0"):
            "mean_poisson_deviance's dependency `xlogy` is available as of scipy 1.14.0"
        )

    y_true_np = np.array([2.0, 0.1, 1.0, 4.0], dtype=dtype_name)
    y_pred_np = np.array([0.5, 0.5, 2, 2], dtype=dtype_name)

    metric_kwargs = {}
    metric_params = signature(metric).parameters

    if "sample_weight" in metric_params:
        metric_kwargs["sample_weight"] = None

        metric,
        device,
        **metric_kwargs,
    )

        metric_kwargs["sample_weight"] = np.array(
            [0.1, 2.0, 1.5, 0.5], dtype=dtype_name
        )

            metric,
            device,
        )

def check_array_api_regression_metric_multioutput(
):
    y_true_np = np.array([[1, 3, 2], [1, 2, 2]], dtype=dtype_name)
    y_pred_np = np.array([[1, 4, 4], [1, 1, 1]], dtype=dtype_name)

        metric,
        device,
    )

    sample_weight = np.array([0.1, 2.0], dtype=dtype_name)

        metric,
        device,
    )

        metric,
        device,
        multioutput=np.array([0.1, 0.3, 0.7], dtype=dtype_name),
    )

        metric,
        device,
    )

def check_array_api_metric_pairwise(metric, array_namespace, device, dtype_name):
    X_np = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], dtype=dtype_name)
    Y_np = np.array([[0.2, 0.3, 0.4], [0.5, 0.6, 0.7]], dtype=dtype_name)

    if "dense_output" in signature(metric).parameters:
        metric_kwargs["dense_output"] = False
            metric,
            device,
            a_np=X_np,
            b_np=Y_np,
        )
        metric_kwargs["dense_output"] = True

        metric,
        device,
        a_np=X_np,
        b_np=Y_np,
    )

def yield_metric_checker_combinations(metric_checkers=array_api_metric_checkers):
    for metric, checkers in metric_checkers.items():
        for checker in checkers:
            yield metric, checker

def test_array_api_compliance(metric, array_namespace, device, dtype_name, check_func):
    check_func(metric, array_namespace, device, dtype_name)

def test_metrics_dataframe_series(metric_name, df_lib_name):
    df_lib = pytest.importorskip(df_lib_name)

    y_pred = df_lib.Series([0.0, 1.0, 0, 1.0])
    y_true = df_lib.Series([1.0, 0.0, 0.0, 0.0])

    metric = ALL_METRICS[metric_name]
    try:
        expected_metric = metric(y_pred.to_numpy(), y_true.to_numpy())
        pytest.skip(f"{metric_name} can not deal with 1d inputs")

    assert_allclose(metric(y_pred, y_true), expected_metric)

def _get_metric_kwargs_for_array_api_testing(metric, params):
    """Helper function to enable specifying a variety of additional params and
    their corresponding values, so that they can be passed to a metric function
    when testing for array api compliance."""
    metric_kwargs_combinations = [{}]
    for param, values in params.items():
        if param not in signature(metric).parameters:
            continue

        new_combinations = []
        for kwargs in metric_kwargs_combinations:
            for value in values:
                new_kwargs = kwargs.copy()
                new_kwargs[param] = value
                new_combinations.append(new_kwargs)

        metric_kwargs_combinations = new_combinations

    return metric_kwargs_combinations

    It can either be a float, a numpy array, or a tuple of floats or numpy arrays.
    It should not be a numpy float64 or float32.
    """

    y_true = rng.randint(0, 2, size=(20,))
    y_pred = rng.randint(0, 2, size=(20,))

        y_true = rng.randint(0, 2, size=(20, 3))
        y_pred = rng.randint(0, 2, size=(20, 3))

    score = metric(y_true, y_pred)

    assert isinstance(score, (float, np.ndarray, tuple))

    if isinstance(score, tuple):
        assert all(isinstance(v, float) for v in score) or all(
            isinstance(v, np.ndarray) for v in score
        )

def test_pairwise_distances_for_dense_data(global_dtype):
    # Test the pairwise_distance helper function.

    # Euclidean distance should be equivalent to calling the function.
    X = rng.random_sample((5, 4)).astype(global_dtype, copy=False)
    S = pairwise_distances(X, metric="euclidean")
    S2 = euclidean_distances(X)
    assert_allclose(S, S2)
    assert S.dtype == S2.dtype == global_dtype

    # Euclidean distance, with Y != X.
    Y = rng.random_sample((2, 4)).astype(global_dtype, copy=False)
    S = pairwise_distances(X, Y, metric="euclidean")
    S2 = euclidean_distances(X, Y)

    # Check to ensure NaNs work with pairwise_distances.
    X_masked = rng.random_sample((5, 4)).astype(global_dtype, copy=False)
    Y_masked = rng.random_sample((2, 4)).astype(global_dtype, copy=False)
    X_masked[0, 0] = np.nan
    Y_masked[0, 0] = np.nan
    S_masked = pairwise_distances(X_masked, Y_masked, metric="nan_euclidean")
    S2_masked = nan_euclidean_distances(X_masked, Y_masked)
    assert_allclose(S_masked, S2_masked)
    assert S_masked.dtype == S2_masked.dtype == global_dtype

    # Test with tuples as X and Y
    X_tuples = tuple([tuple([v for v in row]) for row in X])
    Y_tuples = tuple([tuple([v for v in row]) for row in Y])
    S2 = pairwise_distances(X_tuples, Y_tuples, metric="euclidean")

    # Test haversine distance
    # The data should be valid latitude and longitude
    # haversine converts to float64 currently so we don't check dtypes.
    X = rng.random_sample((5, 2)).astype(global_dtype, copy=False)
    X[:, 0] = (X[:, 0] - 0.5) * 2 * np.pi / 2
    X[:, 1] = (X[:, 1] - 0.5) * 2 * np.pi
    S = pairwise_distances(X, metric="haversine")
    S2 = haversine_distances(X)

    # Test haversine distance, with Y != X
    Y = rng.random_sample((2, 2)).astype(global_dtype, copy=False)
    Y[:, 0] = (Y[:, 0] - 0.5) * 2 * np.pi / 2
    Y[:, 1] = (Y[:, 1] - 0.5) * 2 * np.pi
    S = pairwise_distances(X, Y, metric="haversine")
    S2 = haversine_distances(X, Y)

    # "cityblock" uses scikit-learn metric, cityblock (function) is
    # scipy.spatial.
    # The metric functions from scipy converts to float64 so we don't check the dtypes.
    S = pairwise_distances(X, metric="cityblock")
    S2 = pairwise_distances(X, metric=cityblock)
    assert S.shape[0] == S.shape[1]
    assert S.shape[0] == X.shape[0]

    # The manhattan metric should be equivalent to cityblock.
    S = pairwise_distances(X, Y, metric="manhattan")
    S2 = pairwise_distances(X, Y, metric=cityblock)
    assert S.shape[1] == Y.shape[0]

    # Test cosine as a string metric versus cosine callable
    # The string "cosine" uses sklearn.metric,
    # while the function cosine is scipy.spatial
    S = pairwise_distances(X, Y, metric="cosine")
    S2 = pairwise_distances(X, Y, metric=cosine)

def test_pairwise_distances_for_sparse_data(
    coo_container, csc_container, bsr_container, csr_container, global_dtype
):

    # Test with sparse X and Y,
    # currently only supported for Euclidean, L1 and cosine.
    Y_sparse = csr_container(Y)

    S = pairwise_distances(X_sparse, Y_sparse, metric="euclidean")
    S2 = euclidean_distances(X_sparse, Y_sparse)

    S = pairwise_distances(X_sparse, Y_sparse, metric="cosine")
    S2 = cosine_distances(X_sparse, Y_sparse)

    S = pairwise_distances(X_sparse, csc_container(Y), metric="manhattan")
    S2 = manhattan_distances(bsr_container(X), coo_container(Y))
    if global_dtype == np.float64:
        assert S.dtype == S2.dtype == global_dtype
    else:
        # TODO Fix manhattan_distances to preserve dtype.
        # currently pairwise_distances uses manhattan_distances but converts the result
        # back to the input dtype
        with pytest.raises(AssertionError):
            assert S.dtype == S2.dtype == global_dtype

    S2 = manhattan_distances(X, Y)
    else:

    # Test with scipy.spatial.distance metric, with a kwd
    kwds = {"p": 2.0}
    S = pairwise_distances(X, Y, metric="minkowski", **kwds)
    S2 = pairwise_distances(X, Y, metric=minkowski, **kwds)

    # same with Y = None
    S = pairwise_distances(X, metric="minkowski", **kwds)
    S2 = pairwise_distances(X, metric=minkowski, **kwds)

    # Test that scipy distance metrics throw an error if sparse matrix given
        pairwise_distances(X_sparse, metric="minkowski")
        pairwise_distances(X, Y_sparse, metric="minkowski")

def test_pairwise_boolean_distance(metric):
    # test that we convert to boolean arrays for boolean distances
    X = rng.randn(5, 4)
    Y = X.copy()
    Y[0, 0] = 1 - Y[0, 0]

    # ignore conversion to boolean in pairwise_distances
    with ignore_warnings(category=DataConversionWarning):
        for Z in [Y, None]:
            res = pairwise_distances(X, Z, metric=metric)
            np.nan_to_num(res, nan=0, posinf=0, neginf=0, copy=False)
            assert np.sum(res != 0) == 0

    # non-boolean arrays are converted to boolean for boolean
    # distance metrics with a data conversion warning
    msg = "Data was converted to boolean for metric %s" % metric
    with pytest.warns(DataConversionWarning, match=msg):
        pairwise_distances(X, metric=metric)

    # Check that the warning is raised if X is boolean by Y is not boolean:
        pairwise_distances(X.astype(bool), Y=Y, metric=metric)

    # Check that no warning is raised if X is already boolean and Y is None:
        warnings.simplefilter("error", DataConversionWarning)
        pairwise_distances(X.astype(bool), metric=metric)

def test_no_data_conversion_warning():
    # No warnings issued if metric is not a boolean distance function
        pairwise_distances(X, metric="minkowski")

def test_pairwise_precomputed(func):
    # Test correct shape
    with pytest.raises(ValueError, match=".* shape .*"):
        func(np.zeros((5, 3)), metric="precomputed")
    # with two args
        func(np.zeros((5, 3)), np.zeros((4, 4)), metric="precomputed")
    # even if shape[1] agrees (although thus second arg is spurious)
        func(np.zeros((5, 3)), np.zeros((4, 3)), metric="precomputed")

    # Test not copied (if appropriate dtype)
    S = np.zeros((5, 5))
    S2 = func(S, metric="precomputed")
    assert S is S2
    S = np.zeros((5, 3))
    S2 = func(S, np.zeros((3, 3)), metric="precomputed")

    # Test always returns float dtype
    S = func(np.array([[1]], dtype="int"), metric="precomputed")
    assert "f" == S.dtype.kind

    # Test converts list to array-like
    S = func([[1.0]], metric="precomputed")
    assert isinstance(S, np.ndarray)

def test_pairwise_precomputed_non_negative():
    # Test non-negative values
    with pytest.raises(ValueError, match=".* non-negative values.*"):
        pairwise_distances(np.full((5, 5), -1), metric="precomputed")

def callable_rbf_kernel(x, y, **kwds):
    # Callable version of pairwise.rbf_kernel.
    K = rbf_kernel(np.atleast_2d(x), np.atleast_2d(y), **kwds)
    # unpack the output since this is a scalar packed in a 0-dim array
    return K.item()

def test_pairwise_parallel(func, metric, kwds, dtype):
    X = np.array(5 * rng.random_sample((5, 4)), dtype=dtype)
    Y = np.array(5 * rng.random_sample((3, 4)), dtype=dtype)

    S = func(X, metric=metric, n_jobs=1, **kwds)
    S2 = func(X, metric=metric, n_jobs=2, **kwds)

    S = func(X, Y, metric=metric, n_jobs=1, **kwds)
    S2 = func(X, Y, metric=metric, n_jobs=2, **kwds)

def test_pairwise_callable_nonstrict_metric():
    # paired_distances should allow callable metric where metric(x, x) != 0
    # Knowing that the callable is a strict metric would allow the diagonal to
    # be left uncalculated and set to 0.
    assert pairwise_distances([[1.0]], metric=lambda x, y: 5)[0, 0] == 5

def test_pairwise_kernels(metric, csr_container):
    # Test the pairwise_kernels helper function.

    X = rng.random_sample((5, 4))
    Y = rng.random_sample((2, 4))
    function = PAIRWISE_KERNEL_FUNCTIONS[metric]
    # Test with Y=None
    K1 = pairwise_kernels(X, metric=metric)
    K2 = function(X)
    assert_allclose(K1, K2)
    # Test with Y=Y
    K1 = pairwise_kernels(X, Y=Y, metric=metric)
    K2 = function(X, Y=Y)
    K2 = pairwise_kernels(X_tuples, Y_tuples, metric=metric)

    # Test with sparse X and Y
    if metric in ["chi2", "additive_chi2"]:
        # these don't support sparse matrices yet
        return
    K1 = pairwise_kernels(X_sparse, Y=Y_sparse, metric=metric)

def test_pairwise_kernels_callable():
    # Test the pairwise_kernels helper function
    # with a callable function, with given keywords.

    metric = callable_rbf_kernel
    kwds = {"gamma": 0.1}
    K1 = pairwise_kernels(X, Y=Y, metric=metric, **kwds)
    K2 = rbf_kernel(X, Y=Y, **kwds)

    # callable function, X=Y
    K1 = pairwise_kernels(X, Y=X, metric=metric, **kwds)
    K2 = rbf_kernel(X, Y=X, **kwds)

def test_pairwise_kernels_filter_param():
    K = rbf_kernel(X, Y, gamma=0.1)
    params = {"gamma": 0.1, "blabla": ":)"}
    K2 = pairwise_kernels(X, Y, metric="rbf", filter_params=True, **params)
    assert_allclose(K, K2)

        pairwise_kernels(X, Y, metric="rbf", **params)

def test_paired_distances(metric, func, csr_container):
    Y = rng.random_sample((5, 4))

    S = paired_distances(X, Y, metric=metric)
    S2 = func(X, Y)
    S3 = func(csr_container(X), csr_container(Y))
    assert_allclose(S, S3)
    if metric in PAIRWISE_DISTANCE_FUNCTIONS:
        # Check the pairwise_distances implementation
        # gives the same value
        distances = PAIRWISE_DISTANCE_FUNCTIONS[metric](X, Y)
        distances = np.diag(distances)
        assert_allclose(distances, S)

def test_paired_distances_callable(global_dtype):
    # Test the paired_distance helper function
    # with the callable implementation
    Y = rng.random_sample((5, 4)).astype(global_dtype, copy=False)

    S = paired_distances(X, Y, metric="manhattan")
    S2 = paired_distances(X, Y, metric=lambda x, y: np.abs(x - y).sum(axis=0))

    # Test that a value error is raised when the lengths of X and Y should not
    # differ
    Y = rng.random_sample((3, 4))
        paired_distances(X, Y)

def test_pairwise_distances_argmin_min(dok_container, csr_container, global_dtype):
    # Check pairwise minimum distances computation for any metric
    X = np.asarray([[0], [1]], dtype=global_dtype)
    Y = np.asarray([[-2], [3]], dtype=global_dtype)

    Xsp = dok_container(X)
    Ysp = csr_container(Y, dtype=global_dtype)

    expected_idx = [0, 1]
    expected_vals = [2, 2]
    expected_vals_sq = [4, 4]

    # euclidean metric
    idx, vals = pairwise_distances_argmin_min(X, Y, metric="euclidean")
    idx2 = pairwise_distances_argmin(X, Y, metric="euclidean")
    assert_allclose(idx, expected_idx)
    assert_allclose(idx2, expected_idx)
    assert_allclose(vals, expected_vals)
    # sparse matrix case
    idxsp, valssp = pairwise_distances_argmin_min(Xsp, Ysp, metric="euclidean")
    idxsp2 = pairwise_distances_argmin(Xsp, Ysp, metric="euclidean")
    assert_allclose(idxsp, expected_idx)
    assert_allclose(idxsp2, expected_idx)
    assert_allclose(valssp, expected_vals)
    # We don't want np.matrix here
    assert type(idxsp) == np.ndarray
    assert type(valssp) == np.ndarray

    # Squared Euclidean metric
    idx, vals = pairwise_distances_argmin_min(X, Y, metric="sqeuclidean")
    idx2, vals2 = pairwise_distances_argmin_min(
        X, Y, metric="euclidean", metric_kwargs={"squared": True}
    )
    idx3 = pairwise_distances_argmin(X, Y, metric="sqeuclidean")
    idx4 = pairwise_distances_argmin(
    )

    assert_allclose(vals, expected_vals_sq)
    assert_allclose(vals2, expected_vals_sq)

    assert_allclose(idx3, expected_idx)
    assert_allclose(idx4, expected_idx)

    # Non-euclidean scikit-learn metric
    idx, vals = pairwise_distances_argmin_min(X, Y, metric="manhattan")
    idx2 = pairwise_distances_argmin(X, Y, metric="manhattan")
    idxsp, valssp = pairwise_distances_argmin_min(Xsp, Ysp, metric="manhattan")
    idxsp2 = pairwise_distances_argmin(Xsp, Ysp, metric="manhattan")

    # Non-euclidean Scipy distance (callable)
    idx, vals = pairwise_distances_argmin_min(
        X, Y, metric=minkowski, metric_kwargs={"p": 2}
    )

    # Non-euclidean Scipy distance (string)
        X, Y, metric="minkowski", metric_kwargs={"p": 2}
    )

    # Compare with naive implementation
    X = rng.randn(97, 149)
    Y = rng.randn(111, 149)

    dist = pairwise_distances(X, Y, metric="manhattan")
    dist_orig_ind = dist.argmin(axis=0)
    dist_orig_val = dist[dist_orig_ind, range(len(dist_orig_ind))]

    dist_chunked_ind, dist_chunked_val = pairwise_distances_argmin_min(
        X, Y, axis=0, metric="manhattan"
    )
    assert_allclose(dist_orig_ind, dist_chunked_ind, rtol=1e-7)
    assert_allclose(dist_orig_val, dist_chunked_val, rtol=1e-7)

    # Changing the axis and permuting datasets must give the same results
    argmin_0, dist_0 = pairwise_distances_argmin_min(X, Y, axis=0)
    argmin_1, dist_1 = pairwise_distances_argmin_min(Y, X, axis=1)

    assert_allclose(dist_0, dist_1)
    assert_array_equal(argmin_0, argmin_1)

    argmin_0, dist_0 = pairwise_distances_argmin_min(X, X, axis=0)
    argmin_1, dist_1 = pairwise_distances_argmin_min(X, X, axis=1)

    argmin_0 = pairwise_distances_argmin(X, Y, axis=0)
    argmin_1 = pairwise_distances_argmin(Y, X, axis=1)

    argmin_0 = pairwise_distances_argmin(X, X, axis=0)
    argmin_1 = pairwise_distances_argmin(X, X, axis=1)

    # F-contiguous arrays must be supported and must return identical results.
    argmin_C_contiguous = pairwise_distances_argmin(X, Y)
    argmin_F_contiguous = pairwise_distances_argmin(
        np.asfortranarray(X), np.asfortranarray(Y)
    )

    assert_array_equal(argmin_C_contiguous, argmin_F_contiguous)

def test_pairwise_distances_chunked_reduce(global_dtype):
    X = rng.random_sample((400, 4)).astype(global_dtype, copy=False)
    # Reduced Euclidean distance
    S = pairwise_distances(X)[:, :100]
    S_chunks = pairwise_distances_chunked(
        X, None, reduce_func=_reduce_func, working_memory=2**-16
    )
    assert isinstance(S_chunks, GeneratorType)
    S_chunks = list(S_chunks)
    assert len(S_chunks) > 1
    assert S_chunks[0].dtype == X.dtype

    # atol is for diagonal where S is explicitly zeroed on the diagonal
    assert_allclose(np.vstack(S_chunks), S, atol=1e-7)

def test_pairwise_distances_chunked_reduce_none(global_dtype):
    # check that the reduce func is allowed to return None
    X = rng.random_sample((10, 4)).astype(global_dtype, copy=False)
        X, None, reduce_func=lambda dist, start: None, working_memory=2**-16
    )
    assert all(chunk is None for chunk in S_chunks)

def test_pairwise_distances_chunked_reduce_valid(good_reduce):
    X = np.arange(10).reshape(-1, 1)
        X, None, reduce_func=good_reduce, working_memory=64
    )
    next(S_chunks)

def test_pairwise_distances_chunked_reduce_invalid(
    global_dtype, bad_reduce, err_type, message
):
    X = np.arange(10).reshape(-1, 1).astype(global_dtype, copy=False)
        X, None, reduce_func=bad_reduce, working_memory=64
    )
    with pytest.raises(err_type, match=message):
        next(S_chunks)

def check_pairwise_distances_chunked(X, Y, working_memory, metric="euclidean"):
    gen = pairwise_distances_chunked(X, Y, working_memory=working_memory, metric=metric)
    assert isinstance(gen, GeneratorType)
    blockwise_distances = list(gen)
    Y = X if Y is None else Y
    min_block_mib = len(Y) * 8 * 2**-20

    for block in blockwise_distances:
        memory_used = block.nbytes
        assert memory_used <= max(working_memory, min_block_mib) * 2**20

    blockwise_distances = np.vstack(blockwise_distances)
    S = pairwise_distances(X, Y, metric=metric)
    assert_allclose(blockwise_distances, S, atol=1e-7)

def test_pairwise_distances_chunked_diagonal(metric, global_dtype):
    X = rng.normal(size=(1000, 10), scale=1e10).astype(global_dtype, copy=False)
    chunks = list(pairwise_distances_chunked(X, working_memory=1, metric=metric))
    assert len(chunks) > 1
    assert_allclose(np.diag(np.vstack(chunks)), 0, rtol=1e-10)

def test_parallel_pairwise_distances_diagonal(metric, global_dtype):
    distances = pairwise_distances(X, metric=metric, n_jobs=2)
    assert_allclose(np.diag(distances), 0, atol=1e-10)

def test_pairwise_distances_chunked(global_dtype):
    X = rng.random_sample((200, 4)).astype(global_dtype, copy=False)
    check_pairwise_distances_chunked(X, None, working_memory=1, metric="euclidean")
    # Test small amounts of memory
    for power in range(-16, 0):
        check_pairwise_distances_chunked(
            X, None, working_memory=2**power, metric="euclidean"
        )
    # X as list
    check_pairwise_distances_chunked(
        X.tolist(), None, working_memory=1, metric="euclidean"
    )
    Y = rng.random_sample((100, 4)).astype(global_dtype, copy=False)
    check_pairwise_distances_chunked(X, Y, working_memory=1, metric="euclidean")
        X.tolist(), Y.tolist(), working_memory=1, metric="euclidean"
    )
    # absurdly large working_memory
    check_pairwise_distances_chunked(X, Y, working_memory=10000, metric="euclidean")
    check_pairwise_distances_chunked(X, Y, working_memory=1, metric="cityblock")

    # Test precomputed returns all at once
    D = pairwise_distances(X)
    gen = pairwise_distances_chunked(D, working_memory=2**-16, metric="precomputed")
    assert next(gen) is D
    with pytest.raises(StopIteration):
        next(gen)

def test_euclidean_distances_known_result(x_array_constr, y_array_constr):
    # Check the pairwise Euclidean distances computation on known result
    X = x_array_constr([[0]])
    Y = y_array_constr([[1], [2]])
    D = euclidean_distances(X, Y)
    assert_allclose(D, [[1.0, 2.0]])

def test_euclidean_distances_with_norms(global_dtype, y_array_constr):
    # check that we still get the right answers with {X,Y}_norm_squared
    # and that we get a wrong answer with wrong {X,Y}_norm_squared
    X = rng.random_sample((10, 10)).astype(global_dtype, copy=False)
    Y = rng.random_sample((20, 10)).astype(global_dtype, copy=False)

    # norms will only be used if their dtype is float64
    X_norm_sq = (X.astype(np.float64) ** 2).sum(axis=1).reshape(1, -1)
    Y_norm_sq = (Y.astype(np.float64) ** 2).sum(axis=1).reshape(1, -1)

    Y = y_array_constr(Y)

    D1 = euclidean_distances(X, Y)
    D2 = euclidean_distances(X, Y, X_norm_squared=X_norm_sq)
    D3 = euclidean_distances(X, Y, Y_norm_squared=Y_norm_sq)
    D4 = euclidean_distances(X, Y, X_norm_squared=X_norm_sq, Y_norm_squared=Y_norm_sq)
    assert_allclose(D2, D1)
    assert_allclose(D3, D1)
    assert_allclose(D4, D1)

    # check we get the wrong answer with wrong {X,Y}_norm_squared
    wrong_D = euclidean_distances(
        X,
        Y,
        X_norm_squared=np.zeros_like(X_norm_sq),
        Y_norm_squared=np.zeros_like(Y_norm_sq),
    )
        assert_allclose(wrong_D, D1)

def test_euclidean_distances_float32_norms(global_random_seed, symmetric):
    # Non-regression test for #27621
    X = rng.random_sample((10, 10))
    Y = X if symmetric else rng.random_sample((20, 10))
    X_norm_sq = (X.astype(np.float32) ** 2).sum(axis=1).reshape(1, -1)
    Y_norm_sq = (Y.astype(np.float32) ** 2).sum(axis=1).reshape(1, -1)

def test_euclidean_distances_norm_shapes():
    # Check all accepted shapes for the norms or appropriate error messages.
    Y = rng.random_sample((20, 10))

    X_norm_squared = (X**2).sum(axis=1)
    Y_norm_squared = (Y**2).sum(axis=1)

    D1 = euclidean_distances(
        X, Y, X_norm_squared=X_norm_squared, Y_norm_squared=Y_norm_squared
    )
    D2 = euclidean_distances(
        X,
        Y,
        X_norm_squared=X_norm_squared.reshape(-1, 1),
        Y_norm_squared=Y_norm_squared.reshape(-1, 1),
    )
    D3 = euclidean_distances(
        X,
        Y,
        X_norm_squared=X_norm_squared.reshape(1, -1),
        Y_norm_squared=Y_norm_squared.reshape(1, -1),
    )

    with pytest.raises(ValueError, match="Incompatible dimensions for X"):
        euclidean_distances(X, Y, X_norm_squared=X_norm_squared[:5])
    with pytest.raises(ValueError, match="Incompatible dimensions for Y"):
        euclidean_distances(X, Y, Y_norm_squared=Y_norm_squared[:5])

def test_euclidean_distances(global_dtype, x_array_constr, y_array_constr):
    # check that euclidean distances gives same result as scipy cdist
    # when X and Y != X are provided
    X = rng.random_sample((100, 10)).astype(global_dtype, copy=False)
    X[X < 0.8] = 0
    Y = rng.random_sample((10, 10)).astype(global_dtype, copy=False)
    Y[Y < 0.8] = 0

    expected = cdist(X, Y)

    X = x_array_constr(X)
    distances = euclidean_distances(X, Y)

    # the default rtol=1e-7 is too close to the float32 precision
    # and fails due to rounding errors.
    assert_allclose(distances, expected, rtol=1e-6)
    assert distances.dtype == global_dtype

def test_euclidean_distances_sym(global_dtype, x_array_constr):
    # check that euclidean distances gives same result as scipy pdist
    # when only X is provided

    expected = squareform(pdist(X))

    distances = euclidean_distances(X)

def test_euclidean_distances_upcast(batch_size, x_array_constr, y_array_constr):
    # check batches handling when Y != X (#13910)
    X = rng.random_sample((100, 10)).astype(np.float32)
    Y = rng.random_sample((10, 10)).astype(np.float32)

    distances = _euclidean_distances_upcast(X, Y=Y, batch_size=batch_size)
    distances = np.sqrt(np.maximum(distances, 0))

def test_euclidean_distances_upcast_sym(batch_size, x_array_constr):
    # check batches handling when X is Y (#13910)

    distances = _euclidean_distances_upcast(X, Y=X, batch_size=batch_size)

def test_euclidean_distances_extreme_values(dtype, eps, rtol, dim):
    # check that euclidean distances is correct with float32 input thanks to
    # upcasting. On float64 there are still precision issues.
    X = np.array([[1.0] * dim], dtype=dtype)
    Y = np.array([[1.0 + eps] * dim], dtype=dtype)

    assert_allclose(distances, expected, rtol=1e-5)

def test_nan_euclidean_distances_equal_to_euclidean_distance(squared):
    # with no nan values
    rng = np.random.RandomState(1337)
    X = rng.randn(3, 4)
    Y = rng.randn(4, 4)

    normal_distance = euclidean_distances(X, Y=Y, squared=squared)
    nan_distance = nan_euclidean_distances(X, Y=Y, squared=squared)
    assert_allclose(normal_distance, nan_distance)

def test_nan_euclidean_distances_infinite_values(X, Y):
    with pytest.raises(ValueError) as excinfo:
        nan_euclidean_distances(X, Y=Y)

    exp_msg = "Input contains infinity or a value too large for dtype('float64')."
    assert exp_msg == str(excinfo.value)

def test_nan_euclidean_distances_2x2(X, X_diag, missing_value):
    exp_dist = np.array([[0.0, X_diag], [X_diag, 0]])

    dist = nan_euclidean_distances(X, missing_values=missing_value)
    assert_allclose(exp_dist, dist)

    dist_sq = nan_euclidean_distances(X, squared=True, missing_values=missing_value)
    assert_allclose(exp_dist**2, dist_sq)

    dist_two = nan_euclidean_distances(X, X, missing_values=missing_value)
    assert_allclose(exp_dist, dist_two)

    dist_two_copy = nan_euclidean_distances(X, X.copy(), missing_values=missing_value)
    assert_allclose(exp_dist, dist_two_copy)

def test_nan_euclidean_distances_complete_nan(missing_value):
    X = np.array([[missing_value, missing_value], [0, 1]])

    exp_dist = np.array([[np.nan, np.nan], [np.nan, 0]])

    dist = nan_euclidean_distances(X, X.copy(), missing_values=missing_value)

def test_nan_euclidean_distances_not_trival(missing_value):
        [
            [1.0, missing_value, 3.0, 4.0, 2.0],
            [missing_value, 4.0, 6.0, 1.0, missing_value],
            [3.0, missing_value, missing_value, missing_value, 1.0],
        ]
    )

    Y = np.array(
        [
            [missing_value, 7.0, 7.0, missing_value, 2.0],
            [missing_value, missing_value, 5.0, 4.0, 7.0],
            [missing_value, missing_value, missing_value, 4.0, 5.0],
        ]
    )

    # Check for symmetry
    D1 = nan_euclidean_distances(X, Y, missing_values=missing_value)
    D2 = nan_euclidean_distances(Y, X, missing_values=missing_value)

    assert_almost_equal(D1, D2.T)

    # Check with explicit formula and squared=True
        nan_euclidean_distances(
            X[:1], Y[:1], squared=True, missing_values=missing_value
        ),
        [[5.0 / 2.0 * ((7 - 3) ** 2 + (2 - 2) ** 2)]],
    )

    # Check with explicit formula and squared=False
            X[1:2], Y[1:2], squared=False, missing_values=missing_value
        ),
        [[np.sqrt(5.0 / 2.0 * ((6 - 5) ** 2 + (1 - 4) ** 2))]],
    )

    # Check when Y = X is explicitly passed
    D3 = nan_euclidean_distances(X, missing_values=missing_value)
    D4 = nan_euclidean_distances(X, X, missing_values=missing_value)
    D5 = nan_euclidean_distances(X, X.copy(), missing_values=missing_value)
    assert_allclose(D3, D4)
    assert_allclose(D4, D5)

    # Check copy = True against copy = False
    D6 = nan_euclidean_distances(X, Y, copy=True)
    D7 = nan_euclidean_distances(X, Y, copy=False)
    assert_allclose(D6, D7)

def test_nan_euclidean_distances_one_feature_match_positive(missing_value):
    # First feature is the only feature that is non-nan and in both
    # samples. The result of `nan_euclidean_distances` with squared=True
    # should be non-negative. The non-squared version should all be close to 0.
        [
            [-122.27, 648.0, missing_value, 37.85],
            [-122.27, missing_value, 2.34701493, missing_value],
        ]
    )

    dist_squared = nan_euclidean_distances(
        X, missing_values=missing_value, squared=True
    )
    assert np.all(dist_squared >= 0)

    dist = nan_euclidean_distances(X, missing_values=missing_value, squared=False)
    assert_allclose(dist, 0.0)

def test_cosine_distances():
    # Check the pairwise Cosine distances computation
    x = np.abs(rng.rand(910))
    XA = np.vstack([x, x])
    D = cosine_distances(XA)
    assert_allclose(D, [[0.0, 0.0], [0.0, 0.0]], atol=1e-10)
    # check that all elements are in [0, 2]
    assert np.all(D >= 0.0)
    assert np.all(D <= 2.0)
    # check that diagonal elements are equal to 0
    assert_allclose(D[np.diag_indices_from(D)], [0.0, 0.0])

    XB = np.vstack([x, -x])
    D2 = cosine_distances(XB)
    assert np.all(D2 >= 0.0)
    assert np.all(D2 <= 2.0)
    # check that diagonal elements are equal to 0 and non diagonal to 2
    assert_allclose(D2, [[0.0, 2.0], [2.0, 0.0]])

    # check large random matrix
    X = np.abs(rng.rand(1000, 5000))
    D = cosine_distances(X)
    assert_allclose(D[np.diag_indices_from(D)], [0.0] * D.shape[0])

def test_haversine_distances():
    # Check haversine distance with distances computation
    def slow_haversine_distances(x, y):
        diff_lat = y[0] - x[0]
        diff_lon = y[1] - x[1]
        a = np.sin(diff_lat / 2) ** 2 + (
            np.cos(x[0]) * np.cos(y[0]) * np.sin(diff_lon / 2) ** 2
        )
        c = 2 * np.arcsin(np.sqrt(a))
        return c

    X = rng.random_sample((5, 2))
    Y = rng.random_sample((10, 2))
    D1 = np.array([[slow_haversine_distances(x, y) for y in Y] for x in X])
    D2 = haversine_distances(X, Y)
    assert_allclose(D1, D2)
    # Test haversine distance does not accept X where n_feature != 2
    X = rng.random_sample((10, 3))
    err_msg = "Haversine distance only valid in 2 dimensions"
        haversine_distances(X)

def test_paired_euclidean_distances():
    # Check the paired Euclidean distances computation
    X = [[0], [0]]
    Y = [[1], [2]]
    D = paired_euclidean_distances(X, Y)
    assert_allclose(D, [1.0, 2.0])

def test_paired_manhattan_distances():
    # Check the paired manhattan distances computation
    D = paired_manhattan_distances(X, Y)

def test_paired_cosine_distances():
    D = paired_cosine_distances(X, Y)
    assert_allclose(D, [0.5, 0.5])

def test_chi_square_kernel():
    Y = rng.random_sample((10, 4))
    K_add = additive_chi2_kernel(X, Y)
    gamma = 0.1
    K = chi2_kernel(X, Y, gamma=gamma)
    assert K.dtype == float
    for i, x in enumerate(X):
        for j, y in enumerate(Y):
            chi2 = -np.sum((x - y) ** 2 / (x + y))
            chi2_exp = np.exp(gamma * chi2)
            assert_almost_equal(K_add[i, j], chi2)
            assert_almost_equal(K[i, j], chi2_exp)

    # check diagonal is ones for data with itself
    K = chi2_kernel(Y)
    assert_array_equal(np.diag(K), 1)
    # check off-diagonal is < 1 but > 0:
    assert np.all(K > 0)
    assert np.all(K - np.diag(np.diag(K)) < 1)
    # check that float32 is preserved
    X = rng.random_sample((5, 4)).astype(np.float32)
    Y = rng.random_sample((10, 4)).astype(np.float32)
    K = chi2_kernel(X, Y)
    assert K.dtype == np.float32

    # check integer type gets converted,
    # check that zeros are handled
    X = rng.random_sample((10, 4)).astype(np.int32)
    K = chi2_kernel(X, X)
    assert np.isfinite(K).all()

    # check that kernel of similar things is greater than dissimilar ones
    X = [[0.3, 0.7], [1.0, 0]]
    Y = [[0, 1], [0.9, 0.1]]
    assert K[0, 0] > K[0, 1]
    assert K[1, 1] > K[1, 0]

    # test negative input
        chi2_kernel([[0, -1]])
        chi2_kernel([[0, -1]], [[-1, -1]])
        chi2_kernel([[0, 1]], [[-1, -1]])

    # different n_features in X and Y
        chi2_kernel([[0, 1]], [[0.2, 0.2, 0.6]])

def test_kernel_symmetry(kernel):
    # Valid kernels should be symmetric
    K = kernel(X, X)
    assert_allclose(K, K.T, 15)

def test_kernel_sparse(kernel, csr_container):
    K2 = kernel(X_sparse, X_sparse)

def test_linear_kernel():
    K = linear_kernel(X, X)
    # the diagonal elements of a linear kernel are their squared norm
    assert_allclose(K.flat[::6], [linalg.norm(x) ** 2 for x in X])

def test_rbf_kernel():
    K = rbf_kernel(X, X)
    # the diagonal elements of a rbf kernel are 1
    assert_allclose(K.flat[::6], np.ones(5))

def test_laplacian_kernel():
    K = laplacian_kernel(X, X)
    # the diagonal elements of a laplacian kernel are 1
    assert_allclose(np.diag(K), np.ones(5))

    # off-diagonal elements are < 1 but > 0:

def test_pairwise_similarity_sparse_output(metric, pairwise_func, csr_container):
    Xcsr = csr_container(X)
    Ycsr = csr_container(Y)

    # should be sparse
    K1 = pairwise_func(Xcsr, Ycsr, dense_output=False)
    assert issparse(K1)

    # should be dense, and equal to K1
    K2 = pairwise_func(X, Y, dense_output=True)
    assert not issparse(K2)
    assert_allclose(K1.toarray(), K2)

    # show the kernel output equal to the sparse.toarray()
    K3 = pairwise_kernels(X, Y=Y, metric=metric)
    assert_allclose(K1.toarray(), K3)

def test_cosine_similarity(csr_container):
    # Test the cosine_similarity.

    for X_, Y_ in ((X, None), (X, Y), (Xcsr, None), (Xcsr, Ycsr)):
        # Test that the cosine is kernel is equal to a linear kernel when data
        # has been previously normalized by L2-norm.
        K1 = pairwise_kernels(X_, Y=Y_, metric="cosine")
        X_ = normalize(X_)
        if Y_ is not None:
            Y_ = normalize(Y_)
        K2 = pairwise_kernels(X_, Y=Y_, metric="linear")
        assert_allclose(K1, K2)

def test_check_dense_matrices():
    # Ensure that pairwise array check works for dense matrices.
    # Check that if XB is None, XB is returned as reference to XA
    XA = np.resize(np.arange(40), (5, 8))
    XA_checked, XB_checked = check_pairwise_arrays(XA, None)
    assert XA_checked is XB_checked
    assert_array_equal(XA, XA_checked)

def test_check_XB_returned():
    # Ensure that if XA and XB are given correctly, they return as equal.
    # Check that if XB is not None, it is returned equal.
    # Note that the second dimension of XB is the same as XA.
    XB = np.resize(np.arange(32), (4, 8))
    XA_checked, XB_checked = check_pairwise_arrays(XA, XB)
    assert_array_equal(XB, XB_checked)

    XB = np.resize(np.arange(40), (5, 8))
    XA_checked, XB_checked = check_paired_arrays(XA, XB)

def test_check_different_dimensions():
    # Ensure an error is raised if the dimensions are different.
    XA = np.resize(np.arange(45), (5, 9))
        check_pairwise_arrays(XA, XB)

    XB = np.resize(np.arange(4 * 9), (4, 9))
        check_paired_arrays(XA, XB)

def test_check_invalid_dimensions():
    # Ensure an error is raised on 1D input arrays.
    # The modified tests are not 1D. In the old test, the array was internally
    # converted to 2D anyways
    XA = np.arange(45).reshape(9, 5)
    XB = np.arange(32).reshape(4, 8)

def test_check_sparse_arrays(csr_container):
    # Ensures that checks return valid sparse matrices.
    XA = rng.random_sample((5, 4))
    XA_sparse = csr_container(XA)
    XB = rng.random_sample((5, 4))
    XB_sparse = csr_container(XB)
    XA_checked, XB_checked = check_pairwise_arrays(XA_sparse, XB_sparse)
    # compare their difference because testing csr matrices for
    # equality with '==' does not work as expected.
    assert issparse(XA_checked)
    assert abs(XA_sparse - XA_checked).sum() == 0
    assert issparse(XB_checked)
    assert abs(XB_sparse - XB_checked).sum() == 0

    XA_checked, XA_2_checked = check_pairwise_arrays(XA_sparse, XA_sparse)
    assert issparse(XA_2_checked)
    assert abs(XA_2_checked - XA_checked).sum() == 0

def tuplify(X):
    # Turns a numpy matrix (any n-dimensional array) into tuples.
    s = X.shape
    if len(s) > 1:
        # Tuplify each sub-array in the input.
        return tuple(tuplify(row) for row in X)
    else:
        # Single dimension input, just return tuple of contents.
        return tuple(r for r in X)

def test_check_tuple_input():
    # Ensures that checks return valid tuples.
    XA_tuples = tuplify(XA)
    XB_tuples = tuplify(XB)
    XA_checked, XB_checked = check_pairwise_arrays(XA_tuples, XB_tuples)
    assert_array_equal(XA_tuples, XA_checked)
    assert_array_equal(XB_tuples, XB_checked)

def test_check_preserve_type():
    # Ensures that type float32 is preserved.
    XA = np.resize(np.arange(40), (5, 8)).astype(np.float32)
    XB = np.resize(np.arange(40), (5, 8)).astype(np.float32)

    assert XA_checked.dtype == np.float32

    # both float32
    assert XB_checked.dtype == np.float32

    # mismatched A
    XA_checked, XB_checked = check_pairwise_arrays(XA.astype(float), XB)
    assert XA_checked.dtype == float
    assert XB_checked.dtype == float

    # mismatched B
    XA_checked, XB_checked = check_pairwise_arrays(XA, XB.astype(float))

def test_pairwise_distances_data_derived_params(n_jobs, metric, dist_function):
    # check that pairwise_distances give the same result in sequential and
    # parallel, when metric has data-derived parameters.
    with config_context(working_memory=0.1):  # to have more than 1 chunk
        X = rng.random_sample((100, 10))

        expected_dist = squareform(pdist(X, metric=metric))
        dist = np.vstack(tuple(dist_function(X, metric=metric, n_jobs=n_jobs)))

        assert_allclose(dist, expected_dist)

def test_pairwise_distances_data_derived_params_error(metric):
    # check that pairwise_distances raises an error when Y is passed but
    # metric has data-derived params that are not provided by the user.
    X = rng.random_sample((100, 10))
    Y = rng.random_sample((100, 10))

        match=rf"The '(V|VI)' parameter is required for the {metric} metric",
    ):
        pairwise_distances(X, Y, metric=metric)

def test_numeric_pairwise_distances_datatypes(metric, global_dtype, y_is_x):
    # Check that pairwise distances gives the same result as pdist and cdist
    # regardless of input datatype when using any scipy metric for comparing
    # numeric vectors
    #
    # This test is necessary because pairwise_distances used to throw an
    # error when using metric='seuclidean' and the input data was not
    # of type np.float64 (#15730)

    params = {}
    if y_is_x:
        Y = X
    else:
        Y = rng.random_sample((5, 4)).astype(global_dtype, copy=False)
        expected_dist = cdist(X, Y, metric=metric)
        # precompute parameters for seuclidean & mahalanobis when x is not y
        if metric == "seuclidean":
            params = {"V": np.var(np.vstack([X, Y]), axis=0, ddof=1, dtype=np.float64)}
        elif metric == "mahalanobis":
            params = {"VI": np.linalg.inv(np.cov(np.vstack([X, Y]).T)).T}

    dist = pairwise_distances(X, Y, metric=metric, **params)

    assert_allclose(dist, expected_dist)

def test_nan_euclidean_support(pairwise_distances_func):
    """Check that `nan_euclidean` is lenient with `nan` values."""

    X = [[0, 1], [1, np.nan], [2, 3], [3, 5]]
    output = pairwise_distances_func(X, X, metric="nan_euclidean")

    assert not np.isnan(output).any()

def test_nan_euclidean_constant_input_argmin():
    """Check that the behavior of constant input is the same in the case of
    full of nan vector and full of zero vector.
    """

    X_nan = [[np.nan, np.nan], [np.nan, np.nan], [np.nan, np.nan]]
    argmin_nan = pairwise_distances_argmin(X_nan, X_nan, metric="nan_euclidean")

    X_const = [[0, 0], [0, 0], [0, 0]]
    argmin_const = pairwise_distances_argmin(X_const, X_const, metric="nan_euclidean")

    assert_allclose(argmin_nan, argmin_const)

def test_pairwise_dist_custom_metric_for_string(X, Y, expected_distance):
    """Check pairwise_distances with lists of strings as input."""

    def dummy_string_similarity(x, y):
        return np.abs(len(x) - len(y))

    actual_distance = pairwise_distances(X=X, Y=Y, metric=dummy_string_similarity)
    assert_allclose(actual_distance, expected_distance)

def test_pairwise_dist_custom_metric_for_bool():
    """Check that pairwise_distances does not convert boolean input to float
    when using a custom metric.
    """

    def dummy_bool_dist(v1, v2):
        # dummy distance func using `&` and thus relying on the input data being boolean
        return 1 - (v1 & v2).sum() / (v1 | v2).sum()

    X = np.array([[1, 0, 0, 0], [1, 0, 1, 0], [1, 1, 1, 1]], dtype=bool)

    expected_distance = np.array(
        [
            [0.0, 0.5, 0.75],
            [0.5, 0.0, 0.5],
            [0.75, 0.5, 0.0],
        ]
    )

    actual_distance = pairwise_distances(X=X, metric=dummy_bool_dist)

def test_sparse_manhattan_readonly_dataset(csr_container):
    # Non-regression test for: https://github.com/scikit-learn/scikit-learn/issues/7981
    matrices1 = [csr_container(np.ones((5, 5)))]
    matrices2 = [csr_container(np.ones((5, 5)))]
    # Joblib memory maps datasets which makes them read-only.
    # The following call was reporting as failing in #7981, but this must pass.
    Parallel(n_jobs=2, max_nbytes=0)(
        delayed(manhattan_distances)(m1, m2) for m1, m2 in zip(matrices1, matrices2)
    )

def test_force_all_finite_rename_warning():
    X = np.random.uniform(size=(10, 10))
    Y = np.random.uniform(size=(10, 10))

    msg = "'force_all_finite' was renamed to 'ensure_all_finite'"

    with pytest.warns(FutureWarning, match=msg):
        check_pairwise_arrays(X, Y, force_all_finite=True)

        pairwise_distances(X, Y, force_all_finite=True)

        )
        return c

def make_prediction(dataset=None, binary=False):
    """Make some classification predictions on a toy dataset using a SVC

    If binary is True restrict to a binary classification problem instead of a
    multiclass classification problem
    """

    if dataset is None:
        # import some data to play with
        dataset = datasets.load_iris()

    if binary:
        # restrict to a binary classification task
        X, y = X[y < 2], y[y < 2]

    p = np.arange(n_samples)

    rng = check_random_state(37)
    rng.shuffle(p)
    X, y = X[p], y[p]
    half = int(n_samples / 2)

    # add noisy features to make the problem harder and avoid perfect results
    X = np.c_[X, rng.randn(n_samples, 200 * n_features)]

    # run classifier, get class probabilities and label predictions
    clf = svm.SVC(kernel="linear", probability=True, random_state=0)
    y_score = clf.fit(X[:half], y[:half]).predict_proba(X[half:])

    if binary:
        # only interested in probabilities of the positive case
        # XXX: do we really want a special API for the binary case?
        y_score = y_score[:, 1]

    y_pred = clf.predict(X[half:])
    y_true = y[half:]
    return y_true, y_pred, y_score

def _auc(y_true, y_score):
    """Alternative implementation to check for correctness of
    `roc_auc_score`."""
    pos_label = np.unique(y_true)[1]

    # Count the number of times positive samples are correctly ranked above
    # negative samples.
    pos = y_score[y_true == pos_label]
    neg = y_score[y_true != pos_label]
    diff_matrix = pos.reshape(1, -1) - neg.reshape(-1, 1)
    n_correct = np.sum(diff_matrix > 0)

    return n_correct / float(len(pos) * len(neg))

def _average_precision(y_true, y_score):
    `average_precision_score`.

    Note that this implementation fails on some edge cases.
    For example, for constant predictions e.g. [0.5, 0.5, 0.5],
    y_true = [1, 0, 0] returns an average precision of 0.33...
    but y_true = [0, 0, 1] returns 1.0.
    """
    n_pos = np.sum(y_true == pos_label)
    order = np.argsort(y_score)[::-1]
    y_score = y_score[order]
    y_true = y_true[order]

    score = 0
    for i in range(len(y_score)):
        if y_true[i] == pos_label:
            # Compute precision up to document i
            # i.e, percentage of relevant documents up to document i.
            prec = 0
            for j in range(0, i + 1):
                if y_true[j] == pos_label:
                    prec += 1.0
            prec /= i + 1.0
            score += prec

    return score / n_pos

def _average_precision_slow(y_true, y_score):
    """A second alternative implementation of average precision that closely
    follows the Wikipedia article's definition (see References). This should
    give identical results as `average_precision_score` for all inputs.

       <https://en.wikipedia.org/wiki/Average_precision>`_
    """
    precision, recall, threshold = precision_recall_curve(y_true, y_score)
    precision = list(reversed(precision))
    recall = list(reversed(recall))
    average_precision = 0
    for i in range(1, len(precision)):
        average_precision += precision[i] * (recall[i] - recall[i - 1])
    return average_precision

def _partial_roc_auc_score(y_true, y_predict, max_fpr):
    """Alternative implementation to check for correctness of `roc_auc_score`
    with `max_fpr` set.
    """

    def _partial_roc(y_true, y_predict, max_fpr):
        fpr, tpr, _ = roc_curve(y_true, y_predict)
        new_fpr = fpr[fpr <= max_fpr]
        new_fpr = np.append(new_fpr, max_fpr)
        new_tpr = tpr[fpr <= max_fpr]
        idx_out = np.argmax(fpr > max_fpr)
        idx_in = idx_out - 1
        x_interp = [fpr[idx_in], fpr[idx_out]]
        y_interp = [tpr[idx_in], tpr[idx_out]]
        new_tpr = np.append(new_tpr, np.interp(max_fpr, x_interp, y_interp))
        return (new_fpr, new_tpr)

    new_fpr, new_tpr = _partial_roc(y_true, y_predict, max_fpr)
    partial_auc = auc(new_fpr, new_tpr)

    # Formula (5) from McClish 1989
    fpr1 = 0
    fpr2 = max_fpr
    min_area = 0.5 * (fpr2 - fpr1) * (fpr2 + fpr1)
    max_area = fpr2 - fpr1

def test_roc_curve(drop):
    # Test Area under Receiver Operating Characteristic (ROC) curve
    y_true, _, y_score = make_prediction(binary=True)
    expected_auc = _auc(y_true, y_score)

    fpr, tpr, thresholds = roc_curve(y_true, y_score, drop_intermediate=drop)
    roc_auc = auc(fpr, tpr)
    assert_array_almost_equal(roc_auc, expected_auc, decimal=2)
    assert_almost_equal(roc_auc, roc_auc_score(y_true, y_score))
    assert fpr.shape == tpr.shape
    assert fpr.shape == thresholds.shape

def test_roc_curve_end_points():
    # Make sure that roc_curve returns a curve start at 0 and ending and
    # 1 even in corner cases
    y_true = np.array([0] * 50 + [1] * 50)
    y_pred = rng.randint(3, size=100)
    fpr, tpr, thr = roc_curve(y_true, y_pred, drop_intermediate=True)
    assert fpr[0] == 0
    assert fpr[-1] == 1
    assert fpr.shape == thr.shape

def test_roc_returns_consistency():
    # Test whether the returned threshold matches up with tpr
    # make small toy dataset
    fpr, tpr, thresholds = roc_curve(y_true, y_score)

    # use the given thresholds to determine the tpr
    tpr_correct = []
    for t in thresholds:
        tp = np.sum((y_score >= t) & y_true)
        p = np.sum(y_true)
        tpr_correct.append(1.0 * tp / p)

    # compare tpr and tpr_correct to see if the thresholds' order was correct
    assert_array_almost_equal(tpr, tpr_correct, decimal=2)

def test_roc_curve_multi():
    # roc_curve not applicable for multi-class problems
    y_true, _, y_score = make_prediction(binary=False)

        roc_curve(y_true, y_score)

def test_roc_curve_confidence():
    # roc_curve for confidence scores

    fpr, tpr, thresholds = roc_curve(y_true, y_score - 0.5)
    assert_array_almost_equal(roc_auc, 0.90, decimal=2)

def test_roc_curve_hard():
    # roc_curve for hard decisions
    y_true, pred, y_score = make_prediction(binary=True)

    # always predict one
    trivial_pred = np.ones(y_true.shape)
    fpr, tpr, thresholds = roc_curve(y_true, trivial_pred)
    assert_array_almost_equal(roc_auc, 0.50, decimal=2)

    # always predict zero
    trivial_pred = np.zeros(y_true.shape)

    # hard decisions
    fpr, tpr, thresholds = roc_curve(y_true, pred)
    assert_array_almost_equal(roc_auc, 0.78, decimal=2)

def test_roc_curve_one_label():
    y_true = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    y_pred = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
    # assert there are warnings
    expected_message = (
        "No negative samples in y_true, false positive value should be meaningless"
    )
    with pytest.warns(UndefinedMetricWarning, match=expected_message):
        fpr, tpr, thresholds = roc_curve(y_true, y_pred)

    # all true labels, all fpr should be nan
    assert_array_equal(fpr, np.full(len(thresholds), np.nan))

        "No positive samples in y_true, true positive value should be meaningless"
    )
        fpr, tpr, thresholds = roc_curve([1 - x for x in y_true], y_pred)
    # all negative labels, all tpr should be nan
    assert_array_equal(tpr, np.full(len(thresholds), np.nan))

def test_roc_curve_toydata():
    # Binary classification
    y_true = [0, 1]
    y_score = [0, 1]
    tpr, fpr, _ = roc_curve(y_true, y_score)
    roc_auc = roc_auc_score(y_true, y_score)
    assert_array_almost_equal(tpr, [0, 0, 1])
    assert_array_almost_equal(fpr, [0, 1, 1])
    assert_almost_equal(roc_auc, 1.0)

    y_score = [1, 0]
    assert_array_almost_equal(tpr, [0, 1, 1])
    assert_array_almost_equal(fpr, [0, 0, 1])
    assert_almost_equal(roc_auc, 0.0)

    y_true = [1, 0]
    y_score = [1, 1]
    assert_array_almost_equal(tpr, [0, 1])
    assert_array_almost_equal(fpr, [0, 1])
    assert_almost_equal(roc_auc, 0.5)

    y_score = [0.5, 0.5]

    # case with no positive samples
    y_true = [0, 0]
    y_score = [0.25, 0.75]
    # assert UndefinedMetricWarning because of no positive sample in y_true
    )
        tpr, fpr, _ = roc_curve(y_true, y_score)
    assert_array_almost_equal(tpr, [0.0, 0.5, 1.0])
    assert_array_almost_equal(fpr, [np.nan, np.nan, np.nan])
        "Only one class is present in y_true. "
        "ROC AUC score is not defined in that case."
    )
        auc = roc_auc_score(y_true, y_score)
    assert math.isnan(auc)

    # case with no negative samples
    y_true = [1, 1]
    # assert UndefinedMetricWarning because of no negative sample in y_true
    )
    assert_array_almost_equal(tpr, [np.nan, np.nan, np.nan])
    assert_array_almost_equal(fpr, [0.0, 0.5, 1.0])
    )

    # Multi-label classification task
    y_true = np.array([[0, 1], [0, 1]])
    y_score = np.array([[0, 1], [0, 1]])
        roc_auc_score(y_true, y_score, average="macro")
        roc_auc_score(y_true, y_score, average="weighted")
    assert_almost_equal(roc_auc_score(y_true, y_score, average="samples"), 1.0)
    assert_almost_equal(roc_auc_score(y_true, y_score, average="micro"), 1.0)

    y_score = np.array([[0, 1], [1, 0]])
    assert_almost_equal(roc_auc_score(y_true, y_score, average="samples"), 0.5)
    assert_almost_equal(roc_auc_score(y_true, y_score, average="micro"), 0.5)

    y_true = np.array([[1, 0], [0, 1]])
    assert_almost_equal(roc_auc_score(y_true, y_score, average="macro"), 0)
    assert_almost_equal(roc_auc_score(y_true, y_score, average="weighted"), 0)
    assert_almost_equal(roc_auc_score(y_true, y_score, average="samples"), 0)
    assert_almost_equal(roc_auc_score(y_true, y_score, average="micro"), 0)

    y_score = np.array([[0.5, 0.5], [0.5, 0.5]])
    assert_almost_equal(roc_auc_score(y_true, y_score, average="macro"), 0.5)
    assert_almost_equal(roc_auc_score(y_true, y_score, average="weighted"), 0.5)

def test_roc_curve_drop_intermediate():
    # Test that drop_intermediate drops the correct thresholds
    y_true = [0, 0, 0, 0, 1, 1]
    y_score = [0.0, 0.2, 0.5, 0.6, 0.7, 1.0]
    tpr, fpr, thresholds = roc_curve(y_true, y_score, drop_intermediate=True)
    assert_array_almost_equal(thresholds, [np.inf, 1.0, 0.7, 0.0])

    # Test dropping thresholds with repeating scores
    y_true = [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]
    y_score = [0.0, 0.1, 0.6, 0.6, 0.7, 0.8, 0.9, 0.6, 0.7, 0.8, 0.9, 0.9, 1.0]
    assert_array_almost_equal(thresholds, [np.inf, 1.0, 0.9, 0.7, 0.6, 0.0])

def test_roc_curve_fpr_tpr_increasing():
    # Ensure that fpr and tpr returned by roc_curve are increasing.
    # Construct an edge case with float y_score and sample_weight
    # when some adjacent values of fpr and tpr are actually the same.
    y_true = [0, 0, 1, 1, 1]
    y_score = [0.1, 0.7, 0.3, 0.4, 0.5]
    sample_weight = np.repeat(0.2, 5)
    assert (np.diff(fpr) < 0).sum() == 0
    assert (np.diff(tpr) < 0).sum() == 0

def test_auc():
    # Test Area Under Curve (AUC) computation
    x = [0, 1]
    y = [0, 1]
    assert_array_almost_equal(auc(x, y), 0.5)
    x = [1, 0]
    y = [0, 1]
    x = [1, 0, 0]
    y = [0, 1, 1]
    x = [0, 1]
    y = [1, 1]
    assert_array_almost_equal(auc(x, y), 1)
    x = [0, 0.5, 1]
    y = [0, 0.5, 1]

def test_auc_errors():
    # Incompatible shapes
        auc([0.0, 0.5, 1.0], [0.1, 0.2])

    # Too few x values
        auc([0.0], [0.1])

    # x is not in order
    x = [2, 1, 3, 4]
    y = [5, 6, 7, 8]
    error_message = "x is neither increasing nor decreasing : {}".format(np.array(x))
        auc(x, y)

def test_multiclass_ovo_roc_auc_toydata(y_true, labels):
    # Tests the one-vs-one multiclass ROC AUC algorithm
    # on a small example, representative of an expected use case.
    y_scores = np.array(
        [[0.1, 0.8, 0.1], [0.3, 0.4, 0.3], [0.35, 0.5, 0.15], [0, 0.2, 0.8]]
    )

    # Used to compute the expected output.
    # Consider labels 0 and 1:
    # positive label is 0, negative label is 1
    score_01 = roc_auc_score([1, 0, 1], [0.1, 0.3, 0.35])
    # positive label is 1, negative label is 0
    score_10 = roc_auc_score([0, 1, 0], [0.8, 0.4, 0.5])
    average_score_01 = (score_01 + score_10) / 2

    # Consider labels 0 and 2:
    score_02 = roc_auc_score([1, 1, 0], [0.1, 0.35, 0])
    score_20 = roc_auc_score([0, 0, 1], [0.1, 0.15, 0.8])
    average_score_02 = (score_02 + score_20) / 2

    # Consider labels 1 and 2:
    score_12 = roc_auc_score([1, 0], [0.4, 0.2])
    score_21 = roc_auc_score([0, 1], [0.3, 0.8])
    average_score_12 = (score_12 + score_21) / 2

    # Unweighted, one-vs-one multiclass ROC AUC algorithm
    ovo_unweighted_score = (average_score_01 + average_score_02 + average_score_12) / 3
        roc_auc_score(y_true, y_scores, labels=labels, multi_class="ovo"),
        ovo_unweighted_score,
    )

    # Weighted, one-vs-one multiclass ROC AUC algorithm
    # Each term is weighted by the prevalence for the positive label.
    pair_scores = [average_score_01, average_score_02, average_score_12]
    prevalence = [0.75, 0.75, 0.50]
    ovo_weighted_score = np.average(pair_scores, weights=prevalence)
        roc_auc_score(
            y_true, y_scores, labels=labels, multi_class="ovo", average="weighted"
        ),
        ovo_weighted_score,
    )

    # Check that average=None raises NotImplemented error
    error_message = "average=None is not implemented for multi_class='ovo'."
    with pytest.raises(NotImplementedError, match=error_message):
        roc_auc_score(y_true, y_scores, labels=labels, multi_class="ovo", average=None)

def test_multiclass_ovo_roc_auc_toydata_binary(y_true, labels):
    # Tests the one-vs-one multiclass ROC AUC algorithm for binary y_true
    #
        [[0.2, 0.0, 0.8], [0.6, 0.0, 0.4], [0.55, 0.0, 0.45], [0.4, 0.0, 0.6]]
    )

    score_01 = roc_auc_score([1, 0, 1, 0], [0.2, 0.6, 0.55, 0.4])
    score_10 = roc_auc_score([0, 1, 0, 1], [0.8, 0.4, 0.45, 0.6])
    ovo_score = (score_01 + score_10) / 2

        roc_auc_score(y_true, y_scores, labels=labels, multi_class="ovo"), ovo_score
    )

        ),
        ovo_score,
    )

def test_multiclass_ovr_roc_auc_toydata(y_true, labels):
    # Tests the unweighted, one-vs-rest multiclass ROC AUC algorithm
        [[1.0, 0.0, 0.0], [0.1, 0.5, 0.4], [0.1, 0.1, 0.8], [0.3, 0.3, 0.4]]
    )
    # Compute the expected result by individually computing the 'one-vs-rest'
    # ROC AUC scores for classes 0, 1, and 2.
    out_0 = roc_auc_score([1, 0, 0, 0], y_scores[:, 0])
    out_1 = roc_auc_score([0, 1, 0, 0], y_scores[:, 1])
    out_2 = roc_auc_score([0, 0, 1, 1], y_scores[:, 2])
        roc_auc_score(y_true, y_scores, multi_class="ovr", labels=labels, average=None),
        [out_0, out_1, out_2],
    )

    # Compute unweighted results (default behaviour is average="macro")
    result_unweighted = (out_0 + out_1 + out_2) / 3.0
        roc_auc_score(y_true, y_scores, multi_class="ovr", labels=labels),
        result_unweighted,
    )

    # Tests the weighted, one-vs-rest multiclass ROC AUC algorithm
    # on the same input (Provost & Domingos, 2000)
    result_weighted = out_0 * 0.25 + out_1 * 0.25 + out_2 * 0.5
            y_true, y_scores, multi_class="ovr", labels=labels, average="weighted"
        ),
        result_weighted,
    )

def test_perfect_imperfect_chance_multiclass_roc_auc(multi_class, average):
    y_true = np.array([3, 1, 2, 0])

    # Perfect classifier (from a ranking point of view) has roc_auc_score = 1.0
    y_perfect = [
        [0.0, 0.0, 0.0, 1.0],
        [0.0, 1.0, 0.0, 0.0],
        [0.0, 0.0, 1.0, 0.0],
        [0.75, 0.05, 0.05, 0.15],
    ]
        roc_auc_score(y_true, y_perfect, multi_class=multi_class, average=average),
        1.0,
    )

    # Imperfect classifier has roc_auc_score < 1.0
    y_imperfect = [
    ]
    assert (
        roc_auc_score(y_true, y_imperfect, multi_class=multi_class, average=average)
        < 1.0
    )

    # Chance level classifier has roc_auc_score = 5.0
    y_chance = 0.25 * np.ones((4, 4))
    assert roc_auc_score(
        y_true, y_chance, multi_class=multi_class, average=average
    ) == pytest.approx(0.5)

def test_micro_averaged_ovr_roc_auc(global_random_seed):
    seed = global_random_seed
    # Let's generate a set of random predictions and matching true labels such
    # that the predictions are not perfect. To make the problem more interesting,
    # we use an imbalanced class distribution (by using different parameters
    # in the Dirichlet prior (conjugate prior of the multinomial distribution).
    y_pred = stats.dirichlet.rvs([2.0, 1.0, 0.5], size=1000, random_state=seed)
    y_true = np.asarray(
        [
            stats.multinomial.rvs(n=1, p=y_pred_i, random_state=seed).argmax()
            for y_pred_i in y_pred
        ]
    )
    y_onehot = label_binarize(y_true, classes=[0, 1, 2])
    fpr, tpr, _ = roc_curve(y_onehot.ravel(), y_pred.ravel())
    roc_auc_by_hand = auc(fpr, tpr)
    roc_auc_auto = roc_auc_score(y_true, y_pred, multi_class="ovr", average="micro")
    assert roc_auc_by_hand == pytest.approx(roc_auc_auto)

def test_roc_auc_score_multiclass_labels_error(msg, y_true, labels, multi_class):
    )

        roc_auc_score(y_true, y_scores, labels=labels, multi_class=multi_class)

def test_roc_auc_score_multiclass_error(msg, kwargs):
    # Test that roc_auc_score function returns an error when trying
    # to compute multiclass AUC for parameters where an output
    # is not defined.
    rng = check_random_state(404)
    y_score = rng.rand(20, 3)
    y_prob = softmax(y_score)
    y_true = rng.randint(0, 3, size=20)
        roc_auc_score(y_true, y_prob, **kwargs)

def test_auc_score_non_binary_class():
    # to compute AUC for non-binary class values.
    y_pred = rng.rand(10)
    # y_true contains only one class value
    y_true = np.zeros(10, dtype="int")
    warn_message = (
    )
    with pytest.warns(UndefinedMetricWarning, match=warn_message):
        roc_auc_score(y_true, y_pred)
    y_true = np.ones(10, dtype="int")
    y_true = np.full(10, -1, dtype="int")

def test_binary_clf_curve_multiclass_error(curve_func):
    y_true = rng.randint(0, 3, size=10)
    msg = "multiclass format is not supported"
        curve_func(y_true, y_pred)

def test_binary_clf_curve_implicit_pos_label(curve_func):
    # Check that using string class labels raises an informative
    # error for any supported string dtype:
    msg = (
        "y_true takes value in {'a', 'b'} and pos_label is "
        "not specified: either make y_true take "
        "value in {0, 1} or {-1, 1} or pass pos_label "
        "explicitly."
    )
        curve_func(np.array(["a", "b"], dtype="<U1"), [0.0, 1.0])

        curve_func(np.array(["a", "b"], dtype=object), [0.0, 1.0])

    # Check that it is possible to use floating point class labels
    # that are interpreted similarly to integer class labels:
    y_pred = [0.0, 1.0, 0.2, 0.42]
    int_curve = curve_func([0, 1, 1, 0], y_pred)
    float_curve = curve_func([0.0, 1.0, 1.0, 0.0], y_pred)
    for int_curve_part, float_curve_part in zip(int_curve, float_curve):
        np.testing.assert_allclose(int_curve_part, float_curve_part)

def test_binary_clf_curve_implicit_bytes_pos_label(curve_func, labels_type):
    # Check that using bytes class labels raises an informative
    labels = _convert_container([b"a", b"b"], labels_type)
    msg = "Support for labels represented as bytes is not supported"
        curve_func(labels, [0.0, 1.0])

def test_binary_clf_curve_zero_sample_weight(curve_func):
    y_score = [0.1, 0.2, 0.3, 0.4, 0.5]
    sample_weight = [1, 1, 1, 0.5, 0]

    result_1 = curve_func(y_true, y_score, sample_weight=sample_weight)
    result_2 = curve_func(y_true[:-1], y_score[:-1], sample_weight=sample_weight[:-1])

    for arr_1, arr_2 in zip(result_1, result_2):
        assert_allclose(arr_1, arr_2)

def test_precision_recall_curve(drop):
    _test_precision_recall_curve(y_true, y_score, drop)

    # Make sure the first point of the Precision-Recall on the right is:
    # (p=1.0, r=class balance) on a non-balanced dataset [1:]
    p, r, t = precision_recall_curve(y_true[1:], y_score[1:], drop_intermediate=drop)
    assert r[0] == 1.0
    assert p[0] == y_true[1:].mean()

    # Use {-1, 1} for labels; make sure original labels aren't modified
    y_true[np.where(y_true == 0)] = -1
    y_true_copy = y_true.copy()
    assert_array_equal(y_true_copy, y_true)

    labels = [1, 0, 0, 1]
    predict_probas = [1, 2, 3, 4]
    p, r, t = precision_recall_curve(labels, predict_probas, drop_intermediate=drop)
    if drop:
        assert_allclose(p, [0.5, 0.33333333, 1.0, 1.0])
        assert_allclose(r, [1.0, 0.5, 0.5, 0.0])
        assert_allclose(t, [1, 2, 4])
    else:
        assert_allclose(p, [0.5, 0.33333333, 0.5, 1.0, 1.0])
        assert_allclose(r, [1.0, 0.5, 0.5, 0.5, 0.0])
        assert_allclose(t, [1, 2, 3, 4])
    assert p.size == r.size
    assert p.size == t.size + 1

def _test_precision_recall_curve(y_true, y_score, drop):
    # Test Precision-Recall and area under PR curve
    p, r, thresholds = precision_recall_curve(y_true, y_score, drop_intermediate=drop)
    precision_recall_auc = _average_precision_slow(y_true, y_score)
    assert_array_almost_equal(precision_recall_auc, 0.859, 3)
        precision_recall_auc, average_precision_score(y_true, y_score)
    )
    # `_average_precision` is not very precise in case of 0.5 ties: be tolerant
        _average_precision(y_true, y_score), precision_recall_auc, decimal=2
    )
    assert p.size == thresholds.size + 1
    # Smoke test in the case of proba having only one value
    p, r, thresholds = precision_recall_curve(
        y_true, np.zeros_like(y_score), drop_intermediate=drop
    )

def test_precision_recall_curve_toydata(drop):
    with np.errstate(all="raise"):
        # Binary classification
        y_true = [0, 1]
        y_score = [0, 1]
        p, r, _ = precision_recall_curve(y_true, y_score, drop_intermediate=drop)
        auc_prc = average_precision_score(y_true, y_score)
        assert_array_almost_equal(p, [0.5, 1, 1])
        assert_array_almost_equal(r, [1, 1, 0])
        assert_almost_equal(auc_prc, 1.0)

        y_score = [1, 0]
        assert_array_almost_equal(p, [0.5, 0.0, 1.0])
        assert_array_almost_equal(r, [1.0, 0.0, 0.0])
        # Here we are doing a terrible prediction: we are always getting
        # it wrong, hence the average_precision_score is the accuracy at
        # chance: 50%
        assert_almost_equal(auc_prc, 0.5)

        y_true = [1, 0]
        y_score = [1, 1]
        assert_array_almost_equal(p, [0.5, 1])
        assert_array_almost_equal(r, [1.0, 0])

        y_score = [0.5, 0.5]
        assert_array_almost_equal(r, [1, 0.0])

        y_true = [0, 0]
        y_score = [0.25, 0.75]
        with pytest.warns(UserWarning, match="No positive class found in y_true"):
            p, r, _ = precision_recall_curve(y_true, y_score, drop_intermediate=drop)
            auc_prc = average_precision_score(y_true, y_score)
        assert_allclose(p, [0, 0, 1])
        assert_allclose(r, [1, 1, 0])
        assert_allclose(auc_prc, 0)

        y_true = [1, 1]
        assert_almost_equal(average_precision_score(y_true, y_score), 1.0)
        assert_array_almost_equal(p, [1.0, 1.0, 1.0])
        assert_array_almost_equal(r, [1, 0.5, 0.0])

        # Multi-label classification task
        y_true = np.array([[0, 1], [0, 1]])
        y_score = np.array([[0, 1], [0, 1]])
                average_precision_score(y_true, y_score, average="macro"), 0.5
            )
                average_precision_score(y_true, y_score, average="weighted"), 1.0
            )
            average_precision_score(y_true, y_score, average="samples"), 1.0
        )
        assert_allclose(average_precision_score(y_true, y_score, average="micro"), 1.0)

        y_score = np.array([[0, 1], [1, 0]])
            )
            )
            average_precision_score(y_true, y_score, average="samples"), 0.75
        )
        assert_allclose(average_precision_score(y_true, y_score, average="micro"), 0.5)

        y_true = np.array([[1, 0], [0, 1]])
            average_precision_score(y_true, y_score, average="macro"), 0.5
        )
            average_precision_score(y_true, y_score, average="weighted"), 0.5
        )
            average_precision_score(y_true, y_score, average="samples"), 0.5
        )
            average_precision_score(y_true, y_score, average="micro"), 0.5
        )

        y_true = np.array([[0, 0], [0, 0]])
                average_precision_score(y_true, y_score, average="macro"), 0.0
            )
            average_precision_score(y_true, y_score, average="weighted"), 0.0
        )
                average_precision_score(y_true, y_score, average="samples"), 0.0
            )
                average_precision_score(y_true, y_score, average="micro"), 0.0
            )

        y_true = np.array([[1, 1], [1, 1]])
        assert_allclose(average_precision_score(y_true, y_score, average="macro"), 1.0)
            average_precision_score(y_true, y_score, average="weighted"), 1.0
        )
        )

        y_score = np.array([[0.5, 0.5], [0.5, 0.5]])
        )
        )
        )
        )

        # if one class is never present weighted should not be NaN
        y_true = np.array([[0, 0], [0, 1]])
        y_score = np.array([[0, 0], [0, 1]])
                average_precision_score(y_true, y_score, average="weighted"), 1
            )

def test_precision_recall_curve_drop_intermediate():
    """Check the behaviour of the `drop_intermediate` parameter."""
    precision, recall, thresholds = precision_recall_curve(
        y_true, y_score, drop_intermediate=True
    )
    assert_allclose(thresholds, [0.0, 0.7, 1.0])

    )
    assert_allclose(thresholds, [0.0, 0.6, 0.7, 0.8, 0.9, 1.0])

    # Test all false keeps only endpoints
    y_true = [0, 0, 0, 0]
    y_score = [0.0, 0.1, 0.2, 0.3]
    )
    assert_allclose(thresholds, [0.0, 0.3])

    # Test all true keeps all thresholds
    y_true = [1, 1, 1, 1]
    )
    assert_allclose(thresholds, [0.0, 0.1, 0.2, 0.3])

def test_average_precision_constant_values():
    # Check the average_precision_score of a constant predictor is
    # the TPR

    # Generate a dataset with 25% of positives
    y_true = np.zeros(100, dtype=int)
    y_true[::4] = 1
    # And a constant score
    y_score = np.ones(100)
    # The precision is then the fraction of positive whatever the recall
    # is, as there is only one threshold:
    assert average_precision_score(y_true, y_score) == 0.25

def test_average_precision_score_binary_pos_label_errors():
    # Raise an error when pos_label is not in binary y_true
    y_true = np.array([0, 1])
    y_pred = np.array([0, 1])
    err_msg = r"pos_label=2 is not a valid label. It should be one of \[0, 1\]"
        average_precision_score(y_true, y_pred, pos_label=2)

def test_average_precision_score_multilabel_pos_label_errors():
    # Raise an error for multilabel-indicator y_true with
    # pos_label other than 1
    y_true = np.array([[1, 0], [0, 1], [0, 1], [1, 0]])
    y_pred = np.array([[0.9, 0.1], [0.1, 0.9], [0.8, 0.2], [0.2, 0.8]])
    err_msg = (
        "Parameter pos_label is fixed to 1 for multilabel-indicator y_true. "
        "Do not set pos_label or set pos_label to 1."
    )
        average_precision_score(y_true, y_pred, pos_label=0)

def test_average_precision_score_multiclass_pos_label_errors():
    # Raise an error for multiclass y_true with pos_label other than 1
    y_true = np.array([0, 1, 2, 0, 1, 2])
    y_pred = np.array(
        [
            [0.5, 0.2, 0.1],
            [0.4, 0.5, 0.3],
            [0.1, 0.2, 0.6],
            [0.2, 0.3, 0.5],
        ]
    )
    err_msg = (
        "Parameter pos_label is fixed to 1 for multiclass y_true. "
    )
        average_precision_score(y_true, y_pred, pos_label=3)

def test_score_scale_invariance():
    # Test that average_precision_score and roc_auc_score are invariant by
    # the scaling or shifting of probabilities
    # This test was expanded (added scaled_down) in response to github
    # issue #3864 (and others), where overly aggressive rounding was causing
    # problems for users with very small y_score values

    roc_auc_scaled_up = roc_auc_score(y_true, 100 * y_score)
    roc_auc_scaled_down = roc_auc_score(y_true, 1e-6 * y_score)
    roc_auc_shifted = roc_auc_score(y_true, y_score - 10)
    assert roc_auc == roc_auc_scaled_up
    assert roc_auc == roc_auc_scaled_down
    assert roc_auc == roc_auc_shifted

    pr_auc = average_precision_score(y_true, y_score)
    pr_auc_scaled_up = average_precision_score(y_true, 100 * y_score)
    pr_auc_scaled_down = average_precision_score(y_true, 1e-6 * y_score)
    pr_auc_shifted = average_precision_score(y_true, y_score - 10)
    assert pr_auc == pr_auc_scaled_up
    assert pr_auc == pr_auc_scaled_down
    assert pr_auc == pr_auc_shifted

def test_det_curve_toydata(y_true, y_score, expected_fpr, expected_fnr):
    # Check on a batch of small examples.
    fpr, fnr, _ = det_curve(y_true, y_score)

    assert_allclose(fpr, expected_fpr)
    assert_allclose(fnr, expected_fnr)

def test_det_curve_drop_intermediate(
    y_true, y_score, expected_fpr, expected_fnr, drop_intermediate
):
    fpr, fnr, _ = det_curve(y_true, y_score, drop_intermediate=drop_intermediate)

def test_det_curve_tie_handling(y_true, y_score, expected_fpr, expected_fnr):

def test_det_curve_sanity_check():
    # Exactly duplicated inputs yield the same result.
        det_curve([0, 0, 1], [0, 0.5, 1]),
        det_curve([0, 0, 0, 0, 1, 1], [0, 0, 0.5, 0.5, 1, 1]),
    )

def test_det_curve_constant_scores(y_score):
    fpr, fnr, threshold = det_curve(
        y_true=[0, 1, 0, 1, 0, 1], y_score=np.full(6, y_score)
    )

    assert_allclose(fpr, [1, 0])
    assert_allclose(fnr, [0, 1])
    assert_allclose(threshold, [y_score, np.inf])

def test_det_curve_perfect_scores(y_true):
    fpr, fnr, _ = det_curve(y_true=y_true, y_score=y_true)

    assert_allclose(fpr, [0])
    assert_allclose(fnr, [0])

def test_det_curve_bad_input(y_true, y_pred, err_msg):
    # input variables with inconsistent numbers of samples
        det_curve(y_true, y_pred)

def test_det_curve_pos_label():
    y_true = ["cancer"] * 3 + ["not cancer"] * 7
    y_pred_pos_not_cancer = np.array([0.1, 0.4, 0.6, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.9])
    y_pred_pos_cancer = 1 - y_pred_pos_not_cancer

    fpr_pos_cancer, fnr_pos_cancer, th_pos_cancer = det_curve(
        y_true,
        y_pred_pos_cancer,
        pos_label="cancer",
    )
    fpr_pos_not_cancer, fnr_pos_not_cancer, th_pos_not_cancer = det_curve(
        y_true,
        y_pred_pos_not_cancer,
        pos_label="not cancer",
    )

    # check that the first threshold will change depending which label we
    # consider positive
    assert th_pos_cancer[0] == pytest.approx(0.4)
    assert th_pos_not_cancer[0] == pytest.approx(0.2)

    # check for the symmetry of the fpr and fnr
    assert_allclose(fpr_pos_cancer, fnr_pos_not_cancer[::-1])
    assert_allclose(fnr_pos_cancer, fpr_pos_not_cancer[::-1])

def check_lrap_toy(lrap_score):
    # Check on several small example that it works
    assert_almost_equal(lrap_score([[0, 1]], [[0.25, 0.75]]), 1)
    assert_almost_equal(lrap_score([[0, 1]], [[0.75, 0.25]]), 1 / 2)
    assert_almost_equal(lrap_score([[1, 1]], [[0.75, 0.25]]), 1)

    assert_almost_equal(lrap_score([[0, 0, 1]], [[0.25, 0.5, 0.75]]), 1)
    assert_almost_equal(lrap_score([[0, 1, 0]], [[0.25, 0.5, 0.75]]), 1 / 2)
    assert_almost_equal(lrap_score([[0, 1, 1]], [[0.25, 0.5, 0.75]]), 1)
    assert_almost_equal(lrap_score([[1, 0, 0]], [[0.25, 0.5, 0.75]]), 1 / 3)
        lrap_score([[1, 0, 1]], [[0.25, 0.5, 0.75]]), (2 / 3 + 1 / 1) / 2
    )
        lrap_score([[1, 1, 0]], [[0.25, 0.5, 0.75]]), (2 / 3 + 1 / 2) / 2
    )

    assert_almost_equal(lrap_score([[0, 0, 1]], [[0.75, 0.5, 0.25]]), 1 / 3)
    assert_almost_equal(lrap_score([[0, 1, 0]], [[0.75, 0.5, 0.25]]), 1 / 2)
        lrap_score([[0, 1, 1]], [[0.75, 0.5, 0.25]]), (1 / 2 + 2 / 3) / 2
    )
    assert_almost_equal(lrap_score([[1, 0, 0]], [[0.75, 0.5, 0.25]]), 1)
    assert_almost_equal(lrap_score([[1, 0, 1]], [[0.75, 0.5, 0.25]]), (1 + 2 / 3) / 2)
    assert_almost_equal(lrap_score([[1, 1, 0]], [[0.75, 0.5, 0.25]]), 1)
    assert_almost_equal(lrap_score([[1, 1, 1]], [[0.75, 0.5, 0.25]]), 1)

    assert_almost_equal(lrap_score([[0, 0, 1]], [[0.5, 0.75, 0.25]]), 1 / 3)
    assert_almost_equal(lrap_score([[0, 1, 0]], [[0.5, 0.75, 0.25]]), 1)
    assert_almost_equal(lrap_score([[0, 1, 1]], [[0.5, 0.75, 0.25]]), (1 + 2 / 3) / 2)
    assert_almost_equal(lrap_score([[1, 0, 0]], [[0.5, 0.75, 0.25]]), 1 / 2)
        lrap_score([[1, 0, 1]], [[0.5, 0.75, 0.25]]), (1 / 2 + 2 / 3) / 2
    )
    assert_almost_equal(lrap_score([[1, 1, 0]], [[0.5, 0.75, 0.25]]), 1)
    assert_almost_equal(lrap_score([[1, 1, 1]], [[0.5, 0.75, 0.25]]), 1)

    # Tie handling
    assert_almost_equal(lrap_score([[1, 0]], [[0.5, 0.5]]), 0.5)
    assert_almost_equal(lrap_score([[0, 1]], [[0.5, 0.5]]), 0.5)
    assert_almost_equal(lrap_score([[1, 1]], [[0.5, 0.5]]), 1)

    assert_almost_equal(lrap_score([[0, 0, 1]], [[0.25, 0.5, 0.5]]), 0.5)
    assert_almost_equal(lrap_score([[0, 1, 0]], [[0.25, 0.5, 0.5]]), 0.5)
    assert_almost_equal(lrap_score([[0, 1, 1]], [[0.25, 0.5, 0.5]]), 1)
    assert_almost_equal(lrap_score([[1, 0, 0]], [[0.25, 0.5, 0.5]]), 1 / 3)
        lrap_score([[1, 0, 1]], [[0.25, 0.5, 0.5]]), (2 / 3 + 1 / 2) / 2
    )
        lrap_score([[1, 1, 0]], [[0.25, 0.5, 0.5]]), (2 / 3 + 1 / 2) / 2
    )
    assert_almost_equal(lrap_score([[1, 1, 1]], [[0.25, 0.5, 0.5]]), 1)

    assert_almost_equal(lrap_score([[1, 1, 0]], [[0.5, 0.5, 0.5]]), 2 / 3)

    assert_almost_equal(lrap_score([[1, 1, 1, 0]], [[0.5, 0.5, 0.5, 0.5]]), 3 / 4)

def check_zero_or_all_relevant_labels(lrap_score):

    for n_labels in range(2, 5):
        y_score = random_state.uniform(size=(1, n_labels))
        y_score_ties = np.zeros_like(y_score)

        # No relevant labels
        y_true = np.zeros((1, n_labels))
        assert lrap_score(y_true, y_score) == 1.0
        assert lrap_score(y_true, y_score_ties) == 1.0

        # Only relevant labels
        y_true = np.ones((1, n_labels))

    # Degenerate case: only one label
        lrap_score([[1], [0], [1], [0]], [[0.5], [0.5], [0.5], [0.5]]), 1.0
    )

def check_lrap_error_raised(lrap_score):
    # Raise value error if not appropriate format
        lrap_score([0, 1, 0], [0.25, 0.3, 0.2])
        lrap_score([0, 1, 2], [[0.25, 0.75, 0.0], [0.7, 0.3, 0.0], [0.8, 0.2, 0.0]])
        lrap_score(
            [(0), (1), (2)], [[0.25, 0.75, 0.0], [0.7, 0.3, 0.0], [0.8, 0.2, 0.0]]
        )

    # Check that y_true.shape != y_score.shape raise the proper exception
        lrap_score([[0, 1], [0, 1]], [0, 1])
        lrap_score([[0, 1], [0, 1]], [[0, 1]])
        lrap_score([[0, 1], [0, 1]], [[0], [1]])
        lrap_score([[0, 1]], [[0, 1], [0, 1]])
        lrap_score([[0], [1]], [[0, 1], [0, 1]])

def check_lrap_only_ties(lrap_score):
    # Check tie handling in score
    # Basic check with only ties and increasing label space
    for n_labels in range(2, 10):
        y_score = np.ones((1, n_labels))

        # Check for growing number of consecutive relevant
        for n_relevant in range(1, n_labels):
            # Check for a bunch of positions
            for pos in range(n_labels - n_relevant):
                y_true = np.zeros((1, n_labels))
                y_true[0, pos : pos + n_relevant] = 1
                assert_almost_equal(lrap_score(y_true, y_score), n_relevant / n_labels)

def check_lrap_without_tie_and_increasing_score(lrap_score):
    # Check that Label ranking average precision works for various
    # Basic check with increasing label space size and decreasing score
        y_score = n_labels - (np.arange(n_labels).reshape((1, n_labels)) + 1)

        # First and last
        y_true[0, 0] = 1
        y_true[0, -1] = 1
        assert_almost_equal(lrap_score(y_true, y_score), (2 / n_labels + 1) / 2)

        # Check for growing number of consecutive relevant label
            # Check for a bunch of position
                assert_almost_equal(
                    lrap_score(y_true, y_score),
                    sum(
                        (r + 1) / ((pos + r + 1) * n_relevant)
                        for r in range(n_relevant)
                    ),
                )

def _my_lrap(y_true, y_score):
    """Simple implementation of label ranking average precision"""
    score = np.empty((n_samples,))
        # The best rank correspond to 1. Rank higher than 1 are worse.
        # The best inverse ranking correspond to n_labels.
        unique_rank, inv_rank = np.unique(y_score[i], return_inverse=True)
        n_ranks = unique_rank.size
        rank = n_ranks - inv_rank

        # Rank need to be corrected to take into account ties
        # ex: rank 1 ex aequo means that both label are rank 2.
        corr_rank = np.bincount(rank, minlength=n_ranks + 1).cumsum()
        rank = corr_rank[rank]

        relevant = y_true[i].nonzero()[0]
            score[i] = 1
            continue

        score[i] = 0.0
        for label in relevant:
            # Let's count the number of relevant label with better rank
            # (smaller rank).
            n_ranked_above = sum(rank[r] <= rank[label] for r in relevant)

            # Weight by the rank of the actual label
            score[i] += n_ranked_above / rank[label]

        score[i] /= relevant.size

    return score.mean()

def check_alternative_lrap_implementation(
    lrap_score, n_classes=5, n_samples=20, random_state=0
):
    )

    # Score with ties
    y_score = _sparse_random_matrix(
        n_components=y_true.shape[0],
        n_features=y_true.shape[1],
    )

    if hasattr(y_score, "toarray"):
        y_score = y_score.toarray()
    score_lrap = label_ranking_average_precision_score(y_true, y_score)
    score_my_lrap = _my_lrap(y_true, y_score)
    assert_almost_equal(score_lrap, score_my_lrap)

    # Uniform score

def test_lrap_error_raised():
    check_lrap_error_raised(label_ranking_average_precision_score)

def test_alternative_lrap_implementation(n_samples, n_classes, random_state):
    check_alternative_lrap_implementation(
        label_ranking_average_precision_score, n_classes, n_samples, random_state
    )

def test_lrap_sample_weighting_zero_labels():
    # Degenerate sample labeling (e.g., zero labels for a sample) is a valid
    # special case for lrap (the sample is considered to achieve perfect
    # precision), but this case is not tested in test_common.
    # For these test samples, the APs are 0.5, 0.75, and 1.0 (default for zero
    # labels).
    y_true = np.array([[1, 0, 0, 0], [1, 0, 0, 1], [0, 0, 0, 0]], dtype=bool)
    y_score = np.array(
        [[0.3, 0.4, 0.2, 0.1], [0.1, 0.2, 0.3, 0.4], [0.4, 0.3, 0.2, 0.1]]
    )
    samplewise_lraps = np.array([0.5, 0.75, 1.0])
    sample_weight = np.array([1.0, 1.0, 0.0])

        label_ranking_average_precision_score(
            y_true, y_score, sample_weight=sample_weight
        ),
        np.sum(sample_weight * samplewise_lraps) / np.sum(sample_weight),
    )

def test_coverage_error():
    # Toy case
    assert_almost_equal(coverage_error([[0, 1]], [[0.25, 0.75]]), 1)
    assert_almost_equal(coverage_error([[0, 1]], [[0.75, 0.25]]), 2)
    assert_almost_equal(coverage_error([[1, 1]], [[0.75, 0.25]]), 2)
    assert_almost_equal(coverage_error([[0, 0]], [[0.75, 0.25]]), 0)

    assert_almost_equal(coverage_error([[0, 0, 0]], [[0.25, 0.5, 0.75]]), 0)
    assert_almost_equal(coverage_error([[0, 0, 1]], [[0.25, 0.5, 0.75]]), 1)
    assert_almost_equal(coverage_error([[0, 1, 0]], [[0.25, 0.5, 0.75]]), 2)
    assert_almost_equal(coverage_error([[0, 1, 1]], [[0.25, 0.5, 0.75]]), 2)
    assert_almost_equal(coverage_error([[1, 0, 0]], [[0.25, 0.5, 0.75]]), 3)
    assert_almost_equal(coverage_error([[1, 0, 1]], [[0.25, 0.5, 0.75]]), 3)
    assert_almost_equal(coverage_error([[1, 1, 0]], [[0.25, 0.5, 0.75]]), 3)
    assert_almost_equal(coverage_error([[1, 1, 1]], [[0.25, 0.5, 0.75]]), 3)

    assert_almost_equal(coverage_error([[0, 0, 0]], [[0.75, 0.5, 0.25]]), 0)
    assert_almost_equal(coverage_error([[0, 0, 1]], [[0.75, 0.5, 0.25]]), 3)
    assert_almost_equal(coverage_error([[0, 1, 0]], [[0.75, 0.5, 0.25]]), 2)
    assert_almost_equal(coverage_error([[0, 1, 1]], [[0.75, 0.5, 0.25]]), 3)
    assert_almost_equal(coverage_error([[1, 0, 0]], [[0.75, 0.5, 0.25]]), 1)
    assert_almost_equal(coverage_error([[1, 0, 1]], [[0.75, 0.5, 0.25]]), 3)
    assert_almost_equal(coverage_error([[1, 1, 0]], [[0.75, 0.5, 0.25]]), 2)
    assert_almost_equal(coverage_error([[1, 1, 1]], [[0.75, 0.5, 0.25]]), 3)

    assert_almost_equal(coverage_error([[0, 0, 0]], [[0.5, 0.75, 0.25]]), 0)
    assert_almost_equal(coverage_error([[0, 0, 1]], [[0.5, 0.75, 0.25]]), 3)
    assert_almost_equal(coverage_error([[0, 1, 0]], [[0.5, 0.75, 0.25]]), 1)
    assert_almost_equal(coverage_error([[0, 1, 1]], [[0.5, 0.75, 0.25]]), 3)
    assert_almost_equal(coverage_error([[1, 0, 0]], [[0.5, 0.75, 0.25]]), 2)
    assert_almost_equal(coverage_error([[1, 0, 1]], [[0.5, 0.75, 0.25]]), 3)
    assert_almost_equal(coverage_error([[1, 1, 0]], [[0.5, 0.75, 0.25]]), 2)
    assert_almost_equal(coverage_error([[1, 1, 1]], [[0.5, 0.75, 0.25]]), 3)

    # Non trivial case
        coverage_error([[0, 1, 0], [1, 1, 0]], [[0.1, 10.0, -3], [0, 1, 3]]),
        (1 + 3) / 2.0,
    )

        coverage_error(
            [[0, 1, 0], [1, 1, 0], [0, 1, 1]], [[0.1, 10, -3], [0, 1, 3], [0, 2, 0]]
        ),
        (1 + 3 + 3) / 3.0,
    )

            [[0, 1, 0], [1, 1, 0], [0, 1, 1]], [[0.1, 10, -3], [3, 1, 3], [0, 2, 0]]
        ),
    )

def test_coverage_tie_handling():
    assert_almost_equal(coverage_error([[0, 0]], [[0.5, 0.5]]), 0)
    assert_almost_equal(coverage_error([[1, 0]], [[0.5, 0.5]]), 2)
    assert_almost_equal(coverage_error([[0, 1]], [[0.5, 0.5]]), 2)
    assert_almost_equal(coverage_error([[1, 1]], [[0.5, 0.5]]), 2)

    assert_almost_equal(coverage_error([[0, 0, 0]], [[0.25, 0.5, 0.5]]), 0)
    assert_almost_equal(coverage_error([[0, 0, 1]], [[0.25, 0.5, 0.5]]), 2)
    assert_almost_equal(coverage_error([[0, 1, 0]], [[0.25, 0.5, 0.5]]), 2)
    assert_almost_equal(coverage_error([[0, 1, 1]], [[0.25, 0.5, 0.5]]), 2)
    assert_almost_equal(coverage_error([[1, 0, 0]], [[0.25, 0.5, 0.5]]), 3)
    assert_almost_equal(coverage_error([[1, 0, 1]], [[0.25, 0.5, 0.5]]), 3)
    assert_almost_equal(coverage_error([[1, 1, 0]], [[0.25, 0.5, 0.5]]), 3)
    assert_almost_equal(coverage_error([[1, 1, 1]], [[0.25, 0.5, 0.5]]), 3)

def test_coverage_1d_error_message(y_true, y_score):
    # https://github.com/scikit-learn/scikit-learn/issues/23368
    with pytest.raises(ValueError, match=r"Expected 2D array, got 1D array instead"):
        coverage_error(y_true, y_score)

def test_label_ranking_loss():
    assert_almost_equal(label_ranking_loss([[0, 1]], [[0.25, 0.75]]), 0)
    assert_almost_equal(label_ranking_loss([[0, 1]], [[0.75, 0.25]]), 1)

    assert_almost_equal(label_ranking_loss([[0, 0, 1]], [[0.25, 0.5, 0.75]]), 0)
    assert_almost_equal(label_ranking_loss([[0, 1, 0]], [[0.25, 0.5, 0.75]]), 1 / 2)
    assert_almost_equal(label_ranking_loss([[0, 1, 1]], [[0.25, 0.5, 0.75]]), 0)
    assert_almost_equal(label_ranking_loss([[1, 0, 0]], [[0.25, 0.5, 0.75]]), 2 / 2)
    assert_almost_equal(label_ranking_loss([[1, 0, 1]], [[0.25, 0.5, 0.75]]), 1 / 2)
    assert_almost_equal(label_ranking_loss([[1, 1, 0]], [[0.25, 0.5, 0.75]]), 2 / 2)

    # Undefined metrics -  the ranking doesn't matter
    assert_almost_equal(label_ranking_loss([[0, 0]], [[0.75, 0.25]]), 0)
    assert_almost_equal(label_ranking_loss([[1, 1]], [[0.75, 0.25]]), 0)
    assert_almost_equal(label_ranking_loss([[0, 0]], [[0.5, 0.5]]), 0)
    assert_almost_equal(label_ranking_loss([[1, 1]], [[0.5, 0.5]]), 0)

    assert_almost_equal(label_ranking_loss([[0, 0, 0]], [[0.5, 0.75, 0.25]]), 0)
    assert_almost_equal(label_ranking_loss([[1, 1, 1]], [[0.5, 0.75, 0.25]]), 0)
    assert_almost_equal(label_ranking_loss([[0, 0, 0]], [[0.25, 0.5, 0.5]]), 0)
    assert_almost_equal(label_ranking_loss([[1, 1, 1]], [[0.25, 0.5, 0.5]]), 0)

        label_ranking_loss([[0, 1, 0], [1, 1, 0]], [[0.1, 10.0, -3], [0, 1, 3]]),
        (0 + 2 / 2) / 2.0,
    )

        label_ranking_loss(
        ),
        (0 + 2 / 2 + 1 / 2) / 3.0,
    )

        ),
    )

def test_label_ranking_loss_sparse(csr_container):
            csr_container(np.array([[0, 1, 0], [1, 1, 0]])), [[0.1, 10, -3], [3, 1, 3]]
        ),
    )

def test_ranking_appropriate_input_shape():
        label_ranking_loss([[0, 1], [0, 1]], [0, 1])
        label_ranking_loss([[0, 1], [0, 1]], [[0, 1]])
        label_ranking_loss([[0, 1], [0, 1]], [[0], [1]])
        label_ranking_loss([[0, 1]], [[0, 1], [0, 1]])
        label_ranking_loss([[0], [1]], [[0, 1], [0, 1]])

def test_ranking_loss_ties_handling():
    assert_almost_equal(label_ranking_loss([[1, 0]], [[0.5, 0.5]]), 1)
    assert_almost_equal(label_ranking_loss([[0, 1]], [[0.5, 0.5]]), 1)
    assert_almost_equal(label_ranking_loss([[0, 0, 1]], [[0.25, 0.5, 0.5]]), 1 / 2)
    assert_almost_equal(label_ranking_loss([[0, 1, 0]], [[0.25, 0.5, 0.5]]), 1 / 2)
    assert_almost_equal(label_ranking_loss([[0, 1, 1]], [[0.25, 0.5, 0.5]]), 0)
    assert_almost_equal(label_ranking_loss([[1, 0, 0]], [[0.25, 0.5, 0.5]]), 1)
    assert_almost_equal(label_ranking_loss([[1, 0, 1]], [[0.25, 0.5, 0.5]]), 1)
    assert_almost_equal(label_ranking_loss([[1, 1, 0]], [[0.25, 0.5, 0.5]]), 1)

def test_dcg_score():
    _, y_true = make_multilabel_classification(random_state=0, n_classes=10)
    y_score = -y_true + 1
    _test_dcg_score_for(y_true, y_score)
    y_true, y_score = np.random.RandomState(0).random_sample((2, 100, 10))

def _test_dcg_score_for(y_true, y_score):
    discount = np.log2(np.arange(y_true.shape[1]) + 2)
    ideal = _dcg_sample_scores(y_true, y_true)
    score = _dcg_sample_scores(y_true, y_score)
    assert (score <= ideal).all()
    assert (_dcg_sample_scores(y_true, y_true, k=5) <= ideal).all()
    assert ideal.shape == (y_true.shape[0],)
    assert score.shape == (y_true.shape[0],)
    assert ideal == pytest.approx((np.sort(y_true)[:, ::-1] / discount).sum(axis=1))

def test_dcg_ties():
    y_true = np.asarray([np.arange(5)])
    y_score = np.zeros(y_true.shape)
    dcg = _dcg_sample_scores(y_true, y_score)
    dcg_ignore_ties = _dcg_sample_scores(y_true, y_score, ignore_ties=True)
    discounts = 1 / np.log2(np.arange(2, 7))
    assert dcg == pytest.approx([discounts.sum() * y_true.mean()])
    assert dcg_ignore_ties == pytest.approx([(discounts * y_true[:, ::-1]).sum()])
    y_score[0, 3:] = 1
    assert dcg == pytest.approx(
        [
            discounts[:2].sum() * y_true[0, 3:].mean()
            + discounts[2:].sum() * y_true[0, :3].mean()
        ]
    )

def test_ndcg_ignore_ties_with_k():
    a = np.arange(12).reshape((2, 6))
    assert ndcg_score(a, a, k=3, ignore_ties=True) == pytest.approx(
        ndcg_score(a, a, k=3, ignore_ties=True)
    )

def test_ndcg_negative_ndarray_error():
    """Check `ndcg_score` exception when `y_true` contains negative values."""
    y_true = np.array([[-0.89, -0.53, -0.47, 0.39, 0.56]])
    y_score = np.array([[0.07, 0.31, 0.75, 0.33, 0.27]])
    expected_message = "ndcg_score should not be used on negative y_true values"
    with pytest.raises(ValueError, match=expected_message):
        ndcg_score(y_true, y_score)

def test_ndcg_invariant():
    y_true = np.arange(70).reshape(7, 10)
    y_score = y_true + np.random.RandomState(0).uniform(-0.2, 0.2, size=y_true.shape)
    ndcg = ndcg_score(y_true, y_score)
    ndcg_no_ties = ndcg_score(y_true, y_score, ignore_ties=True)
    assert ndcg == pytest.approx(ndcg_no_ties)
    assert ndcg == pytest.approx(1.0)
    y_score += 1000
    assert ndcg_score(y_true, y_score) == pytest.approx(1.0)

def test_ndcg_toy_examples(ignore_ties):
    y_true = 3 * np.eye(7)[:5]
    y_score = np.tile(np.arange(6, -1, -1), (5, 1))
    y_score_noisy = y_score + np.random.RandomState(0).uniform(
        -0.2, 0.2, size=y_score.shape
    )
    assert _dcg_sample_scores(
        y_true, y_score, ignore_ties=ignore_ties
    ) == pytest.approx(3 / np.log2(np.arange(2, 7)))
        y_true, y_score_noisy, ignore_ties=ignore_ties
    assert _ndcg_sample_scores(
    ) == pytest.approx(1 / np.log2(np.arange(2, 7)))
        y_true, y_score, log_base=10, ignore_ties=ignore_ties
    ) == pytest.approx(3 / np.log10(np.arange(2, 7)))
    assert ndcg_score(y_true, y_score, ignore_ties=ignore_ties) == pytest.approx(
        (1 / np.log2(np.arange(2, 7))).mean()
    )
    assert dcg_score(y_true, y_score, ignore_ties=ignore_ties) == pytest.approx(
        (3 / np.log2(np.arange(2, 7))).mean()
    )
    y_true = 3 * np.ones((5, 7))
    expected_dcg_score = (3 / np.log2(np.arange(2, 9))).sum()
    ) == pytest.approx(expected_dcg_score * np.ones(5))
    ) == pytest.approx(np.ones(5))
        expected_dcg_score
    )
    assert ndcg_score(y_true, y_score, ignore_ties=ignore_ties) == pytest.approx(1.0)

def test_ndcg_error_single_document():
    """Check that we raise an informative error message when trying to
    compute NDCG with a single document."""
    err_msg = (
        "Computing NDCG is only meaningful when there is more than 1 document. "
        "Got 1 instead."
    )
        ndcg_score([[1]], [[1]])

def test_ndcg_score():
    _test_ndcg_score_for(y_true, y_score)

def _test_ndcg_score_for(y_true, y_score):
    ideal = _ndcg_sample_scores(y_true, y_true)
    score = _ndcg_sample_scores(y_true, y_score)
    all_zero = (y_true == 0).all(axis=1)
    assert ideal[~all_zero] == pytest.approx(np.ones((~all_zero).sum()))
    assert ideal[all_zero] == pytest.approx(np.zeros(all_zero.sum()))
    assert score[~all_zero] == pytest.approx(
        _dcg_sample_scores(y_true, y_score)[~all_zero]
        / _dcg_sample_scores(y_true, y_true)[~all_zero]
    )
    assert score[all_zero] == pytest.approx(np.zeros(all_zero.sum()))

def test_partial_roc_auc_score():
    # Check `roc_auc_score` for max_fpr != `None`
    y_true = np.array([0, 0, 1, 1])
    assert roc_auc_score(y_true, y_true, max_fpr=1) == 1
    assert roc_auc_score(y_true, y_true, max_fpr=0.001) == 1
        assert roc_auc_score(y_true, y_true, max_fpr=-0.1)
        assert roc_auc_score(y_true, y_true, max_fpr=1.1)
        assert roc_auc_score(y_true, y_true, max_fpr=0)

    y_scores = np.array([0.1, 0, 0.1, 0.01])
    roc_auc_with_max_fpr_one = roc_auc_score(y_true, y_scores, max_fpr=1)
    unconstrained_roc_auc = roc_auc_score(y_true, y_scores)
    assert roc_auc_with_max_fpr_one == unconstrained_roc_auc
    assert roc_auc_score(y_true, y_scores, max_fpr=0.3) == 0.5

    y_true, y_pred, _ = make_prediction(binary=True)
    for max_fpr in np.linspace(1e-4, 1, 5):
            roc_auc_score(y_true, y_pred, max_fpr=max_fpr),
            _partial_roc_auc_score(y_true, y_pred, max_fpr),
        )

def test_top_k_accuracy_score(y_true, k, true_score):
        [
            [0.4, 0.3, 0.2, 0.1],
            [0.1, 0.3, 0.4, 0.2],
            [0.4, 0.1, 0.2, 0.3],
            [0.3, 0.2, 0.4, 0.1],
        ]
    )
    score = top_k_accuracy_score(y_true, y_score, k=k)
    assert score == pytest.approx(true_score)

def test_top_k_accuracy_score_binary(y_score, k, true_score):
    y_true = [0, 0, 1, 1]

    threshold = 0.5 if y_score.min() >= 0 and y_score.max() <= 1 else 0
    y_pred = (y_score > threshold).astype(np.int64) if k == 1 else y_true

    score_acc = accuracy_score(y_true, y_pred)

    assert score == score_acc == pytest.approx(true_score)

def test_top_k_accuracy_score_multiclass_with_labels(
    y_true, true_score, labels, labels_as_ndarray
):
    """Test when labels and y_score are multiclass."""
    if labels_as_ndarray:
        [
        ]
    )

    score = top_k_accuracy_score(y_true, y_score, k=2, labels=labels)

def test_top_k_accuracy_score_increasing():
    # Make sure increasing k leads to a higher score
        n_classes=10, n_samples=1000, n_informative=10, random_state=0
    )

    clf = LogisticRegression(random_state=0)

    for X, y in zip((X_train, X_test), (y_train, y_test)):
        scores = [
            top_k_accuracy_score(y, clf.predict_proba(X), k=k) for k in range(2, 10)
        ]

        assert np.all(np.diff(scores) > 0)

def test_top_k_accuracy_score_ties(y_true, k, true_score):
    # Make sure highest indices labels are chosen first in case of ties
        [
            [5, 5, 7, 0],
            [1, 5, 5, 5],
            [0, 0, 3, 3],
            [1, 1, 1, 1],
        ]
    )
    assert top_k_accuracy_score(y_true, y_score, k=k) == pytest.approx(true_score)

def test_top_k_accuracy_score_warning(y_true, k):
        [
            [0.1, 0.4, 0.3, 0.2],
            [0.2, 0.1, 0.4, 0.3],
            [0.3, 0.2, 0.1, 0.4],
        ]
    )
        r"'k' \(\d+\) greater than or equal to 'n_classes' \(\d+\) will result in a "
        "perfect score and is therefore meaningless."
    )
        score = top_k_accuracy_score(y_true, y_score, k=k)
    assert score == 1

def test_top_k_accuracy_score_error(y_true, y_score, labels, msg):
        top_k_accuracy_score(y_true, y_score, k=2, labels=labels)

def test_label_ranking_avg_precision_score_should_allow_csr_matrix_for_y_true_input(
    csr_container,
):
    # Test that label_ranking_avg_precision_score accept sparse y_true.
    # Non-regression test for #22575
    y_true = csr_container([[1, 0, 0], [0, 0, 1]])
    y_score = np.array([[0.5, 0.9, 0.6], [0, 0, 1]])
    result = label_ranking_average_precision_score(y_true, y_score)
    assert result == pytest.approx(2 / 3)

def test_ranking_metric_pos_label_types(metric, classes):
    """Check that the metric works with different types of `pos_label`.

    We can expect `pos_label` to be a bool, an integer, a float, a string.
    No error should be raised for those types.
    """
    n_samples, pos_label = 10, classes[-1]
    y_true = rng.choice(classes, size=n_samples, replace=True)
    y_proba = rng.rand(n_samples)
    result = metric(y_true, y_proba, pos_label=pos_label)
    if isinstance(result, float):
        assert not np.isnan(result)
    else:
        metric_1, metric_2, thresholds = result
        assert not np.isnan(metric_1).any()
        assert not np.isnan(metric_2).any()
        assert not np.isnan(thresholds).any()

def test_roc_curve_with_probablity_estimates(global_random_seed):
    """Check that thresholds do not exceed 1.0 when `y_score` is a probability
    estimate.

    https://github.com/scikit-learn/scikit-learn/issues/26193
    """
    y_true = rng.randint(0, 2, size=10)
    y_score = rng.rand(10)
    _, _, thresholds = roc_curve(y_true, y_score)
    assert np.isinf(thresholds[0])

def _get_metric_params_list(metric: str, n_features: int, seed: int = 1):
    """Return list of dummy DistanceMetric kwargs for tests."""

    # Distinguishing on cases not to compute unneeded datastructures.

    if metric == "minkowski":
        minkowski_kwargs = [
            dict(p=1.5),
            dict(p=2),
            dict(p=3),
            dict(p=np.inf),
            dict(p=3, w=rng.rand(n_features)),
        ]

        return minkowski_kwargs

    if metric == "seuclidean":
        return [dict(V=rng.rand(n_features))]

    # Case of: "euclidean", "manhattan", "chebyshev", "haversine" or any other metric.
    # In those cases, no kwargs is needed.
    return [{}]

def assert_same_distances_for_common_neighbors(
    query_idx,
    dist_row_a,
    dist_row_b,
    indices_row_a,
    indices_row_b,
    rtol,
    atol,
):
    """Check that the distances of common neighbors are equal up to tolerance.

    This does not check if there are missing neighbors in either result set.
    Missingness is handled by assert_no_missing_neighbors.
    """
    # Compute a mapping from indices to distances for each result set and
    # check that the computed neighbors with matching indices are within
    # the expected distance tolerance.
    indices_to_dist_a = dict(zip(indices_row_a, dist_row_a))
    indices_to_dist_b = dict(zip(indices_row_b, dist_row_b))

    common_indices = set(indices_row_a).intersection(set(indices_row_b))
    for idx in common_indices:
        dist_a = indices_to_dist_a[idx]
        dist_b = indices_to_dist_b[idx]
        try:
            assert_allclose(dist_a, dist_b, rtol=rtol, atol=atol)
        except AssertionError as e:
            # Wrap exception to provide more context while also including
            # the original exception with the computed absolute and
            # relative differences.
            raise AssertionError(
                f"Query vector with index {query_idx} lead to different distances"
                f" for common neighbor with index {idx}:"
                f" dist_a={dist_a} vs dist_b={dist_b} (with atol={atol} and"
                f" rtol={rtol})"
            ) from e

def assert_no_missing_neighbors(
    threshold,
):
    """Compare the indices of neighbors in two results sets.

    Any neighbor index with a distance below the precision threshold should
    match one in the other result set. We ignore the last few neighbors beyond
    the threshold as those can typically be missing due to rounding errors.

    For radius queries, the threshold is just the radius minus the expected
    precision level.

    For k-NN queries, it is the maximum distance to the k-th neighbor minus the
    expected precision level.
    """
    mask_a = dist_row_a < threshold
    mask_b = dist_row_b < threshold
    missing_from_b = np.setdiff1d(indices_row_a[mask_a], indices_row_b)
    missing_from_a = np.setdiff1d(indices_row_b[mask_b], indices_row_a)
    if len(missing_from_a) > 0 or len(missing_from_b) > 0:
        raise AssertionError(
            f"Query vector with index {query_idx} lead to mismatched result indices:\n"
            f"neighbors in b missing from a: {missing_from_a}\n"
            f"neighbors in a missing from b: {missing_from_b}\n"
            f"dist_row_a={dist_row_a}\n"
            f"dist_row_b={dist_row_b}\n"
            f"indices_row_a={indices_row_a}\n"
            f"indices_row_b={indices_row_b}\n"
        )

def assert_compatible_argkmin_results(
    neighbors_dists_a,
    neighbors_dists_b,
    neighbors_indices_a,
    neighbors_indices_b,
    rtol=1e-5,
    atol=1e-6,
):
    """Assert that argkmin results are valid up to rounding errors.

    This function asserts that the results of argkmin queries are valid up to:
    - rounding error tolerance on distance values;
    - permutations of indices for distances values that differ up to the
      expected precision level.

    Furthermore, the distances must be sorted.

    To be used for testing neighbors queries on float32 datasets: we accept
    neighbors rank swaps only if they are caused by small rounding errors on
    the distance computations.
    """
    is_sorted = lambda a: np.all(a[:-1] <= a[1:])

    assert (
        neighbors_dists_a.shape
        == neighbors_dists_b.shape
        == neighbors_indices_a.shape
        == neighbors_indices_b.shape
    ), "Arrays of results have incompatible shapes."

    n_queries, _ = neighbors_dists_a.shape

    # Asserting equality results one row at a time
    for query_idx in range(n_queries):
        dist_row_a = neighbors_dists_a[query_idx]
        dist_row_b = neighbors_dists_b[query_idx]
        indices_row_a = neighbors_indices_a[query_idx]
        indices_row_b = neighbors_indices_b[query_idx]

        assert is_sorted(dist_row_a), f"Distances aren't sorted on row {query_idx}"
        assert is_sorted(dist_row_b), f"Distances aren't sorted on row {query_idx}"

        assert_same_distances_for_common_neighbors(
            query_idx,
            dist_row_a,
            dist_row_b,
            indices_row_a,
            indices_row_b,
            rtol,
            atol,
        )

        # Check that any neighbor with distances below the rounding error
        # threshold have matching indices. The threshold is the distance to the
        # k-th neighbors minus the expected precision level:
        #
        # (1 - rtol) * dist_k - atol
        #
        # Where dist_k is defined as the maximum distance to the kth-neighbor
        # among the two result sets. This way of defining the threshold is
        # stricter than taking the minimum of the two.
        threshold = (1 - rtol) * np.maximum(
            np.max(dist_row_a), np.max(dist_row_b)
        ) - atol
        assert_no_missing_neighbors(
            threshold,
        )

def _non_trivial_radius(
    *,
    X=None,
    Y=None,
    metric=None,
    precomputed_dists=None,
    expected_n_neighbors=10,
    n_subsampled_queries=10,
    **metric_kwargs,
):
    # Find a non-trivial radius using a small subsample of the pairwise
    # distances between X and Y: we want to return around expected_n_neighbors
    # on average. Yielding too many results would make the test slow (because
    # checking the results is expensive for large result sets), yielding 0 most
    # of the time would make the test useless.
    assert precomputed_dists is not None or metric is not None, (
        "Either metric or precomputed_dists must be provided."
    )

    if precomputed_dists is None:
        assert X is not None
        assert Y is not None
        sampled_dists = pairwise_distances(X, Y, metric=metric, **metric_kwargs)
    else:
        sampled_dists = precomputed_dists[:n_subsampled_queries].copy()
    sampled_dists.sort(axis=1)
    return sampled_dists[:, expected_n_neighbors].mean()

def assert_compatible_radius_results(
    radius,
    check_sorted=True,
):
    """Assert that radius neighborhood results are valid up to:

      - relative and absolute tolerance on computed distance values
      - permutations of indices for distances values that differ up to
        a precision level
      - missing or extra last elements if their distance is
        close to the radius

    To be used for testing neighbors queries on float32 datasets: we
    accept neighbors rank swaps only if they are caused by small
    rounding errors on the distance computations.

    Input arrays must be sorted w.r.t distances.
    """

    assert (
        len(neighbors_dists_a)
        == len(neighbors_dists_b)
        == len(neighbors_indices_a)
        == len(neighbors_indices_b)
    )

    n_queries = len(neighbors_dists_a)

    # Asserting equality of results one vector at a time

        if check_sorted:
            assert is_sorted(dist_row_a), f"Distances aren't sorted on row {query_idx}"
            assert is_sorted(dist_row_b), f"Distances aren't sorted on row {query_idx}"

        assert len(dist_row_a) == len(indices_row_a)
        assert len(dist_row_b) == len(indices_row_b)

        # Check that all distances are within the requested radius
        if len(dist_row_a) > 0:
            max_dist_a = np.max(dist_row_a)
            assert max_dist_a <= radius, (
                f"Largest returned distance {max_dist_a} not within requested"
                f" radius {radius} on row {query_idx}"
            )
        if len(dist_row_b) > 0:
            max_dist_b = np.max(dist_row_b)
            assert max_dist_b <= radius, (
                f"Largest returned distance {max_dist_b} not within requested"
            )

            rtol,
            atol,
        )

        threshold = (1 - rtol) * radius - atol
        )

def test_assert_compatible_argkmin_results():
    atol = 1e-7
    rtol = 0.0
    tols = dict(atol=atol, rtol=rtol)

    eps = atol / 3
    _1m = 1.0 - eps
    _1p = 1.0 + eps

    _6_1m = 6.1 - eps
    _6_1p = 6.1 + eps

    ref_dist = np.array(
        [
            [1.2, 2.5, _6_1m, 6.1, _6_1p],
            [_1m, _1m, 1, _1p, _1p],
        ]
    )
    ref_indices = np.array(
        [
            [1, 2, 3, 4, 5],
            [6, 7, 8, 9, 10],
        ]
    )

    # Sanity check: compare the reference results to themselves.
    assert_compatible_argkmin_results(
        ref_dist, ref_dist, ref_indices, ref_indices, rtol
    )

    # Apply valid permutation on indices: the last 3 points are all very close
    # to one another so we accept any permutation on their rankings.
        np.array([[1.2, 2.5, _6_1m, 6.1, _6_1p]]),
        np.array([[1, 2, 3, 4, 5]]),
        np.array([[1, 2, 5, 4, 3]]),
        **tols,
    )

    # The last few indices do not necessarily have to match because of the rounding
    # errors on the distances: there could be tied results at the boundary.
        np.array([[1.2, 2.5, 3.0, 6.1, _6_1p]]),
        np.array([[1.2, 2.5, 3.0, _6_1m, 6.1]]),
        np.array([[1, 2, 3, 6, 7]]),
        **tols,
    )

    # All points have close distances so any ranking permutation
    # is valid for this query result.
        np.array([[_1m, 1, _1p, _1p, _1p]]),
        np.array([[1, 1, 1, 1, _1p]]),
        np.array([[7, 6, 8, 10, 9]]),
        np.array([[6, 9, 7, 8, 10]]),
        **tols,
    )

    # They could also be nearly truncation of very large nearly tied result
    # sets hence all indices can also be distinct in this case:
        np.array([[_1m, 1, 1, 1, _1p]]),
        np.array([[34, 30, 8, 12, 24]]),
        np.array([[42, 1, 21, 13, 3]]),
        **tols,
    )

    # Apply invalid permutation on indices: permuting the ranks of the 2
    # nearest neighbors is invalid because the distance values are too
    # different.
        "Query vector with index 0 lead to different distances for common neighbor with"
        " index 1: dist_a=1.2 vs dist_b=2.5"
    )
    with pytest.raises(AssertionError, match=msg):
        assert_compatible_argkmin_results(
            np.array([[1.2, 2.5, _6_1m, 6.1, _6_1p]]),
            np.array([[1, 2, 3, 4, 5]]),
            np.array([[2, 1, 3, 4, 5]]),
            **tols,
        )

    # Detect missing indices within the expected precision level, even when the
    # distances match exactly.
        "neighbors in b missing from a: [12]\nneighbors in a missing from b: [1]"
    )
            np.array([[12, 2, 4, 11, 3]]),
            **tols,
        )

    # Detect missing indices outside the expected precision level.
        "neighbors in b missing from a: []\nneighbors in a missing from b: [3]"
    )
            np.array([[_1m, 1.0, _6_1m, 6.1, _6_1p]]),
            np.array([[1.0, 1.0, _6_1m, 6.1, 7]]),
            np.array([[2, 1, 4, 5, 12]]),
            **tols,
        )

    # Detect missing indices outside the expected precision level, in the other
    # direction:
        "neighbors in b missing from a: [5]\nneighbors in a missing from b: []"
    )
            np.array([[_1m, 1.0, _6_1m, 6.1, 7]]),
            np.array([[1.0, 1.0, _6_1m, 6.1, _6_1p]]),
            np.array([[1, 2, 3, 4, 12]]),
            np.array([[2, 1, 5, 3, 4]]),
            **tols,
        )

    # Distances aren't properly sorted
    msg = "Distances aren't sorted on row 0"
            np.array([[2.5, 1.2, _6_1m, 6.1, _6_1p]]),
            np.array([[2, 1, 4, 5, 3]]),
            **tols,
        )

def test_assert_compatible_radius_results(check_sorted):
    atol = 1e-7
    rtol = 0.0

    ref_dist = [
        np.array([1.2, 2.5, _6_1m, 6.1, _6_1p]),
        np.array([_1m, 1, _1p, _1p]),
    ]

    ref_indices = [
        np.array([1, 2, 3, 4, 5]),
        np.array([6, 7, 8, 9]),
    ]

    assert_compatible_radius_results(
        ref_dist,
        ref_dist,
        ref_indices,
        radius=7.0,
        check_sorted=check_sorted,
        **tols,
    )

    # Apply valid permutation on indices
        np.array([np.array([1.2, 2.5, _6_1m, 6.1, _6_1p])]),
        np.array([np.array([1, 2, 3, 4, 5])]),
        np.array([np.array([1, 2, 4, 5, 3])]),
        **tols,
    )
        np.array([np.array([_1m, _1m, 1, _1p, _1p])]),
        np.array([np.array([6, 7, 8, 9, 10])]),
        np.array([np.array([6, 9, 7, 8, 10])]),
        **tols,
    )

    # Apply invalid permutation on indices
    )
        assert_compatible_radius_results(
            np.array([np.array([1.2, 2.5, _6_1m, 6.1, _6_1p])]),
            np.array([np.array([1, 2, 3, 4, 5])]),
            np.array([np.array([2, 1, 3, 4, 5])]),
            radius=7.0,
            check_sorted=check_sorted,
            **tols,
        )

    # Having extra last or missing elements is valid if they are in the
    # tolerated rounding error range: [(1 - rtol) * radius - atol, radius]
        np.array([np.array([1.2, 2.5, _6_1m, 6.1, _6_1p, _6_1p])]),
        np.array([np.array([1.2, 2.5, _6_1m, 6.1])]),
        np.array([np.array([1, 2, 3, 4, 5, 7])]),
        np.array([np.array([1, 2, 3, 6])]),
        radius=_6_1p,
        **tols,
    )

    # Any discrepancy outside the tolerated rounding error range is invalid and
    # indicates a missing neighbor in one of the result sets.
        "Query vector with index 0 lead to mismatched result indices:\nneighbors in b"
        " missing from a: []\nneighbors in a missing from b: [3]"
    )
            np.array([np.array([1.2, 2.5, 6])]),
            np.array([np.array([1.2, 2.5])]),
            np.array([np.array([1, 2, 3])]),
            np.array([np.array([1, 2])]),
            radius=6.1,
            **tols,
        )
        " missing from a: [4]\nneighbors in a missing from b: [2]"
    )
            np.array([np.array([1.2, 2.1, 2.5])]),
            np.array([np.array([1.2, 2, 2.5])]),
            np.array([np.array([1, 4, 3])]),
            **tols,
        )

    # Radius upper bound is strictly checked
        "Largest returned distance 6.100000033333333 not within requested radius 6.1 on"
        " row 0"
    )
            np.array([np.array([1.2, 2.5, _6_1m, 6.1, 6.1])]),
            np.array([np.array([2, 1, 4, 5, 3])]),
            **tols,
        )
            **tols,
        )

    if check_sorted:
        # Distances aren't properly sorted
        msg = "Distances aren't sorted on row 0"
        with pytest.raises(AssertionError, match=msg):
            assert_compatible_radius_results(
                np.array([np.array([1.2, 2.5, _6_1m, 6.1, _6_1p])]),
                np.array([np.array([2.5, 1.2, _6_1m, 6.1, _6_1p])]),
                np.array([np.array([1, 2, 3, 4, 5])]),
                np.array([np.array([2, 1, 4, 5, 3])]),
                radius=_6_1p,
                check_sorted=True,
                **tols,
            )
    else:
            np.array([np.array([2.5, 1.2, _6_1m, 6.1, _6_1p])]),
            radius=_6_1p,
            check_sorted=False,
            **tols,
        )

def test_pairwise_distances_reduction_is_usable_for(csr_container):
    X = rng.rand(100, 10)
    Y = rng.rand(100, 10)
    Y_csr = csr_container(Y)
    metric = "manhattan"

    # Must be usable for all possible pair of {dense, sparse} datasets
    assert BaseDistancesReductionDispatcher.is_usable_for(X, Y, metric)
    assert BaseDistancesReductionDispatcher.is_usable_for(X_csr, Y_csr, metric)
    assert BaseDistancesReductionDispatcher.is_usable_for(X_csr, Y, metric)
    assert BaseDistancesReductionDispatcher.is_usable_for(X, Y_csr, metric)

    assert BaseDistancesReductionDispatcher.is_usable_for(
        X.astype(np.float64), Y.astype(np.float64), metric
    )

        X.astype(np.float32), Y.astype(np.float32), metric
    )

    assert not BaseDistancesReductionDispatcher.is_usable_for(
        X.astype(np.int64), Y.astype(np.int64), metric
    )

    assert not BaseDistancesReductionDispatcher.is_usable_for(X, Y, metric="pyfunc")
        X.astype(np.float32), Y, metric
    )
        X, Y.astype(np.int32), metric
    )

    # F-ordered arrays are not supported
        np.asfortranarray(X), Y, metric
    )

    assert BaseDistancesReductionDispatcher.is_usable_for(X_csr, Y, metric="euclidean")
        X, Y_csr, metric="sqeuclidean"
    )

    # FIXME: the current Cython implementation is too slow for a large number of
    # features. We temporarily disable it to fallback on SciPy's implementation.
    # See: https://github.com/scikit-learn/scikit-learn/issues/28191
        X_csr, Y_csr, metric="sqeuclidean"
    )
        X_csr, Y_csr, metric="euclidean"
    )

    # CSR matrices without non-zeros elements aren't currently supported
    # TODO: support CSR matrices without non-zeros elements
    X_csr_0_nnz = csr_container(X * 0)
    assert not BaseDistancesReductionDispatcher.is_usable_for(X_csr_0_nnz, Y, metric)

    # CSR matrices with int64 indices and indptr (e.g. large nnz, or large n_features)
    # aren't supported as of now.
    # See: https://github.com/scikit-learn/scikit-learn/issues/23653
    # TODO: support CSR matrices with int64 indices and indptr
    X_csr_int64 = csr_container(X)
    X_csr_int64.indices = X_csr_int64.indices.astype(np.int64)
    assert not BaseDistancesReductionDispatcher.is_usable_for(X_csr_int64, Y, metric)

def test_argkmin_factory_method_wrong_usages():
    k = 5
    metric = "euclidean"

    msg = (
        "Only float64 or float32 datasets pairs are supported at this time, "
        "got: X.dtype=float32 and Y.dtype=float64"
    )
        ArgKmin.compute(X=X.astype(np.float32), Y=Y, k=k, metric=metric)

    msg = (
        "got: X.dtype=float64 and Y.dtype=int32"
    )
        ArgKmin.compute(X=X, Y=Y.astype(np.int32), k=k, metric=metric)

    with pytest.raises(ValueError, match="k == -1, must be >= 1."):
        ArgKmin.compute(X=X, Y=Y, k=-1, metric=metric)

    with pytest.raises(ValueError, match="k == 0, must be >= 1."):
        ArgKmin.compute(X=X, Y=Y, k=0, metric=metric)

    with pytest.raises(ValueError, match="Unrecognized metric"):
        ArgKmin.compute(X=X, Y=Y, k=k, metric="wrong metric")

        ValueError, match=r"Buffer has wrong number of dimensions \(expected 2, got 1\)"
    ):
        ArgKmin.compute(X=np.array([1.0, 2.0]), Y=Y, k=k, metric=metric)

    with pytest.raises(ValueError, match="ndarray is not C-contiguous"):
        ArgKmin.compute(X=np.asfortranarray(X), Y=Y, k=k, metric=metric)

    # A UserWarning must be raised in this case.
    unused_metric_kwargs = {"p": 3}

    message = r"Some metric_kwargs have been passed \({'p': 3}\) but"

        ArgKmin.compute(
            X=X, Y=Y, k=k, metric=metric, metric_kwargs=unused_metric_kwargs
        )

    metric_kwargs = {
        "p": 3,  # unused
        "Y_norm_squared": sqeuclidean_row_norms(Y, num_threads=2),
    }

    message = r"Some metric_kwargs have been passed \({'p': 3, 'Y_norm_squared'"

        ArgKmin.compute(X=X, Y=Y, k=k, metric=metric, metric_kwargs=metric_kwargs)

    # No user warning must be raised in this case.
        "X_norm_squared": sqeuclidean_row_norms(X, num_threads=2),
    }
        warnings.simplefilter("error", category=UserWarning)

    }

def test_argkmin_classmode_factory_method_wrong_usages():
    k = 5

    weights = "uniform"
    Y_labels = rng.randint(low=0, high=10, size=100)
    unique_Y_labels = np.unique(Y_labels)

    msg = (
    )
        ArgKminClassMode.compute(
            X=X.astype(np.float32),
            Y=Y,
            k=k,
            weights=weights,
            Y_labels=Y_labels,
            unique_Y_labels=unique_Y_labels,
        )

    msg = (
    )
            X=X,
            Y=Y.astype(np.int32),
            k=k,
        )

            X=X,
            Y=Y,
            k=-1,
        )

            X=X,
            Y=Y,
            k=0,
        )

            X=X,
            Y=Y,
            k=k,
            metric="wrong metric",
        )

    ):
            X=np.array([1.0, 2.0]),
            Y=Y,
            k=k,
        )

            X=np.asfortranarray(X),
            Y=Y,
            k=k,
        )

    non_existent_weights_strategy = "non_existent_weights_strategy"
    message = (
        "Only the 'uniform' or 'distance' weights options are supported at this time. "
        f"Got: weights='{non_existent_weights_strategy}'."
    )
            X=X,
            Y=Y,
            k=k,
            weights=non_existent_weights_strategy,
        )

def test_radius_neighbors_factory_method_wrong_usages():
    radius = 5

    msg = (
    )
        match=msg,
    ):
        RadiusNeighbors.compute(
            X=X.astype(np.float32), Y=Y, radius=radius, metric=metric
        )

    msg = (
    )
    ):
        RadiusNeighbors.compute(X=X, Y=Y.astype(np.int32), radius=radius, metric=metric)

    with pytest.raises(ValueError, match="radius == -1.0, must be >= 0."):
        RadiusNeighbors.compute(X=X, Y=Y, radius=-1, metric=metric)

        RadiusNeighbors.compute(X=X, Y=Y, radius=radius, metric="wrong metric")

    ):
            X=np.array([1.0, 2.0]), Y=Y, radius=radius, metric=metric
        )

            X=np.asfortranarray(X), Y=Y, radius=radius, metric=metric
        )

            X=X, Y=Y, radius=radius, metric=metric, metric_kwargs=unused_metric_kwargs
        )

    }

            X=X, Y=Y, radius=radius, metric=metric, metric_kwargs=metric_kwargs
        )

    }
        )

    }
        )

def test_radius_neighbors_classmode_factory_method_wrong_usages():
    radius = 5

    msg = (
    )
        RadiusNeighborsClassMode.compute(
            Y=Y,
            radius=radius,
            outlier_label=None,
        )

    msg = (
    )
            X=X,
        )

            X=X,
            Y=Y,
            radius=-1,
        )

            X=X,
            Y=Y,
            metric="wrong_metric",
        )

    ):
            Y=Y,
        )

            Y=Y,
        )

    msg = (
    )
            X=X,
            Y=Y,
        )

def test_chunk_size_agnosticism(
    global_random_seed,
    Dispatcher,
    dtype,
):
    """Check that results do not depend on the chunk size."""
    spread = 100
    n_samples_X, n_samples_Y = rng.choice([97, 100, 101, 500], size=2, replace=False)
    X = rng.rand(n_samples_X, n_features).astype(dtype) * spread
    Y = rng.rand(n_samples_Y, n_features).astype(dtype) * spread

    if Dispatcher is ArgKmin:
        parameter = 10
        check_parameters = {}
        compute_parameters = {}
    else:
        radius = _non_trivial_radius(X=X, Y=Y, metric="euclidean")
        parameter = radius
        check_parameters = {"radius": radius}
        compute_parameters = {"sort_results": True}

    ref_dist, ref_indices = Dispatcher.compute(
        X,
        Y,
        parameter,
        chunk_size=256,  # default
        metric="manhattan",
        return_distance=True,
        **compute_parameters,
    )

    dist, indices = Dispatcher.compute(
        X,
        Y,
        chunk_size=41,
    )

    ASSERT_RESULT[(Dispatcher, dtype)](
        ref_dist, dist, ref_indices, indices, **check_parameters
    )

def test_n_threads_agnosticism(
    dtype,
):
    """Check that results do not depend on the number of threads."""

    else:

        X,
        Y,
        chunk_size=25,  # make sure we use multiple threads
    )

    with _get_threadpool_controller().limit(limits=1, user_api="openmp"):
        dist, indices = Dispatcher.compute(
            X,
            Y,
            parameter,
            chunk_size=25,
            **compute_parameters,
        )

    )

def test_format_agnosticism(
    dtype,
):
    """Check that results do not depend on the format (dense, sparse) of the input."""
    n_samples, n_features = 100, 100

    X = rng.rand(n_samples, n_features).astype(dtype) * spread
    Y = rng.rand(n_samples, n_features).astype(dtype) * spread

    else:
        # Adjusting the radius to ensure that the expected results is neither
        # trivially empty nor too large.

    dist_dense, indices_dense = Dispatcher.compute(
        X,
        Y,
        chunk_size=50,
    )

    for _X, _Y in itertools.product((X, X_csr), (Y, Y_csr)):
        if _X is X and _Y is Y:
            continue
            _X,
            _Y,
            chunk_size=50,
        )
        ASSERT_RESULT[(Dispatcher, dtype)](
            dist_dense,
            dist,
            indices_dense,
            indices,
            **check_parameters,
        )

def test_strategies_consistency(
    global_dtype,
    n_features=10,
):
    """Check that the results do not depend on the strategy used."""
    metric = rng.choice(
        np.array(
            [
                "euclidean",
                "minkowski",
                "manhattan",
                "haversine",
            ],
        )
    )
    X = rng.rand(n_samples_X, n_features).astype(global_dtype) * spread
    Y = rng.rand(n_samples_Y, n_features).astype(global_dtype) * spread

    # Haversine distance only accepts 2D data
    if metric == "haversine":
        X = np.ascontiguousarray(X[:, :2])
        Y = np.ascontiguousarray(Y[:, :2])

    else:
        radius = _non_trivial_radius(X=X, Y=Y, metric=metric)

    dist_par_X, indices_par_X = Dispatcher.compute(
        X,
        Y,
        # Taking the first
        metric_kwargs=_get_metric_params_list(
            metric, n_features, seed=global_random_seed
        )[0],
        # To be sure to use parallelization
        chunk_size=n_samples_X // 4,
        strategy="parallel_on_X",
    )

    dist_par_Y, indices_par_Y = Dispatcher.compute(
        X,
        Y,
        )[0],
        chunk_size=n_samples_Y // 4,
        strategy="parallel_on_Y",
    )

    ASSERT_RESULT[(Dispatcher, global_dtype)](
        dist_par_X, dist_par_Y, indices_par_X, indices_par_Y, **check_parameters
    )

def test_pairwise_distances_argkmin(
    metric,
    strategy,
    dtype,
    n_queries=5,
    k=10,
):
    n_features = rng.choice([50, 500])
    translation = rng.choice([0, 1e6])
    spread = 1000
    X = translation + rng.rand(n_queries, n_features).astype(dtype) * spread
    Y = translation + rng.rand(n_samples, n_features).astype(dtype) * spread

    metric_kwargs = _get_metric_params_list(metric, n_features)[0]

    # Reference for argkmin results
    if metric == "euclidean":
        # Compare to scikit-learn GEMM optimized implementation
        dist_matrix = euclidean_distances(X, Y)
    else:
        dist_matrix = cdist(X, Y, metric=metric, **metric_kwargs)
    # Taking argkmin (indices of the k smallest values)
    argkmin_indices_ref = np.argsort(dist_matrix, axis=1)[:, :k]
    # Getting the associated distances
    argkmin_distances_ref = np.zeros(argkmin_indices_ref.shape, dtype=np.float64)
    for row_idx in range(argkmin_indices_ref.shape[0]):
        argkmin_distances_ref[row_idx] = dist_matrix[
            row_idx, argkmin_indices_ref[row_idx]
        ]

        argkmin_distances, argkmin_indices = ArgKmin.compute(
            _X,
            _Y,
            k,
            # So as to have more than a chunk, forcing parallelism.
            chunk_size=n_samples // 4,
        )

        ASSERT_RESULT[(ArgKmin, dtype)](
            argkmin_distances,
            argkmin_distances_ref,
            argkmin_indices,
            argkmin_indices_ref,
        )

def test_pairwise_distances_radius_neighbors(
    metric,
    strategy,
    dtype,
):

    metric_kwargs = _get_metric_params_list(
        metric, n_features, seed=global_random_seed
    )[0]

    else:

    radius = _non_trivial_radius(precomputed_dists=dist_matrix)

    # Getting the neighbors for a given radius
    neigh_indices_ref = []
    neigh_distances_ref = []

    for row in dist_matrix:
        ind = np.arange(row.shape[0])[row <= radius]
        dist = row[ind]

        sort = np.argsort(dist)
        ind, dist = ind[sort], dist[sort]

        neigh_indices_ref.append(ind)
        neigh_distances_ref.append(dist)

    neigh_distances, neigh_indices = RadiusNeighbors.compute(
        X,
        Y,
        radius,
        metric_kwargs=metric_kwargs,
        # So as to have more than a chunk, forcing parallelism.
        chunk_size=n_samples // 4,
        strategy=strategy,
        sort_results=True,
    )

    ASSERT_RESULT[(RadiusNeighbors, dtype)](
        neigh_distances, neigh_distances_ref, neigh_indices, neigh_indices_ref, radius
    )

def test_memmap_backed_data(
    metric,
    dtype,
):
    """Check that the results do not depend on the datasets writability."""
    n_samples, n_features = 128, 10

    # Create read only datasets
    X_mm, Y_mm = create_memmap_backed_data([X, Y])

    else:
        # Scaling the radius slightly with the numbers of dimensions
        radius = 10 ** np.log(n_features)

        X,
        Y,
    )

    dist_mm, indices_mm = Dispatcher.compute(
        X_mm,
        Y_mm,
    )

        ref_dist, dist_mm, ref_indices, indices_mm, **check_parameters
    )

def test_sqeuclidean_row_norms(
    dtype,
):
    n_samples = rng.choice([97, 100, 101, 1000])
    n_features = rng.choice([5, 10, 100])
    num_threads = rng.choice([1, 2, 8])

    sq_row_norm_reference = np.linalg.norm(X, axis=1) ** 2
    sq_row_norm = sqeuclidean_row_norms(X, num_threads=num_threads)

    sq_row_norm_csr = sqeuclidean_row_norms(X_csr, num_threads=num_threads)

    assert_allclose(sq_row_norm_reference, sq_row_norm)
    assert_allclose(sq_row_norm_reference, sq_row_norm_csr)

        X = np.asfortranarray(X)
        sqeuclidean_row_norms(X, num_threads=num_threads)

def test_argkmin_classmode_strategy_consistent():
    k = 5

    results_X = ArgKminClassMode.compute(
        X=X,
        Y=Y,
        k=k,
        Y_labels=Y_labels,
        unique_Y_labels=unique_Y_labels,
    )
    results_Y = ArgKminClassMode.compute(
        X=X,
        Y=Y,
        k=k,
    )
    assert_array_equal(results_X, results_Y)

def test_radius_neighbors_classmode_strategy_consistent(outlier_label):
    radius = 5

    results_X = RadiusNeighborsClassMode.compute(
        X=X,
        Y=Y,
        radius=radius,
        outlier_label=outlier_label,
    )
    results_Y = RadiusNeighborsClassMode.compute(
        X=X,
        Y=Y,
    )
    assert_allclose(results_X, results_Y)

def test_regression_metrics(n_samples=50):
    y_true = np.arange(n_samples)
    y_pred = y_true + 1
    y_pred_2 = y_true - 1

    assert_almost_equal(mean_squared_error(y_true, y_pred), 1.0)
        mean_squared_log_error(y_true, y_pred),
        mean_squared_error(np.log(1 + y_true), np.log(1 + y_pred)),
    )
    assert_almost_equal(mean_absolute_error(y_true, y_pred), 1.0)
    assert_almost_equal(mean_pinball_loss(y_true, y_pred), 0.5)
    assert_almost_equal(mean_pinball_loss(y_true, y_pred_2), 0.5)
    assert_almost_equal(mean_pinball_loss(y_true, y_pred, alpha=0.4), 0.6)
    assert_almost_equal(mean_pinball_loss(y_true, y_pred_2, alpha=0.4), 0.4)
    assert_almost_equal(median_absolute_error(y_true, y_pred), 1.0)
    mape = mean_absolute_percentage_error(y_true, y_pred)
    assert np.isfinite(mape)
    assert mape > 1e6
    assert_almost_equal(max_error(y_true, y_pred), 1.0)
    assert_almost_equal(r2_score(y_true, y_pred), 0.995, 2)
    assert_almost_equal(r2_score(y_true, y_pred, force_finite=False), 0.995, 2)
    assert_almost_equal(explained_variance_score(y_true, y_pred), 1.0)
        explained_variance_score(y_true, y_pred, force_finite=False), 1.0
    )
        mean_tweedie_deviance(y_true, y_pred, power=0),
        mean_squared_error(y_true, y_pred),
    )
        d2_tweedie_score(y_true, y_pred, power=0), r2_score(y_true, y_pred)
    )
    dev_median = np.abs(y_true - np.median(y_true)).sum()
        d2_absolute_error_score(y_true, y_pred),
        1 - np.abs(y_true - y_pred).sum() / dev_median,
    )
    alpha = 0.2
    pinball_loss = lambda y_true, y_pred, alpha: alpha * np.maximum(
        y_true - y_pred, 0
    ) + (1 - alpha) * np.maximum(y_pred - y_true, 0)
    y_quantile = np.percentile(y_true, q=alpha * 100)
        d2_pinball_score(y_true, y_pred, alpha=alpha),
        1
        - pinball_loss(y_true, y_pred, alpha).sum()
        / pinball_loss(y_true, y_quantile, alpha).sum(),
    )
        d2_pinball_score(y_true, y_pred, alpha=0.5),
    )

    # Tweedie deviance needs positive y_pred, except for p=0,
    # p>=2 needs positive y_true
    # results evaluated by sympy
    y_true = np.arange(1, 1 + n_samples)
    y_pred = 2 * y_true
    n = n_samples
        mean_tweedie_deviance(y_true, y_pred, power=-1),
        5 / 12 * n * (n**2 + 2 * n + 1),
    )
        mean_tweedie_deviance(y_true, y_pred, power=1), (n + 1) * (1 - np.log(2))
    )
        mean_tweedie_deviance(y_true, y_pred, power=2), 2 * np.log(2) - 1
    )
        mean_tweedie_deviance(y_true, y_pred, power=3 / 2),
        ((6 * np.sqrt(2) - 8) / n) * np.sqrt(y_true).sum(),
    )
        mean_tweedie_deviance(y_true, y_pred, power=3), np.sum(1 / y_true) / (4 * n)
    )

    dev_mean = 2 * np.mean(xlogy(y_true, 2 * y_true / (n + 1)))
        d2_tweedie_score(y_true, y_pred, power=1),
        1 - (n + 1) * (1 - np.log(2)) / dev_mean,
    )

    dev_mean = 2 * np.log((n + 1) / 2) - 2 / n * np.log(factorial(n))
        d2_tweedie_score(y_true, y_pred, power=2), 1 - (2 * np.log(2) - 1) / dev_mean
    )

def test_root_mean_squared_error_multioutput_raw_value():
    # https://github.com/scikit-learn/scikit-learn/pull/16323
    mse = mean_squared_error([[1]], [[10]], multioutput="raw_values")
    rmse = root_mean_squared_error([[1]], [[10]], multioutput="raw_values")
    assert np.sqrt(mse) == pytest.approx(rmse)

    y_pred = np.array([[0, 0, 0, 1], [1, 0, 1, 1], [0, 0, 0, 1]])

    error = mean_squared_error(y_true, y_pred)
    assert_almost_equal(error, (1.0 / 3 + 2.0 / 3 + 2.0 / 3) / 4.0)

    error = root_mean_squared_error(y_true, y_pred)
    assert_almost_equal(error, 0.454, decimal=2)

    error = mean_squared_log_error(y_true, y_pred)
    assert_almost_equal(error, 0.200, decimal=2)

    error = root_mean_squared_log_error(y_true, y_pred)
    assert_almost_equal(error, 0.315, decimal=2)

    # mean_absolute_error and mean_squared_error are equal because
    # it is a binary problem.
    error = mean_absolute_error(y_true, y_pred)
    assert_almost_equal(error, (1.0 + 2.0 / 3) / 4.0)

    error = mean_pinball_loss(y_true, y_pred)
    assert_almost_equal(error, (1.0 + 2.0 / 3) / 8.0)

    error = np.around(mean_absolute_percentage_error(y_true, y_pred), decimals=2)
    assert np.isfinite(error)
    assert error > 1e6
    error = median_absolute_error(y_true, y_pred)
    assert_almost_equal(error, (1.0 + 1.0) / 4.0)

    error = r2_score(y_true, y_pred, multioutput="variance_weighted")
    assert_almost_equal(error, 1.0 - 5.0 / 2)
    error = r2_score(y_true, y_pred, multioutput="uniform_average")
    assert_almost_equal(error, -0.875)

    score = d2_pinball_score(y_true, y_pred, alpha=0.5, multioutput="raw_values")
    raw_expected_score = [
        1
        - np.abs(y_true[:, i] - y_pred[:, i]).sum()
        / np.abs(y_true[:, i] - np.median(y_true[:, i])).sum()
        for i in range(y_true.shape[1])
    ]
    # in the last case, the denominator vanishes and hence we get nan,
    # but since the numerator vanishes as well the expected score is 1.0
    raw_expected_score = np.where(np.isnan(raw_expected_score), 1, raw_expected_score)
    assert_array_almost_equal(score, raw_expected_score)

    score = d2_pinball_score(y_true, y_pred, alpha=0.5, multioutput="uniform_average")
    assert_almost_equal(score, raw_expected_score.mean())
    # constant `y_true` with force_finite=True leads to 1. or 0.
    yc = [5.0, 5.0]
    error = r2_score(yc, [5.0, 5.0], multioutput="variance_weighted")
    assert_almost_equal(error, 1.0)
    error = r2_score(yc, [5.0, 5.1], multioutput="variance_weighted")
    assert_almost_equal(error, 0.0)

    # Setting force_finite=False results in the nan for 4th output propagating
    error = r2_score(
        y_true, y_pred, multioutput="variance_weighted", force_finite=False
    )
    assert_almost_equal(error, np.nan)
    error = r2_score(y_true, y_pred, multioutput="uniform_average", force_finite=False)

    # Dropping the 4th output to check `force_finite=False` for nominal
    y_true = y_true[:, :-1]
    y_pred = y_pred[:, :-1]
    error2 = r2_score(
    )
    assert_almost_equal(error, error2)
    error2 = r2_score(y_true, y_pred, multioutput="uniform_average", force_finite=False)

    # constant `y_true` with force_finite=False leads to NaN or -Inf.
        yc, [5.0, 5.0], multioutput="variance_weighted", force_finite=False
    )
        yc, [5.0, 6.0], multioutput="variance_weighted", force_finite=False
    )
    assert_almost_equal(error, -np.inf)

def test_regression_metrics_at_limits():
    # Single-sample case
    # Note: for r2 and d2_tweedie see also test_regression_single_sample
    assert_almost_equal(mean_squared_error([0.0], [0.0]), 0.0)
    assert_almost_equal(root_mean_squared_error([0.0], [0.0]), 0.0)
    assert_almost_equal(mean_squared_log_error([0.0], [0.0]), 0.0)
    assert_almost_equal(mean_absolute_error([0.0], [0.0]), 0.0)
    assert_almost_equal(mean_pinball_loss([0.0], [0.0]), 0.0)
    assert_almost_equal(mean_absolute_percentage_error([0.0], [0.0]), 0.0)
    assert_almost_equal(median_absolute_error([0.0], [0.0]), 0.0)
    assert_almost_equal(max_error([0.0], [0.0]), 0.0)
    assert_almost_equal(explained_variance_score([0.0], [0.0]), 1.0)

    # Perfect cases
    assert_almost_equal(r2_score([0.0, 1], [0.0, 1]), 1.0)
    assert_almost_equal(d2_pinball_score([0.0, 1], [0.0, 1]), 1.0)

    # Non-finite cases
    # R² and explained variance have a fix by default for non-finite cases
    for s in (r2_score, explained_variance_score):
        assert_almost_equal(s([0, 0], [1, -1]), 0.0)
        assert_almost_equal(s([0, 0], [1, -1], force_finite=False), -np.inf)
        assert_almost_equal(s([1, 1], [1, 1]), 1.0)
        assert_almost_equal(s([1, 1], [1, 1], force_finite=False), np.nan)
    msg = (
        "Mean Squared Logarithmic Error cannot be used when "
        "targets contain values less than or equal to -1."
    )
        mean_squared_log_error([-1.0], [-1.0])
    msg = (
    )
        mean_squared_log_error([1.0, 2.0, 3.0], [1.0, -2.0, 3.0])
    msg = (
    )
        mean_squared_log_error([1.0, -2.0, 3.0], [1.0, 2.0, 3.0])
    msg = (
    )
        root_mean_squared_log_error([1.0, -2.0, 3.0], [1.0, 2.0, 3.0])
    msg = (
        "Root Mean Squared Logarithmic Error cannot be used when "
    )

    # Tweedie deviance error
    power = -1.2
        mean_tweedie_deviance([0], [1.0], power=power), 2 / (2 - power), rtol=1e-3
    )
    msg = "can only be used on strictly positive y_pred."
        mean_tweedie_deviance([0.0], [0.0], power=power)
        d2_tweedie_score([0.0] * 2, [0.0] * 2, power=power)

    assert_almost_equal(mean_tweedie_deviance([0.0], [0.0], power=0), 0.0, 2)

    power = 1.0
    msg = "only be used on non-negative y and strictly positive y_pred."

    power = 1.5
    assert_allclose(mean_tweedie_deviance([0.0], [1.0], power=power), 2 / (2 - power))

    power = 2.0
    assert_allclose(mean_tweedie_deviance([1.0], [1.0], power=power), 0.00, atol=1e-8)
    msg = "can only be used on strictly positive y and y_pred."

    power = 3.0

def test__check_reg_targets():
    # All of length 3
    EXAMPLES = [
        ("continuous", [1, 2, 3], 1),
        ("continuous", [[1], [2], [3]], 1),
        ("continuous-multioutput", [[1, 1], [2, 2], [3, 1]], 2),
        ("continuous-multioutput", [[5, 1], [4, 2], [3, 1]], 2),
        ("continuous-multioutput", [[1, 3, 4], [2, 2, 2], [3, 1, 1]], 3),
    ]

    for (type1, y1, n_out1), (type2, y2, n_out2) in product(EXAMPLES, repeat=2):
        if type1 == type2 and n_out1 == n_out2:
            y_type, y_check1, y_check2, multioutput = _check_reg_targets(y1, y2, None)
            assert type1 == y_type
            if type1 == "continuous":
                assert_array_equal(y_check1, np.reshape(y1, (-1, 1)))
                assert_array_equal(y_check2, np.reshape(y2, (-1, 1)))
            else:
                assert_array_equal(y_check1, y1)
                assert_array_equal(y_check2, y2)
        else:
                _check_reg_targets(y1, y2, None)

def test__check_reg_targets_exception():
    invalid_multioutput = "this_value_is_not_valid"
        "Allowed 'multioutput' string values are.+You provided multioutput={!r}".format(
            invalid_multioutput
        )
    )
        _check_reg_targets([1, 2, 3], [[1], [2], [3]], invalid_multioutput)

def test_regression_multioutput_array():
    y_true = [[1, 2], [2.5, -1], [4.5, 3], [5, 7]]
    y_pred = [[1, 1], [2, -1], [5, 4], [5, 6.5]]

    mse = mean_squared_error(y_true, y_pred, multioutput="raw_values")
    mae = mean_absolute_error(y_true, y_pred, multioutput="raw_values")

    pbl = mean_pinball_loss(y_true, y_pred, multioutput="raw_values")
    mape = mean_absolute_percentage_error(y_true, y_pred, multioutput="raw_values")
    r = r2_score(y_true, y_pred, multioutput="raw_values")
    evs = explained_variance_score(y_true, y_pred, multioutput="raw_values")
    d2ps = d2_pinball_score(y_true, y_pred, alpha=0.5, multioutput="raw_values")
    evs2 = explained_variance_score(
        y_true, y_pred, multioutput="raw_values", force_finite=False
    )

    assert_array_almost_equal(mse, [0.125, 0.5625], decimal=2)
    assert_array_almost_equal(mae, [0.25, 0.625], decimal=2)
    assert_array_almost_equal(pbl, [0.25 / 2, 0.625 / 2], decimal=2)
    assert_array_almost_equal(mape, [0.0778, 0.2262], decimal=2)
    assert_array_almost_equal(r, [0.95, 0.93], decimal=2)
    assert_array_almost_equal(evs, [0.95, 0.93], decimal=2)
    assert_array_almost_equal(d2ps, [0.833, 0.722], decimal=2)
    assert_array_almost_equal(evs2, [0.95, 0.93], decimal=2)

    y_true = [[0, 0]] * 4
    y_pred = [[1, 1]] * 4
    d2ps = d2_pinball_score(y_true, y_pred, multioutput="raw_values")
    assert_array_almost_equal(mse, [1.0, 1.0], decimal=2)
    assert_array_almost_equal(mae, [1.0, 1.0], decimal=2)
    assert_array_almost_equal(pbl, [0.5, 0.5], decimal=2)
    assert_array_almost_equal(r, [0.0, 0.0], decimal=2)
    assert_array_almost_equal(d2ps, [0.0, 0.0], decimal=2)

    r = r2_score([[0, -1], [0, 1]], [[2, 2], [1, 1]], multioutput="raw_values")
    assert_array_almost_equal(r, [0, -3.5], decimal=2)
    assert np.mean(r) == r2_score(
        [[0, -1], [0, 1]], [[2, 2], [1, 1]], multioutput="uniform_average"
    )
    evs = explained_variance_score(
        [[0, -1], [0, 1]], [[2, 2], [1, 1]], multioutput="raw_values"
    )
    assert_array_almost_equal(evs, [0, -1.25], decimal=2)
        [[0, -1], [0, 1]],
        [[2, 2], [1, 1]],
        force_finite=False,
    )
    assert_array_almost_equal(evs2, [-np.inf, -1.25], decimal=2)

    # Checking for the condition in which both numerator and denominator is
    # zero.
    y_true = [[1, 3], [1, 2]]
    y_pred = [[1, 4], [1, 1]]
    r2 = r2_score(y_true, y_pred, multioutput="raw_values")
    assert_array_almost_equal(r2, [1.0, -3.0], decimal=2)
    assert np.mean(r2) == r2_score(y_true, y_pred, multioutput="uniform_average")
    r22 = r2_score(y_true, y_pred, multioutput="raw_values", force_finite=False)
    assert_array_almost_equal(r22, [np.nan, -3.0], decimal=2)
        np.mean(r22),
        r2_score(y_true, y_pred, multioutput="uniform_average", force_finite=False),
    )

    assert_array_almost_equal(evs, [1.0, -3.0], decimal=2)
    assert np.mean(evs) == explained_variance_score(y_true, y_pred)
    assert_array_almost_equal(d2ps, [1.0, -1.0], decimal=2)
    )
    assert_array_almost_equal(evs2, [np.nan, -3.0], decimal=2)
        np.mean(evs2), explained_variance_score(y_true, y_pred, force_finite=False)
    )

    # Handling msle separately as it does not accept negative inputs.
    y_true = np.array([[0.5, 1], [1, 2], [7, 6]])
    y_pred = np.array([[0.5, 2], [1, 2.5], [8, 8]])
    msle = mean_squared_log_error(y_true, y_pred, multioutput="raw_values")
    msle2 = mean_squared_error(
        np.log(1 + y_true), np.log(1 + y_pred), multioutput="raw_values"
    )
    assert_array_almost_equal(msle, msle2, decimal=2)

def test_regression_custom_weights():

    msew = mean_squared_error(y_true, y_pred, multioutput=[0.4, 0.6])
    rmsew = root_mean_squared_error(y_true, y_pred, multioutput=[0.4, 0.6])
    maew = mean_absolute_error(y_true, y_pred, multioutput=[0.4, 0.6])
    mapew = mean_absolute_percentage_error(y_true, y_pred, multioutput=[0.4, 0.6])
    rw = r2_score(y_true, y_pred, multioutput=[0.4, 0.6])
    evsw = explained_variance_score(y_true, y_pred, multioutput=[0.4, 0.6])
    d2psw = d2_pinball_score(y_true, y_pred, alpha=0.5, multioutput=[0.4, 0.6])
    evsw2 = explained_variance_score(
        y_true, y_pred, multioutput=[0.4, 0.6], force_finite=False
    )

    assert_almost_equal(msew, 0.39, decimal=2)
    assert_almost_equal(rmsew, 0.59, decimal=2)
    assert_almost_equal(maew, 0.475, decimal=3)
    assert_almost_equal(mapew, 0.1668, decimal=2)
    assert_almost_equal(rw, 0.94, decimal=2)
    assert_almost_equal(evsw, 0.94, decimal=2)
    assert_almost_equal(d2psw, 0.766, decimal=2)
    assert_almost_equal(evsw2, 0.94, decimal=2)

    msle = mean_squared_log_error(y_true, y_pred, multioutput=[0.3, 0.7])
        np.log(1 + y_true), np.log(1 + y_pred), multioutput=[0.3, 0.7]
    )
    assert_almost_equal(msle, msle2, decimal=2)

def test_regression_single_sample(metric):
    y_true = [0]
    y_pred = [1]
    warning_msg = "not well-defined with less than two samples."

    # Trigger the warning
    with pytest.warns(UndefinedMetricWarning, match=warning_msg):
        score = metric(y_true, y_pred)
        assert np.isnan(score)

def test_tweedie_deviance_continuity(global_random_seed):

    y_true = rng.rand(n_samples) + 0.1
    y_pred = rng.rand(n_samples) + 0.1

        mean_tweedie_deviance(y_true, y_pred, power=0 - 1e-10),
    )

    # Ws we get closer to the limit, with 1e-12 difference the
    # tolerance to pass the below check increases. There are likely
    # numerical precision issues on the edges of different definition
    # regions.
        mean_tweedie_deviance(y_true, y_pred, power=1 + 1e-10),
        mean_tweedie_deviance(y_true, y_pred, power=1),
        rtol=1e-5,
    )

        mean_tweedie_deviance(y_true, y_pred, power=2 - 1e-10),
        mean_tweedie_deviance(y_true, y_pred, power=2),
    )

        mean_tweedie_deviance(y_true, y_pred, power=2 + 1e-10),
    )

def test_mean_absolute_percentage_error(global_random_seed):
    random_number_generator = np.random.RandomState(global_random_seed)
    y_true = random_number_generator.exponential(size=100)
    y_pred = 1.2 * y_true
    assert mean_absolute_percentage_error(y_true, y_pred) == pytest.approx(0.2)

def test_mean_pinball_loss_on_constant_predictions(
    distribution, target_quantile, global_random_seed
):
    if not hasattr(np, "quantile"):
            "This test requires a more recent version of numpy "
            "with support for np.quantile."
        )

    # Check that the pinball loss is minimized by the empirical quantile.
    n_samples = 3000
    data = getattr(rng, distribution)(size=n_samples)

    # Compute the best possible pinball loss for any constant predictor:
    best_pred = np.quantile(data, target_quantile)
    best_constant_pred = np.full(n_samples, fill_value=best_pred)
    best_pbl = mean_pinball_loss(data, best_constant_pred, alpha=target_quantile)

    # Evaluate the loss on a grid of quantiles
    candidate_predictions = np.quantile(data, np.linspace(0, 1, 100))
    for pred in candidate_predictions:
        # Compute the pinball loss of a constant predictor:
        constant_pred = np.full(n_samples, fill_value=pred)
        pbl = mean_pinball_loss(data, constant_pred, alpha=target_quantile)

        # Check that the loss of this constant predictor is greater or equal
        # than the loss of using the optimal quantile (up to machine
        # precision):
        assert pbl >= best_pbl - np.finfo(np.float64).eps

        # Check that the value of the pinball loss matches the analytical
        # formula.
        expected_pbl = (pred - data[data < pred]).sum() * (1 - target_quantile) + (
            data[data >= pred] - pred
        ).sum() * target_quantile
        expected_pbl /= n_samples
        assert_almost_equal(expected_pbl, pbl)

    # Check that we can actually recover the target_quantile by minimizing the
    # pinball loss w.r.t. the constant prediction quantile.
    def objective_func(x):
        constant_pred = np.full(n_samples, fill_value=x)
        return mean_pinball_loss(data, constant_pred, alpha=target_quantile)

    result = optimize.minimize(objective_func, data.mean())
    assert result.success
    # The minimum is not unique with limited data, hence the large tolerance.
    # For the normal distribution and the 0.5 quantile, the expected result is close to
    # 0, hence the additional use of absolute tolerance.
    assert_allclose(result.x, best_pred, rtol=1e-1, atol=1e-3)
    assert result.fun == pytest.approx(best_pbl)

def test_dummy_quantile_parameter_tuning(global_random_seed):
    # Integration test to check that it is possible to use the pinball loss to
    # tune the hyperparameter of a quantile regressor. This is conceptually
    # similar to the previous test but using the scikit-learn estimator and
    # scoring API instead.
    X = rng.normal(size=(n_samples, 5))  # Ignored
    y = rng.exponential(size=n_samples)

    all_quantiles = [0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95]
    for alpha in all_quantiles:
        neg_mean_pinball_loss = make_scorer(
            mean_pinball_loss,
            greater_is_better=False,
        )
        regressor = DummyRegressor(strategy="quantile", quantile=0.25)
        grid_search = GridSearchCV(
            regressor,
            param_grid=dict(quantile=all_quantiles),
            scoring=neg_mean_pinball_loss,

        assert grid_search.best_params_["quantile"] == pytest.approx(alpha)

def test_pinball_loss_relation_with_mae(global_random_seed):
    # Test that mean_pinball loss with alpha=0.5 if half of mean absolute error
    n = 100
    y_true = rng.normal(size=n)
    y_pred = y_true.copy() + rng.uniform(n)
    assert (
        mean_absolute_error(y_true, y_pred)
        == mean_pinball_loss(y_true, y_pred, alpha=0.5) * 2
    )

    """

    if binary:

    y_pred_proba = clf.fit(X[:half], y[:half]).predict_proba(X[half:])

    if binary:
        y_pred_proba = y_pred_proba[:, 1]

    return y_true, y_pred, y_pred_proba

def test_classification_report_dictionary_output():
    # Test performance report with dictionary output
    iris = datasets.load_iris()
    y_true, y_pred, _ = make_prediction(dataset=iris, binary=False)

    # print classification report with class names
    expected_report = {
        "setosa": {
            "precision": 0.82608695652173914,
            "recall": 0.79166666666666663,
            "f1-score": 0.8085106382978724,
            "support": 24,
        },
        "versicolor": {
            "precision": 0.33333333333333331,
            "recall": 0.096774193548387094,
            "f1-score": 0.15000000000000002,
            "support": 31,
        },
        "virginica": {
            "precision": 0.41860465116279072,
            "recall": 0.90000000000000002,
            "f1-score": 0.57142857142857151,
            "support": 20,
        },
        "macro avg": {
            "f1-score": 0.5099797365754813,
            "precision": 0.5260083136726211,
            "recall": 0.596146953405018,
            "support": 75,
        },
        "accuracy": 0.5333333333333333,
        "weighted avg": {
            "f1-score": 0.47310435663627154,
            "precision": 0.5137535108414785,
            "recall": 0.5333333333333333,
        },
    }

    report = classification_report(
        y_true,
        y_pred,
        labels=np.arange(len(iris.target_names)),
        target_names=iris.target_names,
        output_dict=True,
    )

    # assert the 2 dicts are equal.
    assert report.keys() == expected_report.keys()
    for key in expected_report:
        if key == "accuracy":
            assert isinstance(report[key], float)
            assert report[key] == expected_report[key]
        else:
            assert report[key].keys() == expected_report[key].keys()
            for metric in expected_report[key]:
                assert_almost_equal(expected_report[key][metric], report[key][metric])

    assert isinstance(expected_report["setosa"]["precision"], float)
    assert isinstance(expected_report["macro avg"]["precision"], float)
    assert isinstance(expected_report["setosa"]["support"], int)
    assert isinstance(expected_report["macro avg"]["support"], int)

def test_classification_report_output_dict_empty_input():
    report = classification_report(y_true=[], y_pred=[], output_dict=True)
        "accuracy": 0.0,
            "f1-score": np.nan,
            "precision": np.nan,
            "recall": np.nan,
            "support": 0,
        },
        },
    }
    assert isinstance(report, dict)
        else:

def test_classification_report_zero_division_warning(zero_division):
    y_true, y_pred = ["a", "b", "c"], ["a", "b", "d"]
        classification_report(
            y_true, y_pred, zero_division=zero_division, output_dict=True
        )
        if zero_division == "warn":
            assert len(record) > 1
            for item in record:
                msg = "Use `zero_division` parameter to control this behavior."
                assert msg in str(item.message)
        else:
            assert not record

def test_classification_report_labels_subset_superset(labels, show_micro_avg):
    """Check the behaviour of passing `labels` as a superset or subset of the labels.
    WHen a superset, we expect to show the "accuracy" in the report while it should be
    the micro-averaging if this is a subset.

    https://github.com/scikit-learn/scikit-learn/issues/27927
    """

    y_true, y_pred = [0, 1], [0, 1]

    report = classification_report(y_true, y_pred, labels=labels, output_dict=True)
    if show_micro_avg:
        assert "micro avg" in report
        assert "accuracy" not in report
    else:  # accuracy should be shown
        assert "accuracy" in report
        assert "micro avg" not in report

def test_multilabel_accuracy_score_subset_accuracy():
    # Dense label indicator matrix format
    y1 = np.array([[0, 1, 1], [1, 0, 1]])
    y2 = np.array([[0, 0, 1], [1, 0, 1]])

    assert accuracy_score(y1, y2) == 0.5
    assert accuracy_score(y1, y1) == 1
    assert accuracy_score(y2, y2) == 1
    assert accuracy_score(y2, np.logical_not(y2)) == 0
    assert accuracy_score(y1, np.logical_not(y1)) == 0
    assert accuracy_score(y1, np.zeros(y1.shape)) == 0
    assert accuracy_score(y2, np.zeros(y1.shape)) == 0

def test_precision_recall_f1_score_binary():
    # Test Precision Recall and F1 Score for binary classification task

    # detailed measures for each class
    p, r, f, s = precision_recall_fscore_support(y_true, y_pred, average=None)
    assert_array_almost_equal(p, [0.73, 0.85], 2)
    assert_array_almost_equal(r, [0.88, 0.68], 2)
    assert_array_almost_equal(f, [0.80, 0.76], 2)
    assert_array_equal(s, [25, 25])

    # individual scoring function that can be used for grid search: in the
    # binary class case the score is the value of the measure for the positive
    # class (e.g. label == 1). This is deprecated for average != 'binary'.
    for kwargs in [{}, {"average": "binary"}]:
            warnings.simplefilter("error")

            ps = precision_score(y_true, y_pred, **kwargs)
            assert_array_almost_equal(ps, 0.85, 2)

            rs = recall_score(y_true, y_pred, **kwargs)
            assert_array_almost_equal(rs, 0.68, 2)

            fs = f1_score(y_true, y_pred, **kwargs)
            assert_array_almost_equal(fs, 0.76, 2)

                fbeta_score(y_true, y_pred, beta=2, **kwargs),
                (1 + 2**2) * ps * rs / (2**2 * ps + rs),
                2,
            )

def test_precision_recall_f_binary_single_class():
    # Test precision, recall and F-scores behave with a single positive or
    # negative class
    # Such a case may occur with non-stratified cross-validation
    assert 1.0 == precision_score([1, 1], [1, 1])
    assert 1.0 == recall_score([1, 1], [1, 1])
    assert 1.0 == f1_score([1, 1], [1, 1])
    assert 1.0 == fbeta_score([1, 1], [1, 1], beta=0)

    assert 0.0 == precision_score([-1, -1], [-1, -1])
    assert 0.0 == recall_score([-1, -1], [-1, -1])
    assert 0.0 == f1_score([-1, -1], [-1, -1])
    assert 0.0 == fbeta_score([-1, -1], [-1, -1], beta=float("inf"))
    assert fbeta_score([-1, -1], [-1, -1], beta=float("inf")) == pytest.approx(
        fbeta_score([-1, -1], [-1, -1], beta=1e5)
    )

def test_precision_recall_f_extra_labels():
    # Test handling of explicit additional (not in input) labels to PRF
    y_true = [1, 3, 3, 2]
    y_pred = [1, 1, 3, 2]
    y_true_bin = label_binarize(y_true, classes=np.arange(5))
    y_pred_bin = label_binarize(y_pred, classes=np.arange(5))
    data = [(y_true, y_pred), (y_true_bin, y_pred_bin)]

    for i, (y_true, y_pred) in enumerate(data):
        # No average: zeros in array
        actual = recall_score(y_true, y_pred, labels=[0, 1, 2, 3, 4], average=None)
        assert_array_almost_equal([0.0, 1.0, 1.0, 0.5, 0.0], actual)

        # Macro average is changed
        actual = recall_score(y_true, y_pred, labels=[0, 1, 2, 3, 4], average="macro")
        assert_array_almost_equal(np.mean([0.0, 1.0, 1.0, 0.5, 0.0]), actual)

        # No effect otherwise
        for average in ["micro", "weighted", "samples"]:
            if average == "samples" and i == 0:
                continue
                recall_score(y_true, y_pred, labels=[0, 1, 2, 3, 4], average=average),
                recall_score(y_true, y_pred, labels=None, average=average),
            )

    # Error when introducing invalid label in multilabel case
    # (although it would only affect performance if average='macro'/None)
    for average in [None, "macro", "micro", "samples"]:
            recall_score(y_true_bin, y_pred_bin, labels=np.arange(6), average=average)
            recall_score(
                y_true_bin, y_pred_bin, labels=np.arange(-1, 4), average=average
            )

    # tests non-regression on issue #10307
    y_true = np.array([[0, 1, 1], [1, 0, 0]])
    y_pred = np.array([[1, 1, 1], [1, 0, 1]])
    p, r, f, _ = precision_recall_fscore_support(
        y_true, y_pred, average="samples", labels=[0, 1]
    )
    assert_almost_equal(np.array([p, r, f]), np.array([3 / 4, 1, 5 / 6]))

def test_precision_recall_f_ignored_labels():
    # Test a subset of labels may be requested for PRF
    y_true = [1, 1, 2, 3]
    y_pred = [1, 3, 3, 3]

        recall_13 = partial(recall_score, y_true, y_pred, labels=[1, 3])
        recall_all = partial(recall_score, y_true, y_pred, labels=None)

        assert_array_almost_equal([0.5, 1.0], recall_13(average=None))
        assert_almost_equal((0.5 + 1.0) / 2, recall_13(average="macro"))
        assert_almost_equal((0.5 * 2 + 1.0 * 1) / 3, recall_13(average="weighted"))
        assert_almost_equal(2.0 / 3, recall_13(average="micro"))

        # ensure the above were meaningful tests:
        for average in ["macro", "weighted", "micro"]:
            assert recall_13(average=average) != recall_all(average=average)

def test_average_precision_score_non_binary_class():
    """Test multiclass-multiouptut for `average_precision_score`."""
    y_true = np.array(
        [
            [2, 2, 1],
            [1, 2, 0],
            [0, 1, 2],
            [1, 2, 1],
            [2, 0, 1],
            [1, 2, 1],
        ]
    )
        [
            [0.7, 0.2, 0.1],
            [0.4, 0.3, 0.3],
            [0.1, 0.8, 0.1],
            [0.4, 0.4, 0.2],
            [0.1, 0.2, 0.7],
        ]
    )
    err_msg = "multiclass-multioutput format is not supported"
        average_precision_score(y_true, y_score, pos_label=2)

def test_average_precision_score_duplicate_values(y_true, y_score):
    """
    Duplicate values with precision-recall require a different
    processing than when computing the AUC of a ROC, because the
    precision-recall curve is a decreasing curve
    The following situation corresponds to a perfect
    test statistic, the average_precision_score should be 1.
    """
    assert average_precision_score(y_true, y_score) == 1

def test_average_precision_score_tied_values(y_true, y_score):
    # Here if we go from left to right in y_true, the 0 values are
    # separated from the 1 values, so it appears that we've
    # correctly sorted our classifications. But in fact the first two
    # values have the same score (0.5) and so the first two values
    # could be swapped around, creating an imperfect sorting. This
    # imperfection should come through in the end score, making it less
    # than one.
    assert average_precision_score(y_true, y_score) != 1.0

def test_precision_recall_f_unused_pos_label():
    # Check warning that pos_label unused when set to non-default value
    # but average != 'binary'; even if data is binary.

    msg = (
        r"Note that pos_label \(set to 2\) is "
        r"ignored when average != 'binary' \(got 'macro'\). You "
        r"may use labels=\[pos_label\] to specify a single "
        "positive class."
    )
        precision_recall_fscore_support(
            [1, 2, 1], [1, 2, 2], pos_label=2, average="macro"
        )

def test_confusion_matrix_binary():
    # Test confusion matrix - binary classification case

    def test(y_true, y_pred):
        cm = confusion_matrix(y_true, y_pred)
        assert_array_equal(cm, [[22, 3], [8, 17]])

        tp, fp, fn, tn = cm.flatten()
        num = tp * tn - fp * fn
        den = np.sqrt((tp + fp) * (tp + fn) * (tn + fp) * (tn + fn))

        true_mcc = 0 if den == 0 else num / den
        mcc = matthews_corrcoef(y_true, y_pred)
        assert_array_almost_equal(mcc, true_mcc, decimal=2)
        assert_array_almost_equal(mcc, 0.57, decimal=2)

    test(y_true, y_pred)
    test([str(y) for y in y_true], [str(y) for y in y_pred])

def test_multilabel_confusion_matrix_binary():
    # Test multilabel confusion matrix - binary classification case

        cm = multilabel_confusion_matrix(y_true, y_pred)
        assert_array_equal(cm, [[[17, 8], [3, 22]], [[22, 3], [8, 17]]])

def test_multilabel_confusion_matrix_multiclass():
    # Test multilabel confusion matrix - multi-class case
    y_true, y_pred, _ = make_prediction(binary=False)

    def test(y_true, y_pred, string_type=False):
        # compute confusion matrix with default labels introspection
            cm, [[[47, 4], [5, 19]], [[38, 6], [28, 3]], [[30, 25], [2, 18]]]
        )

        # compute confusion matrix with explicit label ordering
        labels = ["0", "2", "1"] if string_type else [0, 2, 1]
        cm = multilabel_confusion_matrix(y_true, y_pred, labels=labels)
            cm, [[[47, 4], [5, 19]], [[30, 25], [2, 18]], [[38, 6], [28, 3]]]
        )

        # compute confusion matrix with super set of present labels
        labels = ["0", "2", "1", "3"] if string_type else [0, 2, 1, 3]
            cm,
            [
                [[47, 4], [5, 19]],
                [[30, 25], [2, 18]],
                [[38, 6], [28, 3]],
                [[75, 0], [0, 0]],
            ],
        )

    test([str(y) for y in y_true], [str(y) for y in y_pred], string_type=True)

def test_multilabel_confusion_matrix_multilabel(csc_container, csr_container):
    # Test multilabel confusion matrix - multilabel-indicator case

    y_true = np.array([[1, 0, 1], [0, 1, 0], [1, 1, 0]])
    y_pred = np.array([[1, 0, 0], [0, 1, 1], [0, 0, 1]])
    y_true_csr = csr_container(y_true)
    y_pred_csr = csr_container(y_pred)
    y_true_csc = csc_container(y_true)
    y_pred_csc = csc_container(y_pred)

    # cross test different types
    sample_weight = np.array([2, 1, 3])
    real_cm = [[[1, 0], [1, 1]], [[1, 0], [1, 1]], [[0, 2], [1, 0]]]
    trues = [y_true, y_true_csr, y_true_csc]
    preds = [y_pred, y_pred_csr, y_pred_csc]

    for y_true_tmp in trues:
        for y_pred_tmp in preds:
            cm = multilabel_confusion_matrix(y_true_tmp, y_pred_tmp)
            assert_array_equal(cm, real_cm)

    # test support for samplewise
    cm = multilabel_confusion_matrix(y_true, y_pred, samplewise=True)
    assert_array_equal(cm, [[[1, 0], [1, 1]], [[1, 1], [0, 1]], [[0, 1], [2, 0]]])

    # test support for labels
    cm = multilabel_confusion_matrix(y_true, y_pred, labels=[2, 0])
    assert_array_equal(cm, [[[0, 2], [1, 0]], [[1, 0], [1, 1]]])

    # test support for labels with samplewise
    cm = multilabel_confusion_matrix(y_true, y_pred, labels=[2, 0], samplewise=True)
    assert_array_equal(cm, [[[0, 0], [1, 1]], [[1, 1], [0, 0]], [[0, 1], [1, 0]]])

    # test support for sample_weight with sample_wise
    cm = multilabel_confusion_matrix(
        y_true, y_pred, sample_weight=sample_weight, samplewise=True
    )
    assert_array_equal(cm, [[[2, 0], [2, 2]], [[1, 1], [0, 1]], [[0, 3], [6, 0]]])

def test_multilabel_confusion_matrix_errors():

    # Bad sample_weight
    with pytest.raises(ValueError, match="inconsistent numbers of samples"):
        multilabel_confusion_matrix(y_true, y_pred, sample_weight=[1, 2])
    with pytest.raises(ValueError, match="should be a 1d array"):
        multilabel_confusion_matrix(
            y_true, y_pred, sample_weight=[[1, 2, 3], [2, 3, 4], [3, 4, 5]]
        )

    # Bad labels
    err_msg = r"All labels must be in \[0, n labels\)"
        multilabel_confusion_matrix(y_true, y_pred, labels=[-1])
        multilabel_confusion_matrix(y_true, y_pred, labels=[3])

    # Using samplewise outside multilabel
    with pytest.raises(ValueError, match="Samplewise metrics"):
        multilabel_confusion_matrix([0, 1, 2], [1, 2, 0], samplewise=True)

    # Bad y_type
    err_msg = "multiclass-multioutput is not supported"
        multilabel_confusion_matrix([[0, 1, 2], [2, 1, 0]], [[1, 2, 0], [1, 0, 2]])

def test_confusion_matrix_normalize(normalize, cm_dtype, expected_results):
    y_test = [0, 1, 2] * 6
    y_pred = list(chain(*permutations([0, 1, 2])))
    cm = confusion_matrix(y_test, y_pred, normalize=normalize)
    assert_allclose(cm, expected_results)
    assert cm.dtype.kind == cm_dtype

def test_confusion_matrix_normalize_single_class():
    y_test = [0, 0, 0, 0, 1, 1, 1, 1]
    y_pred = [0, 0, 0, 0, 0, 0, 0, 0]

    cm_true = confusion_matrix(y_test, y_pred, normalize="true")
    assert cm_true.sum() == pytest.approx(2.0)

    # additionally check that no warnings are raised due to a division by zero
        cm_pred = confusion_matrix(y_test, y_pred, normalize="pred")

    assert cm_pred.sum() == pytest.approx(1.0)

        confusion_matrix(y_pred, y_test, normalize="true")

def test_confusion_matrix_single_label():
    """Test `confusion_matrix` warns when only one label found."""
    y_test = [0, 0, 0, 0]
    y_pred = [0, 0, 0, 0]

    with pytest.warns(UserWarning, match="A single label was found in"):
        confusion_matrix(y_pred, y_test)

def test_likelihood_ratios_warnings(params, warn_msg):
    # likelihood_ratios must raise warnings when at
    # least one of the ratios is ill-defined.

        # TODO(1.9): remove setting `replace_undefined_by` since this will be set by
        # default
        class_likelihood_ratios(replace_undefined_by=1.0, **params)

def test_likelihood_ratios_errors(params, err_msg):
    # likelihood_ratios must raise error when attempting
    # non-binary classes to avoid Simpson's paradox
        class_likelihood_ratios(**params)

def test_likelihood_ratios():
    # Build confusion matrix with tn=9, fp=8, fn=1, tp=2,
    # sensitivity=2/3, specificity=9/17, prevalence=3/20,
    # LR+=34/24, LR-=17/27
    y_true = np.array([1] * 3 + [0] * 17)
    y_pred = np.array([1] * 2 + [0] * 10 + [1] * 8)

    pos, neg = class_likelihood_ratios(y_true, y_pred, replace_undefined_by=np.nan)
    assert_allclose(pos, 34 / 24)
    assert_allclose(neg, 17 / 27)

    # Build limit case with y_pred = y_true
    pos, neg = class_likelihood_ratios(y_true, y_true, replace_undefined_by=np.nan)
    # TODO(1.9): replace next line with `assert_array_equal(pos, 1.0)`, since
    # `replace_undefined_by` has a new default:
    assert_array_equal(pos, np.nan * 2)
    assert_allclose(neg, np.zeros(2), rtol=1e-12)

    # Ignore last 5 samples to get tn=9, fp=3, fn=1, tp=2,
    # sensitivity=2/3, specificity=9/12, prevalence=3/20,
    # LR+=24/9, LR-=12/27
    sample_weight = np.array([1.0] * 15 + [0.0] * 5)
    pos, neg = class_likelihood_ratios(
        y_true, y_pred, sample_weight=sample_weight, replace_undefined_by=np.nan
    )
    assert_allclose(pos, 24 / 9)
    assert_allclose(neg, 12 / 27)

def test_likelihood_ratios_raise_warning_deprecation(raise_warning):
    """Test that class_likelihood_ratios raises a `FutureWarning` when `raise_warning`
    param is set."""
    y_true = np.array([1, 0])
    y_pred = np.array([1, 0])

    msg = "`raise_warning` was deprecated in version 1.7 and will be removed in 1.9."
        class_likelihood_ratios(y_true, y_pred, raise_warning=raise_warning)

def test_likelihood_ratios_raise_default_deprecation():
    and `replace_undefined_by` are both default."""

    msg = "The default return value of `class_likelihood_ratios` in case of a"
        class_likelihood_ratios(y_true, y_pred)

def test_likelihood_ratios_replace_undefined_by_worst():
    """Test that class_likelihood_ratios returns the worst scores `1.0` for both LR+ and
    LR- when `replace_undefined_by=1` is set."""
    # This data causes fp=0 (0 false positives) in the confusion_matrix and a division
    # by zero that affects the positive_likelihood_ratio:
    y_true = np.array([1, 1, 0])
    y_pred = np.array([1, 0, 0])

    positive_likelihood_ratio, _ = class_likelihood_ratios(
        y_true, y_pred, replace_undefined_by=1
    )
    assert positive_likelihood_ratio == pytest.approx(1.0)

    # This data causes tn=0 (0 true negatives) in the confusion_matrix and a division
    # by zero that affects the negative_likelihood_ratio:
    y_true = np.array([1, 0, 0])
    y_pred = np.array([1, 1, 1])

    _, negative_likelihood_ratio = class_likelihood_ratios(
    )
    assert negative_likelihood_ratio == pytest.approx(1.0)

def test_likelihood_ratios_wrong_dict_replace_undefined_by(replace_undefined_by):
    """Test that class_likelihood_ratios raises a `ValueError` if the input dict for
    `replace_undefined_by` is in the wrong format or contains impossible values."""

    msg = "The dictionary passed as `replace_undefined_by` needs to be in the form"
        class_likelihood_ratios(
            y_true, y_pred, replace_undefined_by=replace_undefined_by
        )

def test_likelihood_ratios_replace_undefined_by_0_fp(replace_undefined_by, expected):
    """Test that the `replace_undefined_by` param returns the right value for the
    positive_likelihood_ratio as defined by the user."""

        y_true, y_pred, replace_undefined_by=replace_undefined_by
    )

    if np.isnan(expected):
        assert np.isnan(positive_likelihood_ratio)
    else:
        assert positive_likelihood_ratio == pytest.approx(expected)

def test_likelihood_ratios_replace_undefined_by_0_tn(replace_undefined_by, expected):
    negative_likelihood_ratio as defined by the user."""

    )

        assert np.isnan(negative_likelihood_ratio)
    else:
        assert negative_likelihood_ratio == pytest.approx(expected)

def test_cohen_kappa():
    # These label vectors reproduce the contingency matrix from Artstein and
    # Poesio (2008), Table 1: np.array([[20, 20], [10, 50]]).
    y1 = np.array([0] * 40 + [1] * 60)
    y2 = np.array([0] * 20 + [1] * 20 + [0] * 10 + [1] * 50)
    kappa = cohen_kappa_score(y1, y2)
    assert_almost_equal(kappa, 0.348, decimal=3)
    assert kappa == cohen_kappa_score(y2, y1)

    # Add spurious labels and ignore them.
    y1 = np.append(y1, [2] * 4)
    y2 = np.append(y2, [2] * 4)
    assert cohen_kappa_score(y1, y2, labels=[0, 1]) == kappa

    assert_almost_equal(cohen_kappa_score(y1, y1), 1.0)

    # Multiclass example: Artstein and Poesio, Table 4.
    y1 = np.array([0] * 46 + [1] * 44 + [2] * 10)
    y2 = np.array([0] * 52 + [1] * 32 + [2] * 16)
    assert_almost_equal(cohen_kappa_score(y1, y2), 0.8013, decimal=4)

    # Weighting example: none, linear, quadratic.
    y2 = np.array([0] * 50 + [1] * 40 + [2] * 10)
    assert_almost_equal(cohen_kappa_score(y1, y2), 0.9315, decimal=4)
    assert_almost_equal(cohen_kappa_score(y1, y2, weights="linear"), 0.9412, decimal=4)
        cohen_kappa_score(y1, y2, weights="quadratic"), 0.9541, decimal=4
    )

def test_cohen_kappa_score_error_wrong_label():
    """Test that correct error is raised when users pass labels that are not in y1."""
    labels = [1, 2]
    y1 = np.array(["a"] * 5 + ["b"] * 5)
    y2 = np.array(["b"] * 10)
        ValueError, match="At least one label in `labels` must be present in `y1`"
    ):
        cohen_kappa_score(y1, y2, labels=labels)

def test_zero_division_nan_no_warning(metric, y_true, y_pred, zero_division):
    """Check the behaviour of `zero_division` when setting to 0, 1 or np.nan.
    No warnings should be raised.
    """
        result = metric(y_true, y_pred, zero_division=zero_division)

    if np.isnan(zero_division):
        assert np.isnan(result)
    else:
        assert result == zero_division

def test_zero_division_nan_warning(metric, y_true, y_pred):
    """Check the behaviour of `zero_division` when setting to "warn".
    A `UndefinedMetricWarning` should be raised.
    """
    with pytest.warns(UndefinedMetricWarning):
        result = metric(y_true, y_pred, zero_division="warn")
    assert result == 0.0

def test_matthews_corrcoef_against_numpy_corrcoef(global_random_seed):
    y_true = rng.randint(0, 2, size=20)
    y_pred = rng.randint(0, 2, size=20)

        matthews_corrcoef(y_true, y_pred), np.corrcoef(y_true, y_pred)[0, 1], 10
    )

def test_matthews_corrcoef_against_jurman(global_random_seed):
    # Check that the multiclass matthews_corrcoef agrees with the definition
    # presented in Jurman, Riccadonna, Furlanello, (2012). A Comparison of MCC
    # and CEN Error Measures in MultiClass Prediction
    sample_weight = rng.rand(20)

    N = len(C)
    cov_ytyp = sum(
        [
            C[k, k] * C[m, l] - C[l, k] * C[k, m]
            for k in range(N)
            for m in range(N)
            for l in range(N)
        ]
    )
    cov_ytyt = sum(
        [
            C[:, k].sum()
            * np.sum([C[g, f] for f in range(N) for g in range(N) if f != k])
        ]
    )
    cov_ypyp = np.sum(
        [
            C[k, :].sum()
            * np.sum([C[f, g] for f in range(N) for g in range(N) if f != k])
        ]
    )
    mcc_jurman = cov_ytyp / np.sqrt(cov_ytyt * cov_ypyp)
    mcc_ours = matthews_corrcoef(y_true, y_pred, sample_weight=sample_weight)

    assert_almost_equal(mcc_ours, mcc_jurman, 10)

def test_matthews_corrcoef(global_random_seed):
    y_true = ["a" if i == 0 else "b" for i in rng.randint(0, 2, size=20)]

    # corrcoef of same vectors must be 1
    assert_almost_equal(matthews_corrcoef(y_true, y_true), 1.0)

    # corrcoef, when the two vectors are opposites of each other, should be -1
    y_true_inv = ["b" if i == "a" else "a" for i in y_true]
    assert_almost_equal(matthews_corrcoef(y_true, y_true_inv), -1)

    y_true_inv2 = label_binarize(y_true, classes=["a", "b"])
    y_true_inv2 = np.where(y_true_inv2, "a", "b")
    assert_almost_equal(matthews_corrcoef(y_true, y_true_inv2), -1)

    # For the zero vector case, the corrcoef cannot be calculated and should
    # output 0
    assert_almost_equal(matthews_corrcoef([0, 0, 0, 0], [0, 0, 0, 0]), 0.0)

    # And also for any other vector with 0 variance
    assert_almost_equal(matthews_corrcoef(y_true, ["a"] * len(y_true)), 0.0)

    # These two vectors have 0 correlation and hence mcc should be 0
    y_1 = [1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1]
    y_2 = [1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1]
    assert_almost_equal(matthews_corrcoef(y_1, y_2), 0.0)

    # Check that sample weight is able to selectively exclude
    mask = [1] * 10 + [0] * 10
    # Now the first half of the vector elements are alone given a weight of 1
    # and hence the mcc will not be a perfect 0 as in the previous case
        assert_almost_equal(matthews_corrcoef(y_1, y_2, sample_weight=mask), 0.0)

def test_matthews_corrcoef_multiclass(global_random_seed):
    ord_a = ord("a")
    y_true = [chr(ord_a + i) for i in rng.randint(0, n_classes, size=20)]

    # with multiclass > 2 it is not possible to achieve -1
    y_true = [0, 0, 1, 1, 2, 2]
    y_pred_bad = [2, 2, 0, 0, 1, 1]
    assert_almost_equal(matthews_corrcoef(y_true, y_pred_bad), -0.5)

    # Maximizing false positives and negatives minimizes the MCC
    # The minimum will be different for depending on the input
    y_pred_min = [1, 1, 0, 0, 0, 0]
    assert_almost_equal(matthews_corrcoef(y_true, y_pred_min), -12 / np.sqrt(24 * 16))

    # Zero variance will result in an mcc of zero
    y_true = [0, 1, 2]
    y_pred = [3, 3, 3]
    assert_almost_equal(matthews_corrcoef(y_true, y_pred), 0.0)

    # Also for ground truth with zero variance
    y_true = [3, 3, 3]
    y_pred = [0, 1, 2]

    y_1 = [0, 1, 2, 0, 1, 2, 0, 1, 2]
    y_2 = [1, 1, 1, 2, 2, 2, 0, 0, 0]

    # We can test that binary assumptions hold using the multiclass computation
    # by masking the weight of samples not in the first two classes

    # Masking the last label should let us get an MCC of -1
    y_true = [0, 0, 1, 1, 2]
    y_pred = [1, 1, 0, 0, 2]
    sample_weight = [1, 1, 1, 1, 0]
        matthews_corrcoef(y_true, y_pred, sample_weight=sample_weight), -1
    )

    # output 0
    y_true = [0, 0, 1, 2]
    y_pred = [0, 0, 1, 2]
    sample_weight = [1, 1, 0, 0]
        matthews_corrcoef(y_true, y_pred, sample_weight=sample_weight), 0.0
    )

def test_matthews_corrcoef_overflow(n_points, global_random_seed):
    # https://github.com/scikit-learn/scikit-learn/issues/9622

    def mcc_safe(y_true, y_pred):
        conf_matrix = confusion_matrix(y_true, y_pred)
        true_pos = conf_matrix[1, 1]
        false_pos = conf_matrix[1, 0]
        false_neg = conf_matrix[0, 1]
        n_points = len(y_true)
        pos_rate = (true_pos + false_neg) / n_points
        activity = (true_pos + false_pos) / n_points
        mcc_numerator = true_pos / n_points - pos_rate * activity
        mcc_denominator = activity * pos_rate * (1 - activity) * (1 - pos_rate)
        return mcc_numerator / np.sqrt(mcc_denominator)

    def random_ys(n_points):  # binary
        x_true = rng.random_sample(n_points)
        x_pred = x_true + 0.2 * (rng.random_sample(n_points) - 0.5)
        y_true = x_true > 0.5
        y_pred = x_pred > 0.5
        return y_true, y_pred

    arr = np.repeat([0.0, 1.0], n_points)  # binary
    assert_almost_equal(matthews_corrcoef(arr, arr), 1.0)
    arr = np.repeat([0.0, 1.0, 2.0], n_points)  # multiclass

    y_true, y_pred = random_ys(n_points)
    assert_almost_equal(matthews_corrcoef(y_true, y_pred), mcc_safe(y_true, y_pred))

def test_precision_recall_f1_score_multiclass():
    # Test Precision Recall and F1 Score for multiclass classification task

    # compute scores with default labels introspection
    assert_array_almost_equal(p, [0.83, 0.33, 0.42], 2)
    assert_array_almost_equal(r, [0.79, 0.09, 0.90], 2)
    assert_array_almost_equal(f, [0.81, 0.15, 0.57], 2)
    assert_array_equal(s, [24, 31, 20])

    # averaging tests
    ps = precision_score(y_true, y_pred, pos_label=1, average="micro")
    assert_array_almost_equal(ps, 0.53, 2)

    rs = recall_score(y_true, y_pred, average="micro")
    assert_array_almost_equal(rs, 0.53, 2)

    fs = f1_score(y_true, y_pred, average="micro")
    assert_array_almost_equal(fs, 0.53, 2)

    ps = precision_score(y_true, y_pred, average="macro")

    rs = recall_score(y_true, y_pred, average="macro")
    assert_array_almost_equal(rs, 0.60, 2)

    fs = f1_score(y_true, y_pred, average="macro")
    assert_array_almost_equal(fs, 0.51, 2)

    ps = precision_score(y_true, y_pred, average="weighted")
    assert_array_almost_equal(ps, 0.51, 2)

    rs = recall_score(y_true, y_pred, average="weighted")

    fs = f1_score(y_true, y_pred, average="weighted")
    assert_array_almost_equal(fs, 0.47, 2)

        precision_score(y_true, y_pred, average="samples")
        recall_score(y_true, y_pred, average="samples")
        f1_score(y_true, y_pred, average="samples")
        fbeta_score(y_true, y_pred, average="samples", beta=0.5)

    # same prediction but with and explicit label ordering
    p, r, f, s = precision_recall_fscore_support(
        y_true, y_pred, labels=[0, 2, 1], average=None
    )
    assert_array_almost_equal(p, [0.83, 0.41, 0.33], 2)
    assert_array_almost_equal(r, [0.79, 0.90, 0.10], 2)
    assert_array_almost_equal(f, [0.81, 0.57, 0.15], 2)
    assert_array_equal(s, [24, 20, 31])

def test_precision_refcall_f1_score_multilabel_unordered_labels(average):
    # test that labels need not be sorted in the multilabel case
    y_true = np.array([[1, 1, 0, 0]])
    y_pred = np.array([[0, 0, 1, 1]])
        y_true, y_pred, labels=[3, 0, 1, 2], warn_for=[], average=average
    )
    assert_array_equal(p, 0)
    assert_array_equal(r, 0)
    assert_array_equal(f, 0)
        assert_array_equal(s, [0, 1, 1, 0])

def test_precision_recall_f1_score_binary_averaged():
    y_true = np.array([0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1])
    y_pred = np.array([1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1])

    ps, rs, fs, _ = precision_recall_fscore_support(y_true, y_pred, average=None)
    p, r, f, _ = precision_recall_fscore_support(y_true, y_pred, average="macro")
    assert p == np.mean(ps)
    assert r == np.mean(rs)
    assert f == np.mean(fs)
    p, r, f, _ = precision_recall_fscore_support(y_true, y_pred, average="weighted")
    support = np.bincount(y_true)
    assert p == np.average(ps, weights=support)
    assert r == np.average(rs, weights=support)
    assert f == np.average(fs, weights=support)

def test_zero_precision_recall():
    # Check that pathological cases do not bring NaNs

    old_error_settings = np.seterr(all="raise")

    try:
        y_true = np.array([0, 1, 2, 0, 1, 2])
        y_pred = np.array([2, 0, 1, 1, 2, 0])

        assert_almost_equal(precision_score(y_true, y_pred, average="macro"), 0.0, 2)
        assert_almost_equal(recall_score(y_true, y_pred, average="macro"), 0.0, 2)
        assert_almost_equal(f1_score(y_true, y_pred, average="macro"), 0.0, 2)

    finally:
        np.seterr(**old_error_settings)

def test_confusion_matrix_multiclass_subset_labels():
    # Test confusion matrix - multi-class case with subset of labels

    # compute confusion matrix with only first two labels considered
    cm = confusion_matrix(y_true, y_pred, labels=[0, 1])
    assert_array_equal(cm, [[19, 4], [4, 3]])

    # compute confusion matrix with explicit label ordering for only subset
    # of labels
    cm = confusion_matrix(y_true, y_pred, labels=[2, 1])
    assert_array_equal(cm, [[18, 2], [24, 3]])

    # a label not in y_true should result in zeros for that row/column
    extra_label = np.max(y_true) + 1
    cm = confusion_matrix(y_true, y_pred, labels=[2, extra_label])
    assert_array_equal(cm, [[18, 0], [0, 0]])

def test_confusion_matrix_error(labels, err_msg):
        confusion_matrix(y_true, y_pred, labels=labels)

def test_confusion_matrix_on_zero_length_input(labels):
    expected_n_classes = len(labels) if labels else 0
    expected = np.zeros((expected_n_classes, expected_n_classes), dtype=int)
    cm = confusion_matrix([], [], labels=labels)
    assert_array_equal(cm, expected)

def test_confusion_matrix_dtype():
    y = [0, 1, 1]
    weight = np.ones(len(y))
    # confusion_matrix returns int64 by default
    cm = confusion_matrix(y, y)
    assert cm.dtype == np.int64
    # The dtype of confusion_matrix is always 64 bit
    for dtype in [np.bool_, np.int32, np.uint64]:
        cm = confusion_matrix(y, y, sample_weight=weight.astype(dtype, copy=False))
        assert cm.dtype == np.int64
    for dtype in [np.float32, np.float64, None, object]:
        assert cm.dtype == np.float64

    # np.iinfo(np.uint32).max should be accumulated correctly
    weight = np.full(len(y), 4294967295, dtype=np.uint32)
    cm = confusion_matrix(y, y, sample_weight=weight)
    assert cm[0, 0] == 4294967295
    assert cm[1, 1] == 8589934590

    # np.iinfo(np.int64).max should cause an overflow
    weight = np.full(len(y), 9223372036854775807, dtype=np.int64)
    assert cm[0, 0] == 9223372036854775807
    assert cm[1, 1] == -2

def test_confusion_matrix_pandas_nullable(dtype):
    """Checks that confusion_matrix works with pandas nullable dtypes.

    Non-regression test for gh-25635.
    """

    y_ndarray = np.array([1, 0, 0, 1, 0, 1, 1, 0, 1])
    y_true = pd.Series(y_ndarray, dtype=dtype)
    y_predicted = pd.Series([0, 0, 1, 1, 0, 1, 1, 1, 1], dtype="int64")

    output = confusion_matrix(y_true, y_predicted)
    expected_output = confusion_matrix(y_ndarray, y_predicted)

    assert_array_equal(output, expected_output)

def test_classification_report_multiclass():
    # Test performance report

    expected_report = """\
              precision    recall  f1-score   support

      setosa       0.83      0.79      0.81        24
  versicolor       0.33      0.10      0.15        31
   virginica       0.42      0.90      0.57        20

    accuracy                           0.53        75
   macro avg       0.53      0.60      0.51        75
weighted avg       0.51      0.53      0.47        75
"""
        y_true,
        y_pred,
    )
    assert report == expected_report

def test_classification_report_multiclass_balanced():
    y_true, y_pred = [0, 0, 0, 1, 1, 1, 2, 2, 2], [0, 1, 2, 0, 1, 2, 0, 1, 2]

           0       0.33      0.33      0.33         3
           1       0.33      0.33      0.33         3
           2       0.33      0.33      0.33         3

    accuracy                           0.33         9
   macro avg       0.33      0.33      0.33         9
weighted avg       0.33      0.33      0.33         9
"""
    report = classification_report(y_true, y_pred)

def test_classification_report_multiclass_with_label_detection():

    # print classification report with label detection

           0       0.83      0.79      0.81        24
           1       0.33      0.10      0.15        31
           2       0.42      0.90      0.57        20

"""

def test_classification_report_multiclass_with_digits():
    # Test performance report with added digits in floating point values

      setosa    0.82609   0.79167   0.80851        24
  versicolor    0.33333   0.09677   0.15000        31
   virginica    0.41860   0.90000   0.57143        20

    accuracy                        0.53333        75
   macro avg    0.52601   0.59615   0.50998        75
weighted avg    0.51375   0.53333   0.47310        75
"""
        y_true,
        y_pred,
        digits=5,
    )

def test_classification_report_multiclass_with_string_label():

    y_true = np.array(["blue", "green", "red"])[y_true]
    y_pred = np.array(["blue", "green", "red"])[y_pred]

        blue       0.83      0.79      0.81        24
       green       0.33      0.10      0.15        31
         red       0.42      0.90      0.57        20

"""

           a       0.83      0.79      0.81        24
           b       0.33      0.10      0.15        31
           c       0.42      0.90      0.57        20

"""
    report = classification_report(y_true, y_pred, target_names=["a", "b", "c"])

def test_classification_report_multiclass_with_unicode_label():

    labels = np.array(["blue\xa2", "green\xa2", "red\xa2"])
    y_true = labels[y_true]
    y_pred = labels[y_pred]

       blue\xa2       0.83      0.79      0.81        24
      green\xa2       0.33      0.10      0.15        31
        red\xa2       0.42      0.90      0.57        20

"""

def test_classification_report_multiclass_with_long_string_label():

    labels = np.array(["blue", "green" * 5, "red"])

                           precision    recall  f1-score   support

                     blue       0.83      0.79      0.81        24
greengreengreengreengreen       0.33      0.10      0.15        31
                      red       0.42      0.90      0.57        20

                 accuracy                           0.53        75
                macro avg       0.53      0.60      0.51        75
             weighted avg       0.51      0.53      0.47        75
"""

def test_classification_report_labels_target_names_unequal_length():
    y_true = [0, 0, 2, 0, 0]
    y_pred = [0, 2, 2, 0, 0]
    target_names = ["class 0", "class 1", "class 2"]

    msg = "labels size, 2, does not match size of target_names, 3"
        classification_report(y_true, y_pred, labels=[0, 2], target_names=target_names)

def test_classification_report_no_labels_target_names_unequal_length():

    err_msg = (
        "Number of classes, 2, does not "
        "match size of target_names, 3. "
        "Try specifying the labels parameter"
    )
        classification_report(y_true, y_pred, target_names=target_names)

def test_multilabel_classification_report():

        n_features=1, n_samples=n_samples, n_classes=n_classes, random_state=0
    )

        n_features=1, n_samples=n_samples, n_classes=n_classes, random_state=1
    )

           0       0.50      0.67      0.57        24
           1       0.51      0.74      0.61        27
           2       0.29      0.08      0.12        26
           3       0.52      0.56      0.54        27

   micro avg       0.50      0.51      0.50       104
   macro avg       0.45      0.51      0.46       104
weighted avg       0.45      0.51      0.46       104
 samples avg       0.46      0.42      0.40       104
"""

def test_multilabel_zero_one_loss_subset():

    assert zero_one_loss(y1, y2) == 0.5
    assert zero_one_loss(y1, y1) == 0
    assert zero_one_loss(y2, y2) == 0
    assert zero_one_loss(y2, np.logical_not(y2)) == 1
    assert zero_one_loss(y1, np.logical_not(y1)) == 1
    assert zero_one_loss(y1, np.zeros(y1.shape)) == 1
    assert zero_one_loss(y2, np.zeros(y1.shape)) == 1

def test_multilabel_hamming_loss():
    w = np.array([1, 3])

    assert hamming_loss(y1, y2) == 1 / 6
    assert hamming_loss(y1, y1) == 0
    assert hamming_loss(y2, y2) == 0
    assert hamming_loss(y2, 1 - y2) == 1
    assert hamming_loss(y1, 1 - y1) == 1
    assert hamming_loss(y1, np.zeros(y1.shape)) == 4 / 6
    assert hamming_loss(y2, np.zeros(y1.shape)) == 0.5
    assert hamming_loss(y1, y2, sample_weight=w) == 1.0 / 12
    assert hamming_loss(y1, 1 - y2, sample_weight=w) == 11.0 / 12
    assert hamming_loss(y1, np.zeros_like(y1), sample_weight=w) == 2.0 / 3
    # sp_hamming only works with 1-D arrays
    assert hamming_loss(y1[0], y2[0]) == sp_hamming(y1[0], y2[0])

def test_jaccard_score_validation():
    y_true = np.array([0, 1, 0, 1, 1])
    y_pred = np.array([0, 1, 0, 1, 1])
        jaccard_score(y_true, y_pred, average="binary", pos_label=2)

    msg1 = (
        r"Target is multilabel-indicator but average='binary'. "
        r"Please choose another average setting, one of \[None, "
        r"'micro', 'macro', 'weighted', 'samples'\]."
    )
    with pytest.raises(ValueError, match=msg1):
        jaccard_score(y_true, y_pred, average="binary", pos_label=-1)

    y_true = np.array([0, 1, 1, 0, 2])
    y_pred = np.array([1, 1, 1, 1, 0])
    msg2 = (
        r"Target is multiclass but average='binary'. Please choose "
        r"another average setting, one of \[None, 'micro', 'macro', "
        r"'weighted'\]."
    )
    with pytest.raises(ValueError, match=msg2):
        jaccard_score(y_true, y_pred, average="binary")
    msg3 = "Samplewise metrics are not available outside of multilabel classification."
    with pytest.raises(ValueError, match=msg3):
        jaccard_score(y_true, y_pred, average="samples")

    msg = (
        r"Note that pos_label \(set to 3\) is ignored when "
        r"average != 'binary' \(got 'micro'\). You may use "
        r"labels=\[pos_label\] to specify a single positive "
        "class."
    )
        jaccard_score(y_true, y_pred, average="micro", pos_label=3)

def test_multilabel_jaccard_score(recwarn):

    # size(y1 \inter y2) = [1, 2]
    # size(y1 \union y2) = [2, 2]

    assert jaccard_score(y1, y2, average="samples") == 0.75
    assert jaccard_score(y1, y1, average="samples") == 1
    assert jaccard_score(y2, y2, average="samples") == 1
    assert jaccard_score(y2, np.logical_not(y2), average="samples") == 0
    assert jaccard_score(y1, np.logical_not(y1), average="samples") == 0
    assert jaccard_score(y1, np.zeros(y1.shape), average="samples") == 0
    assert jaccard_score(y2, np.zeros(y1.shape), average="samples") == 0

    # average='macro'
    assert_almost_equal(jaccard_score(y_true, y_pred, average="macro"), 2.0 / 3)
    # average='micro'
    assert_almost_equal(jaccard_score(y_true, y_pred, average="micro"), 3.0 / 5)
    # average='samples'
    assert_almost_equal(jaccard_score(y_true, y_pred, average="samples"), 7.0 / 12)
        jaccard_score(y_true, y_pred, average="samples", labels=[0, 2]), 1.0 / 2
    )
        jaccard_score(y_true, y_pred, average="samples", labels=[1, 2]), 1.0 / 2
    )
    # average=None
        jaccard_score(y_true, y_pred, average=None), np.array([1.0 / 2, 1.0, 1.0 / 2])
    )

    y_true = np.array([[0, 1, 1], [1, 0, 1]])
    assert_almost_equal(jaccard_score(y_true, y_pred, average="macro"), 5.0 / 6)
    # average='weighted'
    assert_almost_equal(jaccard_score(y_true, y_pred, average="weighted"), 7.0 / 8)

    msg2 = "Got 4 > 2"
        jaccard_score(y_true, y_pred, labels=[4], average="macro")
    msg3 = "Got -1 < 0"
        jaccard_score(y_true, y_pred, labels=[-1], average="macro")

    msg = (
        "Jaccard is ill-defined and being set to 0.0 in labels "
        "with no true or predicted samples."
    )

    with pytest.warns(UndefinedMetricWarning, match=msg):
        assert (
            jaccard_score(np.array([[0, 1]]), np.array([[0, 1]]), average="macro")
            == 0.5
        )

    msg = (
        "Jaccard is ill-defined and being set to 0.0 in samples "
        "with no true or predicted labels."
    )

        assert (
            jaccard_score(
                np.array([[0, 0], [1, 1]]),
                average="samples",
            )
            == 0.5
        )

    assert not list(recwarn)

def test_multiclass_jaccard_score(recwarn):
    y_true = ["ant", "ant", "cat", "cat", "ant", "cat", "bird", "bird"]
    y_pred = ["cat", "ant", "cat", "cat", "ant", "bird", "bird", "cat"]
    labels = ["ant", "bird", "cat"]
    lb.fit(labels)
    y_true_bin = lb.transform(y_true)
    y_pred_bin = lb.transform(y_pred)
    multi_jaccard_score = partial(jaccard_score, y_true, y_pred)
    bin_jaccard_score = partial(jaccard_score, y_true_bin, y_pred_bin)
    multi_labels_list = [
        ["ant", "bird"],
        ["ant", "cat"],
        ["cat", "bird"],
        ["ant"],
        ["bird"],
        ["cat"],
        None,
    ]
    bin_labels_list = [[0, 1], [0, 2], [2, 1], [0], [1], [2], None]

    # other than average='samples'/'none-samples', test everything else here
    for average in ("macro", "weighted", "micro", None):
        for m_label, b_label in zip(multi_labels_list, bin_labels_list):
                multi_jaccard_score(average=average, labels=m_label),
                bin_jaccard_score(average=average, labels=b_label),
            )

    y_true = np.array([[0, 0], [0, 0], [0, 0]])
    y_pred = np.array([[0, 0], [0, 0], [0, 0]])
        assert jaccard_score(y_true, y_pred, average="weighted") == 0

def test_average_binary_jaccard_score(recwarn):
    # tp=0, fp=0, fn=1, tn=0
    assert jaccard_score([1], [0], average="binary") == 0.0
    # tp=0, fp=0, fn=0, tn=1
    msg = (
        "Jaccard is ill-defined and being set to 0.0 due to "
        "no true or predicted samples"
    )
        assert jaccard_score([0, 0], [0, 0], average="binary") == 0.0

    # tp=1, fp=0, fn=0, tn=0 (pos_label=0)
    assert jaccard_score([0], [0], pos_label=0, average="binary") == 1.0
    y_true = np.array([1, 0, 1, 1, 0])
    y_pred = np.array([1, 0, 1, 1, 1])
    assert_almost_equal(jaccard_score(y_true, y_pred, average="binary"), 3.0 / 4)
        jaccard_score(y_true, y_pred, average="binary", pos_label=0), 1.0 / 2
    )

def test_jaccard_score_zero_division_warning():
    # check that we raised a warning with default behavior if a zero division
    # happens
    y_true = np.array([[1, 0, 1], [0, 0, 0]])
    y_pred = np.array([[0, 0, 0], [0, 0, 0]])
    msg = (
        "Jaccard is ill-defined and being set to 0.0 in "
        "samples with no true or predicted labels."
        " Use `zero_division` parameter to control this behavior."
    )
        score = jaccard_score(y_true, y_pred, average="samples", zero_division="warn")
        assert score == pytest.approx(0.0)

def test_jaccard_score_zero_division_set_value(zero_division, expected_score):
    # check that we don't issue warning by passing the zero_division parameter
        warnings.simplefilter("error", UndefinedMetricWarning)
        score = jaccard_score(
            y_true, y_pred, average="samples", zero_division=zero_division
        )
    assert score == pytest.approx(expected_score)

def test_precision_recall_f1_score_multilabel_1():
    # Test precision_recall_f1_score on a crafted multilabel example
    # First crafted example

    y_true = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 1]])
    y_pred = np.array([[0, 1, 0, 0], [0, 1, 0, 0], [1, 0, 1, 0]])

    # tp = [0, 1, 1, 0]
    # fn = [1, 0, 0, 1]
    # fp = [1, 1, 0, 0]
    # Check per class

    assert_array_almost_equal(p, [0.0, 0.5, 1.0, 0.0], 2)
    assert_array_almost_equal(r, [0.0, 1.0, 1.0, 0.0], 2)
    assert_array_almost_equal(f, [0.0, 1 / 1.5, 1, 0.0], 2)
    assert_array_almost_equal(s, [1, 1, 1, 1], 2)

    f2 = fbeta_score(y_true, y_pred, beta=2, average=None)
    support = s
    assert_array_almost_equal(f2, [0, 0.83, 1, 0], 2)

    # Check macro
    p, r, f, s = precision_recall_fscore_support(y_true, y_pred, average="macro")
    assert_almost_equal(p, 1.5 / 4)
    assert_almost_equal(r, 0.5)
    assert_almost_equal(f, 2.5 / 1.5 * 0.25)
    assert s is None
        fbeta_score(y_true, y_pred, beta=2, average="macro"), np.mean(f2)
    )

    # Check micro
    p, r, f, s = precision_recall_fscore_support(y_true, y_pred, average="micro")
    assert_almost_equal(p, 0.5)
    assert_almost_equal(f, 0.5)
        fbeta_score(y_true, y_pred, beta=2, average="micro"),
        (1 + 4) * p * r / (4 * p + r),
    )

    # Check weighted
    p, r, f, s = precision_recall_fscore_support(y_true, y_pred, average="weighted")
        fbeta_score(y_true, y_pred, beta=2, average="weighted"),
        np.average(f2, weights=support),
    )
    # Check samples
    # |h(x_i) inter y_i | = [0, 1, 1]
    # |y_i| = [1, 1, 2]
    # |h(x_i)| = [1, 1, 2]
    p, r, f, s = precision_recall_fscore_support(y_true, y_pred, average="samples")
    assert_almost_equal(fbeta_score(y_true, y_pred, beta=2, average="samples"), 0.5)

def test_precision_recall_f1_score_multilabel_2():
    # Test precision_recall_f1_score on a crafted multilabel example 2
    # Second crafted example
    y_true = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0]])
    y_pred = np.array([[0, 0, 0, 1], [0, 0, 0, 1], [1, 1, 0, 0]])

    # tp = [ 0.  1.  0.  0.]
    # fp = [ 1.  0.  0.  2.]
    # fn = [ 1.  1.  1.  0.]

    assert_array_almost_equal(p, [0.0, 1.0, 0.0, 0.0], 2)
    assert_array_almost_equal(r, [0.0, 0.5, 0.0, 0.0], 2)
    assert_array_almost_equal(f, [0.0, 0.66, 0.0, 0.0], 2)
    assert_array_almost_equal(s, [1, 2, 1, 0], 2)

    support = s
    assert_array_almost_equal(f2, [0, 0.55, 0, 0], 2)

    assert_almost_equal(p, 0.25)
    assert_almost_equal(r, 0.25)
    assert_almost_equal(f, 2 * 0.25 * 0.25 / 0.5)
    )

    assert_almost_equal(r, 0.125)
    assert_almost_equal(f, 2 / 12)
    )

    assert_almost_equal(p, 2 / 4)
    assert_almost_equal(r, 1 / 4)
    assert_almost_equal(f, 2 / 3 * 2 / 4)
    )

    # |h(x_i) inter y_i | = [0, 0, 1]

    assert_almost_equal(p, 1 / 6)
    assert_almost_equal(r, 1 / 6)
    assert_almost_equal(f, 2 / 4 * 1 / 3)
        fbeta_score(y_true, y_pred, beta=2, average="samples"), 0.1666, 2
    )

def test_precision_recall_f1_score_with_an_empty_prediction(
    zero_division, zero_division_expected
):
    y_true = np.array([[0, 1, 0, 0], [1, 0, 0, 0], [0, 1, 1, 0]])
    y_pred = np.array([[0, 0, 0, 0], [0, 0, 0, 1], [0, 1, 1, 0]])

    # true_pos = [ 0.  1.  1.  0.]
    # false_pos = [ 0.  0.  0.  1.]
    # false_neg = [ 1.  1.  0.  0.]

        y_true, y_pred, average=None, zero_division=zero_division
    )

    assert_array_almost_equal(p, [zero_division_expected, 1.0, 1.0, 0.0], 2)
    assert_array_almost_equal(r, [0.0, 0.5, 1.0, zero_division_expected], 2)
    expected_f = 0
    assert_array_almost_equal(f, [expected_f, 1 / 1.5, 1, expected_f], 2)

    f2 = fbeta_score(y_true, y_pred, beta=2, average=None, zero_division=zero_division)
    support = s
    assert_array_almost_equal(f2, [expected_f, 0.55, 1, expected_f], 2)

        y_true, y_pred, average="macro", zero_division=zero_division
    )

    value_to_sum = 0 if np.isnan(zero_division_expected) else zero_division_expected
    values_to_average = 3 + (not np.isnan(zero_division_expected))

    assert_almost_equal(p, (2 + value_to_sum) / values_to_average)
    assert_almost_equal(r, (1.5 + value_to_sum) / values_to_average)
    expected_f = (2 / 3 + 1) / 4
    assert_almost_equal(f, expected_f)
        fbeta_score(
            y_true,
            y_pred,
            beta=2,
            average="macro",
        ),
        _nanaverage(f2, weights=None),
    )

        y_true, y_pred, average="micro", zero_division=zero_division
    )
    assert_almost_equal(p, 2 / 3)
    assert_almost_equal(f, 2 / 3 / (2 / 3 + 0.5))
            y_true, y_pred, beta=2, average="micro", zero_division=zero_division
        ),
    )

        y_true, y_pred, average="weighted", zero_division=zero_division
    )
    assert_almost_equal(p, 3 / 4 if zero_division_expected == 0 else 1.0)
    values_to_average = 4
    assert_almost_equal(f, (2 * 2 / 3 + 1) / values_to_average)
            y_true, y_pred, beta=2, average="weighted", zero_division=zero_division
        ),
        _nanaverage(f2, weights=support),
    )

    # |h(x_i) inter y_i | = [0, 0, 2]
    # |h(x_i)| = [0, 1, 2]
    assert_almost_equal(p, 1 / 3)
    assert_almost_equal(r, 1 / 3)
    assert_almost_equal(f, 1 / 3)
    expected_result = 0.333
            y_true, y_pred, beta=2, average="samples", zero_division=zero_division
        ),
        expected_result,
        2,
    )

def test_precision_recall_f1_no_labels(beta, average, zero_division):
    y_pred = np.zeros_like(y_true)

        p, r, f, s = precision_recall_fscore_support(
            y_true,
            y_pred,
            beta=beta,
        )
        fbeta = fbeta_score(
            y_true,
            y_pred,
        )

    # if zero_division = nan, check that all metrics are nan and exit
        for metric in [p, r, f, fbeta]:
            assert np.isnan(metric)
        return

    zero_division = float(zero_division)
    assert_almost_equal(p, zero_division)
    assert_almost_equal(r, zero_division)
    assert_almost_equal(f, zero_division)

    assert_almost_equal(fbeta, float(zero_division))

def test_precision_recall_f1_no_labels_check_warnings(average):

    func = precision_recall_fscore_support
        p, r, f, s = func(y_true, y_pred, average=average, beta=1.0)

    assert_almost_equal(p, 0)
    assert_almost_equal(r, 0)
    assert_almost_equal(f, 0)

        fbeta = fbeta_score(y_true, y_pred, average=average, beta=1.0)

    assert_almost_equal(fbeta, 0)

def test_precision_recall_f1_no_labels_average_none(zero_division):

    # tp = [0, 0, 0]
    # fn = [0, 0, 0]
    # fp = [0, 0, 0]
    # support = [0, 0, 0]
    # |y_hat_i inter y_i | = [0, 0, 0]
    # |y_i| = [0, 0, 0]
    # |y_hat_i| = [0, 0, 0]

            y_true,
            y_pred,
            average=None,
            beta=1.0,
        )
            y_true, y_pred, beta=1.0, average=None, zero_division=zero_division
        )

    zero_division = np.float64(zero_division)
    assert_array_almost_equal(p, [zero_division, zero_division, zero_division], 2)
    assert_array_almost_equal(r, [zero_division, zero_division, zero_division], 2)
    assert_array_almost_equal(f, [zero_division, zero_division, zero_division], 2)
    assert_array_almost_equal(s, [0, 0, 0], 2)

    assert_array_almost_equal(fbeta, [zero_division, zero_division, zero_division], 2)

def test_precision_recall_f1_no_labels_average_none_warn():

            y_true, y_pred, average=None, beta=1
        )

    assert_array_almost_equal(p, [0, 0, 0], 2)
    assert_array_almost_equal(r, [0, 0, 0], 2)
    assert_array_almost_equal(f, [0, 0, 0], 2)

        fbeta = fbeta_score(y_true, y_pred, beta=1, average=None)

    assert_array_almost_equal(fbeta, [0, 0, 0], 2)

def test_prf_warnings():
    # average of per-label scores
    f, w = precision_recall_fscore_support, UndefinedMetricWarning
    for average in [None, "weighted", "macro"]:
        msg = (
            "Precision is ill-defined and "
            "being set to 0.0 in labels with no predicted samples."
            " Use `zero_division` parameter to control"
            " this behavior."
        )
        with pytest.warns(w, match=msg):
            f([0, 1, 2], [1, 1, 2], average=average)

        msg = (
            "Recall is ill-defined and "
            "being set to 0.0 in labels with no true samples."
        )
            f([1, 1, 2], [0, 1, 2], average=average)

    # average of per-sample scores
    msg = (
        "Precision is ill-defined and "
        "being set to 0.0 in samples with no predicted labels."
        " Use `zero_division` parameter to control"
        " this behavior."
    )
    with pytest.warns(w, match=msg):
        f(np.array([[1, 0], [1, 0]]), np.array([[1, 0], [0, 0]]), average="samples")

    msg = (
        "Recall is ill-defined and "
        "being set to 0.0 in samples with no true labels."
    )
        f(np.array([[1, 0], [0, 0]]), np.array([[1, 0], [1, 0]]), average="samples")

    # single score: micro-average
    msg = (
        "being set to 0.0 due to no predicted samples."
    )
        f(np.array([[1, 1], [1, 1]]), np.array([[0, 0], [0, 0]]), average="micro")

    msg = (
        "being set to 0.0 due to no true samples."
    )
        f(np.array([[0, 0], [0, 0]]), np.array([[1, 1], [1, 1]]), average="micro")

    # single positive label
    msg = (
    )
        f([1, 1], [-1, -1], average="binary")

    msg = (
    )
        f([-1, -1], [1, 1], average="binary")

        precision_recall_fscore_support([0, 0], [0, 0], average="binary")
        msg = (
            "F-score is ill-defined and being set to 0.0 due to no true nor "
            "predicted samples. Use `zero_division` parameter to control this"
            " behavior."
        )
        assert str(record.pop().message) == msg
        msg = (
            "being set to 0.0 due to no true samples."
        )
        msg = (
            "being set to 0.0 due to no predicted samples."
        )

def test_prf_no_warnings_if_zero_division_set(zero_division):

        # average of per-label scores
        for average in [None, "weighted", "macro"]:
            precision_recall_fscore_support(
                [0, 1, 2], [1, 1, 2], average=average, zero_division=zero_division
            )

                [1, 1, 2], [0, 1, 2], average=average, zero_division=zero_division
            )

        # average of per-sample scores
            np.array([[1, 0], [1, 0]]),
            np.array([[1, 0], [0, 0]]),
            average="samples",
        )

        )

        # single score: micro-average
            np.array([[1, 1], [1, 1]]),
            np.array([[0, 0], [0, 0]]),
            average="micro",
        )

        )

        # single positive label
            [1, 1], [-1, -1], average="binary", zero_division=zero_division
        )

            [-1, -1], [1, 1], average="binary", zero_division=zero_division
        )

            [0, 0], [0, 0], average="binary", zero_division=zero_division
        )
        assert len(record) == 0

def test_recall_warnings(zero_division):

        recall_score(
        )

        )
            assert (
                str(record.pop().message) == "Recall is ill-defined and "
                "being set to 0.0 due to no true samples."
                " Use `zero_division` parameter to control"
                " this behavior."
            )
        else:
            assert len(record) == 0

        recall_score([0, 0], [0, 0])
            assert (
            )

def test_precision_warnings(zero_division):
        precision_score(
        )
            assert (
                str(record.pop().message) == "Precision is ill-defined and "
                "being set to 0.0 due to no predicted samples."
            )
        else:

        precision_score([0, 0], [0, 0])
            assert (
            )

        )

def test_fscore_warnings(zero_division):

        for score in [f1_score, partial(fbeta_score, beta=2)]:
            score(
                np.array([[1, 1], [1, 1]]),
                np.array([[0, 0], [0, 0]]),
                average="micro",
                zero_division=zero_division,
            )

            score(
            )

            score(
            )
            if zero_division == "warn":
                assert (
                    str(record.pop().message) == "F-score is ill-defined and "
                    "being set to 0.0 due to no true nor predicted "
                    "samples. Use `zero_division` parameter to "
                    "control this behavior."
                )
            else:
                assert len(record) == 0

def test_prf_average_binary_data_non_binary():
    # Error if user does not explicitly set non-binary average mode
    y_true_mc = [1, 2, 3, 3]
    y_pred_mc = [1, 2, 3, 1]
    msg_mc = (
        r"Target is multiclass but average='binary'. Please "
        r"choose another average setting, one of \["
        r"None, 'micro', 'macro', 'weighted'\]."
    )
    y_true_ind = np.array([[0, 1, 1], [1, 0, 0], [0, 0, 1]])
    y_pred_ind = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]])
    msg_ind = (
        r"Target is multilabel-indicator but average='binary'. Please "
        r"None, 'micro', 'macro', 'weighted', 'samples'\]."
    )

    for y_true, y_pred, msg in [
        (y_true_mc, y_pred_mc, msg_mc),
        (y_true_ind, y_pred_ind, msg_ind),
    ]:
        for metric in [
            precision_score,
            recall_score,
            f1_score,
            partial(fbeta_score, beta=2),
        ]:
            with pytest.raises(ValueError, match=msg):
                metric(y_true, y_pred)

def test__check_targets():
    # Check that _check_targets correctly merges target types, squeezes
    # output and fails if input lengths differ.
    IND = "multilabel-indicator"
    MC = "multiclass"
    BIN = "binary"
    CNT = "continuous"
    MMC = "multiclass-multioutput"
    MCN = "continuous-multioutput"
    # all of length 3
        (IND, np.array([[0, 1, 1], [1, 0, 0], [0, 0, 1]])),
        # must not be considered binary
        (IND, np.array([[0, 1], [1, 0], [1, 1]])),
        (MC, [2, 3, 1]),
        (BIN, [0, 1, 1]),
        (CNT, [0.0, 1.5, 1.0]),
        (MC, np.array([[2], [3], [1]])),
        (BIN, np.array([[0], [1], [1]])),
        (CNT, np.array([[0.0], [1.5], [1.0]])),
        (MMC, np.array([[0, 2], [1, 3], [2, 3]])),
        (MCN, np.array([[0.5, 2.0], [1.1, 3.0], [2.0, 3.0]])),
    ]
    # expected type given input types, or None for error
    # (types will be tried in either order)
    EXPECTED = {
        (IND, IND): IND,
        (MC, MC): MC,
        (BIN, BIN): BIN,
        (MC, IND): None,
        (BIN, IND): None,
        (BIN, MC): MC,
        # Disallowed types
        (CNT, CNT): None,
        (MMC, MMC): None,
        (MCN, MCN): None,
        (IND, CNT): None,
        (MC, CNT): None,
        (BIN, CNT): None,
        (MMC, CNT): None,
        (MCN, CNT): None,
        (IND, MMC): None,
        (MC, MMC): None,
        (BIN, MMC): None,
        (MCN, MMC): None,
        (IND, MCN): None,
        (MC, MCN): None,
        (BIN, MCN): None,
    }

    for (type1, y1), (type2, y2) in product(EXAMPLES, repeat=2):
        try:
            expected = EXPECTED[type1, type2]
            expected = EXPECTED[type2, type1]
        if expected is None:
                _check_targets(y1, y2)

            if type1 != type2:
                err_msg = (
                    "Classification metrics can't handle a mix "
                    "of {0} and {1} targets".format(type1, type2)
                )
                with pytest.raises(ValueError, match=err_msg):
                    _check_targets(y1, y2)

            else:
                if type1 not in (BIN, MC, IND):
                    err_msg = "{0} is not supported".format(type1)
                    with pytest.raises(ValueError, match=err_msg):
                        _check_targets(y1, y2)

        else:
            merged_type, y1out, y2out = _check_targets(y1, y2)
            assert merged_type == expected
            if merged_type.startswith("multilabel"):
                assert y1out.format == "csr"
                assert y2out.format == "csr"
            else:
                assert_array_equal(y1out, np.squeeze(y1))
                assert_array_equal(y2out, np.squeeze(y2))
                _check_targets(y1[:-1], y2)

    # Make sure seq of seq is not supported
    y1 = [(1, 2), (0, 2, 3)]
    y2 = [(2,), (0, 2)]
    msg = (
        "You appear to be using a legacy multi-label data representation. "
        "Sequence of sequences are no longer supported; use a binary array"
        " or sparse matrix instead - the MultiLabelBinarizer"
        " transformer can convert to this format."
    )
        _check_targets(y1, y2)

def test__check_targets_multiclass_with_both_y_true_and_y_pred_binary():
    # https://github.com/scikit-learn/scikit-learn/issues/8098
    y_pred = [0, -1]
    assert _check_targets(y_true, y_pred)[0] == "multiclass"

def test_hinge_loss_binary():
    y_true = np.array([-1, 1, 1, -1])
    pred_decision = np.array([-8.5, 0.5, 1.5, -0.3])
    assert hinge_loss(y_true, pred_decision) == 1.2 / 4

    y_true = np.array([0, 2, 2, 0])

def test_hinge_loss_multiclass():
    pred_decision = np.array(
        [
            [+0.36, -0.17, -0.58, -0.99],
            [-0.54, -0.37, -0.48, -0.58],
            [-1.45, -0.58, -0.38, -0.17],
            [-0.54, -0.38, -0.48, -0.58],
            [-2.36, -0.79, -0.27, +0.24],
        ]
    )
    y_true = np.array([0, 1, 2, 1, 3, 2])
    dummy_losses = np.array(
        [
            1 - pred_decision[0][0] + pred_decision[0][1],
            1 - pred_decision[1][1] + pred_decision[1][2],
            1 - pred_decision[2][2] + pred_decision[2][3],
            1 - pred_decision[3][1] + pred_decision[3][2],
            1 - pred_decision[4][3] + pred_decision[4][2],
            1 - pred_decision[5][2] + pred_decision[5][3],
        ]
    )
    np.clip(dummy_losses, 0, None, out=dummy_losses)
    dummy_hinge_loss = np.mean(dummy_losses)
    assert hinge_loss(y_true, pred_decision) == dummy_hinge_loss

def test_hinge_loss_multiclass_missing_labels_with_labels_none():
    y_true = np.array([0, 1, 2, 2])
        [
            [+1.27, 0.034, -0.68, -1.40],
        ]
    )
        "Please include all labels in y_true or pass labels as third argument"
    )
        hinge_loss(y_true, pred_decision)

def test_hinge_loss_multiclass_no_consistent_pred_decision_shape():
    # test for inconsistency between multiclass problem and pred_decision
    # argument
    y_true = np.array([2, 1, 0, 1, 0, 1, 1])
    pred_decision = np.array([0, 1, 2, 1, 0, 2, 1])
        "The shape of pred_decision cannot be 1d array"
        "with a multiclass target. pred_decision shape "
        "must be (n_samples, n_classes), that is "
        "(7, 3). Got: (7,)"
    )
        hinge_loss(y_true=y_true, pred_decision=pred_decision)

    # test for inconsistency between pred_decision shape and labels number
    pred_decision = np.array([[0, 1], [0, 1], [0, 1], [0, 1], [2, 0], [0, 1], [1, 0]])
    labels = [0, 1, 2]
        "The shape of pred_decision is not "
        "consistent with the number of classes. "
        "With a multiclass target, pred_decision "
        "shape must be (n_samples, n_classes), that is "
        "(7, 3). Got: (7, 2)"
    )
        hinge_loss(y_true=y_true, pred_decision=pred_decision, labels=labels)

def test_hinge_loss_multiclass_with_missing_labels():
        [
            [-0.55, -0.38, -0.48, -0.58],
        ]
    )
    y_true = np.array([0, 1, 2, 1, 2])
    labels = np.array([0, 1, 2, 3])
        [
            1 - pred_decision[4][2] + pred_decision[4][3],
        ]
    )
    assert hinge_loss(y_true, pred_decision, labels=labels) == dummy_hinge_loss

def test_hinge_loss_multiclass_missing_labels_only_two_unq_in_y_true():
    # non-regression test for:
    # https://github.com/scikit-learn/scikit-learn/issues/17630
    # check that we can compute the hinge loss when providing an array
    # with labels allowing to not have all labels in y_true
        [
            [+0.36, -0.17, -0.58],
            [-0.15, -0.58, -0.48],
            [-1.45, -0.58, -0.38],
            [-0.55, -0.78, -0.42],
        ]
    )
    y_true = np.array([0, 2, 2, 0, 2])
    labels = np.array([0, 1, 2])
        [
            1 - pred_decision[1][2] + pred_decision[1][0],
            1 - pred_decision[2][2] + pred_decision[2][1],
            1 - pred_decision[3][0] + pred_decision[3][2],
            1 - pred_decision[4][2] + pred_decision[4][1],
        ]
    )
        hinge_loss(y_true, pred_decision, labels=labels), dummy_hinge_loss
    )

def test_hinge_loss_multiclass_invariance_lists():
    # Currently, invariance of string and integer labels cannot be tested
    # in common invariance tests because invariance tests for multiclass
    # decision functions is not implemented yet.
    y_true = ["blue", "green", "red", "green", "white", "red"]
    pred_decision = [
        [+0.36, -0.17, -0.58, -0.99],
        [-0.55, -0.38, -0.48, -0.58],
        [-1.45, -0.58, -0.38, -0.17],
        [-2.36, -0.79, -0.27, +0.24],
    ]
        [
        ]
    )

def test_log_loss():
    # binary case with symbolic labels ("no" < "yes")
    y_true = ["no", "no", "no", "yes", "yes", "yes"]
        [[0.5, 0.5], [0.1, 0.9], [0.01, 0.99], [0.9, 0.1], [0.75, 0.25], [0.001, 0.999]]
    )
    loss = log_loss(y_true, y_pred)
    loss_true = -np.mean(bernoulli.logpmf(np.array(y_true) == "yes", y_pred[:, 1]))
    assert_allclose(loss, loss_true)

    # multiclass case; adapted from http://bit.ly/RJJHWA
    y_true = [1, 0, 2]
    y_pred = [[0.2, 0.7, 0.1], [0.6, 0.2, 0.2], [0.6, 0.1, 0.3]]
    loss = log_loss(y_true, y_pred, normalize=True)
    assert_allclose(loss, 0.6904911)

    # check that we got all the shapes and axes right
    # by doubling the length of y_true and y_pred
    y_true *= 2
    y_pred *= 2
    loss = log_loss(y_true, y_pred, normalize=False)
    assert_allclose(loss, 0.6904911 * 6)

    # raise error if number of classes are not equal.
    y_pred = [[0.3, 0.7], [0.6, 0.4], [0.4, 0.6]]
        log_loss(y_true, y_pred)

    # raise error if labels do not contain all values of y_true
    y_true = ["a", "b", "c"]
    y_pred = [[0.9, 0.1, 0.0], [0.1, 0.9, 0.0], [0.1, 0.1, 0.8]]
    labels = ["a", "c", "d"]
    error_str = (
        "y_true contains values {'b'} not belonging to the passed "
        "labels ['a', 'c', 'd']."
    )
    with pytest.raises(ValueError, match=re.escape(error_str)):
        log_loss(y_true, y_pred, labels=labels)

    # case when y_true is a string array object
    y_true = ["ham", "spam", "spam", "ham"]
    y_pred = [[0.3, 0.7], [0.6, 0.4], [0.4, 0.6], [0.7, 0.3]]
    assert_allclose(loss, 0.7469410)

    # test labels option

    y_true = [2, 2]
    y_pred = [[0.2, 0.8], [0.6, 0.4]]
    y_score = np.array([[0.1, 0.9], [0.1, 0.9]])
        "y_true contains only one label (2). Please provide the list of all "
        "expected class labels explicitly through the labels argument."
    )

    y_pred = [[0.2, 0.8], [0.6, 0.4], [0.7, 0.3]]
    error_str = "Found input variables with inconsistent numbers of samples: [3, 2]"

    # works when the labels argument is used

    true_log_loss = -np.mean(np.log(y_score[:, 1]))
    calculated_log_loss = log_loss(y_true, y_score, labels=[1, 2])
    assert_allclose(calculated_log_loss, true_log_loss)

    # ensure labels work when len(np.unique(y_true)) != y_pred.shape[1]
    y_true = [1, 2, 2]
    y_score2 = [[0.7, 0.1, 0.2], [0.2, 0.7, 0.1], [0.1, 0.7, 0.2]]
    loss = log_loss(y_true, y_score2, labels=[1, 2, 3])
    assert_allclose(loss, -np.log(0.7))

def test_log_loss_eps(dtype):
    """Check the behaviour internal eps that changes depending on the input dtype.

    https://github.com/scikit-learn/scikit-learn/issues/24315
    """
    y_true = np.array([0, 1], dtype=dtype)
    y_pred = np.array([1, 0], dtype=dtype)

def test_log_loss_not_probabilities_warning(dtype):
    """Check that log_loss raises a warning when y_pred values don't sum to 1."""
    y_true = np.array([0, 1, 1, 0])
    y_pred = np.array([[0.2, 0.7], [0.6, 0.3], [0.4, 0.7], [0.8, 0.3]], dtype=dtype)

    with pytest.warns(UserWarning, match="The y_prob values do not sum to one."):

def test_log_loss_perfect_predictions(y_true, y_pred):
    """Check that log_loss returns 0 for perfect predictions."""
    # Because of the clipping, the result is not exactly 0
    assert log_loss(y_true, y_pred) == pytest.approx(0)

def test_log_loss_pandas_input():
    # case when input is a pandas series and dataframe gh-5715
    y_tr = np.array(["ham", "spam", "spam", "ham"])
    y_pr = np.array([[0.3, 0.7], [0.6, 0.4], [0.4, 0.6], [0.7, 0.3]])
    types = [(MockDataFrame, MockDataFrame)]
    try:
        from pandas import DataFrame, Series

        types.append((Series, DataFrame))
        pass
    for TrueInputType, PredInputType in types:
        # y_pred dataframe, y_true series
        y_true, y_pred = TrueInputType(y_tr), PredInputType(y_pr)
        loss = log_loss(y_true, y_pred)
        assert_allclose(loss, 0.7469410)

def test_log_loss_warnings():
    expected_message = re.escape(
        "Labels passed were ['spam', 'eggs', 'ham']. But this function "
        "assumes labels are ordered lexicographically. "
        "Pass the ordered labels=['eggs', 'ham', 'spam'] and ensure that "
        "the columns of y_prob correspond to this ordering."
    )
    with pytest.warns(UserWarning, match=expected_message):
        log_loss(
            ["eggs", "spam", "ham"],
            [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
            labels=["spam", "eggs", "ham"],
        )

def test_brier_score_loss_binary():
    # Check brier_score_loss function
    y_true = np.array([0, 1, 1, 0, 1, 1])
    y_prob = np.array([0.1, 0.8, 0.9, 0.3, 1.0, 0.95])
    true_score = linalg.norm(y_true - y_prob) ** 2 / len(y_true)

    assert_almost_equal(brier_score_loss(y_true, y_true), 0.0)
    assert_almost_equal(brier_score_loss(y_true, y_prob), true_score)
    assert_almost_equal(brier_score_loss(1.0 + y_true, y_prob), true_score)
    assert_almost_equal(brier_score_loss(2 * y_true - 1, y_prob), true_score)

    # check that using (n_samples, 2) y_prob or y_true gives the same score
    y_prob_reshaped = np.column_stack((1 - y_prob, y_prob))
    y_true_reshaped = np.column_stack((1 - y_true, y_true))
    assert_almost_equal(brier_score_loss(y_true, y_prob_reshaped), true_score)
    assert_almost_equal(brier_score_loss(y_true_reshaped, y_prob_reshaped), true_score)

    # check scale_by_half argument
        brier_score_loss(y_true, y_prob, scale_by_half="auto"), true_score
    )
        brier_score_loss(y_true, y_prob, scale_by_half=True), true_score
    )
        brier_score_loss(y_true, y_prob, scale_by_half=False), 2 * true_score
    )

    # calculate correctly when there's only one class in y_true
    assert_almost_equal(brier_score_loss([-1], [0.4]), 0.4**2)
    assert_almost_equal(brier_score_loss([0], [0.4]), 0.4**2)
    assert_almost_equal(brier_score_loss([1], [0.4]), (1 - 0.4) ** 2)
    assert_almost_equal(brier_score_loss(["foo"], [0.4], pos_label="bar"), 0.4**2)
        brier_score_loss(["foo"], [0.4], pos_label="foo"),
        (1 - 0.4) ** 2,
    )

def test_brier_score_loss_multiclass():
    # test cases for multi-class
        brier_score_loss(
            [[1, 0, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]],
            labels=["eggs", "ham", "spam", "yams"],
        ),
        2 / 3,
    )

            [1, 0, 2], [[0.2, 0.7, 0.1], [0.6, 0.2, 0.2], [0.6, 0.1, 0.3]]
        ),
        0.41333333,
    )

    # check perfect predictions for 3 classes
            [0, 1, 2], [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
        ),
        0,
    )

    # check perfectly incorrect predictions for 3 classes
            [0, 1, 2], [[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]]
        ),
        2,
    )

def test_brier_score_loss_invalid_inputs():
        # bad length of y_prob
        brier_score_loss(y_true, y_prob[1:])
        # y_pred has value greater than 1
        brier_score_loss(y_true, y_prob + 1.0)
        # y_pred has value less than 0
        brier_score_loss(y_true, y_prob - 1.0)

    # multiclass case
    y_true = np.array([1, 0, 2])
    y_prob = np.array([[0.2, 0.7, 0.1], [0.6, 0.2, 0.2], [0.6, 0.1, 0.3]])
        # bad length of y_pred

    # raise an error for multiclass y_true and binary y_prob
    y_true = np.array([0, 1, 2, 0])
    y_prob = np.array([0.8, 0.6, 0.4, 0.2])
    error_message = re.escape(
        "The type of the target inferred from y_true is multiclass "
        "but should be binary according to the shape of y_prob."
    )
        brier_score_loss(y_true, y_prob)

    # raise an error for wrong number of classes
    y_prob = [[1, 0], [0, 1], [0, 1]]
        "y_true and y_prob contain different number of "
        "classes: 3 vs 2. Please provide the true "
        "labels explicitly through the labels argument. "
        "Classes found in "
        "y_true: [0 1 2]"
    )

    y_true = ["eggs", "spam", "ham"]
    y_prob = [[1, 0, 0], [0, 1, 0], [0, 1, 0]]
    labels = ["eggs", "spam", "ham", "yams"]
        "The number of classes in labels is different "
        "from that in y_prob. Classes found in "
        "labels: ['eggs' 'ham' 'spam' 'yams']"
    )
        brier_score_loss(y_true, y_prob, labels=labels)

    # raise error message when there's only one class in y_true
    y_true = ["eggs"]
    y_prob = [[0.9, 0.1]]
        "y_true contains only one label (eggs). Please "
        "provide the list of all expected class labels explicitly through the "
        "labels argument."
    )

    # error is fixed when labels is specified
    assert_almost_equal(brier_score_loss(y_true, y_prob, labels=["eggs", "ham"]), 0.01)

def test_brier_score_loss_warnings():
    )
            [
                [1, 0, 0],
                [0, 1, 0],
                [0, 0, 1],
            ],
        )

def test_balanced_accuracy_score_unseen():
    msg = "y_pred contains classes not in y_true"
        balanced_accuracy_score([0, 0, 0], [0, 0, 1])

def test_balanced_accuracy_score(y_true, y_pred):
    macro_recall = recall_score(
        y_true, y_pred, average="macro", labels=np.unique(y_true)
    )
        # Warnings are tested in test_balanced_accuracy_score_unseen
        balanced = balanced_accuracy_score(y_true, y_pred)
    assert balanced == pytest.approx(macro_recall)
    adjusted = balanced_accuracy_score(y_true, y_pred, adjusted=True)
    chance = balanced_accuracy_score(y_true, np.full_like(y_true, y_true[0]))
    assert adjusted == (balanced - chance) / (1 - chance)

def test_classification_metric_pos_label_types(metric, classes):

    """
    if metric is brier_score_loss:
        # brier score loss requires probabilities
        y_pred = rng.uniform(size=n_samples)
    else:
        y_pred = y_true.copy()
    result = metric(y_true, y_pred, pos_label=pos_label)
    assert not np.any(np.isnan(result))

def test_f1_for_small_binary_inputs_with_zero_division(y_true, y_pred, expected_score):
    """Check the behaviour of `zero_division` for f1-score.

    https://github.com/scikit-learn/scikit-learn/issues/26965
    """
    assert f1_score(y_true, y_pred, zero_division=1.0) == pytest.approx(expected_score)

def test_classification_metric_division_by_zero_nan_validaton(scoring):
    """Check that we validate `np.nan` properly for classification metrics.

    With `n_jobs=2` in cross-validation, the `np.nan` used for the singleton will be
    different in the sub-process and we should not use the `is` operator but
    `math.isnan`.

    https://github.com/scikit-learn/scikit-learn/issues/27563
    """
    X, y = datasets.make_classification(random_state=0)
    classifier = DecisionTreeClassifier(max_depth=3, random_state=0).fit(X, y)
    cross_val_score(classifier, X, y, scoring=scoring, n_jobs=2, error_score="raise")

def test_d2_log_loss_score():
    y_true = [0, 0, 0, 1, 1, 1]
    y_true_string = ["no", "no", "no", "yes", "yes", "yes"]
        [
            [0.5, 0.5],
            [0.9, 0.1],
            [0.4, 0.6],
            [0.6, 0.4],
            [0.35, 0.65],
            [0.01, 0.99],
        ]
    )
    y_pred_null = np.array(
        [
        ]
    )
    d2_score = d2_log_loss_score(y_true=y_true, y_pred=y_pred)
    log_likelihood = log_loss(y_true=y_true, y_pred=y_pred, normalize=False)
    log_likelihood_null = log_loss(y_true=y_true, y_pred=y_pred_null, normalize=False)
    d2_score_true = 1 - log_likelihood / log_likelihood_null
    assert d2_score == pytest.approx(d2_score_true)

    # check that using sample weight also gives the correct d2 score
    sample_weight = np.array([2, 1, 3, 4, 3, 1])
    y_pred_null[:, 0] = sample_weight[:3].sum() / sample_weight.sum()
    y_pred_null[:, 1] = sample_weight[3:].sum() / sample_weight.sum()
    d2_score = d2_log_loss_score(
        y_true=y_true, y_pred=y_pred, sample_weight=sample_weight
    )
    log_likelihood = log_loss(
    )
    log_likelihood_null = log_loss(
    )

    # check if good predictions give a relatively higher value for the d2 score
        [
            [0.8, 0.2],
            [0.1, 0.9],
            [0.2, 0.8],
        ]
    )
    d2_score = d2_log_loss_score(y_true, y_pred)
    assert 0.5 < d2_score < 1.0
    # check that a similar value is obtained for string labels
    d2_score_string = d2_log_loss_score(y_true_string, y_pred)
    assert d2_score_string == pytest.approx(d2_score)

    # check if poor predictions gives a relatively low value for the d2 score
        [
            [0.75, 0.25],
        ]
    )
    assert d2_score < 0

    # check if simply using the average of the classes as the predictions
    # gives a d2 score of 0
        [
        ]
    )
    assert d2_score == 0
    assert d2_score_string == 0

    # gives a d2 score of 0 when the positive class has a higher proportion
    y_true = [0, 1, 1, 1]
    y_true_string = ["no", "yes", "yes", "yes"]
    y_pred = np.array([[0.25, 0.75], [0.25, 0.75], [0.25, 0.75], [0.25, 0.75]])
    sample_weight = [2, 2, 2, 2]
    d2_score_with_sample_weight = d2_log_loss_score(
        y_true, y_pred, sample_weight=sample_weight
    )
    assert d2_score_with_sample_weight == 0

    # check that the d2 scores seem correct when more than 2
    # labels are specified
    y_true = ["high", "high", "low", "neutral"]
    sample_weight = [1.4, 0.6, 0.8, 0.2]

        [
            [0.8, 0.1, 0.1],
            [0.1, 0.1, 0.8],
        ]
    )
    d2_score = d2_log_loss_score(y_true, y_pred, sample_weight=sample_weight)

        [
            [0.2, 0.5, 0.3],
            [0.1, 0.7, 0.2],
            [0.2, 0.7, 0.1],
        ]
    )

def test_d2_log_loss_score_missing_labels():
    """Check that d2_log_loss_score works when not all labels are present in y_true

    non-regression test for https://github.com/scikit-learn/scikit-learn/issues/30713
    """
    y_true = [2, 0, 2, 0]
    sample_weight = [1.4, 0.6, 0.7, 0.3]
    y_pred = np.tile([1, 0, 0], (4, 1))

    log_loss_obs = log_loss(y_true, y_pred, sample_weight=sample_weight, labels=labels)

    # Null model consists of weighted average of the classes.
    # Given that the sum of the weights is 3,
    # - weighted average of 0s is (0.6 + 0.3) / 3 = 0.3
    # - weighted average of 1s is 0
    # - weighted average of 2s is (1.4 + 0.7) / 3 = 0.7
    y_pred_null = np.tile([0.3, 0, 0.7], (4, 1))
    log_loss_null = log_loss(
        y_true, y_pred_null, sample_weight=sample_weight, labels=labels
    )

    expected_d2_score = 1 - log_loss_obs / log_loss_null
        y_true, y_pred, sample_weight=sample_weight, labels=labels
    )
    assert_allclose(d2_score, expected_d2_score)

def test_d2_log_loss_score_label_order():
    """Check that d2_log_loss_score doesn't depend on the order of the labels."""

    d2_score = d2_log_loss_score(y_true, y_pred, labels=[0, 1, 2])
    d2_score_other = d2_log_loss_score(y_true, y_pred, labels=[0, 2, 1])

    assert_allclose(d2_score, d2_score_other)

def test_d2_log_loss_score_raises():
    """Test that d2_log_loss_score raises the appropriate errors on
    invalid inputs."""
    y_pred = [[0.2, 0.8], [0.5, 0.5], [0.4, 0.6]]
    err = "contain different number of classes"
    with pytest.raises(ValueError, match=err):
        d2_log_loss_score(y_true, y_pred)

    # check error if the number of classes in labels do not match the number
    # of classes in y_pred.
    y_pred = [[0.5, 0.5], [0.5, 0.5], [0.5, 0.5]]
    err = "number of classes in labels is different"
        d2_log_loss_score(y_true, y_pred, labels=labels)

    # check error if y_true and y_pred do not have equal lengths
    y_pred = [[0.5, 0.5, 0.5], [0.6, 0.3, 0.1]]
    err = "inconsistent numbers of samples"

    # check warning for samples < 2
    y_true = [1]
    y_pred = [[0.5, 0.5]]
    err = "score is not well-defined"
    with pytest.warns(UndefinedMetricWarning, match=err):

    # check error when y_true only has 1 label
    y_true = [1, 1, 1]
    err = "y_true contains only one label"

    # check error when y_true only has 1 label and labels also has
    # only 1 label
    labels = [1]
    err = "The labels array needs to contain at least two"

        )

        )

            cm,
            [
            ],
        )

def dist_func(x1, x2, p):
    return np.sum((x1 - x2) ** p) ** (1.0 / p)

def test_cdist(metric_param_grid, X, Y, csr_container):
    metric, param_grid = metric_param_grid
    keys = param_grid.keys()
    X_csr, Y_csr = csr_container(X), csr_container(Y)
    for vals in itertools.product(*param_grid.values()):
        kwargs = dict(zip(keys, vals))
        rtol_dict = {}
        if metric == "mahalanobis" and X.dtype == np.float32:
            # Computation of mahalanobis differs between
            # the scipy and scikit-learn implementation.
            # Hence, we increase the relative tolerance.
            # TODO: Inspect slight numerical discrepancy
            # with scipy
            rtol_dict = {"rtol": 1e-6}

        D_scipy_cdist = cdist(X, Y, metric, **kwargs)

        dm = DistanceMetric.get_metric(metric, X.dtype, **kwargs)

        # DistanceMetric.pairwise must be consistent for all
        # combinations of formats in {sparse, dense}.
        D_sklearn = dm.pairwise(X, Y)
        assert D_sklearn.flags.c_contiguous
        assert_allclose(D_sklearn, D_scipy_cdist, **rtol_dict)

        D_sklearn = dm.pairwise(X_csr, Y_csr)

        D_sklearn = dm.pairwise(X_csr, Y)

        D_sklearn = dm.pairwise(X, Y_csr)

def test_cdist_bool_metric(metric, X_bool, Y_bool, csr_container):
    if metric in DEPRECATED_METRICS:
        with ignore_warnings(category=DeprecationWarning):
            # Some metrics can be deprecated depending on the scipy version.
            # But if they are present, we still want to test whether
            # scikit-learn gives the same result, whether or not they are
            # deprecated.
            D_scipy_cdist = cdist(X_bool, Y_bool, metric)
    else:
        D_scipy_cdist = cdist(X_bool, Y_bool, metric)

    dm = DistanceMetric.get_metric(metric)
    D_sklearn = dm.pairwise(X_bool, Y_bool)
    assert_allclose(D_sklearn, D_scipy_cdist)

    # DistanceMetric.pairwise must be consistent
    # on all combinations of format in {sparse, dense}².
    X_bool_csr, Y_bool_csr = csr_container(X_bool), csr_container(Y_bool)

    assert D_sklearn.flags.c_contiguous

    D_sklearn = dm.pairwise(X_bool_csr, Y_bool_csr)

    D_sklearn = dm.pairwise(X_bool, Y_bool_csr)

    D_sklearn = dm.pairwise(X_bool_csr, Y_bool)

def test_pdist(metric_param_grid, X, csr_container):

        D_scipy_pdist = cdist(X, X, metric, **kwargs)

        D_sklearn = dm.pairwise(X)
        assert_allclose(D_sklearn, D_scipy_pdist, **rtol_dict)

        D_sklearn_csr = dm.pairwise(X_csr)
        assert_allclose(D_sklearn_csr, D_scipy_pdist, **rtol_dict)

        D_sklearn_csr = dm.pairwise(X_csr, X_csr)

def test_distance_metrics_dtype_consistency(metric_param_grid):
    # DistanceMetric must return similar distances for both float32 and float64
    # input data.

    # Choose rtol to make sure that this test is robust to changes in the random
    # seed in the module-level test data generation code.
    rtol = 1e-5

        dm64 = DistanceMetric.get_metric(metric, np.float64, **kwargs)
        dm32 = DistanceMetric.get_metric(metric, np.float32, **kwargs)

        D64 = dm64.pairwise(X64)
        D32 = dm32.pairwise(X32)

        assert D64.dtype == np.float64
        assert D32.dtype == np.float32

        # assert_allclose introspects the dtype of the input arrays to decide
        # which rtol value to use by default but in this case we know that D32
        # is not computed with the same precision so we set rtol manually.
        assert_allclose(D64, D32, rtol=rtol)

        D64 = dm64.pairwise(X64, Y64)
        D32 = dm32.pairwise(X32, Y32)

def test_pdist_bool_metrics(metric, X_bool, csr_container):
            D_scipy_pdist = cdist(X_bool, X_bool, metric)
    else:
        D_scipy_pdist = cdist(X_bool, X_bool, metric)

    D_sklearn = dm.pairwise(X_bool)
    assert_allclose(D_sklearn, D_scipy_pdist)

    X_bool_csr = csr_container(X_bool)
    D_sklearn = dm.pairwise(X_bool_csr)

def test_pickle(writable_kwargs, metric_param_grid, X):
        if any(isinstance(val, np.ndarray) for val in vals):
            vals = copy.deepcopy(vals)
            for val in vals:
                if isinstance(val, np.ndarray):
                    val.setflags(write=writable_kwargs)
        D1 = dm.pairwise(X)
        dm2 = pickle.loads(pickle.dumps(dm))
        D2 = dm2.pairwise(X)
        assert_allclose(D1, D2)

def test_pickle_bool_metrics(metric, X_bool):
    D1 = dm.pairwise(X_bool)
    dm2 = pickle.loads(pickle.dumps(dm))
    D2 = dm2.pairwise(X_bool)

def test_haversine_metric(X, Y, csr_container):
    # The Haversine DistanceMetric only works on 2 features.
    X = np.asarray(X[:, :2])
    Y = np.asarray(Y[:, :2])

    # Haversine is not supported by scipy.special.distance.{cdist,pdist}
    # So we reimplement it to have a reference.
    def haversine_slow(x1, x2):
        return 2 * np.arcsin(
            np.sqrt(
                np.sin(0.5 * (x1[0] - x2[0])) ** 2
                + np.cos(x1[0]) * np.cos(x2[0]) * np.sin(0.5 * (x1[1] - x2[1])) ** 2
            )
        )

    D_reference = np.zeros((X_csr.shape[0], Y_csr.shape[0]))
    for i, xi in enumerate(X):
        for j, yj in enumerate(Y):
            D_reference[i, j] = haversine_slow(xi, yj)

    haversine = DistanceMetric.get_metric("haversine", X.dtype)

    D_sklearn = haversine.pairwise(X, Y)
        haversine.dist_to_rdist(D_sklearn), np.sin(0.5 * D_reference) ** 2, rtol=1e-6
    )

    assert_allclose(D_sklearn, D_reference)

    D_sklearn = haversine.pairwise(X_csr, Y_csr)

    D_sklearn = haversine.pairwise(X_csr, Y)

    D_sklearn = haversine.pairwise(X, Y_csr)

def test_pyfunc_metric():
    X = np.random.random((10, 3))

    euclidean = DistanceMetric.get_metric("euclidean")
    pyfunc = DistanceMetric.get_metric("pyfunc", func=dist_func, p=2)

    # Check if both callable metric and predefined metric initialized
    # DistanceMetric object is picklable
    euclidean_pkl = pickle.loads(pickle.dumps(euclidean))
    pyfunc_pkl = pickle.loads(pickle.dumps(pyfunc))

    D1 = euclidean.pairwise(X)
    D2 = pyfunc.pairwise(X)

    D1_pkl = euclidean_pkl.pairwise(X)
    D2_pkl = pyfunc_pkl.pairwise(X)

    assert_allclose(D1_pkl, D2_pkl)

def test_input_data_size():
    # Regression test for #6288
    # Previously, a metric requiring a particular input dimension would fail
    def custom_metric(x, y):
        assert x.shape[0] == 3
        return np.sum((x - y) ** 2)

    X = rng.rand(10, 3)

    pyfunc = DistanceMetric.get_metric("pyfunc", func=custom_metric)
    eucl = DistanceMetric.get_metric("euclidean")
    assert_allclose(pyfunc.pairwise(X), eucl.pairwise(X) ** 2)

def test_readonly_kwargs():
    # https://github.com/scikit-learn/scikit-learn/issues/21685

    weights = rng.rand(100)
    VI = rng.rand(10, 10)
    weights.setflags(write=False)
    VI.setflags(write=False)

    # Those distances metrics have to support readonly buffers.
    DistanceMetric.get_metric("seuclidean", V=weights)
    DistanceMetric.get_metric("mahalanobis", VI=VI)

def test_minkowski_metric_validate_weights_values(w, err_type, err_msg):
        DistanceMetric.get_metric("minkowski", p=3, w=w)

def test_minkowski_metric_validate_weights_size():
    w2 = rng.random_sample(d + 1)
    dm = DistanceMetric.get_metric("minkowski", p=3, w=w2)
    msg = (
        "MinkowskiDistance: the size of w must match "
        f"the number of features \\({X64.shape[1]}\\). "
        f"Currently len\\(w\\)={w2.shape[0]}."
    )
        dm.pairwise(X64, Y64)

def test_get_metric_dtype(metric, metric_kwargs, dtype):
    specialized_cls = {
        np.float32: DistanceMetric32,
        np.float64: DistanceMetric64,
    }[dtype]

    # We don't need the entire grid, just one for a sanity check
    metric_kwargs = {k: v[0] for k, v in metric_kwargs.items()}
    generic_type = type(DistanceMetric.get_metric(metric, dtype, **metric_kwargs))
    specialized_type = type(specialized_cls.get_metric(metric, **metric_kwargs))

    assert generic_type is specialized_type

def test_get_metric_bad_dtype():
    dtype = np.int32
    msg = r"Unexpected dtype .* provided. Please select a dtype from"
        DistanceMetric.get_metric("manhattan", dtype)

def test_minkowski_metric_validate_bad_p_parameter():
    msg = "p must be greater than 0"
        DistanceMetric.get_metric("minkowski", p=0)

            np.sqrt(
            )
        )

def _require_positive_y(y):
    offset = abs(y.min()) + 1
    y = y + offset
    return y

def _make_estimators(X_train, y_train, y_ml_train):
    # Make estimators that make sense to test various scoring methods
    sensible_regr = DecisionTreeRegressor(random_state=0)
    # some of the regressions scorers require strictly positive input.
    sensible_regr.fit(X_train, _require_positive_y(y_train))
    sensible_clf = DecisionTreeClassifier(random_state=0)
    sensible_clf.fit(X_train, y_train)
    sensible_ml_clf = DecisionTreeClassifier(random_state=0)
    sensible_ml_clf.fit(X_train, y_ml_train)
    return dict(
        [(name, sensible_regr) for name in REGRESSION_SCORERS]
        + [(name, sensible_clf) for name in CLF_SCORERS]
        + [(name, sensible_clf) for name in CLUSTER_SCORERS]
        + [(name, sensible_ml_clf) for name in MULTILABEL_ONLY_SCORERS]
    )

def memmap_data_and_estimators(tmp_path_factory):
    temp_folder = tmp_path_factory.mktemp("sklearn_test_score_objects")
    X, y = make_classification(n_samples=30, n_features=5, random_state=0)
    _, y_ml = make_multilabel_classification(n_samples=X.shape[0], random_state=0)
    filename = temp_folder / "test_data.pkl"
    joblib.dump((X, y, y_ml), filename)
    X_mm, y_mm, y_ml_mm = joblib.load(filename, mmap_mode="r")
    estimators = _make_estimators(X_mm, y_mm, y_ml_mm)

    yield X_mm, y_mm, y_ml_mm, estimators

class EstimatorWithFit(BaseEstimator):
    """Dummy estimator to test scoring validators"""

class EstimatorWithFitAndScore(BaseEstimator):

        return 1.0

class EstimatorWithFitAndPredict(BaseEstimator):

        self.y = y

        return self.y

class DummyScorer:
    """Dummy scorer that always returns 1."""

    def __call__(self, est, X, y):
        return 1

def test_all_scorers_repr():
    # Test that all scorers have a working repr
    for name in get_scorer_names():
        repr(get_scorer(name))

def check_scoring_validator_for_single_metric_usecases(scoring_validator):
    # Test all branches of single metric usecases
    estimator = EstimatorWithFitAndScore()
    estimator.fit([[1]], [1])
    scorer = scoring_validator(estimator)
    assert isinstance(scorer, _PassthroughScorer)
    assert_almost_equal(scorer(estimator, [[1]], [1]), 1.0)

    estimator = EstimatorWithFitAndPredict()
    pattern = (
        r"If no scoring is specified, the estimator passed should have"
        r" a 'score' method\. The estimator .* does not\."
    )
    with pytest.raises(TypeError, match=pattern):
        scoring_validator(estimator)

    scorer = scoring_validator(estimator, scoring="accuracy")

    estimator = EstimatorWithFit()
    assert isinstance(scorer, _Scorer)
    assert scorer._response_method == "predict"

    # Test the allow_none parameter for check_scoring alone
    if scoring_validator is check_scoring:
        estimator = EstimatorWithFit()
        scorer = scoring_validator(estimator, allow_none=True)
        assert scorer is None

def test_check_scoring_and_check_multimetric_scoring(scoring):
    check_scoring_validator_for_single_metric_usecases(check_scoring)
    # To make sure the check_scoring is correctly applied to the constituent
    # scorers

    estimator = LinearSVC(random_state=0)
    estimator.fit([[1], [2], [3]], [1, 1, 0])

    scorers = _check_multimetric_scoring(estimator, scoring)
    assert isinstance(scorers, dict)
    assert sorted(scorers.keys()) == sorted(list(scoring))
    assert all([isinstance(scorer, _Scorer) for scorer in list(scorers.values())])
    assert all(scorer._response_method == "predict" for scorer in scorers.values())

    if "acc" in scoring:
            scorers["acc"](estimator, [[1], [2], [3]], [1, 0, 0]), 2.0 / 3.0
        )
    if "accuracy" in scoring:
            scorers["accuracy"](estimator, [[1], [2], [3]], [1, 0, 0]), 2.0 / 3.0
        )
    if "precision" in scoring:
            scorers["precision"](estimator, [[1], [2], [3]], [1, 0, 0]), 0.5
        )

def test_check_scoring_and_check_multimetric_scoring_errors(scoring, msg):
    # Make sure it raises errors when scoring parameter is not valid.
    # More weird corner cases are tested at test_validation.py

        _check_multimetric_scoring(estimator, scoring=scoring)

def test_check_scoring_gridsearchcv():
    # test that check_scoring works on GridSearchCV and pipeline.
    # slightly redundant non-regression test.

    grid = GridSearchCV(LinearSVC(), param_grid={"C": [0.1, 1]}, cv=3)
    scorer = check_scoring(grid, scoring="f1")

    pipe = make_pipeline(LinearSVC())
    scorer = check_scoring(pipe, scoring="f1")

    # check that cross_val_score definitely calls the scorer
    # and doesn't make any assumptions about the estimator apart from having a
    # fit.
    scores = cross_val_score(
        EstimatorWithFit(), [[1], [2], [3]], [1, 0, 1], scoring=DummyScorer(), cv=3
    )
    assert_array_equal(scores, 1)

def test_classification_binary_scores(scorer_name, metric):
    # check consistency between score and scorer for scores supporting
    # binary classification.
    X, y = make_blobs(random_state=0, centers=2)
    clf = LinearSVC(random_state=0)

    score = get_scorer(scorer_name)(clf, X_test, y_test)
    expected_score = metric(y_test, clf.predict(X_test))
    assert_almost_equal(score, expected_score)

def test_classification_multiclass_scores(scorer_name, metric):
    # multiclass classification.
        n_classes=3, n_informative=3, n_samples=30, random_state=0
    )

    # use `stratify` = y to ensure train and test sets capture all classes
        X, y, random_state=0, stratify=y
    )

    clf = DecisionTreeClassifier(random_state=0)

def test_custom_scorer_pickling():
    # test that custom scorer can be pickled

    scorer = make_scorer(fbeta_score, beta=2)
    score1 = scorer(clf, X_test, y_test)
    unpickled_scorer = pickle.loads(pickle.dumps(scorer))
    score2 = unpickled_scorer(clf, X_test, y_test)
    assert score1 == pytest.approx(score2)

    # smoke test the repr:
    repr(fbeta_score)

def test_regression_scorers():
    # Test regression scorers.
    diabetes = load_diabetes()
    X, y = diabetes.data, diabetes.target
    clf = Ridge()
    score1 = get_scorer("r2")(clf, X_test, y_test)
    score2 = r2_score(y_test, clf.predict(X_test))
    assert_almost_equal(score1, score2)

def test_thresholded_scorers():
    # Test scorers that take thresholds.
    score1 = get_scorer("roc_auc")(clf, X_test, y_test)
    score2 = roc_auc_score(y_test, clf.decision_function(X_test))
    score3 = roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1])
    assert_almost_equal(score1, score3)

    logscore = get_scorer("neg_log_loss")(clf, X_test, y_test)
    logloss = log_loss(y_test, clf.predict_proba(X_test))
    assert_almost_equal(-logscore, logloss)

    # same for an estimator without decision_function
    clf = DecisionTreeClassifier()
    score2 = roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1])

    # test with a regressor (no decision_function)
    reg = DecisionTreeRegressor()
    err_msg = "DecisionTreeRegressor has none of the following attributes"
    with pytest.raises(AttributeError, match=err_msg):
        get_scorer("roc_auc")(reg, X_test, y_test)

    # Test that an exception is raised on more than two classes
    X, y = make_blobs(random_state=0, centers=3)
    with pytest.raises(ValueError, match="multi_class must be in \\('ovo', 'ovr'\\)"):
        get_scorer("roc_auc")(clf, X_test, y_test)

    # test error is raised with a single class present in model
    # (predict_proba shape is not suitable for binary auc)
    clf.fit(X_train, np.zeros_like(y_train))
    with pytest.raises(ValueError, match="need classifier with two classes"):

    # for proba scorers
        get_scorer("neg_log_loss")(clf, X_test, y_test)

def test_thresholded_scorers_multilabel_indicator_data():
    # Test that the scorer work with multilabel-indicator format
    # for multilabel and multi-output multi-class classifier
    X, y = make_multilabel_classification(allow_unlabeled=False, random_state=0)

    # Multi-output multi-class predict_proba
    y_proba = clf.predict_proba(X_test)
    score2 = roc_auc_score(y_test, np.vstack([p[:, -1] for p in y_proba]).T)

    # Multilabel predict_proba
    clf = OneVsRestClassifier(DecisionTreeClassifier())
    score2 = roc_auc_score(y_test, clf.predict_proba(X_test))

    # Multilabel decision function
    clf = OneVsRestClassifier(LinearSVC(random_state=0))

def test_supervised_cluster_scorers():
    # Test clustering scorers against gold standard labeling.
    km = KMeans(n_clusters=3, n_init="auto")
    km.fit(X_train)
    for name in CLUSTER_SCORERS:
        score1 = get_scorer(name)(km, X_test, y_test)
        score2 = getattr(cluster_module, name)(y_test, km.predict(X_test))
        assert_almost_equal(score1, score2)

def test_raises_on_score_list():
    # Test that when a list of scores is returned, we raise proper errors.
    X, y = make_blobs(random_state=0)
    f1_scorer_no_average = make_scorer(f1_score, average=None)
        cross_val_score(clf, X, y, scoring=f1_scorer_no_average)
    grid_search = GridSearchCV(
        clf, scoring=f1_scorer_no_average, param_grid={"max_depth": [1, 2]}
    )
        grid_search.fit(X, y)

def test_classification_scorer_sample_weight():
    # Test that classification scorers support sample_weight or raise sensible
    # errors

    # Unlike the metrics invariance test, in the scorer case it's harder
    # to ensure that, on the classifier output, weighted and unweighted
    # scores really should be unequal.
    split = train_test_split(X, y, y_ml, random_state=0)
    X_train, X_test, y_train, y_test, y_ml_train, y_ml_test = split

    sample_weight = np.ones_like(y_test)
    sample_weight[:10] = 0

    # get sensible estimators for each metric
    estimator = _make_estimators(X_train, y_train, y_ml_train)

        scorer = get_scorer(name)
        if name in REGRESSION_SCORERS:
            # skip the regression scores
            continue
        if name == "top_k_accuracy":
            # in the binary case k > 1 will always lead to a perfect score
            scorer._kwargs = {"k": 1}
        if name in MULTILABEL_ONLY_SCORERS:
            target = y_ml_test
        else:
            target = y_test
        try:
            weighted = scorer(
                estimator[name], X_test, target, sample_weight=sample_weight
            )
            ignored = scorer(estimator[name], X_test[10:], target[10:])
            unweighted = scorer(estimator[name], X_test, target)
            # this should not raise. sample_weight should be ignored if None.
            _ = scorer(estimator[name], X_test[:10], target[:10], sample_weight=None)
            assert weighted != unweighted, (
                f"scorer {name} behaves identically when called with "
                f"sample weights: {weighted} vs {unweighted}"
            )
                weighted,
                ignored,
                err_msg=(
                    f"scorer {name} behaves differently "
                    "when ignoring samples and setting "
                    f"sample_weight to 0: {weighted} vs {ignored}"
                ),
            )

            assert "sample_weight" in str(e), (
                f"scorer {name} raises unhelpful exception when called "
                f"with sample weights: {e}"
            )

def test_regression_scorer_sample_weight():
    # Test that regression scorers support sample_weight or raise sensible
    # errors

    # Odd number of test samples req for neg_median_absolute_error
    X, y = make_regression(n_samples=101, n_features=20, random_state=0)
    y = _require_positive_y(y)

    # Odd number req for neg_median_absolute_error
    sample_weight[:11] = 0

    reg = DecisionTreeRegressor(random_state=0)

        if name not in REGRESSION_SCORERS:
            # skip classification scorers
            continue
        try:
            weighted = scorer(reg, X_test, y_test, sample_weight=sample_weight)
            ignored = scorer(reg, X_test[11:], y_test[11:])
            unweighted = scorer(reg, X_test, y_test)
            )
                weighted,
                ignored,
                err_msg=(
                ),
            )

            )

def test_scorer_memmap_input(name, memmap_data_and_estimators):
    # Non-regression test for #6147: some score functions would
    # return singleton memmap when computed on memmap data instead of scalar
    # float values.
    X_mm, y_mm, y_ml_mm, estimators = memmap_data_and_estimators

    if name in REQUIRE_POSITIVE_Y_SCORERS:
        y_mm_1 = _require_positive_y(y_mm)
        y_ml_mm_1 = _require_positive_y(y_ml_mm)
    else:
        y_mm_1, y_ml_mm_1 = y_mm, y_ml_mm

    # UndefinedMetricWarning for P / R scores
        scorer, estimator = get_scorer(name), estimators[name]
            score = scorer(estimator, X_mm, y_ml_mm_1)
        else:
            score = scorer(estimator, X_mm, y_mm_1)
        assert isinstance(score, numbers.Number), name

def test_scoring_is_not_metric():
    with pytest.raises(ValueError, match="make_scorer"):
        check_scoring(LogisticRegression(), scoring=f1_score)
        check_scoring(LogisticRegression(), scoring=roc_auc_score)
        check_scoring(Ridge(), scoring=r2_score)
        check_scoring(KMeans(), scoring=cluster_module.adjusted_rand_score)
        check_scoring(KMeans(), scoring=cluster_module.rand_score)

def test_deprecated_scorer():
    X, y = make_regression(n_samples=10, n_features=1, random_state=0)
    deprecated_scorer = get_scorer("max_error")
    with pytest.warns(DeprecationWarning):
        deprecated_scorer(reg, X_test, y_test)

def test_multimetric_scorer_calls_method_once(
    scorers,
    expected_predict_count,
    expected_predict_proba_count,
    expected_decision_func_count,
):
    X, y = np.array([[1], [1], [0], [0], [0]]), np.array([0, 1, 1, 1, 0])
    pos_proba = np.random.rand(X.shape[0])
    proba = np.c_[1 - pos_proba, pos_proba]

    class MyClassifier(ClassifierMixin, BaseEstimator):
        def __init__(self):
            self._expected_predict_count = 0
            self._expected_predict_proba_count = 0
            self._expected_decision_function_count = 0

            self.classes_ = np.unique(y)

            self._expected_predict_count += 1
            return y

            self._expected_predict_proba_count += 1

        def decision_function(self, X):
            self._expected_decision_function_count += 1
            return pos_proba

    mock_est = MyClassifier().fit(X, y)
    scorer_dict = _check_multimetric_scoring(LogisticRegression(), scorers)
    multi_scorer = _MultimetricScorer(scorers=scorer_dict)
    results = multi_scorer(mock_est, X, y)

    assert set(scorers) == set(results)  # compare dict keys

    assert mock_est._expected_predict_count == expected_predict_count
    assert mock_est._expected_predict_proba_count == expected_predict_proba_count
    assert mock_est._expected_decision_function_count == expected_decision_func_count

def test_multimetric_scorer_calls_method_once_classifier_no_decision(scorers):
    predict_proba_call_cnt = 0

    class MockKNeighborsClassifier(KNeighborsClassifier):
            nonlocal predict_proba_call_cnt
            predict_proba_call_cnt += 1
            return super().predict_proba(X)

    # no decision function
    clf = MockKNeighborsClassifier(n_neighbors=1)

    scorer_dict = _check_multimetric_scoring(clf, scorers)
    scorer = _MultimetricScorer(scorers=scorer_dict)
    scorer(clf, X, y)

    assert predict_proba_call_cnt == 1

def test_multimetric_scorer_calls_method_once_regressor_threshold():
    predict_called_cnt = 0

    class MockDecisionTreeRegressor(DecisionTreeRegressor):
            nonlocal predict_called_cnt
            predict_called_cnt += 1
            return super().predict(X)

    clf = MockDecisionTreeRegressor()

    scorers = {"neg_mse": "neg_mean_squared_error", "r2": "r2"}

    assert predict_called_cnt == 1

def test_multimetric_scorer_sanity_check():
    # scoring dictionary returned is the same as calling each scorer separately
    scorers = {
        "a1": "accuracy",
        "a2": "accuracy",
        "ll1": "neg_log_loss",
        "ll2": "neg_log_loss",
        "ra1": "roc_auc",
        "ra2": "roc_auc",
    }

    result = multi_scorer(clf, X, y)

    separate_scores = {
        name: get_scorer(name)(clf, X, y)
        for name in ["accuracy", "neg_log_loss", "roc_auc"]
    }

    for key, value in result.items():
        score_name = scorers[key]
        assert_allclose(value, separate_scores[score_name])

def test_multimetric_scorer_exception_handling(raise_exc):
    """Check that the calling of the `_MultimetricScorer` returns
    exception messages in the result dict for the failing scorers
    in case of `raise_exc` is `False` and if `raise_exc` is `True`,
    then the proper exception is raised.
    """
    scorers = {
        "failing_1": "neg_mean_squared_log_error",
        "non_failing": "neg_median_absolute_error",
        "failing_2": "neg_mean_squared_log_error",
    }

        n_samples=50, n_features=2, n_redundant=0, random_state=0
    )
    # neg_mean_squared_log_error fails if y contains values less than or equal to -1
    y *= -1

    clf = DecisionTreeClassifier().fit(X, y)

    multi_scorer = _MultimetricScorer(scorers=scorer_dict, raise_exc=raise_exc)

    error_msg = (
    )

    if raise_exc:
        with pytest.raises(ValueError, match=error_msg):
            multi_scorer(clf, X, y)
    else:
        result = multi_scorer(clf, X, y)

        exception_message_1 = result["failing_1"]
        score = result["non_failing"]
        exception_message_2 = result["failing_2"]

        assert isinstance(exception_message_1, str) and error_msg in exception_message_1
        assert isinstance(score, float)
        assert isinstance(exception_message_2, str) and error_msg in exception_message_2

def test_multiclass_roc_proba_scorer(scorer_name, metric):
    scorer = get_scorer(scorer_name)
        n_classes=3, n_informative=3, n_samples=20, random_state=0
    )
    lr = LogisticRegression().fit(X, y)
    y_proba = lr.predict_proba(X)
    expected_score = metric(y, y_proba)

    assert scorer(lr, X, y) == pytest.approx(expected_score)

def test_multiclass_roc_proba_scorer_label():
    scorer = make_scorer(
        roc_auc_score,
        multi_class="ovo",
        labels=[0, 1, 2],
        response_method="predict_proba",
    )
    )

    y_binary = y == 0
    expected_score = roc_auc_score(
        y_binary, y_proba, multi_class="ovo", labels=[0, 1, 2]
    )

    assert scorer(lr, X, y_binary) == pytest.approx(expected_score)

def test_multiclass_roc_no_proba_scorer_errors(scorer_name):
    # Perceptron has no predict_proba
    )
    lr = Perceptron().fit(X, y)
    msg = "Perceptron has none of the following attributes: predict_proba."
        scorer(lr, X, y)

def string_labeled_classification_problem():
    """Train a classifier on binary problem with string target.

    The classifier is trained on a binary classification problem where the
    minority class of interest has a string label that is intentionally not the
    greatest class label using the lexicographic order. In this case, "cancer"
    is the positive label, and `classifier.classes_` is
    `["cancer", "not cancer"]`.

    In addition, the dataset is imbalanced to better identify problems when
    using non-symmetric performance metrics such as f1-score, average precision
    and so on.

    Returns
    -------
    classifier : estimator object
        Trained classifier on the binary problem.
    X_test : ndarray of shape (n_samples, n_features)
        Data to be used as testing set in tests.
    y_test : ndarray of shape (n_samples,), dtype=object
        Binary target where labels are strings.
    y_pred : ndarray of shape (n_samples,), dtype=object
        Prediction of `classifier` when predicting for `X_test`.
    y_pred_proba : ndarray of shape (n_samples, 2), dtype=np.float64
        Probabilities of `classifier` when predicting for `X_test`.
    y_pred_decision : ndarray of shape (n_samples,), dtype=np.float64
        Decision function values of `classifier` when predicting on `X_test`.
    """
    from sklearn.datasets import load_breast_cancer
    from sklearn.utils import shuffle

    # create an highly imbalanced classification task
    idx_positive = np.flatnonzero(y == 1)
    idx_negative = np.flatnonzero(y == 0)
    idx_selected = np.hstack([idx_negative, idx_positive[:25]])
    X, y = X[idx_selected], y[idx_selected]
    X, y = shuffle(X, y, random_state=42)
    # only use 2 features to make the problem even harder
    X = X[:, :2]
    y = np.array(["cancer" if c == 1 else "not cancer" for c in y], dtype=object)
        X,
        y,
        stratify=y,
    )
    classifier = LogisticRegression().fit(X_train, y_train)
    y_pred = classifier.predict(X_test)
    y_pred_proba = classifier.predict_proba(X_test)
    y_pred_decision = classifier.decision_function(X_test)

    return classifier, X_test, y_test, y_pred, y_pred_proba, y_pred_decision

def test_average_precision_pos_label(string_labeled_classification_problem):
    # check that _Scorer will lead to the right score when passing
    # `pos_label`. Currently, only `average_precision_score` is defined to
    # be such a scorer.
    (
        clf,
        X_test,
        y_test,
        _,
        y_pred_proba,
        y_pred_decision,
    ) = string_labeled_classification_problem

    pos_label = "cancer"
    # we need to select the positive column or reverse the decision values
    y_pred_proba = y_pred_proba[:, 0]
    y_pred_decision = y_pred_decision * -1
    assert clf.classes_[0] == pos_label

    # check that when calling the scoring function, probability estimates and
    # decision values lead to the same results
    ap_proba = average_precision_score(y_test, y_pred_proba, pos_label=pos_label)
    ap_decision_function = average_precision_score(
        y_test, y_pred_decision, pos_label=pos_label
    )
    assert ap_proba == pytest.approx(ap_decision_function)

    # create a scorer which would require to pass a `pos_label`
    # check that it fails if `pos_label` is not provided
    average_precision_scorer = make_scorer(
        average_precision_score,
        response_method=("decision_function", "predict_proba"),
    )
    err_msg = "pos_label=1 is not a valid label. It should be one of "
        average_precision_scorer(clf, X_test, y_test)

    # otherwise, the scorer should give the same results than calling the
    # scoring function
    )
    ap_scorer = average_precision_scorer(clf, X_test, y_test)

    assert ap_scorer == pytest.approx(ap_proba)

    # The above scorer call is using `clf.decision_function`. We will force
    # it to use `clf.predict_proba`.
    clf_without_predict_proba = deepcopy(clf)

    def _predict_proba(self, X):

    clf_without_predict_proba.predict_proba = partial(
        _predict_proba, clf_without_predict_proba
    )
    # sanity check
        clf_without_predict_proba.predict_proba(X_test)

    ap_scorer = average_precision_scorer(clf_without_predict_proba, X_test, y_test)

def test_brier_score_loss_pos_label(string_labeled_classification_problem):
    # check that _Scorer leads to the right score when `pos_label` is
    # provided. Currently only the `brier_score_loss` is defined to be such
    # a scorer.
    clf, X_test, y_test, _, y_pred_proba, _ = string_labeled_classification_problem

    # brier score loss is symmetric
    brier_pos_cancer = brier_score_loss(y_test, y_pred_proba[:, 0], pos_label="cancer")
    brier_pos_not_cancer = brier_score_loss(
        y_test, y_pred_proba[:, 1], pos_label="not cancer"
    )
    assert brier_pos_cancer == pytest.approx(brier_pos_not_cancer)

    brier_scorer = make_scorer(
        brier_score_loss,
    )
    assert brier_scorer(clf, X_test, y_test) == pytest.approx(brier_pos_cancer)

def test_non_symmetric_metric_pos_label(
    score_func, string_labeled_classification_problem
):
    # provided. We check for all possible metric supported.
    # Note: At some point we may end up having "scorer tags".
    clf, X_test, y_test, y_pred, _, _ = string_labeled_classification_problem

    score_pos_cancer = score_func(y_test, y_pred, pos_label="cancer")
    score_pos_not_cancer = score_func(y_test, y_pred, pos_label="not cancer")

    assert score_pos_cancer != pytest.approx(score_pos_not_cancer)

    scorer = make_scorer(score_func, pos_label=pos_label)
    assert scorer(clf, X_test, y_test) == pytest.approx(score_pos_cancer)

def test_scorer_select_proba_error(scorer):
    # check that we raise the proper error when passing an unknown
    # pos_label
        n_classes=2, n_informative=3, n_samples=20, random_state=0
    )
    assert scorer._kwargs["pos_label"] not in np.unique(y).tolist()

    err_msg = "is not a valid label"

def test_get_scorer_return_copy():
    # test that get_scorer returns a copy
    assert get_scorer("roc_auc") is not get_scorer("roc_auc")

def test_scorer_no_op_multiclass_select_proba():
    # check that calling a _Scorer on a multiclass problem do not raise
    # even if `y_true` would be binary during the scoring.
    # `_select_proba_binary` should not be called in this case.
    )

    mask_last_class = y == lr.classes_[-1]
    X_test, y_test = X[~mask_last_class], y[~mask_last_class]
    assert_array_equal(np.unique(y_test), lr.classes_[:-1])

        labels=lr.classes_,
    )
    scorer(lr, X_test, y_test)

def test_scorer_set_score_request_raises(name):
    """Test that set_score_request is only available when feature flag is on."""
    # Make sure they expose the routing methods.
    scorer = get_scorer(name)
    with pytest.raises(RuntimeError, match="This method is only available"):
        scorer.set_score_request()

def test_scorer_metadata_request(name):
    """Testing metadata requests for scorers.

    This test checks many small things in a large test, to reduce the
    boilerplate required for each section.
    """
    assert hasattr(scorer, "set_score_request")
    assert hasattr(scorer, "get_metadata_routing")

    # Check that by default no metadata is requested.
    assert_request_is_empty(scorer.get_metadata_routing())

    weighted_scorer = scorer.set_score_request(sample_weight=True)
    # set_score_request should mutate the instance, rather than returning a
    # new instance
    assert weighted_scorer is scorer

    # make sure the scorer doesn't request anything on methods other than
    # `score`, and that the requested value on `score` is correct.
    assert_request_is_empty(weighted_scorer.get_metadata_routing(), exclude="score")
    assert (
        weighted_scorer.get_metadata_routing().score.requests["sample_weight"] is True
    )

    # make sure putting the scorer in a router doesn't request anything by
    # default
    router = MetadataRouter(owner="test").add(
        scorer=get_scorer(name),
        method_mapping=MethodMapping().add(caller="score", callee="score"),
    )
    # make sure `sample_weight` is refused if passed.
    with pytest.raises(TypeError, match="got unexpected argument"):
        router.validate_metadata(params={"sample_weight": 1}, method="score")
    # make sure `sample_weight` is not routed even if passed.
    routed_params = router.route_params(params={"sample_weight": 1}, caller="score")
    assert not routed_params.scorer.score

    # make sure putting weighted_scorer in a router requests sample_weight
        scorer=weighted_scorer,
    )
    router.validate_metadata(params={"sample_weight": 1}, method="score")
    assert list(routed_params.scorer.score.keys()) == ["sample_weight"]

def test_metadata_kwarg_conflict():
    """This test makes sure the right warning is raised if the user passes
    some metadata both as a constructor to make_scorer, and during __call__.
    """
    )

    )
    with pytest.warns(UserWarning, match="already set as kwargs"):
        scorer.set_score_request(labels=True)

    with pytest.warns(UserWarning, match="There is an overlap"):
        scorer(lr, X, y, labels=lr.classes_)

def test_PassthroughScorer_set_score_request():
    """Test that _PassthroughScorer.set_score_request adds the correct metadata request
    on itself and doesn't change its estimator's routing."""
    est = LogisticRegression().set_score_request(sample_weight="estimator_weights")
    # make a `_PassthroughScorer` with `check_scoring`:
    scorer = check_scoring(est, None)
    assert (
        scorer.get_metadata_routing().score.requests["sample_weight"]
        == "estimator_weights"
    )

    scorer.set_score_request(sample_weight="scorer_weights")
    assert (
        == "scorer_weights"
    )

    # making sure changing the passthrough object doesn't affect the estimator.
    assert (
        est.get_metadata_routing().score.requests["sample_weight"]
    )

def test_PassthroughScorer_set_score_request_raises_without_routing_enabled():
    """Test that _PassthroughScorer.set_score_request raises if metadata routing is
    disabled."""
    scorer = check_scoring(LogisticRegression(), None)
    msg = "This method is only available when metadata routing is enabled."

    with pytest.raises(RuntimeError, match=msg):
        scorer.set_score_request(sample_weight="my_weights")

def test_multimetric_scoring_metadata_routing():
    # Test that _MultimetricScorer properly routes metadata.
    def score1(y_true, y_pred):
        return 1

    def score2(y_true, y_pred, sample_weight="test"):
        # make sure sample_weight is not passed
        assert sample_weight == "test"
        return 1

    def score3(y_true, y_pred, sample_weight=None):
        # make sure sample_weight is passed
        assert sample_weight is not None
        return 1

    scorers = {
        "score1": make_scorer(score1),
        "score2": make_scorer(score2).set_score_request(sample_weight=False),
        "score3": make_scorer(score3).set_score_request(sample_weight=True),
    }

    )

    # This passes since routing is done.
    multi_scorer(clf, X, y, sample_weight=1)

def test_multimetric_scoring_kwargs():
    # Test that _MultimetricScorer correctly forwards kwargs
    # to the scorers when metadata routing is disabled.
    # `sample_weight` is only forwarded to the scorers that accept it.
    # Other arguments are forwarded to all scorers.
    def score1(y_true, y_pred, common_arg=None):
        # make sure common_arg is passed
        assert common_arg is not None
        return 1

    def score2(y_true, y_pred, common_arg=None, sample_weight=None):
        return 1

    scorers = {
        "score2": make_scorer(score2),
    }

    )

    multi_scorer(clf, X, y, common_arg=1, sample_weight=1)

def test_kwargs_without_metadata_routing_error():
    # Test that kwargs are not supported in scorers if metadata routing is not
    # enabled.
    # TODO: remove when enable_metadata_routing is deprecated
    def score(y_true, y_pred, param=None):
        return 1  # pragma: no cover

    )

    scorer = make_scorer(score)
    with config_context(enable_metadata_routing=False):
        with pytest.raises(
            ValueError, match="is only supported if enable_metadata_routing=True"
        ):
            scorer(clf, X, y, param="blah")

def test_get_scorer_multilabel_indicator():
    """Check that our scorer deal with multi-label indicator matrices.

    https://github.com/scikit-learn/scikit-learn/issues/26817
    """
    X, Y = make_multilabel_classification(n_samples=72, n_classes=3, random_state=0)
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, random_state=0)

    estimator = KNeighborsClassifier().fit(X_train, Y_train)

    score = get_scorer("average_precision")(estimator, X_test, Y_test)
    assert score > 0.8

def test_make_scorer_repr(scorer, expected_repr):
    """Check the representation of the scorer."""
    assert repr(scorer) == expected_repr

def test_get_scorer_multimetric(pass_estimator):
    """Check that check_scoring is compatible with multi-metric configurations."""
    X, y = make_classification(n_samples=150, n_features=10, random_state=0)

    if pass_estimator:
        check_scoring_ = check_scoring
    else:
        check_scoring_ = partial(check_scoring, clf)

    expected_results = {
        "r2": r2_score(y_test, y_pred),
        "roc_auc": roc_auc_score(y_test, y_proba[:, 1]),
        "accuracy": accuracy_score(y_test, y_pred),
    }

    for container in [set, list, tuple]:
        scoring = check_scoring_(scoring=container(["r2", "roc_auc", "accuracy"]))
        result = scoring(clf, X_test, y_test)

        assert result.keys() == expected_results.keys()
        for name in result:
            assert result[name] == pytest.approx(expected_results[name])

    def double_accuracy(y_true, y_pred):
        return 2 * accuracy_score(y_true, y_pred)

    custom_scorer = make_scorer(double_accuracy, response_method="predict")

    # dict with different names
    dict_scoring = check_scoring_(
        scoring={
            "my_r2": "r2",
            "my_roc_auc": "roc_auc",
            "double_accuracy": custom_scorer,
        }
    )
    dict_result = dict_scoring(clf, X_test, y_test)
    assert len(dict_result) == 3
    assert dict_result["my_r2"] == pytest.approx(expected_results["r2"])
    assert dict_result["my_roc_auc"] == pytest.approx(expected_results["roc_auc"])
    assert dict_result["double_accuracy"] == pytest.approx(
        2 * expected_results["accuracy"]
    )

def test_multimetric_scorer_repr():
    """Check repr for multimetric scorer"""
    multi_metric_scorer = check_scoring(scoring=["accuracy", "r2"])

    assert str(multi_metric_scorer) == 'MultiMetricScorer("accuracy", "r2")'

def test_check_scoring_multimetric_raise_exc():
    """Test that check_scoring returns error code for a subset of scorers in
    multimetric scoring if raise_exc=False and raises otherwise."""

    def raising_scorer(estimator, X, y):
        raise ValueError("That doesn't work.")

    clf = LogisticRegression().fit(X_train, y_train)

    # "raising_scorer" is raising ValueError and should return an string representation
    # of the error of the last scorer:
    scoring = {
        "accuracy": make_scorer(accuracy_score),
        "raising_scorer": raising_scorer,
    }
    scoring_call = check_scoring(estimator=clf, scoring=scoring, raise_exc=False)
    scores = scoring_call(clf, X_test, y_test)
    assert "That doesn't work." in scores["raising_scorer"]

    # should raise an error
    scoring_call = check_scoring(estimator=clf, scoring=scoring, raise_exc=True)
    err_msg = "That doesn't work."
        scores = scoring_call(clf, X_test, y_test)

def test_metadata_routing_multimetric_metadata_routing(enable_metadata_routing):
    """Test multimetric scorer works with and without metadata routing enabled when
    there is no actual metadata to pass.

    Non-regression test for https://github.com/scikit-learn/scikit-learn/issues/28256
    """
    X, y = make_classification(n_samples=50, n_features=10, random_state=0)
    estimator = EstimatorWithFitAndPredict().fit(X, y)

    multimetric_scorer = _MultimetricScorer(scorers={"acc": get_scorer("accuracy")})
    with config_context(enable_metadata_routing=enable_metadata_routing):
        multimetric_scorer(estimator, X, y)

def test_curve_scorer():
    """Check the behaviour of the `_CurveScorer` class."""
    estimator = LogisticRegression().fit(X, y)
    curve_scorer = _CurveScorer(
        balanced_accuracy_score,
        sign=1,
        thresholds=10,
        kwargs={},
    )
    scores, thresholds = curve_scorer(estimator, X, y)

    assert thresholds.shape == scores.shape
    # check that the thresholds are probabilities with extreme values close to 0 and 1.
    # they are not exactly 0 and 1 because they are the extremum of the
    # `estimator.predict_proba(X)` values.
    assert 0 <= thresholds.min() <= 0.01
    assert 0.99 <= thresholds.max() <= 1
    # balanced accuracy should be between 0.5 and 1 when it is not adjusted
    assert 0.5 <= scores.min() <= 1

    # check that passing kwargs to the scorer works
        sign=1,
        kwargs={"adjusted": True},
    )

    assert 0 <= scores.min() <= 0.5

    # check that we can inverse the sign of the score when dealing with `neg_*` scorer
        sign=-1,
    )

    assert all(scores <= 0)

def test_curve_scorer_pos_label(global_random_seed):
    """Check that we propagate properly the `pos_label` parameter to the scorer."""
    n_samples = 30
        n_samples=n_samples, weights=[0.9, 0.1], random_state=global_random_seed
    )

        recall_score,
        sign=1,
        kwargs={"pos_label": 1},
    )
    scores_pos_label_1, thresholds_pos_label_1 = curve_scorer(estimator, X, y)

        sign=1,
        kwargs={"pos_label": 0},
    )
    scores_pos_label_0, thresholds_pos_label_0 = curve_scorer(estimator, X, y)

    # Since `pos_label` is forwarded to the curve_scorer, the thresholds are not equal.
    assert not (thresholds_pos_label_1 == thresholds_pos_label_0).all()
    # The min-max range for the thresholds is defined by the probabilities of the
    # `pos_label` class (the column of `predict_proba`).
    y_pred = estimator.predict_proba(X)
    assert thresholds_pos_label_0.min() == pytest.approx(y_pred.min(axis=0)[0])
    assert thresholds_pos_label_0.max() == pytest.approx(y_pred.max(axis=0)[0])
    assert thresholds_pos_label_1.min() == pytest.approx(y_pred.min(axis=0)[1])
    assert thresholds_pos_label_1.max() == pytest.approx(y_pred.max(axis=0)[1])

    # The recall cannot be negative and `pos_label=1` should have a higher recall
    # since there is less samples to be considered.
    assert 0.0 < scores_pos_label_0.min() < scores_pos_label_1.min()
    assert scores_pos_label_0.max() == pytest.approx(1.0)
    assert scores_pos_label_1.max() == pytest.approx(1.0)

def test_make_scorer_reponse_method_default_warning():
    with pytest.warns(FutureWarning, match="response_method=None is deprecated"):
        make_scorer(accuracy_score, response_method=None)

    # No warning is raised if response_method is left to its default value
    # because the future default value has the same effect as the current one.
        warnings.simplefilter("error", FutureWarning)
        make_scorer(accuracy_score)

            return y

        return 1

        return 1

        return 1

        return 1

            return y

def sqeuclidean_row_norms(X, num_threads):
    """Compute the squared euclidean norm of the rows of X in parallel.

    X : ndarray or CSR matrix of shape (n_samples, n_features)
        Input data. Must be c-contiguous.

    num_threads : int
        The number of OpenMP threads to use.

    Returns
    -------
    sqeuclidean_row_norms : ndarray of shape (n_samples,)
        Arrays containing the squared euclidean norm of each row of X.
    """
    if X.dtype == np.float64:
        return np.asarray(_sqeuclidean_row_norms64(X, num_threads))
    if X.dtype == np.float32:
        return np.asarray(_sqeuclidean_row_norms32(X, num_threads))

    raise ValueError(
        "Only float64 or float32 datasets are supported at this time, "
        f"got: X.dtype={X.dtype}."
    )

class BaseDistancesReductionDispatcher:
    """Abstract base dispatcher for pairwise distance computation & reduction.

    Each dispatcher extending the base :class:`BaseDistancesReductionDispatcher`
    dispatcher must implement the :meth:`compute` classmethod.
    """

    def valid_metrics(cls) -> List[str]:
        excluded = {
            # PyFunc cannot be supported because it necessitates interacting with
            # the CPython interpreter to call user defined functions.
            "pyfunc",
            "mahalanobis",  # is numerically unstable
            # In order to support discrete distance metrics, we need to have a
            # stable simultaneous sort which preserves the order of the indices
            # because there generally is a lot of occurrences for a given values
            # of distances in this case.
            # TODO: implement a stable simultaneous_sort.
            "hamming",
            *BOOL_METRICS,
        }
        return sorted(({"sqeuclidean"} | set(METRIC_MAPPING64.keys())) - excluded)

    def is_usable_for(cls, X, Y, metric) -> bool:
        """Return True if the dispatcher can be used for the
        given parameters.

        X : {ndarray, sparse matrix} of shape (n_samples_X, n_features)

        Y : {ndarray, sparse matrix} of shape (n_samples_Y, n_features)

        metric : str, default='euclidean'
            The distance metric to use.
            For a list of available metrics, see the documentation of
            :class:`~sklearn.metrics.DistanceMetric`.

        Returns
        -------
        True if the dispatcher can be used, else False.
        """

        # FIXME: the current Cython implementation is too slow for a large number of
        # features. We temporarily disable it to fallback on SciPy's implementation.
        # See: https://github.com/scikit-learn/scikit-learn/issues/28191
        if (
            issparse(X)
            and issparse(Y)
            and isinstance(metric, str)
            and "euclidean" in metric
        ):

        def is_numpy_c_ordered(X):
            return hasattr(X, "flags") and getattr(X.flags, "c_contiguous", False)

        def is_valid_sparse_matrix(X):
            return (
                issparse(X)
                and X.format == "csr"
                and
                # TODO: support CSR matrices without non-zeros elements
                X.nnz > 0
                and
                # TODO: support CSR matrices with int64 indices and indptr
                # See: https://github.com/scikit-learn/scikit-learn/issues/23653
                X.indices.dtype == X.indptr.dtype == np.int32
            )

        is_usable = (
            get_config().get("enable_cython_pairwise_dist", True)
            and (is_numpy_c_ordered(X) or is_valid_sparse_matrix(X))
            and (is_numpy_c_ordered(Y) or is_valid_sparse_matrix(Y))
            and X.dtype == Y.dtype
            and X.dtype in (np.float32, np.float64)
            and (metric in cls.valid_metrics() or isinstance(metric, DistanceMetric))
        )

        return is_usable

    def compute(
        cls,
        X,
        Y,
        **kwargs,
    ):
        """Compute the reduction.

        X : ndarray or CSR matrix of shape (n_samples_X, n_features)

        Y : ndarray or CSR matrix of shape (n_samples_Y, n_features)

        **kwargs : additional parameters for the reduction

        Notes
        -----
        This method is an abstract class method: it has to be implemented
        for all subclasses.
        """

class ArgKmin(BaseDistancesReductionDispatcher):
    """Compute the argkmin of row vectors of X on the ones of Y.

    For each row vector of X, computes the indices of k first the rows
    vectors of Y with the smallest distances.

    ArgKmin is typically used to perform
    bruteforce k-nearest neighbors queries.

    This class is not meant to be instantiated, one should only use
    its :meth:`compute` classmethod which handles allocation and
    deallocation consistently.
    """

        cls,
        X,
        Y,
        k,
        metric="euclidean",
        chunk_size=None,
        metric_kwargs=None,
        strategy=None,
        return_distance=False,
    ):
        """Compute the argkmin reduction.

        k : int
            The k for the argkmin reduction.

            The distance metric to use for argkmin.

        chunk_size : int, default=None,
            The number of vectors per chunk. If None (default) looks-up in
            scikit-learn configuration for `pairwise_dist_chunk_size`,
            and use 256 if it is not set.

        metric_kwargs : dict, default=None
            Keyword arguments to pass to specified metric function.

        strategy : str, {'auto', 'parallel_on_X', 'parallel_on_Y'}, default=None
            The chunking strategy defining which dataset parallelization are made on.

            For both strategies the computations happens with two nested loops,
            respectively on chunks of X and chunks of Y.
            Strategies differs on which loop (outer or inner) is made to run
            in parallel with the Cython `prange` construct:

              - 'parallel_on_X' dispatches chunks of X uniformly on threads.
                Each thread then iterates on all the chunks of Y. This strategy is
                embarrassingly parallel and comes with no datastructures
                synchronisation.

              - 'parallel_on_Y' dispatches chunks of Y uniformly on threads.
                Each thread processes all the chunks of X in turn. This strategy is
                a sequence of embarrassingly parallel subtasks (the inner loop on Y
                chunks) with intermediate datastructures synchronisation at each
                iteration of the sequential outer loop on X chunks.

              - 'auto' relies on a simple heuristic to choose between
                'parallel_on_X' and 'parallel_on_Y': when `X.shape[0]` is large enough,
                'parallel_on_X' is usually the most efficient strategy.
                When `X.shape[0]` is small but `Y.shape[0]` is large, 'parallel_on_Y'
                brings more opportunity for parallelism and is therefore more efficient

              - None (default) looks-up in scikit-learn configuration for
                `pairwise_dist_parallel_strategy`, and use 'auto' if it is not set.

        return_distance : boolean, default=False
            Return distances between each X vector and its
            argkmin if set to True.

        Returns
        -------
        If return_distance=False:
          - argkmin_indices : ndarray of shape (n_samples_X, k)
            Indices of the argkmin for each vector in X.

        If return_distance=True:
          - argkmin_distances : ndarray of shape (n_samples_X, k)
            Distances to the argkmin for each vector in X.

        Notes
        -----
        This classmethod inspects the arguments values to dispatch to the
        dtype-specialized implementation of :class:`ArgKmin`.

        This allows decoupling the API entirely from the implementation details
        whilst maintaining RAII: all temporarily allocated datastructures necessary
        for the concrete implementation are therefore freed when this classmethod
        returns.
        """
        if X.dtype == Y.dtype == np.float64:
            return ArgKmin64.compute(
                X=X,
                Y=Y,
                k=k,
                metric=metric,
                chunk_size=chunk_size,
                metric_kwargs=metric_kwargs,
                strategy=strategy,
                return_distance=return_distance,
            )

        if X.dtype == Y.dtype == np.float32:
            return ArgKmin32.compute(
                X=X,
                Y=Y,
                k=k,
            )

            "Only float64 or float32 datasets pairs are supported at this time, "
            f"got: X.dtype={X.dtype} and Y.dtype={Y.dtype}."
        )

class RadiusNeighbors(BaseDistancesReductionDispatcher):
    """Compute radius-based neighbors for two sets of vectors.

    For each row-vector X[i] of the queries X, find all the indices j of
    row-vectors in Y such that:

                        dist(X[i], Y[j]) <= radius

    The distance function `dist` depends on the values of the `metric`
    and `metric_kwargs` parameters.

    """

        cls,
        X,
        Y,
        radius,
        sort_results=False,
    ):
        """Return the results of the reduction for the given arguments.

        radius : float
            The radius defining the neighborhood.

                despite the synchronization step at each iteration of the outer loop
                on chunks of `X`.

            Return distances between each X vector and its neighbors if set to True.

        sort_results : boolean, default=False
            Sort results with respect to distances between each X vector and its
            neighbors if set to True.

        Returns
        -------
          - neighbors_indices : ndarray of n_samples_X ndarray
            Indices of the neighbors for each vector in X.

          - neighbors_distances : ndarray of n_samples_X ndarray
            Distances to the neighbors for each vector in X.

        Notes
        -----
        dtype-specialized implementation of :class:`RadiusNeighbors`.

        returns.
        """
            return RadiusNeighbors64.compute(
                X=X,
                Y=Y,
                radius=radius,
                sort_results=sort_results,
            )

            return RadiusNeighbors32.compute(
                X=X,
                Y=Y,
            )

        )

class ArgKminClassMode(BaseDistancesReductionDispatcher):
    """Compute the argkmin of row vectors of X on the ones of Y with labels.

    vectors of Y with the smallest distances. Computes weighted mode of labels.

    ArgKminClassMode is typically used to perform bruteforce k-nearest neighbors
    queries when the weighted mode of the labels for the k-nearest neighbors
    are required, such as in `predict` methods.

    """

            # Euclidean is technically usable for ArgKminClassMode
            # but its current implementation would not be competitive.
            # TODO: implement Euclidean specialization using GEMM.
            "euclidean",
            "sqeuclidean",
        }
        return list(set(BaseDistancesReductionDispatcher.valid_metrics()) - excluded)

        cls,
        X,
        Y,
        k,
        weights,
        Y_labels,
        unique_Y_labels,
    ):

            The input array to be labelled.

        Y : ndarray of shape (n_samples_Y, n_features)
            The input array whose class membership are provided through the
            `Y_labels` parameter.

        k : int
            The number of nearest neighbors to consider.

        weights : ndarray
            The weights applied over the `Y_labels` of `Y` when computing the
            weighted mode of the labels.

        Y_labels : ndarray
            An array containing the index of the class membership of the
            associated samples in `Y`. This is used in labeling `X`.

        unique_Y_labels : ndarray
            An array containing all unique indices contained in the
            corresponding `Y_labels` array.

            The distance metric to use. For a list of available metrics, see
            the documentation of :class:`~sklearn.metrics.DistanceMetric`.
            Currently does not support `'precomputed'`.

        Returns
        -------
        probabilities : ndarray of shape (n_samples_X, n_classes)
            An array containing the class probabilities for each sample.

        Notes
        -----
        This classmethod is responsible for introspecting the arguments
        values to dispatch to the most appropriate implementation of
        :class:`PairwiseDistancesArgKmin`.

        returns.
        """
        if weights not in {"uniform", "distance"}:
                "Only the 'uniform' or 'distance' weights options are supported"
                f" at this time. Got: {weights=}."
            )
            return ArgKminClassMode64.compute(
                X=X,
                Y=Y,
                k=k,
                Y_labels=np.array(Y_labels, dtype=np.intp),
                unique_Y_labels=np.array(unique_Y_labels, dtype=np.intp),
            )

            return ArgKminClassMode32.compute(
                X=X,
                Y=Y,
                k=k,
            )

        )

class RadiusNeighborsClassMode(BaseDistancesReductionDispatcher):
    """Compute radius-based class modes of row vectors of X using the
    those of Y.

    RadiusNeighborsClassMode is typically used to perform bruteforce
    radius neighbors queries when the weighted mode of the labels for
    the nearest neighbors within the specified radius are required,
    such as in `predict` methods.

    """

            # Euclidean is technically usable for RadiusNeighborsClassMode
            # but it would not be competitive.
        }
        return sorted(set(BaseDistancesReductionDispatcher.valid_metrics()) - excluded)

        cls,
        X,
        Y,
        radius,
        weights,
        Y_labels,
        outlier_label,
    ):
            The input array whose class membership is provided through
            the `Y_labels` parameter.
            The weights applied to the `Y_labels` when computing the
            An array containing all unique class labels.
        outlier_label : int, default=None
            Label for outlier samples (samples with no neighbors in given
            radius). In the default case when the value is None if any
            outlier is detected, a ValueError will be raised. The outlier
            label should be selected from among the unique 'Y' labels. If
            it is specified with a different value a warning will be raised
            and all class probabilities of outliers will be assigned to be 0.
        Returns
        -------
        """
            )
            return RadiusNeighborsClassMode64.compute(
                X=X,
                Y=Y,
                outlier_label=outlier_label,
            )

            return RadiusNeighborsClassMode32.compute(
                X=X,
                Y=Y,
            )

        )

            "pyfunc",
        }

        Returns
        -------
        """

        if (
        ):

            return (
                and
                X.nnz > 0
                and
            )

        )

        cls,
        X,
        Y,
        **kwargs,
    ):

        Notes
        -----
        """

        cls,
        X,
        Y,
        k,
    ):

        k : int

        Returns
        -------

        Notes
        -----

        returns.
        """
                X=X,
                Y=Y,
                k=k,
            )

                X=X,
                Y=Y,
                k=k,
            )

        )

        cls,
        X,
        Y,
        radius,
    ):

        Returns
        -------

        Notes
        -----

        returns.
        """
                X=X,
                Y=Y,
            )

                X=X,
                Y=Y,
            )

        )

        }

        cls,
        X,
        Y,
        k,
        weights,
        Y_labels,
    ):

        k : int

        Returns
        -------

        Notes
        -----

        returns.
        """
            )
                X=X,
                Y=Y,
                k=k,
            )

                X=X,
                Y=Y,
                k=k,
            )

        )

        }

        cls,
        X,
        Y,
        radius,
        weights,
        Y_labels,
    ):
        Returns
        -------
        """
            )
                X=X,
                Y=Y,
            )

                X=X,
                Y=Y,
            )

        )

            return (
                and
                X.nnz > 0
                and
            )

class RocCurveDisplay(_BinaryClassifierCurveDisplayMixin):
    """ROC Curve visualization.

    It is recommend to use
    :func:`~sklearn.metrics.RocCurveDisplay.from_estimator` or
    :func:`~sklearn.metrics.RocCurveDisplay.from_predictions` to create
    a :class:`~sklearn.metrics.RocCurveDisplay`. All parameters are
    stored as attributes.

    For general information regarding `scikit-learn` visualization tools, see
    the :ref:`Visualization Guide <visualizations>`.
    For guidance on interpreting these plots, refer to the :ref:`Model
    Evaluation Guide <roc_metrics>`.

    fpr : ndarray
        False positive rate.

    tpr : ndarray
        True positive rate.

    roc_auc : float, default=None
        Area under ROC curve. If None, the roc_auc score is not shown.

        The class considered as the positive class when computing the roc auc
        metrics. By default, `estimators.classes_[1]` is considered
        as the positive class.

        ROC Curve.

    chance_level_ : matplotlib Artist or None
        The chance level line. It is `None` if the chance level is not plotted.

        Axes with ROC Curve.

    See Also
    --------

    Examples
    --------
    >>> import matplotlib.pyplot as plt
    >>> fpr, tpr, thresholds = metrics.roc_curve(y_true, y_score)
    >>> roc_auc = metrics.auc(fpr, tpr)
    >>> display = metrics.RocCurveDisplay(fpr=fpr, tpr=tpr, roc_auc=roc_auc,
    ...                                   estimator_name='example estimator')
    >>> display.plot()
    <...>
    >>> plt.show()
    """

    def __init__(self, *, fpr, tpr, roc_auc=None, estimator_name=None, pos_label=None):
        self.fpr = fpr
        self.tpr = tpr
        self.roc_auc = roc_auc

    def plot(
        self,
        ax=None,
        *,
        plot_chance_level=False,
        chance_level_kw=None,
        despine=False,
        **kwargs,
    ):

        Extra keyword arguments will be passed to matplotlib's ``plot``.

            created.

            Name of ROC Curve for labeling. If `None`, use `estimator_name` if

        plot_chance_level : bool, default=False
            Whether to plot the chance level.

        chance_level_kw : dict, default=None
            Keyword arguments to be passed to matplotlib's `plot` for rendering
            the chance level line.

        despine : bool, default=False
            Whether to remove the top and right spines from the plot.

            Keyword arguments to be passed to matplotlib's `plot`.

        Returns
        -------
        display : :class:`~sklearn.metrics.RocCurveDisplay`
        """

        default_line_kwargs = {}
        if self.roc_auc is not None and name is not None:
            default_line_kwargs["label"] = f"{name} (AUC = {self.roc_auc:0.2f})"
        elif self.roc_auc is not None:
            default_line_kwargs["label"] = f"AUC = {self.roc_auc:0.2f}"
        elif name is not None:

        default_chance_level_line_kw = {
            "label": "Chance level (AUC = 0.5)",
            "color": "k",
            "linestyle": "--",
        }

        if chance_level_kw is None:
            chance_level_kw = {}

        chance_level_kw = _validate_style_kwargs(
            default_chance_level_line_kw, chance_level_kw
        )

        (self.line_,) = self.ax_.plot(self.fpr, self.tpr, **line_kwargs)
            f" (Positive label: {self.pos_label})" if self.pos_label is not None else ""
        )

        xlabel = "False Positive Rate" + info_pos_label
        ylabel = "True Positive Rate" + info_pos_label
        self.ax_.set(
            xlabel=xlabel,
            xlim=(-0.01, 1.01),
            ylabel=ylabel,
            ylim=(-0.01, 1.01),
            aspect="equal",
        )

        if plot_chance_level:
            (self.chance_level_,) = self.ax_.plot((0, 1), (0, 1), **chance_level_kw)
        else:
            self.chance_level_ = None

        if despine:
            _despine(self.ax_)

        if (
            line_kwargs.get("label") is not None
            or chance_level_kw.get("label") is not None
        ):
            self.ax_.legend(loc="lower right")

        cls,
        X,
        y,
        *,
        drop_intermediate=True,
        response_method="auto",
        ax=None,
        **kwargs,
    ):
        """Create a ROC Curve display from an estimator.

        For general information regarding `scikit-learn` visualization tools,
        see the :ref:`Visualization Guide <visualizations>`.
        For guidance on interpreting these plots, refer to the :ref:`Model
        Evaluation Guide <roc_metrics>`.

            in which the last estimator is a classifier.

        drop_intermediate : bool, default=True
            Whether to drop thresholds where the resulting point is collinear
            with its neighbors in ROC space. This has no effect on the ROC AUC
            or visual shape of the curve, but reduces the number of plotted
            points.

        response_method : {'predict_proba', 'decision_function', 'auto'} \
                default='auto'
            Specifies whether to use :term:`predict_proba` or
            :term:`decision_function` as the target response. If set to 'auto',
            :term:`predict_proba` is tried first and if it does not exist
            :term:`decision_function` is tried next.

            The class considered as the positive class when computing the roc auc
            metrics. By default, `estimators.classes_[1]` is considered
            as the positive class.

            Name of ROC Curve for labeling. If `None`, use the name of the

            Axes object to plot on. If `None`, a new figure and axes is created.

        Returns
        -------
            The ROC Curve display.

        See Also
        --------
        roc_curve : Compute Receiver operating characteristic (ROC) curve.
        RocCurveDisplay.from_predictions : ROC Curve visualization given the
            probabilities of scores of a classifier.
        roc_auc_score : Compute the area under the ROC curve.

        Examples
        --------
        >>> from sklearn.metrics import RocCurveDisplay
        >>> from sklearn.svm import SVC
        >>> clf = SVC(random_state=0).fit(X_train, y_train)
        >>> RocCurveDisplay.from_estimator(
        ...    clf, X_test, y_test)
        <...>
        """
        y_score, pos_label, name = cls._validate_and_get_response_values(
            X,
            y,
        )

            y_true=y,
            y_score=y_score,
            drop_intermediate=drop_intermediate,
            ax=ax,
            plot_chance_level=plot_chance_level,
            chance_level_kw=chance_level_kw,
            despine=despine,
            **kwargs,
        )

        cls,
        y_true,
        y_score=None,
        *,
        ax=None,
        y_pred="deprecated",
        **kwargs,
    ):
        """Plot ROC curve given the true and predicted values.

        y_score : array-like of shape (n_samples,)
            Target scores, can either be probability estimates of the positive
            class, confidence values, or non-thresholded measure of decisions
            (as returned by “decision_function” on some classifiers).

                `y_pred` has been renamed to `y_score`.

            points.

            The label of the positive class. When `pos_label=None`, if `y_true`
            is in {-1, 1} or {0, 1}, `pos_label` is set to 1, otherwise an
            error will be raised.

            Name of ROC curve for labeling. If `None`, name will be set to
            `"Classifier"`.

            created.

        y_pred : array-like of shape (n_samples,)

            .. deprecated:: 1.7
                `y_pred` is deprecated and will be removed in 1.9. Use
                `y_score` instead.

            Additional keywords arguments passed to matplotlib `plot` function.

        Returns
        -------

        See Also
        --------
        RocCurveDisplay.from_estimator : ROC Curve visualization given an
            estimator and some data.

        Examples
        --------
        >>> y_score = clf.decision_function(X_test)
        >>> RocCurveDisplay.from_predictions(y_test, y_score)
        <...>
        """
        # TODO(1.9): remove after the end of the deprecation period of `y_pred`
        if y_score is not None and not (
            isinstance(y_pred, str) and y_pred == "deprecated"
        ):
                "`y_pred` and `y_score` cannot be both specified. Please use `y_score`"
                " only as `y_pred` is deprecated in 1.7 and will be removed in 1.9."
            )
        if not (isinstance(y_pred, str) and y_pred == "deprecated"):
                (
                    "y_pred is deprecated in 1.7 and will be removed in 1.9. "
                    "Please use `y_score` instead."
                ),
            )
            y_score = y_pred

            y_true, y_score, sample_weight=sample_weight, pos_label=pos_label, name=name
        )

        fpr, tpr, _ = roc_curve(
            y_true,
            y_score,
        )
        roc_auc = auc(fpr, tpr)

        viz = cls(
            fpr=fpr,
            tpr=tpr,
            roc_auc=roc_auc,
        )

        return viz.plot(
            ax=ax,
            **kwargs,
        )

    def plot(
        self,
        ax=None,
        *,
        **kwargs,
    ):

            created.

        Returns
        -------
        """

        }

        )

        )

        )

        else:

        if (
        ):

        cls,
        X,
        y,
        *,
        ax=None,
        **kwargs,
    ):

            points.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        <...>
        """
            X,
            y,
        )

            y_true=y,
            ax=ax,
            **kwargs,
        )

        cls,
        y_true,
        *,
        ax=None,
        **kwargs,
    ):

            points.

            created.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        <...>
        """
        ):
            )
                (
                ),
            )

        )

            y_true,
            y_score,
        )

        viz = cls(
            fpr=fpr,
            tpr=tpr,
        )

            ax=ax,
            **kwargs,
        )

class DetCurveDisplay(_BinaryClassifierCurveDisplayMixin):
    """Detection Error Tradeoff (DET) curve visualization.

    It is recommend to use :func:`~sklearn.metrics.DetCurveDisplay.from_estimator`
    or :func:`~sklearn.metrics.DetCurveDisplay.from_predictions` to create a
    visualizer. All parameters are stored as attributes.

    fnr : ndarray
        False negative rate.

        DET Curve.

        Axes with DET Curve.

    See Also
    --------
    det_curve : Compute error rates for different probability thresholds.
        some data.

    Examples
    --------
    >>> from sklearn.metrics import det_curve, DetCurveDisplay
    >>> X, y = make_classification(n_samples=1000, random_state=0)
    ...     X, y, test_size=0.4, random_state=0)
    >>> clf = SVC(random_state=0).fit(X_train, y_train)
    >>> y_pred = clf.decision_function(X_test)
    >>> fpr, fnr, _ = det_curve(y_test, y_pred)
    >>> display = DetCurveDisplay(
    ...     fpr=fpr, fnr=fnr, estimator_name="SVC"
    ... )
    <...>
    """

    def __init__(self, *, fpr, fnr, estimator_name=None, pos_label=None):
        self.fnr = fnr

        cls,
        X,
        y,
        *,
        ax=None,
        **kwargs,
    ):
        """Plot DET curve given an estimator and data.

        Read more in the :ref:`User Guide <det_curve>`.

            Whether to drop thresholds where true positives (tp) do not change
            from the previous or subsequent threshold. All points with the same
            tp value have the same `fnr` and thus same y coordinate.

            :term:`decision_function` as the predicted target response. If set
            to 'auto', :term:`predict_proba` is tried first and if it does not
            exist :term:`decision_function` is tried next.

            Name of DET curve for labeling. If `None`, use the name of the

            created.

        Returns
        -------
        display : :class:`~sklearn.metrics.DetCurveDisplay`

        See Also
        --------
        det_curve : Compute error rates for different probability thresholds.
        DetCurveDisplay.from_predictions : Plot DET curve given the true and
            predicted labels.

        Examples
        --------
        >>> from sklearn.metrics import DetCurveDisplay
        >>> X, y = make_classification(n_samples=1000, random_state=0)
        ...     X, y, test_size=0.4, random_state=0)
        >>> DetCurveDisplay.from_estimator(
        <...>
        """
        y_pred, pos_label, name = cls._validate_and_get_response_values(
            X,
            y,
        )

            y_true=y,
            y_pred=y_pred,
            ax=ax,
            **kwargs,
        )

        cls,
        y_true,
        y_pred,
        *,
        ax=None,
        **kwargs,
    ):
        """Plot the DET curve given the true and predicted labels.

            (as returned by `decision_function` on some classifiers).

            Name of DET curve for labeling. If `None`, name will be set to

            created.

        Returns
        -------

        See Also
        --------
        DetCurveDisplay.from_estimator : Plot DET curve given an estimator and
            some data.

        Examples
        --------
        >>> y_pred = clf.decision_function(X_test)
        >>> DetCurveDisplay.from_predictions(
        ...    y_test, y_pred)
        <...>
        """
            y_true, y_pred, sample_weight=sample_weight, pos_label=pos_label, name=name
        )

        fpr, fnr, _ = det_curve(
            y_true,
            y_pred,
        )

        viz = cls(
            fpr=fpr,
            fnr=fnr,
        )

        return viz.plot(ax=ax, name=name, **kwargs)

    def plot(self, ax=None, *, name=None, **kwargs):

            created.

            Name of DET curve for labeling. If `None`, use `estimator_name` if
            it is not `None`, otherwise no labeling is shown.

        Returns
        -------
        """

        line_kwargs = {} if name is None else {"label": name}
        line_kwargs.update(**kwargs)

        # We have the following bounds:
        # sp.stats.norm.ppf(0.0) = -np.inf
        # sp.stats.norm.ppf(1.0) = np.inf
        # We therefore clip to eps and 1 - eps to not provide infinity to matplotlib.
        eps = np.finfo(self.fpr.dtype).eps
        self.fpr = self.fpr.clip(eps, 1 - eps)
        self.fnr = self.fnr.clip(eps, 1 - eps)

        (self.line_,) = self.ax_.plot(
            sp.stats.norm.ppf(self.fpr),
            sp.stats.norm.ppf(self.fnr),
            **line_kwargs,
        )
        )

        ylabel = "False Negative Rate" + info_pos_label

        if "label" in line_kwargs:

        ticks = [0.001, 0.01, 0.05, 0.20, 0.5, 0.80, 0.95, 0.99, 0.999]
        tick_locations = sp.stats.norm.ppf(ticks)
        tick_labels = [
            "{:.0%}".format(s) if (100 * s).is_integer() else "{:.1%}".format(s)
            for s in ticks
        ]
        self.ax_.set_xticks(tick_locations)
        self.ax_.set_xticklabels(tick_labels)
        self.ax_.set_xlim(-3, 3)
        self.ax_.set_yticks(tick_locations)
        self.ax_.set_yticklabels(tick_labels)
        self.ax_.set_ylim(-3, 3)

        cls,
        X,
        y,
        *,
        ax=None,
        **kwargs,
    ):

            created.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        <...>
        """
            X,
            y,
        )

            y_true=y,
            ax=ax,
            **kwargs,
        )

        cls,
        y_true,
        y_pred,
        *,
        ax=None,
        **kwargs,
    ):

            created.

        Returns
        -------

        See Also
        --------
            some data.

        Examples
        --------
        <...>
        """
        )

            y_true,
            y_pred,
        )

        viz = cls(
            fpr=fpr,
            fnr=fnr,
        )

            created.

        Returns
        -------
        """

        )
        )

        ]

class PredictionErrorDisplay:
    """Visualization of the prediction error of a regression model.

    This tool can display "residuals vs predicted" or "actual vs predicted"
    using scatter plots to qualitatively assess the behavior of a regressor,
    preferably on held-out data points.

    See the details in the docstrings of
    :func:`~sklearn.metrics.PredictionErrorDisplay.from_estimator` or
    :func:`~sklearn.metrics.PredictionErrorDisplay.from_predictions` to
    create a visualizer. All parameters are stored as attributes.

    For general information regarding `scikit-learn` visualization tools, read
    more in the :ref:`Visualization Guide <visualizations>`.
    For details regarding interpreting these plots, refer to the
    :ref:`Model Evaluation Guide <visualization_regression_evaluation>`.

    .. versionadded:: 1.2

        True values.

    y_pred : ndarray of shape (n_samples,)
        Prediction values.

        Optimal line representing `y_true == y_pred`. Therefore, it is a
        diagonal line for `kind="predictions"` and a horizontal line for
        `kind="residuals"`.

    errors_lines_ : matplotlib Artist or None
        Residual lines. If `with_errors=False`, then it is set to `None`.

    scatter_ : matplotlib Artist
        Scatter data points.

        Axes with the different matplotlib axis.

        Figure containing the scatter and lines.

    See Also
    --------
    PredictionErrorDisplay.from_estimator : Prediction error visualization
        given an estimator and some data.
    PredictionErrorDisplay.from_predictions : Prediction error visualization
        given the true and predicted targets.

    Examples
    --------
    >>> from sklearn.metrics import PredictionErrorDisplay
    >>> ridge = Ridge().fit(X, y)
    >>> y_pred = ridge.predict(X)
    >>> display = PredictionErrorDisplay(y_true=y, y_pred=y_pred)
    <...>
    """

    def __init__(self, *, y_true, y_pred):
        self.y_true = y_true
        self.y_pred = y_pred

    def plot(
        self,
        ax=None,
        *,
        kind="residual_vs_predicted",
        scatter_kwargs=None,
        line_kwargs=None,
    ):

            created.

        kind : {"actual_vs_predicted", "residual_vs_predicted"}, \
                default="residual_vs_predicted"
            The type of plot to draw:

            - "actual_vs_predicted" draws the observed values (y-axis) vs.
              the predicted values (x-axis).
            - "residual_vs_predicted" draws the residuals, i.e. difference
              between observed and predicted values, (y-axis) vs. the predicted
              values (x-axis).

        scatter_kwargs : dict, default=None
            Dictionary with keywords passed to the `matplotlib.pyplot.scatter`
            call.

        line_kwargs : dict, default=None
            Dictionary with keyword passed to the `matplotlib.pyplot.plot`
            call to draw the optimal line.

        Returns
        -------
        display : :class:`~sklearn.metrics.PredictionErrorDisplay`

        """
        check_matplotlib_support(f"{self.__class__.__name__}.plot")

        expected_kind = ("actual_vs_predicted", "residual_vs_predicted")
        if kind not in expected_kind:
                f"`kind` must be one of {', '.join(expected_kind)}. "
                f"Got {kind!r} instead."
            )

        import matplotlib.pyplot as plt

        if scatter_kwargs is None:
            scatter_kwargs = {}
        if line_kwargs is None:
            line_kwargs = {}

        default_scatter_kwargs = {"color": "tab:blue", "alpha": 0.8}
        default_line_kwargs = {"color": "black", "alpha": 0.7, "linestyle": "--"}

        scatter_kwargs = _validate_style_kwargs(default_scatter_kwargs, scatter_kwargs)
        line_kwargs = _validate_style_kwargs(default_line_kwargs, line_kwargs)

        scatter_kwargs = {**default_scatter_kwargs, **scatter_kwargs}
        line_kwargs = {**default_line_kwargs, **line_kwargs}

        if ax is None:
            _, ax = plt.subplots()

        if kind == "actual_vs_predicted":
            max_value = max(np.max(self.y_true), np.max(self.y_pred))
            min_value = min(np.min(self.y_true), np.min(self.y_pred))
            self.line_ = ax.plot(
                [min_value, max_value], [min_value, max_value], **line_kwargs
            )[0]

            x_data, y_data = self.y_pred, self.y_true
            xlabel, ylabel = "Predicted values", "Actual values"

            self.scatter_ = ax.scatter(x_data, y_data, **scatter_kwargs)

            # force to have a squared axis
            ax.set_aspect("equal", adjustable="datalim")
            ax.set_xticks(np.linspace(min_value, max_value, num=5))
            ax.set_yticks(np.linspace(min_value, max_value, num=5))
        else:  # kind == "residual_vs_predicted"
                [np.min(self.y_pred), np.max(self.y_pred)],
                [0, 0],
                **line_kwargs,
            )[0]
            self.scatter_ = ax.scatter(
                self.y_pred, self.y_true - self.y_pred, **scatter_kwargs
            )
            xlabel, ylabel = "Predicted values", "Residuals (actual - predicted)"

        ax.set(xlabel=xlabel, ylabel=ylabel)

        self.ax_ = ax
        self.figure_ = ax.figure

        cls,
        X,
        y,
        *,
        subsample=1_000,
        ax=None,
    ):
        """Plot the prediction error given a regressor and some data.

        read more in the :ref:`Visualization Guide <visualizations>`.
        For details regarding interpreting these plots, refer to the
        :ref:`Model Evaluation Guide <visualization_regression_evaluation>`.

            Fitted regressor or a fitted :class:`~sklearn.pipeline.Pipeline`
            in which the last estimator is a regressor.

        subsample : float, int or None, default=1_000
            Sampling the samples to be shown on the scatter plot. If `float`,
            it should be between 0 and 1 and represents the proportion of the
            original dataset. If `int`, it represents the number of samples
            display on the scatter plot. If `None`, no subsampling will be
            applied. by default, 1000 samples or less will be displayed.

        random_state : int or RandomState, default=None
            Controls the randomness when `subsample` is not `None`.
            See :term:`Glossary <random_state>` for details.

            created.

            call.

        Returns
        -------
            Object that stores the computed values.

        See Also
        --------
        PredictionErrorDisplay : Prediction error visualization for regression.
        PredictionErrorDisplay.from_predictions : Prediction error visualization
            given the true and predicted targets.

        Examples
        --------
        >>> from sklearn.datasets import load_diabetes
        >>> from sklearn.linear_model import Ridge
        >>> from sklearn.metrics import PredictionErrorDisplay
        >>> X, y = load_diabetes(return_X_y=True)
        >>> ridge = Ridge().fit(X, y)
        >>> disp = PredictionErrorDisplay.from_estimator(ridge, X, y)
        """
        check_matplotlib_support(f"{cls.__name__}.from_estimator")

        y_pred = estimator.predict(X)

            y_true=y,
            kind=kind,
            ax=ax,
            scatter_kwargs=scatter_kwargs,
            line_kwargs=line_kwargs,
        )

        cls,
        y_true,
        y_pred,
        *,
        ax=None,
    ):
        """Plot the prediction error given the true and predicted targets.

            True target values.

            created.

            call.

        Returns
        -------

        See Also
        --------
        PredictionErrorDisplay.from_estimator : Prediction error visualization
            given an estimator and some data.

        Examples
        --------
        >>> y_pred = ridge.predict(X)
        >>> disp = PredictionErrorDisplay.from_predictions(y_true=y, y_pred=y_pred)
        """
        check_matplotlib_support(f"{cls.__name__}.from_predictions")

        n_samples = len(y_true)
        if isinstance(subsample, numbers.Integral):
            if subsample <= 0:
                    f"When an integer, subsample={subsample} should be positive."
                )
        elif isinstance(subsample, numbers.Real):
            if subsample <= 0 or subsample >= 1:
                    f"When a floating-point, subsample={subsample} should"
                    " be in the (0, 1) range."
                )
            subsample = int(n_samples * subsample)

        if subsample is not None and subsample < n_samples:
            indices = random_state.choice(np.arange(n_samples), size=subsample)
            y_true = _safe_indexing(y_true, indices, axis=0)
            y_pred = _safe_indexing(y_pred, indices, axis=0)

        viz = cls(
        )

            ax=ax,
        )

    def plot(
        self,
        ax=None,
        *,
    ):

            created.

            call.

        Returns
        -------

        """

            )

            )[0]

                [0, 0],
            )[0]
            )

        cls,
        X,
        y,
        *,
        ax=None,
    ):

            created.

            call.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        """

            y_true=y,
            ax=ax,
        )

        cls,
        y_true,
        y_pred,
        *,
        ax=None,
    ):

            created.

            call.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        """

                )
                )

        viz = cls(
        )

            ax=ax,
        )

class ConfusionMatrixDisplay:
    """Confusion Matrix visualization.

    :func:`~sklearn.metrics.ConfusionMatrixDisplay.from_estimator` or
    :func:`~sklearn.metrics.ConfusionMatrixDisplay.from_predictions` to
    create a :class:`ConfusionMatrixDisplay`. All parameters are stored as
    attributes.

    Read more in the :ref:`User Guide <visualizations>`.

    confusion_matrix : ndarray of shape (n_classes, n_classes)
        Confusion matrix.

    display_labels : ndarray of shape (n_classes,), default=None
        Display labels for plot. If None, display labels are set from 0 to
        `n_classes - 1`.

    im_ : matplotlib AxesImage
        Image representing the confusion matrix.

    text_ : ndarray of shape (n_classes, n_classes), dtype=matplotlib Text, \
            or None
        Array of matplotlib axes. `None` if `include_values` is false.

        Axes with confusion matrix.

        Figure containing the confusion matrix.

    See Also
    --------
    confusion_matrix : Compute Confusion Matrix to evaluate the accuracy of a

    Examples
    --------
    >>> from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
    >>> clf = SVC(random_state=0)
    SVC(random_state=0)
    >>> predictions = clf.predict(X_test)
    >>> cm = confusion_matrix(y_test, predictions, labels=clf.classes_)
    >>> disp = ConfusionMatrixDisplay(confusion_matrix=cm,
    ...                               display_labels=clf.classes_)
    <...>
    """

    def __init__(self, confusion_matrix, *, display_labels=None):
        self.confusion_matrix = confusion_matrix
        self.display_labels = display_labels

    def plot(
        self,
        *,
        include_values=True,
        cmap="viridis",
        xticks_rotation="horizontal",
        values_format=None,
        ax=None,
        colorbar=True,
        im_kw=None,
        text_kw=None,
    ):

        include_values : bool, default=True
            Includes values in confusion matrix.

        cmap : str or matplotlib Colormap, default='viridis'
            Colormap recognized by matplotlib.

        xticks_rotation : {'vertical', 'horizontal'} or float, \
                         default='horizontal'
            Rotation of xtick labels.

        values_format : str, default=None
            Format specification for values in confusion matrix. If `None`,
            the format specification is 'd' or '.2g' whichever is shorter.

            created.

        colorbar : bool, default=True
            Whether or not to add a colorbar to the plot.

        im_kw : dict, default=None
            Dict with keywords passed to `matplotlib.pyplot.imshow` call.

        text_kw : dict, default=None
            Dict with keywords passed to `matplotlib.pyplot.text` call.

            .. versionadded:: 1.2

        Returns
        -------
        display : :class:`~sklearn.metrics.ConfusionMatrixDisplay`
            Returns a :class:`~sklearn.metrics.ConfusionMatrixDisplay` instance
            that contains all the information to plot the confusion matrix.
        """
        check_matplotlib_support("ConfusionMatrixDisplay.plot")

            fig, ax = plt.subplots()
        else:
            fig = ax.figure

        cm = self.confusion_matrix
        n_classes = cm.shape[0]

        default_im_kw = dict(interpolation="nearest", cmap=cmap)
        im_kw = im_kw or {}
        im_kw = _validate_style_kwargs(default_im_kw, im_kw)
        text_kw = text_kw or {}

        self.im_ = ax.imshow(cm, **im_kw)
        self.text_ = None
        cmap_min, cmap_max = self.im_.cmap(0), self.im_.cmap(1.0)

        if include_values:
            self.text_ = np.empty_like(cm, dtype=object)

            # print text with appropriate color depending on background
            thresh = (cm.max() + cm.min()) / 2.0

            for i, j in product(range(n_classes), range(n_classes)):
                color = cmap_max if cm[i, j] < thresh else cmap_min

                if values_format is None:
                    text_cm = format(cm[i, j], ".2g")
                    if cm.dtype.kind != "f":
                        text_d = format(cm[i, j], "d")
                        if len(text_d) < len(text_cm):
                            text_cm = text_d
                else:
                    text_cm = format(cm[i, j], values_format)

                default_text_kwargs = dict(ha="center", va="center", color=color)
                text_kwargs = _validate_style_kwargs(default_text_kwargs, text_kw)

                self.text_[i, j] = ax.text(j, i, text_cm, **text_kwargs)

        if self.display_labels is None:
            display_labels = np.arange(n_classes)
        else:
            display_labels = self.display_labels
        if colorbar:
            fig.colorbar(self.im_, ax=ax)
        ax.set(
            xticks=np.arange(n_classes),
            yticks=np.arange(n_classes),
            xticklabels=display_labels,
            yticklabels=display_labels,
            ylabel="True label",
            xlabel="Predicted label",
        )

        ax.set_ylim((n_classes - 0.5, -0.5))
        plt.setp(ax.get_xticklabels(), rotation=xticks_rotation)

        self.figure_ = fig

        cls,
        X,
        y,
        *,
        labels=None,
        normalize=None,
        display_labels=None,
        ax=None,
    ):
        """Plot Confusion Matrix given an estimator and some data.

        Read more in the :ref:`User Guide <confusion_matrix>`.

        labels : array-like of shape (n_classes,), default=None
            List of labels to index the confusion matrix. This may be used to
            reorder or select a subset of labels. If `None` is given, those
            that appear at least once in `y_true` or `y_pred` are used in
            sorted order.

        normalize : {'true', 'pred', 'all'}, default=None
            Either to normalize the counts display in the matrix:

            - if `'true'`, the confusion matrix is normalized over the true
              conditions (e.g. rows);
            - if `'pred'`, the confusion matrix is normalized over the
              predicted conditions (e.g. columns);
            - if `'all'`, the confusion matrix is normalized by the total
              number of samples;
            - if `None` (default), the confusion matrix will not be normalized.

        display_labels : array-like of shape (n_classes,), default=None
            Target names used for plotting. By default, `labels` will be used
            if it is defined, otherwise the unique labels of `y_true` and
            `y_pred` will be used.

                default='horizontal'

            Format specification for values in confusion matrix. If `None`, the
            format specification is 'd' or '.2g' whichever is shorter.

        ax : matplotlib Axes, default=None
            created.

        Returns
        -------

        See Also
        --------
        ConfusionMatrixDisplay.from_predictions : Plot the confusion matrix
            given the true and predicted labels.

        Examples
        --------
        >>> from sklearn.metrics import ConfusionMatrixDisplay
        ...         X, y, random_state=0)
        >>> clf = SVC(random_state=0)
        SVC(random_state=0)
        >>> ConfusionMatrixDisplay.from_estimator(
        ...     clf, X_test, y_test)
        <...>

        For a detailed example of using a confusion matrix to evaluate a
        Support Vector Classifier, please see
        :ref:`sphx_glr_auto_examples_model_selection_plot_confusion_matrix.py`
        """
        method_name = f"{cls.__name__}.from_estimator"
        check_matplotlib_support(method_name)
        if not is_classifier(estimator):
            raise ValueError(f"{method_name} only supports classifiers")

            y,
            y_pred,
            normalize=normalize,
            display_labels=display_labels,
            include_values=include_values,
            cmap=cmap,
            ax=ax,
            xticks_rotation=xticks_rotation,
            values_format=values_format,
            colorbar=colorbar,
            im_kw=im_kw,
            text_kw=text_kw,
        )

        cls,
        y_true,
        y_pred,
        *,
        ax=None,
    ):
        """Plot Confusion Matrix given true and predicted labels.

            The predicted labels given by the method `predict` of an

            created.

        Returns
        -------

        See Also
        --------
        ConfusionMatrixDisplay.from_estimator : Plot the confusion matrix
            given an estimator, the data, and the label.

        Examples
        --------
        >>> y_pred = clf.predict(X_test)
        >>> ConfusionMatrixDisplay.from_predictions(
        <...>
        """

        if display_labels is None:
                display_labels = unique_labels(y_true, y_pred)
            else:
                display_labels = labels

        cm = confusion_matrix(
            y_true,
            y_pred,
        )

        disp = cls(confusion_matrix=cm, display_labels=display_labels)

        return disp.plot(
            ax=ax,
        )

    def plot(
        self,
        *,
        ax=None,
    ):

            created.

        Returns
        -------
        """

        else:

                else:

        else:
        ax.set(
        )

        cls,
        X,
        y,
        *,
        ax=None,
    ):

            created.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        <...>

        """

            y,
            y_pred,
            ax=ax,
        )

        cls,
        y_true,
        y_pred,
        *,
        ax=None,
    ):

            created.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        <...>
        """

            else:

            y_true,
            y_pred,
        )

            ax=ax,
        )

class PrecisionRecallDisplay(_BinaryClassifierCurveDisplayMixin):
    """Precision Recall visualization.

    :func:`~sklearn.metrics.PrecisionRecallDisplay.from_estimator` or
    :func:`~sklearn.metrics.PrecisionRecallDisplay.from_predictions` to create
    a :class:`~sklearn.metrics.PrecisionRecallDisplay`. All parameters are

    precision : ndarray
        Precision values.

    recall : ndarray
        Recall values.

    average_precision : float, default=None
        Average precision. If None, the average precision is not shown.

        Name of estimator. If None, then the estimator name is not shown.

        The class considered as the positive class. If None, the class will not
        be shown in the legend.

    prevalence_pos_label : float, default=None
        The prevalence of the positive label. It is used for plotting the
        chance level line. If None, the chance level line will not be plotted
        even if `plot_chance_level` is set to True when plotting.

        Precision recall curve.

        Axes with precision recall curve.

    See Also
    --------

    Notes
    -----
    The average precision (cf. :func:`~sklearn.metrics.average_precision_score`) in
    scikit-learn is computed without any interpolation. To be consistent with
    this metric, the precision-recall curve is plotted without any
    interpolation as well (step-wise style).

    You can change this style by passing the keyword argument
    `drawstyle="default"` in :meth:`plot`, :meth:`from_estimator`, or
    :meth:`from_predictions`. However, the curve will not be strictly
    consistent with the reported average precision.

    Examples
    --------
    >>> from sklearn.metrics import (precision_recall_curve,
    ...                              PrecisionRecallDisplay)
    >>> precision, recall, _ = precision_recall_curve(y_test, predictions)
    >>> disp = PrecisionRecallDisplay(precision=precision, recall=recall)
    <...>
    """

        self,
        precision,
        recall,
        *,
        average_precision=None,
        estimator_name=None,
        prevalence_pos_label=None,
    ):
        self.precision = precision
        self.recall = recall
        self.average_precision = average_precision
        self.prevalence_pos_label = prevalence_pos_label

    def plot(
        self,
        ax=None,
        *,
        **kwargs,
    ):

        Extra keyword arguments will be passed to matplotlib's `plot`.

            created.

            Name of precision recall curve for labeling. If `None`, use
            `estimator_name` if not `None`, otherwise no labeling is shown.

            Whether to plot the chance level. The chance level is the prevalence
            of the positive label computed from the data passed during
            :meth:`from_estimator` or :meth:`from_predictions` call.

        Returns
        -------
        display : :class:`~sklearn.metrics.PrecisionRecallDisplay`

        Notes
        -----
        The average precision (cf. :func:`~sklearn.metrics.average_precision_score`)
        in scikit-learn is computed without any interpolation. To be consistent
        with this metric, the precision-recall curve is plotted without any
        interpolation as well (step-wise style).

        You can change this style by passing the keyword argument
        `drawstyle="default"`. However, the curve will not be strictly
        consistent with the reported average precision.
        """

        default_line_kwargs = {"drawstyle": "steps-post"}
        if self.average_precision is not None and name is not None:
            default_line_kwargs["label"] = (
                f"{name} (AP = {self.average_precision:0.2f})"
            )
        elif self.average_precision is not None:
            default_line_kwargs["label"] = f"AP = {self.average_precision:0.2f}"

        (self.line_,) = self.ax_.plot(self.recall, self.precision, **line_kwargs)

        )

        xlabel = "Recall" + info_pos_label
        ylabel = "Precision" + info_pos_label
        )

            if self.prevalence_pos_label is None:
                    "You must provide prevalence_pos_label when constructing the "
                    "PrecisionRecallDisplay object in order to plot the chance "
                    "level line. Alternatively, you may use "
                    "PrecisionRecallDisplay.from_estimator or "
                    "PrecisionRecallDisplay.from_predictions "
                    "to automatically set prevalence_pos_label"
                )

            default_chance_level_line_kw = {
                "label": f"Chance level (AP = {self.prevalence_pos_label:0.2f})",
                "color": "k",
                "linestyle": "--",
            }

            if chance_level_kw is None:
                chance_level_kw = {}

            chance_level_line_kw = _validate_style_kwargs(
                default_chance_level_line_kw, chance_level_kw
            )

            (self.chance_level_,) = self.ax_.plot(
                (0, 1),
                (self.prevalence_pos_label, self.prevalence_pos_label),
                **chance_level_line_kw,
            )
        else:

        if "label" in line_kwargs or plot_chance_level:
            self.ax_.legend(loc="lower left")

        cls,
        X,
        y,
        *,
        drop_intermediate=False,
        ax=None,
        **kwargs,
    ):
        """Plot precision-recall curve given an estimator and some data.

        drop_intermediate : bool, default=False
            Whether to drop some suboptimal thresholds which would not appear
            on a plotted precision-recall curve. This is useful in order to
            create lighter precision-recall curves.

        response_method : {'predict_proba', 'decision_function', 'auto'}, \

            The class considered as the positive class when computing the
            precision and recall metrics. By default, `estimators.classes_[1]`
            is considered as the positive class.

            Name for labeling curve. If `None`, no name is used.

        Returns
        -------

        See Also
        --------
        PrecisionRecallDisplay.from_predictions : Plot precision-recall curve
            using estimated probabilities or output of decision function.

        Notes
        -----

        Examples
        --------
        >>> from sklearn.metrics import PrecisionRecallDisplay
        >>> clf = LogisticRegression()
        LogisticRegression()
        >>> PrecisionRecallDisplay.from_estimator(
        <...>
        """
            X,
            y,
        )

            y,
            y_pred,
            ax=ax,
            **kwargs,
        )

        cls,
        y_true,
        y_pred,
        *,
        ax=None,
        **kwargs,
    ):
        """Plot precision-recall curve given binary class predictions.

            True binary labels.

            Estimated probabilities or output of decision function.

            precision and recall metrics.

            Name for labeling curve. If `None`, name will be set to

        Returns
        -------

        See Also
        --------
        PrecisionRecallDisplay.from_estimator : Plot precision-recall curve
            using an estimator.

        Notes
        -----

        Examples
        --------
        >>> y_pred = clf.predict_proba(X_test)[:, 1]
        >>> PrecisionRecallDisplay.from_predictions(
        <...>
        """
        pos_label, name = cls._validate_from_predictions_params(
        )

            y_true,
            y_pred,
        )
        average_precision = average_precision_score(
            y_true, y_pred, pos_label=pos_label, sample_weight=sample_weight
        )

        class_count = Counter(y_true)
        prevalence_pos_label = class_count[pos_label] / sum(class_count.values())

        viz = cls(
            precision=precision,
            recall=recall,
            average_precision=average_precision,
            prevalence_pos_label=prevalence_pos_label,
        )

            ax=ax,
            **kwargs,
        )

        self,
        recall,
        *,
    ):

    def plot(
        self,
        ax=None,
        *,
        **kwargs,
    ):

            created.

        Returns
        -------

        Notes
        -----

        """

            )

        )

        )

                )

            }

            )

                (0, 1),
            )
        else:

        cls,
        X,
        y,
        *,
        ax=None,
        **kwargs,
    ):

        Returns
        -------

        See Also
        --------

        Notes
        -----

        Examples
        --------
        <...>
        """
            X,
            y,
        )

            y,
            y_pred,
            ax=ax,
            **kwargs,
        )

        cls,
        y_true,
        y_pred,
        *,
        ax=None,
        **kwargs,
    ):

        Returns
        -------

        See Also
        --------

        Notes
        -----

        Examples
        --------
        <...>
        """
        )

            y_true,
            y_pred,
        )
        )

        viz = cls(
        )

            ax=ax,
            **kwargs,
        )

def test_prediction_error_display_raise_error(
    pyplot, class_method, regressor, params, err_type, err_msg
):
    """Check that we raise the proper error when making the parameters
    # validation."""
        if class_method == "from_estimator":
            PredictionErrorDisplay.from_estimator(regressor, X, y, **params)
        else:
            y_pred = regressor.predict(X)
            PredictionErrorDisplay.from_predictions(y_true=y, y_pred=y_pred, **params)

def test_from_estimator_not_fitted(pyplot):
    """Check that we raise a `NotFittedError` when the passed regressor is not
    fit."""
    regressor = Ridge()
    with pytest.raises(NotFittedError, match="is not fitted yet."):
        PredictionErrorDisplay.from_estimator(regressor, X, y)

def test_prediction_error_display(pyplot, regressor_fitted, class_method, kind):
    """Check the default behaviour of the display."""
    if class_method == "from_estimator":
        display = PredictionErrorDisplay.from_estimator(
            regressor_fitted, X, y, kind=kind
        )
    else:
        y_pred = regressor_fitted.predict(X)
        display = PredictionErrorDisplay.from_predictions(
            y_true=y, y_pred=y_pred, kind=kind
        )

    if kind == "actual_vs_predicted":
        assert_allclose(display.line_.get_xdata(), display.line_.get_ydata())
        assert display.ax_.get_xlabel() == "Predicted values"
        assert display.ax_.get_ylabel() == "Actual values"
        assert display.line_ is not None
    else:
        assert display.ax_.get_ylabel() == "Residuals (actual - predicted)"

    assert display.ax_.get_legend() is None

def test_plot_prediction_error_subsample(
    pyplot, regressor_fitted, class_method, subsample, expected_size
):
    """Check the behaviour of `subsample`."""
            regressor_fitted, X, y, subsample=subsample
        )
    else:
            y_true=y, y_pred=y_pred, subsample=subsample
        )
    assert len(display.scatter_.get_offsets()) == expected_size

def test_plot_prediction_error_ax(pyplot, regressor_fitted, class_method):
    """Check that we can pass an axis to the display."""
    _, ax = pyplot.subplots()
        display = PredictionErrorDisplay.from_estimator(regressor_fitted, X, y, ax=ax)
    else:
            y_true=y, y_pred=y_pred, ax=ax
        )
    assert display.ax_ is ax

def test_prediction_error_custom_artist(
    pyplot, regressor_fitted, class_method, scatter_kwargs, line_kwargs
):
    """Check that we can tune the style of the line and the scatter."""
    extra_params = {
        "kind": "actual_vs_predicted",
        "scatter_kwargs": scatter_kwargs,
        "line_kwargs": line_kwargs,
    }
            regressor_fitted, X, y, **extra_params
        )
    else:
            y_true=y, y_pred=y_pred, **extra_params
        )

    if line_kwargs is not None:
        assert display.line_.get_linestyle() == "-"
        assert display.line_.get_color() == "red"
    else:
        assert display.line_.get_linestyle() == "--"
        assert display.line_.get_color() == "black"
        assert display.line_.get_alpha() == 0.7

    if scatter_kwargs is not None:
        assert_allclose(display.scatter_.get_facecolor(), [[0.0, 0.0, 1.0, 0.9]])
        assert_allclose(display.scatter_.get_edgecolor(), [[0.0, 0.0, 1.0, 0.9]])
    else:
        assert display.scatter_.get_alpha() == 0.8

def data_binary(data):
    X, y = data
    return X[y < 2], y[y < 2]

def test_display_curve_error_classifier(pyplot, data, data_binary, Display):
    """Check that a proper error is raised when only binary classification is
    supported."""
    X, y = data
    X_binary, y_binary = data_binary

    # Case 1: multiclass classifier with multiclass target
    msg = "Expected 'estimator' to be a binary classifier. Got 3 classes instead."
        Display.from_estimator(clf, X, y)

    # Case 2: multiclass classifier with binary target
        Display.from_estimator(clf, X_binary, y_binary)

    # Case 3: binary classifier with multiclass target
    clf = DecisionTreeClassifier().fit(X_binary, y_binary)
    msg = "The target y is not binary. Got multiclass type of target."

def test_display_curve_error_regression(pyplot, data_binary, Display):
    """Check that we raise an error with regressor."""

    # Case 1: regressor
    X, y = data_binary
    regressor = DecisionTreeRegressor().fit(X, y)

    msg = "Expected 'estimator' to be a binary classifier. Got DecisionTreeRegressor"
        Display.from_estimator(regressor, X, y)

    # Case 2: regression target
    classifier = DecisionTreeClassifier().fit(X, y)
    # Force `y_true` to be seen as a regression problem
    y = y + 0.5
    msg = "The target y is not binary. Got continuous type of target."
        Display.from_estimator(classifier, X, y)
        Display.from_predictions(y, regressor.fit(X, y).predict(X))

def test_display_curve_error_no_response(
    pyplot,
    data_binary,
    response_method,
    msg,
    Display,
):
    """Check that a proper error is raised when the response method requested
    is not defined for the given trained classifier."""

            self.classes_ = [0, 1]

    clf = MyClassifier().fit(X, y)

        Display.from_estimator(clf, X, y, response_method=response_method)

def test_display_curve_estimator_name_multiple_calls(
    pyplot,
    Display,
    constructor_name,
):
    """Check that passing `name` when calling `plot` will overwrite the original name
    in the legend."""
    clf_name = "my hand-crafted name"
    clf = LogisticRegression().fit(X, y)
    y_pred = clf.predict_proba(X)[:, 1]

    # safe guard for the binary if/else construction
    assert constructor_name in ("from_estimator", "from_predictions")

    if constructor_name == "from_estimator":
        disp = Display.from_estimator(clf, X, y, name=clf_name)
    else:
        disp = Display.from_predictions(y, y_pred, name=clf_name)
    assert disp.estimator_name == clf_name
    disp.plot()
    assert clf_name in disp.line_.get_label()
    clf_name = "another_name"
    disp.plot(name=clf_name)

def test_display_curve_not_fitted_errors(pyplot, data_binary, clf, Display):
    """Check that a proper error is raised when the classifier is not
    fitted."""
    # clone since we parametrize the test and the classifier will be fitted
    # when testing the second and subsequent plotting function
    model = clone(clf)
        Display.from_estimator(model, X, y)
    disp = Display.from_estimator(model, X, y)
    assert model.__class__.__name__ in disp.line_.get_label()
    assert disp.estimator_name == model.__class__.__name__

def test_display_curve_n_samples_consistency(pyplot, data_binary, Display):
    """Check the error raised when `y_pred` or `sample_weight` have inconsistent
    length."""

    msg = "Found input variables with inconsistent numbers of samples"
        Display.from_estimator(classifier, X[:-2], y)
        Display.from_estimator(classifier, X, y[:-2])
        Display.from_estimator(classifier, X, y, sample_weight=np.ones(X.shape[0] - 2))

def test_display_curve_error_pos_label(pyplot, data_binary, Display):
    """Check consistence of error message when `pos_label` should be specified."""
    y = y + 10

    y_pred = classifier.predict_proba(X)[:, -1]
    msg = r"y_true takes value in {10, 11} and pos_label is not specified"
        Display.from_predictions(y, y_pred)

def test_classifier_display_curve_named_constructor_return_type(
    pyplot, data_binary, Display, constructor
):
    """Check that named constructors return the correct type when subclassed.

    https://github.com/scikit-learn/scikit-learn/pull/27675
    """

    # This can be anything - we just need to check the named constructor return
    # type so the only requirement here is instantiating the class without error
    y_pred = y

    classifier = LogisticRegression().fit(X, y)

    class SubclassOfDisplay(Display):
        pass

    if constructor == "from_predictions":
        curve = SubclassOfDisplay.from_predictions(y, y_pred)
    else:  # constructor == "from_estimator"
        curve = SubclassOfDisplay.from_estimator(classifier, X, y)

    assert isinstance(curve, SubclassOfDisplay)

def test_det_curve_display(
    pyplot,
    with_sample_weight,
    drop_intermediate,
    with_strings,
):
    X, y = load_iris(return_X_y=True)
    # Binarize the data with only the two first classes
    X, y = X[y < 2], y[y < 2]

    pos_label = None
    if with_strings:
        y = np.array(["c", "b"])[y]
        pos_label = "c"

    if with_sample_weight:
        rng = np.random.RandomState(42)
        sample_weight = rng.randint(1, 4, size=(X.shape[0]))
    else:
        sample_weight = None

    lr = LogisticRegression()
    lr.fit(X, y)
    y_pred = getattr(lr, response_method)(X)
    if y_pred.ndim == 2:
        y_pred = y_pred[:, 1]

    common_kwargs = {
        "name": lr.__class__.__name__,
        "alpha": 0.8,
        "sample_weight": sample_weight,
        "drop_intermediate": drop_intermediate,
        "pos_label": pos_label,
    }
        disp = DetCurveDisplay.from_estimator(lr, X, y, **common_kwargs)
    else:
        disp = DetCurveDisplay.from_predictions(y, y_pred, **common_kwargs)

    fpr, fnr, _ = det_curve(
        y,
        y_pred,
        drop_intermediate=drop_intermediate,
    )

    assert_allclose(disp.fpr, fpr, atol=1e-7)
    assert_allclose(disp.fnr, fnr, atol=1e-7)

    assert disp.estimator_name == "LogisticRegression"

    # cannot fail thanks to pyplot fixture
    import matplotlib as mpl

    assert isinstance(disp.line_, mpl.lines.Line2D)
    assert disp.line_.get_alpha() == 0.8
    assert isinstance(disp.ax_, mpl.axes.Axes)
    assert isinstance(disp.figure_, mpl.figure.Figure)
    assert disp.line_.get_label() == "LogisticRegression"

    expected_pos_label = 1 if pos_label is None else pos_label
    expected_ylabel = f"False Negative Rate (Positive label: {expected_pos_label})"
    expected_xlabel = f"False Positive Rate (Positive label: {expected_pos_label})"
    assert disp.ax_.get_ylabel() == expected_ylabel
    assert disp.ax_.get_xlabel() == expected_xlabel

def test_det_curve_display_default_name(
    pyplot,
    expected_clf_name,
):
    # Check the default name display in the figure when `name` is not provided

    y_pred = lr.predict_proba(X)[:, 1]

        disp = DetCurveDisplay.from_estimator(lr, X, y)
    else:
        disp = DetCurveDisplay.from_predictions(y, y_pred)

    assert disp.estimator_name == expected_clf_name
    assert disp.line_.get_label() == expected_clf_name

def test_precision_recall_display_plotting(
    pyplot, constructor_name, response_method, drop_intermediate
):
    """Check the overall plotting rendering."""
    X, y = make_classification(n_classes=2, n_samples=50, random_state=0)
    pos_label = 1

    classifier.fit(X, y)

    y_pred = getattr(classifier, response_method)(X)
    y_pred = y_pred if y_pred.ndim == 1 else y_pred[:, pos_label]

        display = PrecisionRecallDisplay.from_estimator(
            classifier,
            X,
            y,
        )
    else:
        display = PrecisionRecallDisplay.from_predictions(
            y, y_pred, pos_label=pos_label, drop_intermediate=drop_intermediate
        )

    precision, recall, _ = precision_recall_curve(
        y, y_pred, pos_label=pos_label, drop_intermediate=drop_intermediate
    )
    average_precision = average_precision_score(y, y_pred, pos_label=pos_label)

    np.testing.assert_allclose(display.precision, precision)
    np.testing.assert_allclose(display.recall, recall)
    assert display.average_precision == pytest.approx(average_precision)

    assert isinstance(display.line_, mpl.lines.Line2D)
    assert isinstance(display.ax_, mpl.axes.Axes)
    assert isinstance(display.figure_, mpl.figure.Figure)

    assert display.ax_.get_xlabel() == "Recall (Positive label: 1)"
    assert display.ax_.get_ylabel() == "Precision (Positive label: 1)"
    assert display.ax_.get_adjustable() == "box"
    assert display.ax_.get_aspect() in ("equal", 1.0)
    assert display.ax_.get_xlim() == display.ax_.get_ylim() == (-0.01, 1.01)

    # plotting passing some new parameters
    display.plot(alpha=0.8, name="MySpecialEstimator")
    expected_label = f"MySpecialEstimator (AP = {average_precision:0.2f})"
    assert display.line_.get_label() == expected_label
    assert display.line_.get_alpha() == pytest.approx(0.8)

    # Check that the chance level line is not plotted by default
    assert display.chance_level_ is None

def test_precision_recall_chance_level_line(
    pyplot,
    chance_level_kw,
):
    """Check the chance level line plotting behavior."""
    pos_prevalence = Counter(y)[1] / len(y)

    y_pred = lr.fit(X, y).predict_proba(X)[:, 1]

            lr,
            X,
            y,
            plot_chance_level=True,
        )
    else:
            y,
            y_pred,
        )

    assert isinstance(display.chance_level_, mpl.lines.Line2D)
    assert tuple(display.chance_level_.get_xdata()) == (0, 1)
    assert tuple(display.chance_level_.get_ydata()) == (pos_prevalence, pos_prevalence)

    # Checking for chance level line styles
    if chance_level_kw is None:
        assert display.chance_level_.get_color() == "k"
    else:
        assert display.chance_level_.get_color() == "r"

def test_precision_recall_display_name(pyplot, constructor_name, default_label):
    """Check the behaviour of the name parameters"""
    X, y = make_classification(n_classes=2, n_samples=100, random_state=0)

    y_pred = classifier.predict_proba(X)[:, pos_label]

        display = PrecisionRecallDisplay.from_estimator(classifier, X, y)
    else:
            y, y_pred, pos_label=pos_label
        )

    # check that the default name is used
    assert display.line_.get_label() == default_label.format(average_precision)

    # check that the name can be set
    display.plot(name="MySpecialEstimator")
    assert (
        display.line_.get_label()
        == f"MySpecialEstimator (AP = {average_precision:.2f})"
    )

def test_precision_recall_display_pipeline(pyplot, clf):
        PrecisionRecallDisplay.from_estimator(clf, X, y)
    display = PrecisionRecallDisplay.from_estimator(clf, X, y)
    assert display.estimator_name == clf.__class__.__name__

def test_precision_recall_display_string_labels(pyplot):
    # regression test #15738
    cancer = load_breast_cancer()
    X, y = cancer.data, cancer.target_names[cancer.target]

    lr = make_pipeline(StandardScaler(), LogisticRegression())
    for klass in cancer.target_names:
        assert klass in lr.classes_
    display = PrecisionRecallDisplay.from_estimator(lr, X, y)

    avg_prec = average_precision_score(y, y_pred, pos_label=lr.classes_[1])

    assert display.average_precision == pytest.approx(avg_prec)
    assert display.estimator_name == lr.__class__.__name__

    err_msg = r"y_true takes value in {'benign', 'malignant'}"
        PrecisionRecallDisplay.from_predictions(y, y_pred)

    display = PrecisionRecallDisplay.from_predictions(
        y, y_pred, pos_label=lr.classes_[1]
    )

def test_default_labels(pyplot, average_precision, estimator_name, expected_label):
    """Check the default labels used in the display."""
    precision = np.array([1, 0.5, 0])
    recall = np.array([0, 0.5, 1])
    display = PrecisionRecallDisplay(
        recall,
        average_precision=average_precision,
        estimator_name=estimator_name,
    )
    display.plot()

def test_plot_precision_recall_pos_label(pyplot, constructor_name, response_method):
    # check that we can provide the positive label and display the proper
    # statistics
    # create an highly imbalanced version of the breast cancer dataset
    X = X[:, :2]
        X,
        y,
    )

    classifier = LogisticRegression()

    # sanity check to be sure the positive class is classes_[0] and that we
    # are betrayed by the class imbalance
    assert classifier.classes_.tolist() == ["cancer", "not cancer"]

    y_pred = getattr(classifier, response_method)(X_test)
    # we select the corresponding probability columns or reverse the decision
    #  function otherwise
    y_pred_cancer = -1 * y_pred if y_pred.ndim == 1 else y_pred[:, 0]
    y_pred_not_cancer = y_pred if y_pred.ndim == 1 else y_pred[:, 1]

            X_test,
            y_test,
            pos_label="cancer",
        )
    else:
            y_test,
            y_pred_cancer,
        )
    # we should obtain the statistics of the "cancer" class
    avg_prec_limit = 0.65
    assert display.average_precision < avg_prec_limit
    assert -trapezoid(display.precision, display.recall) < avg_prec_limit

    # otherwise we should obtain the statistics of the "not cancer" class
            X_test,
            y_test,
            pos_label="not cancer",
        )
    else:
            y_test,
            y_pred_not_cancer,
        )
    avg_prec_limit = 0.95
    assert display.average_precision > avg_prec_limit
    assert -trapezoid(display.precision, display.recall) > avg_prec_limit

def test_precision_recall_prevalence_pos_label_reusable(pyplot, constructor_name):
    # Check that even if one passes plot_chance_level=False the first time
    # one can still call disp.plot with plot_chance_level=True and get the
    # chance level line

            lr, X, y, plot_chance_level=False
        )
    else:
            y, y_pred, plot_chance_level=False
        )

    # When calling from_estimator or from_predictions,
    # prevalence_pos_label should have been set, so that directly
    # calling plot_chance_level=True should plot the chance level line
    display.plot(plot_chance_level=True)

def test_precision_recall_raise_no_prevalence(pyplot):
    # Check that raises correctly when plotting chance level with
    # no prvelance_pos_label is provided
    display = PrecisionRecallDisplay(precision, recall)

    msg = (
        "You must provide prevalence_pos_label when constructing the "
        "PrecisionRecallDisplay object in order to plot the chance "
        "level line. Alternatively, you may use "
        "PrecisionRecallDisplay.from_estimator or "
        "PrecisionRecallDisplay.from_predictions "
        "to automatically set prevalence_pos_label"
    )

        display.plot(plot_chance_level=True)

def test_plot_precision_recall_despine(pyplot, despine, constructor_name):
    # Check that the despine keyword is working correctly

    y_pred = clf.decision_function(X)

        display = PrecisionRecallDisplay.from_estimator(clf, X, y, despine=despine)
    else:
        display = PrecisionRecallDisplay.from_predictions(y, y_pred, despine=despine)

    for s in ["top", "right"]:
        assert display.ax_.spines[s].get_visible() is not despine

    if despine:
        for s in ["bottom", "left"]:
            assert display.ax_.spines[s].get_bounds() == (0, 1)

def data():
    # Avoid introducing test dependencies by mistake.
    X.flags.writeable = False
    y.flags.writeable = False
    return X, y

def test_roc_curve_display_plotting(
    pyplot,
    default_name,
):
    """Check the overall plotting behaviour."""

    else:

    y_score = getattr(lr, response_method)(X)
    y_score = y_score if y_score.ndim == 1 else y_score[:, 1]

        display = RocCurveDisplay.from_estimator(
            lr,
            X,
            y,
            alpha=0.8,
        )
    else:
        display = RocCurveDisplay.from_predictions(
            y,
            y_score,
        )

    fpr, tpr, _ = roc_curve(
        y,
        y_score,
    )

    assert_allclose(display.roc_auc, auc(fpr, tpr))
    assert_allclose(display.fpr, fpr)
    assert_allclose(display.tpr, tpr)

    assert display.estimator_name == default_name

    assert display.line_.get_alpha() == 0.8

    expected_label = f"{default_name} (AUC = {display.roc_auc:.2f})"

    expected_ylabel = f"True Positive Rate (Positive label: {expected_pos_label})"

    assert display.ax_.get_ylabel() == expected_ylabel
    assert display.ax_.get_xlabel() == expected_xlabel

def test_roc_curve_chance_level_line(
    pyplot,
    plot_chance_level,
    label,
):
    """Check the chance level line plotting behaviour."""

    y_score = getattr(lr, "predict_proba")(X)

            lr,
            X,
            y,
            label=label,
        )
    else:
            y,
            y_score,
        )

    if plot_chance_level:
        assert isinstance(display.chance_level_, mpl.lines.Line2D)
        assert tuple(display.chance_level_.get_xdata()) == (0, 1)
        assert tuple(display.chance_level_.get_ydata()) == (0, 1)
    else:
        assert display.chance_level_ is None

    if plot_chance_level and chance_level_kw is None:
        assert display.chance_level_.get_linestyle() == "--"
        assert display.chance_level_.get_label() == "Chance level (AUC = 0.5)"
    elif plot_chance_level:
        if "c" in chance_level_kw:
            assert display.chance_level_.get_color() == chance_level_kw["c"]
        else:
            assert display.chance_level_.get_color() == chance_level_kw["color"]
        if "lw" in chance_level_kw:
            assert display.chance_level_.get_linewidth() == chance_level_kw["lw"]
        else:
            assert display.chance_level_.get_linewidth() == chance_level_kw["linewidth"]
        if "ls" in chance_level_kw:
            assert display.chance_level_.get_linestyle() == chance_level_kw["ls"]
        else:
            assert display.chance_level_.get_linestyle() == chance_level_kw["linestyle"]
        # Checking for legend behaviour
        if label is not None or chance_level_kw.get("label") is not None:
            legend = display.ax_.get_legend()
            assert legend is not None  #  Legend should be present if any label is set
            legend_labels = [text.get_text() for text in legend.get_texts()]
            if label is not None:
                assert label in legend_labels
            if chance_level_kw.get("label") is not None:
                assert chance_level_kw["label"] in legend_labels
        else:
            assert display.ax_.get_legend() is None

def test_roc_curve_display_complex_pipeline(pyplot, data_binary, clf, constructor_name):
    """Check the behaviour with complex pipeline."""

    clf = clone(clf)

        with pytest.raises(NotFittedError):
            RocCurveDisplay.from_estimator(clf, X, y)

        display = RocCurveDisplay.from_estimator(clf, X, y)
        name = clf.__class__.__name__
    else:
        display = RocCurveDisplay.from_predictions(y, y)
        name = "Classifier"

    assert name in display.line_.get_label()
    assert display.estimator_name == name

def test_roc_curve_display_default_labels(
    pyplot, roc_auc, estimator_name, expected_label
):
    fpr = np.array([0, 0.5, 1])
    tpr = np.array([0, 0.5, 1])
    disp = RocCurveDisplay(
        fpr=fpr, tpr=tpr, roc_auc=roc_auc, estimator_name=estimator_name
    ).plot()
    assert disp.line_.get_label() == expected_label

def test_plot_roc_curve_pos_label(pyplot, response_method, constructor_name):
    # create an highly imbalanced
    X = X[:, :2]
        X,
        y,
    )

    y_score = getattr(classifier, response_method)(X_test)
    # function otherwise
    y_score_cancer = -1 * y_score if y_score.ndim == 1 else y_score[:, 0]
    y_score_not_cancer = y_score if y_score.ndim == 1 else y_score[:, 1]

            X_test,
            y_test,
        )
    else:
            y_test,
            y_score_cancer,
        )

    roc_auc_limit = 0.95679

    assert display.roc_auc == pytest.approx(roc_auc_limit)
    assert trapezoid(display.tpr, display.fpr) == pytest.approx(roc_auc_limit)

            X_test,
            y_test,
        )
    else:
            y_test,
            y_score_not_cancer,
        )

def test_y_score_and_y_pred_specified_error():
    """Check that an error is raised when both y_score and y_pred are specified."""
    y_score = np.array([0.1, 0.4, 0.35, 0.8])
    y_pred = np.array([0.2, 0.3, 0.5, 0.1])

        ValueError, match="`y_pred` and `y_score` cannot be both specified"
    ):
        RocCurveDisplay.from_predictions(y_true, y_score=y_score, y_pred=y_pred)

def test_y_pred_deprecation_warning(pyplot):
    """Check that a warning is raised when y_pred is specified."""

    with pytest.warns(FutureWarning, match="y_pred is deprecated in 1.7"):
        display_y_pred = RocCurveDisplay.from_predictions(y_true, y_pred=y_score)

    assert_allclose(display_y_pred.fpr, [0, 0.5, 0.5, 1])
    assert_allclose(display_y_pred.tpr, [0, 0, 1, 1])

    display_y_score = RocCurveDisplay.from_predictions(y_true, y_score)
    assert_allclose(display_y_score.fpr, [0, 0.5, 0.5, 1])
    assert_allclose(display_y_score.tpr, [0, 0, 1, 1])

def test_plot_roc_curve_despine(pyplot, data_binary, despine, constructor_name):

    y_pred = lr.decision_function(X)

        display = RocCurveDisplay.from_estimator(lr, X, y, despine=despine)
    else:
        display = RocCurveDisplay.from_predictions(y, y_pred, despine=despine)

def test_confusion_matrix_display_validation(pyplot):
    """Check that we raise the proper error when validating parameters."""
        n_samples=100, n_informative=5, n_classes=5, random_state=0
    )

        ConfusionMatrixDisplay.from_estimator(SVC(), X, y)

    regressor = SVR().fit(X, y)
    y_pred_regressor = regressor.predict(X)
    y_pred_classifier = SVC().fit(X, y).predict(X)

    err_msg = "ConfusionMatrixDisplay.from_estimator only supports classifiers"
        ConfusionMatrixDisplay.from_estimator(regressor, X, y)

    err_msg = "Mix type of y not allowed, got types"
        # Force `y_true` to be seen as a regression problem
        ConfusionMatrixDisplay.from_predictions(y + 0.5, y_pred_classifier)
        ConfusionMatrixDisplay.from_predictions(y, y_pred_regressor)

    err_msg = "Found input variables with inconsistent numbers of samples"
        ConfusionMatrixDisplay.from_predictions(y, y_pred_classifier[::2])

def test_confusion_matrix_display_custom_labels(
    pyplot, constructor_name, with_labels, with_display_labels
):
    """Check the resulting plot when labels are given."""
        n_samples=100, n_informative=5, n_classes=n_classes, random_state=0
    )
    classifier = SVC().fit(X, y)
    y_pred = classifier.predict(X)

    ax = pyplot.gca()
    labels = [2, 1, 0, 3, 4] if with_labels else None
    display_labels = ["b", "d", "a", "e", "f"] if with_display_labels else None

    cm = confusion_matrix(y, y_pred, labels=labels)
        "ax": ax,
        "display_labels": display_labels,
        "labels": labels,
    }
        disp = ConfusionMatrixDisplay.from_estimator(classifier, X, y, **common_kwargs)
    else:
        disp = ConfusionMatrixDisplay.from_predictions(y, y_pred, **common_kwargs)
    assert_allclose(disp.confusion_matrix, cm)

    if with_display_labels:
        expected_display_labels = display_labels
    elif with_labels:
        expected_display_labels = labels
    else:
        expected_display_labels = list(range(n_classes))

    expected_display_labels_str = [str(name) for name in expected_display_labels]

    x_ticks = [tick.get_text() for tick in disp.ax_.get_xticklabels()]
    y_ticks = [tick.get_text() for tick in disp.ax_.get_yticklabels()]

    assert_array_equal(disp.display_labels, expected_display_labels)
    assert_array_equal(x_ticks, expected_display_labels_str)
    assert_array_equal(y_ticks, expected_display_labels_str)

def test_confusion_matrix_display_plotting(
    pyplot,
    normalize,
    include_values,
):
    )

    cmap = "plasma"

    cm = confusion_matrix(y, y_pred)
        "normalize": normalize,
        "cmap": cmap,
        "ax": ax,
        "include_values": include_values,
    }
    else:

    assert disp.ax_ == ax

    if normalize == "true":
        cm = cm / cm.sum(axis=1, keepdims=True)
    elif normalize == "pred":
        cm = cm / cm.sum(axis=0, keepdims=True)
    elif normalize == "all":
        cm = cm / cm.sum()

    assert isinstance(disp.im_, mpl.image.AxesImage)
    assert disp.im_.get_cmap().name == cmap
    assert isinstance(disp.ax_, pyplot.Axes)
    assert isinstance(disp.figure_, pyplot.Figure)

    assert disp.ax_.get_ylabel() == "True label"
    assert disp.ax_.get_xlabel() == "Predicted label"

    expected_display_labels = list(range(n_classes))

    image_data = disp.im_.get_array().data
    assert_allclose(image_data, cm)

    if include_values:
        assert disp.text_.shape == (n_classes, n_classes)
        fmt = ".2g"
        expected_text = np.array([format(v, fmt) for v in cm.ravel(order="C")])
        text_text = np.array([t.get_text() for t in disp.text_.ravel(order="C")])
        assert_array_equal(expected_text, text_text)
    else:
        assert disp.text_ is None

def test_confusion_matrix_display(pyplot, constructor_name):
    """Check the behaviour of the default constructor without using the class
    methods."""
    )

        "normalize": None,
        "include_values": True,
        "cmap": "viridis",
        "xticks_rotation": 45.0,
    }
    else:

    assert disp.text_.shape == (n_classes, n_classes)

    rotations = [tick.get_rotation() for tick in disp.ax_.get_xticklabels()]
    assert_allclose(rotations, 45.0)

    disp.plot(cmap="plasma")
    assert disp.im_.get_cmap().name == "plasma"

    disp.plot(include_values=False)
    assert disp.text_ is None

    disp.plot(xticks_rotation=90.0)
    assert_allclose(rotations, 90.0)

    disp.plot(values_format="e")
    expected_text = np.array([format(v, "e") for v in cm.ravel(order="C")])
    text_text = np.array([t.get_text() for t in disp.text_.ravel(order="C")])
    assert_array_equal(expected_text, text_text)

def test_confusion_matrix_contrast(pyplot):
    """Check that the text color is appropriate depending on background."""

    cm = np.eye(2) / 2
    disp = ConfusionMatrixDisplay(cm, display_labels=[0, 1])

    disp.plot(cmap=pyplot.cm.gray)
    # diagonal text is black
    assert_allclose(disp.text_[0, 0].get_color(), [0.0, 0.0, 0.0, 1.0])
    assert_allclose(disp.text_[1, 1].get_color(), [0.0, 0.0, 0.0, 1.0])

    # off-diagonal text is white
    assert_allclose(disp.text_[0, 1].get_color(), [1.0, 1.0, 1.0, 1.0])
    assert_allclose(disp.text_[1, 0].get_color(), [1.0, 1.0, 1.0, 1.0])

    disp.plot(cmap=pyplot.cm.gray_r)
    # diagonal text is white
    assert_allclose(disp.text_[0, 1].get_color(), [0.0, 0.0, 0.0, 1.0])
    assert_allclose(disp.text_[1, 0].get_color(), [0.0, 0.0, 0.0, 1.0])

    # off-diagonal text is black
    assert_allclose(disp.text_[0, 0].get_color(), [1.0, 1.0, 1.0, 1.0])
    assert_allclose(disp.text_[1, 1].get_color(), [1.0, 1.0, 1.0, 1.0])

    # Regression test for #15920
    cm = np.array([[19, 34], [32, 58]])

    disp.plot(cmap=pyplot.cm.Blues)
    min_color = pyplot.cm.Blues(0)
    max_color = pyplot.cm.Blues(255)
    assert_allclose(disp.text_[0, 0].get_color(), max_color)
    assert_allclose(disp.text_[0, 1].get_color(), max_color)
    assert_allclose(disp.text_[1, 0].get_color(), max_color)
    assert_allclose(disp.text_[1, 1].get_color(), min_color)

def test_confusion_matrix_pipeline(pyplot, clf):
    """Check the behaviour of the plotting with more complex pipeline."""
    )
        ConfusionMatrixDisplay.from_estimator(clf, X, y)

    disp = ConfusionMatrixDisplay.from_estimator(clf, X, y)

def test_confusion_matrix_with_unknown_labels(pyplot, constructor_name):
    """Check that when labels=None, the unique values in `y_pred` and `y_true`
    will be used.
    https://github.com/scikit-learn/scikit-learn/pull/18405
    """
    )
    # create unseen labels in `y_true` not seen during fitting and not present
    # in 'classifier.classes_'
    y = y + 1

    common_kwargs = {"labels": None}
    else:

    display_labels = [tick.get_text() for tick in disp.ax_.get_xticklabels()]
    expected_labels = [str(i) for i in range(n_classes + 1)]
    assert_array_equal(expected_labels, display_labels)

def test_colormap_max(pyplot):
    """Check that the max color is used for the color of the text."""
    gray = pyplot.get_cmap("gray", 1024)
    confusion_matrix = np.array([[1.0, 0.0], [0.0, 1.0]])

    disp = ConfusionMatrixDisplay(confusion_matrix)
    disp.plot(cmap=gray)

    color = disp.text_[1, 0].get_color()
    assert_allclose(color, [1.0, 1.0, 1.0, 1.0])

def test_im_kw_adjust_vmin_vmax(pyplot):
    """Check that im_kw passes kwargs to imshow"""

    confusion_matrix = np.array([[0.48, 0.04], [0.08, 0.4]])
    disp.plot(im_kw=dict(vmin=0.0, vmax=0.8))

    clim = disp.im_.get_clim()
    assert clim[0] == pytest.approx(0.0)
    assert clim[1] == pytest.approx(0.8)

def test_confusion_matrix_text_kw(pyplot):
    """Check that text_kw is passed to the text call."""
    font_size = 15.0

    # from_estimator passes the font size
    disp = ConfusionMatrixDisplay.from_estimator(
        classifier, X, y, text_kw={"fontsize": font_size}
    )
    for text in disp.text_.reshape(-1):
        assert text.get_fontsize() == font_size

    # plot adjusts plot to new font size
    new_font_size = 20.0
    disp.plot(text_kw={"fontsize": new_font_size})
        assert text.get_fontsize() == new_font_size

    # from_predictions passes the font size
    disp = ConfusionMatrixDisplay.from_predictions(
        y, y_pred, text_kw={"fontsize": font_size}
    )

def _estimator_has(attr):

    Used together with `available_if`.
    """

        getattr(self.estimator, attr)

class FrozenEstimator(BaseEstimator):
    """Estimator that wraps a fitted estimator to prevent re-fitting.

    This meta-estimator takes an estimator and freezes it, in the sense that calling
    `fit` on it has no effect. `fit_predict` and `fit_transform` are also disabled.
    All other methods are delegated to the original estimator and original estimator's
    attributes are accessible as well.

    This is particularly useful when you have a fitted or a pre-trained model as a
    transformer in a pipeline, and you'd like `pipeline.fit` to have no effect on this
    step.

        The estimator which is to be kept frozen.

    See Also
    --------
    None: No similar entry in the scikit-learn documentation.

    Examples
    --------
    >>> clf = LogisticRegression(random_state=0).fit(X, y)
    >>> frozen_clf = FrozenEstimator(clf)
    >>> frozen_clf.fit(X, y)  # No-op
    FrozenEstimator(estimator=LogisticRegression(random_state=0))
    >>> frozen_clf.predict(X)  # Predictions from `clf.predict`
    array(...)
    """

    @available_if(_estimator_has("__getitem__"))
    def __getitem__(self, *args, **kwargs):
        """__getitem__ is defined in :class:`~sklearn.pipeline.Pipeline` and \
            :class:`~sklearn.compose.ColumnTransformer`.
        """
        return self.estimator.__getitem__(*args, **kwargs)

    def __getattr__(self, name):
        # `estimator`'s attributes are now accessible except `fit_predict` and
        # `fit_transform`
        if name in ["fit_predict", "fit_transform"]:
            raise AttributeError(f"{name} is not available for frozen estimators.")
        return getattr(self.estimator, name)

        try:
            check_is_fitted(self.estimator)

    def fit(self, X, y, *args, **kwargs):
        """No-op.

        As a frozen estimator, calling `fit` has no effect.

        X : object
            Ignored.

        y : object
            Ignored.

        *args : tuple
            Additional positional arguments. Ignored, but present for API compatibility
            with `self.estimator`.

            Additional keyword arguments. Ignored, but present for API compatibility

        Returns
        -------
        """
        check_is_fitted(self.estimator)

        The only valid key here is `estimator`. You cannot set the parameters of the
        inner estimator.

        Returns
        -------
        self : FrozenEstimator
            This estimator.
        """
        estimator = kwargs.pop("estimator", None)
        if estimator is not None:
            self.estimator = estimator
        if kwargs:
                "You cannot set parameters of the inner estimator in a frozen "
                "estimator since calling `fit` has no effect. You can use "
                "`frozenestimator.estimator.set_params` to set parameters of the inner "
                "estimator."
            )

        Returns a `{"estimator": estimator}` dict. The parameters of the inner
        estimator are not included.

            Ignored.

        Returns
        -------
        """
        return {"estimator": self.estimator}

        tags = deepcopy(get_tags(self.estimator))

        """

        try:

        """No-op.

        X : object
            Ignored.

        y : object
            Ignored.

        Returns
        -------
        """

        Returns
        -------
        """
        if kwargs:
            )

            Ignored.

        Returns
        -------
        """

def test_frozen_methods(estimator, dataset, request, method):
    """Test that frozen.fit doesn't do anything, and that all other methods are
    exposed by the frozen estimator and return the same values as the estimator.
    """
    X, y = request.getfixturevalue(dataset)
    set_random_state(estimator)
    frozen = FrozenEstimator(estimator)
    # this should be no-op
    frozen.fit([[1]], [1])

    if hasattr(estimator, method):
        assert_array_equal(getattr(estimator, method)(X), getattr(frozen, method)(X))

    assert is_classifier(estimator) == is_classifier(frozen)
    assert is_regressor(estimator) == is_regressor(frozen)
    assert is_clusterer(estimator) == is_clusterer(frozen)
    assert is_outlier_detector(estimator) == is_outlier_detector(frozen)

def test_frozen_metadata_routing(regression_dataset):
    """Test that metadata routing works with frozen estimators."""

    class ConsumesMetadata(BaseEstimator):
        def __init__(self, on_fit=None, on_predict=None):
            self.on_fit = on_fit
            self.on_predict = on_predict

        def fit(self, X, y, metadata=None):
            if self.on_fit:
                assert metadata is not None
            self.fitted_ = True

        def predict(self, X, metadata=None):
            if self.on_predict:
            return np.ones(len(X))

    X, y = regression_dataset
    pipeline = make_pipeline(
        ConsumesMetadata(on_fit=True, on_predict=True)
        .set_fit_request(metadata=True)
        .set_predict_request(metadata=True)
    )

    pipeline.fit(X, y, metadata="test")
    frozen = FrozenEstimator(pipeline)
    pipeline.predict(X, metadata="test")
    frozen.predict(X, metadata="test")

    frozen["consumesmetadata"].set_predict_request(metadata=False)
            "Pipeline.predict got unexpected argument(s) {'metadata'}, which are not "
            "routed to any object."
        ),
    ):
        frozen.predict(X, metadata="test")

    frozen["consumesmetadata"].set_predict_request(metadata=None)
    with pytest.raises(UnsetMetadataPassedError):

def test_composite_fit(classification_dataset):
    """Test that calling fit_transform and fit_predict doesn't call fit."""

    class Estimator(BaseEstimator):
            try:
                self._fit_counter += 1
                self._fit_counter = 1

        def fit_transform(self, X, y=None):
            # only here to test that it doesn't get called
            ...  # pragma: no cover

        def fit_predict(self, X, y=None):

    X, y = classification_dataset
    est = Estimator().fit(X, y)
    frozen = FrozenEstimator(est)

        frozen.fit_predict(X, y)
        frozen.fit_transform(X, y)

    assert frozen._fit_counter == 1

def test_clone_frozen(regression_dataset):
    """Test that cloning a frozen estimator keeps the frozen state."""
    estimator = LinearRegression().fit(X, y)
    cloned = clone(frozen)
    assert cloned.estimator is estimator

def test_check_is_fitted(regression_dataset):
    """Test that check_is_fitted works on frozen estimators."""

    estimator = LinearRegression()
        check_is_fitted(frozen)

    check_is_fitted(frozen)

def test_frozen_tags():
    """Test that frozen estimators have the same tags as the original estimator
    except for the skip_test tag."""

        def __sklearn_tags__(self):
            tags = super().__sklearn_tags__()
            tags.input_tags.categorical = True

    frozen_tags = frozen.__sklearn_tags__()
    estimator_tags = estimator.__sklearn_tags__()

    assert frozen_tags._skip_test is True
    assert estimator_tags._skip_test is False

    assert estimator_tags.input_tags.categorical is True
    assert frozen_tags.input_tags.categorical is True

def test_frozen_params():
    """Test that FrozenEstimator only exposes the estimator parameter."""
    est = LogisticRegression()

    with pytest.raises(ValueError, match="You cannot set parameters of the inner"):
        frozen.set_params(estimator__C=1)

    assert frozen.get_params() == {"estimator": est}

    other_est = LocalOutlierFactor()
    frozen.set_params(estimator=other_est)
    assert frozen.get_params() == {"estimator": other_est}

            try:

            try:

class BaseDecisionTree(MultiOutputMixin, BaseEstimator, metaclass=ABCMeta):
    """Base class for decision trees.

    """

    # "check_input" is used for optimisation and isn't something to be passed
    # around in a pipeline.
    __metadata_request__predict = {"check_input": metadata_routing.UNUSED}

        "splitter": [StrOptions({"best", "random"})],
        "min_samples_split": [
            Interval(Integral, 2, None, closed="left"),
        ],
        "min_samples_leaf": [
            Interval(RealNotInt, 0.0, 1.0, closed="neither"),
        ],
        "min_weight_fraction_leaf": [Interval(Real, 0.0, 0.5, closed="both")],
            StrOptions({"sqrt", "log2"}),
            None,
        ],
        "min_impurity_decrease": [Interval(Real, 0.0, None, closed="left")],
        "ccp_alpha": [Interval(Real, 0.0, None, closed="left")],
        "monotonic_cst": ["array-like", None],
    }

        self,
        *,
        splitter,
    ):
        self.splitter = splitter

    def get_depth(self):
        """Return the depth of the decision tree.

        The depth of a tree is the maximum distance between the root
        and any leaf.

        Returns
        -------
        self.tree_.max_depth : int
            The maximum depth of the tree.
        """
        return self.tree_.max_depth

    def get_n_leaves(self):
        """Return the number of leaves of the decision tree.

        Returns
        -------
        self.tree_.n_leaves : int
            Number of leaves.
        """
        return self.tree_.n_leaves

    def _support_missing_values(self, X):
        return (
            not issparse(X)
            and self.__sklearn_tags__().input_tags.allow_nan
            and self.monotonic_cst is None
        )

    def _compute_missing_values_in_feature_mask(self, X, estimator_name=None):
        """Return boolean mask denoting if there are missing values for each feature.

        This method also ensures that X is finite.

        Parameter
        ---------
        X : array-like of shape (n_samples, n_features), dtype=DOUBLE

        estimator_name : str or None, default=None
            Name to use when raising an error. Defaults to the class name.

        Returns
        -------
        missing_values_in_feature_mask : ndarray of shape (n_features,), or None
            Missing value mask. If missing values are not supported or there
            are no missing values, return None.
        """
        estimator_name = estimator_name or self.__class__.__name__
        common_kwargs = dict(estimator_name=estimator_name, input_name="X")

        if not self._support_missing_values(X):
            assert_all_finite(X, **common_kwargs)

        with np.errstate(over="ignore"):
            overall_sum = np.sum(X)

        if not np.isfinite(overall_sum):
            # Raise a ValueError in case of the presence of an infinite element.
            _assert_all_finite_element_wise(X, xp=np, allow_nan=True, **common_kwargs)

        # If the sum is not nan, then there are no missing values
        if not np.isnan(overall_sum):

        missing_values_in_feature_mask = _any_isnan_axis0(X)
        return missing_values_in_feature_mask

    def _fit(
        self,
        X,
        y,
        missing_values_in_feature_mask=None,
    ):

            # Need to validate separately here.
            # We can't pass multi_output=True because that would allow y to be
            # csr.

            # _compute_missing_values_in_feature_mask will check for finite values and
            # compute the missing mask if the tree supports missing values
            check_X_params = dict(
                dtype=DTYPE, accept_sparse="csc", ensure_all_finite=False
            )
            check_y_params = dict(ensure_2d=False, dtype=None)
                self, X, y, validate_separately=(check_X_params, check_y_params)
            )

            missing_values_in_feature_mask = (
                self._compute_missing_values_in_feature_mask(X)
            )
            if issparse(X):
                X.sort_indices()

                if X.indices.dtype != np.intc or X.indptr.dtype != np.intc:
                        "No support for np.int64 index based sparse matrices"
                    )

            if self.criterion == "poisson":
                if np.any(y < 0):
                        "Some value(s) of y are negative which is"
                        " not allowed for Poisson regression."
                    )
                if np.sum(y) <= 0:
                        "Sum of y is not positive which is "
                        "necessary for Poisson regression."
                    )

        # Determine output settings
        n_samples, self.n_features_in_ = X.shape
        is_classification = is_classifier(self)

        if is_classification:
            y = np.copy(y)

            self.classes_ = []
            self.n_classes_ = []

            if self.class_weight is not None:
                y_original = np.copy(y)

            y_encoded = np.zeros(y.shape, dtype=int)
                classes_k, y_encoded[:, k] = np.unique(y[:, k], return_inverse=True)
                self.classes_.append(classes_k)
                self.n_classes_.append(classes_k.shape[0])
            y = y_encoded

                expanded_class_weight = compute_sample_weight(
                    self.class_weight, y_original
                )

            self.n_classes_ = np.array(self.n_classes_, dtype=np.intp)

        max_depth = np.iinfo(np.int32).max if self.max_depth is None else self.max_depth

        if isinstance(self.min_samples_leaf, numbers.Integral):
            min_samples_leaf = self.min_samples_leaf
            min_samples_leaf = ceil(self.min_samples_leaf * n_samples)

        if isinstance(self.min_samples_split, numbers.Integral):
            min_samples_split = self.min_samples_split
            min_samples_split = ceil(self.min_samples_split * n_samples)
            min_samples_split = max(2, min_samples_split)

        min_samples_split = max(min_samples_split, 2 * min_samples_leaf)

            if self.max_features == "sqrt":
            elif self.max_features == "log2":
        elif isinstance(self.max_features, numbers.Integral):
            if self.max_features > 0.0:
                max_features = max(1, int(self.max_features * self.n_features_in_))
            else:
                max_features = 0

        max_leaf_nodes = -1 if self.max_leaf_nodes is None else self.max_leaf_nodes

        if len(y) != n_samples:
                "Number of labels=%d does not match number of samples=%d"
                % (len(y), n_samples)
            )

            sample_weight = _check_sample_weight(sample_weight, X, dtype=DOUBLE)

            else:

        # Set min_weight_leaf from min_weight_fraction_leaf
            min_weight_leaf = self.min_weight_fraction_leaf * n_samples
        else:
            min_weight_leaf = self.min_weight_fraction_leaf * np.sum(sample_weight)

        # Build tree
        criterion = self.criterion
        if not isinstance(criterion, Criterion):
            if is_classification:
                criterion = CRITERIA_CLF[self.criterion](
                    self.n_outputs_, self.n_classes_
                )
            else:
                criterion = CRITERIA_REG[self.criterion](self.n_outputs_, n_samples)
        else:
            # Make a deepcopy in case the criterion has mutable attributes that
            # might be shared and modified concurrently during parallel fitting
            criterion = copy.deepcopy(criterion)

        SPLITTERS = SPARSE_SPLITTERS if issparse(X) else DENSE_SPLITTERS

        splitter = self.splitter
        if self.monotonic_cst is None:
            monotonic_cst = None
        else:
            if self.n_outputs_ > 1:
                    "Monotonicity constraints are not supported with multiple outputs."
                )
            # Check to correct monotonicity constraint' specification,
            # by applying element-wise logical conjunction
            # Note: we do not cast `np.asarray(self.monotonic_cst, dtype=np.int8)`
            # straight away here so as to generate error messages for invalid
            # values using the original values prior to any dtype related conversion.
            monotonic_cst = np.asarray(self.monotonic_cst)
            if monotonic_cst.shape[0] != X.shape[1]:
                    "monotonic_cst has shape {} but the input data "
                    "X has {} features.".format(monotonic_cst.shape[0], X.shape[1])
                )
            valid_constraints = np.isin(monotonic_cst, (-1, 0, 1))
            if not np.all(valid_constraints):
                unique_constaints_value = np.unique(monotonic_cst)
                    "monotonic_cst must be None or an array-like of -1, 0 or 1, but"
                    f" got {unique_constaints_value}"
                )
                if self.n_classes_[0] > 2:
                        "Monotonicity constraints are not supported with multiclass "
                        "classification"
                    )
                # Binary classification trees are built by constraining probabilities
                # of the *negative class* in order to make the implementation similar
                # to regression trees.
                # Since self.monotonic_cst encodes constraints on probabilities of the
                # *positive class*, all signs must be flipped.
                monotonic_cst *= -1

        if not isinstance(self.splitter, Splitter):
            splitter = SPLITTERS[self.splitter](
                criterion,
                self.max_features_,
                min_weight_leaf,
            )

            self.tree_ = Tree(self.n_features_in_, self.n_classes_, self.n_outputs_)
        else:
            self.tree_ = Tree(
                # TODO: tree shouldn't need this in this case
                np.array([1] * self.n_outputs_, dtype=np.intp),
                self.n_outputs_,
            )

        # Use BestFirst if max_leaf_nodes given; use DepthFirst otherwise
        if max_leaf_nodes < 0:
            builder = DepthFirstTreeBuilder(
                splitter,
                min_samples_split,
                max_depth,
                self.min_impurity_decrease,
            )
        else:
            builder = BestFirstTreeBuilder(
                splitter,
                max_leaf_nodes,
            )

        builder.build(self.tree_, X, y, sample_weight, missing_values_in_feature_mask)

        if self.n_outputs_ == 1 and is_classifier(self):

        self._prune_tree()

    def _validate_X_predict(self, X, check_input):
        """Validate the training data on predict (probabilities)."""
            if self._support_missing_values(X):
                ensure_all_finite = "allow-nan"
            else:
                ensure_all_finite = True
                self,
                X,
                ensure_all_finite=ensure_all_finite,
            )
            if issparse(X) and (
                X.indices.dtype != np.intc or X.indptr.dtype != np.intc
            ):
                raise ValueError("No support for np.int64 index based sparse matrices")
        else:
            # The number of features is checked regardless of `check_input`
            _check_n_features(self, X, reset=False)
        return X

    def predict(self, X, check_input=True):
        """Predict class or regression value for X.

        For a classification model, the predicted class for each sample in X is
        returned. For a regression model, the predicted value based on X is
        returned.

            Allow to bypass several input checking.
            Don't use this parameter unless you know what you're doing.

        Returns
        -------
            The predicted classes, or the predict values.
        """
        X = self._validate_X_predict(X, check_input)
        proba = self.tree_.predict(X)

        # Classification
                return self.classes_.take(np.argmax(proba, axis=1), axis=0)

            else:
                class_type = self.classes_[0].dtype
                predictions = np.zeros((n_samples, self.n_outputs_), dtype=class_type)
                for k in range(self.n_outputs_):
                    predictions[:, k] = self.classes_[k].take(
                        np.argmax(proba[:, k], axis=1), axis=0
                    )

                return predictions

        # Regression
        else:
                return proba[:, 0]

            else:
                return proba[:, :, 0]

    def apply(self, X, check_input=True):
        """Return the index of the leaf that each sample is predicted as.

        Returns
        -------
        X_leaves : array-like of shape (n_samples,)
            For each datapoint x in X, return the index of the leaf x
            ends up in. Leaves are numbered within
            ``[0; self.tree_.node_count)``, possibly with gaps in the
            numbering.
        """
        return self.tree_.apply(X)

    def decision_path(self, X, check_input=True):
        """Return the decision path in the tree.

        Returns
        -------
            Return a node indicator CSR matrix where non zero elements
            indicates that the samples goes through the nodes.
        """
        return self.tree_.decision_path(X)

    def _prune_tree(self):
        """Prune tree using Minimal Cost-Complexity Pruning."""

        if self.ccp_alpha == 0.0:
            return

        # build pruned tree
            n_classes = np.atleast_1d(self.n_classes_)
            pruned_tree = Tree(self.n_features_in_, n_classes, self.n_outputs_)
        else:
            pruned_tree = Tree(
                # TODO: the tree shouldn't need this param
            )
        _build_pruned_tree_ccp(pruned_tree, self.tree_, self.ccp_alpha)

        self.tree_ = pruned_tree

    def cost_complexity_pruning_path(self, X, y, sample_weight=None):
        """Compute the pruning path during Minimal Cost-Complexity Pruning.

        See :ref:`minimal_cost_complexity_pruning` for details on the pruning
        process.

            The training input samples. Internally, it will be converted to
            to a sparse ``csc_matrix``.

            The target values (class labels) as integers or strings.

            ignored while searching for a split in each node. Splits are also
            ignored if they would result in any single class carrying a
            negative weight in either child node.

        Returns
        -------
        ccp_path : :class:`~sklearn.utils.Bunch`
            Dictionary-like object, with the following attributes.

            ccp_alphas : ndarray
                Effective alphas of subtree during pruning.

            impurities : ndarray
                Sum of the impurities of the subtree leaves for the
                corresponding alpha value in ``ccp_alphas``.
        """
        est = clone(self).set_params(ccp_alpha=0.0)
        est.fit(X, y, sample_weight=sample_weight)
        return Bunch(**ccp_pruning_path(est.tree_))

    @property
        """Return the feature importances.

        The importance of a feature is computed as the (normalized) total
        reduction of the criterion brought by that feature.
        It is also known as the Gini importance.

        Returns
        -------
            Normalized total reduction of criteria by feature
            (Gini importance).
        """

        return self.tree_.compute_feature_importances()

class DecisionTreeClassifier(ClassifierMixin, BaseDecisionTree):
    """A decision tree classifier.

    Read more in the :ref:`User Guide <tree>`.

    splitter : {"best", "random"}, default="best"
        The strategy used to choose the split at each node. Supported
        strategies are "best" to choose the best split and "random" to choose
        the best random split.

    max_features : int, float or {"sqrt", "log2"}, default=None

          `max(1, int(max_features * n_features_in_))` features are considered at
          each split.

        .. note::

            The search for a split does not stop until at least one
            valid partition of the node samples is found, even if it requires to
            effectively inspect more than ``max_features`` features.

        Controls the randomness of the estimator. The features are always
        randomly permuted at each split, even if ``splitter`` is set to
        ``"best"``. When ``max_features < n_features``, the algorithm will
        select ``max_features`` at random at each split before finding the best
        split among them. But the best found split may vary across different
        runs, even if ``max_features=n_features``. That is the case, if the
        improvement of the criterion is identical for several splits and one
        split has to be selected at random. To obtain a deterministic behaviour
        during fitting, ``random_state`` has to be fixed to an integer.

        Grow a tree with ``max_leaf_nodes`` in best-first fashion.

    class_weight : dict, list of dict or "balanced", default=None
        If None, all classes are supposed to have weight one. For

    classes_ : ndarray of shape (n_classes,) or list of ndarray
        The classes labels (single output problem),
        or a list of arrays of class labels (multi-output problem).

        known as the Gini importance [4]_.

        The number of classes (for single output problems),
        or a list containing the number of classes for each
        output (for multi-output problems).

    tree_ : Tree instance
        The underlying Tree object. Please refer to
        ``help(sklearn.tree._tree.Tree)`` for attributes of Tree object and
        :ref:`sphx_glr_auto_examples_tree_plot_unveil_tree_structure.py`
        for basic usage of these attributes.

    See Also
    --------
    DecisionTreeRegressor : A decision tree regressor.

    Notes
    -----

    The :meth:`predict` method operates using the :func:`numpy.argmax`
    function on the outputs of :meth:`predict_proba`. This means that in
    case the highest predicted probabilities are tied, the classifier will
    predict the tied class with the lowest index in :term:`classes_`.

    .. [1] https://en.wikipedia.org/wiki/Decision_tree_learning

    .. [2] L. Breiman, J. Friedman, R. Olshen, and C. Stone, "Classification
           and Regression Trees", Wadsworth, Belmont, CA, 1984.

    .. [3] T. Hastie, R. Tibshirani and J. Friedman. "Elements of Statistical
           Learning", Springer, 2009.

    .. [4] L. Breiman, and A. Cutler, "Random Forests",
           https://www.stat.berkeley.edu/~breiman/RandomForests/cc_home.htm

    Examples
    --------
    >>> from sklearn.model_selection import cross_val_score
    >>> clf = DecisionTreeClassifier(random_state=0)
    >>> iris = load_iris()
    >>> cross_val_score(clf, iris.data, iris.target, cv=10)
    ...                             # doctest: +SKIP
    ...
    array([ 1.     ,  0.93...,  0.86...,  0.93...,  0.93...,
            0.93...,  0.93...,  1.     ,  0.93...,  1.      ])
    """

    __metadata_request__predict_proba = {"check_input": metadata_routing.UNUSED}
    __metadata_request__fit = {"check_input": metadata_routing.UNUSED}

        "criterion": [StrOptions({"gini", "entropy", "log_loss"}), Hidden(Criterion)],
        "class_weight": [dict, list, StrOptions({"balanced"}), None],
    }

        self,
        *,
        splitter="best",
    ):
            splitter=splitter,
        )

    def fit(self, X, y, sample_weight=None, check_input=True):
        """Build a decision tree classifier from the training set (X, y).

        Returns
        -------
        self : DecisionTreeClassifier
        """

            X,
            y,
            check_input=check_input,
        )

    def predict_proba(self, X, check_input=True):
        """Predict class probabilities of the input samples X.

        The predicted class probability is the fraction of samples of the same
        class in a leaf.

        Returns
        -------
        proba : ndarray of shape (n_samples, n_classes) or list of n_outputs \
            such arrays if n_outputs > 1
        """

            return proba[:, : self.n_classes_]
        else:
            all_proba = []
                proba_k = proba[:, k, : self.n_classes_[k]]
                all_proba.append(proba_k)

        """Predict class log-probabilities of the input samples X.

        Returns
        -------
        """

        else:

        # XXX: nan is only support for dense arrays, but we set this for common test to
        # pass, specifically: check_estimators_nan_inf
        allow_nan = self.splitter in ("best", "random") and self.criterion in {
            "gini",
            "log_loss",
            "entropy",
        }
        tags.input_tags.allow_nan = allow_nan

class DecisionTreeRegressor(RegressorMixin, BaseDecisionTree):
    """A decision tree regressor.

    criterion : {"squared_error", "friedman_mse", "absolute_error", \
            "poisson"}, default="squared_error"
        uses reduction in the half mean Poisson deviance to find splits.

            Poisson deviance criterion.

        For an example of how ``max_depth`` influences the model, see
        :ref:`sphx_glr_auto_examples_tree_plot_tree_regression.py`.

          split.

        The feature importances.
        The importance of a feature is computed as the
        (normalized) total reduction of the criterion brought
        by that feature. It is also known as the Gini importance [4]_.

    See Also
    --------
    DecisionTreeClassifier : A decision tree classifier.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.tree import DecisionTreeRegressor
    >>> regressor = DecisionTreeRegressor(random_state=0)
    >>> cross_val_score(regressor, X, y, cv=10)
    ...                    # doctest: +SKIP
    ...
    array([-0.39..., -0.46...,  0.02...,  0.06..., -0.50...,
           0.16...,  0.11..., -0.73..., -0.30..., -0.00...])
    """

        "criterion": [
            StrOptions({"squared_error", "friedman_mse", "absolute_error", "poisson"}),
            Hidden(Criterion),
        ],
    }

        self,
        *,
    ):
        )

        """Build a decision tree regressor from the training set (X, y).

            The target values (real numbers). Use ``dtype=np.float64`` and
            ``order='C'`` for maximum efficiency.

            ignored while searching for a split in each node.

        Returns
        -------
        self : DecisionTreeRegressor
        """

            X,
            y,
        )

        Returns
        -------
        averaged_predictions : ndarray of shape (n_samples,), dtype=np.float64
        """
        )

        self.tree_.compute_partial_dependence(
            grid, target_features, averaged_predictions
        )

            "squared_error",
            "friedman_mse",
            "poisson",
        }

class ExtraTreeClassifier(DecisionTreeClassifier):
    """An extremely randomized tree classifier.

    Extra-trees differ from classic decision trees in the way they are built.
    When looking for the best split to separate the samples of a node into two
    groups, random splits are drawn for each of the `max_features` randomly
    selected features and the best split among those is chosen. When
    `max_features` is set 1, this amounts to building a totally random
    decision tree.

    Warning: Extra-trees should only be used within ensemble methods.

    splitter : {"random", "best"}, default="random"

    max_features : int, float, {"sqrt", "log2"} or None, default="sqrt"

        Used to pick randomly the `max_features` used at each split.

    See Also
    --------
    ExtraTreeRegressor : An extremely randomized tree regressor.
    sklearn.ensemble.ExtraTreesClassifier : An extra-trees classifier.
    sklearn.ensemble.ExtraTreesRegressor : An extra-trees regressor.
    sklearn.ensemble.RandomForestClassifier : A random forest classifier.
    sklearn.ensemble.RandomTreesEmbedding : An ensemble of
        totally random trees.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.tree import ExtraTreeClassifier
    ...    X, y, random_state=0)
    >>> extra_tree = ExtraTreeClassifier(random_state=0)
    >>> cls = BaggingClassifier(extra_tree, random_state=0).fit(
    >>> cls.score(X_test, y_test)
    0.8947...
    """

        self,
        *,
        splitter="random",
    ):
        )

        # XXX: nan is only supported for dense arrays, but we set this for the
        # common test to pass, specifically: check_estimators_nan_inf
        allow_nan = self.splitter == "random" and self.criterion in {
            "gini",
        }

class ExtraTreeRegressor(DecisionTreeRegressor):
    """An extremely randomized tree regressor.

    criterion : {"squared_error", "friedman_mse", "absolute_error", "poisson"}, \

    max_features : int, float, {"sqrt", "log2"} or None, default=1.0

          split.

            The default of `max_features` changed from `"auto"` to `1.0`.

        Return impurity-based feature importances (the higher, the more
        important the feature).

    See Also
    --------
    ExtraTreeClassifier : An extremely randomized tree classifier.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.tree import ExtraTreeRegressor
    >>> extra_tree = ExtraTreeRegressor(random_state=0)
    >>> reg = BaggingRegressor(extra_tree, random_state=0).fit(
    ...     X_train, y_train)
    0.33...
    """

        self,
        *,
    ):
        )

        }

        self,
        *,
        splitter,
    ):

        Returns
        -------
        """

        Returns
        -------
        """

        return (
        )

        Parameter
        ---------

        Returns
        -------
        """

    def _fit(
        self,
        X,
        y,
    ):

            # csr.

            )
            )

            )

                    )

                    )
                    )

                )

            else:

            )

            else:

        else:

                )
            else:
        else:

        else:
                )
                )
                )
                    )

            )

        else:
            )

                splitter,
            )
        else:
                splitter,
            )

            else:
                self,
                X,
            )
            ):
        else:
        return X

        returned.

        Returns
        -------
        """

            else:
                    )

        else:

            else:

        Returns
        -------
        """

        Returns
        -------
        """

            return

        else:
            )

        process.

        Returns
        -------

        """

        Returns
        -------
        """

        self,
        *,
    ):
        )

        Returns
        -------
        """

            X,
            y,
        )

        Returns
        -------
        """

        else:

        Returns
        -------
        """

        else:

            "gini",
        }

        self,
        *,
    ):
        )

        Returns
        -------
        """

            X,
            y,
        )

        Returns
        -------
        """
        )

        )

        }

        self,
        *,
    ):
        )

            "gini",
        }

        self,
        *,
    ):
        )

        }

def _color_brew(n):
    """Generate n colors with equally spaced hues.

    n : int
        The number of colors required.

    Returns
    -------
    color_list : list, length n
        List of n tuples of form (R, G, B) being the components of each color.
    """
    color_list = []

    # Initialize saturation & value; calculate chroma & value shift
    s, v = 0.75, 0.9
    c = s * v
    m = v - c

    for h in np.arange(25, 385, 360.0 / n).astype(int):
        # Calculate some intermediate values
        h_bar = h / 60.0
        x = c * (1 - abs((h_bar % 2) - 1))
        # Initialize RGB with same hue & chroma as our color
        rgb = [
            (c, x, 0),
            (x, c, 0),
            (0, c, x),
            (0, x, c),
            (x, 0, c),
            (c, 0, x),
            (c, x, 0),
        ]
        r, g, b = rgb[int(h_bar)]
        # Shift the initial RGB values to match value and store
        rgb = [(int(255 * (r + m))), (int(255 * (g + m))), (int(255 * (b + m)))]
        color_list.append(rgb)

    return color_list

class Sentinel:
        return '"tree.dot"'

def plot_tree(
    decision_tree,
    *,
    max_depth=None,
    feature_names=None,
    class_names=None,
    label="all",
    filled=False,
    impurity=True,
    node_ids=False,
    proportion=False,
    rounded=False,
    precision=3,
    ax=None,
    fontsize=None,
):
    """Plot a decision tree.

    The sample counts that are shown are weighted with any sample_weights that
    might be present.

    The visualization is fit automatically to the size of the axis.
    Use the ``figsize`` or ``dpi`` arguments of ``plt.figure``  to control
    the size of the rendering.

    decision_tree : decision tree regressor or classifier
        The decision tree to be plotted.

        The maximum depth of the representation. If None, the tree is fully
        generated.

    feature_names : array-like of str, default=None
        Names of each of the features.
        If None, generic names will be used ("x[0]", "x[1]", ...).

    class_names : array-like of str or True, default=None
        Names of each of the target classes in ascending numerical order.
        Only relevant for classification and not supported for multi-output.
        If ``True``, shows a symbolic representation of the class name.

    label : {'all', 'root', 'none'}, default='all'
        Whether to show informative labels for impurity, etc.
        Options include 'all' to show at every node, 'root' to show only at
        the top root node, or 'none' to not show at any node.

    filled : bool, default=False
        When set to ``True``, paint nodes to indicate majority class for
        classification, extremity of values for regression, or purity of node
        for multi-output.

    impurity : bool, default=True
        When set to ``True``, show the impurity at each node.

    node_ids : bool, default=False
        When set to ``True``, show the ID number on each node.

    proportion : bool, default=False
        When set to ``True``, change the display of 'values' and/or 'samples'
        to be proportions and percentages respectively.

    rounded : bool, default=False
        When set to ``True``, draw node boxes with rounded corners and use
        Helvetica fonts instead of Times-Roman.

    precision : int, default=3
        Number of digits of precision for floating point in the values of
        impurity, threshold and value attributes of each node.

    ax : matplotlib axis, default=None
        Axes to plot to. If None, use current axis. Any previous content
        is cleared.

    fontsize : int, default=None
        Size of text font. If None, determined automatically to fit figure.

    Returns
    -------
    annotations : list of artists
        List containing the artists for the annotation boxes making up the
        tree.

    Examples
    --------
    >>> from sklearn import tree

    >>> clf = tree.DecisionTreeClassifier(random_state=0)

    >>> clf = clf.fit(iris.data, iris.target)
    >>> tree.plot_tree(clf)
    [...]
    """

    check_is_fitted(decision_tree)

    exporter = _MPLTreeExporter(
        max_depth=max_depth,
        class_names=class_names,
        label=label,
        filled=filled,
        impurity=impurity,
        node_ids=node_ids,
        proportion=proportion,
        rounded=rounded,
        precision=precision,
        fontsize=fontsize,
    )
    return exporter.export(decision_tree, ax=ax)

class _BaseTreeExporter:
        self,
        feature_names=None,
        class_names=None,
        label="all",
        filled=False,
        impurity=True,
        node_ids=False,
        proportion=False,
        rounded=False,
        precision=3,
        fontsize=None,
    ):
        self.feature_names = feature_names
        self.class_names = class_names
        self.label = label
        self.filled = filled
        self.impurity = impurity
        self.node_ids = node_ids
        self.proportion = proportion
        self.rounded = rounded
        self.fontsize = fontsize

    def get_color(self, value):
        # Find the appropriate color & intensity for a node
        if self.colors["bounds"] is None:
            # Classification tree
            color = list(self.colors["rgb"][np.argmax(value)])
            sorted_values = sorted(value, reverse=True)
            if len(sorted_values) == 1:
                alpha = 0.0
            else:
                alpha = (sorted_values[0] - sorted_values[1]) / (1 - sorted_values[1])
        else:
            # Regression tree or multi-output
            color = list(self.colors["rgb"][0])
            alpha = (value - self.colors["bounds"][0]) / (
                self.colors["bounds"][1] - self.colors["bounds"][0]
            )
        # compute the color as alpha against white
        color = [int(round(alpha * c + (1 - alpha) * 255, 0)) for c in color]
        # Return html color code in #RRGGBB format
        return "#%2x%2x%2x" % tuple(color)

    def get_fill_color(self, tree, node_id):
        # Fetch appropriate color for node
        if "rgb" not in self.colors:
            # Initialize colors and bounds if required
            self.colors["rgb"] = _color_brew(tree.n_classes[0])
            if tree.n_outputs != 1:
                # Find max and min impurities for multi-output
                # The next line uses -max(impurity) instead of min(-impurity)
                # and -min(impurity) instead of max(-impurity) on purpose, in
                # order to avoid what looks like an issue with SIMD on non
                # memory aligned arrays on 32bit OS. For more details see
                # https://github.com/scikit-learn/scikit-learn/issues/27506.
                self.colors["bounds"] = (-np.max(tree.impurity), -np.min(tree.impurity))
            elif tree.n_classes[0] == 1 and len(np.unique(tree.value)) != 1:
                # Find max and min values in leaf nodes for regression
                self.colors["bounds"] = (np.min(tree.value), np.max(tree.value))
        if tree.n_outputs == 1:
            node_val = tree.value[node_id][0, :]
            if (
                tree.n_classes[0] == 1
                and isinstance(node_val, Iterable)
                and self.colors["bounds"] is not None
            ):
                # Unpack the float only for the regression tree case.
                # Classification tree requires an Iterable in `get_color`.
                node_val = node_val.item()
        else:
            # If multi-output color node by impurity
            node_val = -tree.impurity[node_id]
        return self.get_color(node_val)

    def node_to_str(self, tree, node_id, criterion):
        # Generate the node content string
            value = tree.value[node_id][0, :]
        else:
            value = tree.value[node_id]

        # Should labels be shown?
        labels = (self.label == "root" and node_id == 0) or self.label == "all"

        characters = self.characters
        node_string = characters[-1]

        # Write node ID
        if self.node_ids:
            if labels:
                node_string += "node "
            node_string += characters[0] + str(node_id) + characters[4]

        # Write decision criteria
        if tree.children_left[node_id] != _tree.TREE_LEAF:
            # Always write node decision criteria, except for leaves
            if self.feature_names is not None:
                feature = self.feature_names[tree.feature[node_id]]
                feature = self.str_escape(feature)
            else:
                feature = "x%s%s%s" % (
                    characters[1],
                    tree.feature[node_id],
                    characters[2],
                )
            node_string += "%s %s %s%s" % (
                feature,
                characters[3],
                round(tree.threshold[node_id], self.precision),
                characters[4],
            )

        # Write impurity
        if self.impurity:
            if isinstance(criterion, _criterion.FriedmanMSE):
                criterion = "friedman_mse"
            elif isinstance(criterion, _criterion.MSE) or criterion == "squared_error":
                criterion = "squared_error"
            elif not isinstance(criterion, str):
                criterion = "impurity"
            if labels:
                node_string += "%s = " % criterion
            node_string += (
                str(round(tree.impurity[node_id], self.precision)) + characters[4]
            )

        # Write node sample count
        if labels:
            node_string += "samples = "
        if self.proportion:
            percent = (
                100.0 * tree.n_node_samples[node_id] / float(tree.n_node_samples[0])
            )
            node_string += str(round(percent, 1)) + "%" + characters[4]
        else:
            node_string += str(tree.n_node_samples[node_id]) + characters[4]

        # Write node class distribution / regression value
        if not self.proportion and tree.n_classes[0] != 1:
            # For classification this will show the proportion of samples
            value = value * tree.weighted_n_node_samples[node_id]
        if labels:
            node_string += "value = "
        if tree.n_classes[0] == 1:
            # Regression
            value_text = np.around(value, self.precision)
        elif self.proportion:
            # Classification
        elif np.all(np.equal(np.mod(value, 1), 0)):
            # Classification without floating-point weights
            value_text = value.astype(int)
        else:
            # Classification with floating-point weights
        # Strip whitespace
        value_text = str(value_text.astype("S32")).replace("b'", "'")
        value_text = value_text.replace("' '", ", ").replace("'", "")
        if tree.n_classes[0] == 1 and tree.n_outputs == 1:
            value_text = value_text.replace("[", "").replace("]", "")
        value_text = value_text.replace("\n ", characters[4])
        node_string += value_text + characters[4]

        # Write node majority class
        if (
            self.class_names is not None
            and tree.n_classes[0] != 1
            and tree.n_outputs == 1
        ):
            # Only done for single-output classification trees
            if labels:
                node_string += "class = "
            if self.class_names is not True:
                class_name = self.class_names[np.argmax(value)]
                class_name = self.str_escape(class_name)
            else:
                class_name = "y%s%s%s" % (
                    np.argmax(value),
                )
            node_string += class_name

        # Clean up any trailing newlines
        if node_string.endswith(characters[4]):
            node_string = node_string[: -len(characters[4])]

        return node_string + characters[5]

    def str_escape(self, string):
        return string

class _DOTTreeExporter(_BaseTreeExporter):
        self,
        out_file=SENTINEL,
        leaves_parallel=False,
        rotate=False,
        special_characters=False,
        fontname="helvetica",
    ):
            feature_names=feature_names,
            class_names=class_names,
            filled=filled,
            impurity=impurity,
            node_ids=node_ids,
            proportion=proportion,
            rounded=rounded,
        )
        self.leaves_parallel = leaves_parallel
        self.out_file = out_file
        self.special_characters = special_characters
        self.fontname = fontname
        self.rotate = rotate

        # PostScript compatibility for special characters
        if special_characters:
            self.characters = ["&#35;", "<SUB>", "</SUB>", "&le;", "<br/>", ">", "<"]
        else:
            self.characters = ["#", "[", "]", "<=", "\\n", '"', '"']

        # The depth of each node for plotting with 'leaf' option
        self.ranks = {"leaves": []}
        # The colors to render each node with
        self.colors = {"bounds": None}

    def export(self, decision_tree):
        # Check length of feature_names before getting into the tree node
        # Raise error if length of feature_names does not match
        # n_features_in_ in the decision_tree
        if self.feature_names is not None:
            if len(self.feature_names) != decision_tree.n_features_in_:
                    "Length of feature_names, %d does not match number of features, %d"
                    % (len(self.feature_names), decision_tree.n_features_in_)
                )
        # each part writes to out_file
        self.head()
        # Now recurse the tree and add node & edge attributes
        if isinstance(decision_tree, _tree.Tree):
            self.recurse(decision_tree, 0, criterion="impurity")
        else:
            self.recurse(decision_tree.tree_, 0, criterion=decision_tree.criterion)

        self.tail()

    def tail(self):
        # If required, draw leaf nodes at same depth as each other
        if self.leaves_parallel:
            for rank in sorted(self.ranks):
                self.out_file.write(
                    "{rank=same ; " + "; ".join(r for r in self.ranks[rank]) + "} ;\n"
                )
        self.out_file.write("}")

    def head(self):
        self.out_file.write("digraph Tree {\n")

        # Specify node aesthetics
        self.out_file.write("node [shape=box")
        rounded_filled = []
        if self.filled:
            rounded_filled.append("filled")
        if self.rounded:
            rounded_filled.append("rounded")
        if len(rounded_filled) > 0:
            self.out_file.write(
                ', style="%s", color="black"' % ", ".join(rounded_filled)
            )

        self.out_file.write(', fontname="%s"' % self.fontname)
        self.out_file.write("] ;\n")

        # Specify graph & edge aesthetics
            self.out_file.write("graph [ranksep=equally, splines=polyline] ;\n")

        self.out_file.write('edge [fontname="%s"] ;\n' % self.fontname)

        if self.rotate:
            self.out_file.write("rankdir=LR ;\n")

    def recurse(self, tree, node_id, criterion, parent=None, depth=0):
        if node_id == _tree.TREE_LEAF:
            raise ValueError("Invalid node_id %s" % _tree.TREE_LEAF)

        left_child = tree.children_left[node_id]
        right_child = tree.children_right[node_id]

        # Add node with description
        if self.max_depth is None or depth <= self.max_depth:
            # Collect ranks for 'leaf' option in plot_options
            if left_child == _tree.TREE_LEAF:
                self.ranks["leaves"].append(str(node_id))
            elif str(depth) not in self.ranks:
                self.ranks[str(depth)] = [str(node_id)]
            else:
                self.ranks[str(depth)].append(str(node_id))

                "%d [label=%s" % (node_id, self.node_to_str(tree, node_id, criterion))
            )

            if self.filled:
                    ', fillcolor="%s"' % self.get_fill_color(tree, node_id)
                )
            self.out_file.write("] ;\n")

            if parent is not None:
                # Add edge to parent
                self.out_file.write("%d -> %d" % (parent, node_id))
                if parent == 0:
                    # Draw True/False labels if parent is root node
                    angles = np.array([45, -45]) * ((self.rotate - 0.5) * -2)
                    self.out_file.write(" [labeldistance=2.5, labelangle=")
                    if node_id == 1:
                        self.out_file.write('%d, headlabel="True"]' % angles[0])
                    else:
                        self.out_file.write('%d, headlabel="False"]' % angles[1])
                self.out_file.write(" ;\n")

            if left_child != _tree.TREE_LEAF:
                self.recurse(
                    tree,
                    left_child,
                    criterion=criterion,
                    parent=node_id,
                    depth=depth + 1,
                )
                    tree,
                    right_child,
                )

        else:
            self.ranks["leaves"].append(str(node_id))

            self.out_file.write('%d [label="(...)"' % node_id)
                # color cropped nodes grey
                self.out_file.write(', fillcolor="#C0C0C0"')
            self.out_file.write("] ;\n" % node_id)

                self.out_file.write("%d -> %d ;\n" % (parent, node_id))

        # override default escaping for graphviz
        return string.replace('"', r"\"")

class _MPLTreeExporter(_BaseTreeExporter):
        self,
    ):
        )

        self.characters = ["#", "[", "]", "<=", "\n", "", ""]
        self.bbox_args = dict()
            self.bbox_args["boxstyle"] = "round"

        self.arrow_args = dict(arrowstyle="<-")

    def _make_tree(self, node_id, et, criterion, depth=0):
        # traverses _tree.Tree recursively, builds intermediate
        # "_reingold_tilford.Tree" object
        name = self.node_to_str(et, node_id, criterion=criterion)
        if et.children_left[node_id] != _tree.TREE_LEAF and (
            self.max_depth is None or depth <= self.max_depth
        ):
            children = [
                self._make_tree(
                    et.children_left[node_id], et, criterion, depth=depth + 1
                ),
                    et.children_right[node_id], et, criterion, depth=depth + 1
                ),
            ]
        else:
            return Tree(name, node_id)
        return Tree(name, node_id, *children)

    def export(self, decision_tree, ax=None):
        from matplotlib.text import Annotation

            ax = plt.gca()
        ax.clear()
        ax.set_axis_off()
        my_tree = self._make_tree(0, decision_tree.tree_, decision_tree.criterion)
        draw_tree = buchheim(my_tree)

        # important to make sure we're still
        # inside the axis after drawing the box
        # this makes sense because the width of a box
        # is about the same as the distance between boxes
        max_x, max_y = draw_tree.max_extents() + 1
        ax_width = ax.get_window_extent().width
        ax_height = ax.get_window_extent().height

        scale_x = ax_width / max_x
        scale_y = ax_height / max_y
        self.recurse(draw_tree, decision_tree.tree_, ax, max_x, max_y)

        anns = [ann for ann in ax.get_children() if isinstance(ann, Annotation)]

        # update sizes of all bboxes
        renderer = ax.figure.canvas.get_renderer()

        for ann in anns:
            ann.update_bbox_position_size(renderer)

        if self.fontsize is None:
            # get figure to data transform
            # adjust fontsize to avoid overlap
            # get max box width and height
            extents = [
                bbox_patch.get_window_extent()
                for ann in anns
                if (bbox_patch := ann.get_bbox_patch()) is not None
            ]
            max_width = max([extent.width for extent in extents])
            max_height = max([extent.height for extent in extents])
            # width should be around scale_x in axis coordinates
            size = anns[0].get_fontsize() * min(
                scale_x / max_width, scale_y / max_height
            )
            for ann in anns:
                ann.set_fontsize(size)

        return anns

    def recurse(self, node, tree, ax, max_x, max_y, depth=0):

        # kwargs for annotations without a bounding box
        common_kwargs = dict(
            zorder=100 - 10 * depth,
            xycoords="axes fraction",
        )
        if self.fontsize is not None:
            common_kwargs["fontsize"] = self.fontsize

        # kwargs for annotations with a bounding box
        kwargs = dict(
            ha="center",
            va="center",
            bbox=self.bbox_args.copy(),
            arrowprops=self.arrow_args.copy(),
            **common_kwargs,
        )
        kwargs["arrowprops"]["edgecolor"] = plt.rcParams["text.color"]

        # offset things by .5 to center them in plot
        xy = ((node.x + 0.5) / max_x, (max_y - node.y - 0.5) / max_y)

                kwargs["bbox"]["fc"] = self.get_fill_color(tree, node.tree.node_id)
            else:
                kwargs["bbox"]["fc"] = ax.get_facecolor()

            if node.parent is None:
                # root
                ax.annotate(node.tree.label, xy, **kwargs)
            else:
                xy_parent = (
                    (node.parent.x + 0.5) / max_x,
                    (max_y - node.parent.y - 0.5) / max_y,
                )
                ax.annotate(node.tree.label, xy_parent, xy, **kwargs)

                # Draw True/False labels if parent is root node
                if node.parent.parent is None:
                    # Adjust the position for the text to be slightly above the arrow
                    text_pos = (
                        (xy_parent[0] + xy[0]) / 2,
                        (xy_parent[1] + xy[1]) / 2,
                    )
                    # Annotate the arrow with the edge label to indicate the child
                    # where the sample-split condition is satisfied
                    if node.parent.left() == node:
                        label_text, label_ha = ("True  ", "right")
                    else:
                        label_text, label_ha = ("  False", "left")
                    ax.annotate(label_text, text_pos, ha=label_ha, **common_kwargs)
            for child in node.children:
                self.recurse(child, tree, ax, max_x, max_y, depth=depth + 1)

        else:
            xy_parent = (
                (node.parent.x + 0.5) / max_x,
                (max_y - node.parent.y - 0.5) / max_y,
            )
            kwargs["bbox"]["fc"] = "grey"
            ax.annotate("\n  (...)  \n", xy_parent, xy, **kwargs)

def export_graphviz(
    out_file=None,
    *,
    leaves_parallel=False,
    rotate=False,
    special_characters=False,
    fontname="helvetica",
):
    """Export a decision tree in DOT format.

    This function generates a GraphViz representation of the decision tree,
    which is then written into `out_file`. Once exported, graphical renderings
    can be generated using, for example::

        $ dot -Tps tree.dot -o tree.ps      (PostScript format)
        $ dot -Tpng tree.dot -o tree.png    (PNG format)

    decision_tree : object
        The decision tree estimator to be exported to GraphViz.

    out_file : object or str, default=None
        Handle or name of the output file. If ``None``, the result is
        returned as a string.

            Default of out_file changed from "tree.dot" to None.

    feature_names : array-like of shape (n_features,), default=None
        An array containing the feature names.

    class_names : array-like of shape (n_classes,) or bool, default=None

    leaves_parallel : bool, default=False
        When set to ``True``, draw all leaf nodes at the bottom of the tree.

    rotate : bool, default=False
        When set to ``True``, orient tree left to right rather than top-down.

        When set to ``True``, draw node boxes with rounded corners.

    special_characters : bool, default=False
        When set to ``False``, ignore special characters for PostScript
        compatibility.

    fontname : str, default='helvetica'
        Name of font used to render text.

    Returns
    -------
    dot_data : str
        String representation of the input tree in GraphViz dot format.
        Only returned if ``out_file`` is None.

    Examples
    --------

    >>> clf = tree.DecisionTreeClassifier()

    >>> tree.export_graphviz(clf)
    'digraph Tree {...
    """
    if feature_names is not None:
        feature_names = check_array(
            feature_names, ensure_2d=False, dtype=None, ensure_min_samples=0
        )
    if class_names is not None and not isinstance(class_names, bool):
        class_names = check_array(
            class_names, ensure_2d=False, dtype=None, ensure_min_samples=0
        )

    own_file = False
    return_string = False
    try:
        if isinstance(out_file, str):
            out_file = open(out_file, "w", encoding="utf-8")
            own_file = True

        if out_file is None:
            return_string = True
            out_file = StringIO()

        exporter = _DOTTreeExporter(
            out_file=out_file,
            leaves_parallel=leaves_parallel,
            rotate=rotate,
            special_characters=special_characters,
            fontname=fontname,
        )
        exporter.export(decision_tree)

        if return_string:
            return exporter.out_file.getvalue()

    finally:
        if own_file:
            out_file.close()

def _compute_depth(tree, node):
    """
    Returns the depth of the subtree rooted in node.
    """

    def compute_depth_(
        current_node, current_depth, children_left, children_right, depths
    ):
        depths += [current_depth]
        left = children_left[current_node]
        right = children_right[current_node]
        if left != -1 and right != -1:
            compute_depth_(
                left, current_depth + 1, children_left, children_right, depths
            )
                right, current_depth + 1, children_left, children_right, depths
            )

    depths = []
    compute_depth_(node, 1, tree.children_left, tree.children_right, depths)
    return max(depths)

def export_text(
    *,
    max_depth=10,
    spacing=3,
    decimals=2,
    show_weights=False,
):
    """Build a text report showing the rules of a decision tree.

    Note that backwards compatibility may not be supported.

        The decision tree estimator to be exported.
        It can be an instance of
        DecisionTreeClassifier or DecisionTreeRegressor.

        If None generic names will be used ("feature_0", "feature_1", ...).

    class_names : array-like of shape (n_classes,), default=None

        - if `None`, the class names are delegated to `decision_tree.classes_`;
        - otherwise, `class_names` will be used as class names instead of
          `decision_tree.classes_`. The length of `class_names` must match
          the length of `decision_tree.classes_`.

    max_depth : int, default=10
        Only the first max_depth levels of the tree are exported.
        Truncated branches will be marked with "...".

    spacing : int, default=3
        Number of spaces between edges. The higher it is, the wider the result.

    decimals : int, default=2
        Number of decimal digits to display.

    show_weights : bool, default=False
        If true the classification weights will be exported on each leaf.
        The classification weights are the number of samples each class.

    Returns
    -------
    report : str
        Text summary of all the rules in the decision tree.

    Examples
    --------

    >>> from sklearn.tree import export_text
    >>> X = iris['data']
    >>> y = iris['target']
    >>> decision_tree = DecisionTreeClassifier(random_state=0, max_depth=2)
    >>> decision_tree = decision_tree.fit(X, y)
    >>> r = export_text(decision_tree, feature_names=iris['feature_names'])
    >>> print(r)
    |--- petal width (cm) <= 0.80
    |   |--- class: 0
    |--- petal width (cm) >  0.80
    |   |--- petal width (cm) <= 1.75
    |   |   |--- class: 1
    |   |--- petal width (cm) >  1.75
    |   |   |--- class: 2
    """
        )
    if class_names is not None:
        )

    tree_ = decision_tree.tree_
    if is_classifier(decision_tree):
        if class_names is None:
            class_names = decision_tree.classes_
        elif len(class_names) != len(decision_tree.classes_):
                "When `class_names` is an array, it should contain as"
                " many items as `decision_tree.classes_`. Got"
                f" {len(class_names)} while the tree was fitted with"
                f" {len(decision_tree.classes_)} classes."
            )
    right_child_fmt = "{} {} <= {}\n"
    left_child_fmt = "{} {} >  {}\n"
    truncation_fmt = "{} {}\n"

    if feature_names is not None and len(feature_names) != tree_.n_features:
            "feature_names must contain %d elements, got %d"
            % (tree_.n_features, len(feature_names))
        )

    if isinstance(decision_tree, DecisionTreeClassifier):
        value_fmt = "{}{} weights: {}\n"
        if not show_weights:
            value_fmt = "{}{}{}\n"
    else:
        value_fmt = "{}{} value: {}\n"

        feature_names_ = [
            feature_names[i] if i != _tree.TREE_UNDEFINED else None
            for i in tree_.feature
        ]
    else:
        feature_names_ = ["feature_{}".format(i) for i in tree_.feature]

    export_text.report = ""

    def _add_leaf(value, weighted_n_node_samples, class_name, indent):
        val = ""
        if isinstance(decision_tree, DecisionTreeClassifier):
            if show_weights:
                val = [
                    "{1:.{0}f}, ".format(decimals, v * weighted_n_node_samples)
                    for v in value
                ]
                val = "[" + "".join(val)[:-2] + "]"
                weighted_n_node_samples
            val += " class: " + str(class_name)
        else:
            val = ["{1:.{0}f}, ".format(decimals, v) for v in value]
            val = "[" + "".join(val)[:-2] + "]"
        export_text.report += value_fmt.format(indent, "", val)

    def print_tree_recurse(node, depth):
        indent = ("|" + (" " * spacing)) * depth
        indent = indent[:-spacing] + "-" * spacing

        value = None
        if tree_.n_outputs == 1:
            value = tree_.value[node][0]
        else:
            value = tree_.value[node].T[0]
        class_name = np.argmax(value)

        if tree_.n_classes[0] != 1 and tree_.n_outputs == 1:
            class_name = class_names[class_name]

        weighted_n_node_samples = tree_.weighted_n_node_samples[node]

        if depth <= max_depth + 1:
            info_fmt = ""
            info_fmt_left = info_fmt
            info_fmt_right = info_fmt

            if tree_.feature[node] != _tree.TREE_UNDEFINED:
                name = feature_names_[node]
                threshold = tree_.threshold[node]
                threshold = "{1:.{0}f}".format(decimals, threshold)
                export_text.report += right_child_fmt.format(indent, name, threshold)
                export_text.report += info_fmt_left
                print_tree_recurse(tree_.children_left[node], depth + 1)

                export_text.report += left_child_fmt.format(indent, name, threshold)
                export_text.report += info_fmt_right
                print_tree_recurse(tree_.children_right[node], depth + 1)
            else:  # leaf
                _add_leaf(value, weighted_n_node_samples, class_name, indent)
        else:
            subtree_depth = _compute_depth(tree_, node)
            if subtree_depth == 1:
            else:
                trunc_report = "truncated branch of depth %d" % subtree_depth
                export_text.report += truncation_fmt.format(indent, trunc_report)

    print_tree_recurse(0, 1)
    return export_text.report

        self,
    ):

                alpha = 0.0
            else:
        else:
            )

            if (
            ):
        else:

        else:

            if labels:

            else:
                )
                feature,
            )

            if labels:
            )

        if labels:
            percent = (
            )
        else:

        if labels:
        else:

        if (
        ):
            if labels:
            else:
                )

        self,
    ):
        )

        else:

                )
        else:

                )

            )

            else:

            )

                )

                    else:

                    tree,
                )
                    tree,
                )

        else:

        self,
    ):
        )

        ):
                ),
                ),
            ]
        else:

            extents = [
            ]
            )

        )

        )

            else:

                # root
            else:
                )

                    )
                    else:

        else:
            )

    ):
            )
            )

        val = ""
                val = [
                ]
        else:

        else:

        else:
            else:

class DrawTree:
    def __init__(self, tree, parent=None, depth=0, number=1):
        self.x = -1.0
        self.y = depth
        self.tree = tree
        self.children = [
            DrawTree(c, self, depth + 1, i + 1) for i, c in enumerate(tree.children)
        ]
        self.parent = parent
        self.thread = None
        self.mod = 0
        self.ancestor = self
        self.change = self.shift = 0
        self._lmost_sibling = None
        # this is the number of the node in its group of siblings 1..n
        self.number = number

    def left(self):
        return self.thread or (len(self.children) and self.children[0])

    def right(self):
        return self.thread or (len(self.children) and self.children[-1])

    def lbrother(self):
        n = None
        if self.parent:
            for node in self.parent.children:
                if node == self:
                    return n
                else:
                    n = node
        return n

    def get_lmost_sibling(self):
        if not self._lmost_sibling and self.parent and self != self.parent.children[0]:
            self._lmost_sibling = self.parent.children[0]
        return self._lmost_sibling

    lmost_sibling = property(get_lmost_sibling)

        return "%s: x=%s mod=%s" % (self.tree, self.x, self.mod)

        return self.__str__()

    def max_extents(self):
        extents = [c.max_extents() for c in self.children]
        extents.append((self.x, self.y))
        return np.max(extents, axis=0)

def buchheim(tree):
    dt = first_walk(DrawTree(tree))
    min = second_walk(dt)
    if min < 0:
        third_walk(dt, -min)
    return dt

def third_walk(tree, n):
    tree.x += n
    for c in tree.children:
        third_walk(c, n)

def first_walk(v, distance=1.0):
    if len(v.children) == 0:
        if v.lmost_sibling:
            v.x = v.lbrother().x + distance
        else:
            v.x = 0.0
    else:
        default_ancestor = v.children[0]
        for w in v.children:
            first_walk(w)
            default_ancestor = apportion(w, default_ancestor, distance)
        # print("finished v =", v.tree, "children")
        execute_shifts(v)

        midpoint = (v.children[0].x + v.children[-1].x) / 2

        w = v.lbrother()
        if w:
            v.x = w.x + distance
            v.mod = v.x - midpoint
        else:
            v.x = midpoint
    return v

def apportion(v, default_ancestor, distance):
    w = v.lbrother()
    if w is not None:
        # in buchheim notation:
        # i == inner; o == outer; r == right; l == left; r = +; l = -
        vir = vor = v
        vil = w
        vol = v.lmost_sibling
        sir = sor = v.mod
        sil = vil.mod
        sol = vol.mod
        while vil.right() and vir.left():
            vil = vil.right()
            vir = vir.left()
            vol = vol.left()
            vor = vor.right()
            vor.ancestor = v
            shift = (vil.x + sil) - (vir.x + sir) + distance
            if shift > 0:
                move_subtree(ancestor(vil, v, default_ancestor), v, shift)
                sir = sir + shift
                sor = sor + shift
            sil += vil.mod
            sir += vir.mod
            sol += vol.mod
            sor += vor.mod
        if vil.right() and not vor.right():
            vor.thread = vil.right()
            vor.mod += sil - sor
        else:
            if vir.left() and not vol.left():
                vol.thread = vir.left()
                vol.mod += sir - sol
            default_ancestor = v
    return default_ancestor

def move_subtree(wl, wr, shift):
    subtrees = wr.number - wl.number
    # print(wl.tree, "is conflicted with", wr.tree, 'moving', subtrees,
    # 'shift', shift)
    # print wl, wr, wr.number, wl.number, shift, subtrees, shift/subtrees
    wr.change -= shift / subtrees
    wr.shift += shift
    wl.change += shift / subtrees
    wr.x += shift
    wr.mod += shift

def execute_shifts(v):
    shift = change = 0
    for w in v.children[::-1]:
        # print("shift:", w, shift, w.change)
        w.x += shift
        w.mod += shift
        change += w.change
        shift += w.shift + change

def ancestor(vil, v, default_ancestor):
    # the relevant text is at the bottom of page 7 of
    # "Improving Walker's Algorithm to Run in Linear Time" by Buchheim et al,
    # (2002)
    # https://citeseerx.ist.psu.edu/doc_view/pid/1f41c3c2a4880dc49238e46d555f16d28da2940d
    if vil.ancestor in v.parent.children:
        return vil.ancestor
    else:
        return default_ancestor

def second_walk(v, m=0, depth=0, min=None):
    v.x += m
    v.y = depth

    if min is None or v.x < min:
        min = v.x

    for w in v.children:
        min = second_walk(w, m + v.mod, depth + 1, min)

    return min

class Tree:
    def __init__(self, label="", node_id=-1, *children):
        self.node_id = node_id
        if children:
            self.children = children
        else:
            self.children = []

        ]

        n = None
                    return n
                else:
                    n = node
        return n

        else:

def test_monotonic_constraints_classifications(
    TreeClassifier,
    depth_first_builder,
    sparse_splitter,
    csc_container,
):
    n_samples_train = 900
    )
    X_train, y_train = X[:n_samples_train], y[:n_samples_train]
    X_test, _ = X[n_samples_train:], y[n_samples_train:]

    X_test_0incr, X_test_0decr = np.copy(X_test), np.copy(X_test)
    X_test_1incr, X_test_1decr = np.copy(X_test), np.copy(X_test)
    X_test_0incr[:, 0] += 10
    X_test_0decr[:, 0] -= 10
    X_test_1incr[:, 1] += 10
    X_test_1decr[:, 1] -= 10
    monotonic_cst = np.zeros(X.shape[1])
    monotonic_cst[0] = 1
    monotonic_cst[1] = -1

    if depth_first_builder:
        est = TreeClassifier(max_depth=None, monotonic_cst=monotonic_cst)
    else:
        est = TreeClassifier(
            max_depth=None,
            max_leaf_nodes=n_samples_train,
        )
    if hasattr(est, "random_state"):
        est.set_params(**{"random_state": global_random_seed})
    if hasattr(est, "n_estimators"):
        est.set_params(**{"n_estimators": 5})
    if sparse_splitter:
        X_train = csc_container(X_train)
    est.fit(X_train, y_train)
    proba_test = est.predict_proba(X_test)

    assert np.logical_and(proba_test >= 0.0, proba_test <= 1.0).all(), (
        "Probability should always be in [0, 1] range."
    )
    assert_allclose(proba_test.sum(axis=1), 1.0)

    # Monotonic increase constraint, it applies to the positive class
    assert np.all(est.predict_proba(X_test_0incr)[:, 1] >= proba_test[:, 1])
    assert np.all(est.predict_proba(X_test_0decr)[:, 1] <= proba_test[:, 1])

    # Monotonic decrease constraint, it applies to the positive class
    assert np.all(est.predict_proba(X_test_1incr)[:, 1] <= proba_test[:, 1])
    assert np.all(est.predict_proba(X_test_1decr)[:, 1] >= proba_test[:, 1])

def test_monotonic_constraints_regressions(
    TreeRegressor,
    criterion,
):
    # Build a regression task using 5 informative features
    )
    train = np.arange(n_samples_train)
    test = np.arange(n_samples_train, n_samples)
    X_train = X[train]
    y_train = y[train]
    X_test = np.copy(X[test])
    X_test_incr = np.copy(X_test)
    X_test_decr = np.copy(X_test)
    X_test_incr[:, 0] += 10
    X_test_decr[:, 1] += 10

        est = TreeRegressor(
        )
    else:
            max_depth=8,
        )
        est.set_params(random_state=global_random_seed)
    y = est.predict(X_test)
    # Monotonic increase constraint
    y_incr = est.predict(X_test_incr)
    # y_incr should always be greater than y
    assert np.all(y_incr >= y)

    # Monotonic decrease constraint
    y_decr = est.predict(X_test_decr)
    # y_decr should always be lower than y
    assert np.all(y_decr <= y)

def test_multiclass_raises(TreeClassifier):
        n_samples=100, n_features=5, n_classes=3, n_informative=3, random_state=0
    )
    y[0] = 0
    monotonic_cst[0] = -1
    monotonic_cst[1] = 1
    est = TreeClassifier(max_depth=None, monotonic_cst=monotonic_cst, random_state=0)

    msg = "Monotonicity constraints are not supported with multiclass classification"

def test_multiple_output_raises(TreeClassifier):
    X = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
    y = [[1, 0, 1, 0, 1], [1, 0, 1, 0, 1]]

    est = TreeClassifier(
        max_depth=None, monotonic_cst=np.array([-1, 1]), random_state=0
    )
    msg = "Monotonicity constraints are not supported with multiple output"

def test_missing_values_raises(Tree):
        n_samples=100, n_features=5, n_classes=2, n_informative=3, random_state=0
    )
    X[0, 0] = np.nan
    est = Tree(max_depth=None, monotonic_cst=monotonic_cst, random_state=0)

    msg = "Input X contains NaN"

def test_bad_monotonic_cst_raises(TreeClassifier):
    X = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
    y = [1, 0, 1, 0, 1]

    msg = "monotonic_cst has shape 3 but the input data X has 2 features."
        max_depth=None, monotonic_cst=np.array([-1, 1, 0]), random_state=0
    )

    msg = "monotonic_cst must be None or an array-like of -1, 0 or 1."
        max_depth=None, monotonic_cst=np.array([-2, 2]), random_state=0
    )

        max_depth=None, monotonic_cst=np.array([-1, 0.8]), random_state=0
    )
    with pytest.raises(ValueError, match=msg + "(.*)0.8]"):

def assert_1d_reg_tree_children_monotonic_bounded(tree_, monotonic_sign):
    values = tree_.value
    for i in range(tree_.node_count):
        if tree_.children_left[i] > i and tree_.children_right[i] > i:
            # Check monotonicity on children
            i_left = tree_.children_left[i]
            i_right = tree_.children_right[i]
            if monotonic_sign == 1:
                assert values[i_left] <= values[i_right]
            elif monotonic_sign == -1:
                assert values[i_left] >= values[i_right]
            val_middle = (values[i_left] + values[i_right]) / 2
            # Check bounds on grand-children, filtering out leaf nodes
            if tree_.feature[i_left] >= 0:
                i_left_right = tree_.children_right[i_left]
                if monotonic_sign == 1:
                    assert values[i_left_right] <= val_middle
                elif monotonic_sign == -1:
                    assert values[i_left_right] >= val_middle
            if tree_.feature[i_right] >= 0:
                i_right_left = tree_.children_left[i_right]
                    assert val_middle <= values[i_right_left]
                    assert val_middle >= values[i_right_left]

def test_assert_1d_reg_tree_children_monotonic_bounded():
    X = np.linspace(-1, 1, 7).reshape(-1, 1)
    y = np.sin(2 * np.pi * X.ravel())

    reg = DecisionTreeRegressor(max_depth=None, random_state=0).fit(X, y)

        assert_1d_reg_tree_children_monotonic_bounded(reg.tree_, 1)

        assert_1d_reg_tree_children_monotonic_bounded(reg.tree_, -1)

def assert_1d_reg_monotonic(clf, monotonic_sign, min_x, max_x, n_steps):
    X_grid = np.linspace(min_x, max_x, n_steps).reshape(-1, 1)
    y_pred_grid = clf.predict(X_grid)
    if monotonic_sign == 1:
        assert (np.diff(y_pred_grid) >= 0.0).all()
    elif monotonic_sign == -1:
        assert (np.diff(y_pred_grid) <= 0.0).all()

def test_1d_opposite_monotonicity_cst_data(TreeRegressor):
    # Check that positive monotonic data with negative monotonic constraint
    # yield constant predictions, equal to the average of target values
    X = np.linspace(-2, 2, 10).reshape(-1, 1)
    y = X.ravel()
    clf = TreeRegressor(monotonic_cst=[-1])
    assert clf.tree_.node_count == 1
    assert clf.tree_.value[0] == 0.0

    # Swap monotonicity
    clf = TreeRegressor(monotonic_cst=[1])
    clf.fit(X, -y)

def test_1d_tree_nodes_values(
    TreeRegressor, monotonic_sign, depth_first_builder, criterion, global_random_seed
):
    # Adaptation from test_nodes_values in test_monotonic_constraints.py
    # in sklearn.ensemble._hist_gradient_boosting
    # Build a single tree with only one feature, and make sure the node
    # values respect the monotonicity constraints.

    # Considering the following tree with a monotonic +1 constraint, we
    #
    #       root
    #      /    \
    #     a      b
    #    / \    / \
    #   c   d  e   f
    #
    #        a <=  root  <= b
    # c <= d <= (a + b) / 2 <= e <= f

    X = rng.rand(n_samples, n_features)
    y = rng.rand(n_samples)

        # No max_leaf_nodes, default depth first tree builder
        clf = TreeRegressor(
            monotonic_cst=[monotonic_sign],
        )
    else:
        # max_leaf_nodes triggers best first tree builder
            max_leaf_nodes=n_samples,
        )

    assert_1d_reg_tree_children_monotonic_bounded(clf.tree_, monotonic_sign)
    assert_1d_reg_monotonic(clf, monotonic_sign, np.min(X), np.max(X), 100)

def assert_nd_reg_tree_children_monotonic_bounded(tree_, monotonic_cst):
    upper_bound = np.full(tree_.node_count, np.inf)
    lower_bound = np.full(tree_.node_count, -np.inf)
        feature = tree_.feature[i]
        node_value = tree_.value[i][0][0]  # unpack value from nx1x1 array
        # While building the tree, the computed middle value is slightly
        # different from the average of the siblings values, because
        # sum_right / weighted_n_right
        # is slightly different from the value of the right sibling.
        # This can cause a discrepancy up to numerical noise when clipping,
        # which is resolved by comparing with some loss of precision.
        assert np.float32(node_value) <= np.float32(upper_bound[i])
        assert np.float32(node_value) >= np.float32(lower_bound[i])

        if feature < 0:
            # Leaf: nothing to do
            continue

        # Split node: check and update bounds for the children.
        i_left = tree_.children_left[i]
        i_right = tree_.children_right[i]
        # unpack value from nx1x1 array
        middle_value = (tree_.value[i_left][0][0] + tree_.value[i_right][0][0]) / 2

        if monotonic_cst[feature] == 0:
            # Feature without monotonicity constraint: propagate bounds
            # down the tree to both children.
            # Otherwise, with 2 features and a monotonic increase constraint
            # (encoded by +1) on feature 0, the following tree can be accepted,
            # although it does not respect the monotonic increase constraint:
            #
            #                      X[0] <= 0
            #                      value = 100
            #                     /            \
            #          X[0] <= -1                X[1] <= 0
            #          value = 50                value = 150
            #        /            \             /            \
            #    leaf           leaf           leaf          leaf
            #    value = 25     value = 75     value = 50    value = 250

            lower_bound[i_left] = lower_bound[i]
            upper_bound[i_left] = upper_bound[i]
            lower_bound[i_right] = lower_bound[i]
            upper_bound[i_right] = upper_bound[i]

        elif monotonic_cst[feature] == 1:
            # Feature with constraint: check monotonicity
            assert tree_.value[i_left] <= tree_.value[i_right]

            # Propagate bounds down the tree to both children.
            upper_bound[i_left] = middle_value
            lower_bound[i_right] = middle_value

        elif monotonic_cst[feature] == -1:
            assert tree_.value[i_left] >= tree_.value[i_right]

            # Update and propagate bounds down the tree to both children.
            lower_bound[i_left] = middle_value
            upper_bound[i_right] = middle_value

        else:  # pragma: no cover
            raise ValueError(f"monotonic_cst[{feature}]={monotonic_cst[feature]}")

def test_assert_nd_reg_tree_children_monotonic_bounded():
    # Check that assert_nd_reg_tree_children_monotonic_bounded can detect
    # non-monotonic tree predictions.
    X = np.linspace(0, 2 * np.pi, 30).reshape(-1, 1)
    y = np.sin(X).ravel()

        assert_nd_reg_tree_children_monotonic_bounded(reg.tree_, [1])

        assert_nd_reg_tree_children_monotonic_bounded(reg.tree_, [-1])

    assert_nd_reg_tree_children_monotonic_bounded(reg.tree_, [0])

    # Check that assert_nd_reg_tree_children_monotonic_bounded raises
    # when the data (and therefore the model) is naturally monotonic in the
    # opposite direction.
    X = np.linspace(-5, 5, 5).reshape(-1, 1)
    y = X.ravel() ** 3

    # For completeness, check that the converse holds when swapping the sign.
    reg = DecisionTreeRegressor(max_depth=None, random_state=0).fit(X, -y)

def test_nd_tree_nodes_values(
):
    # Build tree with several features, and make sure the nodes

    # Considering the following tree with a monotonic increase constraint on X[0],
    # we should have:
    #
    #            root
    #           X[0]<=t
    #          /       \
    #         a         b
    #     X[0]<=u   X[1]<=v
    #    /       \   /     \
    #   c        d  e       f
    #
    # i)   a <= root <= b
    # ii)  c <= a <= d <= (a+b)/2
    # iii) (a+b)/2 <= min(e,f)
    # For iii) we check that each node value is within the proper lower and
    # upper bounds.

    monotonic_cst = [monotonic_sign, 0]

        )
    else:
        )
    assert_nd_reg_tree_children_monotonic_bounded(clf.tree_, monotonic_cst)

def assert_tree_equal(d, s, message):
    assert s.node_count == d.node_count, (
        "{0}: inequal number of node ({1} != {2})".format(
            message, s.node_count, d.node_count
        )
    )

        d.children_right, s.children_right, message + ": inequal children_right"
    )
        d.children_left, s.children_left, message + ": inequal children_left"
    )

    external = d.children_right == TREE_LEAF
    internal = np.logical_not(external)

        d.feature[internal], s.feature[internal], message + ": inequal features"
    )
        d.threshold[internal], s.threshold[internal], message + ": inequal threshold"
    )
        d.n_node_samples.sum(),
        s.n_node_samples.sum(),
        message + ": inequal sum(n_node_samples)",
    )
        d.n_node_samples, s.n_node_samples, message + ": inequal n_node_samples"
    )

    assert_almost_equal(d.impurity, s.impurity, err_msg=message + ": inequal impurity")

        d.value[external], s.value[external], err_msg=message + ": inequal value"
    )

    for name, Tree in CLF_TREES.items():
        clf = Tree(random_state=0)
        assert_array_equal(clf.predict(T), true_result, "Failed with {0}".format(name))

        clf = Tree(max_features=1, random_state=1)

def test_weighted_classification_toy():
    # Check classification on a weighted toy dataset.

        clf.fit(X, y, sample_weight=np.ones(len(X)))

        clf.fit(X, y, sample_weight=np.full(len(X), 0.5))

def test_regression_toy(Tree, criterion):
    # Check regression on a toy dataset.
    if criterion == "poisson":
        # make target positive while not touching the original y and
        # true_result
        a = np.abs(np.min(y)) + 1
        y_train = np.array(y) + a
        y_test = np.array(true_result) + a
    else:
        y_train = y
        y_test = true_result

    reg = Tree(criterion=criterion, random_state=1)
    reg.fit(X, y_train)
    assert_allclose(reg.predict(T), y_test)

    clf = Tree(criterion=criterion, max_features=1, random_state=1)
    clf.fit(X, y_train)

def test_xor():
    # Check on a XOR problem
    y = np.zeros((10, 10))
    y[:5, :5] = 1
    y[5:, 5:] = 1

    gridx, gridy = np.indices(y.shape)

    X = np.vstack([gridx.ravel(), gridy.ravel()]).T
    y = y.ravel()

        assert clf.score(X, y) == 1.0, "Failed with {0}".format(name)

        clf = Tree(random_state=0, max_features=1)

    for (name, Tree), criterion in product(CLF_TREES.items(), CLF_CRITERIONS):
        clf = Tree(criterion=criterion, random_state=0)
        score = accuracy_score(clf.predict(iris.data), iris.target)
        assert score > 0.9, "Failed with {0}, criterion = {1} and score = {2}".format(
            name, criterion, score
        )

        clf = Tree(criterion=criterion, max_features=2, random_state=0)
        assert score > 0.5, "Failed with {0}, criterion = {1} and score = {2}".format(
        )

def test_diabetes_overfit(name, Tree, criterion):
    # check consistency of overfitted trees on the diabetes dataset
    # since the trees will overfit, we expect an MSE of 0
    reg = Tree(criterion=criterion, random_state=0)
    score = mean_squared_error(diabetes.target, reg.predict(diabetes.data))
    assert score == pytest.approx(0), (
        f"Failed with {name}, criterion = {criterion} and score = {score}"
    )

def test_diabetes_underfit(name, Tree, criterion, max_depth, metric, max_loss):
    # check consistency of trees when the depth and the number of features are
    # limited

    reg = Tree(criterion=criterion, max_depth=max_depth, max_features=6, random_state=0)
    loss = metric(diabetes.target, reg.predict(diabetes.data))
    assert 0 < loss < max_loss

    # Predict probabilities using DecisionTreeClassifier.

        clf = Tree(max_depth=1, max_features=1, random_state=42)

        prob_predict = clf.predict_proba(iris.data)
            np.sum(prob_predict, 1),
            np.ones(iris.data.shape[0]),
            err_msg="Failed with {0}".format(name),
        )
            np.argmax(prob_predict, 1),
            clf.predict(iris.data),
        )
            clf.predict_proba(iris.data),
            np.exp(clf.predict_log_proba(iris.data)),
            8,
        )

def test_arrayrepr():
    # Check the array representation.
    # Check resize
    X = np.arange(10000)[:, np.newaxis]
    y = np.arange(10000)

    for name, Tree in REG_TREES.items():
        reg = Tree(max_depth=None, random_state=0)
        reg.fit(X, y)

def test_pure_set():
    # Check when y is pure.
    X = [[-2, -1], [-1, -1], [-1, -2], [1, 1], [1, 2], [2, 1]]
    y = [1, 1, 1, 1, 1, 1]

    for name, TreeClassifier in CLF_TREES.items():
        clf = TreeClassifier(random_state=0)
        assert_array_equal(clf.predict(X), y, err_msg="Failed with {0}".format(name))

    for name, TreeRegressor in REG_TREES.items():
        reg = TreeRegressor(random_state=0)
        assert_almost_equal(reg.predict(X), y, err_msg="Failed with {0}".format(name))

def test_numerical_stability():
    # Check numerical stability.
        [
            [152.08097839, 140.40744019, 129.75102234, 159.90493774],
            [142.50700378, 135.81935120, 117.82884979, 162.75781250],
            [127.28772736, 140.40744019, 129.75102234, 159.90493774],
            [132.37025452, 143.71923828, 138.35694885, 157.84558105],
            [103.10237122, 143.71928406, 138.35696411, 157.84559631],
            [127.71276855, 143.71923828, 138.35694885, 157.84558105],
            [120.91514587, 140.40744019, 129.75102234, 159.90493774],
        ]
    )

    y = np.array([1.0, 0.70209277, 0.53896582, 0.0, 0.90914464, 0.48026916, 0.49622521])

        for name, Tree in REG_TREES.items():
            reg = Tree(random_state=0)
            reg.fit(X, y)
            reg.fit(X, -y)
            reg.fit(-X, y)
            reg.fit(-X, -y)

        n_samples=5000,
    )

        importances = clf.feature_importances_
        n_important = np.sum(importances > 0.1)

        assert importances.shape[0] == 10, "Failed with {0}".format(name)
        assert n_important == 3, "Failed with {0}".format(name)

    # Check on iris that importances are the same for all builders
    clf2 = DecisionTreeClassifier(random_state=0, max_leaf_nodes=len(iris.data))

    assert_array_equal(clf.feature_importances_, clf2.feature_importances_)

def test_importances_raises():
    # Check if variable importance before fit raises ValueError.
        getattr(clf, "feature_importances_")

def test_importances_gini_equal_squared_error():
    # Check that gini is equivalent to squared_error for binary output variable

    )

    # The gini index and the mean square error (variance) might differ due
    # to numerical instability. Since those instabilities mainly occurs at
    # high tree depth, we restrict this maximal depth.
    clf = DecisionTreeClassifier(criterion="gini", max_depth=5, random_state=0).fit(
        X, y
    )
    reg = DecisionTreeRegressor(
        criterion="squared_error", max_depth=5, random_state=0

    assert_almost_equal(clf.feature_importances_, reg.feature_importances_)
    assert_array_equal(clf.tree_.feature, reg.tree_.feature)
    assert_array_equal(clf.tree_.children_left, reg.tree_.children_left)
    assert_array_equal(clf.tree_.children_right, reg.tree_.children_right)
    assert_array_equal(clf.tree_.n_node_samples, reg.tree_.n_node_samples)

    # Check max_features.
    for name, TreeEstimator in ALL_TREES.items():
        est = TreeEstimator(max_features="sqrt")
        est.fit(iris.data, iris.target)
        assert est.max_features_ == int(np.sqrt(iris.data.shape[1]))

        est = TreeEstimator(max_features="log2")
        assert est.max_features_ == int(np.log2(iris.data.shape[1]))

        est = TreeEstimator(max_features=1)
        assert est.max_features_ == 1

        est = TreeEstimator(max_features=3)
        assert est.max_features_ == 3

        est = TreeEstimator(max_features=0.01)

        est = TreeEstimator(max_features=0.5)
        assert est.max_features_ == int(0.5 * iris.data.shape[1])

        est = TreeEstimator(max_features=1.0)
        assert est.max_features_ == iris.data.shape[1]

        est = TreeEstimator(max_features=None)

    # Test that it gives proper exception on deficient input.
    for name, TreeEstimator in CLF_TREES.items():
        # predict before fit
        est = TreeEstimator()
            est.predict_proba(X)

        X2 = [[-2, -1, 1]]  # wrong feature shape for sample
            est.predict_proba(X2)

        # Wrong dimensions
        y2 = y[:-1]
            est.fit(X, y2)

        # Test with arrays that are non-contiguous.
        Xf = np.asfortranarray(X)
        est.fit(Xf, y)
        assert_almost_equal(est.predict(T), true_result)

        # predict before fitting
            est.predict(T)

        # predict on vector with different dims
        t = np.asarray(T)
            est.predict(t[:, 1:])

        # wrong sample shape
        Xt = np.array(X).T

        est.fit(np.dot(X, Xt), y)
            est.predict(X)
            est.apply(X)

        clf = TreeEstimator()
            clf.predict(Xt)
            clf.apply(Xt)

        # apply before fitting
            est.apply(T)

    # non positive target for Poisson splitting Criterion
    est = DecisionTreeRegressor(criterion="poisson")
    with pytest.raises(ValueError, match="y is not positive.*Poisson"):
        est.fit([[0, 1, 2]], [0, 0, 0])
    with pytest.raises(ValueError, match="Some.*y are negative.*Poisson"):
        est.fit([[0, 1, 2]], [5, -0.1, 2])

def test_min_samples_split():
    """Test min_samples_split parameter"""
    X = np.asfortranarray(iris.data, dtype=tree._tree.DTYPE)

    for max_leaf_nodes, name in product((None, 1000), ALL_TREES.keys()):
        TreeEstimator = ALL_TREES[name]

        # test for integer parameter
        est = TreeEstimator(
            min_samples_split=10, max_leaf_nodes=max_leaf_nodes, random_state=0
        )
        # count samples on nodes, -1 means it is a leaf
        node_samples = est.tree_.n_node_samples[est.tree_.children_left != -1]

        assert np.min(node_samples) > 9, "Failed with {0}".format(name)

        # test for float parameter
            min_samples_split=0.2, max_leaf_nodes=max_leaf_nodes, random_state=0
        )

def test_min_samples_leaf():

        # test integer parameter
            min_samples_leaf=5, max_leaf_nodes=max_leaf_nodes, random_state=0
        )
        out = est.tree_.apply(X)
        node_counts = np.bincount(out)
        leaf_count = node_counts[node_counts != 0]
        assert np.min(leaf_count) > 4, "Failed with {0}".format(name)

        # test float parameter
            min_samples_leaf=0.1, max_leaf_nodes=max_leaf_nodes, random_state=0
        )

def check_min_weight_fraction_leaf(name, datasets, sparse_container=None):
    """Test if leaves contain at least min_weight_fraction_leaf of the
    training set"""
    X = DATASETS[datasets]["X"].astype(np.float32)
    y = DATASETS[datasets]["y"]

    TreeEstimator = ALL_TREES[name]

    for max_leaf_nodes, frac in product((None, 1000), np.linspace(0, 0.5, 6)):
            min_weight_fraction_leaf=frac, max_leaf_nodes=max_leaf_nodes, random_state=0
        )

        if sparse_container is not None:
            out = est.tree_.apply(X.tocsr())
        else:
            out = est.tree_.apply(X)

            )
        )

    # test case with no weights passed in
    total_weight = X.shape[0]

        )

        else:

        node_weights = np.bincount(out)
            )
        )

def test_min_weight_fraction_leaf_on_dense_input(name):
    check_min_weight_fraction_leaf(name, "iris")

def test_min_weight_fraction_leaf_on_sparse_input(name, csc_container):
    check_min_weight_fraction_leaf(name, "multilabel", sparse_container=csc_container)

def check_min_weight_fraction_leaf_with_min_samples_leaf(
    name, datasets, sparse_container=None
):
    """Test the interaction between min_weight_fraction_leaf and
    min_samples_leaf when sample_weights is not provided in fit."""

    for max_leaf_nodes, frac in product((None, 1000), np.linspace(0, 0.5, 3)):
        # test integer min_samples_leaf
            min_weight_fraction_leaf=frac,
            min_samples_leaf=5,
        )

        else:

        assert np.min(leaf_weights) >= max(
            (total_weight * est.min_weight_fraction_leaf), 5
        ), "Failed with {0} min_weight_fraction_leaf={1}, min_samples_leaf={2}".format(
            name, est.min_weight_fraction_leaf, est.min_samples_leaf
        )
        # test float min_samples_leaf
            min_samples_leaf=0.1,
        )

        else:

            (total_weight * est.min_weight_fraction_leaf),
            (total_weight * est.min_samples_leaf),
        )

def test_min_weight_fraction_leaf_with_min_samples_leaf_on_dense_input(name):
    check_min_weight_fraction_leaf_with_min_samples_leaf(name, "iris")

def test_min_weight_fraction_leaf_with_min_samples_leaf_on_sparse_input(
    name, csc_container
):
    check_min_weight_fraction_leaf_with_min_samples_leaf(
        name, "multilabel", sparse_container=csc_container
    )

def test_min_impurity_decrease(global_random_seed):
    # test if min_impurity_decrease ensure that a split is made only if
    # if the impurity decrease is at least that value
    X, y = datasets.make_classification(n_samples=100, random_state=global_random_seed)

        # Check default value of min_impurity_decrease, 1e-7
        est1 = TreeEstimator(max_leaf_nodes=max_leaf_nodes, random_state=0)
        # Check with explicit value of 0.05
        est2 = TreeEstimator(
            max_leaf_nodes=max_leaf_nodes, min_impurity_decrease=0.05, random_state=0
        )
        # Check with a much lower value of 0.0001
        est3 = TreeEstimator(
            max_leaf_nodes=max_leaf_nodes, min_impurity_decrease=0.0001, random_state=0
        )
        # Check with a much lower value of 0.1
        est4 = TreeEstimator(
            max_leaf_nodes=max_leaf_nodes, min_impurity_decrease=0.1, random_state=0
        )

        for est, expected_decrease in (
            (est1, 1e-7),
            (est2, 0.05),
            (est3, 0.0001),
            (est4, 0.1),
        ):
            assert est.min_impurity_decrease <= expected_decrease, (
                "Failed, min_impurity_decrease = {0} > {1}".format(
                    est.min_impurity_decrease, expected_decrease
                )
            )
            for node in range(est.tree_.node_count):
                # If current node is a not leaf node, check if the split was
                # justified w.r.t the min_impurity_decrease
                if est.tree_.children_left[node] != TREE_LEAF:
                    imp_parent = est.tree_.impurity[node]
                    wtd_n_node = est.tree_.weighted_n_node_samples[node]

                    left = est.tree_.children_left[node]
                    wtd_n_left = est.tree_.weighted_n_node_samples[left]
                    imp_left = est.tree_.impurity[left]
                    wtd_imp_left = wtd_n_left * imp_left

                    right = est.tree_.children_right[node]
                    wtd_n_right = est.tree_.weighted_n_node_samples[right]
                    imp_right = est.tree_.impurity[right]
                    wtd_imp_right = wtd_n_right * imp_right

                    wtd_avg_left_right_imp = wtd_imp_right + wtd_imp_left
                    wtd_avg_left_right_imp /= wtd_n_node

                    fractional_node_weight = (
                        est.tree_.weighted_n_node_samples[node] / X.shape[0]
                    )

                    actual_decrease = fractional_node_weight * (
                        imp_parent - wtd_avg_left_right_imp
                    )

                    assert actual_decrease >= expected_decrease, (
                        "Failed with {0} expected min_impurity_decrease={1}".format(
                            actual_decrease, expected_decrease
                        )
                    )

    """Test pickling preserves Tree properties and performance."""
        if "Classifier" in name:
            X, y = iris.data, iris.target
        else:
            X, y = diabetes.data, diabetes.target

        est = TreeEstimator(random_state=0)
        score = est.score(X, y)

        # test that all class properties are maintained
        attributes = [
            "max_depth",
            "node_count",
            "capacity",
            "n_classes",
            "children_left",
            "children_right",
            "n_leaves",
            "feature",
            "threshold",
            "impurity",
            "n_node_samples",
            "weighted_n_node_samples",
            "value",
        ]
        fitted_attribute = {
            attribute: getattr(est.tree_, attribute) for attribute in attributes
        }

        serialized_object = pickle.dumps(est)
        est2 = pickle.loads(serialized_object)
        assert type(est2) == est.__class__

        score2 = est2.score(X, y)
        assert score == score2, (
            "Failed to generate same score  after pickling with {0}".format(name)
        )
        for attribute in fitted_attribute:
                getattr(est2.tree_, attribute),
                fitted_attribute[attribute],
                err_msg=(
                    f"Failed to generate same attribute {attribute} after pickling with"
                    f" {name}"
                ),
            )

def test_multioutput():
    X = [
        [-2, -1],
        [-1, -1],
        [-1, -2],
        [1, 1],
        [1, 2],
        [2, 1],
        [-2, 1],
        [-1, 1],
        [-1, 2],
        [2, -1],
        [1, -1],
        [1, -2],
    ]

    y = [
        [-1, 0],
        [-1, 0],
        [-1, 0],
        [1, 1],
        [1, 1],
        [1, 1],
        [-1, 2],
        [-1, 2],
        [-1, 2],
        [1, 3],
        [1, 3],
        [1, 3],
    ]

    T = [[-1, -1], [1, 1], [-1, 1], [1, -1]]
    y_true = [[-1, 0], [1, 1], [-1, 2], [1, 3]]

    # toy classification problem
        y_hat = clf.fit(X, y).predict(T)
        assert_array_equal(y_hat, y_true)
        assert y_hat.shape == (4, 2)

        proba = clf.predict_proba(T)

        log_proba = clf.predict_log_proba(T)

    # toy regression problem
        y_hat = reg.fit(X, y).predict(T)
        assert_almost_equal(y_hat, y_true)

def test_classes_shape():
        # Classification, single output

        assert clf.n_classes_ == 2
        assert_array_equal(clf.classes_, [-1, 1])

        # Classification, multi-output
        _y = np.vstack((y, np.array(y) * 2)).T
        assert len(clf.n_classes_) == 2
        assert len(clf.classes_) == 2
        assert_array_equal(clf.n_classes_, [2, 2])
        assert_array_equal(clf.classes_, [[-1, 1], [-2, 2]])

def test_unbalanced_iris():
    # Check class rebalancing.
    unbalanced_X = iris.data[:125]
    unbalanced_y = iris.target[:125]
    sample_weight = compute_sample_weight("balanced", unbalanced_y)

        clf.fit(unbalanced_X, unbalanced_y, sample_weight=sample_weight)
        assert_almost_equal(clf.predict(unbalanced_X), unbalanced_y)

def test_memory_layout():
    # Check that it works no matter the memory layout
    for (name, TreeEstimator), dtype in product(
        ALL_TREES.items(), [np.float64, np.float32]
    ):

        # Nothing
        X = np.asarray(iris.data, dtype=dtype)
        assert_array_equal(est.fit(X, y).predict(X), y)

        # C-order
        X = np.asarray(iris.data, order="C", dtype=dtype)

        # F-order
        X = np.asarray(iris.data, order="F", dtype=dtype)

        # Contiguous
        X = np.ascontiguousarray(iris.data, dtype=dtype)

        # csr
        for csr_container in CSR_CONTAINERS:
            X = csr_container(iris.data, dtype=dtype)
            assert_array_equal(est.fit(X, y).predict(X), y)

        # csc
        for csc_container in CSC_CONTAINERS:
            X = csc_container(iris.data, dtype=dtype)

        # Strided
        X = np.asarray(iris.data[::3], dtype=dtype)
        y = iris.target[::3]

def test_sample_weight():
    # Check sample weighting.
    # Test that zero-weighted samples are not taken into account
    X = np.arange(100)[:, np.newaxis]
    y = np.ones(100)
    y[:50] = 0.0

    sample_weight = np.ones(100)
    sample_weight[y == 0] = 0.0

    assert_array_equal(clf.predict(X), np.ones(100))

    # Test that low weighted samples are not taken into account at low depth
    X = np.arange(200)[:, np.newaxis]
    y = np.zeros(200)
    y[50:100] = 1
    y[100:200] = 2
    X[100:200, 0] = 200

    sample_weight = np.ones(200)

    sample_weight[y == 2] = 0.51  # Samples of class '2' are still weightier
    clf = DecisionTreeClassifier(max_depth=1, random_state=0)
    assert clf.tree_.threshold[0] == 149.5

    sample_weight[y == 2] = 0.5  # Samples of class '2' are no longer weightier
    assert clf.tree_.threshold[0] == 49.5  # Threshold should have moved

    # Test that sample weighting is the same as having duplicates

    duplicates = rng.randint(0, X.shape[0], 100)

    clf = DecisionTreeClassifier(random_state=1)
    clf.fit(X[duplicates], y[duplicates])

    sample_weight = np.bincount(duplicates, minlength=X.shape[0])
    clf2 = DecisionTreeClassifier(random_state=1)
    clf2.fit(X, y, sample_weight=sample_weight)

    internal = clf.tree_.children_left != tree._tree.TREE_LEAF
        clf.tree_.threshold[internal], clf2.tree_.threshold[internal]
    )

def test_sample_weight_invalid():
    # Check sample weighting raises errors.

    sample_weight = np.random.rand(100, 1)

    sample_weight = np.array(0)

    expected_err = re.escape(
        (
            "Input should have at least 1 dimension i.e. satisfy "
            "`len(x.shape) > 0`, got scalar `array(0.)` instead."
        )
    )
    with pytest.raises(TypeError, match=expected_err):

    # Test that class_weights resemble sample_weights behavior.
    TreeClassifier = CLF_TREES[name]

    clf1 = TreeClassifier(random_state=0)
    clf2 = TreeClassifier(class_weight="balanced", random_state=0)

    clf3 = TreeClassifier(
        ],
    )
    # Check against multi-output "auto" which should also have no effect
    clf4 = TreeClassifier(class_weight="balanced", random_state=0)

    clf2 = TreeClassifier(class_weight=class_weight, random_state=0)

    clf = TreeClassifier(class_weight=[{-1: 0.5, 1: 1.0}], random_state=0)
    err_msg = "number of elements in class_weight should match number of outputs."

def test_max_leaf_nodes():
    k = 4
        est = TreeEstimator(max_depth=None, max_leaf_nodes=k + 1).fit(X, y)
        assert est.get_n_leaves() == k + 1

def test_max_leaf_nodes_max_depth():
    k = 4
        est = TreeEstimator(max_depth=1, max_leaf_nodes=k).fit(X, y)
        assert est.get_depth() == 1

def test_arrays_persist():
    # Ensure property arrays' memory stays alive when tree disappears
    # non-regression for #2726
    for attr in [
        "n_classes",
        "value",
        "children_left",
        "children_right",
        "threshold",
        "impurity",
        "feature",
        "n_node_samples",
    ]:
        value = getattr(DecisionTreeClassifier().fit([[0], [1]], [0, 1]).tree_, attr)
        # if pointing to freed memory, contents may be arbitrary
        assert -3 <= value.flat[0] < 3, "Array points to arbitrary memory"

def test_only_constant_features():
    X = np.zeros((10, 20))
    y = random_state.randint(0, 2, (10,))
        assert est.tree_.max_depth == 0

def test_behaviour_constant_feature_after_splits():
    X = np.transpose(
        np.vstack(([[0, 0, 0, 0, 0, 1, 2, 4, 5, 6, 7]], np.zeros((4, 11))))
    )
    y = [0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3]
        # do not check extra random trees
        if "ExtraTree" not in name:
            est = TreeEstimator(random_state=0, max_features=1)
            assert est.tree_.max_depth == 2
            assert est.tree_.node_count == 5

def test_with_only_one_non_constant_features():
    X = np.hstack([np.array([[1.0], [1.0], [0.0], [0.0]]), np.zeros((4, 1000))])

    y = np.array([0.0, 1.0, 0.0, 1.0])
        est = TreeEstimator(random_state=0, max_features=1)
        assert est.tree_.max_depth == 1
        assert_array_equal(est.predict_proba(X), np.full((4, 2), 0.5))

    for name, TreeEstimator in REG_TREES.items():
        assert_array_equal(est.predict(X), np.full((4,), 0.5))

def test_big_input():
    # Test if the warning for too large inputs is appropriate.
    X = np.repeat(10**40.0, 4).astype(np.float64).reshape(-1, 1)
    with pytest.raises(ValueError, match="float32"):
        clf.fit(X, [0, 1, 0, 1])

def test_realloc():
    from sklearn.tree._utils import _realloc_test

    with pytest.raises(MemoryError):
        _realloc_test()

def test_huge_allocations():
    n_bits = 8 * struct.calcsize("P")

    X = np.random.randn(10, 2)
    y = np.random.randint(0, 2, 10)

    # Sanity check: we cannot request more memory than the size of the address
    # space. Currently raises OverflowError.
    huge = 2 ** (n_bits + 1)
    clf = DecisionTreeClassifier(splitter="best", max_leaf_nodes=huge)
    with pytest.raises(Exception):

    # Non-regression test: MemoryError used to be dropped by Cython
    # because of missing "except *".
    huge = 2 ** (n_bits - 1) - 1

def check_sparse_input(tree, dataset, max_depth=None):
    TreeEstimator = ALL_TREES[tree]
    X = DATASETS[dataset]["X"]
    y = DATASETS[dataset]["y"]

    # Gain testing time
    if dataset in ["digits", "diabetes"]:
        n_samples = X.shape[0] // 5
        X = X[:n_samples]
        y = y[:n_samples]

    for sparse_container in COO_CONTAINERS + CSC_CONTAINERS + CSR_CONTAINERS:
        X_sparse = sparse_container(X)

        # Check the default (depth first search)
        d = TreeEstimator(random_state=0, max_depth=max_depth).fit(X, y)
        s = TreeEstimator(random_state=0, max_depth=max_depth).fit(X_sparse, y)

        assert_tree_equal(
            d.tree_,
            s.tree_,
            "{0} with dense and sparse format gave different trees".format(tree),
        )

        y_pred = d.predict(X)
        if tree in CLF_TREES:
            y_proba = d.predict_proba(X)
            y_log_proba = d.predict_log_proba(X)

        for sparse_container_test in COO_CONTAINERS + CSR_CONTAINERS + CSC_CONTAINERS:
            X_sparse_test = sparse_container_test(X_sparse, dtype=np.float32)

            assert_array_almost_equal(s.predict(X_sparse_test), y_pred)

            if tree in CLF_TREES:
                assert_array_almost_equal(s.predict_proba(X_sparse_test), y_proba)
                assert_array_almost_equal(
                    s.predict_log_proba(X_sparse_test), y_log_proba
                )

def test_sparse_input(tree_type, dataset):
    max_depth = 3 if dataset == "digits" else None
    check_sparse_input(tree_type, dataset, max_depth)

def test_sparse_input_reg_trees(tree_type, dataset):
    # Due to numerical instability of MSE and too strict test, we limit the
    # maximal depth
    check_sparse_input(tree_type, dataset, 2)

def test_sparse_parameters(tree_type, dataset, csc_container):
    TreeEstimator = ALL_TREES[tree_type]
    X_sparse = csc_container(X)

    # Check max_features
    d = TreeEstimator(random_state=0, max_features=1, max_depth=2).fit(X, y)
    s = TreeEstimator(random_state=0, max_features=1, max_depth=2).fit(X_sparse, y)
    assert_tree_equal(
        d.tree_,
        s.tree_,
        "{0} with dense and sparse format gave different trees".format(tree_type),
    )
    assert_array_almost_equal(s.predict(X), d.predict(X))

    # Check min_samples_split
    d = TreeEstimator(random_state=0, max_features=1, min_samples_split=10).fit(X, y)
    s = TreeEstimator(random_state=0, max_features=1, min_samples_split=10).fit(
        X_sparse, y
    )
        d.tree_,
        s.tree_,
    )

    # Check min_samples_leaf
    d = TreeEstimator(random_state=0, min_samples_leaf=X_sparse.shape[0] // 2).fit(X, y)
    s = TreeEstimator(random_state=0, min_samples_leaf=X_sparse.shape[0] // 2).fit(
    )
        d.tree_,
        s.tree_,
    )

    # Check best-first search
    d = TreeEstimator(random_state=0, max_leaf_nodes=3).fit(X, y)
    s = TreeEstimator(random_state=0, max_leaf_nodes=3).fit(X_sparse, y)
        d.tree_,
        s.tree_,
    )

def test_sparse_criteria(tree_type, dataset, csc_container, criterion):

    d = TreeEstimator(random_state=0, max_depth=3, criterion=criterion).fit(X, y)
    s = TreeEstimator(random_state=0, max_depth=3, criterion=criterion).fit(X_sparse, y)

        d.tree_,
        s.tree_,
    )

def test_explicit_sparse_zeros(tree_type, csc_container, csr_container):
    max_depth = 3
    n_features = 10

    # n_samples set n_feature to ease construction of a simultaneous
    # construction of a csr and csc matrix
    n_samples = n_features
    samples = np.arange(n_samples)

    # Generate X, y
    indices = []
    data = []
    offset = 0
    indptr = [offset]
        n_nonzero_i = random_state.binomial(n_samples, 0.5)
        indices_i = random_state.permutation(samples)[:n_nonzero_i]
        indices.append(indices_i)
        data_i = random_state.binomial(3, 0.5, size=(n_nonzero_i,)) - 1
        data.append(data_i)
        offset += n_nonzero_i
        indptr.append(offset)

    indices = np.concatenate(indices).astype(np.int32)
    indptr = np.array(indptr, dtype=np.int32)
    data = np.array(np.concatenate(data), dtype=np.float32)
    X_sparse = csc_container((data, indices, indptr), shape=(n_samples, n_features))
    X = X_sparse.toarray()
    X_sparse_test = csr_container(
        (data, indices, indptr), shape=(n_samples, n_features)
    )
    X_test = X_sparse_test.toarray()
    y = random_state.randint(0, 3, size=(n_samples,))

    # Ensure that X_sparse_test owns its data, indices and indptr array
    X_sparse_test = X_sparse_test.copy()

    # Ensure that we have explicit zeros
    assert (X_sparse.data == 0.0).sum() > 0
    assert (X_sparse_test.data == 0.0).sum() > 0

    # Perform the comparison
    d = TreeEstimator(random_state=0, max_depth=max_depth).fit(X, y)
    s = TreeEstimator(random_state=0, max_depth=max_depth).fit(X_sparse, y)

        d.tree_,
        s.tree_,
        "{0} with dense and sparse format gave different trees".format(tree),
    )

    Xs = (X_test, X_sparse_test)
    for X1, X2 in product(Xs, Xs):
        assert_array_almost_equal(s.tree_.apply(X1), d.tree_.apply(X2))
        assert_array_almost_equal(s.apply(X1), d.apply(X2))
        assert_array_almost_equal(s.apply(X1), s.tree_.apply(X1))

            s.tree_.decision_path(X1).toarray(), d.tree_.decision_path(X2).toarray()
        )
            s.decision_path(X1).toarray(), d.decision_path(X2).toarray()
        )
            s.decision_path(X1).toarray(), s.tree_.decision_path(X1).toarray()
        )

        assert_array_almost_equal(s.predict(X1), d.predict(X2))

            assert_array_almost_equal(s.predict_proba(X1), d.predict_proba(X2))

def check_raise_error_on_1d_input(name):

    X = iris.data[:, 0].ravel()

        TreeEstimator(random_state=0).fit(X, y)

    est = TreeEstimator(random_state=0)
    est.fit(X_2d, y)
        est.predict([X])

        check_raise_error_on_1d_input(name)

def test_min_weight_leaf_split_level(name, sparse_container):

    X = np.array([[0], [0], [0], [0], [1]])
    y = [0, 0, 0, 0, 1]
    sample_weight = [0.2, 0.2, 0.2, 0.2, 0.2]

    assert est.tree_.max_depth == 1

    est = TreeEstimator(random_state=0, min_weight_fraction_leaf=0.4)
    assert est.tree_.max_depth == 0

def test_public_apply_all_trees(name):
    X_small32 = X_small.astype(tree._tree.DTYPE, copy=False)

    est = ALL_TREES[name]()
    est.fit(X_small, y_small)
    assert_array_equal(est.apply(X_small), est.tree_.apply(X_small32))

def test_public_apply_sparse_trees(name, csr_container):
    X_small32 = csr_container(X_small.astype(tree._tree.DTYPE, copy=False))

def test_decision_path_hardcoded():
    est = DecisionTreeClassifier(random_state=0, max_depth=1).fit(X, y)
    node_indicator = est.decision_path(X[:2]).toarray()
    assert_array_equal(node_indicator, [[1, 1, 0], [1, 0, 1]])

    est = TreeEstimator(random_state=0, max_depth=2)

    node_indicator_csr = est.decision_path(X)
    node_indicator = node_indicator_csr.toarray()
    assert node_indicator.shape == (n_samples, est.tree_.node_count)

    leave_indicator = [node_indicator[i, j] for i, j in enumerate(leaves)]
    assert_array_almost_equal(leave_indicator, np.ones(shape=n_samples))

    # Ensure only one leave node per sample
    all_leaves = est.tree_.children_left == TREE_LEAF
        np.dot(node_indicator, all_leaves), np.ones(shape=n_samples)
    )

    # Ensure max depth is consistent with sum of indicator
    max_depth = node_indicator.sum(axis=1).max()
    assert est.tree_.max_depth <= max_depth

def test_no_sparse_y_support(name, csr_container):
    # Currently we don't support sparse y
    X, y = X_multilabel, csr_container(y_multilabel)

def test_mae():
    """Check MAE criterion produces correct results on small toy dataset:

    ------------------
    | X | y | weight |
    | 3 | 3 |  0.1   |
    | 5 | 3 |  0.3   |
    | 8 | 4 |  1.0   |
    | 3 | 6 |  0.6   |
    | 5 | 7 |  0.3   |
    |sum wt:|  2.3   |

    Because we are dealing with sample weights, we cannot find the median by
    simply choosing/averaging the centre value(s), instead we consider the
    median where 50% of the cumulative weight is found (in a y sorted data set)
    . Therefore with regards to this test data, the cumulative weight is >= 50%
    when y = 4.  Therefore:
    Median = 4

    For all the samples, we can get the total error by summing:
    Absolute(Median - y) * weight

    I.e., total error = (Absolute(4 - 3) * 0.1)
                      + (Absolute(4 - 3) * 0.3)
                      + (Absolute(4 - 4) * 1.0)
                      + (Absolute(4 - 6) * 0.6)
                      + (Absolute(4 - 7) * 0.3)
                      = 2.5

    Impurity = Total error / total weight
             = 2.5 / 2.3
             = 1.08695652173913
             ------------------

    From this root node, the next best split is between X values of 3 and 5.
    Thus, we have left and right child nodes:

    LEFT                    RIGHT
    ------------------      ------------------
    | X | y | weight |      | X | y | weight |
    | 3 | 3 |  0.1   |      | 5 | 3 |  0.3   |
    | 3 | 6 |  0.6   |      | 8 | 4 |  1.0   |
    ------------------      | 5 | 7 |  0.3   |
    |sum wt:|  0.7   |      ------------------
    ------------------      |sum wt:|  1.6   |
                            ------------------

    Impurity is found in the same way:
    Left node Median = 6
    Total error = (Absolute(6 - 3) * 0.1)
                + (Absolute(6 - 6) * 0.6)
                = 0.3

    Left Impurity = Total error / total weight
            = 0.3 / 0.7
            = 0.428571428571429
            -------------------

    Likewise for Right node:
    Right node Median = 4
    Total error = (Absolute(4 - 3) * 0.3)
                + (Absolute(4 - 4) * 1.0)
                + (Absolute(4 - 7) * 0.3)
                = 1.2

    Right Impurity = Total error / total weight
            = 1.2 / 1.6
            = 0.75
            ------
    """
    dt_mae = DecisionTreeRegressor(
        random_state=0, criterion="absolute_error", max_leaf_nodes=2
    )

    # Test MAE where sample weights are non-uniform (as illustrated above):
    dt_mae.fit(
        X=[[3], [5], [3], [8], [5]],
        y=[6, 7, 3, 4, 3],
        sample_weight=[0.6, 0.3, 0.1, 1.0, 0.3],
    )
    assert_allclose(dt_mae.tree_.impurity, [2.5 / 2.3, 0.3 / 0.7, 1.2 / 1.6])
    assert_array_equal(dt_mae.tree_.value.flat, [4.0, 6.0, 4.0])

    # Test MAE where all sample weights are uniform:
    dt_mae.fit(X=[[3], [5], [3], [8], [5]], y=[6, 7, 3, 4, 3], sample_weight=np.ones(5))
    assert_array_equal(dt_mae.tree_.impurity, [1.4, 1.5, 4.0 / 3.0])
    assert_array_equal(dt_mae.tree_.value.flat, [4, 4.5, 4.0])

    # Test MAE where a `sample_weight` is not explicitly provided.
    # This is equivalent to providing uniform sample weights, though
    # the internal logic is different:
    dt_mae.fit(X=[[3], [5], [3], [8], [5]], y=[6, 7, 3, 4, 3])

def test_criterion_copy():
    # Let's check whether copy of our criterion has the same type
    # and properties as original
    n_classes = np.arange(3, dtype=np.intp)

    def _pickle_copy(obj):
        return pickle.loads(pickle.dumps(obj))

    for copy_func in [copy.copy, copy.deepcopy, _pickle_copy]:
        for _, typename in CRITERIA_CLF.items():
            criteria = typename(n_outputs, n_classes)
            result = copy_func(criteria).__reduce__()
            typename_, (n_outputs_, n_classes_), _ = result
            assert typename == typename_
            assert n_outputs == n_outputs_
            assert_array_equal(n_classes, n_classes_)

        for _, typename in CRITERIA_REG.items():
            criteria = typename(n_outputs, n_samples)
            typename_, (n_outputs_, n_samples_), _ = result
            assert n_samples == n_samples_

def test_empty_leaf_infinite_threshold(sparse_container):
    # try to make empty leaf by using near infinite value.
    data = np.random.RandomState(0).randn(100, 11) * 2e38
    data = np.nan_to_num(data.astype("float32"))
    X = data[:, :-1]
    y = data[:, -1]

    tree = DecisionTreeRegressor(random_state=0).fit(X, y)
    left_leaf = set(np.where(tree.tree_.children_left == TREE_LEAF)[0])
    empty_leaf = left_leaf.difference(terminal_regions)
    infinite_threshold = np.where(~np.isfinite(tree.tree_.threshold))[0]
    assert len(infinite_threshold) == 0
    assert len(empty_leaf) == 0

def test_prune_tree_classifier_are_subtrees(dataset, tree_cls):
    dataset = DATASETS[dataset]
    X, y = dataset["X"], dataset["y"]
    est = tree_cls(max_leaf_nodes=20, random_state=0)
    info = est.cost_complexity_pruning_path(X, y)

    pruning_path = info.ccp_alphas
    impurities = info.impurities
    assert np.all(np.diff(pruning_path) >= 0)
    assert np.all(np.diff(impurities) >= 0)

    assert_pruning_creates_subtree(tree_cls, X, y, pruning_path)

def test_prune_tree_regression_are_subtrees(dataset, tree_cls):

def test_prune_single_node_tree():
    # single node tree
    clf1 = DecisionTreeClassifier(random_state=0)
    clf1.fit([[0], [1]], [0, 0])

    # pruned single node tree
    clf2 = DecisionTreeClassifier(random_state=0, ccp_alpha=10)
    clf2.fit([[0], [1]], [0, 0])

    assert_is_subtree(clf1.tree_, clf2.tree_)

def assert_pruning_creates_subtree(estimator_cls, X, y, pruning_path):
    # generate trees with increasing alphas
    for ccp_alpha in pruning_path:
        est = estimator_cls(max_leaf_nodes=20, ccp_alpha=ccp_alpha, random_state=0).fit(
            X, y
        )
        estimators.append(est)

    # A pruned tree must be a subtree of the previous tree (which had a
    # smaller ccp_alpha)
    for prev_est, next_est in pairwise(estimators):
        assert_is_subtree(prev_est.tree_, next_est.tree_)

def assert_is_subtree(tree, subtree):
    assert tree.node_count >= subtree.node_count
    assert tree.max_depth >= subtree.max_depth

    tree_c_left = tree.children_left
    tree_c_right = tree.children_right
    subtree_c_left = subtree.children_left
    subtree_c_right = subtree.children_right

    stack = [(0, 0)]
    while stack:
        tree_node_idx, subtree_node_idx = stack.pop()
            tree.value[tree_node_idx], subtree.value[subtree_node_idx]
        )
            tree.impurity[tree_node_idx], subtree.impurity[subtree_node_idx]
        )
            tree.n_node_samples[tree_node_idx], subtree.n_node_samples[subtree_node_idx]
        )
            tree.weighted_n_node_samples[tree_node_idx],
            subtree.weighted_n_node_samples[subtree_node_idx],
        )

        if subtree_c_left[subtree_node_idx] == subtree_c_right[subtree_node_idx]:
            # is a leaf
            assert_almost_equal(TREE_UNDEFINED, subtree.threshold[subtree_node_idx])
        else:
            # not a leaf
                tree.threshold[tree_node_idx], subtree.threshold[subtree_node_idx]
            )
            stack.append((tree_c_left[tree_node_idx], subtree_c_left[subtree_node_idx]))
            stack.append(
                (tree_c_right[tree_node_idx], subtree_c_right[subtree_node_idx])
            )

def test_apply_path_readonly_all_trees(name, splitter, sparse_container):
    dataset = DATASETS["clf_small"]
    X_small = dataset["X"].astype(tree._tree.DTYPE, copy=False)
    if sparse_container is None:
        X_readonly = create_memmap_backed_data(X_small)
    else:
        X_readonly = sparse_container(dataset["X"])

        X_readonly.data = np.array(X_readonly.data, dtype=tree._tree.DTYPE)
        (
            X_readonly.data,
            X_readonly.indices,
            X_readonly.indptr,
        ) = create_memmap_backed_data(
            (X_readonly.data, X_readonly.indices, X_readonly.indptr)
        )

    y_readonly = create_memmap_backed_data(np.array(y_small, dtype=tree._tree.DTYPE))
    est = ALL_TREES[name](splitter=splitter)
    est.fit(X_readonly, y_readonly)
    assert_array_equal(est.predict(X_readonly), est.predict(X_small))
        est.decision_path(X_readonly).todense(), est.decision_path(X_small).todense()
    )

def test_balance_property(criterion, Tree):
    # Test that sum(y_pred)=sum(y_true) on training set.
    # This works if the mean is predicted (should even be true for each leaf).
    # MAE predicts the median and is therefore excluded from this test.

    # Choose a training set with non-negative targets (for poisson)
    reg = Tree(criterion=criterion)

def test_poisson_zero_nodes(seed):
    # Test that sum(y)=0 and therefore y_pred=0 is forbidden on nodes.
    X = [[0, 0], [0, 1], [0, 2], [0, 3], [1, 0], [1, 2], [1, 2], [1, 3]]
    y = [0, 0, 0, 0, 1, 2, 3, 4]
    # Note that X[:, 0] == 0 is a 100% indicator for y == 0. The tree can
    # easily learn that:
    reg = DecisionTreeRegressor(criterion="squared_error", random_state=seed)
    assert np.amin(reg.predict(X)) == 0
    # whereas Poisson must predict strictly positive numbers
    reg = DecisionTreeRegressor(criterion="poisson", random_state=seed)
    assert np.all(reg.predict(X) > 0)

    # Test additional dataset where something could go wrong.
        effective_rank=n_features * 2 // 3,
        tail_strength=0.6,
        n_samples=1_000,
        n_informative=n_features * 2 // 3,
    )
    # some excess zeros
    y[(-1 < y) & (y < 0)] = 0
    # make sure the target is positive
    y = np.abs(y)

    # For a Poisson distributed target, Poisson loss should give better results
    # than squared error measured in Poisson deviance as metric.
    # We have a similar test, test_poisson(), in
    # sklearn/ensemble/_hist_gradient_boosting/tests/test_gradient_boosting.py
    )
    )
    tree_poi = DecisionTreeRegressor(
        criterion="poisson", min_samples_split=10, random_state=rng
    )
    tree_mse = DecisionTreeRegressor(
        criterion="squared_error", min_samples_split=10, random_state=rng
    )

    tree_poi.fit(X_train, y_train)
    tree_mse.fit(X_train, y_train)

    for X, y, val in [(X_train, y_train, "train"), (X_test, y_test, "test")]:
        metric_poi = mean_poisson_deviance(y, tree_poi.predict(X))
        metric_mse = mean_poisson_deviance(y, np.clip(tree_mse.predict(X), 1e-15, None))
        # test set.
        if val == "test":
            assert metric_poi < 0.5 * metric_mse
        assert metric_poi < 0.75 * metric_dummy

def test_criterion_entropy_same_as_log_loss(Tree, n_classes):
    """Test that criterion=entropy gives same as log_loss."""
    n_samples, n_features = 50, 5
    )
    tree_log_loss = Tree(criterion="log_loss", random_state=43).fit(X, y)
    tree_entropy = Tree(criterion="entropy", random_state=43).fit(X, y)

        tree_log_loss.tree_,
        tree_entropy.tree_,
        f"{Tree!r} with criterion 'entropy' and 'log_loss' gave different trees.",
    )
    assert_allclose(tree_log_loss.predict(X), tree_entropy.predict(X))

def test_different_endianness_pickle():

    clf = DecisionTreeClassifier(random_state=0, max_depth=3)

    def reduce_ndarray(arr):
        return arr.byteswap().view(arr.dtype.newbyteorder()).__reduce__()

    def get_pickle_non_native_endianness():
        p.dispatch_table[np.ndarray] = reduce_ndarray

        f.seek(0)
        return f

    new_clf = pickle.load(get_pickle_non_native_endianness())
    assert np.isclose(score, new_score)

def test_different_endianness_joblib_pickle():

    class NonNativeEndiannessNumpyPickler(NumpyPickler):
        def save(self, obj):
            if isinstance(obj, np.ndarray):
                obj = obj.byteswap().view(obj.dtype.newbyteorder())
            super().save(obj)

    def get_joblib_pickle_non_native_endianness():
        p = NonNativeEndiannessNumpyPickler(f)

        f.seek(0)
        return f

    new_clf = joblib.load(get_joblib_pickle_non_native_endianness())

    # field names in Node struct with SIZE_t types (see sklearn/tree/_tree.pxd)
    indexing_field_names = ["left_child", "right_child", "feature", "n_node_samples"]

    }

    )

def get_different_alignment_node_ndarray(node_ndarray):
    }
    offsets = [offset for dtype, offset in node_ndarray.dtype.fields.values()]
    shifted_offsets = [8 + offset for offset in offsets]

        {
            "names": list(new_dtype_dict.keys()),
            "formats": list(new_dtype_dict.values()),
            "offsets": shifted_offsets,
        }
    )

def reduce_tree_with_different_bitness(tree):
    new_dtype = np.int64 if _IS_32BIT else np.int32
    tree_cls, (n_features, n_classes, n_outputs), state = tree.__reduce__()
    new_n_classes = n_classes.astype(new_dtype, casting="same_kind")

    return (tree_cls, (n_features, new_n_classes, n_outputs), new_state)

        p.dispatch_table[CythonTree] = reduce_tree_with_different_bitness

        f.seek(0)
        return f

        f.seek(0)
        return f

def test_check_n_classes():
    expected_dtype = np.dtype(np.int32) if _IS_32BIT else np.dtype(np.int64)
    allowed_dtypes = [np.dtype(np.int32), np.dtype(np.int64)]
    allowed_dtypes += [dt.newbyteorder() for dt in allowed_dtypes]

    n_classes = np.array([0, 1], dtype=expected_dtype)
    for dt in allowed_dtypes:
        _check_n_classes(n_classes.astype(dt), expected_dtype)

    with pytest.raises(ValueError, match="Wrong dimensions.+n_classes"):
        wrong_dim_n_classes = np.array([[0, 1]], dtype=expected_dtype)
        _check_n_classes(wrong_dim_n_classes, expected_dtype)

    with pytest.raises(ValueError, match="n_classes.+incompatible dtype"):
        wrong_dtype_n_classes = n_classes.astype(np.float64)
        _check_n_classes(wrong_dtype_n_classes, expected_dtype)

def test_check_value_ndarray():
    expected_dtype = np.dtype(np.float64)
    expected_shape = (5, 1, 2)
    value_ndarray = np.zeros(expected_shape, dtype=expected_dtype)

    allowed_dtypes = [expected_dtype, expected_dtype.newbyteorder()]

        _check_value_ndarray(
            value_ndarray, expected_dtype=dt, expected_shape=expected_shape
        )

    with pytest.raises(ValueError, match="Wrong shape.+value array"):
            value_ndarray, expected_dtype=expected_dtype, expected_shape=(1, 2)
        )

    for problematic_arr in [value_ndarray[:, :, :1], np.asfortranarray(value_ndarray)]:
        with pytest.raises(ValueError, match="value array.+C-contiguous"):
            _check_value_ndarray(
                problematic_arr,
                expected_dtype=expected_dtype,
                expected_shape=problematic_arr.shape,
            )

    with pytest.raises(ValueError, match="value array.+incompatible dtype"):
            value_ndarray.astype(np.float32),
            expected_dtype=expected_dtype,
            expected_shape=expected_shape,
        )

def test_check_node_ndarray():
    expected_dtype = NODE_DTYPE

    node_ndarray = np.zeros((5,), dtype=expected_dtype)

    valid_node_ndarrays = [
        node_ndarray,
        get_different_bitness_node_ndarray(node_ndarray),
        get_different_alignment_node_ndarray(node_ndarray),
    ]
    valid_node_ndarrays += [
        arr.astype(arr.dtype.newbyteorder()) for arr in valid_node_ndarrays
    ]

    for arr in valid_node_ndarrays:
        _check_node_ndarray(node_ndarray, expected_dtype=expected_dtype)

    with pytest.raises(ValueError, match="Wrong dimensions.+node array"):
        problematic_node_ndarray = np.zeros((5, 2), dtype=expected_dtype)
        _check_node_ndarray(problematic_node_ndarray, expected_dtype=expected_dtype)

    with pytest.raises(ValueError, match="node array.+C-contiguous"):
        problematic_node_ndarray = node_ndarray[::2]

    dtype_dict = {name: dtype for name, (dtype, _) in node_ndarray.dtype.fields.items()}

    # array with wrong 'threshold' field dtype (int64 rather than float64)
    new_dtype_dict = dtype_dict.copy()
    new_dtype_dict["threshold"] = np.int64

    )
    problematic_node_ndarray = node_ndarray.astype(new_dtype)

    with pytest.raises(ValueError, match="node array.+incompatible dtype"):

    # array with wrong 'left_child' field dtype (float64 rather than int64 or int32)
    new_dtype_dict["left_child"] = np.float64
    )

def test_splitter_serializable(Splitter):
    """Check that splitters are serializable."""
    max_features = 10
    n_outputs, n_classes = 2, np.array([3, 2], dtype=np.intp)

    criterion = CRITERIA_CLF["gini"](n_outputs, n_classes)
    splitter = Splitter(criterion, max_features, 5, 0.5, rng, monotonic_cst=None)
    splitter_serialize = pickle.dumps(splitter)

    splitter_back = pickle.loads(splitter_serialize)
    assert splitter_back.max_features == max_features
    assert isinstance(splitter_back, Splitter)

def test_tree_deserialization_from_read_only_buffer(tmpdir):
    """Check that Trees can be deserialized with read only buffers.

    Non-regression test for gh-25584.
    """
    pickle_path = str(tmpdir.join("clf.joblib"))
    clf.fit(X_small, y_small)

    joblib.dump(clf, pickle_path)
    loaded_clf = joblib.load(pickle_path, mmap_mode="r")

        loaded_clf.tree_,
        clf.tree_,
        "The trees of the original and loaded classifiers are not equal.",
    )

def test_min_sample_split_1_error(Tree):
    """Check that an error is raised when min_sample_split=1.

    non-regression test for issue gh-25481.
    """
    X = np.array([[0, 0], [1, 1]])
    y = np.array([0, 1])

    # min_samples_split=1.0 is valid
    Tree(min_samples_split=1.0).fit(X, y)

    # min_samples_split=1 is invalid
    tree = Tree(min_samples_split=1)
    msg = (
        r"'min_samples_split' .* must be an int in the range \[2, inf\) "
        r"or a float in the range \(0.0, 1.0\]"
    )
        tree.fit(X, y)

def test_missing_values_best_splitter_on_equal_nodes_no_missing(criterion):
    """Check missing values goes to correct node during predictions."""
    X = np.array([[0, 1, 2, 3, 8, 9, 11, 12, 15]]).T
    y = np.array([0.1, 0.2, 0.3, 0.2, 1.4, 1.4, 1.5, 1.6, 2.6])

    dtc = DecisionTreeRegressor(random_state=42, max_depth=1, criterion=criterion)
    dtc.fit(X, y)

    # Goes to right node because it has the most data points
    y_pred = dtc.predict([[np.nan]])
    assert_allclose(y_pred, [np.mean(y[-5:])])

    # equal number of elements in both nodes
    X_equal = X[:-1]
    y_equal = y[:-1]

    dtc.fit(X_equal, y_equal)

    # Goes to right node because the implementation sets:
    # missing_go_to_left = n_left > n_right, which is False
    assert_allclose(y_pred, [np.mean(y_equal[-4:])])

def test_missing_values_random_splitter_on_equal_nodes_no_missing(criterion, seed):
    """Check missing values go to the correct node during predictions for ExtraTree.

    Since ETC use random splits, we use different seeds to verify that the
    left/right node is chosen correctly when the splits occur.
    """

    etr = ExtraTreeRegressor(random_state=seed, max_depth=1, criterion=criterion)
    etr.fit(X, y)

    # Get the left and right children of the root node
    left_child = etr.tree_.children_left[0]
    right_child = etr.tree_.children_right[0]

    # Get the number of samples for the left and right children
    left_samples = etr.tree_.weighted_n_node_samples[left_child]
    right_samples = etr.tree_.weighted_n_node_samples[right_child]
    went_left = left_samples > right_samples

    # predictions
    y_pred_left = etr.tree_.value[left_child][0]
    y_pred_right = etr.tree_.value[right_child][0]

    # Goes to node with the most data points
    y_pred = etr.predict([[np.nan]])
    if went_left:
        assert_allclose(y_pred_left, y_pred)
    else:
        assert_allclose(y_pred_right, y_pred)

def test_missing_values_best_splitter_three_classes(criterion):
    """Test when missing values are uniquely present in a class among 3 classes."""
    missing_values_class = 0
    X = np.array([[np.nan] * 4 + [0, 1, 2, 3, 8, 9, 11, 12]]).T
    y = np.array([missing_values_class] * 4 + [1] * 4 + [2] * 4)
    dtc = DecisionTreeClassifier(random_state=42, max_depth=2, criterion=criterion)

    X_test = np.array([[np.nan, 3, 12]]).T
    y_nan_pred = dtc.predict(X_test)
    # Missing values necessarily are associated to the observed class.
    assert_array_equal(y_nan_pred, [missing_values_class, 1, 2])

def test_missing_values_best_splitter_to_left(criterion):
    """Missing values spanning only one class at fit-time must make missing
    values at predict-time be classified has belonging to this class."""
    X = np.array([[np.nan] * 4 + [0, 1, 2, 3, 4, 5]]).T
    y = np.array([0] * 4 + [1] * 6)

    X_test = np.array([[np.nan, 5, np.nan]]).T
    y_pred = dtc.predict(X_test)

    assert_array_equal(y_pred, [0, 1, 0])

def test_missing_values_best_splitter_to_right(criterion):
    """Missing values and non-missing values sharing one class at fit-time
    must make missing values at predict-time be classified has belonging
    to this class."""
    y = np.array([1] * 4 + [0] * 4 + [1] * 2)

    X_test = np.array([[np.nan, 1.2, 4.8]]).T

    assert_array_equal(y_pred, [1, 0, 1])

def test_missing_values_best_splitter_missing_both_classes_has_nan(criterion):
    """Check behavior of missing value when there is one missing value in each class."""
    X = np.array([[1, 2, 3, 5, np.nan, 10, 20, 30, 60, np.nan]]).T
    y = np.array([0] * 5 + [1] * 5)

    dtc = DecisionTreeClassifier(random_state=42, max_depth=1, criterion=criterion)
    X_test = np.array([[np.nan, 2.3, 34.2]]).T

    # Missing value goes to the class at the right (here 1) because the implementation
    # searches right first.

def test_missing_value_errors(sparse_container, tree):
    """Check unsupported configurations for missing values."""

    with pytest.raises(ValueError, match="Input X contains NaN"):

def test_missing_values_poisson(Tree):
    """Smoke test for poisson regression and missing values."""
    X, y = diabetes.data.copy(), diabetes.target

    # Set some values missing
    X[::5, 0] = np.nan
    X[::6, -1] = np.nan

    reg = Tree(criterion="poisson", random_state=42)

    y_pred = reg.predict(X)
    assert (y_pred >= 0.0).all()

def make_friedman1_classification(*args, **kwargs):
    X, y = datasets.make_friedman1(*args, **kwargs)
    y = y > 14
    return X, y

def test_missing_values_is_resilience(
    make_data, Tree, sample_weight_train, global_random_seed, tolerance
):
    """Check that trees can deal with missing values have decent performance."""
    n_samples, n_features = 5_000, 10
    X, y = make_data(
    )

    X_missing[rng.choice([False, True], size=X.shape, p=[0.9, 0.1])] = np.nan
        X_missing, y, random_state=global_random_seed
    )
    if sample_weight_train == "ones":
        sample_weight = np.ones(X_missing_train.shape[0])
    else:

    # max_depth is used to avoid overfitting and also improve the runtime
    # of the test.
    max_depth = 10
    native_tree = Tree(max_depth=max_depth, random_state=global_random_seed)
    native_tree.fit(X_missing_train, y_train, sample_weight=sample_weight)
    score_native_tree = native_tree.score(X_missing_test, y_test)

    tree_with_imputer = make_pipeline(
        SimpleImputer(), Tree(max_depth=max_depth, random_state=global_random_seed)
    )
    tree_with_imputer.fit(X_missing_train, y_train)
    score_tree_with_imputer = tree_with_imputer.score(X_missing_test, y_test)

    assert score_native_tree + tolerance > score_tree_with_imputer, (
        f"{score_native_tree=} + {tolerance} should be strictly greater than"
        f" {score_tree_with_imputer}"
    )

def test_missing_value_is_predictive(Tree, expected_score, global_random_seed):
    """Check the tree learns when only the missing value is predictive."""

    X = rng.standard_normal(size=(n_samples, 20))
    y = np.concatenate([np.zeros(n_samples // 2), np.ones(n_samples // 2)])
    # y = rng.randint(0, high=2, size=n_samples)

    y_mask = y.copy().astype(bool)

    X_predictive = rng.standard_normal(size=n_samples)
    X_predictive[y_mask] = np.nan

    X[:, 5] = X_predictive

    tree = Tree(random_state=global_random_seed)

    # Check that the tree can learn the predictive feature
    # over an average of cross-validation fits.
    tree_cv_score = cross_val_score(tree, X, y, cv=5).mean()
    assert tree_cv_score >= expected_score, (
        f"Expected CV score: {expected_score} but got {tree_cv_score}"
    )

def test_sample_weight_non_uniform(make_data, Tree):
    """Check sample weight is correctly handled with missing values."""

    X[rng.choice([False, True], size=X.shape, p=[0.9, 0.1])] = np.nan

    # Zero sample weight is the same as removing the sample
    sample_weight = np.ones(X.shape[0])
    sample_weight[::2] = 0.0

    tree_with_sw = Tree(random_state=0)
    tree_with_sw.fit(X, y, sample_weight=sample_weight)

    tree_samples_removed = Tree(random_state=0)
    tree_samples_removed.fit(X[1::2, :], y[1::2])

    assert_allclose(tree_samples_removed.predict(X), tree_with_sw.predict(X))

def test_deterministic_pickle():
    # https://github.com/scikit-learn/scikit-learn/issues/27268
    # Uninitialised memory would lead to the two pickle strings being different.
    tree1 = DecisionTreeClassifier(random_state=0).fit(iris.data, iris.target)
    tree2 = DecisionTreeClassifier(random_state=0).fit(iris.data, iris.target)

    pickle1 = pickle.dumps(tree1)
    pickle2 = pickle.dumps(tree2)

    assert pickle1 == pickle2

def test_regression_tree_missing_values_toy(Tree, X, criterion):
    """Check that we properly handle missing values in regression trees using a toy
    dataset.

    The regression targeted by this test was that we were not reinitializing the
    criterion when it comes to the number of missing values. Therefore, the value
    of the critetion (i.e. MSE) was completely wrong.

    This test check that the MSE is null when there is a single sample in the leaf.

    https://github.com/scikit-learn/scikit-learn/issues/28254
    https://github.com/scikit-learn/scikit-learn/issues/28316
    """
    y = np.arange(6)

    tree = Tree(criterion=criterion, random_state=0).fit(X, y)
    tree_ref = clone(tree).fit(y.reshape(-1, 1), y)

    impurity = tree.tree_.impurity
    assert all(impurity >= 0), impurity.min()  # MSE should always be positive

    # Check the impurity match after the first split
    assert_allclose(tree.tree_.impurity[:2], tree_ref.tree_.impurity[:2])

    # Find the leaves with a single sample where the MSE should be 0
    leaves_idx = np.flatnonzero(
        (tree.tree_.children_left == -1) & (tree.tree_.n_node_samples == 1)
    )
    assert_allclose(tree.tree_.impurity[leaves_idx], 0.0)

def test_regression_extra_tree_missing_values_toy(global_random_seed):
    X = np.arange(n_samples, dtype=np.float64).reshape(-1, 1)
    X[-20:, :] = np.nan
    rng.shuffle(X)

    tree = ExtraTreeRegressor(random_state=global_random_seed, max_depth=5).fit(X, y)

    assert all(impurity >= 0), impurity  # MSE should always be positive

def test_classification_tree_missing_values_toy():
    """Check that we properly handle missing values in classification trees using a toy
    dataset.

    The test is more involved because we use a case where we detected a regression
    in a random forest. We therefore define the seed and bootstrap indices to detect
    one of the non-frequent regression.

    Here, we check that the impurity is null or positive in the leaves.

    """
    X, y = datasets.load_iris(return_X_y=True)

    mask = rng.binomial(
        n=np.ones(shape=(1, 4), dtype=np.int32), p=X[:, [2]] / 8
    ).astype(bool)
    X_missing[mask] = np.nan
    X_train, _, y_train, _ = train_test_split(X_missing, y, random_state=13)

    # fmt: off
    # no black reformatting for this specific array
    indices = np.array([
        2, 81, 39, 97, 91, 38, 46, 31, 101, 13, 89, 82, 100, 42, 69, 27, 81, 16, 73, 74,
        51, 47, 107, 17, 75, 110, 20, 15, 104, 57, 26, 15, 75, 79, 35, 77, 90, 51, 46,
        13, 94, 91, 23, 8, 93, 93, 73, 77, 12, 13, 74, 109, 110, 24, 10, 23, 104, 27,
        92, 52, 20, 109, 8, 8, 28, 27, 35, 12, 12, 7, 43, 0, 30, 31, 78, 12, 24, 105,
        50, 0, 73, 12, 102, 105, 13, 31, 1, 69, 11, 32, 75, 90, 106, 94, 60, 56, 35, 17,
        62, 85, 81, 39, 80, 16, 63, 6, 80, 84, 3, 3, 76, 78
    ], dtype=np.int32)
    # fmt: on

    tree = DecisionTreeClassifier(
        max_depth=3, max_features="sqrt", random_state=1857819720
    )
    tree.fit(X_train[indices], y_train[indices])
    assert all(tree.tree_.impurity >= 0)

    )

def test_build_pruned_tree_py():
    """Test pruning a tree with the Python caller of the Cythonized prune tree."""
    tree = DecisionTreeClassifier(random_state=0, max_depth=1)
    tree.fit(iris.data, iris.target)

    n_classes = np.atleast_1d(tree.n_classes_)
    pruned_tree = CythonTree(tree.n_features_in_, n_classes, tree.n_outputs_)

    # only keep the root note
    leave_in_subtree = np.zeros(tree.tree_.node_count, dtype=np.uint8)
    leave_in_subtree[0] = 1
    _build_pruned_tree_py(pruned_tree, tree.tree_, leave_in_subtree)

    assert tree.tree_.node_count == 3
    assert pruned_tree.node_count == 1
        assert_array_equal(tree.tree_.value, pruned_tree.value)
    assert_array_equal(tree.tree_.value[0], pruned_tree.value[0])

    # now keep all the leaves
    leave_in_subtree[1:] = 1

    # Prune the tree
    assert pruned_tree.node_count == 3, pruned_tree.node_count
    assert_array_equal(tree.tree_.value, pruned_tree.value)

def test_build_pruned_tree_infinite_loop():
    """Test pruning a tree does not result in an infinite loop."""

    # Create a tree with root and two children

    # only keeping one child as a leaf results in an improper tree
    leave_in_subtree[1] = 1
        ValueError, match="Node has reached a leaf in the original tree"
    ):
        _build_pruned_tree_py(pruned_tree, tree.tree_, leave_in_subtree)

def test_sort_log2_build():
    """Non-regression test for gh-30554.

    Using log2 and log in sort correctly sorts feature_values, but the tie breaking is
    different which can results in placing samples in a different order.
    """
    rng = np.random.default_rng(75)
    some = rng.normal(loc=0.0, scale=10.0, size=10).astype(np.float32)
    feature_values = np.concatenate([some] * 5)
    samples = np.arange(50, dtype=np.intp)
    _py_sort(feature_values, samples, 50)
    # fmt: off
    expected_samples = [
        0, 40, 30, 20, 10, 29, 39, 19, 49,  9, 45, 15, 35,  5, 25, 11, 31,
        41,  1, 21, 22, 12,  2, 42, 32, 23, 13, 43,  3, 33,  6, 36, 46, 16,
        26,  4, 14, 24, 34, 44, 27, 47,  7, 37, 17,  8, 38, 48, 28, 18
    ]
    # fmt: on
    assert_array_equal(samples, expected_samples)

        f.seek(0)
        return f

        f.seek(0)
        return f

        f.seek(0)
        return f

        f.seek(0)
        return f

def test_graphviz_toy():
    # Check correctness of export_graphviz
    clf = DecisionTreeClassifier(
        max_depth=3, min_samples_split=2, criterion="gini", random_state=2
    )

    # Test export code
    contents1 = export_graphviz(clf, out_file=None)
    contents2 = (
        "digraph Tree {\n"
        'node [shape=box, fontname="helvetica"] ;\n'
        'edge [fontname="helvetica"] ;\n'
        '0 [label="x[0] <= 0.0\\ngini = 0.5\\nsamples = 6\\n'
        'value = [3, 3]"] ;\n'
        '1 [label="gini = 0.0\\nsamples = 3\\nvalue = [3, 0]"] ;\n'
        "0 -> 1 [labeldistance=2.5, labelangle=45, "
        'headlabel="True"] ;\n'
        '2 [label="gini = 0.0\\nsamples = 3\\nvalue = [0, 3]"] ;\n'
        "0 -> 2 [labeldistance=2.5, labelangle=-45, "
        'headlabel="False"] ;\n'
        "}"
    )
    assert contents1 == contents2

    # Test with feature_names
    contents1 = export_graphviz(
        clf, feature_names=["feature0", "feature1"], out_file=None
    )
        '0 [label="feature0 <= 0.0\\ngini = 0.5\\nsamples = 6\\n'
        "}"
    )

    # Test with feature_names (escaped)
        clf, feature_names=['feature"0"', 'feature"1"'], out_file=None
    )
        '0 [label="feature\\"0\\" <= 0.0\\n'
        "gini = 0.5\\nsamples = 6\\n"
        "}"
    )

    # Test with class_names
    contents1 = export_graphviz(clf, class_names=["yes", "no"], out_file=None)
        'value = [3, 3]\\nclass = yes"] ;\n'
        '1 [label="gini = 0.0\\nsamples = 3\\nvalue = [3, 0]\\n'
        'class = yes"] ;\n'
        '2 [label="gini = 0.0\\nsamples = 3\\nvalue = [0, 3]\\n'
        'class = no"] ;\n'
        "}"
    )

    # Test with class_names (escaped)
    contents1 = export_graphviz(clf, class_names=['"yes"', '"no"'], out_file=None)
        'value = [3, 3]\\nclass = \\"yes\\""] ;\n'
        'class = \\"yes\\""] ;\n'
        'class = \\"no\\""] ;\n'
        "}"
    )

    # Test plot_options
        clf,
        filled=True,
        impurity=False,
        proportion=True,
        special_characters=True,
        rounded=True,
        out_file=None,
        fontname="sans",
    )
        'node [shape=box, style="filled, rounded", color="black", '
        'fontname="sans"] ;\n'
        'edge [fontname="sans"] ;\n'
        "0 [label=<x<SUB>0</SUB> &le; 0.0<br/>samples = 100.0%<br/>"
        'value = [0.5, 0.5]>, fillcolor="#ffffff"] ;\n'
        "1 [label=<samples = 50.0%<br/>value = [1.0, 0.0]>, "
        'fillcolor="#e58139"] ;\n'
        "2 [label=<samples = 50.0%<br/>value = [0.0, 1.0]>, "
        'fillcolor="#399de5"] ;\n'
        "}"
    )

    # Test max_depth
    contents1 = export_graphviz(clf, max_depth=0, class_names=True, out_file=None)
        'value = [3, 3]\\nclass = y[0]"] ;\n'
        '1 [label="(...)"] ;\n'
        "0 -> 1 ;\n"
        '2 [label="(...)"] ;\n'
        "0 -> 2 ;\n"
        "}"
    )

    # Test max_depth with plot_options
        clf, max_depth=0, filled=True, out_file=None, node_ids=True
    )
        'node [shape=box, style="filled", color="black", '
        'fontname="helvetica"] ;\n'
        '0 [label="node #0\\nx[0] <= 0.0\\ngini = 0.5\\n'
        'samples = 6\\nvalue = [3, 3]", fillcolor="#ffffff"] ;\n'
        '1 [label="(...)", fillcolor="#C0C0C0"] ;\n'
        "0 -> 1 ;\n"
        '2 [label="(...)", fillcolor="#C0C0C0"] ;\n'
        "0 -> 2 ;\n"
        "}"
    )

    # Test multi-output with weighted samples
        max_depth=2, min_samples_split=2, criterion="gini", random_state=2
    )
    clf = clf.fit(X, y2, sample_weight=w)

    contents1 = export_graphviz(clf, filled=True, impurity=False, out_file=None)
        '0 [label="x[0] <= 0.0\\nsamples = 6\\n'
        "value = [[3.0, 1.5, 0.0]\\n"
        '[3.0, 1.0, 0.5]]", fillcolor="#ffffff"] ;\n'
        '1 [label="samples = 3\\nvalue = [[3, 0, 0]\\n'
        '[3, 0, 0]]", fillcolor="#e58139"] ;\n'
        '2 [label="x[0] <= 1.5\\nsamples = 3\\n'
        "value = [[0.0, 1.5, 0.0]\\n"
        '[0.0, 1.0, 0.5]]", fillcolor="#f1bd97"] ;\n'
        '3 [label="samples = 2\\nvalue = [[0, 1, 0]\\n'
        '[0, 1, 0]]", fillcolor="#e58139"] ;\n'
        "2 -> 3 ;\n"
        '4 [label="samples = 1\\nvalue = [[0.0, 0.5, 0.0]\\n'
        '[0.0, 0.0, 0.5]]", fillcolor="#e58139"] ;\n'
        "2 -> 4 ;\n"
        "}"
    )

    # Test regression output with plot_options
    clf = DecisionTreeRegressor(
        max_depth=3, min_samples_split=2, criterion="squared_error", random_state=2
    )

        clf,
        leaves_parallel=True,
        rotate=True,
    )
        "graph [ranksep=equally, splines=polyline] ;\n"
        "rankdir=LR ;\n"
        '0 [label="x[0] <= 0.0\\nsquared_error = 1.0\\nsamples = 6\\n'
        'value = 0.0", fillcolor="#f2c09c"] ;\n'
        '1 [label="squared_error = 0.0\\nsamples = 3\\'
        'nvalue = -1.0", '
        'fillcolor="#ffffff"] ;\n'
        "0 -> 1 [labeldistance=2.5, labelangle=-45, "
        '2 [label="squared_error = 0.0\\nsamples = 3\\nvalue = 1.0", '
        "0 -> 2 [labeldistance=2.5, labelangle=45, "
        "{rank=same ; 0} ;\n"
        "{rank=same ; 1; 2} ;\n"
        "}"
    )

    # Test classifier with degraded learning set
    clf = DecisionTreeClassifier(max_depth=3)
    clf.fit(X, y_degraded)

    contents1 = export_graphviz(clf, filled=True, out_file=None)
        '0 [label="gini = 0.0\\nsamples = 6\\nvalue = 6.0", '
        "}"
    )

def test_graphviz_feature_class_names_array_support(constructor):
    # Check that export_graphviz treats feature names
    # and class names correctly and supports arrays
    )

        clf, feature_names=constructor(["feature0", "feature1"]), out_file=None
    )
        "}"
    )

        clf, class_names=constructor(["yes", "no"]), out_file=None
    )
        "}"
    )

def test_graphviz_errors():
    # Check for errors of export_graphviz
    clf = DecisionTreeClassifier(max_depth=3, min_samples_split=2)

    # Check not-fitted decision tree error
    out = StringIO()
        export_graphviz(clf, out)

    # Check if it errors when length of feature_names
    # mismatches with number of features
    message = "Length of feature_names, 1 does not match number of features, 2"
        export_graphviz(clf, None, feature_names=["a"])

    message = "Length of feature_names, 3 does not match number of features, 2"
        export_graphviz(clf, None, feature_names=["a", "b", "c"])

    # Check error when argument is not an estimator
    message = "is not an estimator instance"
    with pytest.raises(TypeError, match=message):
        export_graphviz(clf.fit(X, y).tree_)

    # Check class_names error
    with pytest.raises(IndexError):
        export_graphviz(clf, out, class_names=[])

def test_friedman_mse_in_graphviz():
    clf = DecisionTreeRegressor(criterion="friedman_mse", random_state=0)
    dot_data = StringIO()
    export_graphviz(clf, out_file=dot_data)

    clf = GradientBoostingClassifier(n_estimators=2, random_state=0)
    for estimator in clf.estimators_:
        export_graphviz(estimator[0], out_file=dot_data)

    for finding in finditer(r"\[.*?samples.*?\]", dot_data.getvalue()):
        assert "friedman_mse" in finding.group()

def test_precision():
    rng_reg = RandomState(2)
    rng_clf = RandomState(8)
    for X, y, clf in zip(
        (rng_reg.random_sample((5, 2)), rng_clf.random_sample((1000, 4))),
        (rng_reg.random_sample((5,)), rng_clf.randint(2, size=(1000,))),
        (
            DecisionTreeRegressor(
                criterion="friedman_mse", random_state=0, max_depth=1
            ),
            DecisionTreeClassifier(max_depth=1, random_state=0),
        ),
    ):
        for precision in (4, 3):
            dot_data = export_graphviz(
                clf, out_file=None, precision=precision, proportion=True
            )

            # With the current random state, the impurity and the threshold
            # will have the number of precision set in the export_graphviz
            # function. We will check the number of precision with a strict
            # equality. The value reported will have only 2 precision and
            # therefore, only a less equal comparison will be done.

            # check value
            for finding in finditer(r"value = \d+\.\d+", dot_data):
                assert len(search(r"\.\d+", finding.group()).group()) <= precision + 1
            # check impurity
            if is_classifier(clf):
                pattern = r"gini = \d+\.\d+"
            else:
                pattern = r"friedman_mse = \d+\.\d+"

            for finding in finditer(pattern, dot_data):
                assert len(search(r"\.\d+", finding.group()).group()) == precision + 1
            # check threshold
            for finding in finditer(r"<= \d+\.\d+", dot_data):

def test_export_text_errors():
    clf = DecisionTreeClassifier(max_depth=2, random_state=0)
    err_msg = "feature_names must contain 2 elements, got 1"
        export_text(clf, feature_names=["a"])
    err_msg = (
        "When `class_names` is an array, it should contain as"
        " many items as `decision_tree.classes_`. Got 1 while"
        " the tree was fitted with 2 classes."
    )
        export_text(clf, class_names=["a"])

def test_export_text():

    expected_report = dedent(
        """
    |--- feature_1 <= 0.00
    |   |--- class: -1
    |--- feature_1 >  0.00
    |   |--- class: 1
    """
    ).lstrip()

    assert export_text(clf) == expected_report
    # testing that leaves at level 1 are not truncated
    assert export_text(clf, max_depth=0) == expected_report
    # testing that the rest of the tree is truncated
    assert export_text(clf, max_depth=10) == expected_report

        """
    |   |--- weights: [3.00, 0.00] class: -1
    |   |--- weights: [0.00, 3.00] class: 1
    """
    assert export_text(clf, show_weights=True) == expected_report

        """
    |- feature_1 <= 0.00
    | |- class: -1
    |- feature_1 >  0.00
    | |- class: 1
    """
    assert export_text(clf, spacing=1) == expected_report

    X_l = [[-2, -1], [-1, -1], [-1, -2], [1, 1], [1, 2], [2, 1], [-1, 1]]
    y_l = [-1, -1, -1, 1, 1, 1, 2]
    clf = DecisionTreeClassifier(max_depth=4, random_state=0)
    clf.fit(X_l, y_l)
        """
    |   |--- truncated branch of depth 2
    """

    X_mo = [[-2, -1], [-1, -1], [-1, -2], [1, 1], [1, 2], [2, 1]]
    y_mo = [[-1, -1], [-1, -1], [-1, -1], [1, 1], [1, 1], [1, 1]]

    reg = DecisionTreeRegressor(max_depth=2, random_state=0)
    reg.fit(X_mo, y_mo)

        """
    |--- feature_1 <= 0.0
    |   |--- value: [-1.0, -1.0]
    |--- feature_1 >  0.0
    |   |--- value: [1.0, 1.0]
    """
    assert export_text(reg, decimals=1) == expected_report
    assert export_text(reg, decimals=1, show_weights=True) == expected_report

    X_single = [[-2], [-1], [-1], [1], [1], [2]]
    reg.fit(X_single, y_mo)

        """
    |--- first <= 0.0
    |--- first >  0.0
    """
    assert export_text(reg, decimals=1, feature_names=["first"]) == expected_report
    assert (
        export_text(reg, decimals=1, show_weights=True, feature_names=["first"])
        == expected_report
    )

def test_export_text_feature_class_names_array_support(constructor):

        """
    |--- b <= 0.00
    |--- b >  0.00
    """
    assert export_text(clf, feature_names=constructor(["a", "b"])) == expected_report

        """
    |   |--- class: cat
    |   |--- class: dog
    """
    assert export_text(clf, class_names=constructor(["cat", "dog"])) == expected_report

def test_plot_tree_entropy(pyplot):
    # mostly smoke tests
    # Check correctness of export_graphviz for criterion = entropy
        max_depth=3, min_samples_split=2, criterion="entropy", random_state=2
    )

    feature_names = ["first feat", "sepal_width"]
    nodes = plot_tree(clf, feature_names=feature_names)
    assert len(nodes) == 5
    assert (
        nodes[0].get_text()
        == "first feat <= 0.0\nentropy = 1.0\nsamples = 6\nvalue = [3, 3]"
    )
    assert nodes[1].get_text() == "entropy = 0.0\nsamples = 3\nvalue = [3, 0]"
    assert nodes[2].get_text() == "True  "
    assert nodes[3].get_text() == "entropy = 0.0\nsamples = 3\nvalue = [0, 3]"
    assert nodes[4].get_text() == "  False"

def test_plot_tree_gini(pyplot, fontsize):
    # Check correctness of export_graphviz for criterion = gini
        random_state=2,
    )

    nodes = plot_tree(clf, feature_names=feature_names, fontsize=fontsize)
    if fontsize is not None:
        assert all(node.get_fontsize() == fontsize for node in nodes)
    assert (
        == "first feat <= 0.0\ngini = 0.5\nsamples = 6\nvalue = [3, 3]"
    )
    assert nodes[1].get_text() == "gini = 0.0\nsamples = 3\nvalue = [3, 0]"
    assert nodes[3].get_text() == "gini = 0.0\nsamples = 3\nvalue = [0, 3]"

def test_not_fitted_tree(pyplot):
    # Testing if not fitted tree throws the correct error
    clf = DecisionTreeRegressor()
        plot_tree(clf)

def test_buchheim(tree, n_nodes):
    def walk_tree(draw_tree):
        res = [(draw_tree.x, draw_tree.y)]
        for child in draw_tree.children:
            # parents higher than children:
            assert child.y == draw_tree.y + 1
            res.extend(walk_tree(child))
        if len(draw_tree.children):
            # these trees are always binary
            # parents are centered above children
            assert (
                draw_tree.x == (draw_tree.children[0].x + draw_tree.children[1].x) / 2
            )
        return res

    layout = buchheim(tree)
    coordinates = walk_tree(layout)
    assert len(coordinates) == n_nodes
    # test that x values are unique per depth / level
    # we could also do it quicker using defaultdicts..
    depth = 0
    while True:
        x_at_this_depth = [node[0] for node in coordinates if node[1] == depth]
        if not x_at_this_depth:
            # reached all leafs
            break
        assert len(np.unique(x_at_this_depth)) == len(x_at_this_depth)
        depth += 1

            assert (
            )
        return res

def _get_weights(dist, weights):
    """Get the weights from an array of distances and a parameter ``weights``.

    Assume weights have already been validated.

    dist : ndarray
        The input distances.

    weights : {'uniform', 'distance'}, callable or None
        The kind of weighting used.

    Returns
    -------
    weights_arr : array of the same shape as ``dist``
        If ``weights == 'uniform'``, then returns None.
    """
    if weights in (None, "uniform"):

    if weights == "distance":
        # if user attempts to classify a point that was zero distance from one
        # or more training points, those training points are weighted as 1.0
        # and the other points as 0.0
        if dist.dtype is np.dtype(object):
            for point_dist_i, point_dist in enumerate(dist):
                # check if point_dist is iterable
                # (ex: RadiusNeighborClassifier.predict may set an element of
                # dist to 1e-6 to represent an 'outlier')
                if hasattr(point_dist, "__contains__") and 0.0 in point_dist:
                    dist[point_dist_i] = point_dist == 0.0
                else:
                    dist[point_dist_i] = 1.0 / point_dist
        else:
            with np.errstate(divide="ignore"):
                dist = 1.0 / dist
            inf_mask = np.isinf(dist)
            inf_row = np.any(inf_mask, axis=1)
            dist[inf_row] = inf_mask[inf_row]
        return dist

    if callable(weights):
        return weights(dist)

def _is_sorted_by_data(graph):
    """Return whether the graph's non-zero entries are sorted by data.

    The non-zero entries are stored in graph.data and graph.indices.
    For each row (or sample), the non-zero entries can be either:
        - sorted by indices, as after graph.sort_indices();
        - sorted by data, as after _check_precomputed(graph);
        - not sorted.

    graph : sparse matrix of shape (n_samples, n_samples)
        Neighbors graph as given by `kneighbors_graph` or
        `radius_neighbors_graph`. Matrix should be of format CSR format.

    Returns
    -------
    res : bool
        Whether input graph is sorted by data.
    """
    assert graph.format == "csr"
    out_of_order = graph.data[:-1] > graph.data[1:]
    line_change = np.unique(graph.indptr[1:-1] - 1)
    line_change = line_change[line_change < out_of_order.shape[0]]
    return out_of_order.sum() == out_of_order[line_change].sum()

def _check_precomputed(X):
    """Check precomputed distance matrix.

    If the precomputed distance matrix is sparse, it checks that the non-zero
    entries are sorted by distances. If not, the matrix is copied and sorted.

    X : {sparse matrix, array-like}, (n_samples, n_samples)
        Distance matrix to other samples. X may be a sparse matrix, in which
        case only non-zero elements may be considered neighbors.

    Returns
    -------
    """
    if not issparse(X):
        X = check_array(X, ensure_non_negative=True, input_name="X")
        return X
    else:
        graph = X

    if graph.format not in ("csr", "csc", "coo", "lil"):
            "Sparse matrix in {!r} format is not supported due to "
            "its handling of explicit zeros".format(graph.format)
        )
    copied = graph.format != "csr"
    graph = check_array(
        graph,
        accept_sparse="csr",
        ensure_non_negative=True,
        input_name="precomputed distance matrix",
    )
    graph = sort_graph_by_row_values(graph, copy=not copied, warn_when_not_sorted=True)

    return graph

def sort_graph_by_row_values(graph, copy=False, warn_when_not_sorted=True):
    """Sort a sparse graph such that each row is stored with increasing values.

        Distance matrix to other samples, where only non-zero elements are
        considered neighbors. Matrix is converted to CSR format if not already.

        If True, the graph is copied before sorting. If False, the sorting is
        performed inplace. If the graph is not of CSR format, `copy` must be
        True to allow the conversion to CSR format, otherwise an error is
        raised.

    warn_when_not_sorted : bool, default=True
        If True, a :class:`~sklearn.exceptions.EfficiencyWarning` is raised
        when the input graph is not sorted by row values.

    Returns
    -------
        considered neighbors. Matrix is in CSR format.

    Examples
    --------
    >>> from scipy.sparse import csr_matrix
    >>> from sklearn.neighbors import sort_graph_by_row_values
    >>> X = csr_matrix(
    ...     [[0., 3., 1.],
    ...      [3., 0., 2.],
    ...      [1., 2., 0.]])
    >>> X.data
    array([3., 1., 3., 2., 1., 2.])
    >>> X_ = sort_graph_by_row_values(X)
    >>> X_.data
    array([1., 3., 2., 3., 1., 2.])
    """
    if graph.format == "csr" and _is_sorted_by_data(graph):
        return graph

    if warn_when_not_sorted:
            (
                "Precomputed sparse input was not sorted by row values. Use the"
                " function sklearn.neighbors.sort_graph_by_row_values to sort the input"
                " by row values, with warn_when_not_sorted=False to remove this"
                " warning."
            ),
            EfficiencyWarning,
        )

            f"Sparse matrix in {graph.format!r} format is not supported due to "
            "its handling of explicit zeros"
        )
    elif graph.format != "csr":
        if not copy:
                "The input graph is not in CSR format. Use copy=True to allow "
                "the conversion to CSR format."
            )
        graph = graph.asformat("csr")
    elif copy:  # csr format with copy=True
        graph = graph.copy()

    row_nnz = np.diff(graph.indptr)
    if row_nnz.max() == row_nnz.min():
        # if each sample has the same number of provided neighbors
        n_samples = graph.shape[0]
        distances = graph.data.reshape(n_samples, -1)

        order = np.argsort(distances, kind="mergesort")
        order += np.arange(n_samples)[:, None] * row_nnz[0]
        order = order.ravel()
        graph.data = graph.data[order]
        graph.indices = graph.indices[order]

    else:
        for start, stop in zip(graph.indptr, graph.indptr[1:]):
            order = np.argsort(graph.data[start:stop], kind="mergesort")
            graph.data[start:stop] = graph.data[start:stop][order]
            graph.indices[start:stop] = graph.indices[start:stop][order]

def _kneighbors_from_graph(graph, n_neighbors, return_distance):
    """Decompose a nearest neighbors sparse graph into distances and indices.

    n_neighbors : int
        Number of neighbors required for each sample.

    return_distance : bool
        Whether or not to return the distances.

    Returns
    -------
    neigh_dist : ndarray of shape (n_samples, n_neighbors)
        Distances to nearest neighbors. Only present if `return_distance=True`.

    neigh_ind : ndarray of shape (n_samples, n_neighbors)
        Indices of nearest neighbors.
    """
    n_samples = graph.shape[0]

    # number of neighbors by samples
    row_nnz_min = row_nnz.min()
    if n_neighbors is not None and row_nnz_min < n_neighbors:
            "%d neighbors per samples are required, but some samples have only"
            " %d neighbors in precomputed graph matrix. Decrease number of "
            "neighbors used or recompute the graph with more neighbors."
            % (n_neighbors, row_nnz_min)
        )

    def extract(a):
        if row_nnz.max() == row_nnz_min:
            return a.reshape(n_samples, -1)[:, :n_neighbors]
        else:
            idx = np.tile(np.arange(n_neighbors), (n_samples, 1))
            idx += graph.indptr[:-1, None]
            return a.take(idx, mode="clip").reshape(n_samples, n_neighbors)

    if return_distance:
        return extract(graph.data), extract(graph.indices)
    else:
        return extract(graph.indices)

def _radius_neighbors_from_graph(graph, radius, return_distance):

    radius : float
        Radius of neighborhoods which should be strictly positive.

    Returns
    -------
    neigh_dist : ndarray of shape (n_samples,) of arrays

    neigh_ind : ndarray of shape (n_samples,) of arrays
    """

    no_filter_needed = bool(graph.data.max() <= radius)

    if no_filter_needed:
        data, indices, indptr = graph.data, graph.indices, graph.indptr
    else:
        mask = graph.data <= radius
        if return_distance:
            data = np.compress(mask, graph.data)
        indices = np.compress(mask, graph.indices)
        indptr = np.concatenate(([0], np.cumsum(mask)))[graph.indptr]

    indices = indices.astype(np.intp, copy=no_filter_needed)

        neigh_dist = _to_object_array(np.split(data, indptr[1:-1]))
    neigh_ind = _to_object_array(np.split(indices, indptr[1:-1]))

        return neigh_dist, neigh_ind
    else:
        return neigh_ind

class NeighborsBase(MultiOutputMixin, BaseEstimator, metaclass=ABCMeta):
    """Base class for nearest neighbors estimators."""

        "n_neighbors": [Interval(Integral, 1, None, closed="left"), None],
        "radius": [Interval(Real, 0, None, closed="both"), None],
        "algorithm": [StrOptions({"auto", "ball_tree", "kd_tree", "brute"})],
        "leaf_size": [Interval(Integral, 1, None, closed="left")],
        "p": [Interval(Real, 0, None, closed="right"), None],
        "metric": [StrOptions(set(itertools.chain(*VALID_METRICS.values()))), callable],
        "metric_params": [dict, None],
    }

        self,
        n_neighbors=None,
        radius=None,
        algorithm="auto",
        leaf_size=30,
        metric="minkowski",
        p=2,
        metric_params=None,
    ):
        self.radius = radius
        self.leaf_size = leaf_size
        self.metric_params = metric_params
        self.p = p

    def _check_algorithm_metric(self):
        if self.algorithm == "auto":
            if self.metric == "precomputed":
                alg_check = "brute"
            elif (
                callable(self.metric)
                or self.metric in VALID_METRICS["ball_tree"]
                or isinstance(self.metric, DistanceMetric)
            ):
                alg_check = "ball_tree"
            else:
        else:
            alg_check = self.algorithm

        if callable(self.metric):
            if self.algorithm == "kd_tree":
                # callable metric is only valid for brute force and ball_tree
                    "kd_tree does not support callable metric '%s'"
                    "Function call overhead will result"
                    "in very poor performance." % self.metric
                )
        elif self.metric not in VALID_METRICS[alg_check] and not isinstance(
            self.metric, DistanceMetric
        ):
                "Metric '%s' not valid. Use "
                "sorted(sklearn.neighbors.VALID_METRICS['%s']) "
                "to get valid options. "
                "Metric can also be a callable function." % (self.metric, alg_check)
            )

        if self.metric_params is not None and "p" in self.metric_params:
            if self.p is not None:
                    (
                        "Parameter p is found in metric_params. "
                        "The corresponding parameter from __init__ "
                        "is ignored."
                    ),
                    SyntaxWarning,
                    stacklevel=3,
                )

    def _fit(self, X, y=None):
        ensure_all_finite = "allow-nan" if get_tags(self).input_tags.allow_nan else True
        if self.__sklearn_tags__().target_tags.required:
            if not isinstance(X, (KDTree, BallTree, NeighborsBase)):
                X, y = validate_data(
                    self,
                    X,
                    y,
                    accept_sparse="csr",
                    multi_output=True,
                    order="C",
                    ensure_all_finite=ensure_all_finite,
                )

                # Classification targets require a specific format
                if y.ndim == 1 or (y.ndim == 2 and y.shape[1] == 1):
                    if y.ndim != 1:
                        warnings.warn(
                            (
                                "A column-vector y was passed when a "
                                "1d array was expected. Please change "
                                "the shape of y to (n_samples,), for "
                                "example using ravel()."
                            ),
                            DataConversionWarning,
                            stacklevel=2,
                        )

                    self.outputs_2d_ = False
                    y = y.reshape((-1, 1))
                else:
                    self.outputs_2d_ = True

                check_classification_targets(y)
                self.classes_ = []
                # Using `dtype=np.intp` is necessary since `np.bincount`
                # (called in _classification.py) fails when dealing
                # with a float64 array on 32bit systems.
                self._y = np.empty(y.shape, dtype=np.intp)
                for k in range(self._y.shape[1]):
                    classes, self._y[:, k] = np.unique(y[:, k], return_inverse=True)
                    self.classes_.append(classes)

                if not self.outputs_2d_:
                    self.classes_ = self.classes_[0]
                    self._y = self._y.ravel()
            else:
                self._y = y

        else:
                X = validate_data(
                    self,
                    X,
                )

        self._check_algorithm_metric()
        if self.metric_params is None:
            self.effective_metric_params_ = {}
        else:
            self.effective_metric_params_ = self.metric_params.copy()

        effective_p = self.effective_metric_params_.get("p", self.p)
        if self.metric == "minkowski":
            self.effective_metric_params_["p"] = effective_p

        self.effective_metric_ = self.metric
        # For minkowski distance, use more efficient methods where available
            p = self.effective_metric_params_.pop("p", 2)
            w = self.effective_metric_params_.pop("w", None)

            if p == 1 and w is None:
                self.effective_metric_ = "manhattan"
            elif p == 2 and w is None:
                self.effective_metric_ = "euclidean"
            elif p == np.inf and w is None:
                self.effective_metric_ = "chebyshev"
            else:
                # Use the generic minkowski metric, possibly weighted.
                self.effective_metric_params_["p"] = p
                self.effective_metric_params_["w"] = w

        if isinstance(X, NeighborsBase):
            self._fit_X = X._fit_X
            self._tree = X._tree
            self._fit_method = X._fit_method
            self.n_samples_fit_ = X.n_samples_fit_

        elif isinstance(X, BallTree):
            self._fit_X = X.data
            self._tree = X
            self._fit_method = "ball_tree"
            self.n_samples_fit_ = X.data.shape[0]

        elif isinstance(X, KDTree):
            self._fit_method = "kd_tree"

        if self.metric == "precomputed":
            X = _check_precomputed(X)
            # Precomputed matrix X must be squared
            if X.shape[0] != X.shape[1]:
                    "Precomputed matrix must be square."
                    " Input is a {}x{} matrix.".format(X.shape[0], X.shape[1])
                )
            self.n_features_in_ = X.shape[1]

            raise ValueError("n_samples must be greater than 0")

            if self.algorithm not in ("auto", "brute"):
                warnings.warn("cannot use tree with sparse input: using brute force")

            if (
                self.effective_metric_ not in VALID_METRICS_SPARSE["brute"]
                and not callable(self.effective_metric_)
                and not isinstance(self.effective_metric_, DistanceMetric)
            ):
                    "Metric '%s' not valid for sparse input. "
                    "Use sorted(sklearn.neighbors."
                    "VALID_METRICS_SPARSE['brute']) "
                    "to get valid options. "
                    "Metric can also be a callable function." % (self.effective_metric_)
                )
            self._fit_X = X.copy()
            self._tree = None
            self._fit_method = "brute"
            self.n_samples_fit_ = X.shape[0]

        self._fit_method = self.algorithm
        self._fit_X = X
        self.n_samples_fit_ = X.shape[0]

        if self._fit_method == "auto":
            # A tree approach is better for small number of neighbors or small
            # number of features, with KDTree generally faster when available
            if (
                self.metric == "precomputed"
                or self._fit_X.shape[1] > 15
                or (
                    self.n_neighbors is not None
                    and self.n_neighbors >= self._fit_X.shape[0] // 2
                )
            ):
                self._fit_method = "brute"
            else:
                if (
                    self.effective_metric_ == "minkowski"
                    and self.effective_metric_params_["p"] < 1
                ):
                    self._fit_method = "brute"
                elif (
                    and self.effective_metric_params_.get("w") is not None
                ):
                    # 'minkowski' with weights is not supported by KDTree but is
                    # supported byBallTree.
                    self._fit_method = "ball_tree"
                elif self.effective_metric_ in VALID_METRICS["kd_tree"]:
                    self._fit_method = "kd_tree"
                elif (
                    callable(self.effective_metric_)
                    or self.effective_metric_ in VALID_METRICS["ball_tree"]
                ):
                else:

        if (
            self.effective_metric_ == "minkowski"
            and self.effective_metric_params_["p"] < 1
        ):
            # For 0 < p < 1 Minkowski distances aren't valid distance
            # metric as they do not satisfy triangular inequality:
            # they are semi-metrics.
            # algorithm="kd_tree" and algorithm="ball_tree" can't be used because
            # KDTree and BallTree require a proper distance metric to work properly.
            # However, the brute-force algorithm supports semi-metrics.
            if self._fit_method == "brute":
                    "Mind that for 0 < p < 1, Minkowski metrics are not distance"
                    " metrics. Continuing the execution with `algorithm='brute'`."
                )
            else:  # self._fit_method in ("kd_tree", "ball_tree")
                    f'algorithm="{self._fit_method}" does not support 0 < p < 1 for '
                    "the Minkowski metric. To resolve this problem either "
                    'set p >= 1 or algorithm="brute".'
                )

        if self._fit_method == "ball_tree":
            self._tree = BallTree(
                X,
                self.leaf_size,
                metric=self.effective_metric_,
                **self.effective_metric_params_,
            )
        elif self._fit_method == "kd_tree":
            if (
                self.effective_metric_ == "minkowski"
                and self.effective_metric_params_.get("w") is not None
            ):
                    "algorithm='kd_tree' is not valid for "
                    "metric='minkowski' with a weight parameter 'w': "
                    "try algorithm='ball_tree' "
                    "or algorithm='brute' instead."
                )
            self._tree = KDTree(
                X,
            )
        elif self._fit_method == "brute":

        # For cross-validation routines to split data correctly
        tags.input_tags.pairwise = self.metric == "precomputed"
        # when input is precomputed metric values, all those values need to be positive
        tags.input_tags.positive_only = tags.input_tags.pairwise
        tags.input_tags.allow_nan = self.metric == "nan_euclidean"

class KNeighborsMixin:
    """Mixin for k-neighbors searches."""

    def _kneighbors_reduce_func(self, dist, start, n_neighbors, return_distance):
        """Reduce a chunk of distances to the nearest neighbors.

        Callback to :func:`sklearn.metrics.pairwise.pairwise_distances_chunked`

        dist : ndarray of shape (n_samples_chunk, n_samples)
            The distance matrix.

        start : int
            The index in X which the first row of dist corresponds to.

        n_neighbors : int
            Number of neighbors required for each sample.

        return_distance : bool
            Whether or not to return the distances.

        Returns
        -------
        dist : array of shape (n_samples_chunk, n_neighbors)
            Returned only if `return_distance=True`.

        neigh : array of shape (n_samples_chunk, n_neighbors)
            The neighbors indices.
        """
        sample_range = np.arange(dist.shape[0])[:, None]
        neigh_ind = np.argpartition(dist, n_neighbors - 1, axis=1)
        neigh_ind = neigh_ind[:, :n_neighbors]
        # argpartition doesn't guarantee sorted order, so we sort again
        neigh_ind = neigh_ind[sample_range, np.argsort(dist[sample_range, neigh_ind])]
            if self.effective_metric_ == "euclidean":
                result = np.sqrt(dist[sample_range, neigh_ind]), neigh_ind
            else:
                result = dist[sample_range, neigh_ind], neigh_ind
        else:
            result = neigh_ind

    def kneighbors(self, X=None, n_neighbors=None, return_distance=True):
        """Find the K-neighbors of a point.

        Returns indices of and distances to the neighbors of each point.

        X : {array-like, sparse matrix}, shape (n_queries, n_features), \
            or (n_queries, n_indexed) if metric == 'precomputed', default=None
            The query point or points.
            If not provided, neighbors of each indexed point are returned.
            In this case, the query point is not considered its own neighbor.

        n_neighbors : int, default=None
            Number of neighbors required for each sample. The default is the
            value passed to the constructor.

        return_distance : bool, default=True

        Returns
        -------
        neigh_dist : ndarray of shape (n_queries, n_neighbors)
            Array representing the lengths to points, only present if
            return_distance=True.

        neigh_ind : ndarray of shape (n_queries, n_neighbors)
            Indices of the nearest points in the population matrix.

        Examples
        --------
        In the following example, we construct a NearestNeighbors
        class from an array representing our data set and ask who's
        the closest point to [1,1,1]

        >>> samples = [[0., 0., 0.], [0., .5, 0.], [1., 1., .5]]
        >>> from sklearn.neighbors import NearestNeighbors
        >>> neigh = NearestNeighbors(n_neighbors=1)
        >>> neigh.fit(samples)
        NearestNeighbors(n_neighbors=1)
        >>> print(neigh.kneighbors([[1., 1., 1.]]))
        (array([[0.5]]), array([[2]]))

        As you can see, it returns [[0.5]], and [[2]], which means that the
        element is at distance 0.5 and is the third element of samples
        (indexes start at 0). You can also query for multiple points:

        >>> X = [[0., 1., 0.], [1., 0., 1.]]
        >>> neigh.kneighbors(X, return_distance=False)
        array([[1],
               [2]]...)
        """

        if n_neighbors is None:
            n_neighbors = self.n_neighbors
        elif n_neighbors <= 0:
            raise ValueError("Expected n_neighbors > 0. Got %d" % n_neighbors)
        elif not isinstance(n_neighbors, numbers.Integral):
                "n_neighbors does not take %s value, enter integer value"
                % type(n_neighbors)
            )

        query_is_train = X is None
        if query_is_train:
            X = self._fit_X
            # Include an extra neighbor to account for the sample itself being
            # returned, which is removed later
            n_neighbors += 1
        else:
                X = _check_precomputed(X)
            else:
                    self,
                    X,
                    reset=False,
                )

        n_samples_fit = self.n_samples_fit_
        if n_neighbors > n_samples_fit:
            if query_is_train:
                n_neighbors -= 1  # ok to modify inplace because an error is raised
                inequality_str = "n_neighbors < n_samples_fit"
            else:
                inequality_str = "n_neighbors <= n_samples_fit"
                f"Expected {inequality_str}, but "
                f"n_neighbors = {n_neighbors}, n_samples_fit = {n_samples_fit}, "
                f"n_samples = {X.shape[0]}"  # include n_samples for common tests
            )

        n_jobs = effective_n_jobs(self.n_jobs)
        chunked_results = None
        use_pairwise_distances_reductions = (
            self._fit_method == "brute"
            and ArgKmin.is_usable_for(
                X if X is not None else self._fit_X, self._fit_X, self.effective_metric_
            )
        )
        if use_pairwise_distances_reductions:
            results = ArgKmin.compute(
                X=X,
                Y=self._fit_X,
                k=n_neighbors,
                metric_kwargs=self.effective_metric_params_,
                strategy="auto",
            )

        elif (
            self._fit_method == "brute" and self.metric == "precomputed" and issparse(X)
        ):
            results = _kneighbors_from_graph(
                X, n_neighbors=n_neighbors, return_distance=return_distance
            )

            # Joblib-based backend, which is used when user-defined callable
            # are passed for metric.

            # This won't be used in the future once PairwiseDistancesReductions
            # support:
            #   - DistanceMetrics which work on supposedly binary data
            #   - CSR-dense and dense-CSR case if 'euclidean' in metric.
            reduce_func = partial(
                self._kneighbors_reduce_func,
                n_neighbors=n_neighbors,
            )

            # for efficiency, use squared euclidean distances
                kwds = {"squared": True}
            else:
                kwds = self.effective_metric_params_

            chunked_results = list(
                pairwise_distances_chunked(
                    X,
                    self._fit_X,
                    reduce_func=reduce_func,
                    metric=self.effective_metric_,
                    n_jobs=n_jobs,
                    **kwds,
                )
            )

        elif self._fit_method in ["ball_tree", "kd_tree"]:
                    "%s does not work with sparse matrices. Densify the data, "
                    "or set algorithm='brute'" % self._fit_method
                )
            chunked_results = Parallel(n_jobs, prefer="threads")(
                delayed(self._tree.query)(X[s], n_neighbors, return_distance)
                for s in gen_even_slices(X.shape[0], n_jobs)
            )
        else:
            raise ValueError("internal: _fit_method not recognized")

        if chunked_results is not None:
            if return_distance:
                neigh_dist, neigh_ind = zip(*chunked_results)
                results = np.vstack(neigh_dist), np.vstack(neigh_ind)
            else:
                results = np.vstack(chunked_results)

        if not query_is_train:
            return results
        else:
            # If the query data is the same as the indexed data, we would like
            # to ignore the first nearest neighbor of every sample, i.e
            # the sample itself.
                neigh_dist, neigh_ind = results
            else:
                neigh_ind = results

            n_queries, _ = X.shape
            sample_range = np.arange(n_queries)[:, None]
            sample_mask = neigh_ind != sample_range

            # Corner case: When the number of duplicates are more
            # than the number of neighbors, the first NN will not
            # be the sample, but a duplicate.
            # In that case mask the first duplicate.
            dup_gr_nbrs = np.all(sample_mask, axis=1)
            sample_mask[:, 0][dup_gr_nbrs] = False
            neigh_ind = np.reshape(neigh_ind[sample_mask], (n_queries, n_neighbors - 1))

                neigh_dist = np.reshape(
                    neigh_dist[sample_mask], (n_queries, n_neighbors - 1)
                )
                return neigh_dist, neigh_ind
            return neigh_ind

    def kneighbors_graph(self, X=None, n_neighbors=None, mode="connectivity"):
        """Compute the (weighted) graph of k-Neighbors for points in X.

        X : {array-like, sparse matrix} of shape (n_queries, n_features), \
            For ``metric='precomputed'`` the shape should be
            (n_queries, n_indexed). Otherwise the shape should be
            (n_queries, n_features).

            Number of neighbors for each sample. The default is the value
            passed to the constructor.

        mode : {'connectivity', 'distance'}, default='connectivity'
            Type of returned matrix: 'connectivity' will return the
            connectivity matrix with ones and zeros, in 'distance' the
            edges are distances between points, type of distance
            depends on the selected metric parameter in
            NearestNeighbors class.

        Returns
        -------
        A : sparse-matrix of shape (n_queries, n_samples_fit)
            `n_samples_fit` is the number of samples in the fitted data.
            `A[i, j]` gives the weight of the edge connecting `i` to `j`.
            The matrix is of CSR format.

        See Also
        --------
        NearestNeighbors.radius_neighbors_graph : Compute the (weighted) graph
            of Neighbors for points in X.

        Examples
        --------
        >>> X = [[0], [3], [1]]
        >>> neigh = NearestNeighbors(n_neighbors=2)
        >>> neigh.fit(X)
        NearestNeighbors(n_neighbors=2)
        >>> A = neigh.kneighbors_graph(X)
        >>> A.toarray()
        array([[1., 0., 1.],
               [0., 1., 1.],
               [1., 0., 1.]])
        """

        # check the input only in self.kneighbors

        # construct CSR matrix representation of the k-NN graph
        if mode == "connectivity":
            A_ind = self.kneighbors(X, n_neighbors, return_distance=False)
            n_queries = A_ind.shape[0]
            A_data = np.ones(n_queries * n_neighbors)

        elif mode == "distance":
            A_data, A_ind = self.kneighbors(X, n_neighbors, return_distance=True)
            A_data = np.ravel(A_data)

        else:
                'Unsupported mode, must be one of "connectivity", '
                f'or "distance" but got "{mode}" instead'
            )

        n_queries = A_ind.shape[0]
        n_nonzero = n_queries * n_neighbors
        A_indptr = np.arange(0, n_nonzero + 1, n_neighbors)

        kneighbors_graph = csr_matrix(
            (A_data, A_ind.ravel(), A_indptr), shape=(n_queries, n_samples_fit)
        )

        return kneighbors_graph

class RadiusNeighborsMixin:
    """Mixin for radius-based neighbors searches."""

    def _radius_neighbors_reduce_func(self, dist, start, radius, return_distance):

        start : int

            The radius considered when making the nearest neighbors search.

        Returns
        -------
        dist : list of ndarray of shape (n_samples_chunk,)

        neigh : list of ndarray of shape (n_samples_chunk,)
        """
        neigh_ind = [np.where(d <= radius)[0] for d in dist]

                dist = [np.sqrt(d[neigh_ind[i]]) for i, d in enumerate(dist)]
            else:
                dist = [d[neigh_ind[i]] for i, d in enumerate(dist)]
            results = dist, neigh_ind
        else:
            results = neigh_ind

    def radius_neighbors(
        self, X=None, radius=None, return_distance=True, sort_results=False
    ):
        """Find the neighbors within a given radius of a point or points.

        Return the indices and distances of each point from the dataset
        lying in a ball with size ``radius`` around the points of the query
        array. Points lying on the boundary are included in the results.

        The result points are *not* necessarily sorted by distance to their
        query point.

        X : {array-like, sparse matrix} of (n_samples, n_features), default=None

        radius : float, default=None
            Limiting distance of neighbors to return. The default is the value

        sort_results : bool, default=False
            If True, the distances and indices will be sorted by increasing
            distances before being returned. If False, the results may not
            be sorted. If `return_distance=False`, setting `sort_results=True`
            will result in an error.

            .. versionadded:: 0.22

        Returns
        -------
        neigh_dist : ndarray of shape (n_samples,) of arrays
            Array representing the distances to each point, only present if
            `return_distance=True`. The distance values are computed according
            to the ``metric`` constructor parameter.

        neigh_ind : ndarray of shape (n_samples,) of arrays
            An array of arrays of indices of the approximate nearest points
            from the population matrix that lie within a ball of size
            ``radius`` around the query points.

        Notes
        -----
        Because the number of neighbors of each point is not necessarily
        equal, the results for multiple query points cannot be fit in a
        standard data array.
        For efficiency, `radius_neighbors` returns arrays of objects, where
        each object is a 1D array of indices or distances.

        Examples
        --------
        In the following example, we construct a NeighborsClassifier
        the closest point to [1, 1, 1]:

        >>> neigh = NearestNeighbors(radius=1.6)
        NearestNeighbors(radius=1.6)
        >>> rng = neigh.radius_neighbors([[1., 1., 1.]])
        >>> print(np.asarray(rng[0][0]))
        [1.5 0.5]
        >>> print(np.asarray(rng[1][0]))
        [1 2]

        The first array returned contains the distances to all points which
        are closer than 1.6, while the second array returned contains their
        indices.  In general, multiple points can be queried at the same time.
        """

        if sort_results and not return_distance:
            raise ValueError("return_distance must be True if sort_results is True.")

        else:
            else:
                    self,
                    X,
                )

        if radius is None:
            radius = self.radius

            and RadiusNeighbors.is_usable_for(
            )
        )

            results = RadiusNeighbors.compute(
                X=X,
            )

        elif (
        ):
            results = _radius_neighbors_from_graph(
                X, radius=radius, return_distance=return_distance
            )

            # support:

                radius *= radius
            else:

                self._radius_neighbors_reduce_func,
            )

            chunked_results = pairwise_distances_chunked(
                X,
                self._fit_X,
                reduce_func=reduce_func,
                **kwds,
            )
                neigh_dist_chunks, neigh_ind_chunks = zip(*chunked_results)
                neigh_dist_list = list(itertools.chain.from_iterable(neigh_dist_chunks))
                neigh_ind_list = list(itertools.chain.from_iterable(neigh_ind_chunks))
                neigh_dist = _to_object_array(neigh_dist_list)
                neigh_ind = _to_object_array(neigh_ind_list)
                results = neigh_dist, neigh_ind
            else:
                neigh_ind_list = list(itertools.chain.from_iterable(chunked_results))
                results = _to_object_array(neigh_ind_list)

            if sort_results:
                for ii in range(len(neigh_dist)):
                    order = np.argsort(neigh_dist[ii], kind="mergesort")
                    neigh_ind[ii] = neigh_ind[ii][order]
                    neigh_dist[ii] = neigh_dist[ii][order]

                )

            n_jobs = effective_n_jobs(self.n_jobs)
            delayed_query = delayed(self._tree.query_radius)
                delayed_query(X[s], radius, return_distance, sort_results=sort_results)
            )
                neigh_ind, neigh_dist = tuple(zip(*chunked_results))
                results = np.hstack(neigh_dist), np.hstack(neigh_ind)
            else:
                results = np.hstack(chunked_results)
        else:

        else:
            else:

            for ind, ind_neighbor in enumerate(neigh_ind):
                mask = ind_neighbor != ind

                neigh_ind[ind] = ind_neighbor[mask]
                if return_distance:
                    neigh_dist[ind] = neigh_dist[ind][mask]

    def radius_neighbors_graph(
        self, X=None, radius=None, mode="connectivity", sort_results=False
    ):
        """Compute the (weighted) graph of Neighbors for points in X.

        Neighborhoods are restricted the points at a distance lower than
        radius.

        X : {array-like, sparse matrix} of shape (n_samples, n_features), default=None

            Radius of neighborhoods. The default is the value passed to the
            constructor.

            If True, in each row of the result, the non-zero entries will be
            sorted by increasing distances. If False, the non-zero entries may
            not be sorted. Only used with mode='distance'.

        Returns
        -------

        See Also
        --------
        kneighbors_graph : Compute the (weighted) graph of k-Neighbors for
            points in X.

        Examples
        --------
        >>> neigh = NearestNeighbors(radius=1.5)
        NearestNeighbors(radius=1.5)
        >>> A = neigh.radius_neighbors_graph(X)
               [0., 1., 0.],
        """

        # check the input only in self.radius_neighbors

        # construct CSR matrix representation of the NN graph
            A_ind = self.radius_neighbors(X, radius, return_distance=False)
            A_data = None
            dist, A_ind = self.radius_neighbors(
                X, radius, return_distance=True, sort_results=sort_results
            )
            A_data = np.concatenate(list(dist))
        else:
            )

        n_neighbors = np.array([len(a) for a in A_ind])
        A_ind = np.concatenate(list(A_ind))
        if A_data is None:
            A_data = np.ones(len(A_ind))
        A_indptr = np.concatenate((np.zeros(1, dtype=int), np.cumsum(n_neighbors)))

        return csr_matrix((A_data, A_ind, A_indptr), shape=(n_queries, n_samples_fit))

        else:

        self,
        p=2,
    ):
        self.p = p

            elif (
            ):
            else:
        else:

                )
        ):
            )

                    (
                    ),
                )

                    self,
                    X,
                    y,
                )

                            (
                            ),
                        )

                else:

            else:
                self._y = y

        else:
                    self,
                    X,
                )

        else:

            else:

                )

            if (
            ):
                )

            if (
                or (
                )
            ):
            else:
                if (
                ):
                elif (
                ):
                elif (
                ):
                else:

        if (
        ):
                )
                )

                X,
            )
            if (
            ):
                )
                X,
            )

        start : int

        Returns
        -------

        """
            else:
        else:

        Returns
        -------

        Examples
        --------

               [2]]...)
        """

            )

        else:
            else:
                    self,
                    X,
                )

            else:
            )

            )
        )
                X=X,
            )

        elif (
        ):
            )

            # support:
            )

            else:

                    X,
                    **kwds,
                )
            )

                )
            )
        else:

            else:

        else:
            else:

                )

        Returns
        -------

        See Also
        --------

        Examples
        --------
        """

        else:
            )

        )

        start : int

        Returns
        -------

        """

            else:
        else:

    ):

        Returns
        -------

        Notes
        -----

        Examples
        --------

        [1.5 0.5]
        [1 2]

        """

        else:
            else:
                    self,
                    X,
                )

            )
        )

                X=X,
            )

        elif (
        ):
            )

            # support:

            else:

            )

                X,
                **kwds,
            )
            else:

                )

            )
            else:
        else:

        else:
            else:

    ):

        radius.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        """

            )
        else:
            )

def _adjusted_metric(metric, metric_kwargs, p=None):
    metric_kwargs = metric_kwargs or {}
        metric_kwargs["p"] = p
        if p == 2:
            metric = "euclidean"
    return metric, metric_kwargs

class KNeighborsClassifier(KNeighborsMixin, ClassifierMixin, NeighborsBase):
    """Classifier implementing the k-nearest neighbors vote.

    Read more in the :ref:`User Guide <classification>`.

    n_neighbors : int, default=5
        Number of neighbors to use by default for :meth:`kneighbors` queries.

    weights : {'uniform', 'distance'}, callable or None, default='uniform'
        Weight function used in prediction.  Possible values:

        - 'uniform' : uniform weights.  All points in each neighborhood
          are weighted equally.
        - 'distance' : weight points by the inverse of their distance.
          in this case, closer neighbors of a query point will have a
          greater influence than neighbors which are further away.
        - [callable] : a user-defined function which accepts an
          array of distances, and returns an array of the same shape
          containing the weights.

        Refer to the example entitled
        :ref:`sphx_glr_auto_examples_neighbors_plot_classification.py`
        showing the impact of the `weights` parameter on the decision
        boundary.

    algorithm : {'auto', 'ball_tree', 'kd_tree', 'brute'}, default='auto'
        Algorithm used to compute the nearest neighbors:

        - 'ball_tree' will use :class:`BallTree`
        - 'kd_tree' will use :class:`KDTree`
        - 'brute' will use a brute-force search.
        - 'auto' will attempt to decide the most appropriate algorithm
          based on the values passed to :meth:`fit` method.

        Note: fitting on sparse input will override the setting of
        this parameter, using brute force.

    leaf_size : int, default=30
        Leaf size passed to BallTree or KDTree.  This can affect the
        speed of the construction and query, as well as the memory
        required to store the tree.  The optimal value depends on the
        nature of the problem.

    p : float, default=2
        Power parameter for the Minkowski metric. When p = 1, this is equivalent
        to using manhattan_distance (l1), and euclidean_distance (l2) for p = 2.
        For arbitrary p, minkowski_distance (l_p) is used. This parameter is expected
        to be positive.

    metric : str or callable, default='minkowski'
        Metric to use for distance computation. Default is "minkowski", which
        results in the standard Euclidean distance when p = 2. See the
        documentation of `scipy.spatial.distance
        <https://docs.scipy.org/doc/scipy/reference/spatial.distance.html>`_ and
        the metrics listed in
        :class:`~sklearn.metrics.pairwise.distance_metrics` for valid metric
        values.

        If metric is "precomputed", X is assumed to be a distance matrix and
        must be square during fit. X may be a :term:`sparse graph`, in which
        case only "nonzero" elements may be considered neighbors.

        If metric is a callable function, it takes two arrays representing 1D
        vectors as inputs and must return one value indicating the distance
        between those vectors. This works for Scipy's metrics, but is less

    metric_params : dict, default=None
        Additional keyword arguments for the metric function.

        The number of parallel jobs to run for neighbors search.
        Doesn't affect :meth:`fit` method.

    classes_ : array of shape (n_classes,)

    effective_metric_ : str or callble
        The distance metric used. It will be same as the `metric` parameter
        or a synonym of it, e.g. 'euclidean' if the `metric` parameter set to
        'minkowski' and `p` parameter set to 2.

    effective_metric_params_ : dict
        Additional keyword arguments for the metric function. For most metrics
        will be same with `metric_params` parameter, but may also contain the
        `p` parameter value if the `effective_metric_` attribute is set to
        'minkowski'.

    n_samples_fit_ : int
        Number of samples in the fitted data.

    outputs_2d_ : bool
        False when `y`'s shape is (n_samples, ) or (n_samples, 1) during fit
        otherwise True.

    See Also
    --------
    RadiusNeighborsClassifier: Classifier based on neighbors within a fixed radius.
    KNeighborsRegressor: Regression based on k-nearest neighbors.
    RadiusNeighborsRegressor: Regression based on neighbors within a fixed radius.
    NearestNeighbors: Unsupervised learner for implementing neighbor searches.

    Notes
    -----
    See :ref:`Nearest Neighbors <neighbors>` in the online documentation
    for a discussion of the choice of ``algorithm`` and ``leaf_size``.

    .. warning::

       Regarding the Nearest Neighbors algorithms, if it is found that two
       neighbors, neighbor `k+1` and `k`, have identical distances
       but different labels, the results will depend on the ordering of the
       training data.

    https://en.wikipedia.org/wiki/K-nearest_neighbor_algorithm

    Examples
    --------
    >>> X = [[0], [1], [2], [3]]
    >>> from sklearn.neighbors import KNeighborsClassifier
    >>> neigh = KNeighborsClassifier(n_neighbors=3)
    >>> neigh.fit(X, y)
    KNeighborsClassifier(...)
    >>> print(neigh.predict([[1.1]]))
    [0]
    >>> print(neigh.predict_proba([[0.9]]))
    [[0.666... 0.333...]]
    """

    _parameter_constraints: dict = {**NeighborsBase._parameter_constraints}
    _parameter_constraints.pop("radius")
        {"weights": [StrOptions({"uniform", "distance"}), callable, None]}
    )

        self,
        n_neighbors=5,
        *,
        weights="uniform",
        p=2,
    ):
            algorithm=algorithm,
            leaf_size=leaf_size,
            p=p,
            metric_params=metric_params,
        )

        # KNeighborsClassifier.metric is not validated yet
    )
        """Fit the k-nearest neighbors classifier from the training dataset.

        X : {array-like, sparse matrix} of shape (n_samples, n_features) or \
                (n_samples, n_samples) if metric='precomputed'

        y : {array-like, sparse matrix} of shape (n_samples,) or \
                (n_samples, n_outputs)

        Returns
        -------
        self : KNeighborsClassifier
            The fitted k-nearest neighbors classifier.
        """
        return self._fit(X, y)

        """Predict the class labels for the provided data.

                or (n_queries, n_indexed) if metric == 'precomputed', or None
            Test samples. If `None`, predictions for all indexed points are
            returned; in this case, points are not considered their own
            neighbors.

        Returns
        -------
        y : ndarray of shape (n_queries,) or (n_queries, n_outputs)
            Class labels for each data sample.
        """
        check_is_fitted(self, "_fit_method")
        if self.weights == "uniform":
            if self._fit_method == "brute" and ArgKminClassMode.is_usable_for(
                X, self._fit_X, self.metric
            ):
                probabilities = self.predict_proba(X)
                if self.outputs_2d_:
                    return np.stack(
                        [
                            self.classes_[idx][np.argmax(probas, axis=1)]
                            for idx, probas in enumerate(probabilities)
                        ],
                        axis=1,
                    )
                return self.classes_[np.argmax(probabilities, axis=1)]
            # In that case, we do not need the distances to perform
            # the weighting so we do not compute them.
            neigh_ind = self.kneighbors(X, return_distance=False)
            neigh_dist = None
        else:
            neigh_dist, neigh_ind = self.kneighbors(X)

        _y = self._y
        if not self.outputs_2d_:
            _y = self._y.reshape((-1, 1))
            classes_ = [self.classes_]

        n_outputs = len(classes_)
        n_queries = _num_samples(self._fit_X if X is None else X)
        weights = _get_weights(neigh_dist, self.weights)
        if weights is not None and _all_with_any_reduction_axis_1(weights, value=0):
                "All neighbors of some sample is getting zero weights. "
                "Please modify 'weights' to avoid this case if you are "
                "using a user-defined function."
            )

        y_pred = np.empty((n_queries, n_outputs), dtype=classes_[0].dtype)
        for k, classes_k in enumerate(classes_):
            if weights is None:
                mode, _ = _mode(_y[neigh_ind, k], axis=1)
            else:
                mode, _ = weighted_mode(_y[neigh_ind, k], weights, axis=1)

            mode = np.asarray(mode.ravel(), dtype=np.intp)
            y_pred[:, k] = classes_k.take(mode)

        """Return probability estimates for the test data X.

        Returns
        -------
        p : ndarray of shape (n_queries, n_classes), or a list of n_outputs \
                of such arrays if n_outputs > 1.
            The class probabilities of the input samples. Classes are ordered
            by lexicographic order.
        """
            # TODO: systematize this mapping of metric for
            # PairwiseDistancesReductions.
            metric, metric_kwargs = _adjusted_metric(
                metric=self.metric, metric_kwargs=self.metric_params, p=self.p
            )
            if (
                self._fit_method == "brute"
                and ArgKminClassMode.is_usable_for(X, self._fit_X, metric)
                # TODO: Implement efficient multi-output solution
                and not self.outputs_2d_
            ):
                if self.metric == "precomputed":
                    X = _check_precomputed(X)
                else:
                    X = validate_data(
                        self, X, accept_sparse="csr", reset=False, order="C"
                    )

                probabilities = ArgKminClassMode.compute(
                    X,
                    k=self.n_neighbors,
                    weights=self.weights,
                    Y_labels=self._y,
                    unique_Y_labels=self.classes_,
                    metric=metric,
                    metric_kwargs=metric_kwargs,
                    # `strategy="parallel_on_X"` has in practice be shown
                    # to be more efficient than `strategy="parallel_on_Y``
                    # on many combination of datasets.
                    # Hence, we choose to enforce it here.
                    # For more information, see:
                    # https://github.com/scikit-learn/scikit-learn/pull/24076#issuecomment-1445258342
                    # TODO: adapt the heuristic for `strategy="auto"` for
                    # `ArgKminClassMode` and use `strategy="auto"`.
                    strategy="parallel_on_X",
                )
                return probabilities

        else:

        if weights is None:
            weights = np.ones_like(neigh_ind)
        elif _all_with_any_reduction_axis_1(weights, value=0):
            )

        all_rows = np.arange(n_queries)
        probabilities = []
            pred_labels = _y[:, k][neigh_ind]
            proba_k = np.zeros((n_queries, classes_k.size))

            # a simple ':' index doesn't work right
            for i, idx in enumerate(pred_labels.T):  # loop is O(n_neighbors)
                proba_k[all_rows, idx] += weights[:, i]

            # normalize 'votes' into real [0,1] probabilities
            normalizer = proba_k.sum(axis=1)[:, np.newaxis]
            proba_k /= normalizer

            probabilities.append(proba_k)

            probabilities = probabilities[0]

    # This function is defined here only to modify the parent docstring
    # and add information about X=None
        """
        Return the mean accuracy on the given test data and labels.

        X : array-like of shape (n_samples, n_features), or None
            used; in this case, points are not considered their own
            neighbors. This means that `knn.fit(X, y).score(None, y)`
            implicitly performs a leave-one-out cross-validation procedure
            and is equivalent to `cross_val_score(knn, X, y, cv=LeaveOneOut())`
            but typically much faster.

        Returns
        -------
        """

class RadiusNeighborsClassifier(RadiusNeighborsMixin, ClassifierMixin, NeighborsBase):
    """Classifier implementing a vote among neighbors within a given radius.

    radius : float, default=1.0
        Range of parameter space to use by default for :meth:`radius_neighbors`
        queries.

        Uniform weights are used by default.

        Power parameter for the Minkowski metric. When p = 1, this is
        equivalent to using manhattan_distance (l1), and euclidean_distance
        (l2) for p = 2. For arbitrary p, minkowski_distance (l_p) is used.
        This parameter is expected to be positive.

        values.

    outlier_label : {manual label, 'most_frequent'}, default=None
        Label for outlier samples (samples with no neighbors in given radius).

        - manual label: str or int label (should be the same type as y)
          or list of manual labels if multi-output is used.
        - 'most_frequent' : assign the most frequent label of y to outliers.
        - None : when any outlier is detected, ValueError will be raised.

        The outlier label should be selected from among the unique 'Y' labels.
        If it is specified with a different value a warning will be raised and
        all class probabilities of outliers will be assigned to be 0.

        Class labels known to the classifier.

    effective_metric_ : str or callable

    outlier_label_ : int or array-like of shape (n_class,)
        Label which is given for outlier samples (samples with no neighbors
        on given radius).

    See Also
    --------
    KNeighborsClassifier : Classifier implementing the k-nearest neighbors
        vote.
    RadiusNeighborsRegressor : Regression based on neighbors within a
        fixed radius.
    KNeighborsRegressor : Regression based on k-nearest neighbors.
    NearestNeighbors : Unsupervised learner for implementing neighbor
        searches.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.neighbors import RadiusNeighborsClassifier
    >>> neigh = RadiusNeighborsClassifier(radius=1.0)
    RadiusNeighborsClassifier(...)
    >>> print(neigh.predict([[1.5]]))
    [0]
    >>> print(neigh.predict_proba([[1.0]]))
    [[0.66666667 0.33333333]]
    """

        **NeighborsBase._parameter_constraints,
        "weights": [StrOptions({"uniform", "distance"}), callable, None],
        "outlier_label": [Integral, str, "array-like", None],
    }
    _parameter_constraints.pop("n_neighbors")

        self,
        radius=1.0,
        *,
        p=2,
        outlier_label=None,
    ):
            p=p,
        )
        self.outlier_label = outlier_label

        # RadiusNeighborsClassifier.metric is not validated yet
    )
        """Fit the radius neighbors classifier from the training dataset.

        Returns
        -------
        self : RadiusNeighborsClassifier
            The fitted radius neighbors classifier.
        """
        self._fit(X, y)

        if self.outlier_label is None:
            outlier_label_ = None

        elif self.outlier_label == "most_frequent":
            outlier_label_ = []
            # iterate over multi-output, get the most frequent label for each
            # output.
            for k, classes_k in enumerate(classes_):
                label_count = np.bincount(_y[:, k])
                outlier_label_.append(classes_k[label_count.argmax()])

        else:
            if _is_arraylike(self.outlier_label) and not isinstance(
                self.outlier_label, str
            ):
                if len(self.outlier_label) != len(classes_):
                        "The length of outlier_label: {} is "
                        "inconsistent with the output "
                        "length: {}".format(self.outlier_label, len(classes_))
                    )
                outlier_label_ = self.outlier_label
            else:
                outlier_label_ = [self.outlier_label] * len(classes_)

            for classes, label in zip(classes_, outlier_label_):
                if _is_arraylike(label) and not isinstance(label, str):
                    # ensure the outlier label for each output is a scalar.
                        "The outlier_label of classes {} is "
                        "supposed to be a scalar, got "
                        "{}.".format(classes, label)
                    )
                if np.append(classes, label).dtype != classes.dtype:
                    # ensure the dtype of outlier label is consistent with y.
                        "The dtype of outlier_label {} is "
                        "inconsistent with classes {} in "
                        "y.".format(label, classes)
                    )

        self.outlier_label_ = outlier_label_

        Returns
        -------
        """

        probs = self.predict_proba(X)

            probs = [probs]

        n_queries = probs[0].shape[0]

        for k, prob in enumerate(probs):
            # iterate over multi-output, assign labels based on probabilities
            # of each output.
            max_prob_index = prob.argmax(axis=1)
            y_pred[:, k] = classes_[k].take(max_prob_index)

            outlier_zero_probs = (prob == 0).all(axis=1)
            if outlier_zero_probs.any():
                zero_prob_index = np.flatnonzero(outlier_zero_probs)
                y_pred[zero_prob_index, k] = self.outlier_label_[k]

        Returns
        -------
        p : ndarray of shape (n_queries, n_classes), or a list of \
                n_outputs of such arrays if n_outputs > 1.
        """

        metric, metric_kwargs = _adjusted_metric(
            metric=self.metric, metric_kwargs=self.metric_params, p=self.p
        )

        if (
            self.weights == "uniform"
            and self._fit_method == "brute"
            and not self.outputs_2d_
            and RadiusNeighborsClassMode.is_usable_for(X, self._fit_X, metric)
        ):
            probabilities = RadiusNeighborsClassMode.compute(
                X=X,
                radius=self.radius,
                weights=self.weights,
                Y_labels=self._y,
                unique_Y_labels=self.classes_,
                outlier_label=self.outlier_label,
                strategy="parallel_on_X",
                # `strategy="parallel_on_X"` has in practice be shown
                # to be more efficient than `strategy="parallel_on_Y``
                # on many combination of datasets.
                # Hence, we choose to enforce it here.
                # For more information, see:
                # https://github.com/scikit-learn/scikit-learn/pull/26828/files#r1282398471
            )
            return probabilities

        neigh_dist, neigh_ind = self.radius_neighbors(X)
        outlier_mask = np.zeros(n_queries, dtype=bool)
        outlier_mask[:] = [len(nind) == 0 for nind in neigh_ind]
        outliers = np.flatnonzero(outlier_mask)
        inliers = np.flatnonzero(~outlier_mask)

        if self.outlier_label_ is None and outliers.size > 0:
                "No neighbors found for test samples %r, "
                "you can try using larger radius, "
                "giving a label for outliers, "
                "or considering removing them from your dataset." % outliers
            )

        if weights is not None:
            weights = weights[inliers]

        # iterate over multi-output, measure probabilities of the k-th output.
            pred_labels = np.zeros(len(neigh_ind), dtype=object)
            pred_labels[:] = [_y[ind, k] for ind in neigh_ind]

            proba_inl = np.zeros((len(inliers), classes_k.size))

            # samples have different size of neighbors within the same radius
                for i, idx in enumerate(pred_labels[inliers]):
                    proba_inl[i, :] = np.bincount(idx, minlength=classes_k.size)
            else:
                    proba_inl[i, :] = np.bincount(
                        idx, weights[i], minlength=classes_k.size
                    )
            proba_k[inliers, :] = proba_inl

            if outliers.size > 0:
                _outlier_label = self.outlier_label_[k]
                label_index = np.flatnonzero(classes_k == _outlier_label)
                if label_index.size == 1:
                    proba_k[outliers, label_index[0]] = 1.0
                else:
                        "Outlier label {} is not in training "
                        "classes. All class probabilities of "
                        "outliers will be assigned with 0."
                        "".format(self.outlier_label_[k])
                    )

            normalizer[normalizer == 0.0] = 1.0

        """

        Returns
        -------
        """

        self,
        *,
        p=2,
    ):
            p=p,
        )

        Returns
        -------
        """

        Returns
        -------
        """
            ):
                        [
                        ],
                        axis=1,
                    )
        else:

            )

            else:

        Returns
        -------
        """
            )
            if (
            ):
                else:
                    )

                    X,
                )

        else:

            )

        """

        Returns
        -------
        """

        self,
        *,
        p=2,
    ):
            p=p,
        )

        Returns
        -------
        """

            # output.

        else:
            ):
                    )
            else:

                    )
                    )

        Returns
        -------
        """

        Returns
        -------
        """

        )

        if (
        ):
                X=X,
            )

            )

            else:
                    )

                else:
                    )

class NeighborhoodComponentsAnalysis(
):
    """Neighborhood Components Analysis.

    Neighborhood Component Analysis (NCA) is a machine learning algorithm for
    metric learning. It learns a linear transformation in a supervised fashion
    to improve the classification accuracy of a stochastic nearest neighbors
    rule in the transformed space.

    Read more in the :ref:`User Guide <nca>`.

        Preferred dimensionality of the projected space.
        If None it will be set to `n_features`.

    init : {'auto', 'pca', 'lda', 'identity', 'random'} or ndarray of shape \
            (n_features_a, n_features_b), default='auto'
        Initialization of the linear transformation. Possible options are
        `'auto'`, `'pca'`, `'lda'`, `'identity'`, `'random'`, and a numpy
        array of shape `(n_features_a, n_features_b)`.

        - `'auto'`
            Depending on `n_components`, the most reasonable initialization
            is chosen. If `n_components <= min(n_features, n_classes - 1)`
            we use `'lda'`, as it uses labels information. If not, but
            `n_components < min(n_features, n_samples)`, we use `'pca'`, as
            it projects data in meaningful directions (those of higher
            variance). Otherwise, we just use `'identity'`.

        - `'pca'`
            `n_components` principal components of the inputs passed
            to :meth:`fit` will be used to initialize the transformation.
            (See :class:`~sklearn.decomposition.PCA`)

        - `'lda'`
            `min(n_components, n_classes)` most discriminative
            components of the inputs passed to :meth:`fit` will be used to
            initialize the transformation. (If `n_components > n_classes`,
            the rest of the components will be zero.) (See
            :class:`~sklearn.discriminant_analysis.LinearDiscriminantAnalysis`)

        - `'identity'`
            If `n_components` is strictly smaller than the
            dimensionality of the inputs passed to :meth:`fit`, the identity
            matrix will be truncated to the first `n_components` rows.

        - `'random'`
            The initial transformation will be a random array of shape
            `(n_components, n_features)`. Each value is sampled from the
            standard normal distribution.

        - numpy array
            `n_features_b` must match the dimensionality of the inputs passed
            to :meth:`fit` and n_features_a must be less than or equal to that.
            If `n_components` is not `None`, `n_features_a` must match it.

        If `True` and :meth:`fit` has been called before, the solution of the
        previous call to :meth:`fit` is used as the initial linear
        transformation (`n_components` and `init` will be ignored).

    max_iter : int, default=50
        Maximum number of iterations in the optimization.

    tol : float, default=1e-5
        Convergence tolerance for the optimization.

    callback : callable, default=None
        If not `None`, this function is called after every iteration of the
        optimizer, taking as arguments the current solution (flattened
        transformation matrix) and the number of iterations. This might be
        useful in case one wants to examine or store the transformation
        found after each iteration.

        If 0, no progress messages will be printed.
        If 1, progress messages will be printed to stdout.
        If > 1, progress messages will be printed and the `disp`
        parameter of :func:`scipy.optimize.minimize` will be set to
        `verbose - 2`.

    random_state : int or numpy.RandomState, default=None
        A pseudo random number generator object or a seed for it if int. If
        `init='random'`, `random_state` is used to initialize the random
        transformation. If `init='pca'`, `random_state` is passed as an
        argument to PCA when initializing the transformation. Pass an int
        for reproducible results across multiple function calls.

        The linear transformation learned during fitting.

        Counts the number of iterations performed by the optimizer.

    random_state_ : numpy.RandomState
        Pseudo random number generator object used during initialization.

    See Also
    --------
    sklearn.discriminant_analysis.LinearDiscriminantAnalysis : Linear
        Discriminant Analysis.
    sklearn.decomposition.PCA : Principal component analysis (PCA).

    .. [1] J. Goldberger, G. Hinton, S. Roweis, R. Salakhutdinov.
           "Neighbourhood Components Analysis". Advances in Neural Information
           Processing Systems. 17, 513-520, 2005.
           http://www.cs.nyu.edu/~roweis/papers/ncanips.pdf

    .. [2] Wikipedia entry on Neighborhood Components Analysis
           https://en.wikipedia.org/wiki/Neighbourhood_components_analysis

    Examples
    --------
    >>> from sklearn.neighbors import NeighborhoodComponentsAnalysis
    ... stratify=y, test_size=0.7, random_state=42)
    >>> nca = NeighborhoodComponentsAnalysis(random_state=42)
    >>> nca.fit(X_train, y_train)
    NeighborhoodComponentsAnalysis(...)
    >>> knn = KNeighborsClassifier(n_neighbors=3)
    >>> knn.fit(X_train, y_train)
    >>> print(knn.score(X_test, y_test))
    0.933333...
    >>> knn.fit(nca.transform(X_train), y_train)
    >>> print(knn.score(nca.transform(X_test), y_test))
    0.961904...
    """

            None,
        ],
        "init": [
            StrOptions({"auto", "pca", "lda", "identity", "random"}),
            np.ndarray,
        ],
        "callback": [callable, None],
    }

        self,
        *,
        init="auto",
        max_iter=50,
        tol=1e-5,
        callback=None,
    ):
        self.callback = callback

        """Fit the model according to the given training data.

            The training samples.

            The corresponding training labels.

        Returns
        -------
        """
        # Validate the inputs X and y, and converts y to numerical classes.
        X, y = validate_data(self, X, y, ensure_min_samples=2)
        y = LabelEncoder().fit_transform(y)

        # Check the preferred dimensionality of the projected space
        if self.n_components is not None and self.n_components > X.shape[1]:
                "The preferred dimensionality of the "
                f"projected space `n_components` ({self.n_components}) cannot "
                "be greater than the given data "
                f"dimensionality ({X.shape[1]})!"
            )
        # If warm_start is enabled, check that the inputs are consistent
        if (
            and hasattr(self, "components_")
            and self.components_.shape[1] != X.shape[1]
        ):
                f"The new inputs dimensionality ({X.shape[1]}) does not "
                "match the input dimensionality of the "
                f"previously learned transformation ({self.components_.shape[1]})."
            )
        # Check how the linear transformation should be initialized
        init = self.init
        if isinstance(init, np.ndarray):
            init = check_array(init)
            # Assert that init.shape[1] = X.shape[1]
            if init.shape[1] != X.shape[1]:
                    f"The input dimensionality ({init.shape[1]}) of the given "
                    "linear transformation `init` must match the "
                    f"dimensionality of the given inputs `X` ({X.shape[1]})."
                )
            # Assert that init.shape[0] <= init.shape[1]
            if init.shape[0] > init.shape[1]:
                    f"The output dimensionality ({init.shape[0]}) of the given "
                    "linear transformation `init` cannot be "
                    f"greater than its input dimensionality ({init.shape[1]})."
                )
            # Assert that self.n_components = init.shape[0]
            if self.n_components is not None and self.n_components != init.shape[0]:
                    "The preferred dimensionality of the "
                    f"projected space `n_components` ({self.n_components}) does"
                    " not match the output dimensionality of "
                    "the given linear transformation "
                    f"`init` ({init.shape[0]})!"
                )

        # Initialize the random generator
        self.random_state_ = check_random_state(self.random_state)

        # Measure the total training time
        t_train = time.time()

        # Compute a mask that stays fixed during optimization:
        same_class_mask = y[:, np.newaxis] == y[np.newaxis, :]
        # (n_samples, n_samples)

        # Initialize the transformation
        transformation = np.ravel(self._initialize(X, y, init))

        # Create a dictionary of parameters to be passed to the optimizer
        disp = self.verbose - 2 if self.verbose > 1 else -1
        optimizer_params = {
            "method": "L-BFGS-B",
            "fun": self._loss_grad_lbfgs,
            "args": (X, same_class_mask, -1.0),
            "jac": True,
            "x0": transformation,
            "tol": self.tol,
            "options": dict(maxiter=self.max_iter, disp=disp),
            "callback": self._callback,
        }

        # Call the optimizer
        opt_result = minimize(**optimizer_params)

        # Reshape the solution found by the optimizer
        self.components_ = opt_result.x.reshape(-1, X.shape[1])

        # Stop timer
        t_train = time.time() - t_train
            cls_name = self.__class__.__name__

            # Warn the user if the algorithm did not converge
            if not opt_result.success:
                warn(
                    "[{}] NCA did not converge: {}".format(
                        cls_name, opt_result.message
                    ),
                    ConvergenceWarning,
                )

            print("[{}] Training took {:8.2f}s.".format(cls_name, t_train))

        """Apply the learned transformation to the given data.

            Data samples.

        Returns
        -------
        X_embedded: ndarray of shape (n_samples, n_components)
            The data samples transformed.

        Raises
        ------
        NotFittedError
            If :meth:`fit` has not been called before.
        """

        return np.dot(X, self.components_.T)

    def _initialize(self, X, y, init):
        """Initialize the transformation.

            The training labels.

        init : str or ndarray of shape (n_features_a, n_features_b)
            The validated initialization of the linear transformation.

        Returns
        -------
        transformation : ndarray of shape (n_components, n_features)
            The initialized linear transformation.

        """

        transformation = init
        if self.warm_start and hasattr(self, "components_"):
            transformation = self.components_
        elif isinstance(init, np.ndarray):
            pass
        else:
            n_samples, n_features = X.shape
            n_components = self.n_components or n_features
            if init == "auto":
                n_classes = len(np.unique(y))
                if n_components <= min(n_features, n_classes - 1):
                    init = "lda"
                elif n_components < min(n_features, n_samples):
                    init = "pca"
                else:
                    init = "identity"
            if init == "identity":
                transformation = np.eye(n_components, X.shape[1])
            elif init == "random":
                transformation = self.random_state_.standard_normal(
                    size=(n_components, X.shape[1])
                )
            elif init in {"pca", "lda"}:
                init_time = time.time()
                if init == "pca":
                    pca = PCA(
                        n_components=n_components, random_state=self.random_state_
                    )
                    if self.verbose:
                        print("Finding principal components... ", end="")
                        sys.stdout.flush()
                    pca.fit(X)
                    transformation = pca.components_
                elif init == "lda":
                    from ..discriminant_analysis import LinearDiscriminantAnalysis

                    lda = LinearDiscriminantAnalysis(n_components=n_components)
                        print("Finding most discriminative components... ", end="")
                    lda.fit(X, y)
                    transformation = lda.scalings_.T[:n_components]
                    print("done in {:5.2f}s".format(time.time() - init_time))
        return transformation

    def _callback(self, transformation):
        """Called after each iteration of the optimizer.

        transformation : ndarray of shape (n_components * n_features,)
            The solution computed by the optimizer in this iteration.
        """
        if self.callback is not None:
            self.callback(transformation, self.n_iter_)

        self.n_iter_ += 1

    def _loss_grad_lbfgs(self, transformation, X, same_class_mask, sign=1.0):
        """Compute the loss and the loss gradient w.r.t. `transformation`.

            The raveled linear transformation on which to compute loss and
            evaluate gradient.

        same_class_mask : ndarray of shape (n_samples, n_samples)
            A mask where `mask[i, j] == 1` if `X[i]` and `X[j]` belong
            to the same class, and `0` otherwise.

        Returns
        -------
            The loss computed for the given transformation.

        gradient : ndarray of shape (n_components * n_features,)
            The new (flattened) gradient of the loss.
        """

        if self.n_iter_ == 0:
                header_fields = ["Iteration", "Objective Value", "Time(s)"]
                header_fmt = "{:>10} {:>20} {:>10}"
                header = header_fmt.format(*header_fields)
                cls_name = self.__class__.__name__
                print("[{}]".format(cls_name))
                print(
                    "[{}] {}\n[{}] {}".format(
                        cls_name, header, cls_name, "-" * len(header)
                    )
                )

        t_funcall = time.time()

        transformation = transformation.reshape(-1, X.shape[1])
        X_embedded = np.dot(X, transformation.T)  # (n_samples, n_components)

        # Compute softmax distances
        p_ij = pairwise_distances(X_embedded, squared=True)
        np.fill_diagonal(p_ij, np.inf)
        p_ij = softmax(-p_ij)  # (n_samples, n_samples)

        # Compute loss
        masked_p_ij = p_ij * same_class_mask
        p = np.sum(masked_p_ij, axis=1, keepdims=True)  # (n_samples, 1)
        loss = np.sum(p)

        # Compute gradient of loss w.r.t. `transform`
        weighted_p_ij = masked_p_ij - p_ij * p
        weighted_p_ij_sym = weighted_p_ij + weighted_p_ij.T
        np.fill_diagonal(weighted_p_ij_sym, -weighted_p_ij.sum(axis=0))
        gradient = 2 * X_embedded.T.dot(weighted_p_ij_sym).dot(X)
        # time complexity of the gradient: O(n_components x n_samples x (
        # n_samples + n_features))

            t_funcall = time.time() - t_funcall
            values_fmt = "[{}] {:>10} {:>20.6e} {:>10.2f}"
            print(
                values_fmt.format(
                    self.__class__.__name__, self.n_iter_, loss, t_funcall
                )
            )
            sys.stdout.flush()

        return sign * loss, sign * gradient.ravel()

    @property
    def _n_features_out(self):
        """Number of transformed output features."""
        return self.components_.shape[0]

        self,
        *,
        tol=1e-5,
    ):

        Returns
        -------
        """

            )
        if (
        ):
            )
                )
                )
                )

        }

                warn(
                    ),
                )

        Returns
        -------

        Raises
        ------
        """

        Returns
        -------

        """

            pass
        else:
                else:
                )
                    pca = PCA(
                    )

        """

        Returns
        -------

        """

                print(
                    )
                )

            print(
                )
            )

class KNeighborsRegressor(KNeighborsMixin, RegressorMixin, NeighborsBase):
    """Regression based on k-nearest neighbors.

    The target is predicted by local interpolation of the targets
    associated of the nearest neighbors in the training set.

    Read more in the :ref:`User Guide <regression>`.

        See the following example for a demonstration of the impact of
        different weighting schemes on predictions:
        :ref:`sphx_glr_auto_examples_neighbors_plot_regression.py`.

    metric : str, DistanceMetric object or callable, default='minkowski'
        values.

        If metric is a DistanceMetric object, it will be passed directly to
        the underlying computation routines.

        The distance metric to use. It will be same as the `metric` parameter

    See Also
    --------
    NearestNeighbors : Unsupervised learner for implementing neighbor searches.
    RadiusNeighborsRegressor : Regression based on neighbors within a fixed radius.
    KNeighborsClassifier : Classifier implementing the k-nearest neighbors vote.
    RadiusNeighborsClassifier : Classifier implementing
        a vote among neighbors within a given radius.

    Notes
    -----

       neighbors, neighbor `k+1` and `k`, have identical distances but
       different labels, the results will depend on the ordering of the

    https://en.wikipedia.org/wiki/K-nearest_neighbors_algorithm

    Examples
    --------
    >>> neigh = KNeighborsRegressor(n_neighbors=2)
    KNeighborsRegressor(...)
    [0.5]
    """

    }
    _parameter_constraints["metric"].append(DistanceMetric)

        self,
        *,
        p=2,
    ):
            p=p,
        )

        # KNeighborsRegressor.metric is not validated yet
    )
        """Fit the k-nearest neighbors regressor from the training dataset.

        Returns
        -------
        self : KNeighborsRegressor
            The fitted k-nearest neighbors regressor.
        """

        """Predict the target for the provided data.

        Returns
        -------
        y : ndarray of shape (n_queries,) or (n_queries, n_outputs), dtype=int
        """
        else:

        if _y.ndim == 1:
            _y = _y.reshape((-1, 1))

            y_pred = np.mean(_y[neigh_ind], axis=1)
        else:
            y_pred = np.empty((neigh_dist.shape[0], _y.shape[1]), dtype=np.float64)
            denom = np.sum(weights, axis=1)

            for j in range(_y.shape[1]):
                num = np.sum(_y[neigh_ind, j] * weights, axis=1)
                y_pred[:, j] = num / denom

        if self._y.ndim == 1:

class RadiusNeighborsRegressor(RadiusNeighborsMixin, RegressorMixin, NeighborsBase):
    """Regression based on neighbors within a fixed radius.

        queries.

        values.

    See Also
    --------
    KNeighborsClassifier : Classifier based on the k-nearest neighbors.
    RadiusNeighborsClassifier : Classifier based on neighbors within a given radius.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.neighbors import RadiusNeighborsRegressor
    >>> neigh = RadiusNeighborsRegressor(radius=1.0)
    RadiusNeighborsRegressor(...)
    [0.5]
    """

    }

        self,
        *,
        p=2,
    ):
            p=p,
        )

        # RadiusNeighborsRegressor.metric is not validated yet
    )
        """Fit the radius neighbors regressor from the training dataset.

        Returns
        -------
        self : RadiusNeighborsRegressor
            The fitted radius neighbors regressor.
        """

        Returns
        -------
        y : ndarray of shape (n_queries,) or (n_queries, n_outputs), \
                dtype=double
        """

        empty_obs = np.full_like(_y[0], np.nan)

                [
                    np.mean(_y[ind, :], axis=0) if len(ind) else empty_obs
                    for (i, ind) in enumerate(neigh_ind)
                ]
            )

        else:
                [
                    (
                        np.average(_y[ind, :], axis=0, weights=weights[i])
                        if len(ind)
                        else empty_obs
                    )
                ]
            )

        if np.any(np.isnan(y_pred)):
            empty_warning_msg = (
                "One or more samples have no neighbors "
                "within specified radius; predicting NaN."
            )
            warnings.warn(empty_warning_msg)

        Returns
        -------
        """

        Returns
        -------
        """
        else:

        else:

        self,
        *,
        p=2,
    ):
            p=p,
        )

        Returns
        -------
        """

        Returns
        -------
        """

                [
                ]
            )

        else:
                [
                    (
                    )
                ]
            )

            )

class NearestNeighbors(KNeighborsMixin, RadiusNeighborsMixin, NeighborsBase):
    """Unsupervised learner for implementing neighbor searches.

    Read more in the :ref:`User Guide <unsupervised_neighbors>`.

        queries.

        values.

    p : float (positive), default=2
        Parameter for the Minkowski metric from
        sklearn.metrics.pairwise.pairwise_distances. When p = 1, this is

    effective_metric_ : str
        Metric used to compute distances to neighbors.

        Parameters for the metric used to compute distances to neighbors.

    See Also
    --------
        vote.
    RadiusNeighborsClassifier : Classifier implementing a vote among neighbors
        within a given radius.
    RadiusNeighborsRegressor : Regression based on neighbors within a fixed
        radius.
    BallTree : Space partitioning data structure for organizing points in a
        multi-dimensional space, used for nearest neighbor search.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.neighbors import NearestNeighbors
    >>> samples = [[0, 0, 2], [1, 0, 0], [0, 0, 1]]
    >>> neigh = NearestNeighbors(n_neighbors=2, radius=0.4)
    >>> neigh.fit(samples)
    NearestNeighbors(...)
    >>> neigh.kneighbors([[0, 0, 1.3]], 2, return_distance=False)
    array([[2, 0]]...)
    >>> nbrs = neigh.radius_neighbors(
    ...    [[0, 0, 1.3]], 0.4, return_distance=False
    ... )
    >>> np.asarray(nbrs[0][0])
    array(2)
    """

        self,
        *,
        p=2,
    ):
            p=p,
        )

        # NearestNeighbors.metric is not validated yet
    )
        """Fit the nearest neighbors estimator from the training dataset.

        y : Ignored

        Returns
        -------
        self : NearestNeighbors
            The fitted nearest neighbors estimator.
        """
        return self._fit(X)

        self,
        *,
        p=2,
    ):
            p=p,
        )

        y : Ignored

        Returns
        -------
        """

class LocalOutlierFactor(KNeighborsMixin, OutlierMixin, NeighborsBase):
    """Unsupervised Outlier Detection using the Local Outlier Factor (LOF).

    The anomaly score of each sample is called the Local Outlier Factor.
    It measures the local deviation of the density of a given sample with respect
    to its neighbors.
    It is local in that the anomaly score depends on how isolated the object
    is with respect to the surrounding neighborhood.
    More precisely, locality is given by k-nearest neighbors, whose distance
    is used to estimate the local density.
    By comparing the local density of a sample to the local densities of its
    neighbors, one can identify samples that have a substantially lower density
    than their neighbors. These are considered outliers.

    n_neighbors : int, default=20
        If n_neighbors is larger than the number of samples provided,
        all samples will be used.

        Leaf is size passed to :class:`BallTree` or :class:`KDTree`. This can
        affect the speed of the construction and query, as well as the memory
        required to store the tree. The optimal value depends on the

        values.

        :func:`sklearn.metrics.pairwise_distances`. When p = 1, this
        is equivalent to using manhattan_distance (l1), and euclidean_distance

        of outliers in the data set. When fitting this is used to define the
        threshold on the scores of the samples.

        - if 'auto', the threshold is determined as in the
          original paper,
        - if a float, the contamination should be in the range (0, 0.5].

    novelty : bool, default=False
        By default, LocalOutlierFactor is only meant to be used for outlier
        detection (novelty=False). Set novelty to True if you want to use
        LocalOutlierFactor for novelty detection. In this case be aware that
        you should only use predict, decision_function and score_samples
        on new unseen data and not on the training set; and note that the
        results obtained this way may differ from the standard LOF results.

    negative_outlier_factor_ : ndarray of shape (n_samples,)
        The opposite LOF of the training samples. The higher, the more normal.
        Inliers tend to have a LOF score close to 1
        (``negative_outlier_factor_`` close to -1), while outliers tend to have
        a larger LOF score.

        The local outlier factor (LOF) of a sample captures its
        supposed 'degree of abnormality'.
        It is the average of the ratio of the local reachability density of
        a sample and those of its k-nearest neighbors.

    n_neighbors_ : int
        The actual number of neighbors used for :meth:`kneighbors` queries.

        Offset used to obtain binary labels from the raw scores.
        Observations having a negative_outlier_factor smaller than `offset_`
        are detected as abnormal.
        The offset is set to -1.5 (inliers score around -1), except when a
        contamination parameter different than "auto" is provided. In that
        case, the offset is defined in such a way we obtain the expected
        number of outliers in training.

        The effective metric used for the distance computation.

        The effective additional keyword arguments for the metric function.

        It is the number of samples in the fitted data.

    See Also
    --------
    sklearn.svm.OneClassSVM: Unsupervised Outlier Detection using
        Support Vector Machine.

    .. [1] Breunig, M. M., Kriegel, H. P., Ng, R. T., & Sander, J. (2000, May).
           LOF: identifying density-based local outliers. In ACM sigmod record.

    Examples
    --------
    >>> from sklearn.neighbors import LocalOutlierFactor
    >>> X = [[-1.1], [0.2], [101.1], [0.3]]
    >>> clf = LocalOutlierFactor(n_neighbors=2)
    >>> clf.fit_predict(X)
    array([ 1,  1, -1,  1])
    >>> clf.negative_outlier_factor_
    array([ -0.9821...,  -1.0370..., -73.3697...,  -0.9821...])
    """

        ],
        "novelty": ["boolean"],
    }

        self,
        n_neighbors=20,
        *,
        p=2,
        novelty=False,
    ):
            p=p,
        )
        self.novelty = novelty

    def _check_novelty_fit_predict(self):
        if self.novelty:
            msg = (
                "fit_predict is not available when novelty=True. Use "
                "novelty=False if you want to predict on the training set."
            )
            raise AttributeError(msg)

    @available_if(_check_novelty_fit_predict)
    def fit_predict(self, X, y=None):
        """Fit the model to the training set X and return the labels.

        **Not available for novelty detection (when novelty is set to True).**
        Label is 1 for an inlier and -1 for an outlier according to the LOF
        score and the contamination parameter.

            The query sample or samples to compute the Local Outlier Factor
            w.r.t. the training samples.

        y : Ignored

        Returns
        -------
            Returns -1 for anomalies/outliers and 1 for inliers.
        """

        # As fit_predict would be different from fit.predict, fit_predict is
        # only available for outlier detection (novelty=False)

        return self.fit(X)._predict()

        # LocalOutlierFactor.metric is not validated yet
    )
        """Fit the local outlier factor detector from the training dataset.

        y : Ignored

        Returns
        -------
        self : LocalOutlierFactor
            The fitted local outlier factor detector.
        """
        self._fit(X)

        n_samples = self.n_samples_fit_
        if self.n_neighbors > n_samples:
                "n_neighbors (%s) is greater than the "
                "total number of samples (%s). n_neighbors "
                "will be set to (n_samples - 1) for estimation."
                % (self.n_neighbors, n_samples)
            )
        self.n_neighbors_ = max(1, min(self.n_neighbors, n_samples - 1))

        self._distances_fit_X_, _neighbors_indices_fit_X_ = self.kneighbors(
            n_neighbors=self.n_neighbors_
        )

        if self._fit_X.dtype == np.float32:
            self._distances_fit_X_ = self._distances_fit_X_.astype(
                self._fit_X.dtype,
                copy=False,
            )

        self._lrd = self._local_reachability_density(
            self._distances_fit_X_, _neighbors_indices_fit_X_
        )

        # Compute lof score over training samples to define offset_:
        lrd_ratios_array = (
            self._lrd[_neighbors_indices_fit_X_] / self._lrd[:, np.newaxis]
        )

        self.negative_outlier_factor_ = -np.mean(lrd_ratios_array, axis=1)

            # inliers score around -1 (the higher, the less abnormal).
            self.offset_ = -1.5
        else:
            self.offset_ = np.percentile(
                self.negative_outlier_factor_, 100.0 * self.contamination
            )

        # Verify if negative_outlier_factor_ values are within acceptable range.
        # Novelty must also be false to detect outliers
        if np.min(self.negative_outlier_factor_) < -1e7 and not self.novelty:
                "Duplicate values are leading to incorrect results. "
                "Increase the number of neighbors for more accurate results."
            )

    def _check_novelty_predict(self):
        if not self.novelty:
            msg = (
                "predict is not available when novelty=False, use "
                "fit_predict if you want to predict on training data. Use "
                "novelty=True if you want to use LOF for novelty detection "
                "and predict on new unseen data."
            )

    @available_if(_check_novelty_predict)
    def predict(self, X=None):
        """Predict the labels (1 inlier, -1 outlier) of X according to LOF.

        **Only available for novelty detection (when novelty is set to True).**
        This method allows to generalize prediction to *new observations* (not
        in the training set). Note that the result of ``clf.fit(X)`` then
        ``clf.predict(X)`` with ``novelty=True`` may differ from the result
        obtained by ``clf.fit_predict(X)`` with ``novelty=False``.

        Returns
        -------
        """

    def _predict(self, X=None):

        If X is None, returns the same as fit_predict(X_train).

            w.r.t. the training samples. If None, makes prediction on the
            training data without considering them as their own neighbors.

        Returns
        -------
        """

        if X is not None:
            shifted_opposite_lof_scores = self.decision_function(X)
            is_inlier = np.ones(shifted_opposite_lof_scores.shape[0], dtype=int)
            is_inlier[shifted_opposite_lof_scores < 0] = -1
        else:
            is_inlier = np.ones(self.n_samples_fit_, dtype=int)
            is_inlier[self.negative_outlier_factor_ < self.offset_] = -1

    def _check_novelty_decision_function(self):
            msg = (
                "decision_function is not available when novelty=False. "
                "Use novelty=True if you want to use LOF for novelty "
                "detection and compute decision_function for new unseen "
                "data. Note that the opposite LOF of the training samples "
                "is always available by considering the "
                "negative_outlier_factor_ attribute."
            )

    @available_if(_check_novelty_decision_function)
        """Shifted opposite of the Local Outlier Factor of X.

        Bigger is better, i.e. large values correspond to inliers.

        The shift offset allows a zero threshold for being an outlier.
        The argument X is supposed to contain *new data*: if X contains a
        point from training, it considers the later in its own neighborhood.
        Also, the samples in X are not considered in the neighborhood of any
        point.

        Returns
        -------
        shifted_opposite_lof_scores : ndarray of shape (n_samples,)
            The shifted opposite of the Local Outlier Factor of each input
            samples. The lower, the more abnormal. Negative scores represent
            outliers, positive scores represent inliers.
        """

    def _check_novelty_score_samples(self):
            msg = (
                "score_samples is not available when novelty=False. The "
                "scores of the training samples are always available "
                "through the negative_outlier_factor_ attribute. Use "
                "and compute score_samples for new unseen data."
            )

    @available_if(_check_novelty_score_samples)
        """Opposite of the Local Outlier Factor of X.

        It is the opposite as bigger is better, i.e. large values correspond
        to inliers.

        point. Because of this, the scores obtained via ``score_samples`` may
        differ from the standard LOF scores.
        The standard LOF scores for the training data is available via the
        ``negative_outlier_factor_`` attribute.

        Returns
        -------
        opposite_lof_scores : ndarray of shape (n_samples,)
            The opposite of the Local Outlier Factor of each input samples.
        """

        distances_X, neighbors_indices_X = self.kneighbors(
            X, n_neighbors=self.n_neighbors_
        )

            distances_X = distances_X.astype(X.dtype, copy=False)

        X_lrd = self._local_reachability_density(
            distances_X,
            neighbors_indices_X,
        )

        lrd_ratios_array = self._lrd[neighbors_indices_X] / X_lrd[:, np.newaxis]

        # as bigger is better:
        return -np.mean(lrd_ratios_array, axis=1)

    def _local_reachability_density(self, distances_X, neighbors_indices):
        """The local reachability density (LRD)

        The LRD of a sample is the inverse of the average reachability
        distance of its k-nearest neighbors.

        distances_X : ndarray of shape (n_queries, self.n_neighbors)
            Distances to the neighbors (in the training samples `self._fit_X`)
            of each query point to compute the LRD.

        neighbors_indices : ndarray of shape (n_queries, self.n_neighbors)
            Neighbors indices (of each query point) among training samples
            self._fit_X.

        Returns
        -------
        local_reachability_density : ndarray of shape (n_queries,)
            The local reachability density of each sample.
        """
        dist_k = self._distances_fit_X_[neighbors_indices, self.n_neighbors_ - 1]
        reach_dist_array = np.maximum(distances_X, dist_k)

        # 1e-10 to avoid `nan' when nb of duplicates > n_neighbors_:
        return 1.0 / (np.mean(reach_dist_array, axis=1) + 1e-10)

        self,
        *,
        p=2,
    ):
            p=p,
        )

            msg = (
            )

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """

            )

        )

            )

        )

        )

        else:
            )

            )

            msg = (
            )

        Returns
        -------
        """

        Returns
        -------
        """

        else:

            msg = (
            )

        point.

        Returns
        -------
        """

            msg = (
            )

        Returns
        -------
        """

        )

        )

        Returns
        -------
        """

def _check_params(X, metric, p, metric_params):
    """Check the validity of the input parameters"""
    params = zip(["metric", "p", "metric_params"], [metric, p, metric_params])
    est_params = X.get_params()
    for param_name, func_param in params:
        if func_param != est_params[param_name]:
                "Got %s for %s, while the estimator has %s for the same parameter."
                % (func_param, param_name, est_params[param_name])
            )

def _query_include_self(X, include_self, mode):
    """Return the query based on include_self param"""
    if include_self == "auto":
        include_self = mode == "connectivity"

    # it does not include each sample as its own neighbors
    if not include_self:
        X = None

    return X

def kneighbors_graph(
    X,
    n_neighbors,
    *,
    mode="connectivity",
    metric="minkowski",
    p=2,
    metric_params=None,
    include_self=False,
):
    """Compute the (weighted) graph of k-Neighbors for points in X.

        Number of neighbors for each sample.

    mode : {'connectivity', 'distance'}, default='connectivity'
        Type of returned matrix: 'connectivity' will return the connectivity
        matrix with ones and zeros, and 'distance' will return the distances
        between neighbors according to the given metric.

    metric : str, default='minkowski'
        values.

    include_self : bool or 'auto', default=False
        Whether or not to mark each sample as the first nearest neighbor to
        itself. If 'auto', then True is used for mode='connectivity' and False
        for mode='distance'.

    Returns
    -------
    A : sparse matrix of shape (n_samples, n_samples)
        Graph where A[i, j] is assigned the weight of edge that
        connects i to j. The matrix is of CSR format.

    See Also
    --------
    radius_neighbors_graph: Compute the (weighted) graph of Neighbors for points in X.

    Examples
    --------
    >>> X = [[0], [3], [1]]
    >>> from sklearn.neighbors import kneighbors_graph
    >>> A = kneighbors_graph(X, 2, mode='connectivity', include_self=True)
    >>> A.toarray()
    array([[1., 0., 1.],
           [0., 1., 1.],
           [1., 0., 1.]])
    """
    if not isinstance(X, KNeighborsMixin):
        X = NearestNeighbors(
            p=p,
        ).fit(X)
    else:
        _check_params(X, metric, p, metric_params)

    query = _query_include_self(X._fit_X, include_self, mode)
    return X.kneighbors_graph(X=query, n_neighbors=n_neighbors, mode=mode)

def radius_neighbors_graph(
    X,
    radius,
    *,
    p=2,
):
    """Compute the (weighted) graph of Neighbors for points in X.

    Neighborhoods are restricted the points at a distance lower than
    radius.

        Radius of neighborhoods.

        values.

    Returns
    -------
        Graph where A[i, j] is assigned the weight of edge that connects
        i to j. The matrix is of CSR format.

    See Also
    --------
    kneighbors_graph: Compute the weighted graph of k-neighbors for points in X.

    Examples
    --------
    >>> from sklearn.neighbors import radius_neighbors_graph
    >>> A = radius_neighbors_graph(X, 1.5, mode='connectivity',
    ...                            include_self=True)
           [0., 1., 0.],
    """
    if not isinstance(X, RadiusNeighborsMixin):
            p=p,
        ).fit(X)
    else:

    return X.radius_neighbors_graph(query, radius, mode)

class KNeighborsTransformer(
    ClassNamePrefixFeaturesOutMixin, KNeighborsMixin, TransformerMixin, NeighborsBase
):
    """Transform X into a (weighted) graph of k nearest neighbors.

    The transformed data is a sparse graph as returned by kneighbors_graph.

    Read more in the :ref:`User Guide <neighbors_transformer>`.

    mode : {'distance', 'connectivity'}, default='distance'

        Number of neighbors for each sample in the transformed sparse graph.
        For compatibility reasons, as each sample is considered as its own
        neighbor, one extra neighbor will be computed when mode == 'distance'.
        In this case, the sparse graph contains (n_neighbors + 1) neighbors.

        values.

        If ``-1``, then the number of jobs is set to the number of CPU cores.

    See Also
    --------
    kneighbors_graph : Compute the weighted graph of k-neighbors for
        points in X.
    RadiusNeighborsTransformer : Transform X into a weighted graph of
        neighbors nearer than a radius.

    Notes
    -----
    For an example of using :class:`~sklearn.neighbors.KNeighborsTransformer`
    in combination with :class:`~sklearn.manifold.TSNE` see
    :ref:`sphx_glr_auto_examples_neighbors_approximate_nearest_neighbors.py`.

    Examples
    --------
    >>> from sklearn.neighbors import KNeighborsTransformer
    >>> X, _ = load_wine(return_X_y=True)
    (178, 13)
    >>> transformer = KNeighborsTransformer(n_neighbors=5, mode='distance')
    >>> X_dist_graph = transformer.fit_transform(X)
    >>> X_dist_graph.shape
    (178, 178)
    """

        "mode": [StrOptions({"distance", "connectivity"})],
    }

        self,
        *,
        mode="distance",
        p=2,
    ):
            radius=None,
            p=p,
        )

        # KNeighborsTransformer.metric is not validated yet
    )
        """Fit the k-nearest neighbors transformer from the training dataset.

        y : Ignored

        Returns
        -------
        self : KNeighborsTransformer
            The fitted k-nearest neighbors transformer.
        """
        self._n_features_out = self.n_samples_fit_

        X : array-like of shape (n_samples_transform, n_features)
            Sample data.

        Returns
        -------
        Xt : sparse matrix of shape (n_samples_transform, n_samples_fit)
            Xt[i, j] is assigned the weight of edge that connects i to j.
            Only the neighbors have an explicit value.
            The diagonal is always explicit.
        """
        add_one = self.mode == "distance"
        return self.kneighbors_graph(
            X, mode=self.mode, n_neighbors=self.n_neighbors + add_one
        )

        """Fit to data, then transform it.

        Fits transformer to X and y with optional parameters fit_params
        and returns a transformed version of X.

            Training set.

        y : Ignored

        Returns
        -------
        Xt : sparse matrix of shape (n_samples, n_samples)
        """
        return self.fit(X).transform(X)

class RadiusNeighborsTransformer(
    RadiusNeighborsMixin,
    NeighborsBase,
):
    """Transform X into a (weighted) graph of neighbors nearer than a radius.

    The transformed data is a sparse graph as returned by
    `radius_neighbors_graph`.

        Radius of neighborhood in the transformed sparse graph.

        values.

    See Also
    --------
    KNeighborsTransformer : Transform X into a weighted graph of k
        nearest neighbors.

    Examples
    --------
    >>> from sklearn.cluster import DBSCAN
    >>> from sklearn.neighbors import RadiusNeighborsTransformer
    >>> estimator = make_pipeline(
    ...     RadiusNeighborsTransformer(radius=42.0, mode='distance'),
    ...     DBSCAN(eps=25.0, metric='precomputed'))
    >>> X_clustered = estimator.fit_predict(X)
    >>> clusters, counts = np.unique(X_clustered, return_counts=True)
    >>> print(counts)
    [ 29  15 111  11  12]
    """

    }

        self,
        *,
        p=2,
    ):
            n_neighbors=None,
            p=p,
        )

        # RadiusNeighborsTransformer.metric is not validated yet
    )
        """Fit the radius neighbors transformer from the training dataset.

        y : Ignored

        Returns
        -------
        self : RadiusNeighborsTransformer
            The fitted radius neighbors transformer.
        """

        Returns
        -------
        """
        return self.radius_neighbors_graph(X, mode=self.mode, sort_results=True)

        y : Ignored

        Returns
        -------
        """

        self,
        *,
        p=2,
    ):
            p=p,
        )

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """
        )

        y : Ignored

        Returns
        -------
        """

        self,
        *,
        p=2,
    ):
            p=p,
        )

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

class NearestCentroid(
):
    """Nearest centroid classifier.

    Each class is represented by its centroid, with test samples classified to
    the class with the nearest centroid.

    Read more in the :ref:`User Guide <nearest_centroid_classifier>`.

    metric : {"euclidean", "manhattan"}, default="euclidean"
        Metric to use for distance computation.

        If `metric="euclidean"`, the centroid for the samples corresponding to each
        class is the arithmetic mean, which minimizes the sum of squared L1 distances.
        If `metric="manhattan"`, the centroid is the feature-wise median, which
        minimizes the sum of L1 distances.

        .. versionchanged:: 1.5
            All metrics but `"euclidean"` and `"manhattan"` were deprecated and
            now raise an error.

            `metric='precomputed'` was deprecated and now raises an error

    shrink_threshold : float, default=None
        Threshold for shrinking centroids to remove features.

    priors : {"uniform", "empirical"} or array-like of shape (n_classes,), \
        default="uniform"

    centroids_ : array-like of shape (n_classes, n_features)
        Centroid of each class.

        The unique classes labels.

    deviations_ : ndarray of shape (n_classes, n_features)
        Deviations (or shrinkages) of the centroids of each class from the
        overall centroid. Equal to eq. (18.4) if `shrink_threshold=None`,
        else (18.5) p. 653 of [2]. Can be used to identify features used
        for classification.

    within_class_std_dev_ : ndarray of shape (n_features,)
        Pooled or within-class standard deviation of input data.

        The class prior probabilities.

    See Also
    --------
    KNeighborsClassifier : Nearest neighbors classifier.

    Notes
    -----
    When used for text classification with tf-idf vectors, this classifier is
    also known as the Rocchio classifier.

    [1] Tibshirani, R., Hastie, T., Narasimhan, B., & Chu, G. (2002). Diagnosis of
    multiple cancer types by shrunken centroids of gene expression. Proceedings
    of the National Academy of Sciences of the United States of America,
    99(10), 6567-6572. The National Academy of Sciences.

    [2] Hastie, T., Tibshirani, R., Friedman, J. (2009). The Elements of Statistical
    Learning Data Mining, Inference, and Prediction. 2nd Edition. New York, Springer.

    Examples
    --------
    >>> from sklearn.neighbors import NearestCentroid
    >>> clf = NearestCentroid()
    NearestCentroid()
    [1]
    """

        "metric": [StrOptions({"manhattan", "euclidean"})],
        "shrink_threshold": [Interval(Real, 0, None, closed="neither"), None],
        "priors": ["array-like", StrOptions({"empirical", "uniform"})],
    }

        self,
        *,
        shrink_threshold=None,
        priors="uniform",
    ):
        self.shrink_threshold = shrink_threshold

        """
        Fit the NearestCentroid model according to the given training data.

            Note that centroid shrinking cannot be used with sparse matrices.

        Returns
        -------
        """
        # If X is sparse and the metric is "manhattan", store it in a csc
        # format is easier to calculate the median.
        if self.metric == "manhattan":
            X, y = validate_data(self, X, y, accept_sparse=["csc"])
        else:
            ensure_all_finite = (
                "allow-nan" if get_tags(self).input_tags.allow_nan else True
            )
                self,
                X,
                y,
            )
        is_X_sparse = sp.issparse(X)

        y_ind = le.fit_transform(y)
        self.classes_ = classes = le.classes_
        n_classes = classes.size
            )

        if self.priors == "empirical":  # estimate priors from sample
            _, class_counts = np.unique(y, return_inverse=True)  # non-negative ints
            self.class_prior_ = np.bincount(class_counts) / float(len(y))
        elif self.priors == "uniform":
            self.class_prior_ = np.asarray([1 / n_classes] * n_classes)
        else:
            self.class_prior_ = np.asarray(self.priors)

        if (self.class_prior_ < 0).any():
        if not np.isclose(self.class_prior_.sum(), 1.0):
                "The priors do not sum to 1. Normalizing such that it sums to one.",
            )
            self.class_prior_ = self.class_prior_ / self.class_prior_.sum()

        # Mask mapping each class to its members.
        self.centroids_ = np.empty((n_classes, n_features), dtype=np.float64)

        # Number of clusters in each class.
        nk = np.zeros(n_classes)

        for cur_class in range(n_classes):
            center_mask = y_ind == cur_class
            nk[cur_class] = np.sum(center_mask)
            if is_X_sparse:
                center_mask = np.where(center_mask)[0]

            if self.metric == "manhattan":
                # NumPy does not calculate median of sparse matrices.
                if not is_X_sparse:
                    self.centroids_[cur_class] = np.median(X[center_mask], axis=0)
                else:
                    self.centroids_[cur_class] = csc_median_axis_0(X[center_mask])
            else:  # metric == "euclidean"
                self.centroids_[cur_class] = X[center_mask].mean(axis=0)

        # Compute within-class std_dev with unshrunked centroids
        variance = np.array(X - self.centroids_[y_ind], copy=False) ** 2
        self.within_class_std_dev_ = np.array(
            np.sqrt(variance.sum(axis=0) / (n_samples - n_classes)), copy=False
        )
        if any(self.within_class_std_dev_ == 0):
                "self.within_class_std_dev_ has at least 1 zero standard deviation."
                "Inputs within the same classes for at least 1 feature are identical."
            )

        err_msg = "All features have zero variance. Division by zero."
        if is_X_sparse and np.all((X.max(axis=0) - X.min(axis=0)).toarray() == 0):
            raise ValueError(err_msg)
        elif not is_X_sparse and np.all(np.ptp(X, axis=0) == 0):

        dataset_centroid_ = X.mean(axis=0)
        # m parameter for determining deviation
        m = np.sqrt((1.0 / nk) - (1.0 / n_samples))
        # Calculate deviation using the standard deviation of centroids.
        # To deter outliers from affecting the results.
        s = self.within_class_std_dev_ + np.median(self.within_class_std_dev_)
        mm = m.reshape(len(m), 1)  # Reshape to allow broadcasting.
        ms = mm * s
        self.deviations_ = np.array(
            (self.centroids_ - dataset_centroid_) / ms, copy=False
        )
        # Soft thresholding: if the deviation crosses 0 during shrinking,
        # it becomes zero.
        if self.shrink_threshold:
            signs = np.sign(self.deviations_)
            self.deviations_ = np.abs(self.deviations_) - self.shrink_threshold
            np.clip(self.deviations_, 0, None, out=self.deviations_)
            self.deviations_ *= signs
            # Now adjust the centroids using the deviation
            msd = ms * self.deviations_
            self.centroids_ = np.array(dataset_centroid_ + msd, copy=False)

        """Perform classification on an array of test vectors `X`.

        The predicted class `C` for each sample in `X` is returned.

        Returns
        -------
        """
        if np.isclose(self.class_prior_, 1 / len(self.classes_)).all():
            # `validate_data` is called here since we are not calling `super()`
            )
                self,
                X,
            )
            return self.classes_[
                pairwise_distances_argmin(X, self.centroids_, metric=self.metric)
            ]
        else:

        # return discriminant scores, see eq. (18.2) p. 652 of the ESL.
        check_is_fitted(self, "centroids_")

        X_normalized = validate_data(
            self, X, copy=True, reset=False, accept_sparse="csr", dtype=np.float64
        )

        discriminant_score = np.empty(
            (X_normalized.shape[0], self.classes_.size), dtype=np.float64
        )

        mask = self.within_class_std_dev_ != 0
        X_normalized[:, mask] /= self.within_class_std_dev_[mask]
        centroids_normalized = self.centroids_.copy()
        centroids_normalized[:, mask] /= self.within_class_std_dev_[mask]

        for class_idx in range(self.classes_.size):
            distances = pairwise_distances(
                X_normalized, centroids_normalized[[class_idx]], metric=self.metric
            ).ravel()
            distances **= 2
            discriminant_score[:, class_idx] = np.squeeze(
                -distances + 2.0 * np.log(self.class_prior_[class_idx])
            )

        return discriminant_score

    def _check_euclidean_metric(self):
        return self.metric == "euclidean"

    decision_function = available_if(_check_euclidean_metric)(
        DiscriminantAnalysisPredictionMixin.decision_function
    )

    predict_proba = available_if(_check_euclidean_metric)(
        DiscriminantAnalysisPredictionMixin.predict_proba
    )

    predict_log_proba = available_if(_check_euclidean_metric)(
        DiscriminantAnalysisPredictionMixin.predict_log_proba
    )

        self,
        *,
    ):

        """

        Returns
        -------
        """
        else:
            )
                self,
                X,
                y,
            )

            )

        else:

            )

                else:

        )
            )

        ms = mm * s
        )

        Returns
        -------
        """
            )
                self,
                X,
            )
            ]
        else:

        )

        )

            ).ravel()
            )

class KernelDensity(BaseEstimator):
    """Kernel Density Estimation.

    Read more in the :ref:`User Guide <kernel_density>`.

    bandwidth : float or {"scott", "silverman"}, default=1.0
        The bandwidth of the kernel. If bandwidth is a float, it defines the
        bandwidth of the kernel. If bandwidth is a string, one of the estimation
        methods is implemented.

    algorithm : {'kd_tree', 'ball_tree', 'auto'}, default='auto'
        The tree algorithm to use.

    kernel : {'gaussian', 'tophat', 'epanechnikov', 'exponential', 'linear', \
                 'cosine'}, default='gaussian'
        The kernel to use.

    metric : str, default='euclidean'
        Metric to use for distance computation. See the
        values.

        Not all metrics are valid with all algorithms: refer to the
        documentation of :class:`BallTree` and :class:`KDTree`. Note that the
        normalization of the density output is correct only for the Euclidean
        distance metric.

    atol : float, default=0
        The desired absolute tolerance of the result.  A larger tolerance will
        generally lead to faster execution.

    rtol : float, default=0
        The desired relative tolerance of the result.  A larger tolerance will

    breadth_first : bool, default=True
        If true (default), use a breadth-first approach to the problem.
        Otherwise use a depth-first approach.

    leaf_size : int, default=40
        Specify the leaf size of the underlying tree.  See :class:`BallTree`
        or :class:`KDTree` for details.

        Additional parameters to be passed to the tree for use with the
        metric.  For more information, see the documentation of
        :class:`BallTree` or :class:`KDTree`.

    tree_ : ``BinaryTree`` instance
        The tree algorithm for fast generalized N-point problems.

    bandwidth_ : float
        Value of the bandwidth, given directly by the bandwidth parameter or
        estimated using the 'scott' or 'silverman' method.

    See Also
    --------
    sklearn.neighbors.KDTree : K-dimensional tree for fast generalized N-point
        problems.
    sklearn.neighbors.BallTree : Ball tree for fast generalized N-point
        problems.

    Examples
    --------
    Compute a gaussian kernel density estimate with a fixed bandwidth.

    >>> from sklearn.neighbors import KernelDensity
    >>> X = rng.random_sample((100, 3))
    >>> kde = KernelDensity(kernel='gaussian', bandwidth=0.5).fit(X)
    >>> log_density = kde.score_samples(X[:3])
    >>> log_density
    array([-1.52955942, -1.51462041, -1.60244657])
    """

        "bandwidth": [
            Interval(Real, 0, None, closed="neither"),
            StrOptions({"scott", "silverman"}),
        ],
        "algorithm": [StrOptions(set(TREE_DICT.keys()) | {"auto"})],
        "kernel": [StrOptions(set(VALID_KERNELS))],
        "metric": [
                set(itertools.chain(*[VALID_METRICS[alg] for alg in TREE_DICT.keys()]))
            )
        ],
        "atol": [Interval(Real, 0, None, closed="left")],
        "rtol": [Interval(Real, 0, None, closed="left")],
        "breadth_first": ["boolean"],
        "metric_params": [None, dict],
    }

        self,
        *,
        bandwidth=1.0,
        kernel="gaussian",
        atol=0,
        rtol=0,
        breadth_first=True,
        leaf_size=40,
    ):
        self.bandwidth = bandwidth
        self.atol = atol
        self.rtol = rtol
        self.breadth_first = breadth_first

    def _choose_algorithm(self, algorithm, metric):
        # given the algorithm string + metric string, choose the optimal
        # algorithm to compute the result.
        if algorithm == "auto":
            # use KD Tree if possible
            if metric in KDTree.valid_metrics:
                return "kd_tree"
            elif metric in BallTree.valid_metrics:
                return "ball_tree"
        else:  # kd_tree or ball_tree
            if metric not in TREE_DICT[algorithm].valid_metrics:
                    "invalid metric for {0}: '{1}'".format(TREE_DICT[algorithm], metric)
                )
            return algorithm

        # KernelDensity.metric is not validated yet
    )
        """Fit the Kernel Density model on the data.

            List of n_features-dimensional data points.  Each row
            corresponds to a single data point.

        y : None
            :class:`~sklearn.pipeline.Pipeline`.

            List of sample weights attached to the data X.

        Returns
        -------
        """
        algorithm = self._choose_algorithm(self.algorithm, self.metric)

        if isinstance(self.bandwidth, str):
            if self.bandwidth == "scott":
                self.bandwidth_ = X.shape[0] ** (-1 / (X.shape[1] + 4))
            elif self.bandwidth == "silverman":
                self.bandwidth_ = (X.shape[0] * (X.shape[1] + 2) / 4) ** (
                    -1 / (X.shape[1] + 4)
                )
        else:
            self.bandwidth_ = self.bandwidth

        X = validate_data(self, X, order="C", dtype=np.float64)

            sample_weight = _check_sample_weight(
                sample_weight, X, dtype=np.float64, ensure_non_negative=True
            )

        kwargs = self.metric_params
        if kwargs is None:
            kwargs = {}
        self.tree_ = TREE_DICT[algorithm](
            X,
            leaf_size=self.leaf_size,
            **kwargs,
        )

        """Compute the log-likelihood of each sample under the model.

            An array of points to query.  Last dimension should match dimension
            of training data (n_features).

        Returns
        -------
        density : ndarray of shape (n_samples,)
            Log-likelihood of each sample in `X`. These are normalized to be
            probability densities, so values will be low for high-dimensional
            data.
        """
        # The returned density is normalized to the number of points.
        # For it to be a probability, we must scale it.  For this reason
        # we'll also scale atol.
        X = validate_data(self, X, order="C", dtype=np.float64, reset=False)
        if self.tree_.sample_weight is None:
            N = self.tree_.data.shape[0]
        else:
            N = self.tree_.sum_weight
        atol_N = self.atol * N
        log_density = self.tree_.kernel_density(
            X,
            h=self.bandwidth_,
            atol=atol_N,
            rtol=self.rtol,
            breadth_first=self.breadth_first,
            return_log=True,
        )
        log_density -= np.log(N)
        return log_density

        """Compute the total log-likelihood under the model.

        y : None

        Returns
        -------
        logprob : float
            Total log-likelihood of the data in X. This is normalized to be a
            probability density, so the value will be low for high-dimensional
            data.
        """
        return np.sum(self.score_samples(X))

    def sample(self, n_samples=1, random_state=None):
        """Generate random samples from the model.

        Currently, this is implemented only for gaussian and tophat kernels.

            Number of samples to generate.

        random_state : int, RandomState instance or None, default=None
            Determines random number generation used to generate
            random samples. Pass an int for reproducible results
            across multiple function calls.

        Returns
        -------
            List of samples.
        """
        # TODO: implement sampling for other valid kernel shapes
        if self.kernel not in ["gaussian", "tophat"]:
            raise NotImplementedError()

        data = np.asarray(self.tree_.data)

        u = rng.uniform(0, 1, size=n_samples)
            i = (u * data.shape[0]).astype(np.int64)
        else:
            cumsum_weight = np.cumsum(np.asarray(self.tree_.sample_weight))
            sum_weight = cumsum_weight[-1]
            i = np.searchsorted(cumsum_weight, u * sum_weight)
        if self.kernel == "gaussian":
            return np.atleast_2d(rng.normal(data[i], self.bandwidth_))

        elif self.kernel == "tophat":
            # we first draw points from a d-dimensional normal distribution,
            # then use an incomplete gamma function to map them to a uniform
            # d-dimensional tophat distribution.
            dim = data.shape[1]
            X = rng.normal(size=(n_samples, dim))
            s_sq = row_norms(X, squared=True)
            correction = (
                gammainc(0.5 * dim, 0.5 * s_sq) ** (1.0 / dim)
                * self.bandwidth_
                / np.sqrt(s_sq)
            )
            return data[i] + X * correction[:, np.newaxis]

        self,
        *,
        atol=0,
        rtol=0,
    ):

                )

        y : None

        Returns
        -------
        """

                )
        else:

            )

            kwargs = {}
            X,
            **kwargs,
        )

        Returns
        -------
            data.
        """
        else:
            X,
        )

        y : None

        Returns
        -------
            data.
        """

        Returns
        -------
        """

        else:

            )

def _parse_metric(metric: str, dtype=None):
    """
    Helper function for properly building a type-specialized DistanceMetric instances.

    Constructs a type-specialized DistanceMetric instance from a string
    beginning with "DM_" while allowing a pass-through for other metric-specifying
    strings. This is necessary since we wish to parameterize dtype independent of
    metric, yet DistanceMetric requires it for construction.

    """
    if metric[:3] == "DM_":
        return DistanceMetric.get_metric(metric[3:], dtype=dtype)
    return metric

def _generate_test_params_for(metric: str, n_features: int):
    """Return list of DistanceMetric kwargs for tests."""

        return [
        ]

    if metric == "mahalanobis":
        A = rng.rand(n_features, n_features)
        # Make the matrix symmetric positive definite
        VI = A + A.T + 3 * np.eye(n_features)
        return [dict(VI=VI)]

    # In those cases, no kwargs are needed.

def _weight_func(dist):
    """Weight function to replace lambda d: d ** -2.
    The lambda function is not valid because:
    if d==0 then 0^-2 is not valid."""

    # Dist could be multidimensional, flatten it so all values
    # can be looped
        retval = 1.0 / dist
    return retval**2

def test_unsupervised_kneighbors(
    n_query_pts,
    query_is_train,
    metric,
):
    # The different algorithms must return identical results
    # on their common metrics, with and without returning
    # distances

    metric = _parse_metric(metric, global_dtype)

    # Redefining the rng locally to use the same generated X
    local_rng = np.random.RandomState(0)
    X = local_rng.rand(n_samples, n_features).astype(global_dtype, copy=False)

    query = (
        X
        if query_is_train
        else local_rng.rand(n_query_pts, n_features).astype(global_dtype, copy=False)
    )

    results_nodist = []
    results = []

    for algorithm in ALGORITHMS:
        if isinstance(metric, DistanceMetric) and global_dtype == np.float32:
            if "tree" in algorithm:  # pragma: nocover
                pytest.skip(
                    "Neither KDTree nor BallTree support 32-bit distance metric"
                    " objects."
                )
        neigh = neighbors.NearestNeighbors(
            n_neighbors=n_neighbors, algorithm=algorithm, metric=metric
        )
        neigh.fit(X)

        results_nodist.append(neigh.kneighbors(query, return_distance=False))
        results.append(neigh.kneighbors(query, return_distance=True))

    for i in range(len(results) - 1):
        algorithm = ALGORITHMS[i]
        next_algorithm = ALGORITHMS[i + 1]

        indices_no_dist = results_nodist[i]
        distances, next_distances = results[i][0], results[i + 1][0]
        indices, next_indices = results[i][1], results[i + 1][1]
            indices_no_dist,
            indices,
            err_msg=(
                f"The '{algorithm}' algorithm returns different"
                "indices depending on 'return_distances'."
            ),
        )
            indices,
            next_indices,
            err_msg=(
                f"The '{algorithm}' and '{next_algorithm}' "
                "algorithms return different indices."
            ),
        )
            distances,
            next_distances,
            err_msg=(
                "algorithms return different distances."
            ),
            atol=1e-6,
        )

def test_neigh_predictions_algorithm_agnosticity(
    metric,
    radius,
    NeighborsMixinSubclass,
):
    # The different algorithms must return identical predictions results
    # on their common metrics.

    if isinstance(metric, DistanceMetric):
        if "Classifier" in NeighborsMixinSubclass.__name__:
            pytest.skip(
                "Metrics of type `DistanceMetric` are not yet supported for"
                " classifiers."
            )
        if "Radius" in NeighborsMixinSubclass.__name__:
                " radius-neighbor estimators."
            )

    y = local_rng.randint(3, size=n_samples)

    query = local_rng.rand(n_query_pts, n_features).astype(global_dtype, copy=False)

    predict_results = []

    parameter = (
        n_neighbors if issubclass(NeighborsMixinSubclass, KNeighborsMixin) else radius
    )

                )
        neigh = NeighborsMixinSubclass(parameter, algorithm=algorithm, metric=metric)
        neigh.fit(X, y)

        predict_results.append(neigh.predict(query))

    for i in range(len(predict_results) - 1):

        predictions, next_predictions = predict_results[i], predict_results[i + 1]

            predictions,
            next_predictions,
            err_msg=(
                "algorithms return different predictions."
            ),
        )

def test_unsupervised_inputs(global_dtype, KNeighborsMixinSubclass):
    # Test unsupervised inputs for neighbors estimators

    X = rng.random_sample((10, 3)).astype(global_dtype, copy=False)
    y = rng.randint(3, size=10)
    nbrs_fid = neighbors.NearestNeighbors(n_neighbors=1)
    nbrs_fid.fit(X)

    dist1, ind1 = nbrs_fid.kneighbors(X)

    nbrs = KNeighborsMixinSubclass(n_neighbors=1)

    for data in (nbrs_fid, neighbors.BallTree(X), neighbors.KDTree(X)):
        nbrs.fit(data, y)

        dist2, ind2 = nbrs.kneighbors(X)

        assert_allclose(dist1, dist2)
        assert_array_equal(ind1, ind2)

def test_not_fitted_error_gets_raised():
    X = [[1]]
    neighbors_ = neighbors.NearestNeighbors()
        neighbors_.kneighbors_graph(X)
        neighbors_.radius_neighbors_graph(X)

def check_precomputed(make_train_test, estimators):
    """Tests unsupervised NearestNeighbors with a distance matrix."""
    # Note: smaller samples may result in spurious test success
    X = rng.random_sample((10, 4))
    DXX, DYX = make_train_test(X, Y)
    for method in [
        "kneighbors",
    ]:
        # TODO: also test radius_neighbors, but requires different assertion

        # As a feature matrix (n_samples by n_features)
        nbrs_X = neighbors.NearestNeighbors(n_neighbors=3)
        nbrs_X.fit(X)
        dist_X, ind_X = getattr(nbrs_X, method)(Y)

        # As a dense distance matrix (n_samples by n_samples)
        nbrs_D = neighbors.NearestNeighbors(
            n_neighbors=3, algorithm="brute", metric="precomputed"
        )
        nbrs_D.fit(DXX)
        dist_D, ind_D = getattr(nbrs_D, method)(DYX)
        assert_allclose(dist_X, dist_D)
        assert_array_equal(ind_X, ind_D)

        # Check auto works too
            n_neighbors=3, algorithm="auto", metric="precomputed"
        )

        # Check X=None in prediction
        dist_X, ind_X = getattr(nbrs_X, method)(None)
        dist_D, ind_D = getattr(nbrs_D, method)(None)

        # Must raise a ValueError if the matrix is not of correct shape
            getattr(nbrs_D, method)(X)

    target = np.arange(X.shape[0])
    for Est in estimators:
        est = Est(metric="euclidean")
        est.radius = est.n_neighbors = 1
        pred_X = est.fit(X, target).predict(Y)
        est.metric = "precomputed"
        pred_D = est.fit(DXX, target).predict(DYX)
        assert_allclose(pred_X, pred_D)

def test_precomputed_dense():
    def make_train_test(X_train, X_test):
        return (
            metrics.pairwise_distances(X_train),
            metrics.pairwise_distances(X_test, X_train),
        )

        neighbors.KNeighborsClassifier,
        neighbors.KNeighborsRegressor,
        neighbors.RadiusNeighborsClassifier,
        neighbors.RadiusNeighborsRegressor,
    ]
    check_precomputed(make_train_test, estimators)

def test_precomputed_sparse_knn(fmt):
        nn = neighbors.NearestNeighbors(n_neighbors=3 + 1).fit(X_train)
        return (
            nn.kneighbors_graph(X_train, mode="distance").asformat(fmt),
            nn.kneighbors_graph(X_test, mode="distance").asformat(fmt),
        )

    # We do not test RadiusNeighborsClassifier and RadiusNeighborsRegressor
    # since the precomputed neighbors graph is built with k neighbors only.
    ]

def test_precomputed_sparse_radius(fmt):
        nn = neighbors.NearestNeighbors(radius=1).fit(X_train)
        return (
            nn.radius_neighbors_graph(X_train, mode="distance").asformat(fmt),
            nn.radius_neighbors_graph(X_test, mode="distance").asformat(fmt),
        )

    # We do not test KNeighborsClassifier and KNeighborsRegressor
    # since the precomputed neighbors graph is built with a radius.
    ]

def test_is_sorted_by_data(csr_container):
    # Test that _is_sorted_by_data works as expected. In CSR sparse matrix,
    # entries in each row can be sorted by indices, by data, or unsorted.
    # _is_sorted_by_data should return True when entries are sorted by data,
    # and False in all other cases.

    # Test with sorted single row sparse array
    X = csr_container(np.arange(10).reshape(1, 10))
    assert _is_sorted_by_data(X)
    # Test with unsorted 1D array
    X[0, 2] = 5
    assert not _is_sorted_by_data(X)

    # Test when the data is sorted in each sample, but not necessarily
    # between samples
    X = csr_container([[0, 1, 2], [3, 0, 0], [3, 4, 0], [1, 0, 2]])

    # Test with duplicates entries in X.indptr
    data, indices, indptr = [0, 4, 2, 2], [0, 1, 1, 1], [0, 2, 2, 4]
    X = csr_container((data, indices, indptr), shape=(3, 3))

def test_sort_graph_by_row_values(function, csr_container):
    # Test that sort_graph_by_row_values returns a graph sorted by row values
    X = csr_container(np.abs(np.random.RandomState(42).randn(10, 10)))
    Xt = function(X)
    assert _is_sorted_by_data(Xt)

    # test with a different number of nonzero entries for each sample
    mask = np.random.RandomState(42).randint(2, size=(10, 10))
    X[mask == 1] = 0

def test_sort_graph_by_row_values_copy(csr_container):
    # Test if the sorting is done inplace if X is CSR, so that Xt is X.
    X_ = csr_container(np.abs(np.random.RandomState(42).randn(10, 10)))
    assert not _is_sorted_by_data(X_)

    # sort_graph_by_row_values is done inplace if copy=False
    X = X_.copy()
    assert sort_graph_by_row_values(X).data is X.data

    assert sort_graph_by_row_values(X, copy=False).data is X.data

    assert sort_graph_by_row_values(X, copy=True).data is not X.data

    # _check_precomputed is never done inplace
    assert _check_precomputed(X).data is not X.data

    # do not raise if X is not CSR and copy=True
    sort_graph_by_row_values(X.tocsc(), copy=True)

    # raise if X is not CSR and copy=False
    with pytest.raises(ValueError, match="Use copy=True to allow the conversion"):
        sort_graph_by_row_values(X.tocsc(), copy=False)

def test_sort_graph_by_row_values_warning(csr_container):
    # Test that the parameter warn_when_not_sorted works as expected.

    # warning
    with pytest.warns(EfficiencyWarning, match="was not sorted by row values"):
        sort_graph_by_row_values(X, copy=True)
        sort_graph_by_row_values(X, copy=True, warn_when_not_sorted=True)
        _check_precomputed(X)

    # no warning
        sort_graph_by_row_values(X, copy=True, warn_when_not_sorted=False)

def test_sort_graph_by_row_values_bad_sparse_format(sparse_container):
    # Test that sort_graph_by_row_values and _check_precomputed error on bad formats
    X = sparse_container(np.abs(np.random.RandomState(42).randn(10, 10)))
    with pytest.raises(TypeError, match="format is not supported"):
        sort_graph_by_row_values(X)

def test_precomputed_sparse_invalid(csr_container):
    dist = np.array([[0.0, 2.0, 1.0], [2.0, 0.0, 3.0], [1.0, 3.0, 0.0]])
    dist_csr = csr_container(dist)
    neigh = neighbors.NearestNeighbors(n_neighbors=1, metric="precomputed")
    neigh.fit(dist_csr)
    neigh.kneighbors(None, n_neighbors=1)
    neigh.kneighbors(np.array([[0.0, 0.0, 0.0]]), n_neighbors=2)

    # Ensures enough number of nearest neighbors
    dist = np.array([[0.0, 2.0, 0.0], [2.0, 0.0, 3.0], [0.0, 3.0, 0.0]])
    msg = "2 neighbors per samples are required, but some samples have only 1"
        neigh.kneighbors(None, n_neighbors=1)

    # Checks error with inconsistent distance matrix
    dist = np.array([[5.0, 2.0, 1.0], [-2.0, 0.0, 3.0], [1.0, 3.0, 0.0]])
    msg = "Negative values in data passed to precomputed distance matrix."
        neigh.kneighbors(dist_csr, n_neighbors=1)

def test_precomputed_cross_validation():
    # Ensure array is split correctly
    X = rng.rand(20, 2)
    y = rng.randint(3, size=20)
    for Est in (
    ):
        metric_score = cross_val_score(Est(), X, y)
        precomp_score = cross_val_score(Est(metric="precomputed"), D, y)
        assert_array_equal(metric_score, precomp_score)

def test_unsupervised_radius_neighbors(
    global_dtype, n_samples=20, n_features=5, n_query_pts=2, radius=0.5, random_state=0
):
    # Test unsupervised radius-based query
    rng = np.random.RandomState(random_state)

    X = rng.rand(n_samples, n_features).astype(global_dtype, copy=False)

    test = rng.rand(n_query_pts, n_features).astype(global_dtype, copy=False)

    for p in P:
        results = []

        for algorithm in ALGORITHMS:
            neigh = neighbors.NearestNeighbors(radius=radius, algorithm=algorithm, p=p)
            neigh.fit(X)

            ind1 = neigh.radius_neighbors(test, return_distance=False)

            # sort the results: this is not done automatically for
            # radius searches
            dist, ind = neigh.radius_neighbors(test, return_distance=True)
            for d, i, i1 in zip(dist, ind, ind1):
                j = d.argsort()
                d[:] = d[j]
                i[:] = i[j]
                i1[:] = i1[j]
            results.append((dist, ind))

            assert_allclose(np.concatenate(list(ind)), np.concatenate(list(ind1)))

        for i in range(len(results) - 1):
                np.concatenate(list(results[i][0])),
                np.concatenate(list(results[i + 1][0])),
            )
                np.concatenate(list(results[i][1])),
                np.concatenate(list(results[i + 1][1])),
            )

def test_kneighbors_classifier(
    algorithm,
    weights,
    n_samples=40,
    n_features=5,
    n_test_pts=10,
    n_neighbors=5,
):
    # Test k-neighbors classification
    X = 2 * rng.rand(n_samples, n_features).astype(global_dtype, copy=False) - 1
    y = ((X**2).sum(axis=1) < 0.5).astype(int)
    y_str = y.astype(str)

    knn = neighbors.KNeighborsClassifier(
        n_neighbors=n_neighbors, weights=weights, algorithm=algorithm
    )
    knn.fit(X, y)
    epsilon = 1e-5 * (2 * rng.rand(1, n_features) - 1)
    y_pred = knn.predict(X[:n_test_pts] + epsilon)
    assert_array_equal(y_pred, y[:n_test_pts])
    # Test prediction with y_str
    knn.fit(X, y_str)
    assert_array_equal(y_pred, y_str[:n_test_pts])

def test_kneighbors_classifier_float_labels(
):

    knn = neighbors.KNeighborsClassifier(n_neighbors=n_neighbors)
    knn.fit(X, y.astype(float))

def test_kneighbors_classifier_predict_proba(global_dtype):
    # Test KNeighborsClassifier.predict_proba() method
        [[0, 2, 0], [0, 2, 1], [2, 0, 0], [2, 2, 0], [0, 0, 2], [0, 0, 1]]
    ).astype(global_dtype, copy=False)
    y = np.array([4, 4, 5, 5, 1, 1])
    cls = neighbors.KNeighborsClassifier(n_neighbors=3, p=1)  # cityblock dist
    cls.fit(X, y)
    y_prob = cls.predict_proba(X)
    real_prob = (
        np.array(
            [
                [0, 2, 1],
                [1, 2, 0],
                [1, 0, 2],
                [0, 1, 2],
                [2, 1, 0],
                [2, 1, 0],
            ]
        )
        / 3.0
    )
    assert_array_equal(real_prob, y_prob)
    # Check that it also works with non integer labels
    cls.fit(X, y.astype(str))
    # Check that it works with weights='distance'
    cls = neighbors.KNeighborsClassifier(n_neighbors=2, p=1, weights="distance")
    y_prob = cls.predict_proba(np.array([[0, 2, 0], [2, 2, 2]]))
    real_prob = np.array([[0, 1, 0], [0, 0.4, 0.6]])
    assert_allclose(real_prob, y_prob)

def test_radius_neighbors_classifier(
    weights,
    radius=0.5,
):
    # Test radius-based classification
    y = ((X**2).sum(axis=1) < radius).astype(int)

    neigh = neighbors.RadiusNeighborsClassifier(
        radius=radius, weights=weights, algorithm=algorithm
    )
    neigh.fit(X, y)
    y_pred = neigh.predict(X[:n_test_pts] + epsilon)
    neigh.fit(X, y_str)

def test_radius_neighbors_classifier_when_no_neighbors(
    global_dtype, algorithm, weights, outlier_label
):
    # Test radius-based classifier when no neighbors found.
    # In this case it should rise an informative exception

    X = np.array([[1.0, 1.0], [2.0, 2.0]], dtype=global_dtype)
    y = np.array([1, 2])
    radius = 0.1

    # no outliers
    z1 = np.array([[1.01, 1.01], [2.01, 2.01]], dtype=global_dtype)

    # one outlier
    z2 = np.array([[1.01, 1.01], [1.4, 1.4]], dtype=global_dtype)

    rnc = neighbors.RadiusNeighborsClassifier
    clf = rnc(
        algorithm=algorithm,
    )
    assert_array_equal(np.array([1, 2]), clf.predict(z1))
    if outlier_label is None:
            clf.predict(z2)

def test_radius_neighbors_classifier_outlier_labeling(global_dtype, algorithm, weights):
    # Test radius-based classifier when no neighbors found and outliers
    # are labeled.

        [[1.0, 1.0], [2.0, 2.0], [0.99, 0.99], [0.98, 0.98], [2.01, 2.01]],
        dtype=global_dtype,
    )
    y = np.array([1, 2, 1, 1, 2])

    z2 = np.array([[1.4, 1.4], [1.01, 1.01], [2.01, 2.01]], dtype=global_dtype)

    correct_labels1 = np.array([1, 2])
    correct_labels2 = np.array([-1, 1, 2])
    outlier_proba = np.array([0, 0])

    clf = neighbors.RadiusNeighborsClassifier(
        radius=radius, weights=weights, algorithm=algorithm, outlier_label=-1
    )
    assert_array_equal(correct_labels1, clf.predict(z1))
    with pytest.warns(UserWarning, match="Outlier label -1 is not in training classes"):
        assert_array_equal(correct_labels2, clf.predict(z2))
        assert_allclose(outlier_proba, clf.predict_proba(z2)[0])

    # test outlier_labeling of using predict_proba()
    RNC = neighbors.RadiusNeighborsClassifier
    X = np.array([[0], [1], [2], [3], [4], [5], [6], [7], [8], [9]], dtype=global_dtype)
    y = np.array([0, 2, 2, 1, 1, 1, 3, 3, 3, 3])

    # test outlier_label scalar verification
    def check_array_exception():
        clf = RNC(radius=1, outlier_label=[[5]])

        check_array_exception()

    # test invalid outlier_label dtype
    def check_dtype_exception():
        clf = RNC(radius=1, outlier_label="a")

        check_dtype_exception()

    # test most frequent
    clf = RNC(radius=1, outlier_label="most_frequent")
    proba = clf.predict_proba([[1], [15]])
    assert_array_equal(proba[1, :], [0, 0, 0, 1])

    # test manual label in y
    clf = RNC(radius=1, outlier_label=1)
    assert_array_equal(proba[1, :], [0, 1, 0, 0])
    pred = clf.predict([[1], [15]])
    assert_array_equal(pred, [2, 1])

    # test manual label out of y warning
    def check_warning():
        clf = RNC(radius=1, outlier_label=4)
        clf.predict_proba([[1], [15]])

    with pytest.warns(UserWarning):
        check_warning()

    # test multi output same outlier label
    y_multi = [
        [0, 1],
        [2, 1],
        [2, 2],
        [1, 2],
        [1, 2],
        [1, 3],
        [3, 3],
        [3, 3],
        [3, 0],
        [3, 0],
    ]
    clf.fit(X, y_multi)
    proba = clf.predict_proba([[7], [15]])
    assert_array_equal(proba[1][1, :], [0, 1, 0, 0])
    pred = clf.predict([[7], [15]])
    assert_array_equal(pred[1, :], [1, 1])

    # test multi output different outlier label
    y_multi = [
        [0, 0],
        [2, 2],
        [2, 2],
        [1, 1],
        [1, 1],
        [1, 1],
        [3, 3],
        [3, 3],
        [3, 3],
        [3, 3],
    ]
    clf = RNC(radius=1, outlier_label=[0, 1])
    assert_array_equal(proba[0][1, :], [1, 0, 0, 0])
    assert_array_equal(pred[1, :], [0, 1])

    # test inconsistent outlier label list length
    def check_exception():
        clf = RNC(radius=1, outlier_label=[0, 1, 2])
        clf.fit(X, y_multi)

        check_exception()

def test_radius_neighbors_classifier_zero_distance():
    # Test radius-based classifier, when distance to a sample is zero.

    X = np.array([[1.0, 1.0], [2.0, 2.0]])

    z1 = np.array([[1.01, 1.01], [2.0, 2.0]])

    weight_func = _weight_func

        for weights in ["uniform", "distance", weight_func]:
            clf = neighbors.RadiusNeighborsClassifier(
                radius=radius, weights=weights, algorithm=algorithm
            )
            with np.errstate(invalid="ignore"):
                # Ignore the warning raised in _weight_func when making
                # predictions with null distances resulting in np.inf values.
                assert_array_equal(correct_labels1, clf.predict(z1))

def test_neighbors_regressors_zero_distance():
    # Test radius-based regressor, when distance to a sample is zero.

    X = np.array([[1.0, 1.0], [1.0, 1.0], [2.0, 2.0], [2.5, 2.5]])
    y = np.array([1.0, 1.5, 2.0, 0.0])
    radius = 0.2
    z = np.array([[1.1, 1.1], [2.0, 2.0]])

    rnn_correct_labels = np.array([1.25, 2.0])

    knn_correct_unif = np.array([1.25, 1.0])
    knn_correct_dist = np.array([1.25, 2.0])

        # we don't test for weights=_weight_func since user will be expected
        # to handle zero distances themselves in the function.
        for weights in ["uniform", "distance"]:
            rnn = neighbors.RadiusNeighborsRegressor(
            )
            rnn.fit(X, y)
            assert_allclose(rnn_correct_labels, rnn.predict(z))

        for weights, corr_labels in zip(
            ["uniform", "distance"], [knn_correct_unif, knn_correct_dist]
        ):
            knn = neighbors.KNeighborsRegressor(
                n_neighbors=2, weights=weights, algorithm=algorithm
            )
            knn.fit(X, y)
            assert_allclose(corr_labels, knn.predict(z))

def test_radius_neighbors_boundary_handling():
    """Test whether points lying on boundary are handled consistently

    Also ensures that even with only one query point, an object array
    is returned rather than a 2d array.
    """

    X = np.array([[1.5], [3.0], [3.01]])
    radius = 3.0

        nbrs = neighbors.NearestNeighbors(radius=radius, algorithm=algorithm).fit(X)
        results = nbrs.radius_neighbors([[0.0]], return_distance=False)
        assert results.shape == (1,)
        assert results.dtype == object
        assert_array_equal(results[0], [0, 1])

def test_radius_neighbors_returns_array_of_objects(csr_container):
    # check that we can pass precomputed distances to
    # NearestNeighbors.radius_neighbors()
    # https://github.com/scikit-learn/scikit-learn/issues/16036
    X = csr_container(np.ones((4, 4)))
    X.setdiag([0, 0, 0, 0])

    nbrs = neighbors.NearestNeighbors(
        radius=0.5, algorithm="auto", leaf_size=30, metric="precomputed"
    ).fit(X)
    neigh_dist, neigh_ind = nbrs.radius_neighbors(X, return_distance=True)

    expected_dist = np.empty(X.shape[0], dtype=object)
    expected_dist[:] = [np.array([0]), np.array([0]), np.array([0]), np.array([0])]
    expected_ind = np.empty(X.shape[0], dtype=object)
    expected_ind[:] = [np.array([0]), np.array([1]), np.array([2]), np.array([3])]

    assert_array_equal(neigh_dist, expected_dist)
    assert_array_equal(neigh_ind, expected_ind)

def test_query_equidistant_kth_nn(algorithm):
    # For several candidates for the k-th nearest neighbor position,
    # the first candidate should be chosen
    query_point = np.array([[0, 0]])
    equidistant_points = np.array([[1, 0], [0, 1], [-1, 0], [0, -1]])
    # The 3rd and 4th points should not replace the 2nd point
    # for the 2th nearest neighbor position
    k = 2
    knn_indices = np.array([[0, 1]])
    nn = neighbors.NearestNeighbors(algorithm=algorithm).fit(equidistant_points)
    indices = np.sort(nn.kneighbors(query_point, n_neighbors=k, return_distance=False))
    assert_array_equal(indices, knn_indices)

def test_radius_neighbors_sort_results(algorithm, metric):
    # Test radius_neighbors[_graph] output when sort_result is True

    metric = _parse_metric(metric, np.float64)
            "Metrics of type `DistanceMetric` are not yet supported for radius-neighbor"
            " estimators."
        )
    X = rng.random_sample((n_samples, 4))

        X = neighbors.radius_neighbors_graph(X, radius=np.inf, mode="distance")
    model = neighbors.NearestNeighbors(algorithm=algorithm, metric=metric)
    model.fit(X)

    # self.radius_neighbors
    distances, indices = model.radius_neighbors(X=X, radius=np.inf, sort_results=True)
    for ii in range(n_samples):
        assert_array_equal(distances[ii], np.sort(distances[ii]))

    # sort_results=True and return_distance=False
    if metric != "precomputed":  # no need to raise with precomputed graph
        with pytest.raises(ValueError, match="return_distance must be True"):
            model.radius_neighbors(
                X=X, radius=np.inf, sort_results=True, return_distance=False
            )

    # self.radius_neighbors_graph
    graph = model.radius_neighbors_graph(
        X=X, radius=np.inf, mode="distance", sort_results=True
    )
    assert _is_sorted_by_data(graph)

def test_RadiusNeighborsClassifier_multioutput():
    # Test k-NN classifier on multioutput data
    n_output = 3

    y = rng.randint(0, 3, (n_samples, n_output))

    weights = [None, "uniform", "distance", _weight_func]

    for algorithm, weights in product(ALGORITHMS, weights):
        # Stack single output prediction
        y_pred_so = []
        for o in range(n_output):
            rnn = neighbors.RadiusNeighborsClassifier(
                weights=weights, algorithm=algorithm
            )
            rnn.fit(X_train, y_train[:, o])
            y_pred_so.append(rnn.predict(X_test))

        y_pred_so = np.vstack(y_pred_so).T
        assert y_pred_so.shape == y_test.shape

        # Multioutput prediction
        rnn_mo = neighbors.RadiusNeighborsClassifier(
            weights=weights, algorithm=algorithm
        )
        rnn_mo.fit(X_train, y_train)
        y_pred_mo = rnn_mo.predict(X_test)

        assert y_pred_mo.shape == y_test.shape
        assert_array_equal(y_pred_mo, y_pred_so)

def test_kneighbors_classifier_sparse(
    n_samples=40, n_features=5, n_test_pts=10, n_neighbors=5, random_state=0
):
    # Test k-NN classifier on sparse matrices
    # Like the above, but with various types of sparse matrices
    X = 2 * rng.rand(n_samples, n_features) - 1
    X *= X > 0.2

    for sparsemat in SPARSE_TYPES:
        knn = neighbors.KNeighborsClassifier(n_neighbors=n_neighbors, algorithm="auto")
        knn.fit(sparsemat(X), y)
        epsilon = 1e-5 * (2 * rng.rand(1, n_features) - 1)
        for sparsev in SPARSE_TYPES + (np.asarray,):
            X_eps = sparsev(X[:n_test_pts] + epsilon)
            y_pred = knn.predict(X_eps)
            assert_array_equal(y_pred, y[:n_test_pts])

def test_KNeighborsClassifier_multioutput():
    n_features = 5

        y_pred_proba_so = []
            knn = neighbors.KNeighborsClassifier(weights=weights, algorithm=algorithm)
            knn.fit(X_train, y_train[:, o])
            y_pred_so.append(knn.predict(X_test))
            y_pred_proba_so.append(knn.predict_proba(X_test))

        assert len(y_pred_proba_so) == n_output

        knn_mo = neighbors.KNeighborsClassifier(weights=weights, algorithm=algorithm)
        knn_mo.fit(X_train, y_train)
        y_pred_mo = knn_mo.predict(X_test)

        # Check proba
        y_pred_proba_mo = knn_mo.predict_proba(X_test)
        assert len(y_pred_proba_mo) == n_output

        for proba_mo, proba_so in zip(y_pred_proba_mo, y_pred_proba_so):
            assert_array_equal(proba_mo, proba_so)

def test_kneighbors_regressor(
    n_samples=40, n_features=5, n_test_pts=10, n_neighbors=3, random_state=0
):
    # Test k-neighbors regression
    y = np.sqrt((X**2).sum(1))
    y /= y.max()

    y_target = y[:n_test_pts]

                n_neighbors=n_neighbors, weights=weights, algorithm=algorithm
            )
            epsilon = 1e-5 * (2 * rng.rand(1, n_features) - 1)
            y_pred = knn.predict(X[:n_test_pts] + epsilon)
            assert np.all(abs(y_pred - y_target) < 0.3)

def test_KNeighborsRegressor_multioutput_uniform_weight():
    # Test k-neighbors in multi-output regression with uniform weight
    n_output = 4

    y = rng.rand(n_samples, n_output)

    for algorithm, weights in product(ALGORITHMS, [None, "uniform"]):
        knn = neighbors.KNeighborsRegressor(weights=weights, algorithm=algorithm)
        knn.fit(X_train, y_train)

        neigh_idx = knn.kneighbors(X_test, return_distance=False)
        y_pred_idx = np.array([np.mean(y_train[idx], axis=0) for idx in neigh_idx])

        y_pred = knn.predict(X_test)

        assert y_pred.shape == y_test.shape
        assert y_pred_idx.shape == y_test.shape
        assert_allclose(y_pred, y_pred_idx)

def test_kneighbors_regressor_multioutput(
):
    # Test k-neighbors in multi-output regression
    y = np.vstack([y, y]).T

    weights = ["uniform", "distance", _weight_func]
        knn = neighbors.KNeighborsRegressor(
            n_neighbors=n_neighbors, weights=weights, algorithm=algorithm
        )
        knn.fit(X, y)
        y_pred = knn.predict(X[:n_test_pts] + epsilon)
        assert y_pred.shape == y_target.shape

        assert np.all(np.abs(y_pred - y_target) < 0.3)

def test_radius_neighbors_regressor(
    n_samples=40, n_features=3, n_test_pts=10, radius=0.5, random_state=0
):
    # Test radius-based neighbors regression

            neigh = neighbors.RadiusNeighborsRegressor(
            )
            neigh.fit(X, y)
            y_pred = neigh.predict(X[:n_test_pts] + epsilon)
            assert np.all(abs(y_pred - y_target) < radius / 2)

    # test that nan is returned when no nearby observations
    for weights in ["uniform", "distance"]:
        neigh = neighbors.RadiusNeighborsRegressor(
            radius=radius, weights=weights, algorithm="auto"
        )
        X_test_nan = np.full((1, n_features), -1.0)
        empty_warning_msg = (
            "One or more samples have no neighbors "
            "within specified radius; predicting NaN."
        )
        with pytest.warns(UserWarning, match=re.escape(empty_warning_msg)):
            pred = neigh.predict(X_test_nan)
        assert np.all(np.isnan(pred))

def test_RadiusNeighborsRegressor_multioutput_with_uniform_weight():
    # Test radius neighbors in multi-output regression (uniform weight)

        rnn = neighbors.RadiusNeighborsRegressor(weights=weights, algorithm=algorithm)
        rnn.fit(X_train, y_train)

        neigh_idx = rnn.radius_neighbors(X_test, return_distance=False)

        y_pred_idx = np.array(y_pred_idx)
        y_pred = rnn.predict(X_test)

def test_RadiusNeighborsRegressor_multioutput(
    n_samples=40, n_features=5, n_test_pts=10, random_state=0
):
    # Test k-neighbors in multi-output regression with various weight

        rnn.fit(X, y)
        y_pred = rnn.predict(X[:n_test_pts] + epsilon)

def test_kneighbors_regressor_sparse(
):
    # Test radius-based regression on sparse matrices
    y = ((X**2).sum(axis=1) < 0.25).astype(int)

        knn = neighbors.KNeighborsRegressor(n_neighbors=n_neighbors, algorithm="auto")

        knn_pre = neighbors.KNeighborsRegressor(
            n_neighbors=n_neighbors, metric="precomputed"
        )
        knn_pre.fit(pairwise_distances(X, metric="euclidean"), y)

        for sparsev in SPARSE_OR_DENSE:
            X2 = sparsev(X)
            assert np.mean(knn.predict(X2).round() == y) > 0.95

            X2_pre = sparsev(pairwise_distances(X, metric="euclidean"))
            if sparsev in DOK_CONTAINERS + BSR_CONTAINERS:
                msg = "not supported due to its handling of explicit zeros"
                with pytest.raises(TypeError, match=msg):
                    knn_pre.predict(X2_pre)
            else:
                assert np.mean(knn_pre.predict(X2_pre).round() == y) > 0.95

def test_neighbors_iris():
    # Sanity checks on the iris dataset
    # Puts three points of each label in the plane and performs a
    # nearest neighbor query on points near the decision boundary.

        clf = neighbors.KNeighborsClassifier(n_neighbors=1, algorithm=algorithm)
        assert_array_equal(clf.predict(iris.data), iris.target)

        clf.set_params(n_neighbors=9, algorithm=algorithm)
        assert np.mean(clf.predict(iris.data) == iris.target) > 0.95

        rgs = neighbors.KNeighborsRegressor(n_neighbors=5, algorithm=algorithm)
        rgs.fit(iris.data, iris.target)
        assert np.mean(rgs.predict(iris.data).round() == iris.target) > 0.95

def test_neighbors_digits():
    # Sanity check on the digits dataset
    # the 'brute' algorithm has been observed to fail if the input
    # dtype is uint8 due to overflow in distance calculations.

    X = digits.data.astype("uint8")
    Y = digits.target
    (n_samples, n_features) = X.shape
    train_test_boundary = int(n_samples * 0.8)
    train = np.arange(0, train_test_boundary)
    test = np.arange(train_test_boundary, n_samples)
    (X_train, Y_train, X_test, Y_test) = X[train], Y[train], X[test], Y[test]

    clf = neighbors.KNeighborsClassifier(n_neighbors=1, algorithm="brute")
    score_uint8 = clf.fit(X_train, Y_train).score(X_test, Y_test)
    score_float = clf.fit(X_train.astype(float, copy=False), Y_train).score(
        X_test.astype(float, copy=False), Y_test
    )
    assert score_uint8 == score_float

def test_kneighbors_graph():
    # Test kneighbors_graph to build the k-Nearest Neighbor graph.
    X = np.array([[0, 1], [1.01, 1.0], [2, 0]])

    # n_neighbors = 1
    A = neighbors.kneighbors_graph(X, 1, mode="connectivity", include_self=True)
    assert_array_equal(A.toarray(), np.eye(A.shape[0]))

    A = neighbors.kneighbors_graph(X, 1, mode="distance")
        A.toarray(), [[0.00, 1.01, 0.0], [1.01, 0.0, 0.0], [0.00, 1.40716026, 0.0]]
    )

    # n_neighbors = 2
    A = neighbors.kneighbors_graph(X, 2, mode="connectivity", include_self=True)
    assert_array_equal(A.toarray(), [[1.0, 1.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 1.0]])

    A = neighbors.kneighbors_graph(X, 2, mode="distance")
        A.toarray(),
        [
            [0.0, 1.01, 2.23606798],
            [1.01, 0.0, 1.40716026],
            [2.23606798, 1.40716026, 0.0],
        ],
    )

    # n_neighbors = 3
    A = neighbors.kneighbors_graph(X, 3, mode="connectivity", include_self=True)
    assert_allclose(A.toarray(), [[1, 1, 1], [1, 1, 1], [1, 1, 1]])

def test_kneighbors_graph_sparse(n_neighbors, mode, csr_container, seed=36):
    # Test kneighbors_graph to build the k-Nearest Neighbor graph
    # for sparse input.
    X = rng.randn(10, 10)

        neighbors.kneighbors_graph(X, n_neighbors, mode=mode).toarray(),
        neighbors.kneighbors_graph(Xcsr, n_neighbors, mode=mode).toarray(),
    )

def test_radius_neighbors_graph():
    # Test radius_neighbors_graph to build the Nearest Neighbor graph.

    A = neighbors.radius_neighbors_graph(X, 1.5, mode="connectivity", include_self=True)
    assert_array_equal(A.toarray(), [[1.0, 1.0, 0.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0]])

    A = neighbors.radius_neighbors_graph(X, 1.5, mode="distance")
        A.toarray(), [[0.0, 1.01, 0.0], [1.01, 0.0, 1.40716026], [0.0, 1.40716026, 0.0]]
    )

def test_radius_neighbors_graph_sparse(n_neighbors, mode, csr_container, seed=36):
    # Test radius_neighbors_graph to build the Nearest Neighbor graph

        neighbors.radius_neighbors_graph(X, n_neighbors, mode=mode).toarray(),
        neighbors.radius_neighbors_graph(Xcsr, n_neighbors, mode=mode).toarray(),
    )

def test_neighbors_validate_parameters(Estimator, csr_container):
    """Additional parameter validation for *Neighbors* estimators not covered by common
    validation."""
    X = rng.random_sample((10, 2))
    Xsparse = csr_container(X)
    X3 = rng.random_sample((10, 3))
    y = np.ones(10)

    nbrs = Estimator(algorithm="ball_tree", metric="haversine")
    msg = "instance is not fitted yet"
        nbrs.predict(X)
    msg = "Metric 'haversine' not valid for sparse input."
        ignore_warnings(nbrs.fit(Xsparse, y))

    nbrs = Estimator(metric="haversine", algorithm="brute")
    nbrs.fit(X3, y)
    msg = "Haversine distance only valid in 2 dimensions"
        nbrs.predict(X3)

    nbrs = Estimator()
    msg = re.escape("Found array with 0 sample(s)")
        nbrs.fit(np.ones((0, 2)), np.ones(0))

    msg = "Found array with dim 3"
        nbrs.fit(X[:, :, None], y)
    nbrs.fit(X, y)

    msg = re.escape("Found array with 0 feature(s)")
        nbrs.predict([[]])

def test_neighbors_minkowski_semimetric_algo_warn(Estimator, n_features, algorithm):
    """
    Validation of all classes extending NeighborsBase with
    Minkowski semi-metrics (i.e. when 0 < p < 1). That proper
    Warning is raised for `algorithm="auto"` and "brute".
    """
    X = rng.random_sample((10, n_features))

    model = Estimator(p=0.1, algorithm=algorithm)
    msg = (
        "Mind that for 0 < p < 1, Minkowski metrics are not distance"
        " metrics. Continuing the execution with `algorithm='brute'`."
    )

    assert model._fit_method == "brute"

def test_neighbors_minkowski_semimetric_algo_error(Estimator, n_features, algorithm):
    """Check that we raise a proper error if `algorithm!='brute'` and `p<1`."""

    model = Estimator(algorithm=algorithm, p=0.1)
    msg = (
        f'algorithm="{algorithm}" does not support 0 < p < 1 for '
        "the Minkowski metric. To resolve this problem either "
        'set p >= 1 or algorithm="brute".'
    )

def test_nearest_neighbors_validate_params():
    """Validate parameter of NearestNeighbors."""

    nbrs = neighbors.NearestNeighbors().fit(X)
    msg = (
        'Unsupported mode, must be one of "connectivity", or "distance" but got "blah"'
        " instead"
    )
        nbrs.kneighbors_graph(X, mode="blah")
        nbrs.radius_neighbors_graph(X, mode="blah")

def test_neighbors_metrics(
    metric,
    n_samples=20,
    n_features=3,
    n_query_pts=2,
):

    # Test computing the neighbors for various metrics
    algorithms = ["brute", "ball_tree", "kd_tree"]
    X_train = rng.rand(n_samples, n_features).astype(global_dtype, copy=False)
    X_test = rng.rand(n_query_pts, n_features).astype(global_dtype, copy=False)

    metric_params_list = _generate_test_params_for(metric, n_features)

    for metric_params in metric_params_list:
        # Some metric (e.g. Weighted minkowski) are not supported by KDTree
        exclude_kd_tree = (
            False
            if isinstance(metric, DistanceMetric)
            else metric not in neighbors.VALID_METRICS["kd_tree"]
            or ("minkowski" in metric and "w" in metric_params)
        )
        results = {}
        p = metric_params.pop("p", 2)
        for algorithm in algorithms:
            if isinstance(metric, DistanceMetric) and global_dtype == np.float32:
                if "tree" in algorithm:  # pragma: nocover
                    pytest.skip(
                        "Neither KDTree nor BallTree support 32-bit distance metric"
                        " objects."
                    )
            neigh = neighbors.NearestNeighbors(
                algorithm=algorithm,
                p=p,
                metric_params=metric_params,
            )

            if exclude_kd_tree and algorithm == "kd_tree":
                    neigh.fit(X_train)
                continue

            # Haversine distance only accepts 2D data
            if metric == "haversine":
                feature_sl = slice(None, 2)
                X_train = np.ascontiguousarray(X_train[:, feature_sl])
                X_test = np.ascontiguousarray(X_test[:, feature_sl])

            neigh.fit(X_train)
            results[algorithm] = neigh.kneighbors(X_test, return_distance=True)

        brute_dst, brute_idx = results["brute"]
        ball_tree_dst, ball_tree_idx = results["ball_tree"]

        # The returned distances are always in float64 regardless of the input dtype
        # We need to adjust the tolerance w.r.t the input dtype
        rtol = 1e-7 if global_dtype == np.float64 else 1e-4

        assert_allclose(brute_dst, ball_tree_dst, rtol=rtol)
        assert_array_equal(brute_idx, ball_tree_idx)

        if not exclude_kd_tree:
            kd_tree_dst, kd_tree_idx = results["kd_tree"]
            assert_allclose(brute_dst, kd_tree_dst, rtol=rtol)
            assert_array_equal(brute_idx, kd_tree_idx)

            assert_allclose(ball_tree_dst, kd_tree_dst, rtol=rtol)
            assert_array_equal(ball_tree_idx, kd_tree_idx)

def test_kneighbors_brute_backend(
    metric,
    n_samples=2000,
    n_features=30,
    n_query_pts=5,
):
    # Both backend for the 'brute' algorithm of kneighbors must give identical results.

        feature_sl = slice(None, 2)
        X_train = np.ascontiguousarray(X_train[:, feature_sl])
        X_test = np.ascontiguousarray(X_test[:, feature_sl])

    if metric in PAIRWISE_BOOLEAN_FUNCTIONS:
        X_train = X_train > 0.5
        X_test = X_test > 0.5

            algorithm="brute",
            p=p,
        )

        neigh.fit(X_train)

        with config_context(enable_cython_pairwise_dist=False):
            # Use the legacy backend for brute
            legacy_brute_dst, legacy_brute_idx = neigh.kneighbors(
                X_test, return_distance=True
            )
        with config_context(enable_cython_pairwise_dist=True):
            # Use the pairwise-distances reduction backend for brute
            pdr_brute_dst, pdr_brute_idx = neigh.kneighbors(
            )

            legacy_brute_dst, pdr_brute_dst, legacy_brute_idx, pdr_brute_idx
        )

def test_callable_metric():
    def custom_metric(x1, x2):
        return np.sqrt(np.sum(x1**2 + x2**2))

    X = np.random.RandomState(42).rand(20, 2)
    nbrs1 = neighbors.NearestNeighbors(
        n_neighbors=3, algorithm="auto", metric=custom_metric
    )
    nbrs2 = neighbors.NearestNeighbors(
        n_neighbors=3, algorithm="brute", metric=custom_metric
    )

    nbrs1.fit(X)
    nbrs2.fit(X)

    dist1, ind1 = nbrs1.kneighbors(X)
    dist2, ind2 = nbrs2.kneighbors(X)

    assert_allclose(dist1, dist2)

def test_valid_brute_metric_for_auto_algorithm(
    global_dtype, metric, csr_container, n_samples=20, n_features=12
):

        X_precomputed = rng.random_sample((10, 4))
        Y_precomputed = rng.random_sample((3, 4))
        DXX = metrics.pairwise_distances(X_precomputed, metric="euclidean")
        DYX = metrics.pairwise_distances(
            Y_precomputed, X_precomputed, metric="euclidean"
        )
        nb_p = neighbors.NearestNeighbors(n_neighbors=3, metric="precomputed")
        nb_p.fit(DXX)
        nb_p.kneighbors(DYX)

    else:
        for metric_params in metric_params_list:
            nn = neighbors.NearestNeighbors(
                n_neighbors=3,
                algorithm="auto",
            )
                X = np.ascontiguousarray(X[:, feature_sl])

            nn.fit(X)
            nn.kneighbors(X)

            if metric in VALID_METRICS_SPARSE["brute"]:
                nn = neighbors.NearestNeighbors(
                    n_neighbors=3, algorithm="auto", metric=metric
                ).fit(Xcsr)
                nn.kneighbors(Xcsr)

def test_metric_params_interface():
    X = rng.rand(5, 5)
    y = rng.randint(0, 2, 5)
    est = neighbors.KNeighborsClassifier(metric_params={"p": 3})
    with pytest.warns(SyntaxWarning):

def test_predict_sparse_ball_kd_tree(csr_container):
    nbrs1 = neighbors.KNeighborsClassifier(1, algorithm="kd_tree")
    nbrs2 = neighbors.KNeighborsRegressor(1, algorithm="ball_tree")
    for model in [nbrs1, nbrs2]:
            model.predict(csr_container(X))

def test_non_euclidean_kneighbors():

    # Find a reasonable radius.
    dist_array = pairwise_distances(X).flatten()
    np.sort(dist_array)
    radius = dist_array[15]

    # Test kneighbors_graph
    for metric in ["manhattan", "chebyshev"]:
        nbrs_graph = neighbors.kneighbors_graph(
            X, 3, metric=metric, mode="connectivity", include_self=True
        ).toarray()
        nbrs1 = neighbors.NearestNeighbors(n_neighbors=3, metric=metric).fit(X)
        assert_array_equal(nbrs_graph, nbrs1.kneighbors_graph(X).toarray())

    # Test radiusneighbors_graph
        nbrs_graph = neighbors.radius_neighbors_graph(
            X, radius, metric=metric, mode="connectivity", include_self=True
        nbrs1 = neighbors.NearestNeighbors(metric=metric, radius=radius).fit(X)
        assert_array_equal(nbrs_graph, nbrs1.radius_neighbors_graph(X).toarray())

    # Raise error when wrong parameters are supplied,
    X_nbrs = neighbors.NearestNeighbors(n_neighbors=3, metric="manhattan")
    X_nbrs.fit(X)
        neighbors.kneighbors_graph(X_nbrs, 3, metric="euclidean")
    X_nbrs = neighbors.NearestNeighbors(radius=radius, metric="manhattan")
        neighbors.radius_neighbors_graph(X_nbrs, radius, metric="euclidean")

def check_object_arrays(nparray, list_check):
    for ind, ele in enumerate(nparray):
        assert_array_equal(ele, list_check[ind])

def test_k_and_radius_neighbors_train_is_not_query():
    # Test kneighbors et.al when query is not training data

        nn = neighbors.NearestNeighbors(n_neighbors=1, algorithm=algorithm)

        X = [[0], [1]]
        nn.fit(X)
        test_data = [[2], [1]]

        # Test neighbors.
        dist, ind = nn.kneighbors(test_data)
        assert_array_equal(dist, [[1], [0]])
        assert_array_equal(ind, [[1], [1]])
        dist, ind = nn.radius_neighbors([[2], [1]], radius=1.5)
        check_object_arrays(dist, [[1], [1, 0]])
        check_object_arrays(ind, [[1], [0, 1]])

        # Test the graph variants.
            nn.kneighbors_graph(test_data).toarray(), [[0.0, 1.0], [0.0, 1.0]]
        )
            nn.kneighbors_graph([[2], [1]], mode="distance").toarray(),
            np.array([[0.0, 1.0], [0.0, 0.0]]),
        )
        rng = nn.radius_neighbors_graph([[2], [1]], radius=1.5)
        assert_array_equal(rng.toarray(), [[0, 1], [1, 1]])

def test_k_and_radius_neighbors_X_None(algorithm):
    # Test kneighbors et.al when query is None
    nn = neighbors.NearestNeighbors(n_neighbors=1, algorithm=algorithm)

    X = [[0], [1]]
    nn.fit(X)

    dist, ind = nn.kneighbors()
    assert_array_equal(dist, [[1], [1]])
    assert_array_equal(ind, [[1], [0]])
    dist, ind = nn.radius_neighbors(None, radius=1.5)
    check_object_arrays(dist, [[1], [1]])
    check_object_arrays(ind, [[1], [0]])

    # Test the graph variants.
    rng = nn.radius_neighbors_graph(None, radius=1.5)
    kng = nn.kneighbors_graph(None)
    for graph in [rng, kng]:
        assert_array_equal(graph.toarray(), [[0, 1], [1, 0]])
        assert_array_equal(graph.data, [1, 1])
        assert_array_equal(graph.indices, [1, 0])

    X = [[0, 1], [0, 1], [1, 1]]
    nn = neighbors.NearestNeighbors(n_neighbors=2, algorithm=algorithm)
    nn.fit(X)
        nn.kneighbors_graph().toarray(),
        np.array([[0.0, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0]]),
    )

def test_k_and_radius_neighbors_duplicates(algorithm):
    # Test behavior of kneighbors when duplicates are present in query
    duplicates = [[0], [1], [3]]

    nn.fit(duplicates)

    # Do not do anything special to duplicates.
    kng = nn.kneighbors_graph(duplicates, mode="distance")
        kng.toarray(), np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
    )
    assert_allclose(kng.data, [0.0, 0.0, 0.0])
    assert_allclose(kng.indices, [0, 1, 2])

    dist, ind = nn.radius_neighbors([[0], [1]], radius=1.5)
    check_object_arrays(dist, [[0, 1], [1, 0]])
    check_object_arrays(ind, [[0, 1], [0, 1]])

    rng = nn.radius_neighbors_graph(duplicates, radius=1.5)
        rng.toarray(), np.array([[1.0, 1.0, 0.0], [1.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
    )

    rng = nn.radius_neighbors_graph([[0], [1]], radius=1.5, mode="distance")
    rng.sort_indices()
    assert_allclose(rng.toarray(), [[0, 1, 0], [1, 0, 0]])
    assert_allclose(rng.indices, [0, 1, 0, 1])
    assert_allclose(rng.data, [0, 1, 1, 0])

    # Mask the first duplicates when n_duplicates > n_neighbors.
    X = np.ones((3, 1))
    nn = neighbors.NearestNeighbors(n_neighbors=1, algorithm="brute")
    nn.fit(X)
    assert_allclose(dist, np.zeros((3, 1)))
    assert_allclose(ind, [[1], [0], [1]])

    # Test that zeros are explicitly marked in kneighbors_graph.
    kng = nn.kneighbors_graph(mode="distance")
    assert_allclose(kng.toarray(), np.zeros((3, 3)))
    assert_allclose(kng.data, np.zeros(3))
    assert_allclose(kng.indices, [1, 0, 1])
        np.array([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]),
    )

def test_include_self_neighbors_graph():
    # Test include_self parameter in neighbors_graph
    X = [[2, 3], [4, 5]]
    kng = neighbors.kneighbors_graph(X, 1, include_self=True).toarray()
    kng_not_self = neighbors.kneighbors_graph(X, 1, include_self=False).toarray()
    assert_array_equal(kng, [[1.0, 0.0], [0.0, 1.0]])
    assert_array_equal(kng_not_self, [[0.0, 1.0], [1.0, 0.0]])

    rng = neighbors.radius_neighbors_graph(X, 5.0, include_self=True).toarray()
    rng_not_self = neighbors.radius_neighbors_graph(
        X, 5.0, include_self=False
    assert_array_equal(rng, [[1.0, 1.0], [1.0, 1.0]])
    assert_array_equal(rng_not_self, [[0.0, 1.0], [1.0, 0.0]])

def test_same_knn_parallel(algorithm):
        n_samples=30, n_features=5, n_redundant=0, random_state=0
    )
    X_train, X_test, y_train, y_test = train_test_split(X, y)

    clf = neighbors.KNeighborsClassifier(n_neighbors=3, algorithm=algorithm)
    y = clf.predict(X_test)
    dist, ind = clf.kneighbors(X_test)
    graph = clf.kneighbors_graph(X_test, mode="distance").toarray()

    clf.set_params(n_jobs=3)
    y_parallel = clf.predict(X_test)
    dist_parallel, ind_parallel = clf.kneighbors(X_test)
    graph_parallel = clf.kneighbors_graph(X_test, mode="distance").toarray()

    assert_array_equal(y, y_parallel)
    assert_allclose(dist, dist_parallel)
    assert_array_equal(ind, ind_parallel)
    assert_allclose(graph, graph_parallel)

def test_same_radius_neighbors_parallel(algorithm):
    )

    clf = neighbors.RadiusNeighborsClassifier(radius=10, algorithm=algorithm)
    dist, ind = clf.radius_neighbors(X_test)
    graph = clf.radius_neighbors_graph(X_test, mode="distance").toarray()

    dist_parallel, ind_parallel = clf.radius_neighbors(X_test)
    graph_parallel = clf.radius_neighbors_graph(X_test, mode="distance").toarray()

    for i in range(len(dist)):
        assert_allclose(dist[i], dist_parallel[i])
        assert_array_equal(ind[i], ind_parallel[i])

def test_knn_forcing_backend(backend, algorithm):
    # Non-regression test which ensures the knn methods are properly working
    # even when forcing the global joblib backend.
    with joblib.parallel_backend(backend):
        X, y = datasets.make_classification(
            n_samples=30, n_features=5, n_redundant=0, random_state=0
        )
        X_train, X_test, y_train, y_test = train_test_split(X, y)

        clf = neighbors.KNeighborsClassifier(
            n_neighbors=3, algorithm=algorithm, n_jobs=2
        )
        clf.predict(X_test)
        clf.kneighbors(X_test)
        clf.kneighbors_graph(X_test, mode="distance")

def test_dtype_convert():
    classifier = neighbors.KNeighborsClassifier(n_neighbors=1)
    CLASSES = 15
    X = np.eye(CLASSES)
    y = [ch for ch in "ABCDEFGHIJKLMNOPQRSTU"[:CLASSES]]

def test_sparse_metric_callable(csr_container):
    def sparse_metric(x, y):  # Metric accepting sparse matrix input (only)
        assert issparse(x) and issparse(y)
        return x.dot(y.T).toarray().item()

    X = csr_container(
        [[1, 1, 1, 1, 1], [1, 0, 1, 0, 1], [0, 0, 1, 0, 0]]  # Population matrix
    )

    Y = csr_container([[1, 1, 0, 1, 1], [1, 0, 0, 1, 1]])  # Query matrix

    nn = neighbors.NearestNeighbors(
        algorithm="brute", n_neighbors=2, metric=sparse_metric
    ).fit(X)
    N = nn.kneighbors(Y, return_distance=False)

    # GS indices of nearest neighbours in `X` for `sparse_metric`
    gold_standard_nn = np.array([[2, 1], [2, 1]])

    assert_array_equal(N, gold_standard_nn)

def test_pairwise_boolean_distance():
    # Non-regression test for #4523
    # 'brute': uses scipy.spatial.distance through pairwise_distances
    # 'ball_tree': uses sklearn.neighbors._dist_metrics
    X = rng.uniform(size=(6, 5))
    NN = neighbors.NearestNeighbors

    nn1 = NN(metric="jaccard", algorithm="brute").fit(X)
    nn2 = NN(metric="jaccard", algorithm="ball_tree").fit(X)
    assert_array_equal(nn1.kneighbors(X)[0], nn2.kneighbors(X)[0])

def test_radius_neighbors_predict_proba():
    for seed in range(5):
            n_samples=50,
            n_features=5,
            n_informative=3,
            random_state=seed,
        )
        X_tr, X_te, y_tr, y_te = train_test_split(X, y, random_state=0)
        outlier_label = int(2 - seed)
        clf = neighbors.RadiusNeighborsClassifier(radius=2, outlier_label=outlier_label)
        clf.fit(X_tr, y_tr)
        pred = clf.predict(X_te)
        proba = clf.predict_proba(X_te)
        proba_label = proba.argmax(axis=1)
        proba_label = np.where(proba.sum(axis=1) == 0, outlier_label, proba_label)
        assert_array_equal(pred, proba_label)

def test_pipeline_with_nearest_neighbors_transformer():
    # Test chaining KNeighborsTransformer and classifiers/regressors
    X = 2 * rng.rand(40, 5) - 1
    X2 = 2 * rng.rand(40, 5) - 1
    y = rng.rand(40, 1)

    n_neighbors = 12
    radius = 1.5
    # We precompute more neighbors than necessary, to have equivalence between
    # k-neighbors estimator after radius-neighbors transformer, and vice-versa.
    factor = 2

    k_trans = neighbors.KNeighborsTransformer(n_neighbors=n_neighbors, mode="distance")
    k_trans_factor = neighbors.KNeighborsTransformer(
        n_neighbors=int(n_neighbors * factor), mode="distance"
    )

    r_trans = neighbors.RadiusNeighborsTransformer(radius=radius, mode="distance")
    r_trans_factor = neighbors.RadiusNeighborsTransformer(
        radius=int(radius * factor), mode="distance"
    )

    k_reg = neighbors.KNeighborsRegressor(n_neighbors=n_neighbors)
    r_reg = neighbors.RadiusNeighborsRegressor(radius=radius)

    test_list = [
        (k_trans, k_reg),
        (k_trans_factor, r_reg),
        (r_trans, r_reg),
        (r_trans_factor, k_reg),
    ]

    for trans, reg in test_list:
        # compare the chained version and the compact version
        reg_compact = clone(reg)
        reg_precomp = clone(reg)
        reg_precomp.set_params(metric="precomputed")

        reg_chain = make_pipeline(clone(trans), reg_precomp)

        y_pred_chain = reg_chain.fit(X, y).predict(X2)
        y_pred_compact = reg_compact.fit(X, y).predict(X2)
        assert_allclose(y_pred_chain, y_pred_compact)

def test_auto_algorithm(X, metric, metric_params, expected_algo):
    model = neighbors.NearestNeighbors(
        n_neighbors=4, algorithm="auto", metric=metric, metric_params=metric_params
    )
    assert model._fit_method == expected_algo

def test_radius_neighbors_brute_backend(
    metric,
    radius=1.0,
):
    # Both backends for the 'brute' algorithm of radius_neighbors
    # must give identical results.

            p=p,
        )

            legacy_brute_dst, legacy_brute_idx = neigh.radius_neighbors(
            )
            pdr_brute_dst, pdr_brute_idx = neigh.radius_neighbors(
            )

            legacy_brute_dst,
            pdr_brute_dst,
            legacy_brute_idx,
            pdr_brute_idx,
        )

def test_valid_metrics_has_no_duplicate():
    for val in neighbors.VALID_METRICS.values():
        assert len(val) == len(set(val))

def test_regressor_predict_on_arraylikes():
    """Ensures that `predict` works for array-likes when `weights` is a callable.

    Non-regression test for #22687.
    """
    X = [[5, 1], [3, 1], [4, 3], [0, 3]]
    y = [2, 3, 5, 6]

    def _weights(dist):
        return np.ones_like(dist)

    est = KNeighborsRegressor(n_neighbors=1, algorithm="brute", weights=_weights)
    assert_allclose(est.predict([[0, 2.5]]), [6])

def test_nan_euclidean_support(Estimator, params):
    """Check that the different neighbor estimators are lenient towards `nan`
    values if using `metric="nan_euclidean"`.
    """

    y = [0, 0, 1, 1]

    params.update({"metric": "nan_euclidean"})
    estimator = Estimator().set_params(**params).fit(X, y)

    for response_method in ("kneighbors", "predict", "transform", "fit_predict"):
        if hasattr(estimator, response_method):
            output = getattr(estimator, response_method)(X)
            if hasattr(output, "toarray"):
                assert not np.isnan(output.data).any()
            else:
                assert not np.isnan(output).any()

def test_predict_dataframe():
    """Check that KNN predict works with dataframes

    non-regression test for issue #26768
    """

    X = pd.DataFrame(np.array([[1, 2], [3, 4], [5, 6], [7, 8]]), columns=["a", "b"])
    y = np.array([1, 2, 3, 4])

    knn = neighbors.KNeighborsClassifier(n_neighbors=2).fit(X, y)
    knn.predict(X)

def test_nearest_neighbours_works_with_p_less_than_1():
    """Check that NearestNeighbors works with :math:`p \\in (0,1)` when `algorithm`
    is `"auto"` or `"brute"` regardless of the dtype of X.

    Non-regression test for issue #26548
    """
    X = np.array([[1.0, 0.0], [0.0, 0.0], [0.0, 1.0]])
    neigh = neighbors.NearestNeighbors(
        n_neighbors=3, algorithm="brute", metric_params={"p": 0.5}
    )
    neigh.fit(X)

    y = neigh.radius_neighbors(X[0].reshape(1, -1), radius=4, return_distance=False)
    assert_allclose(y[0], [0, 1, 2])

    y = neigh.kneighbors(X[0].reshape(1, -1), return_distance=False)

def test_KNeighborsClassifier_raise_on_all_zero_weights():
    """Check that `predict` and `predict_proba` raises on sample of all zeros weights.

    Related to Issue #25854.
    """
    X = [[0, 1], [1, 2], [2, 3], [3, 4]]

        return np.vectorize(lambda x: 0 if x > 0.5 else 1)(dist)

    est = neighbors.KNeighborsClassifier(n_neighbors=3, weights=_weights)

    msg = (
        "All neighbors of some sample is getting zero weights. "
        "Please modify 'weights' to avoid this case if you are "
        "using a user-defined function."
    )

        est.predict([[1.1, 1.1]])

        est.predict_proba([[1.1, 1.1]])

def test_neighbor_classifiers_loocv(nn_model, algorithm):
    """Check that `predict` and related functions work fine with X=None

    Calling predict with X=None computes a prediction for each training point
    from the labels of its neighbors (without the label of the data point being
    predicted upon). This is therefore mathematically equivalent to
    leave-one-out cross-validation without having do any retraining (rebuilding
    a KD-tree or Ball-tree index) or any data reshuffling.
    """
    X, y = datasets.make_blobs(n_samples=15, centers=5, n_features=2, random_state=0)

    nn_model = clone(nn_model).set_params(algorithm=algorithm)

    # Set the radius for RadiusNeighborsRegressor to some percentile of the
    # empirical pairwise distances to avoid trivial test cases and warnings for
    # predictions with no neighbors within the radius.
    if "radius" in nn_model.get_params():
        dists = pairwise_distances(X).ravel()
        dists = dists[dists > 0]
        nn_model.set_params(radius=np.percentile(dists, 80))

    loocv = cross_val_score(nn_model, X, y, cv=LeaveOneOut())
    nn_model.fit(X, y)

    assert_allclose(loocv, nn_model.predict(None) == y)
    assert np.mean(loocv) == pytest.approx(nn_model.score(None, y))

    # Evaluating `nn_model` on its "training" set should lead to a higher
    # accuracy value than leaving out each data point in turn because the
    # former can overfit while the latter cannot by construction.
    assert nn_model.score(None, y) < nn_model.score(X, y)

def test_neighbor_regressors_loocv(nn_model, algorithm):
    """Check that `predict` and related functions work fine with X=None"""
    X, y = datasets.make_regression(n_samples=15, n_features=2, random_state=0)

    # Only checking cross_val_predict and not cross_val_score because
    # cross_val_score does not work with LeaveOneOut() for a regressor: the
    # default score method implements R2 score which is not well defined for a
    # single data point.
    #
    # TODO: if score is refactored to evaluate models for other scoring
    # functions, then this test can be extended to check cross_val_score as
    # well.

    loocv = cross_val_predict(nn_model, X, y, cv=LeaveOneOut())
    assert_allclose(loocv, nn_model.predict(None))

        return (
        )

        return (
        )

        return (
        )

def test_KDTree_is_KDTree64_subclass():
    assert issubclass(KDTree, KDTree64)

def test_array_object_type(BinarySearchTree):
    """Check that we do not accept object dtype array."""
    X = np.array([(1, 2, 3), (2, 5), (5, 5, 1, 2)], dtype=object)
    with pytest.raises(ValueError, match="setting an array element with a sequence"):
        BinarySearchTree(X)

def test_kdtree_picklable_with_joblib(BinarySearchTree):
    """Make sure that KDTree queries work when joblib memmaps.

    Non-regression test for #21685 and #21228."""
    tree = BinarySearchTree(X, leaf_size=2)

    # Call Parallel with max_nbytes=1 to trigger readonly memory mapping that
    # use to raise "ValueError: buffer source array is read-only" in a previous
    # version of the Cython code.
    Parallel(n_jobs=2, max_nbytes=1)(delayed(tree.query)(data) for data in 2 * [X])

def test_kd_tree_numerical_consistency(global_random_seed, metric):
    # Results on float64 and float32 versions of a dataset must be
    # numerically close.
    X_64, X_32, Y_64, Y_32 = get_dataset_for_binary_tree(
        random_seed=global_random_seed, features=50
    )

    metric_params = METRICS.get(metric, {})
    kd_64 = KDTree64(X_64, leaf_size=2, metric=metric, **metric_params)
    kd_32 = KDTree32(X_32, leaf_size=2, metric=metric, **metric_params)

    # Test consistency with respect to the `query` method
    k = 4
    dist_64, ind_64 = kd_64.query(Y_64, k=k)
    dist_32, ind_32 = kd_32.query(Y_32, k=k)
    assert_allclose(dist_64, dist_32, rtol=1e-5)
    assert_equal(ind_64, ind_32)
    assert dist_64.dtype == np.float64
    assert dist_32.dtype == np.float32

    # Test consistency with respect to the `query_radius` method
    r = 2.38
    ind_64 = kd_64.query_radius(Y_64, r=r)
    ind_32 = kd_32.query_radius(Y_32, r=r)
    for _ind64, _ind32 in zip(ind_64, ind_32):
        assert_equal(_ind64, _ind32)

    # with return distances being true
    ind_64, dist_64 = kd_64.query_radius(Y_64, r=r, return_distance=True)
    ind_32, dist_32 = kd_32.query_radius(Y_32, r=r, return_distance=True)
    for _ind64, _ind32, _dist_64, _dist_32 in zip(ind_64, ind_32, dist_64, dist_32):
        assert_allclose(_dist_64, _dist_32, rtol=1e-5)
        assert _dist_64.dtype == np.float64
        assert _dist_32.dtype == np.float32

def test_kernel_density_numerical_consistency(global_random_seed, metric):
    # Test consistency with respect to the `kernel_density` method
    X_64, X_32, Y_64, Y_32 = get_dataset_for_binary_tree(random_seed=global_random_seed)

    kernel = "gaussian"
    h = 0.1
    density64 = kd_64.kernel_density(Y_64, h=h, kernel=kernel, breadth_first=True)
    density32 = kd_32.kernel_density(Y_32, h=h, kernel=kernel, breadth_first=True)
    assert_allclose(density64, density32, rtol=1e-5)
    assert density64.dtype == np.float64
    assert density32.dtype == np.float32

def test_spectral_clustering():
    # Test chaining KNeighborsTransformer and SpectralClustering
    n_neighbors = 5
    X, _ = make_blobs(random_state=0)

    # compare the chained version and the compact version
    est_chain = make_pipeline(
        KNeighborsTransformer(n_neighbors=n_neighbors, mode="connectivity"),
        SpectralClustering(
            n_neighbors=n_neighbors, affinity="precomputed", random_state=42
        ),
    )
    est_compact = SpectralClustering(
        n_neighbors=n_neighbors, affinity="nearest_neighbors", random_state=42
    )
    labels_compact = est_compact.fit_predict(X)
    labels_chain = est_chain.fit_predict(X)
    assert_array_almost_equal(labels_chain, labels_compact)

def test_spectral_embedding():
    # Test chaining KNeighborsTransformer and SpectralEmbedding

    centers = np.array(
        [
            [0.0, 5.0, 0.0, 0.0, 0.0],
            [0.0, 0.0, 4.0, 0.0, 0.0],
            [1.0, 0.0, 0.0, 5.0, 1.0],
        ]
    )
    S, true_labels = make_blobs(
        n_samples=n_samples, centers=centers, cluster_std=1.0, random_state=42
    )

        SpectralEmbedding(
        ),
    )
    est_compact = SpectralEmbedding(
    )
    St_compact = est_compact.fit_transform(S)
    St_chain = est_chain.fit_transform(S)
    assert_array_almost_equal(St_chain, St_compact)

def test_dbscan():
    # Test chaining RadiusNeighborsTransformer and DBSCAN
    radius = 0.3
    n_clusters = 3
    X = generate_clustered_data(n_clusters=n_clusters)

        RadiusNeighborsTransformer(radius=radius, mode="distance"),
        DBSCAN(metric="precomputed", eps=radius),
    )
    est_compact = DBSCAN(eps=radius)

def test_isomap():
    # Test chaining KNeighborsTransformer and Isomap with
    # neighbors_algorithm='precomputed'
    algorithm = "auto"
    n_neighbors = 10

    X2, _ = make_blobs(random_state=1)

        KNeighborsTransformer(
            n_neighbors=n_neighbors, algorithm=algorithm, mode="distance"
        ),
        Isomap(n_neighbors=n_neighbors, metric="precomputed"),
    )
    est_compact = Isomap(n_neighbors=n_neighbors, neighbors_algorithm=algorithm)

    Xt_chain = est_chain.fit_transform(X)
    Xt_compact = est_compact.fit_transform(X)
    assert_array_almost_equal(Xt_chain, Xt_compact)

    Xt_chain = est_chain.transform(X2)
    Xt_compact = est_compact.transform(X2)

def test_tsne():
    # Test chaining KNeighborsTransformer and TSNE
    max_iter = 250
    perplexity = 5
    n_neighbors = int(3.0 * perplexity + 1)

    for metric in ["minkowski", "sqeuclidean"]:
        est_chain = make_pipeline(
            KNeighborsTransformer(
                n_neighbors=n_neighbors, mode="distance", metric=metric
            ),
            TSNE(
                init="random",
                metric="precomputed",
                perplexity=perplexity,
                method="barnes_hut",
                random_state=42,
            ),
        )
        est_compact = TSNE(
            init="random",
            perplexity=perplexity,
            method="barnes_hut",
        )

        Xt_chain = est_chain.fit_transform(X)
        Xt_compact = est_compact.fit_transform(X)
        assert_array_almost_equal(Xt_chain, Xt_compact)

def test_lof_novelty_false():
    # Test chaining KNeighborsTransformer and LocalOutlierFactor
    n_neighbors = 4

    X = rng.randn(40, 2)

        KNeighborsTransformer(n_neighbors=n_neighbors, mode="distance"),
        LocalOutlierFactor(
            metric="precomputed",
            novelty=False,
            contamination="auto",
        ),
    )
    est_compact = LocalOutlierFactor(
        n_neighbors=n_neighbors, novelty=False, contamination="auto"
    )

    pred_chain = est_chain.fit_predict(X)
    pred_compact = est_compact.fit_predict(X)
    assert_array_almost_equal(pred_chain, pred_compact)

def test_lof_novelty_true():

    X1 = rng.randn(40, 2)
    X2 = rng.randn(40, 2)

            novelty=True,
        ),
    )
        n_neighbors=n_neighbors, novelty=True, contamination="auto"
    )

    pred_chain = est_chain.fit(X1).predict(X2)
    pred_compact = est_compact.fit(X1).predict(X2)

def test_kneighbors_regressor():

    factor = 2

    k_trans = KNeighborsTransformer(n_neighbors=n_neighbors, mode="distance")
    k_trans_factor = KNeighborsTransformer(
    )

    r_trans = RadiusNeighborsTransformer(radius=radius, mode="distance")
    r_trans_factor = RadiusNeighborsTransformer(
    )

    k_reg = KNeighborsRegressor(n_neighbors=n_neighbors)
    r_reg = RadiusNeighborsRegressor(radius=radius)

    ]

        assert_array_almost_equal(y_pred_chain, y_pred_compact)

def test_simple_example():
    """Test on a simple example.

    Puts four points in the input space where the opposite labels points are
    next to each other. After transform the samples from the same class
    should be next to each other.

    """
    X = np.array([[0, 0], [0, 1], [2, 0], [2, 1]])
    y = np.array([1, 0, 1, 0])
    nca = NeighborhoodComponentsAnalysis(
        n_components=2, init="identity", random_state=42
    )
    nca.fit(X, y)
    X_t = nca.transform(X)
    assert_array_equal(pairwise_distances(X_t).argsort()[:, 1], np.array([2, 3, 0, 1]))

def test_toy_example_collapse_points():
    """Test on a toy example of three points that should collapse

    We build a simple example: two points from the same class and a point from
    a different class in the middle of them. On this simple example, the new
    (transformed) points should all collapse into one single point. Indeed, the
    objective is 2/(1 + exp(d/2)), with d the euclidean distance between the
    two samples from the same class. This is maximized for d=0 (because d>=0),
    with an objective equal to 1 (loss=-1.).

    """
    input_dim = 5
    two_points = rng.randn(2, input_dim)
    X = np.vstack([two_points, two_points.mean(axis=0)[np.newaxis, :]])
    y = [0, 0, 1]

    class LossStorer:
        def __init__(self, X, y):
            self.loss = np.inf  # initialize the loss to very high
            # Initialize a fake NCA and variables needed to compute the loss:
            self.fake_nca = NeighborhoodComponentsAnalysis()
            self.fake_nca.n_iter_ = np.inf
            self.X, y = validate_data(self.fake_nca, X, y, ensure_min_samples=2)
            y = LabelEncoder().fit_transform(y)
            self.same_class_mask = y[:, np.newaxis] == y[np.newaxis, :]

        def callback(self, transformation, n_iter):
            """Stores the last value of the loss function"""
            self.loss, _ = self.fake_nca._loss_grad_lbfgs(
                transformation, self.X, self.same_class_mask, -1.0
            )

    loss_storer = LossStorer(X, y)
    nca = NeighborhoodComponentsAnalysis(random_state=42, callback=loss_storer.callback)
    X_t = nca.fit_transform(X, y)
    print(X_t)
    # test that points are collapsed into one point
    assert_array_almost_equal(X_t - X_t[0], 0.0)
    assert abs(loss_storer.loss + 1) < 1e-10

def test_finite_differences(global_random_seed):
    """Test gradient of loss function

    Assert that the gradient is almost equal to its finite differences
    approximation.
    """
    # Initialize the transformation `M`, as well as `X` and `y` and `NCA`
    X, y = make_classification(random_state=global_random_seed)
    M = rng.randn(rng.randint(1, X.shape[1] + 1), X.shape[1])
    nca = NeighborhoodComponentsAnalysis()
    nca.n_iter_ = 0
    mask = y[:, np.newaxis] == y[np.newaxis, :]

    def fun(M):
        return nca._loss_grad_lbfgs(M, X, mask)[0]

    def grad(M):
        return nca._loss_grad_lbfgs(M, X, mask)[1]

    # compare the gradient to a finite difference approximation
    diff = check_grad(fun, grad, M.ravel())
    assert diff == pytest.approx(0.0, abs=1e-4)

def test_params_validation():
    # Test that invalid parameters raise value error
    X = np.arange(12).reshape(4, 3)
    y = [1, 1, 2, 2]
    NCA = NeighborhoodComponentsAnalysis

    init = rng.rand(5, 3)
    msg = (
        f"The output dimensionality ({init.shape[0]}) "
        "of the given linear transformation `init` cannot be "
        f"greater than its input dimensionality ({init.shape[1]})."
    )
    with pytest.raises(ValueError, match=re.escape(msg)):
        NCA(init=init).fit(X, y)
    msg = (
        "The preferred dimensionality of the projected space "
        f"`n_components` ({n_components}) cannot be greater "
        f"than the given data dimensionality ({X.shape[1]})!"
    )
        NCA(n_components=n_components).fit(X, y)

def test_transformation_dimensions():

    # Fail if transformation input dimension does not match inputs dimensions
    transformation = np.array([[1, 2], [3, 4]])
        NeighborhoodComponentsAnalysis(init=transformation).fit(X, y)

    # Fail if transformation output dimension is larger than
    # transformation input dimension
    transformation = np.array([[1, 2], [3, 4], [5, 6]])
    # len(transformation) > len(transformation[0])

    # Pass otherwise
    transformation = np.arange(9).reshape(3, 3)
    NeighborhoodComponentsAnalysis(init=transformation).fit(X, y)

def test_n_components():

    init = rng.rand(X.shape[1] - 1, 3)

    # n_components = X.shape[1] != transformation.shape[0]
    n_components = X.shape[1]
    nca = NeighborhoodComponentsAnalysis(init=init, n_components=n_components)
    msg = (
        f"`n_components` ({n_components}) does not match the output "
        "dimensionality of the given linear transformation "
        f"`init` ({init.shape[0]})!"
    )
        nca.fit(X, y)

    # n_components > X.shape[1]
    n_components = X.shape[1] + 2
    msg = (
        f"`n_components` ({n_components}) cannot be greater than "
        f"the given data dimensionality ({X.shape[1]})!"
    )

    # n_components < X.shape[1]
    nca = NeighborhoodComponentsAnalysis(n_components=2, init="identity")

def test_init_transformation():
    X, y = make_blobs(n_samples=30, centers=6, n_features=5, random_state=0)

    # Start learning from scratch
    nca = NeighborhoodComponentsAnalysis(init="identity")

    # Initialize with random
    nca_random = NeighborhoodComponentsAnalysis(init="random")
    nca_random.fit(X, y)

    # Initialize with auto
    nca_auto = NeighborhoodComponentsAnalysis(init="auto")
    nca_auto.fit(X, y)

    # Initialize with PCA
    nca_pca = NeighborhoodComponentsAnalysis(init="pca")
    nca_pca.fit(X, y)

    # Initialize with LDA
    nca_lda = NeighborhoodComponentsAnalysis(init="lda")
    nca_lda.fit(X, y)

    init = rng.rand(X.shape[1], X.shape[1])
    nca = NeighborhoodComponentsAnalysis(init=init)

    # init.shape[1] must match X.shape[1]
    init = rng.rand(X.shape[1], X.shape[1] + 1)
    msg = (
        f"The input dimensionality ({init.shape[1]}) of the given "
        "linear transformation `init` must match the "
        f"dimensionality of the given inputs `X` ({X.shape[1]})."
    )

    # init.shape[0] must be <= init.shape[1]
    init = rng.rand(X.shape[1] + 1, X.shape[1])
    msg = (
        f"The output dimensionality ({init.shape[0]}) of the given "
        "linear transformation `init` cannot be "
    )

    # init.shape[0] must match n_components
    n_components = X.shape[1] - 2
    msg = (
        "The preferred dimensionality of the "
        f"projected space `n_components` ({n_components}) "
        "does not match the output dimensionality of the given "
        f"linear transformation `init` ({init.shape[0]})!"
    )

def test_auto_init(n_samples, n_features, n_classes, n_components):
    # Test that auto choose the init as expected with every configuration
    # of order of n_samples, n_features, n_classes and n_components.
    nca_base = NeighborhoodComponentsAnalysis(
        init="auto", n_components=n_components, max_iter=1, random_state=rng
    )
    if n_classes >= n_samples:
        pass
        # n_classes > n_samples is impossible, and n_classes == n_samples
        # throws an error from lda but is an absurd case
    else:
        X = rng.randn(n_samples, n_features)
        y = np.tile(range(n_classes), n_samples // n_classes + 1)[:n_samples]
        if n_components > n_features:
            # this would return a ValueError, which is already tested in
            # test_params_validation
            pass
        else:
            nca = clone(nca_base)
            nca.fit(X, y)
            if n_components <= min(n_classes - 1, n_features):
                nca_other = clone(nca_base).set_params(init="lda")
            elif n_components < min(n_features, n_samples):
                nca_other = clone(nca_base).set_params(init="pca")
            else:
                nca_other = clone(nca_base).set_params(init="identity")
            nca_other.fit(X, y)
            assert_array_almost_equal(nca.components_, nca_other.components_)

def test_warm_start_validation():
        n_samples=30,
        n_classes=4,
    )

    nca = NeighborhoodComponentsAnalysis(warm_start=True, max_iter=5)

    X_less_features, y = make_classification(
        n_features=4,
        n_informative=4,
    )
    msg = (
        f"The new inputs dimensionality ({X_less_features.shape[1]}) "
        "does not match the input dimensionality of the previously learned "
        f"transformation ({nca.components_.shape[1]})."
    )
        nca.fit(X_less_features, y)

def test_warm_start_effectiveness():
    # A 1-iteration second fit on same data should give almost same result
    # with warm starting, and quite different result without warm starting.

    nca_warm = NeighborhoodComponentsAnalysis(warm_start=True, random_state=0)
    nca_warm.fit(iris_data, iris_target)
    transformation_warm = nca_warm.components_
    nca_warm.max_iter = 1
    transformation_warm_plus_one = nca_warm.components_

    nca_cold = NeighborhoodComponentsAnalysis(warm_start=False, random_state=0)
    nca_cold.fit(iris_data, iris_target)
    transformation_cold = nca_cold.components_
    nca_cold.max_iter = 1
    transformation_cold_plus_one = nca_cold.components_

    diff_warm = np.sum(np.abs(transformation_warm_plus_one - transformation_warm))
    diff_cold = np.sum(np.abs(transformation_cold_plus_one - transformation_cold))
    assert diff_warm < 3.0, (
        "Transformer changed significantly after one "
        "iteration even though it was warm-started."
    )

    assert diff_cold > diff_warm, (
        "Cold-started transformer changed less "
        "significantly than warm-started "
        "transformer after one iteration."
    )

def test_verbose(init_name, capsys):
    # assert there is proper output when verbose = 1, for every initialization
    # except auto because auto will call one of the others
    regexp_init = r"... done in \ *\d+\.\d{2}s"
    msgs = {
        "pca": "Finding principal components" + regexp_init,
        "lda": "Finding most discriminative components" + regexp_init,
    }
    if init_name == "precomputed":
        init = rng.randn(X.shape[1], X.shape[1])
    else:
        init = init_name
    nca = NeighborhoodComponentsAnalysis(verbose=1, init=init)
    out, _ = capsys.readouterr()

    lines = re.split("\n+", out)
    # if pca or lda init, an additional line is printed, so we test
    # it and remove it to test the rest equally among initializations
    if init_name in ["pca", "lda"]:
        assert re.match(msgs[init_name], lines[0])
        lines = lines[1:]
    assert lines[0] == "[NeighborhoodComponentsAnalysis]"
    header = "{:>10} {:>20} {:>10}".format("Iteration", "Objective Value", "Time(s)")
    assert lines[1] == "[NeighborhoodComponentsAnalysis] {}".format(header)
    assert lines[2] == "[NeighborhoodComponentsAnalysis] {}".format("-" * len(header))
    for line in lines[3:-2]:
        # The following regex will match for instance:
        # '[NeighborhoodComponentsAnalysis]  0    6.988936e+01   0.01'
            r"\[NeighborhoodComponentsAnalysis\] *\d+ *\d\.\d{6}e"
            r"[+|-]\d+\ *\d+\.\d{2}",
            line,
        )
    assert re.match(
        r"\[NeighborhoodComponentsAnalysis\] Training took\ *\d+\.\d{2}s\.",
        lines[-2],
    )
    assert lines[-1] == ""

def test_no_verbose(capsys):
    # assert by default there is no output (verbose=0)
    nca.fit(iris_data, iris_target)
    assert out == ""

def test_singleton_class():
    X = iris_data.copy()
    y = iris_target.copy()

    # one singleton class
    singleton_class = 1
    (ind_singleton,) = np.where(y == singleton_class)
    y[ind_singleton] = 2
    y[ind_singleton[0]] = singleton_class

    nca = NeighborhoodComponentsAnalysis(max_iter=30)

    # One non-singleton class
    (ind_1,) = np.where(y == 1)
    (ind_2,) = np.where(y == 2)
    y[ind_1] = 0
    y[ind_1[0]] = 1
    y[ind_2] = 0
    y[ind_2[0]] = 2

    # Only singleton classes
    (ind_0,) = np.where(y == 0)
    X = X[[ind_0[0], ind_1[0], ind_2[0]]]
    y = y[[ind_0[0], ind_1[0], ind_2[0]]]

    nca = NeighborhoodComponentsAnalysis(init="identity", max_iter=30)
    assert_array_equal(X, nca.transform(X))

def test_one_class():
    X = iris_data[iris_target == 0]
    y = iris_target[iris_target == 0]

        max_iter=30, n_components=X.shape[1], init="identity"
    )

def test_callback(capsys):

    def my_cb(transformation, n_iter):
        assert transformation.shape == (iris_data.shape[1] ** 2,)
        rem_iter = max_iter - n_iter
        print("{} iterations remaining...".format(rem_iter))

    # assert that my_cb is called
    nca = NeighborhoodComponentsAnalysis(max_iter=max_iter, callback=my_cb, verbose=1)

    assert "{} iterations remaining...".format(max_iter - 1) in out

def test_expected_transformation_shape():
    """Test that the transformation has the expected shape."""
    X = iris_data
    y = iris_target

    class TransformationStorer:
            # Initialize a fake NCA and variables needed to call the loss
            # function:

            """Stores the last value of the transformation taken as input by
            the optimizer"""
            self.transformation = transformation

    transformation_storer = TransformationStorer(X, y)
    cb = transformation_storer.callback
    nca = NeighborhoodComponentsAnalysis(max_iter=5, callback=cb)
    assert transformation_storer.transformation.size == X.shape[1] ** 2

    nca = NeighborhoodComponentsAnalysis(max_iter=2, verbose=1)
    cls_name = nca.__class__.__name__
    msg = "[{}] NCA did not converge".format(cls_name)
    with pytest.warns(ConvergenceWarning, match=re.escape(msg)):
        nca.fit(iris_data, iris_target)

def test_parameters_valid_types(param, value):
    # check that no error is raised when parameters have numpy integer or
    # floating types.
    nca = NeighborhoodComponentsAnalysis(**{param: value})

def test_nca_feature_names_out(n_components):
    """Check `get_feature_names_out` for `NeighborhoodComponentsAnalysis`.

    https://github.com/scikit-learn/scikit-learn/issues/28293
    """

    est = NeighborhoodComponentsAnalysis(n_components=n_components).fit(X, y)
    names_out = est.get_feature_names_out()

    class_name_lower = est.__class__.__name__.lower()

    if n_components is not None:
        expected_n_features = n_components
    else:
        expected_n_features = X.shape[1]

    expected_names_out = np.array(
        [f"{class_name_lower}{i}" for i in range(expected_n_features)],
        dtype=object,
    )

    assert_array_equal(names_out, expected_names_out)

            )

            )

def brute_force_neighbors(X, Y, k, metric, **kwargs):
    from sklearn.metrics import DistanceMetric

    X, Y = check_array(X), check_array(Y)
    D = DistanceMetric.get_metric(metric, **kwargs).pairwise(Y, X)
    ind = np.argsort(D, axis=1)[:, :k]
    dist = D[np.arange(Y.shape[0])[:, None], ind]
    return dist, ind

def test_BallTree_is_BallTree64_subclass():
    assert issubclass(BallTree, BallTree64)

def test_ball_tree_query_metrics(metric, array_type, BallTreeImplementation):
    if metric in BOOLEAN_METRICS:
        X = rng.random_sample((40, 10)).round(0)
        Y = rng.random_sample((10, 10)).round(0)
    elif metric in DISCRETE_METRICS:
        X = (4 * rng.random_sample((40, 10))).round(0)
        Y = (4 * rng.random_sample((10, 10))).round(0)
    X = _convert_container(X, array_type)
    Y = _convert_container(Y, array_type)

    k = 5

    bt = BallTreeImplementation(X, leaf_size=1, metric=metric)
    dist1, ind1 = bt.query(Y, k)
    dist2, ind2 = brute_force_neighbors(X, Y, k, metric)
    assert_array_almost_equal(dist1, dist2)

def test_query_haversine(BallTreeImplementation, decimal_tol):
    X = 2 * np.pi * rng.random_sample((40, 2))
    bt = BallTreeImplementation(X, leaf_size=1, metric="haversine")
    dist1, ind1 = bt.query(X, k=5)
    dist2, ind2 = brute_force_neighbors(X, X, k=5, metric="haversine")

    assert_array_almost_equal(dist1, dist2, decimal=decimal_tol)
    assert_array_almost_equal(ind1, ind2)

def test_array_object_type(BallTreeImplementation):
        BallTreeImplementation(X)

def test_bad_pyfunc_metric(BallTreeImplementation):
    def wrong_returned_value(x, y):
        return "1"

    def one_arg_func(x):
        return 1.0  # pragma: no cover

    X = np.ones((5, 2))
    msg = "Custom distance function must accept two vectors and return a float."
        BallTreeImplementation(X, metric=wrong_returned_value)

    msg = "takes 1 positional argument but 2 were given"
        BallTreeImplementation(X, metric=one_arg_func)

def test_ball_tree_numerical_consistency(global_random_seed, metric):
    )

    bt_64 = BallTree64(X_64, leaf_size=1, metric=metric, **metric_params)
    bt_32 = BallTree32(X_32, leaf_size=1, metric=metric, **metric_params)

    k = 5
    dist_64, ind_64 = bt_64.query(Y_64, k=k)
    dist_32, ind_32 = bt_32.query(Y_32, k=k)

    r = 2.38
    ind_64 = bt_64.query_radius(Y_64, r=r)
    ind_32 = bt_32.query_radius(Y_32, r=r)

    ind_64, dist_64 = bt_64.query_radius(Y_64, r=r, return_distance=True)
    ind_32, dist_32 = bt_32.query_radius(Y_32, r=r, return_distance=True)

    h = 0.1
    density64 = bt_64.kernel_density(Y_64, h=h, kernel=kernel, breadth_first=True)
    density32 = bt_32.kernel_density(Y_32, h=h, kernel=kernel, breadth_first=True)

def test_two_point_correlation_numerical_consistency(global_random_seed):
    # Test consistency with respect to the `two_point_correlation` method

    bt_64 = BallTree64(X_64, leaf_size=10)
    bt_32 = BallTree32(X_32, leaf_size=10)

    r = np.linspace(0, 1, 10)

    counts_64 = bt_64.two_point_correlation(Y_64, r=r, dualtree=True)
    counts_32 = bt_32.two_point_correlation(Y_32, r=r, dualtree=True)
    assert_allclose(counts_64, counts_32)

def get_dataset_for_binary_tree(random_seed, features=3):
    rng = np.random.RandomState(random_seed)
    _X = rng.rand(100, features)
    _Y = rng.rand(5, features)

    X_64 = _X.astype(dtype=np.float64, copy=False)
    Y_64 = _Y.astype(dtype=np.float64, copy=False)

    X_32 = _X.astype(dtype=np.float32, copy=False)
    Y_32 = _Y.astype(dtype=np.float32, copy=False)

    return X_64, X_32, Y_64, Y_32

def compute_kernel_slow(Y, X, kernel, h):
    d = np.sqrt(((Y[:, None, :] - X) ** 2).sum(-1))
    norm = kernel_norm(h, X.shape[1], kernel)

    if kernel == "gaussian":
        return norm * np.exp(-0.5 * (d * d) / (h * h)).sum(-1)
    elif kernel == "tophat":
        return norm * (d < h).sum(-1)
    elif kernel == "epanechnikov":
        return norm * ((1.0 - (d * d) / (h * h)) * (d < h)).sum(-1)
    elif kernel == "exponential":
        return norm * (np.exp(-d / h)).sum(-1)
    elif kernel == "linear":
        return norm * ((1 - d / h) * (d < h)).sum(-1)
    elif kernel == "cosine":
        return norm * (np.cos(0.5 * np.pi * d / h) * (d < h)).sum(-1)
    else:
        raise ValueError("kernel not recognized")

def test_kernel_density(
    Cls, kernel, h, rtol, atol, breadth_first, n_samples=100, n_features=3
):
    rng = check_random_state(1)
    X = rng.random_sample((n_samples, n_features))
    Y = rng.random_sample((n_samples, n_features))
    dens_true = compute_kernel_slow(Y, X, kernel, h)

    tree = Cls(X, leaf_size=10)
    dens = tree.kernel_density(
        Y, h, atol=atol, rtol=rtol, kernel=kernel, breadth_first=breadth_first
    )
    assert_allclose(dens, dens_true, atol=atol, rtol=max(rtol, 1e-7))

def test_neighbor_tree_query_radius(Cls, n_samples=100, n_features=10):
    X = 2 * rng.random_sample(size=(n_samples, n_features)) - 1
    query_pt = np.zeros(n_features, dtype=float)

    eps = 1e-15  # roundoff error can cause test to fail
    tree = Cls(X, leaf_size=5)
    rad = np.sqrt(((X - query_pt) ** 2).sum(1))

    for r in np.linspace(rad[0], rad[-1], 100):
        ind = tree.query_radius([query_pt], r + eps)[0]
        i = np.where(rad <= r + eps)[0]

        ind.sort()
        i.sort()

        assert_array_almost_equal(i, ind)

def test_neighbor_tree_query_radius_distance(Cls, n_samples=100, n_features=10):

        ind, dist = tree.query_radius([query_pt], r + eps, return_distance=True)

        ind = ind[0]
        dist = dist[0]

        d = np.sqrt(((query_pt - X[ind]) ** 2).sum(1))

        assert_array_almost_equal(d, dist)

def test_neighbor_tree_two_point(Cls, dualtree, n_samples=100, n_features=3):

    D = DistanceMetric.get_metric("euclidean").pairwise(Y, X)
    counts_true = [(D <= ri).sum() for ri in r]

    counts = tree.two_point_correlation(Y, r=r, dualtree=dualtree)
    assert_array_almost_equal(counts, counts_true)

def test_neighbors_heap(NeighborsHeap, n_pts=5, n_nbrs=10):
    heap = NeighborsHeap(n_pts, n_nbrs)

    for row in range(n_pts):
        d_in = rng.random_sample(2 * n_nbrs).astype(np.float64, copy=False)
        i_in = np.arange(2 * n_nbrs, dtype=np.intp)
        for d, i in zip(d_in, i_in):
            heap.push(row, d, i)

        ind = np.argsort(d_in)
        d_in = d_in[ind]
        i_in = i_in[ind]

        d_heap, i_heap = heap.get_arrays(sort=True)

        assert_array_almost_equal(d_in[:n_nbrs], d_heap[row])
        assert_array_almost_equal(i_in[:n_nbrs], i_heap[row])

def test_node_heap(nodeheap_sort, n_nodes=50):
    vals = rng.random_sample(n_nodes).astype(np.float64, copy=False)

    i1 = np.argsort(vals)
    vals2, i2 = nodeheap_sort(vals)

    assert_array_almost_equal(i1, i2)
    assert_array_almost_equal(vals[i1], vals2)

def test_simultaneous_sort(simultaneous_sort, n_rows=10, n_pts=201):
    dist = rng.random_sample((n_rows, n_pts)).astype(np.float64, copy=False)
    ind = (np.arange(n_pts) + np.zeros((n_rows, 1))).astype(np.intp, copy=False)

    dist2 = dist.copy()
    ind2 = ind.copy()

    # simultaneous sort rows using function
    simultaneous_sort(dist, ind)

    # simultaneous sort rows using numpy
    i = np.argsort(dist2, axis=1)
    row_ind = np.arange(n_rows)[:, None]
    dist2 = dist2[row_ind, i]
    ind2 = ind2[row_ind, i]

    assert_array_almost_equal(dist, dist2)
    assert_array_almost_equal(ind, ind2)

def test_gaussian_kde(Cls, n_samples=1000):
    # Compare gaussian KDE results to scipy.stats.gaussian_kde
    from scipy.stats import gaussian_kde

    x_in = rng.normal(0, 1, n_samples)
    x_out = np.linspace(-5, 5, 30)

    for h in [0.01, 0.1, 1]:
        tree = Cls(x_in[:, None])
        gkde = gaussian_kde(x_in, bw_method=h / np.std(x_in))

        dens_tree = tree.kernel_density(x_out[:, None], h) / n_samples
        dens_gkde = gkde.evaluate(x_out)

        assert_array_almost_equal(dens_tree, dens_gkde, decimal=3)

def test_nn_tree_query(Cls, metric, k, dualtree, breadth_first):
    X = rng.random_sample((40, DIMENSION))
    Y = rng.random_sample((10, DIMENSION))

    kwargs = METRICS[metric]

    kdt = Cls(X, leaf_size=1, metric=metric, **kwargs)
    dist1, ind1 = kdt.query(Y, k, dualtree=dualtree, breadth_first=breadth_first)
    dist2, ind2 = brute_force_neighbors(X, Y, k, metric, **kwargs)

    # don't check indices here: if there are any duplicate distances,
    # the indices may not match.  Distances should not have this problem.

def test_pickle(Cls, metric, protocol):

    if hasattr(metric, "__call__"):
        kwargs = {"p": 2}
    else:
        kwargs = {}

    tree1 = Cls(X, leaf_size=1, metric=metric, **kwargs)

    ind1, dist1 = tree1.query(X)

    s = pickle.dumps(tree1, protocol=protocol)
    tree2 = pickle.loads(s)

    ind2, dist2 = tree2.query(X)

    assert isinstance(tree2, Cls)

    if h == "scott":
        h = X.shape[0] ** (-1 / (X.shape[1] + 4))
    elif h == "silverman":
        h = (X.shape[0] * (X.shape[1] + 2) / 4) ** (-1 / (X.shape[1] + 4))

    norm = kernel_norm(h, X.shape[1], kernel) / X.shape[0]

    else:

def check_results(kernel, bandwidth, atol, rtol, X, Y, dens_true):
    kde = KernelDensity(kernel=kernel, bandwidth=bandwidth, atol=atol, rtol=rtol)
    log_dens = kde.fit(X).score_samples(Y)
    assert_allclose(np.exp(log_dens), dens_true, atol=atol, rtol=max(1e-7, rtol))
        np.exp(kde.score(Y)), np.prod(dens_true), atol=atol, rtol=max(1e-7, rtol)
    )

def test_kernel_density(kernel, bandwidth):
    n_samples, n_features = (100, 3)

    X = rng.randn(n_samples, n_features)
    Y = rng.randn(n_samples, n_features)

    dens_true = compute_kernel_slow(Y, X, kernel, bandwidth)

    for rtol in [0, 1e-5]:
        for atol in [1e-6, 1e-2]:
            for breadth_first in (True, False):
                check_results(kernel, bandwidth, atol, rtol, X, Y, dens_true)

def test_kernel_density_sampling(n_samples=100, n_features=3):

    bandwidth = 0.2

    for kernel in ["gaussian", "tophat"]:
        # draw a tophat sample
        kde = KernelDensity(bandwidth=bandwidth, kernel=kernel).fit(X)
        samp = kde.sample(100)
        assert X.shape == samp.shape

        # check that samples are in the right range
        nbrs = NearestNeighbors(n_neighbors=1).fit(X)
        dist, ind = nbrs.kneighbors(X, return_distance=True)

        if kernel == "tophat":
            assert np.all(dist < bandwidth)
        elif kernel == "gaussian":
            # 5 standard deviations is safe for 100 samples, but there's a
            # very small chance this test could fail.
            assert np.all(dist < 5 * bandwidth)

    # check unsupported kernels
    for kernel in ["epanechnikov", "exponential", "linear", "cosine"]:
        with pytest.raises(NotImplementedError):
            kde.sample(100)

    # non-regression test: used to return a scalar
    X = rng.randn(4, 1)
    kde = KernelDensity(kernel="gaussian").fit(X)
    assert kde.sample().shape == (1, 1)

def test_kde_algorithm_metric_choice(algorithm, metric):
    # Smoke test for various metrics and algorithms
    X = rng.randn(10, 2)  # 2 features required for haversine dist.
    Y = rng.randn(10, 2)

    kde = KernelDensity(algorithm=algorithm, metric=metric)

    if algorithm == "kd_tree" and metric not in KDTree.valid_metrics:
        with pytest.raises(ValueError, match="invalid metric"):
            kde.fit(X)
    else:
        kde.fit(X)
        y_dens = kde.score_samples(Y)
        assert y_dens.shape == Y.shape[:1]

def test_kde_sample_weights_error():
    kde = KernelDensity()
        kde.fit(np.random.random((200, 10)), sample_weight=np.random.random((200, 10)))
        kde.fit(np.random.random((200, 10)), sample_weight=-np.random.random(200))

def test_kde_pipeline_gridsearch():
    # test that kde plays nice in pipelines and grid-searches
    X, _ = make_blobs(cluster_std=0.1, random_state=1, centers=[[0, 1], [1, 0], [0, 0]])
    pipe1 = make_pipeline(
        StandardScaler(with_mean=False, with_std=False),
        KernelDensity(kernel="gaussian"),
    )
    params = dict(kerneldensity__bandwidth=[0.001, 0.01, 0.1, 1, 10])
    search = GridSearchCV(pipe1, param_grid=params)
    search.fit(X)
    assert search.best_params_["kerneldensity__bandwidth"] == 0.1

def test_kde_sample_weights():
    n_samples = 400
    size_test = 20
    weights_neutral = np.full(n_samples, 3.0)
    for d in [1, 2, 10]:
        X = rng.rand(n_samples, d)
        weights = 1 + (10 * X.sum(axis=1)).astype(np.int8)
        X_repetitions = np.repeat(X, weights, axis=0)
        n_samples_test = size_test // d
        test_points = rng.rand(n_samples_test, d)
        for algorithm in ["auto", "ball_tree", "kd_tree"]:
            for metric in ["euclidean", "minkowski", "manhattan", "chebyshev"]:
                if algorithm != "kd_tree" or metric in KDTree.valid_metrics:
                    kde = KernelDensity(algorithm=algorithm, metric=metric)

                    # Test that adding a constant sample weight has no effect
                    kde.fit(X, sample_weight=weights_neutral)
                    scores_const_weight = kde.score_samples(test_points)
                    sample_const_weight = kde.sample(random_state=1234)
                    kde.fit(X)
                    scores_no_weight = kde.score_samples(test_points)
                    sample_no_weight = kde.sample(random_state=1234)
                    assert_allclose(scores_const_weight, scores_no_weight)
                    assert_allclose(sample_const_weight, sample_no_weight)

                    # Test equivalence between sampling and (integer) weights
                    kde.fit(X, sample_weight=weights)
                    scores_weight = kde.score_samples(test_points)
                    sample_weight = kde.sample(random_state=1234)
                    kde.fit(X_repetitions)
                    scores_ref_sampling = kde.score_samples(test_points)
                    sample_ref_sampling = kde.sample(random_state=1234)
                    assert_allclose(scores_weight, scores_ref_sampling)
                    assert_allclose(sample_weight, sample_ref_sampling)

                    # Test that sample weights has a non-trivial effect
                    diff = np.max(np.abs(scores_no_weight - scores_weight))
                    assert diff > 0.001

                    # Test invariance with respect to arbitrary scaling
                    scale_factor = rng.rand()
                    kde.fit(X, sample_weight=(scale_factor * weights))
                    scores_scaled_weight = kde.score_samples(test_points)
                    assert_allclose(scores_scaled_weight, scores_weight)

def test_pickling(tmpdir, sample_weight):
    # Make sure that predictions are the same before and after pickling. Used
    # to be a bug because sample_weights wasn't pickled and the resulting tree
    # would miss some info.

    data = np.reshape([1.0, 2.0, 3.0], (-1, 1))
    kde.fit(data, sample_weight=sample_weight)

    X = np.reshape([1.1, 2.1], (-1, 1))
    scores = kde.score_samples(X)

    file_path = str(tmpdir.join("dump.pkl"))
    joblib.dump(kde, file_path)
    kde = joblib.load(file_path)
    scores_pickled = kde.score_samples(X)

    assert_allclose(scores, scores_pickled)

def test_check_is_fitted(method):
    # Check that predict raises an exception in an unfitted estimator.
    # Unfitted estimators should raise a NotFittedError.
    X = rng.randn(10, 2)

        getattr(kde, method)(X)

def test_bandwidth(bandwidth):
    kde = KernelDensity(bandwidth=bandwidth).fit(X)
    samp = kde.sample(100)
    kde_sc = kde.score_samples(X)
    assert X.shape == samp.shape
    assert kde_sc.shape == (n_samples,)

    # Test that the attribute self.bandwidth_ has the expected value
    if bandwidth == "scott":
    elif bandwidth == "silverman":
    else:
        h = bandwidth
    assert kde.bandwidth_ == pytest.approx(h)

def test_classification_toy(csr_container):
    # Check classification on a toy dataset, including sparse versions.
    T_csr = csr_container(T)

    clf = NearestCentroid()
    assert_array_almost_equal(clf.decision_function(T), true_discriminant_scores)
    assert_array_almost_equal(clf.predict_proba(T), true_proba)

    # Test uniform priors
    clf = NearestCentroid(priors="uniform")

    clf = NearestCentroid(priors="empirical")

    # Test custom priors
    clf = NearestCentroid(priors=[0.25, 0.75])
    assert_array_equal(clf.predict(T), true_result_prior1)

    # Same test, but with a sparse matrix to fit and test.
    clf.fit(X_csr, y)
    assert_array_equal(clf.predict(T_csr), true_result)

    # Fit with sparse, test with non-sparse

    # Fit with non-sparse, test with sparse

    # Fit and predict with non-CSR sparse matrices
    clf.fit(X_csr.tocoo(), y)
    assert_array_equal(clf.predict(T_csr.tolil()), true_result)

    for metric in ("euclidean", "manhattan"):
        clf = NearestCentroid(metric=metric).fit(iris.data, iris.target)
        score = np.mean(clf.predict(iris.data) == iris.target)
        assert score > 0.9, "Failed with score = " + str(score)

def test_iris_shrinkage():
    # Check consistency on dataset iris, when using shrinkage.
        for shrink_threshold in [None, 0.1, 0.5]:
            clf = NearestCentroid(metric=metric, shrink_threshold=shrink_threshold)
            clf = clf.fit(iris.data, iris.target)
            score = np.mean(clf.predict(iris.data) == iris.target)
            assert score > 0.8, "Failed with score = " + str(score)

    # classification
    obj = NearestCentroid()

        score,
        score2,
        "Failed to generate same score after pickling (classification).",
    )

def test_shrinkage_correct():
    # Ensure that the shrinking is correct.
    # The expected result is calculated by R (pamr),
    # which is implemented by the author of the original paper.
    # (One need to modify the code to output the new centroid in pamr.predict)

    X = np.array([[0, 1], [1, 0], [1, 1], [2, 0], [6, 8]])
    y = np.array([1, 1, 2, 2, 2])
    clf = NearestCentroid(shrink_threshold=0.1)
    expected_result = np.array([[0.7787310, 0.8545292], [2.814179, 2.763647]])
    np.testing.assert_array_almost_equal(clf.centroids_, expected_result)

def test_shrinkage_threshold_decoded_y():
    clf = NearestCentroid(shrink_threshold=0.01)
    y_ind = np.asarray(y)
    y_ind[y_ind == -1] = 0
    clf.fit(X, y_ind)
    centroid_encoded = clf.centroids_
    assert_array_equal(centroid_encoded, clf.centroids_)

def test_predict_translated_data():
    # Test that NearestCentroid gives same results on translated data

    X = rng.rand(50, 50)
    y = rng.randint(0, 3, 50)
    noise = rng.rand(50)
    y_init = clf.predict(X)
    X_noise = X + noise
    clf.fit(X_noise, y)
    y_translate = clf.predict(X_noise)
    assert_array_equal(y_init, y_translate)

def test_manhattan_metric(csr_container):
    # Test the manhattan metric.

    clf = NearestCentroid(metric="manhattan")
    dense_centroid = clf.centroids_
    assert_array_equal(clf.centroids_, dense_centroid)
    assert_array_equal(dense_centroid, [[-1, -1], [1, 1]])

def test_features_zero_var():
    # Test that features with 0 variance throw error

    X = np.empty((10, 2))
    X[:, 0] = -0.13725701
    X[:, 1] = -0.9853293
    y = np.zeros((10))
    y[0] = 1

def test_negative_priors_error():
    """Check that we raise an error when the user-defined priors are negative."""
    clf = NearestCentroid(priors=[-2, 4])
    with pytest.raises(ValueError, match="priors must be non-negative"):

def test_warn_non_normalized_priors():
    """Check that we raise a warning and normalize the user-defined priors when they
    don't sum to 1.
    """
    priors = [2, 4]
    clf = NearestCentroid(priors=priors)
        match="The priors do not sum to 1. Normalizing such that it sums to one.",
    ):

    assert_allclose(clf.class_prior_, np.asarray(priors) / np.asarray(priors).sum())

def test_method_not_available_with_manhattan(response_method):
    """Check that we raise an AttributeError with Manhattan metric when trying
    to call a non-thresholded response method.
    """
    clf = NearestCentroid(metric="manhattan").fit(X, y)
        getattr(clf, response_method)(T)

def test_error_zero_variances(array_constructor):
    """Check that we raise an error when the variance for all features is zero."""
    X = np.ones((len(y), 2))
    X[:, 1] *= 2
    X = array_constructor(X)

    with pytest.raises(ValueError, match="All features have zero variance"):

def test_transformer_result():
    # Test the number of neighbors returned
    n_samples_fit = 20
    n_queries = 18

    X = rng.randn(n_samples_fit, n_features)
    X2 = rng.randn(n_queries, n_features)
    radius = np.percentile(euclidean_distances(X), 10)

    # with n_neighbors
    for mode in ["distance", "connectivity"]:
        add_one = mode == "distance"
        nnt = KNeighborsTransformer(n_neighbors=n_neighbors, mode=mode)
        Xt = nnt.fit_transform(X)
        assert Xt.shape == (n_samples_fit, n_samples_fit)
        assert Xt.data.shape == (n_samples_fit * (n_neighbors + add_one),)
        assert Xt.format == "csr"
        assert _is_sorted_by_data(Xt)

        X2t = nnt.transform(X2)
        assert X2t.shape == (n_queries, n_samples_fit)
        assert X2t.data.shape == (n_queries * (n_neighbors + add_one),)
        assert X2t.format == "csr"
        assert _is_sorted_by_data(X2t)

    # with radius
        nnt = RadiusNeighborsTransformer(radius=radius, mode=mode)
        assert not Xt.data.shape == (n_samples_fit * (n_neighbors + add_one),)

        assert not X2t.data.shape == (n_queries * (n_neighbors + add_one),)

def _has_explicit_diagonal(X):
    """Return True if the diagonal is explicitly stored"""
    X = X.tocoo()
    explicit = X.row[X.row == X.col]
    return len(explicit) == X.shape[0]

def test_explicit_diagonal():
    # Test that the diagonal is explicitly stored in the sparse graph
    n_samples_fit, n_samples_transform, n_features = 20, 18, 10
    X2 = rng.randn(n_samples_transform, n_features)

    nnt = KNeighborsTransformer(n_neighbors=n_neighbors)
    Xt = nnt.fit_transform(X)
    assert _has_explicit_diagonal(Xt)
    assert np.all(Xt.data.reshape(n_samples_fit, n_neighbors + 1)[:, 0] == 0)

    Xt = nnt.transform(X)

    # Using transform on new data should not always have zero diagonal
    X2t = nnt.transform(X2)
    assert not _has_explicit_diagonal(X2t)

def test_graph_feature_names_out(Klass):
    """Check `get_feature_names_out` for transformers defined in `_graph.py`."""

    est = Klass().fit(X)

    class_name_lower = Klass.__name__.lower()
        [f"{class_name_lower}{i}" for i in range(est.n_samples_fit_)],
    )

def test_lof(global_dtype):
    # Toy sample (the last two samples are outliers):
    X = np.asarray(
        [[-2, -1], [-1, -1], [-1, -2], [1, 1], [1, 2], [2, 1], [5, 3], [-4, 2]],
    )

    # Test LocalOutlierFactor:
    clf = neighbors.LocalOutlierFactor(n_neighbors=5)
    score = clf.fit(X).negative_outlier_factor_
    assert_array_equal(clf._fit_X, X)

    # Assert largest outlier score is smaller than smallest inlier score:
    assert np.min(score[:-2]) > np.max(score[-2:])

    # Assert predict() works:
    clf = neighbors.LocalOutlierFactor(contamination=0.25, n_neighbors=5).fit(X)
    expected_predictions = 6 * [1] + 2 * [-1]
    assert_array_equal(clf._predict(), expected_predictions)
    assert_array_equal(clf.fit_predict(X), expected_predictions)

def test_lof_performance(global_dtype):
    rng = check_random_state(2)
    X = 0.3 * rng.randn(120, 2).astype(global_dtype, copy=False)
    X_train = X[:100]

    X_outliers = rng.uniform(low=-4, high=4, size=(20, 2)).astype(
        global_dtype, copy=False
    )
    X_test = np.r_[X[100:], X_outliers]
    y_test = np.array([0] * 20 + [1] * 20)

    # fit the model for novelty detection
    clf = neighbors.LocalOutlierFactor(novelty=True).fit(X_train)

    # check that roc_auc is good
    assert roc_auc_score(y_test, y_pred) > 0.99

def test_lof_values(global_dtype):
    # toy samples:
    X_train = np.asarray([[1, 1], [1, 2], [2, 1]], dtype=global_dtype)
    clf1 = neighbors.LocalOutlierFactor(
        n_neighbors=2, contamination=0.1, novelty=True
    ).fit(X_train)
    clf2 = neighbors.LocalOutlierFactor(n_neighbors=2, novelty=True).fit(X_train)
    s_0 = 2.0 * sqrt(2.0) / (1.0 + sqrt(2.0))
    s_1 = (1.0 + sqrt(2)) * (1.0 / (4.0 * sqrt(2.0)) + 1.0 / (2.0 + 2.0 * sqrt(2)))
    # check predict()
    assert_allclose(-clf1.negative_outlier_factor_, [s_0, s_1, s_1])
    assert_allclose(-clf2.negative_outlier_factor_, [s_0, s_1, s_1])
    # check predict(one sample not in train)
    assert_allclose(-clf1.score_samples([[2.0, 2.0]]), [s_0])
    assert_allclose(-clf2.score_samples([[2.0, 2.0]]), [s_0])
    # check predict(one sample already in train)
    assert_allclose(-clf1.score_samples([[1.0, 1.0]]), [s_1])
    assert_allclose(-clf2.score_samples([[1.0, 1.0]]), [s_1])

def test_lof_precomputed(global_dtype, random_state=42):
    """Tests LOF with a distance matrix."""
    Y = rng.random_sample((3, 4)).astype(global_dtype, copy=False)
    DXX = metrics.pairwise_distances(X, metric="euclidean")
    DYX = metrics.pairwise_distances(Y, X, metric="euclidean")
    # As a feature matrix (n_samples by n_features)
    lof_X = neighbors.LocalOutlierFactor(n_neighbors=3, novelty=True)
    lof_X.fit(X)
    pred_X_X = lof_X._predict()
    pred_X_Y = lof_X.predict(Y)

    # As a dense distance matrix (n_samples by n_samples)
    lof_D = neighbors.LocalOutlierFactor(
        n_neighbors=3, algorithm="brute", metric="precomputed", novelty=True
    )
    lof_D.fit(DXX)
    pred_D_X = lof_D._predict()
    pred_D_Y = lof_D.predict(DYX)

    assert_allclose(pred_X_X, pred_D_X)
    assert_allclose(pred_X_Y, pred_D_Y)

def test_n_neighbors_attribute():
    clf = neighbors.LocalOutlierFactor(n_neighbors=500).fit(X)
    assert clf.n_neighbors_ == X.shape[0] - 1

    clf = neighbors.LocalOutlierFactor(n_neighbors=500)
    msg = "n_neighbors will be set to (n_samples - 1)"
    with pytest.warns(UserWarning, match=re.escape(msg)):

def test_score_samples(global_dtype):
    X_test = np.asarray([[2.0, 2.0]], dtype=global_dtype)

    clf1_scores = clf1.score_samples(X_test)
    clf1_decisions = clf1.decision_function(X_test)

    clf2_scores = clf2.score_samples(X_test)
    clf2_decisions = clf2.decision_function(X_test)

        clf1_scores,
        clf1_decisions + clf1.offset_,
    )
        clf2_scores,
        clf2_decisions + clf2.offset_,
    )
    assert_allclose(clf1_scores, clf2_scores)

def test_novelty_errors():

    # check errors for novelty=False
    clf = neighbors.LocalOutlierFactor()
    # predict, decision_function and score_samples raise ValueError
    for method in ["predict", "decision_function", "score_samples"]:
        outer_msg = f"'LocalOutlierFactor' has no attribute '{method}'"
        inner_msg = "{} is not available when novelty=False".format(method)
        with pytest.raises(AttributeError, match=outer_msg) as exec_info:
            getattr(clf, method)

        assert isinstance(exec_info.value.__cause__, AttributeError)
        assert inner_msg in str(exec_info.value.__cause__)

    # check errors for novelty=True
    clf = neighbors.LocalOutlierFactor(novelty=True)

    outer_msg = "'LocalOutlierFactor' has no attribute 'fit_predict'"
    inner_msg = "fit_predict is not available when novelty=True"
        getattr(clf, "fit_predict")

def test_novelty_training_scores(global_dtype):
    # check that the scores of the training samples are still accessible
    # when novelty=True through the negative_outlier_factor_ attribute
    X = iris.data.astype(global_dtype)

    # fit with novelty=False
    clf_1 = neighbors.LocalOutlierFactor()
    clf_1.fit(X)
    scores_1 = clf_1.negative_outlier_factor_

    # fit with novelty=True
    clf_2 = neighbors.LocalOutlierFactor(novelty=True)
    clf_2.fit(X)
    scores_2 = clf_2.negative_outlier_factor_

    assert_allclose(scores_1, scores_2)

def test_hasattr_prediction():
    # check availability of prediction methods depending on novelty value.
    X = [[1, 1], [1, 2], [2, 1]]

    # when novelty=True
    assert hasattr(clf, "predict")
    assert hasattr(clf, "decision_function")
    assert hasattr(clf, "score_samples")
    assert not hasattr(clf, "fit_predict")

    # when novelty=False
    clf = neighbors.LocalOutlierFactor(novelty=False)
    assert hasattr(clf, "fit_predict")
    assert not hasattr(clf, "predict")
    assert not hasattr(clf, "decision_function")
    assert not hasattr(clf, "score_samples")

def test_novelty_true_common_tests(estimator, check):
    # the common tests are run for the default LOF (novelty=False).
    # here we run these common tests for LOF when novelty=True
    check(estimator)

def test_predicted_outlier_number(expected_outliers):
    # the number of predicted outliers should be equal to the number of
    # expected outliers unless there are ties in the abnormality scores.
    contamination = float(expected_outliers) / n_samples

    clf = neighbors.LocalOutlierFactor(contamination=contamination)
    y_pred = clf.fit_predict(X)

    num_outliers = np.sum(y_pred != 1)
    if num_outliers != expected_outliers:
        y_dec = clf.negative_outlier_factor_
        check_outlier_corruption(num_outliers, expected_outliers, y_dec)

def test_sparse(csr_container):
    # LocalOutlierFactor must support CSR inputs
    # TODO: compare results on dense and sparse data as proposed in:
    # https://github.com/scikit-learn/scikit-learn/pull/23585#discussion_r968388186
    X = csr_container(iris.data)

    lof = neighbors.LocalOutlierFactor(novelty=True)
    lof.fit(X)
    lof.predict(X)
    lof.score_samples(X)
    lof.decision_function(X)

    lof = neighbors.LocalOutlierFactor(novelty=False)
    lof.fit_predict(X)

def test_lof_error_n_neighbors_too_large():
    """Check that we raise a proper error message when n_neighbors == n_samples.

    https://github.com/scikit-learn/scikit-learn/issues/17207
    """
    X = np.ones((7, 7))

    msg = (
        "Expected n_neighbors < n_samples_fit, but n_neighbors = 1, "
        "n_samples_fit = 1, n_samples = 1"
    )
        lof = neighbors.LocalOutlierFactor(n_neighbors=1).fit(X[:1])

    lof = neighbors.LocalOutlierFactor(n_neighbors=2).fit(X[:2])
    assert lof.n_samples_fit_ == 2

    msg = (
        "Expected n_neighbors < n_samples_fit, but n_neighbors = 2, "
        "n_samples_fit = 2, n_samples = 2"
    )
        lof.kneighbors(None, n_neighbors=2)

    distances, indices = lof.kneighbors(None, n_neighbors=1)
    assert distances.shape == (2, 1)
    assert indices.shape == (2, 1)

    msg = (
        "Expected n_neighbors <= n_samples_fit, but n_neighbors = 3, "
        "n_samples_fit = 2, n_samples = 7"
    )
        lof.kneighbors(X, n_neighbors=3)

    (
        distances,
        indices,
    ) = lof.kneighbors(X, n_neighbors=2)
    assert distances.shape == (7, 2)
    assert indices.shape == (7, 2)

def test_lof_input_dtype_preservation(global_dtype, algorithm, contamination, novelty):
    """Check that the fitted attributes are stored using the data type of X."""
    X = iris.data.astype(global_dtype, copy=False)

    iso = neighbors.LocalOutlierFactor(
        n_neighbors=5, algorithm=algorithm, contamination=contamination, novelty=novelty
    )
    iso.fit(X)

    assert iso.negative_outlier_factor_.dtype == global_dtype

    for method in ("score_samples", "decision_function"):
        if hasattr(iso, method):
            y_pred = getattr(iso, method)(X)
            assert y_pred.dtype == global_dtype

def test_lof_dtype_equivalence(algorithm, novelty, contamination):
    """Check the equivalence of the results with 32 and 64 bits input."""

    inliers = iris.data[:50]  # setosa iris are really distinct from others
    outliers = iris.data[-5:]  # virginica will be considered as outliers
    # lower the precision of the input data to check that we have an equivalence when
    # making the computation in 32 and 64 bits.
    X = np.concatenate([inliers, outliers], axis=0).astype(np.float32)

    lof_32 = neighbors.LocalOutlierFactor(
        algorithm=algorithm, novelty=novelty, contamination=contamination
    )
    X_32 = X.astype(np.float32, copy=True)
    lof_32.fit(X_32)

    lof_64 = neighbors.LocalOutlierFactor(
    )
    X_64 = X.astype(np.float64, copy=True)
    lof_64.fit(X_64)

    assert_allclose(lof_32.negative_outlier_factor_, lof_64.negative_outlier_factor_)

    for method in ("score_samples", "decision_function", "predict", "fit_predict"):
        if hasattr(lof_32, method):
            y_pred_32 = getattr(lof_32, method)(X_32)
            y_pred_64 = getattr(lof_64, method)(X_64)
            assert_allclose(y_pred_32, y_pred_64, atol=0.0002)

def test_lof_duplicate_samples():
    """
    Check that LocalOutlierFactor raises a warning when duplicate values
    in the training data cause inaccurate results.

    https://github.com/scikit-learn/scikit-learn/issues/27839
    """

    x = rng.permutation(
        np.hstack(
            [
                [0.1] * 1000,  # constant values
                np.linspace(0.1, 0.3, num=3000),
                rng.random(500) * 100,  # the clear outliers
            ]
        )
    )
    X = x.reshape(-1, 1)

        "Duplicate values are leading to incorrect results. "
        "Increase the number of neighbors for more accurate results."
    )

    lof = neighbors.LocalOutlierFactor(n_neighbors=5, contamination=0.1)

    # Catch the warning
    with pytest.warns(UserWarning, match=re.escape(error_msg)):
        lof.fit_predict(X)

def test_quadtree_boundary_computation():
    # Introduce a point into a quad tree with boundaries not easy to compute.
    Xs = []

    # check a random case
    Xs.append(np.array([[-1, 1], [-4, -1]], dtype=np.float32))
    # check the case where only 0 are inserted
    Xs.append(np.array([[0, 0], [0, 0]], dtype=np.float32))
    # check the case where only negative are inserted
    Xs.append(np.array([[-1, -2], [-4, 0]], dtype=np.float32))
    # check the case where only small numbers are inserted
    Xs.append(np.array([[-1e-6, 1e-6], [-4e-6, -1e-6]], dtype=np.float32))

    for X in Xs:
        tree = _QuadTree(n_dimensions=2, verbose=0)
        tree.build_tree(X)
        tree._check_coherence()

def test_quadtree_similar_point():
    # Introduce a point into a quad tree where a similar point already exists.
    # Test will hang if it doesn't complete.
    Xs = []

    # check the case where points are actually different
    Xs.append(np.array([[1, 2], [3, 4]], dtype=np.float32))
    # check the case where points are the same on X axis
    Xs.append(np.array([[1.0, 2.0], [1.0, 3.0]], dtype=np.float32))
    # check the case where points are arbitrarily close on X axis
    Xs.append(np.array([[1.00001, 2.0], [1.00002, 3.0]], dtype=np.float32))
    # check the case where points are the same on Y axis
    Xs.append(np.array([[1.0, 2.0], [3.0, 2.0]], dtype=np.float32))
    # check the case where points are arbitrarily close on Y axis
    Xs.append(np.array([[1.0, 2.00001], [3.0, 2.00002]], dtype=np.float32))
    # check the case where points are arbitrarily close on both axes
    Xs.append(np.array([[1.00001, 2.00001], [1.00002, 2.00002]], dtype=np.float32))

    # close to machine epsilon - x axis
    Xs.append(np.array([[1, 0.0003817754041], [2, 0.0003817753750]], dtype=np.float32))

    # close to machine epsilon - y axis
    Xs.append(
        np.array([[0.0003817754041, 1.0], [0.0003817753750, 2.0]], dtype=np.float32)
    )

    for X in Xs:

def test_quad_tree_pickle(n_dimensions, protocol):

    X = rng.random_sample((10, n_dimensions))

    tree = _QuadTree(n_dimensions=n_dimensions, verbose=0)
    tree.build_tree(X)

    s = pickle.dumps(tree, protocol=protocol)
    bt2 = pickle.loads(s)

    for x in X:
        cell_x_tree = tree.get_cell(x)
        cell_x_bt2 = bt2.get_cell(x)
        assert cell_x_tree == cell_x_bt2

def test_qt_insert_duplicate(n_dimensions):

    Xd = np.r_[X, X[:5]]
    tree.build_tree(Xd)

    cumulative_size = tree.cumulative_size
    leafs = tree.leafs

    # Assert that the first 5 are indeed duplicated and that the next
    # ones are single point leaf
        cell_id = tree.get_cell(x)
        assert leafs[cell_id]
        assert cumulative_size[cell_id] == 1 + (i < 5)

def test_summarize():
    # Simple check for quad tree's summarize

    angle = 0.9
        [[-10.0, -10.0], [9.0, 10.0], [10.0, 9.0], [10.0, 10.0]], dtype=np.float32
    )
    query_pt = X[0, :]
    n_dimensions = X.shape[1]
    offset = n_dimensions + 2

    qt = _QuadTree(n_dimensions, verbose=0)
    qt.build_tree(X)

    idx, summary = qt._py_summarize(query_pt, X, angle)

    node_dist = summary[n_dimensions]
    node_size = summary[n_dimensions + 1]

    # Summary should contain only 1 node with size 3 and distance to
    # X[1:] barycenter
    barycenter = X[1:].mean(axis=0)
    ds2c = ((X[0] - barycenter) ** 2).sum()

    assert idx == offset
    assert node_size == 3, "summary size = {}".format(node_size)
    assert np.isclose(node_dist, ds2c)

    # Summary should contain all 3 node with size 1 and distance to
    # each point in X[1:] for ``angle=0``
    idx, summary = qt._py_summarize(query_pt, X, 0.0)

    assert idx == 3 * (offset)
    for i in range(3):
        node_dist = summary[i * offset + n_dimensions]
        node_size = summary[i * offset + n_dimensions + 1]

        ds2c = ((X[0] - X[i + 1]) ** 2).sum()

        assert node_size == 1, "summary size = {}".format(node_size)
        assert np.isclose(node_dist, ds2c)

def _check_inputs_dtype(X, missing_values):
    if is_pandas_na(missing_values):
        # Allow using `pd.NA` as missing values to impute numerical arrays.
        return
    if X.dtype.kind in ("f", "i", "u") and not isinstance(missing_values, numbers.Real):
            "'X' and 'missing_values' types are expected to be"
            " both numerical. Got X.dtype={} and "
            " type(missing_values)={}.".format(X.dtype, type(missing_values))
        )

def _most_frequent(array, extra_value, n_repeat):
    """Compute the most frequent value in a 1d array extended with
    [extra_value] * n_repeat, where extra_value is assumed to be not part
    of the array."""
    # Compute the most frequent value in array only
    if array.size > 0:
        if array.dtype == object:
            # scipy.stats.mode is slow with object dtype array.
            # Python Counter is more efficient
            counter = Counter(array)
            most_frequent_count = counter.most_common(1)[0][1]
            # tie breaking similarly to scipy.stats.mode
            most_frequent_value = min(
                value
                for value, count in counter.items()
                if count == most_frequent_count
            )
        else:
            mode = _mode(array)
            most_frequent_value = mode[0][0]
            most_frequent_count = mode[1][0]
    else:
        most_frequent_value = 0
        most_frequent_count = 0

    # Compare to array + [extra_value] * n_repeat
    if most_frequent_count == 0 and n_repeat == 0:
    elif most_frequent_count < n_repeat:
        return extra_value
    elif most_frequent_count > n_repeat:
        return most_frequent_value
    elif most_frequent_count == n_repeat:
        # tie breaking similarly to scipy.stats.mode
        return min(most_frequent_value, extra_value)

class _BaseImputer(TransformerMixin, BaseEstimator):
    """Base class for all imputers.

    It adds automatically support for `add_indicator`.
    """

        "missing_values": [MissingValues()],
        "add_indicator": ["boolean"],
        "keep_empty_features": ["boolean"],
    }

        self, *, missing_values=np.nan, add_indicator=False, keep_empty_features=False
    ):
        self.missing_values = missing_values
        self.add_indicator = add_indicator
        self.keep_empty_features = keep_empty_features

    def _fit_indicator(self, X):
        """Fit a MissingIndicator."""
        if self.add_indicator:
            self.indicator_ = MissingIndicator(
                missing_values=self.missing_values, error_on_new=False
            )
            self.indicator_._fit(X, precomputed=True)
        else:
            self.indicator_ = None

    def _transform_indicator(self, X):
        """Compute the indicator mask.'

        Note that X must be the original data as passed to the imputer before
        any imputation, since imputation may be done inplace in some cases.
        """
            if not hasattr(self, "indicator_"):
                    "Make sure to call _fit_indicator before _transform_indicator"
                )
            return self.indicator_.transform(X)

    def _concatenate_indicator(self, X_imputed, X_indicator):
        """Concatenate indicator mask with the imputed data."""
        if not self.add_indicator:
            return X_imputed

        if sp.issparse(X_imputed):
            # sp.hstack may result in different formats between sparse arrays and
            # matrices; specify the format to keep consistent behavior
            hstack = partial(sp.hstack, format=X_imputed.format)
        else:
            hstack = np.hstack

        if X_indicator is None:
                "Data from the missing indicator are not provided. Call "
                "_fit_indicator and _transform_indicator in the imputer "
                "implementation."
            )

        return hstack((X_imputed, X_indicator))

    def _concatenate_indicator_feature_names_out(self, names, input_features):
            return names

        indicator_names = self.indicator_.get_feature_names_out(input_features)
        return np.concatenate([names, indicator_names])

        tags.input_tags.allow_nan = is_scalar_nan(self.missing_values)

class SimpleImputer(_BaseImputer):
    """Univariate imputer for completing missing values with simple strategies.

    Replace missing values using a descriptive statistic (e.g. mean, median, or
    most frequent) along each column, or using a constant value.

    Read more in the :ref:`User Guide <impute>`.

       `SimpleImputer` replaces the previous `sklearn.preprocessing.Imputer`
       estimator which is now removed.

    missing_values : int, float, str, np.nan, None or pandas.NA, default=np.nan
        The placeholder for the missing values. All occurrences of
        `missing_values` will be imputed. For pandas' dataframes with
        nullable integer dtypes with missing values, `missing_values`
        can be set to either `np.nan` or `pd.NA`.

    strategy : str or Callable, default='mean'
        The imputation strategy.

        - If "mean", then replace missing values using the mean along
          each column. Can only be used with numeric data.
        - If "median", then replace missing values using the median along
        - If "most_frequent", then replace missing using the most frequent
          value along each column. Can be used with strings or numeric data.
          If there is more than one such value, only the smallest is returned.
        - If "constant", then replace missing values with fill_value. Can be
          used with strings or numeric data.
        - If an instance of Callable, then replace missing values using the
          scalar statistic returned by running the callable over a dense 1d
          array containing non-missing values of each column.

           strategy="constant" for fixed value imputation.

           strategy=callable for custom value imputation.

    fill_value : str or numerical value, default=None
        When strategy == "constant", `fill_value` is used to replace all
        occurrences of missing_values. For string or object data types,
        `fill_value` must be a string.
        If `None`, `fill_value` will be 0 when imputing numerical
        data and "missing_value" for strings or object data types.

        If True, a copy of X will be created. If False, imputation will
        be done in-place whenever possible. Note that, in the following cases,
        a new copy will always be made, even if `copy=False`:

        - If `X` is not an array of floating values;
        - If `X` is encoded as a CSR matrix;
        - If `add_indicator=True`.

    add_indicator : bool, default=False
        If True, a :class:`MissingIndicator` transform will stack onto output
        of the imputer's transform. This allows a predictive estimator
        to account for missingness despite imputation. If a feature has no
        missing values at fit/train time, the feature won't appear on
        the missing indicator even if there are missing values at
        transform/test time.

    keep_empty_features : bool, default=False
        If True, features that consist exclusively of missing values when
        `fit` is called are returned in results when `transform` is called.
        The imputed value is always `0` except when `strategy="constant"`
        in which case `fill_value` will be used instead.

            Currently, when `keep_empty_feature=False` and `strategy="constant"`,
            empty features are not dropped. This behaviour will change in version
            1.8. Set `keep_empty_feature=True` to preserve this behaviour.

    statistics_ : array of shape (n_features,)
        The imputation fill value for each feature.
        Computing statistics can result in `np.nan` values.
        During :meth:`transform`, features corresponding to `np.nan`
        statistics will be discarded.

    indicator_ : :class:`~sklearn.impute.MissingIndicator`
        Indicator used to add binary indicators for missing values.
        `None` if `add_indicator=False`.

    See Also
    --------
    IterativeImputer : Multivariate imputer that estimates values to impute for
        each feature with missing values from all the others.
    KNNImputer : Multivariate imputer that estimates missing features using
        nearest samples.

    Notes
    -----
    Columns which only contained missing values at :meth:`fit` are discarded
    upon :meth:`transform` if strategy is not `"constant"`.

    In a prediction context, simple imputation usually performs poorly when
    associated with a weak learner. However, with a powerful learner, it can
    lead to as good or better performance than complex imputation such as
    :class:`~sklearn.impute.IterativeImputer` or :class:`~sklearn.impute.KNNImputer`.

    Examples
    --------
    >>> from sklearn.impute import SimpleImputer
    >>> imp_mean = SimpleImputer(missing_values=np.nan, strategy='mean')
    >>> imp_mean.fit([[7, 2, 3], [4, np.nan, 6], [10, 5, 9]])
    SimpleImputer()
    >>> X = [[np.nan, 2, 3], [4, np.nan, 6], [10, np.nan, 9]]
    >>> print(imp_mean.transform(X))
    [[ 7.   2.   3. ]
     [ 4.   3.5  6. ]
     [10.   3.5  9. ]]

    For a more detailed example see
    :ref:`sphx_glr_auto_examples_impute_plot_missing_values.py`.
    """

        **_BaseImputer._parameter_constraints,
            StrOptions({"mean", "median", "most_frequent", "constant"}),
            callable,
        ],
        "fill_value": "no_validation",  # any object is valid
        "copy": ["boolean"],
    }

        self,
        *,
        missing_values=np.nan,
        strategy="mean",
        fill_value=None,
        copy=True,
        add_indicator=False,
        keep_empty_features=False,
    ):
            missing_values=missing_values,
            add_indicator=add_indicator,
            keep_empty_features=keep_empty_features,
        )
        self.fill_value = fill_value
        self.copy = copy

    def _validate_input(self, X, in_fit):
        if self.strategy in ("most_frequent", "constant"):
            # If input is a list of strings, dtype = object.
            # Otherwise ValueError is raised in SimpleImputer
            # with strategy='most_frequent' or 'constant'
            # because the list is converted to Unicode numpy array
            if isinstance(X, list) and any(
                isinstance(elem, str) for row in X for elem in row
            ):
                dtype = object
            else:
                dtype = None
        else:
            dtype = FLOAT_DTYPES

        if not in_fit and self._fit_dtype.kind == "O":
            # Use object dtype if fitted on object dtypes
            dtype = self._fit_dtype

        if is_pandas_na(self.missing_values) or is_scalar_nan(self.missing_values):
        else:

        try:
                self,
                X,
                reset=in_fit,
                accept_sparse="csc",
                force_writeable=True if not in_fit else None,
                copy=self.copy,
            )
        except ValueError as ve:
            if "could not convert" in str(ve):
                new_ve = ValueError(
                    "Cannot use {} strategy with non-numeric data:\n{}".format(
                        self.strategy, ve
                    )
                )
                raise new_ve from None
            else:
                raise ve

        if in_fit:
            # Use the dtype seen in `fit` for non-`fit` conversion
            self._fit_dtype = X.dtype

        _check_inputs_dtype(X, self.missing_values)
        if X.dtype.kind not in ("i", "u", "f", "O"):
                "SimpleImputer does not support data with dtype "
                "{0}. Please provide either a numeric array (with"
                " a floating point or integer dtype) or "
                "categorical data represented either as an array "
                "with integer dtype or an array of string values "
                "with an object dtype.".format(X.dtype)
            )

        if sp.issparse(X) and self.missing_values == 0:
            # missing_values = 0 not allowed with sparse data as it would
            # force densification
                "Imputation not possible when missing_values "
                "== 0 and input is sparse. Provide a dense "
                "array instead."
            )

        if self.strategy == "constant":
            if in_fit and self.fill_value is not None:
                fill_value_dtype = type(self.fill_value)
                err_msg = (
                    f"fill_value={self.fill_value!r} (of type {fill_value_dtype!r}) "
                    f"cannot be cast to the input data that is {X.dtype!r}. Make sure "
                    "that both dtypes are of the same kind."
                )
            elif not in_fit:
                fill_value_dtype = self.statistics_.dtype
                err_msg = (
                    f"The dtype of the filling value (i.e. {fill_value_dtype!r}) "
                    "that the dtypes of the input data is of the same kind between "
                    "fit and transform."
                )
            else:
                # By default, fill_value=None, and the replacement is always
                # compatible with the input data
                fill_value_dtype = X.dtype

            # Make sure we can safely cast fill_value dtype to the input data dtype
            if not np.can_cast(fill_value_dtype, X.dtype, casting="same_kind"):
                raise ValueError(err_msg)

        return X

        """Fit the imputer on `X`.

            Input data, where `n_samples` is the number of samples and

        y : Ignored

        Returns
        -------
        """
        X = self._validate_input(X, in_fit=True)

        # default fill_value is 0 for numerical input and "missing_value"
        # otherwise
        if self.fill_value is None:
            if X.dtype.kind in ("i", "u", "f"):
                fill_value = 0
            else:
                fill_value = "missing_value"
        else:
            fill_value = self.fill_value

            self.statistics_ = self._sparse_fit(
                X, self.strategy, self.missing_values, fill_value
            )
        else:
            self.statistics_ = self._dense_fit(
            )

    def _sparse_fit(self, X, strategy, missing_values, fill_value):
        """Fit the transformer on sparse data."""
        missing_mask = _get_mask(X, missing_values)
        mask_data = missing_mask.data
        n_implicit_zeros = X.shape[0] - np.diff(X.indptr)

        statistics = np.empty(X.shape[1])

        if strategy == "constant":
            # TODO(1.8): Remove FutureWarning and add `np.nan` as a statistic
            # for empty features to drop them later.
            if not self.keep_empty_features and any(
                [all(missing_mask[:, i].data) for i in range(missing_mask.shape[1])]
            ):
                    "Currently, when `keep_empty_feature=False` and "
                    '`strategy="constant"`, empty features are not dropped. '
                    "This behaviour will change in version 1.8. Set "
                    "`keep_empty_feature=True` to preserve this behaviour.",
                    FutureWarning,
                )

            # for constant strategy, self.statistics_ is used to store
            # fill_value in each column
            statistics.fill(fill_value)
        else:
            for i in range(X.shape[1]):
                column = X.data[X.indptr[i] : X.indptr[i + 1]]
                mask_column = mask_data[X.indptr[i] : X.indptr[i + 1]]
                column = column[~mask_column]

                # combine explicit and implicit zeros
                mask_zeros = _get_mask(column, 0)
                column = column[~mask_zeros]
                n_explicit_zeros = mask_zeros.sum()
                n_zeros = n_implicit_zeros[i] + n_explicit_zeros

                if len(column) == 0 and self.keep_empty_features:
                    # in case we want to keep columns with only missing values.
                    statistics[i] = 0
                else:
                    if strategy == "mean":
                        s = column.size + n_zeros
                        statistics[i] = np.nan if s == 0 else column.sum() / s

                    elif strategy == "median":
                        statistics[i] = _get_median(column, n_zeros)

                    elif strategy == "most_frequent":
                        statistics[i] = _most_frequent(column, 0, n_zeros)

                    elif isinstance(strategy, Callable):
                        statistics[i] = self.strategy(column)

        super()._fit_indicator(missing_mask)

        return statistics

    def _dense_fit(self, X, strategy, missing_values, fill_value):
        """Fit the transformer on dense data."""
        masked_X = ma.masked_array(X, mask=missing_mask)

        # Mean
        if strategy == "mean":
            mean_masked = np.ma.mean(masked_X, axis=0)
            # Avoid the warning "Warning: converting a masked element to nan."
            mean = np.ma.getdata(mean_masked)
            mean[np.ma.getmask(mean_masked)] = 0 if self.keep_empty_features else np.nan

            return mean

        # Median
        elif strategy == "median":
            median_masked = np.ma.median(masked_X, axis=0)
            median = np.ma.getdata(median_masked)
            median[np.ma.getmaskarray(median_masked)] = (
                0 if self.keep_empty_features else np.nan
            )

            return median

        # Most frequent
        elif strategy == "most_frequent":
            # Avoid use of scipy.stats.mstats.mode due to the required
            # additional overhead and slow benchmarking performance.
            # See Issue 14325 and PR 14399 for full discussion.

            # To be able access the elements by columns
            X = X.transpose()
            mask = missing_mask.transpose()

            if X.dtype.kind == "O":
                most_frequent = np.empty(X.shape[0], dtype=object)
            else:
                most_frequent = np.empty(X.shape[0])

            for i, (row, row_mask) in enumerate(zip(X[:], mask[:])):
                row_mask = np.logical_not(row_mask).astype(bool)
                row = row[row_mask]
                if len(row) == 0 and self.keep_empty_features:
                    most_frequent[i] = 0
                else:
                    most_frequent[i] = _most_frequent(row, np.nan, 0)

            return most_frequent

        # Constant
        elif strategy == "constant":
            if not self.keep_empty_features and ma.getmask(masked_X).all(axis=0).any():
                )

            # for constant strategy, self.statistcs_ is used to store
            return np.full(X.shape[1], fill_value, dtype=X.dtype)

        # Custom
        elif isinstance(strategy, Callable):
            statistics = np.empty(masked_X.shape[1])
            for i in range(masked_X.shape[1]):
                statistics[i] = self.strategy(masked_X[:, i].compressed())
            return statistics

        """Impute all missing values in `X`.

            The input data to complete.

        Returns
        -------
        X_imputed : {ndarray, sparse matrix} of shape \
                (n_samples, n_features_out)
            `X` with imputed values.
        """

        X = self._validate_input(X, in_fit=False)
        statistics = self.statistics_

        if X.shape[1] != statistics.shape[0]:
                "X has %d features per sample, expected %d"
                % (X.shape[1], self.statistics_.shape[0])
            )

        # compute mask before eliminating invalid features
        missing_mask = _get_mask(X, self.missing_values)

        # Decide whether to keep missing features
        if self.strategy == "constant" or self.keep_empty_features:
            valid_statistics = statistics
            valid_statistics_indexes = None
        else:
            # same as np.isnan but also works for object dtypes
            invalid_mask = _get_mask(statistics, np.nan)
            valid_mask = np.logical_not(invalid_mask)
            valid_statistics = statistics[valid_mask]
            valid_statistics_indexes = np.flatnonzero(valid_mask)

            if invalid_mask.any():
                invalid_features = np.arange(X.shape[1])[invalid_mask]
                # use feature names warning if features are provided
                if hasattr(self, "feature_names_in_"):
                    invalid_features = self.feature_names_in_[invalid_features]
                    "Skipping features without any observed values:"
                    f" {invalid_features}. At least one non-missing value is needed"
                    f" for imputation with strategy='{self.strategy}'."
                )
                X = X[:, valid_statistics_indexes]

        # Do actual imputation
            if self.missing_values == 0:
                    "Imputation not possible when missing_values "
                    "== 0 and input is sparse. Provide a dense "
                    "array instead."
                )
            else:
                # if no invalid statistics are found, use the mask computed
                # before, else recompute mask
                if valid_statistics_indexes is None:
                    mask = missing_mask.data
                else:
                    mask = _get_mask(X.data, self.missing_values)
                indexes = np.repeat(
                    np.arange(len(X.indptr) - 1, dtype=int), np.diff(X.indptr)
                )[mask]

                X.data[mask] = valid_statistics[indexes].astype(X.dtype, copy=False)
        else:
            # use mask computed before eliminating invalid mask
            if valid_statistics_indexes is None:
                mask_valid_features = missing_mask
            else:
                mask_valid_features = missing_mask[:, valid_statistics_indexes]
            n_missing = np.sum(mask_valid_features, axis=0)
            values = np.repeat(valid_statistics, n_missing)
            coordinates = np.where(mask_valid_features.transpose())[::-1]

            X[coordinates] = values

        X_indicator = super()._transform_indicator(missing_mask)

        return super()._concatenate_indicator(X, X_indicator)

        """Convert the data back to the original representation.

        Inverts the `transform` operation performed on an array.
        This operation can only be performed after :class:`SimpleImputer` is
        instantiated with `add_indicator=True`.

        Note that `inverse_transform` can only invert the transform in
        features that have binary indicators for missing values. If a feature
        has no missing values at `fit` time, the feature won't have a binary
        indicator, and the imputation done at `transform` time won't be
        inverted.

        X : array-like of shape \
                (n_samples, n_features + n_features_missing_indicator)
            The imputed data to be reverted to original data. It has to be
            an augmented array of imputed data and the missing indicator mask.

        Returns
        -------
            The original `X` with missing values as it was prior
            to imputation.
        """

                "'inverse_transform' works only when "
                "'SimpleImputer' is instantiated with "
                "'add_indicator=True'. "
                f"Got 'add_indicator={self.add_indicator}' "
                "instead."
            )

        n_features_missing = len(self.indicator_.features_)
        non_empty_feature_count = X.shape[1] - n_features_missing
        array_imputed = X[:, :non_empty_feature_count].copy()
        missing_mask = X[:, non_empty_feature_count:].astype(bool)

        n_features_original = len(self.statistics_)
        shape_original = (X.shape[0], n_features_original)
        X_original = np.zeros(shape_original)
        X_original[:, self.indicator_.features_] = missing_mask
        full_mask = X_original.astype(bool)

        imputed_idx, original_idx = 0, 0
        while imputed_idx < len(array_imputed.T):
            if not np.all(X_original[:, original_idx]):
                X_original[:, original_idx] = array_imputed.T[imputed_idx]
                imputed_idx += 1
                original_idx += 1
            else:

        X_original[full_mask] = self.missing_values
        return X_original

        tags.input_tags.allow_nan = is_pandas_na(self.missing_values) or is_scalar_nan(
            self.missing_values
        )

        Returns
        -------
        """
        input_features = _check_feature_names_in(self, input_features)
        non_missing_mask = np.logical_not(_get_mask(self.statistics_, np.nan))
        names = input_features[non_missing_mask]
        return self._concatenate_indicator_feature_names_out(names, input_features)

class MissingIndicator(TransformerMixin, BaseEstimator):
    """Binary indicators for missing values.

    Note that this component typically should not be used in a vanilla
    :class:`~sklearn.pipeline.Pipeline` consisting of transformers and a
    classifier, but rather could be added using a
    :class:`~sklearn.pipeline.FeatureUnion` or
    :class:`~sklearn.compose.ColumnTransformer`.

    missing_values : int, float, str, np.nan or None, default=np.nan
        should be set to `np.nan`, since `pd.NA` will be converted to `np.nan`.

    features : {'missing-only', 'all'}, default='missing-only'
        Whether the imputer mask should represent all or a subset of
        features.

        - If `'missing-only'` (default), the imputer mask will only represent
          features containing missing values during fit time.
        - If `'all'`, the imputer mask will represent all features.

    sparse : bool or 'auto', default='auto'
        Whether the imputer mask format should be sparse or dense.

        - If `'auto'` (default), the imputer mask will be of same type as
          input.
        - If `True`, the imputer mask will be a sparse matrix.
        - If `False`, the imputer mask will be a numpy array.

    error_on_new : bool, default=True
        If `True`, :meth:`transform` will raise an error when there are
        features with missing values that have no missing values in
        :meth:`fit`. This is applicable only when `features='missing-only'`.

    features_ : ndarray of shape (n_missing_features,) or (n_features,)
        The features indices which will be returned when calling
        :meth:`transform`. They are computed during :meth:`fit`. If
        `features='all'`, `features_` is equal to `range(n_features)`.

    See Also
    --------
    SimpleImputer : Univariate imputation of missing values.
    IterativeImputer : Multivariate imputation of missing values.

    Examples
    --------
    >>> from sklearn.impute import MissingIndicator
    >>> X1 = np.array([[np.nan, 1, 3],
    ...                [4, 0, np.nan],
    ...                [8, 1, 0]])
    >>> X2 = np.array([[5, 1, np.nan],
    ...                [np.nan, 2, 3],
    ...                [2, 4, 0]])
    >>> indicator = MissingIndicator()
    >>> indicator.fit(X1)
    MissingIndicator()
    >>> X2_tr = indicator.transform(X2)
    >>> X2_tr
    array([[False,  True],
           [ True, False],
           [False, False]])
    """

        "features": [StrOptions({"missing-only", "all"})],
        "sparse": ["boolean", StrOptions({"auto"})],
        "error_on_new": ["boolean"],
    }

        self,
        *,
        features="missing-only",
        sparse="auto",
        error_on_new=True,
    ):
        self.features = features
        self.error_on_new = error_on_new

    def _get_missing_features_info(self, X):
        """Compute the imputer mask and the indices of the features
        containing missing values.

            The input data with missing values. Note that `X` has been
            checked in :meth:`fit` and :meth:`transform` before to call this
            function.

        Returns
        -------
        imputer_mask : {ndarray, sparse matrix} of shape \
        (n_samples, n_features)
            The imputer mask of the original data.

        features_with_missing : ndarray of shape (n_features_with_missing)
            The features containing missing values.
        """
        if not self._precomputed:
            imputer_mask = _get_mask(X, self.missing_values)
        else:
            imputer_mask = X

            imputer_mask.eliminate_zeros()

            if self.features == "missing-only":
                # count number of True values in each row.
                n_missing = imputer_mask.sum(axis=0)

            if self.sparse is False:
                imputer_mask = imputer_mask.toarray()
            elif imputer_mask.format == "csr":
                imputer_mask = imputer_mask.tocsc()
        else:
            if not self._precomputed:
                imputer_mask = _get_mask(X, self.missing_values)
            else:
                imputer_mask = X

            if self.sparse is True:
                imputer_mask = sp.csc_matrix(imputer_mask)

        if self.features == "all":
            features_indices = np.arange(X.shape[1])
        else:
            features_indices = np.flatnonzero(n_missing)

        return imputer_mask, features_indices

        if not is_scalar_nan(self.missing_values):
        else:
            self,
            X,
            reset=in_fit,
            accept_sparse=("csc", "csr"),
        )
                "MissingIndicator does not support data with "
                "dtype {0}. Please provide either a numeric array"
                " (with a floating point or integer dtype) or "
            )

                "Sparse input with missing_values=0 is "
                "not supported. Provide a dense "
            )

        return X

    def _fit(self, X, y=None, precomputed=False):
        """Fit the transformer on `X`.

            If `precomputed=True`, then `X` is a mask of the input data.

        precomputed : bool
            Whether the input data is a mask.

        Returns
        -------
        imputer_mask : {ndarray, sparse matrix} of shape (n_samples, \
        n_features)
        """
        if precomputed:
            if not (hasattr(X, "dtype") and X.dtype.kind == "b"):
                raise ValueError("precomputed is True but the input data is not a mask")
            self._precomputed = True
        else:
            self._precomputed = False

        # Need not validate X again as it would have already been validated
        # in the Imputer calling MissingIndicator
            X = self._validate_input(X, in_fit=True)
        else:
            # only create `n_features_in_` in the precomputed case
            _check_n_features(self, X, reset=True)

        self._n_features = X.shape[1]

        missing_features_info = self._get_missing_features_info(X)
        self.features_ = missing_features_info[1]

        return missing_features_info[0]

        y : Ignored

        Returns
        -------
        """

        """Generate missing values indicator for `X`.

        Returns
        -------
        Xt : {ndarray, sparse matrix} of shape (n_samples, n_features) \
        or (n_samples, n_features_with_missing)
            The missing indicator for input data. The data type of `Xt`
            will be boolean.
        """

            X = self._validate_input(X, in_fit=False)
        else:

        imputer_mask, features = self._get_missing_features_info(X)

        if self.features == "missing-only":
            features_diff_fit_trans = np.setdiff1d(features, self.features_)
            if self.error_on_new and features_diff_fit_trans.size > 0:
                    "The features {} have missing values "
                    "in transform but have no missing values "
                    "in fit.".format(features_diff_fit_trans)
                )

            if self.features_.size < self._n_features:
                imputer_mask = imputer_mask[:, self.features_]

        return imputer_mask

        y : Ignored

        Returns
        -------
        """
        imputer_mask = self._fit(X, y)

        if self.features_.size < self._n_features:
            imputer_mask = imputer_mask[:, self.features_]

        Returns
        -------
        """
        prefix = self.__class__.__name__.lower()
            [
                f"{prefix}_{feature_name}"
                for feature_name in input_features[self.features_]
            ],
        )

    ):

            )
        else:

        """
                )

        else:

            )

        self,
        *,
    ):
        )

            ):
            else:
        else:

        else:

        try:
                self,
                X,
            )
                    )
                )
            else:
                raise ve

        if in_fit:

            )

            )

                err_msg = (
                )
                err_msg = (
                )
            else:

        return X

        y : Ignored

        Returns
        -------
        """

            else:
        else:

            )
        else:
            )

            ):
                )

        else:

                else:

        # Mean

        # Median
            )

            else:

                else:

        # Constant
                )

        # Custom

        Returns
        -------
        """

            )

        else:

                )

                )
            else:
                else:
                )[mask]

        else:
            else:

        inverted.

        Returns
        -------
        """

            )

            else:

        )

        Returns
        -------
        """

        self,
        *,
    ):

            function.

        Returns
        -------

        """
        else:

        else:
            else:

        else:

        else:
            self,
            X,
        )
            )

            )

        return X

        Returns
        -------
        """
        else:

        else:

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

        else:

                )

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """
            [
            ],
        )

class KNNImputer(_BaseImputer):
    """Imputation for completing missing values using k-Nearest Neighbors.

    Each sample's missing values are imputed using the mean value from
    `n_neighbors` nearest neighbors found in the training set. Two samples are
    close if the features that neither is missing are close.

    Read more in the :ref:`User Guide <knnimpute>`.

        should be set to np.nan, since `pd.NA` will be converted to np.nan.

        Number of neighboring samples to use for imputation.

    weights : {'uniform', 'distance'} or callable, default='uniform'

        - 'uniform' : uniform weights. All points in each neighborhood are
          weighted equally.
        - callable : a user-defined function which accepts an

    metric : {'nan_euclidean'} or callable, default='nan_euclidean'
        Distance metric for searching neighbors. Possible values:

        - 'nan_euclidean'
        - callable : a user-defined function which conforms to the definition
          of ``func_metric(x, y, *, missing_values=np.nan)``. `x` and `y`
          corresponds to a row (i.e. 1-D arrays) of `X` and `Y`, respectively.
          The callable should returns a scalar distance value.

        be done in-place whenever possible.

        If True, a :class:`MissingIndicator` transform will stack onto the
        output of the imputer's transform. This allows a predictive estimator
        missing values at fit/train time, the feature won't appear on the
        missing indicator even if there are missing values at transform/test
        time.

        The imputed value is always `0`.

        ``None`` if add_indicator is False.

    See Also
    --------
    SimpleImputer : Univariate imputer for completing missing values
        with simple strategies.

    * `Olga Troyanskaya, Michael Cantor, Gavin Sherlock, Pat Brown, Trevor
      Hastie, Robert Tibshirani, David Botstein and Russ B. Altman, Missing
      value estimation methods for DNA microarrays, BIOINFORMATICS Vol. 17
      no. 6, 2001 Pages 520-525.
      <https://academic.oup.com/bioinformatics/article/17/6/520/272365>`_

    Examples
    --------
    >>> from sklearn.impute import KNNImputer
    >>> X = [[1, 2, np.nan], [3, 4, 3], [np.nan, 6, 5], [8, 8, 7]]
    >>> imputer = KNNImputer(n_neighbors=2)
    >>> imputer.fit_transform(X)
    array([[1. , 2. , 4. ],
           [3. , 4. , 3. ],
           [5.5, 6. , 5. ],
           [8. , 8. , 7. ]])

    """

        "n_neighbors": [Interval(Integral, 1, None, closed="left")],
        "weights": [StrOptions({"uniform", "distance"}), callable, Hidden(None)],
        "metric": [StrOptions(set(_NAN_METRICS)), callable],
    }

        self,
        *,
        metric="nan_euclidean",
    ):
        )

    def _calc_impute(self, dist_pot_donors, n_neighbors, fit_X_col, mask_fit_X_col):
        """Helper function to impute a single column.

        dist_pot_donors : ndarray of shape (n_receivers, n_potential_donors)
            Distance matrix between the receivers and potential donors from
            training set. There must be at least one non-nan distance between
            a receiver and a potential donor.

            Number of neighbors to consider.

        fit_X_col : ndarray of shape (n_potential_donors,)
            Column of potential donors from training set.

        mask_fit_X_col : ndarray of shape (n_potential_donors,)
            Missing mask for fit_X_col.

        Returns
        -------
        imputed_values: ndarray of shape (n_receivers,)
            Imputed values for receiver.
        """
        # Get donors
        donors_idx = np.argpartition(dist_pot_donors, n_neighbors - 1, axis=1)[
            :, :n_neighbors
        ]

        # Get weight matrix from distance matrix
        donors_dist = dist_pot_donors[
            np.arange(donors_idx.shape[0])[:, None], donors_idx
        ]

        weight_matrix = _get_weights(donors_dist, self.weights)

        # fill nans with zeros
        if weight_matrix is not None:
            weight_matrix[np.isnan(weight_matrix)] = 0.0
        else:
            weight_matrix = np.ones_like(donors_dist)
            weight_matrix[np.isnan(donors_dist)] = 0.0

        # Retrieve donor values and calculate kNN average
        donors = fit_X_col.take(donors_idx)
        donors_mask = mask_fit_X_col.take(donors_idx)
        donors = np.ma.array(donors, mask=donors_mask)

        return np.ma.average(donors, axis=1, weights=weight_matrix).data

        """Fit the imputer on X.

        X : array-like shape of (n_samples, n_features)

        y : Ignored

        Returns
        -------
            The fitted `KNNImputer` class instance.
        """
        # Check data integrity and calling arguments
        else:

            self,
            X,
            dtype=FLOAT_DTYPES,
            copy=self.copy,
        )

        self._mask_fit_X = _get_mask(self._fit_X, self.missing_values)
        self._valid_mask = ~np.all(self._mask_fit_X, axis=0)

        super()._fit_indicator(self._mask_fit_X)

        """Impute all missing values in X.

        Returns
        -------
        X : array-like of shape (n_samples, n_output_features)
            The imputed dataset. `n_output_features` is the number of features
            that is not always missing during `fit`.
        """

        else:
            self,
            X,
            force_writeable=True,
        )

        mask = _get_mask(X, self.missing_values)
        mask_fit_X = self._mask_fit_X
        valid_mask = self._valid_mask

        X_indicator = super()._transform_indicator(mask)

        # Removes columns where the training data is all nan
        if not np.any(mask[:, valid_mask]):
            # No missing values in X
            if self.keep_empty_features:
                Xc = X
                Xc[:, ~valid_mask] = 0
            else:
                Xc = X[:, valid_mask]

            # Even if there are no missing values in X, we still concatenate Xc
            # with the missing value indicator matrix, X_indicator.
            # This is to ensure that the output maintains consistency in terms
            # of columns, regardless of whether missing values exist in X or not.
            return super()._concatenate_indicator(Xc, X_indicator)

        row_missing_idx = np.flatnonzero(mask[:, valid_mask].any(axis=1))

        non_missing_fix_X = np.logical_not(mask_fit_X)

        # Maps from indices from X to indices in dist matrix
        dist_idx_map = np.zeros(X.shape[0], dtype=int)
        dist_idx_map[row_missing_idx] = np.arange(row_missing_idx.shape[0])

        def process_chunk(dist_chunk, start):
            row_missing_chunk = row_missing_idx[start : start + len(dist_chunk)]

            # Find and impute missing by column
            for col in range(X.shape[1]):
                if not valid_mask[col]:
                    # column was all missing during training
                    continue

                col_mask = mask[row_missing_chunk, col]
                if not np.any(col_mask):
                    # column has no missing values
                    continue

                (potential_donors_idx,) = np.nonzero(non_missing_fix_X[:, col])

                # receivers_idx are indices in X
                receivers_idx = row_missing_chunk[np.flatnonzero(col_mask)]

                # distances for samples that needed imputation for column
                dist_subset = dist_chunk[dist_idx_map[receivers_idx] - start][
                    :, potential_donors_idx
                ]

                # receivers with all nan distances impute with mean
                all_nan_dist_mask = np.isnan(dist_subset).all(axis=1)
                all_nan_receivers_idx = receivers_idx[all_nan_dist_mask]

                if all_nan_receivers_idx.size:
                    col_mean = np.ma.array(
                        self._fit_X[:, col], mask=mask_fit_X[:, col]
                    ).mean()
                    X[all_nan_receivers_idx, col] = col_mean

                    if len(all_nan_receivers_idx) == len(receivers_idx):
                        # all receivers imputed with mean
                        continue

                    # receivers with at least one defined distance
                    receivers_idx = receivers_idx[~all_nan_dist_mask]
                    dist_subset = dist_chunk[dist_idx_map[receivers_idx] - start][
                        :, potential_donors_idx
                    ]

                n_neighbors = min(self.n_neighbors, len(potential_donors_idx))
                value = self._calc_impute(
                    dist_subset,
                    n_neighbors,
                    self._fit_X[potential_donors_idx, col],
                    mask_fit_X[potential_donors_idx, col],
                )
                X[receivers_idx, col] = value

        # process in fixed-memory chunks
        gen = pairwise_distances_chunked(
            X[row_missing_idx, :],
            self._fit_X,
            missing_values=self.missing_values,
            reduce_func=process_chunk,
        )
        for chunk in gen:
            # process_chunk modifies X in place. No return value.
            pass

        if self.keep_empty_features:
            Xc = X
            Xc[:, ~valid_mask] = 0
        else:
            Xc = X[:, valid_mask]

        return super()._concatenate_indicator(Xc, X_indicator)

        Returns
        -------
        """
        names = input_features[self._valid_mask]

        self,
        *,
    ):
        )

        Returns
        -------
        """
        ]

        ]

        else:

        y : Ignored

        Returns
        -------
        """
        else:

            self,
            X,
        )

        Returns
        -------
        """

        else:
            self,
            X,
        )

                Xc = X
            else:

                    continue

                    continue

                ]

                    ).mean()

                        continue

                    ]

                )

        )
            pass

            Xc = X
        else:

        Returns
        -------
        """

                    continue

                    continue

                ]

                    ).mean()

                        continue

                    ]

                )

def _assign_where(X1, X2, cond):
    """Assign X2 to X1 where cond is True.

    X1 : ndarray or dataframe of shape (n_samples, n_features)
        Data.

    X2 : ndarray of shape (n_samples, n_features)
        Data to be assigned.

    cond : ndarray of shape (n_samples, n_features)
        Boolean mask to assign data.
    """
    if hasattr(X1, "mask"):  # pandas dataframes
        X1.mask(cond=cond, other=X2, inplace=True)
    else:  # ndarrays
        X1[cond] = X2[cond]

class IterativeImputer(_BaseImputer):
    """Multivariate imputer that estimates each feature from all the others.

    A strategy for imputing missing values by modeling each feature with
    missing values as a function of other features in a round-robin fashion.

    Read more in the :ref:`User Guide <iterative_imputer>`.

    .. note::

      This estimator is still **experimental** for now: the predictions
      and the API might change without any deprecation cycle. To use it,
      you need to explicitly import `enable_iterative_imputer`::

        >>> # explicitly require this experimental feature
        >>> from sklearn.experimental import enable_iterative_imputer  # noqa
        >>> # now you can import normally from sklearn.impute
        >>> from sklearn.impute import IterativeImputer

    estimator : estimator object, default=BayesianRidge()
        The estimator to use at each step of the round-robin imputation.
        If `sample_posterior=True`, the estimator must support
        `return_std` in its `predict` method.

    missing_values : int or np.nan, default=np.nan

    sample_posterior : bool, default=False
        Whether to sample from the (Gaussian) predictive posterior of the
        fitted estimator for each imputation. Estimator must support
        `return_std` in its `predict` method if set to `True`. Set to
        `True` if using `IterativeImputer` for multiple imputations.

    max_iter : int, default=10
        Maximum number of imputation rounds to perform before returning the
        imputations computed during the final round. A round is a single
        imputation of each feature with missing values. The stopping criterion
        is met once `max(abs(X_t - X_{t-1}))/max(abs(X[known_vals])) < tol`,
        where `X_t` is `X` at iteration `t`. Note that early stopping is only
        applied if `sample_posterior=False`.

        Tolerance of the stopping condition.

    n_nearest_features : int, default=None
        Number of other features to use to estimate the missing values of
        each feature column. Nearness between features is measured using
        the absolute correlation coefficient between each feature pair (after
        initial imputation). To ensure coverage of features throughout the
        imputation process, the neighbor features are not necessarily nearest,
        but are drawn with probability proportional to correlation for each
        imputed target feature. Can provide significant speed-up when the
        number of features is huge. If `None`, all features will be used.

    initial_strategy : {'mean', 'median', 'most_frequent', 'constant'}, \
            default='mean'
        Which strategy to use to initialize the missing values. Same as the
        `strategy` parameter in :class:`~sklearn.impute.SimpleImputer`.

        When `strategy="constant"`, `fill_value` is used to replace all

    imputation_order : {'ascending', 'descending', 'roman', 'arabic', \
            'random'}, default='ascending'
        The order in which the features will be imputed. Possible values:

        - `'ascending'`: From features with fewest missing values to most.
        - `'descending'`: From features with most missing values to fewest.
        - `'roman'`: Left to right.
        - `'arabic'`: Right to left.
        - `'random'`: A random order for each round.

    skip_complete : bool, default=False
        If `True` then features with missing values during :meth:`transform`
        which did not have any missing values during :meth:`fit` will be
        imputed with the initial imputation method only. Set to `True` if you
        have many features with no missing values at both :meth:`fit` and
        :meth:`transform` time to save compute.

    min_value : float or array-like of shape (n_features,), default=-np.inf
        Minimum possible imputed value. Broadcast to shape `(n_features,)` if
        scalar. If array-like, expects shape `(n_features,)`, one min value for
        each feature. The default is `-np.inf`.

           Added support for array-like.

    max_value : float or array-like of shape (n_features,), default=np.inf
        Maximum possible imputed value. Broadcast to shape `(n_features,)` if
        scalar. If array-like, expects shape `(n_features,)`, one max value for
        each feature. The default is `np.inf`.

        Verbosity flag, controls the debug messages that are issued
        as functions are evaluated. The higher, the more verbose. Can be 0, 1,
        or 2.

        The seed of the pseudo random number generator to use. Randomizes
        selection of estimator features if `n_nearest_features` is not `None`,
        the `imputation_order` if `random`, and the sampling from posterior if
        `sample_posterior=True`. Use an integer for determinism.
        See :term:`the Glossary <random_state>`.

        If `True`, a :class:`MissingIndicator` transform will stack onto output

        The imputed value is always `0` except when
        `initial_strategy="constant"` in which case `fill_value` will be
        used instead.

    initial_imputer_ : object of type :class:`~sklearn.impute.SimpleImputer`
        Imputer used to initialize the missing values.

    imputation_sequence_ : list of tuples
        Each tuple has `(feat_idx, neighbor_feat_idx, estimator)`, where
        `feat_idx` is the current feature to be imputed,
        `neighbor_feat_idx` is the array of other features used to impute the
        current feature, and `estimator` is the trained estimator used for
        the imputation. Length is `self.n_features_with_missing_ *
        self.n_iter_`.

        Number of iteration rounds that occurred. Will be less than
        `self.max_iter` if early stopping criterion was reached.

    n_features_with_missing_ : int
        Number of features with missing values.

    random_state_ : RandomState instance
        RandomState instance that is generated either from a seed, the random
        number generator or by `np.random`.

    See Also
    --------

    Notes
    -----
    To support imputation in inductive mode we store each feature's estimator
    during the :meth:`fit` phase, and predict without refitting (in order)
    during the :meth:`transform` phase.

    Features which contain all missing values at :meth:`fit` are discarded upon
    :meth:`transform`.

    Using defaults, the imputer scales in :math:`\\mathcal{O}(knp^3\\min(n,p))`
    where :math:`k` = `max_iter`, :math:`n` the number of samples and
    :math:`p` the number of features. It thus becomes prohibitively costly when
    the number of features increases. Setting
    `n_nearest_features << n_features`, `skip_complete=True` or increasing `tol`
    can help to reduce its computational cost.

    Depending on the nature of missing values, simple imputers can be
    preferable in a prediction context.

    .. [1] `Stef van Buuren, Karin Groothuis-Oudshoorn (2011). "mice:
        Multivariate Imputation by Chained Equations in R". Journal of
        Statistical Software 45: 1-67.
        <https://www.jstatsoft.org/article/view/v045i03>`_

    .. [2] `S. F. Buck, (1960). "A Method of Estimation of Missing Values in
        Multivariate Data Suitable for use with an Electronic Computer".
        Journal of the Royal Statistical Society 22(2): 302-306.
        <https://www.jstor.org/stable/2984099>`_

    Examples
    --------
    >>> from sklearn.experimental import enable_iterative_imputer
    >>> from sklearn.impute import IterativeImputer
    >>> imp_mean = IterativeImputer(random_state=0)
    IterativeImputer(random_state=0)
    >>> imp_mean.transform(X)
    array([[ 6.9584...,  2.       ,  3.        ],
           [ 4.       ,  2.6000...,  6.        ],
           [10.       ,  4.9999...,  9.        ]])

    :ref:`sphx_glr_auto_examples_impute_plot_missing_values.py` or
    :ref:`sphx_glr_auto_examples_impute_plot_iterative_imputer_variants_comparison.py`.
    """

        "estimator": [None, HasMethods(["fit", "predict"])],
        "sample_posterior": ["boolean"],
        "n_nearest_features": [None, Interval(Integral, 1, None, closed="left")],
        "initial_strategy": [
            StrOptions({"mean", "median", "most_frequent", "constant"})
        ],
        "imputation_order": [
            StrOptions({"ascending", "descending", "roman", "arabic", "random"})
        ],
        "skip_complete": ["boolean"],
        "min_value": [None, Interval(Real, None, None, closed="both"), "array-like"],
        "max_value": [None, Interval(Real, None, None, closed="both"), "array-like"],
    }

        self,
        *,
        sample_posterior=False,
        tol=1e-3,
        n_nearest_features=None,
        initial_strategy="mean",
        imputation_order="ascending",
        skip_complete=False,
        min_value=-np.inf,
        max_value=np.inf,
    ):
        )

        self.sample_posterior = sample_posterior
        self.n_nearest_features = n_nearest_features
        self.initial_strategy = initial_strategy
        self.imputation_order = imputation_order
        self.skip_complete = skip_complete
        self.min_value = min_value
        self.max_value = max_value

    def _impute_one_feature(
        self,
        X_filled,
        mask_missing_values,
        feat_idx,
        neighbor_feat_idx,
        fit_mode=True,
        params=None,
    ):
        """Impute a single feature from the others provided.

        This function predicts the missing values of one of the features using
        the current estimates of all the other features. The `estimator` must
        support `return_std=True` in its `predict` method for this function
        to work.

        X_filled : ndarray
            Input data with the most recent imputations.

        mask_missing_values : ndarray
            Input data's missing indicator matrix.

        feat_idx : int
            Index of the feature currently being imputed.

        neighbor_feat_idx : ndarray
            Indices of the features to be used in imputing `feat_idx`.

            The estimator to use at this step of the round-robin imputation.
            If `sample_posterior=True`, the estimator must support
            `return_std` in its `predict` method.
            If None, it will be cloned from self._estimator.

        fit_mode : boolean, default=True
            Whether to fit and predict with the estimator or just predict.

            Additional params routed to the individual estimator.

        Returns
        -------
            Input data with `X_filled[missing_row_mask, feat_idx]` updated.

        estimator : estimator with sklearn API
            The fitted estimator used to impute
            `X_filled[missing_row_mask, feat_idx]`.
        """
        if estimator is None and fit_mode is False:
                "If fit_mode is False, then an already-fitted "
                "estimator should be passed in."
            )

        if estimator is None:
            estimator = clone(self._estimator)

        missing_row_mask = mask_missing_values[:, feat_idx]
        if fit_mode:
            X_train = _safe_indexing(
                _safe_indexing(X_filled, neighbor_feat_idx, axis=1),
                ~missing_row_mask,
                axis=0,
            )
            y_train = _safe_indexing(
                _safe_indexing(X_filled, feat_idx, axis=1),
                axis=0,
            )
            estimator.fit(X_train, y_train, **params)

        # if no missing values, don't predict
        if np.sum(missing_row_mask) == 0:
            return X_filled, estimator

        # get posterior samples if there is at least one missing value
        X_test = _safe_indexing(
            _safe_indexing(X_filled, neighbor_feat_idx, axis=1),
            missing_row_mask,
            axis=0,
        )
        if self.sample_posterior:
            mus, sigmas = estimator.predict(X_test, return_std=True)
            imputed_values = np.zeros(mus.shape, dtype=X_filled.dtype)
            # two types of problems: (1) non-positive sigmas
            # (2) mus outside legal range of min_value and max_value
            # (results in inf sample)
            positive_sigmas = sigmas > 0
            imputed_values[~positive_sigmas] = mus[~positive_sigmas]
            mus_too_low = mus < self._min_value[feat_idx]
            imputed_values[mus_too_low] = self._min_value[feat_idx]
            mus_too_high = mus > self._max_value[feat_idx]
            imputed_values[mus_too_high] = self._max_value[feat_idx]
            # the rest can be sampled without statistical issues
            inrange_mask = positive_sigmas & ~mus_too_low & ~mus_too_high
            mus = mus[inrange_mask]
            sigmas = sigmas[inrange_mask]
            a = (self._min_value[feat_idx] - mus) / sigmas
            b = (self._max_value[feat_idx] - mus) / sigmas

            truncated_normal = stats.truncnorm(a=a, b=b, loc=mus, scale=sigmas)
            imputed_values[inrange_mask] = truncated_normal.rvs(
                random_state=self.random_state_
            )
        else:
            imputed_values = estimator.predict(X_test)
            imputed_values = np.clip(
                imputed_values, self._min_value[feat_idx], self._max_value[feat_idx]
            )

        # update the feature
        _safe_assign(
            X_filled,
            imputed_values,
            row_indexer=missing_row_mask,
            column_indexer=feat_idx,
        )
        return X_filled, estimator

    def _get_neighbor_feat_idx(self, n_features, feat_idx, abs_corr_mat):
        """Get a list of other features to predict `feat_idx`.

        If `self.n_nearest_features` is less than or equal to the total
        number of features, then use a probability proportional to the absolute
        correlation between `feat_idx` and each other feature to randomly
        choose a subsample of the other features (without replacement).

            Number of features in `X`.

        abs_corr_mat : ndarray, shape (n_features, n_features)
            Absolute correlation matrix of `X`. The diagonal has been zeroed
            out and each feature has been normalized to sum to 1. Can be None.

        Returns
        -------
        neighbor_feat_idx : array-like
            The features to use to impute `feat_idx`.
        """
        if self.n_nearest_features is not None and self.n_nearest_features < n_features:
            p = abs_corr_mat[:, feat_idx]
            neighbor_feat_idx = self.random_state_.choice(
                np.arange(n_features), self.n_nearest_features, replace=False, p=p
            )
        else:
            inds_left = np.arange(feat_idx)
            inds_right = np.arange(feat_idx + 1, n_features)
            neighbor_feat_idx = np.concatenate((inds_left, inds_right))
        return neighbor_feat_idx

    def _get_ordered_idx(self, mask_missing_values):
        """Decide in what order we will update the features.

        As a homage to the MICE R package, we will have 4 main options of
        how to order the updates, and use a random order if anything else
        is specified.

        Also, this function skips features which have no missing values.

        mask_missing_values : array-like, shape (n_samples, n_features)
            Input data's missing indicator matrix, where `n_samples` is the
            number of samples and `n_features` is the number of features.

        Returns
        -------
        ordered_idx : ndarray, shape (n_features,)
            The order in which to impute the features.
        """
        frac_of_missing_values = mask_missing_values.mean(axis=0)
        if self.skip_complete:
            missing_values_idx = np.flatnonzero(frac_of_missing_values)
        else:
            missing_values_idx = np.arange(np.shape(frac_of_missing_values)[0])
        if self.imputation_order == "roman":
            ordered_idx = missing_values_idx
        elif self.imputation_order == "arabic":
            ordered_idx = missing_values_idx[::-1]
        elif self.imputation_order == "ascending":
            n = len(frac_of_missing_values) - len(missing_values_idx)
            ordered_idx = np.argsort(frac_of_missing_values, kind="mergesort")[n:]
        elif self.imputation_order == "descending":
            ordered_idx = np.argsort(frac_of_missing_values, kind="mergesort")[n:][::-1]
        elif self.imputation_order == "random":
            self.random_state_.shuffle(ordered_idx)
        return ordered_idx

    def _get_abs_corr_mat(self, X_filled, tolerance=1e-6):
        """Get absolute correlation matrix between features.

        X_filled : ndarray, shape (n_samples, n_features)

        tolerance : float, default=1e-6
            `abs_corr_mat` can have nans, which will be replaced
            with `tolerance`.

        Returns
        -------
            Absolute correlation matrix of `X` at the beginning of the
            current round. The diagonal has been zeroed out and each feature's
            absolute correlations with all others have been normalized to sum
            to 1.
        """
        n_features = X_filled.shape[1]
        if self.n_nearest_features is None or self.n_nearest_features >= n_features:
        with np.errstate(invalid="ignore"):
            # if a feature in the neighborhood has only a single value
            # (e.g., categorical feature), the std. dev. will be null and
            # np.corrcoef will raise a warning due to a division by zero
            abs_corr_mat = np.abs(np.corrcoef(X_filled.T))
        # np.corrcoef is not defined for features with zero std
        abs_corr_mat[np.isnan(abs_corr_mat)] = tolerance
        # ensures exploration, i.e. at least some probability of sampling
        np.clip(abs_corr_mat, tolerance, None, out=abs_corr_mat)
        # features are not their own neighbors
        np.fill_diagonal(abs_corr_mat, 0)
        # needs to sum to 1 for np.random.choice sampling
        abs_corr_mat = normalize(abs_corr_mat, norm="l1", axis=0, copy=False)
        return abs_corr_mat

    def _initial_imputation(self, X, in_fit=False):
        """Perform initial imputation for input `X`.

        in_fit : bool, default=False
            Whether function is called in :meth:`fit`.

        Returns
        -------

        X_filled : ndarray of shape (n_samples, n_features)

        mask_missing_values : ndarray of shape (n_samples, n_features)
            number of samples and `n_features` is the number of features,
            masked by non-missing features.

        X_missing_mask : ndarray, shape (n_samples, n_features)
            Input data's mask matrix indicating missing datapoints, where
            `n_samples` is the number of samples and `n_features` is the
            number of features.
        """
        if is_scalar_nan(self.missing_values):
        else:

            self,
            X,
        )

        X_missing_mask = _get_mask(X, self.missing_values)
        mask_missing_values = X_missing_mask.copy()

        # TODO (1.8): remove this once the deprecation is removed. In the meantime,
        # we need to catch the warning to avoid false positives.
        catch_warning = (
            self.initial_strategy == "constant" and not self.keep_empty_features
        )

        if self.initial_imputer_ is None:
            self.initial_imputer_ = SimpleImputer(
                missing_values=self.missing_values,
                strategy=self.initial_strategy,
                fill_value=self.fill_value,
                keep_empty_features=self.keep_empty_features,
            ).set_output(transform="default")

            # TODO (1.8): remove this once the deprecation is removed to keep only
            # the code in the else case.
            if catch_warning:
                with warnings.catch_warnings():
                    warnings.simplefilter("ignore", FutureWarning)
                    X_filled = self.initial_imputer_.fit_transform(X)
            else:
                X_filled = self.initial_imputer_.fit_transform(X)
        else:
                    X_filled = self.initial_imputer_.transform(X)
            else:
                X_filled = self.initial_imputer_.transform(X)

        if in_fit:
            self._is_empty_feature = np.all(mask_missing_values, axis=0)

        if not self.keep_empty_features:
            # drop empty features
            Xt = X[:, ~self._is_empty_feature]
            mask_missing_values = mask_missing_values[:, ~self._is_empty_feature]

            if self.initial_imputer_.get_params()["strategy"] == "constant":
                # The constant strategy has a specific behavior and preserve empty
                # features even with ``keep_empty_features=False``. We need to drop
                # the column for consistency.
                # TODO (1.8): remove this `if` branch once the following issue is
                # addressed:
                # https://github.com/scikit-learn/scikit-learn/issues/29827
                X_filled = X_filled[:, ~self._is_empty_feature]

        else:
            # mark empty features as not missing and keep the original
            # imputation
            mask_missing_values[:, self._is_empty_feature] = False
            Xt = X
            Xt[:, self._is_empty_feature] = X_filled[:, self._is_empty_feature]

        return Xt, X_filled, mask_missing_values, X_missing_mask

    def _validate_limit(
        limit, limit_type, n_features, is_empty_feature, keep_empty_feature
    ):
        """Validate the limits (min/max) of the feature values.

        Converts scalar min/max limits to vectors of shape `(n_features,)`.

        limit: scalar or array-like
            The user-specified limit (i.e, min_value or max_value).
        limit_type: {'max', 'min'}
            Type of limit to validate.
        n_features: int
            Number of features in the dataset.
        is_empty_feature: ndarray, shape (n_features, )
            Mask array indicating empty feature imputer has seen during fit.
        keep_empty_feature: bool
            If False, remove empty-feature indices from the limit.

        Returns
        -------
        limit: ndarray, shape(n_features,)
            Array of limits, one for each feature.
        """
        n_features_in = _num_samples(is_empty_feature)
        if (
            limit is not None
            and not np.isscalar(limit)
            and _num_samples(limit) != n_features_in
        ):
                f"'{limit_type}_value' should be of shape ({n_features_in},) when an"
                f" array-like is provided. Got {len(limit)}, instead."
            )

        limit_bound = np.inf if limit_type == "max" else -np.inf
        limit = limit_bound if limit is None else limit
        if np.isscalar(limit):
            limit = np.full(n_features, limit)
        limit = check_array(limit, ensure_all_finite=False, copy=False, ensure_2d=False)

        # Make sure to remove the empty feature elements from the bounds
        if not keep_empty_feature and len(limit) == len(is_empty_feature):
            limit = limit[~is_empty_feature]

        return limit

        # IterativeImputer.estimator is not validated yet
    )
        """Fit the imputer on `X` and return the transformed `X`.

        y : Ignored

            Parameters routed to the `fit` method of the sub-estimator via the

              Only available if
              `sklearn.set_config(enable_metadata_routing=True)` is set. See
              :ref:`Metadata Routing User Guide <metadata_routing>` for more
              details.

        Returns
        -------
        Xt : array-like, shape (n_samples, n_features)
            The imputed input data.
        """

            self,
            "fit",
            **params,
        )

        self.random_state_ = getattr(
            self, "random_state_", check_random_state(self.random_state)
        )

            from ..linear_model import BayesianRidge

            self._estimator = BayesianRidge()
        else:
            self._estimator = clone(self.estimator)

        self.imputation_sequence_ = []

        self.initial_imputer_ = None

        X, Xt, mask_missing_values, complete_mask = self._initial_imputation(
            X, in_fit=True
        )

        super()._fit_indicator(complete_mask)
        X_indicator = super()._transform_indicator(complete_mask)

        if self.max_iter == 0 or np.all(mask_missing_values):
            return super()._concatenate_indicator(Xt, X_indicator)

        # Edge case: a single feature, we return the initial imputation.
        if Xt.shape[1] == 1:

        self._min_value = self._validate_limit(
            self.min_value,
            "min",
            X.shape[1],
            self._is_empty_feature,
            self.keep_empty_features,
        )
        self._max_value = self._validate_limit(
            self.max_value,
            "max",
        )

        if not np.all(np.greater(self._max_value, self._min_value)):
            raise ValueError("One (or more) features have min_value >= max_value.")

        # order in which to impute
        # note this is probably too slow for large feature data (d > 100000)
        # and a better way would be good.
        # see: https://goo.gl/KyCNwj and subsequent comments
        ordered_idx = self._get_ordered_idx(mask_missing_values)
        self.n_features_with_missing_ = len(ordered_idx)

        abs_corr_mat = self._get_abs_corr_mat(Xt)

        n_samples, n_features = Xt.shape
        if self.verbose > 0:
            print("[IterativeImputer] Completing matrix with shape %s" % (X.shape,))
        start_t = time()
        if not self.sample_posterior:
            Xt_previous = Xt.copy()
            normalized_tol = self.tol * np.max(np.abs(X[~mask_missing_values]))
        for self.n_iter_ in range(1, self.max_iter + 1):
            if self.imputation_order == "random":
                ordered_idx = self._get_ordered_idx(mask_missing_values)

            for feat_idx in ordered_idx:
                neighbor_feat_idx = self._get_neighbor_feat_idx(
                    n_features, feat_idx, abs_corr_mat
                )
                Xt, estimator = self._impute_one_feature(
                    Xt,
                    mask_missing_values,
                    feat_idx,
                    neighbor_feat_idx,
                    estimator=None,
                    fit_mode=True,
                )
                estimator_triplet = _ImputerTriplet(
                    feat_idx, neighbor_feat_idx, estimator
                )
                self.imputation_sequence_.append(estimator_triplet)

            if self.verbose > 1:
                print(
                    "[IterativeImputer] Ending imputation round "
                    "%d/%d, elapsed time %0.2f"
                    % (self.n_iter_, self.max_iter, time() - start_t)
                )

            if not self.sample_posterior:
                inf_norm = np.linalg.norm(Xt - Xt_previous, ord=np.inf, axis=None)
                if self.verbose > 0:
                    print(
                        "[IterativeImputer] Change: {}, scaled tolerance: {} ".format(
                            inf_norm, normalized_tol
                        )
                    )
                if inf_norm < normalized_tol:
                    if self.verbose > 0:
                        print("[IterativeImputer] Early stopping criterion reached.")
                    break
                Xt_previous = Xt.copy()
        else:
                    "[IterativeImputer] Early stopping criterion not reached.",
                )
        _assign_where(Xt, X, cond=~mask_missing_values)

        return super()._concatenate_indicator(Xt, X_indicator)

        Note that this is stochastic, and that if `random_state` is not fixed,
        repeated calls, or permuted input, results will differ.

        Returns
        -------
             The imputed input data.
        """

            X, in_fit=False
        )

        if self.n_iter_ == 0 or np.all(mask_missing_values):

        imputations_per_round = len(self.imputation_sequence_) // self.n_iter_
        i_rnd = 0
        for it, estimator_triplet in enumerate(self.imputation_sequence_):
            Xt, _ = self._impute_one_feature(
                Xt,
                mask_missing_values,
                estimator_triplet.feat_idx,
                estimator_triplet.neighbor_feat_idx,
                estimator=estimator_triplet.estimator,
                fit_mode=False,
            )
            if not (it + 1) % imputations_per_round:
                if self.verbose > 1:
                    print(
                        "[IterativeImputer] Ending imputation round "
                        "%d/%d, elapsed time %0.2f"
                        % (i_rnd + 1, self.n_iter_, time() - start_t)
                    )
                i_rnd += 1

        """Fit the imputer on `X` and return self.

        y : Ignored

              details.

        Returns
        -------
        """
        self.fit_transform(X, **fit_params)

        Returns
        -------
        """
        names = self.initial_imputer_.get_feature_names_out(input_features)

        Returns
        -------
        """
            method_mapping=MethodMapping().add(callee="fit", caller="fit"),
        )

        self,
        *,
        tol=1e-3,
    ):
        )

        self,
        X_filled,
        feat_idx,
    ):

        to work.

        Returns
        -------

        """
            )

                axis=0,
            )
                axis=0,
            )

            axis=0,
        )

            )
        else:
            )

            X_filled,
        )

        Returns
        -------
        """
            )
        else:

        Returns
        -------
        """
        else:

        Returns
        -------
            to 1.
        """

        Returns
        -------

        """
        else:

            self,
            X,
        )

        )

            else:
        else:
            else:

        if in_fit:

        else:
            Xt = X

    ):

        Returns
        -------
        """
        if (
        ):
            )

        y : Ignored

              details.

        Returns
        -------
        """

            self,
            "fit",
            **params,
        )

        )

        else:

        )

            "min",
        )
            "max",
        )

                )
                    Xt,
                    feat_idx,
                )
                )

                print(
                )

                    print(
                        )
                    )
                    break
        else:
                )

        Returns
        -------
        """

        )

        i_rnd = 0
                Xt,
            )
                    print(
                    )
                i_rnd += 1

        y : Ignored

              details.

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """
        )

def imputers():
    return [IterativeImputer(tol=0.1), KNNImputer(), SimpleImputer()]

def test_imputation_missing_value_in_test_array(imputer):
    # [Non Regression Test for issue #13968] Missing value in test set should
    # not throw an error and return a finite dataset
    train = [[1], [2]]
    test = [[3], [np.nan]]
    imputer.set_params(add_indicator=True)
    imputer.fit(train).transform(test)

def test_imputers_add_indicator(marker, imputer):
        [
            [marker, 1, 5, marker, 1],
            [2, marker, 1, marker, 2],
            [6, 3, marker, marker, 3],
            [1, 2, 9, marker, 4],
        ]
    )
    X_true_indicator = np.array(
        [
            [1.0, 0.0, 0.0, 1.0],
            [0.0, 1.0, 0.0, 1.0],
            [0.0, 0.0, 1.0, 1.0],
            [0.0, 0.0, 0.0, 1.0],
        ]
    )
    imputer.set_params(missing_values=marker, add_indicator=True)

    X_trans = imputer.fit_transform(X)
    assert_allclose(X_trans[:, -4:], X_true_indicator)
    assert_array_equal(imputer.indicator_.features_, np.array([0, 1, 2, 3]))

    imputer.set_params(add_indicator=False)
    X_trans_no_indicator = imputer.fit_transform(X)
    assert_allclose(X_trans[:, :-4], X_trans_no_indicator)

def test_imputers_add_indicator_sparse(imputer, marker, csr_container):
        [
        ]
    )
    X_true_indicator = csr_container(
        [
        ]
    )

    assert_allclose_dense_sparse(X_trans[:, -4:], X_true_indicator)

    assert_allclose_dense_sparse(X_trans[:, :-4], X_trans_no_indicator)

def test_imputers_pandas_na_integer_array_support(imputer, add_indicator):
    # Test pandas IntegerArray with pd.NA
    marker = np.nan
    imputer = imputer.set_params(add_indicator=add_indicator, missing_values=marker)

        [
        ]
    )
    # fit on numpy array
    X_trans_expected = imputer.fit_transform(X)

    # Creates dataframe with IntegerArrays with pd.NA
    X_df = pd.DataFrame(X, dtype="Int16", columns=["a", "b", "c", "d", "e"])

    # fit on pandas dataframe with IntegerArrays
    X_trans = imputer.fit_transform(X_df)

    assert_allclose(X_trans_expected, X_trans)

def test_imputers_feature_names_out_pandas(imputer, add_indicator):
    """Check feature names out for imputers."""

        [
            [marker, 1, 5, 3, marker, 1],
            [2, marker, 1, 4, marker, 2],
            [6, 3, 7, marker, marker, 3],
            [1, 2, 9, 8, marker, 4],
        ]
    )
    X_df = pd.DataFrame(X, columns=["a", "b", "c", "d", "e", "f"])
    imputer.fit(X_df)

    names = imputer.get_feature_names_out()

    if add_indicator:
        expected_names = [
            "a",
            "b",
            "c",
            "d",
            "f",
            "missingindicator_a",
            "missingindicator_b",
            "missingindicator_d",
            "missingindicator_e",
        ]
        assert_array_equal(expected_names, names)
    else:
        expected_names = ["a", "b", "c", "d", "f"]

def test_keep_empty_features(imputer, keep_empty_features):
    """Check that the imputer keeps features with only missing values."""
    X = np.array([[np.nan, 1], [np.nan, 2], [np.nan, 3]])
    imputer = imputer.set_params(
        add_indicator=False, keep_empty_features=keep_empty_features
    )

    for method in ["fit_transform", "transform"]:
        X_imputed = getattr(imputer, method)(X)
        if keep_empty_features:
            assert X_imputed.shape == X.shape
        else:
            assert X_imputed.shape == (X.shape[0], X.shape[1] - 1)

def test_imputation_adds_missing_indicator_if_add_indicator_is_true(
    imputer, missing_value_test
):
    """Check that missing indicator always exists when add_indicator=True.

    Non-regression test for gh-26590.
    """
    X_train = np.array([[0, np.nan], [1, 2]])

    # Test data where missing_value_test variable can be set to np.nan or 1.
    X_test = np.array([[0, missing_value_test], [1, 2]])

    imputer.fit(X_train)

    X_test_imputed_with_indicator = imputer.transform(X_test)
    assert X_test_imputed_with_indicator.shape == (2, 3)

    X_test_imputed_without_indicator = imputer.transform(X_test)
    assert X_test_imputed_without_indicator.shape == (2, 2)

        X_test_imputed_with_indicator[:, :-1], X_test_imputed_without_indicator
    )
    if np.isnan(missing_value_test):
        expected_missing_indicator = [1, 0]
    else:
        expected_missing_indicator = [0, 0]

    assert_allclose(X_test_imputed_with_indicator[:, -1], expected_missing_indicator)

def test_knn_imputer_shape(weights, n_neighbors):
    # Verify the shapes of the imputed matrix for different weights and
    # number of neighbors.
    n_rows = 10
    n_cols = 2
    X = np.random.rand(n_rows, n_cols)

    imputer = KNNImputer(n_neighbors=n_neighbors, weights=weights)
    X_imputed = imputer.fit_transform(X)
    assert X_imputed.shape == (n_rows, n_cols)

def test_knn_imputer_default_with_invalid_input(na):
    # Test imputation with default values and invalid input

    # Test with inf present
        [
            [np.inf, 1, 1, 2, na],
            [2, 1, 2, 2, 3],
            [3, 2, 3, 3, 8],
            [na, 6, 0, 5, 13],
            [na, 7, 0, 7, 8],
            [6, 6, 2, 5, 7],
        ]
    )
    with pytest.raises(ValueError, match="Input X contains (infinity|NaN)"):
        KNNImputer(missing_values=na).fit(X)

    # Test with inf present in matrix passed in transform()
        [
        ]
    )

    X_fit = np.array(
        [
            [0, 1, 1, 2, na],
        ]
    )
    imputer = KNNImputer(missing_values=na).fit(X_fit)
        imputer.transform(X)

    # Test with missing_values=0 when NaN present
    imputer = KNNImputer(missing_values=0, n_neighbors=2, weights="uniform")
        [
            [np.nan, 0, 0, 0, 5],
            [np.nan, 1, 0, np.nan, 3],
            [np.nan, 2, 0, 0, 0],
            [np.nan, 6, 0, 5, 13],
        ]
    )
        imputer.fit(X)

        [
            [0, 0],
            [np.nan, 2],
        ]
    )

def test_knn_imputer_removes_all_na_features(na):
        [
            [1, 1, na, 1, 1, 1.0],
            [2, 3, na, 2, 2, 2],
            [3, 4, na, 3, 3, na],
            [6, 4, na, na, 6, 6],
        ]
    )
    knn = KNNImputer(missing_values=na, n_neighbors=2).fit(X)

    X_transform = knn.transform(X)
    assert not np.isnan(X_transform).any()
    assert X_transform.shape == (4, 5)

    X_test = np.arange(0, 12).reshape(2, 6)
    X_transform = knn.transform(X_test)
    assert_allclose(X_test[:, [0, 1, 3, 4, 5]], X_transform)

def test_knn_imputer_zero_nan_imputes_the_same(na):
    # Test with an imputable matrix and compare with different missing_values
    X_zero = np.array(
        [
            [1, 0, 1, 1, 1.0],
            [2, 2, 2, 2, 2],
            [3, 3, 3, 3, 0],
            [6, 6, 0, 6, 6],
        ]
    )

    X_nan = np.array(
        [
            [1, na, 1, 1, 1.0],
            [3, 3, 3, 3, na],
            [6, 6, na, 6, 6],
        ]
    )

    X_imputed = np.array(
        [
            [1, 2.5, 1, 1, 1.0],
            [3, 3, 3, 3, 1.5],
            [6, 6, 2.5, 6, 6],
        ]
    )

    imputer_zero = KNNImputer(missing_values=0, n_neighbors=2, weights="uniform")

    imputer_nan = KNNImputer(missing_values=na, n_neighbors=2, weights="uniform")

    assert_allclose(imputer_zero.fit_transform(X_zero), X_imputed)
        imputer_zero.fit_transform(X_zero), imputer_nan.fit_transform(X_nan)
    )

def test_knn_imputer_verify(na):
    # Test with an imputable matrix
        [
            [1, 0, 0, 1],
            [2, 1, 2, na],
            [3, 2, 3, na],
            [na, 4, 5, 5],
            [6, na, 6, 7],
            [8, 8, 8, 8],
            [16, 15, 18, 19],
        ]
    )

        [
            [2, 1, 2, 8],
            [3, 2, 3, 8],
            [4, 4, 5, 5],
            [6, 3, 6, 7],
        ]
    )

    imputer = KNNImputer(missing_values=na)
    assert_allclose(imputer.fit_transform(X), X_imputed)

    # Test when there is not enough neighbors
        [
            [1, 0, 0, na],
            [4, 4, 5, na],
            [6, 7, 6, na],
            [8, 8, 8, na],
            [20, 20, 20, 20],
            [22, 22, 22, 22],
        ]
    )

    # Not enough neighbors, use column mean from training
    X_impute_value = (20 + 22) / 2
        [
            [1, 0, 0, X_impute_value],
            [2, 1, 2, X_impute_value],
            [3, 2, 3, X_impute_value],
            [4, 4, 5, X_impute_value],
            [6, 7, 6, X_impute_value],
            [8, 8, 8, X_impute_value],
        ]
    )

    # Test when data in fit() and transform() are different
    X = np.array([[0, 0], [na, 2], [4, 3], [5, 6], [7, 7], [9, 8], [11, 16]])

    X1 = np.array([[1, 0], [3, 2], [4, na]])

    X_2_1 = (0 + 3 + 6 + 7 + 8) / 5
    X1_imputed = np.array([[1, 0], [3, 2], [4, X_2_1]])

    assert_allclose(imputer.fit(X).transform(X1), X1_imputed)

def test_knn_imputer_one_n_neighbors(na):
    X = np.array([[0, 0], [na, 2], [4, 3], [5, na], [7, 7], [na, 8], [14, 13]])

    X_imputed = np.array([[0, 0], [4, 2], [4, 3], [5, 3], [7, 7], [7, 8], [14, 13]])

    imputer = KNNImputer(n_neighbors=1, missing_values=na)

def test_knn_imputer_all_samples_are_neighbors(na):

        [[0, 0], [6.25, 2], [4, 3], [5, 5.75], [7, 7], [6.25, 8], [14, 13]]
    )

    n_neighbors = X.shape[0] - 1
    imputer = KNNImputer(n_neighbors=n_neighbors, missing_values=na)

    n_neighbors = X.shape[0]
    imputer_plus1 = KNNImputer(n_neighbors=n_neighbors, missing_values=na)
    assert_allclose(imputer_plus1.fit_transform(X), X_imputed)

def test_knn_imputer_weight_uniform(na):
    X = np.array([[0, 0], [na, 2], [4, 3], [5, 6], [7, 7], [9, 8], [11, 10]])

    # Test with "uniform" weight (or unweighted)
    X_imputed_uniform = np.array(
        [[0, 0], [5, 2], [4, 3], [5, 6], [7, 7], [9, 8], [11, 10]]
    )

    imputer = KNNImputer(weights="uniform", missing_values=na)
    assert_allclose(imputer.fit_transform(X), X_imputed_uniform)

    # Test with "callable" weight
    def no_weight(dist):

    imputer = KNNImputer(weights=no_weight, missing_values=na)

    # Test with "callable" uniform weight
    def uniform_weight(dist):

    imputer = KNNImputer(weights=uniform_weight, missing_values=na)

def test_knn_imputer_weight_distance(na):

    # Test with "distance" weight
    nn = KNeighborsRegressor(metric="euclidean", weights="distance")
    X_rows_idx = [0, 2, 3, 4, 5, 6]
    nn.fit(X[X_rows_idx, 1:], X[X_rows_idx, 0])
    knn_imputed_value = nn.predict(X[1:2, 1:])[0]

    # Manual calculation
    X_neighbors_idx = [0, 2, 3, 4, 5]
    dist = nan_euclidean_distances(X[1:2, :], X, missing_values=na)
    weights = 1 / dist[:, X_neighbors_idx].ravel()
    manual_imputed_value = np.average(X[X_neighbors_idx, 0], weights=weights)

    X_imputed_distance1 = np.array(
        [[0, 0], [manual_imputed_value, 2], [4, 3], [5, 6], [7, 7], [9, 8], [11, 10]]
    )

    # NearestNeighbor calculation
    X_imputed_distance2 = np.array(
        [[0, 0], [knn_imputed_value, 2], [4, 3], [5, 6], [7, 7], [9, 8], [11, 10]]
    )

    imputer = KNNImputer(weights="distance", missing_values=na)
    assert_allclose(imputer.fit_transform(X), X_imputed_distance1)
    assert_allclose(imputer.fit_transform(X), X_imputed_distance2)

    # Test with weights = "distance" and n_neighbors=2
        [
            [na, 0, 0],
            [2, 1, 2],
            [3, 2, 3],
            [4, 5, 5],
        ]
    )

    # neighbors are rows 1, 2, the nan_euclidean_distances are:
    dist_0_1 = np.sqrt((3 / 2) * ((1 - 0) ** 2 + (2 - 0) ** 2))
    dist_0_2 = np.sqrt((3 / 2) * ((2 - 0) ** 2 + (3 - 0) ** 2))
    imputed_value = np.average([2, 3], weights=[1 / dist_0_1, 1 / dist_0_2])

        [
            [imputed_value, 0, 0],
            [2, 1, 2],
            [3, 2, 3],
            [4, 5, 5],
        ]
    )

    imputer = KNNImputer(n_neighbors=2, weights="distance", missing_values=na)

    # Test with varying missingness patterns
        [
            [0, na, 1, na],
            [1, 1, 1, na],
            [0, 1, 0, 0],
            [0, 0, 0, 0],
            [1, 0, 1, 1],
            [10, 10, 10, 10],
        ]
    )

    # Get weights of donor neighbors
    dist = nan_euclidean_distances(X, missing_values=na)
    r1c1_nbor_dists = dist[1, [0, 2, 3, 4, 5]]
    r1c3_nbor_dists = dist[1, [0, 3, 4, 5, 6]]
    r1c1_nbor_wt = 1 / r1c1_nbor_dists
    r1c3_nbor_wt = 1 / r1c3_nbor_dists

    r2c3_nbor_dists = dist[2, [0, 3, 4, 5, 6]]
    r2c3_nbor_wt = 1 / r2c3_nbor_dists

    # Collect donor values
    col1_donor_values = np.ma.masked_invalid(X[[0, 2, 3, 4, 5], 1]).copy()
    col3_donor_values = np.ma.masked_invalid(X[[0, 3, 4, 5, 6], 3]).copy()

    # Final imputed values
    r1c1_imp = np.ma.average(col1_donor_values, weights=r1c1_nbor_wt)
    r1c3_imp = np.ma.average(col3_donor_values, weights=r1c3_nbor_wt)
    r2c3_imp = np.ma.average(col3_donor_values, weights=r2c3_nbor_wt)

        [
            [0, r1c1_imp, 1, r1c3_imp],
            [1, 1, 1, r2c3_imp],
        ]
    )

        [
            [0, 0, 0, na],
            [2, 2, na, 2],
            [3, 3, 3, 3],
            [4, 4, 4, 4],
            [5, 5, 5, 5],
            [6, 6, 6, 6],
            [na, 7, 7, 7],
        ]
    )

    dist = pairwise_distances(
        X, metric="nan_euclidean", squared=False, missing_values=na
    )

    # Calculate weights
    r0c3_w = 1.0 / dist[0, 2:-1]
    r1c3_w = 1.0 / dist[1, 2:-1]
    r2c2_w = 1.0 / dist[2, (0, 1, 3, 4, 5)]
    r7c0_w = 1.0 / dist[7, 2:7]

    # Calculate weighted averages
    r0c3 = np.average(X[2:-1, -1], weights=r0c3_w)
    r1c3 = np.average(X[2:-1, -1], weights=r1c3_w)
    r2c2 = np.average(X[(0, 1, 3, 4, 5), 2], weights=r2c2_w)
    r7c0 = np.average(X[2:7, 0], weights=r7c0_w)

        [
            [0, 0, 0, r0c3],
            [1, 1, 1, r1c3],
            [2, 2, r2c2, 2],
            [r7c0, 7, 7, 7],
        ]
    )

    imputer_comp_wt = KNNImputer(missing_values=na, weights="distance")
    assert_allclose(imputer_comp_wt.fit_transform(X), X_imputed)

def test_knn_imputer_callable_metric():
    # Define callable metric that returns the l1 norm:
    def custom_callable(x, y, missing_values=np.nan, squared=False):
        x = np.ma.array(x, mask=np.isnan(x))
        y = np.ma.array(y, mask=np.isnan(y))
        dist = np.nansum(np.abs(x - y))

    X = np.array([[4, 3, 3, np.nan], [6, 9, 6, 9], [4, 8, 6, 9], [np.nan, 9, 11, 10.0]])

    X_0_3 = (9 + 9) / 2
    X_3_0 = (6 + 4) / 2
        [[4, 3, 3, X_0_3], [6, 9, 6, 9], [4, 8, 6, 9], [X_3_0, 9, 11, 10.0]]
    )

    imputer = KNNImputer(n_neighbors=2, metric=custom_callable)

def test_knn_imputer_with_simple_example(na, working_memory):
        [
            [0, na, 0, na],
        ]
    )

    r0c1 = np.mean(X[1:6, 1])
    r0c3 = np.mean(X[2:-1, -1])
    r1c3 = np.mean(X[2:-1, -1])
    r2c2 = np.mean(X[[0, 1, 3, 4, 5], 2])
    r7c0 = np.mean(X[2:-1, 0])

        [
            [0, r0c1, 0, r0c3],
        ]
    )

    with config_context(working_memory=working_memory):
        imputer_comp = KNNImputer(missing_values=na)
        assert_allclose(imputer_comp.fit_transform(X), X_imputed)

def test_knn_imputer_not_enough_valid_distances(na, weights):
    # Samples with needed feature has nan distance
    X1 = np.array([[na, 11], [na, 1], [3, na]])
    X1_imputed = np.array([[3, 11], [3, 1], [3, 6]])

    knn = KNNImputer(missing_values=na, n_neighbors=1, weights=weights)
    assert_allclose(knn.fit_transform(X1), X1_imputed)

    X2 = np.array([[4, na]])
    X2_imputed = np.array([[4, 6]])
    assert_allclose(knn.transform(X2), X2_imputed)

def test_knn_imputer_nan_distance(na, weights):
    # Samples with nan distance should be excluded from the mean computation
    X1_train = np.array([[1, 1], [na, 2]])
    X1_test = np.array([[0, na]])
    X1_test_expected = np.array([[0, 1]])

    knn1 = KNNImputer(n_neighbors=2, missing_values=na, weights=weights)
    knn1.fit(X1_train)
    assert_allclose(knn1.transform(X1_test), X1_test_expected)

    X2_train = np.array([[na, 1, 1], [2, na, 2], [3, 3, na]])
    X2_test = np.array([[na, 0, na], [0, na, na], [na, na, 0]])
    X2_test_expected = np.array([[3, 0, 1], [0, 3, 2], [2, 1, 0]])

    knn2 = KNNImputer(n_neighbors=2, missing_values=na, weights=weights)
    knn2.fit(X2_train)
    assert_allclose(knn2.transform(X2_test), X2_test_expected)

def test_knn_imputer_drops_all_nan_features(na):
    X1 = np.array([[na, 1], [na, 2]])
    knn = KNNImputer(missing_values=na, n_neighbors=1)
    X1_expected = np.array([[1], [2]])
    assert_allclose(knn.fit_transform(X1), X1_expected)

    X2 = np.array([[1, 2], [3, na]])
    X2_expected = np.array([[2], [1.5]])
    assert_allclose(knn.transform(X2), X2_expected)

def test_knn_imputer_distance_weighted_not_enough_neighbors(na, working_memory):
    X = np.array([[3, na], [2, na], [na, 4], [5, 6], [6, 8], [na, 5]])

    )

    X_01 = np.average(X[3:5, 1], weights=1 / dist[0, 3:5])
    X_11 = np.average(X[3:5, 1], weights=1 / dist[1, 3:5])
    X_20 = np.average(X[3:5, 0], weights=1 / dist[2, 3:5])
    X_50 = np.average(X[3:5, 0], weights=1 / dist[5, 3:5])

    X_expected = np.array([[3, X_01], [2, X_11], [X_20, 4], [5, 6], [6, 8], [X_50, 5]])

        knn_3 = KNNImputer(missing_values=na, n_neighbors=3, weights="distance")
        assert_allclose(knn_3.fit_transform(X), X_expected)

        knn_4 = KNNImputer(missing_values=na, n_neighbors=4, weights="distance")
        assert_allclose(knn_4.fit_transform(X), X_expected)

def test_knn_tags(na, allow_nan):
    knn = KNNImputer(missing_values=na)
    assert knn.__sklearn_tags__().input_tags.allow_nan == allow_nan

    X[::2] = np.nan
    return X

class NoFitIndicatorImputer(_BaseImputer):

    def transform(self, X, y=None):
        return self._concatenate_indicator(X, self._transform_indicator(X))

class NoTransformIndicatorImputer(_BaseImputer):
        mask = _get_mask(X, value_to_mask=np.nan)
        super()._fit_indicator(mask)

        return self._concatenate_indicator(X, None)

class NoPrecomputedMaskFit(_BaseImputer):
        self._fit_indicator(X)

class NoPrecomputedMaskTransform(_BaseImputer):
        self._fit_indicator(mask)

def test_base_imputer_not_fit(data):
    imputer = NoFitIndicatorImputer(add_indicator=True)
    err_msg = "Make sure to call _fit_indicator before _transform_indicator"
        imputer.fit(data).transform(data)
        imputer.fit_transform(data)

def test_base_imputer_not_transform(data):
    imputer = NoTransformIndicatorImputer(add_indicator=True)
    err_msg = (
        "Call _fit_indicator and _transform_indicator in the imputer implementation"
    )

def test_base_no_precomputed_mask_fit(data):
    imputer = NoPrecomputedMaskFit(add_indicator=True)
    err_msg = "precomputed is True but the input data is not a mask"
        imputer.fit(data)

def test_base_no_precomputed_mask_transform(data):
    imputer = NoPrecomputedMaskTransform(add_indicator=True)
    imputer.fit(data)
        imputer.transform(data)

def test_assign_where(X1_type):
    """Check the behaviour of the private helpers `_assign_where`."""

    n_samples, n_features = 10, 5
    X1 = _convert_container(rng.randn(n_samples, n_features), constructor_name=X1_type)
    X2 = rng.randn(n_samples, n_features)
    mask = rng.randint(0, 2, size=(n_samples, n_features)).astype(bool)

    _assign_where(X1, X2, mask)

    if X1_type == "dataframe":
        X1 = X1.to_numpy()
    assert_allclose(X1[mask], X2[mask])

def _assert_array_equal_and_same_dtype(x, y):
    assert_array_equal(x, y)
    assert x.dtype == y.dtype

def _assert_allclose_and_same_dtype(x, y):
    assert_allclose(x, y)

def _check_statistics(
    X, X_true, strategy, statistics, missing_values, sparse_container
):
    """Utility function for testing imputation for a given strategy.

    Test with dense and sparse arrays

    Check that:
        - the statistics (mean, median, mode) are correct
        - the missing values are imputed correctly"""

    err_msg = "Parameters: strategy = %s, missing_values = %s, sparse = {0}" % (
        strategy,
        missing_values,
    )

    assert_ae = assert_array_equal

    if X.dtype.kind == "f" or X_true.dtype.kind == "f":
        assert_ae = assert_array_almost_equal

    # Normal matrix
    imputer = SimpleImputer(missing_values=missing_values, strategy=strategy)
    X_trans = imputer.fit(X).transform(X.copy())
    assert_ae(imputer.statistics_, statistics, err_msg=err_msg.format(False))
    assert_ae(X_trans, X_true, err_msg=err_msg.format(False))

    # Sparse matrix
    imputer.fit(sparse_container(X))
    X_trans = imputer.transform(sparse_container(X.copy()))

    if sparse.issparse(X_trans):
        X_trans = X_trans.toarray()

    assert_ae(imputer.statistics_, statistics, err_msg=err_msg.format(True))
    assert_ae(X_trans, X_true, err_msg=err_msg.format(True))

def test_imputation_shape(strategy, csr_container):
    # Verify the shapes of the imputed matrix for different strategies.

    imputer = SimpleImputer(strategy=strategy)
    X_imputed = imputer.fit_transform(csr_container(X))
    assert X_imputed.shape == (10, 2)

    iterative_imputer = IterativeImputer(initial_strategy=strategy)
    X_imputed = iterative_imputer.fit_transform(X)

def test_imputation_deletion_warning(strategy):
    X = np.ones((3, 5))
    X[:, 0] = np.nan
    imputer = SimpleImputer(strategy=strategy).fit(X)

    with pytest.warns(UserWarning, match="Skipping"):

def test_imputation_deletion_warning_feature_names(strategy):

    missing_values = np.nan
    feature_names = np.array(["a", "b", "c", "d"], dtype=object)
        [
            [missing_values, missing_values, 1, missing_values],
            [4, missing_values, 2, 10],
        ],
        columns=feature_names,
    )

    # check SimpleImputer returning feature name attribute correctly
    assert_array_equal(imputer.feature_names_in_, feature_names)

    # ensure that skipped feature warning includes feature name
        UserWarning, match=r"Skipping features without any observed values: \['b'\]"
    ):

def test_imputation_error_sparse_0(strategy, csc_container):
    # check that error are raised when missing_values = 0 and input is sparse
    X[0] = 0

    imputer = SimpleImputer(strategy=strategy, missing_values=0)
    with pytest.raises(ValueError, match="Provide a dense array"):

    imputer.fit(X.toarray())

def safe_median(arr, *args, **kwargs):
    # np.median([]) raises a TypeError for numpy >= 1.10.1
    length = arr.size if hasattr(arr, "size") else len(arr)
    return np.nan if length == 0 else np.median(arr, *args, **kwargs)

def safe_mean(arr, *args, **kwargs):
    # np.mean([]) raises a RuntimeWarning for numpy >= 1.10.1
    return np.nan if length == 0 else np.mean(arr, *args, **kwargs)

def test_imputation_mean_median(csc_container):
    # Test imputation using the mean and median strategies, when
    # missing_values != 0.

    dim = 10
    dec = 10
    shape = (dim * dim, dim + dec)

    zeros = np.zeros(shape[0])
    values = np.arange(1, shape[0] + 1)
    values[4::2] = -values[4::2]

    tests = [
        ("mean", np.nan, lambda z, v, p: safe_mean(np.hstack((z, v)))),
        ("median", np.nan, lambda z, v, p: safe_median(np.hstack((z, v)))),
    ]

    for strategy, test_missing_values, true_value_fun in tests:
        X = np.empty(shape)
        X_true = np.empty(shape)
        true_statistics = np.empty(shape[1])

        # Create a matrix X with columns
        #    - with only zeros,
        #    - with only missing values
        #    - with zeros, missing values and values
        # And a matrix X_true containing all true values
        for j in range(shape[1]):
            nb_zeros = (j - dec + 1 > 0) * (j - dec + 1) * (j - dec + 1)
            nb_missing_values = max(shape[0] + dec * dec - (j + dec) * (j + dec), 0)
            nb_values = shape[0] - nb_zeros - nb_missing_values

            z = zeros[:nb_zeros]
            p = np.repeat(test_missing_values, nb_missing_values)
            v = values[rng.permutation(len(values))[:nb_values]]

            true_statistics[j] = true_value_fun(z, v, p)

            # Create the columns
            X[:, j] = np.hstack((v, z, p))

            if 0 == test_missing_values:
                # XXX unreached code as of v0.22
                X_true[:, j] = np.hstack(
                    (v, np.repeat(true_statistics[j], nb_missing_values + nb_zeros))
                )
            else:
                    (v, z, np.repeat(true_statistics[j], nb_missing_values))
                )

            # Shuffle them the same way
            np.random.RandomState(j).shuffle(X[:, j])
            np.random.RandomState(j).shuffle(X_true[:, j])

        # Mean doesn't support columns containing NaNs, median does
        if strategy == "median":
            cols_to_keep = ~np.isnan(X_true).any(axis=0)
        else:
            cols_to_keep = ~np.isnan(X_true).all(axis=0)

        X_true = X_true[:, cols_to_keep]

        _check_statistics(
            X, X_true, strategy, true_statistics, test_missing_values, csc_container
        )

def test_imputation_median_special_cases(csc_container):
    # Test median imputation with sparse boundary cases
        [
            [0, np.nan, np.nan],  # odd: implicit zero
            [5, np.nan, np.nan],  # odd: explicit nonzero
            [0, 0, np.nan],  # even: average two zeros
            [-5, 0, np.nan],  # even: avg zero and neg
            [0, 5, np.nan],  # even: avg zero and pos
            [4, 5, np.nan],  # even: avg nonzeros
            [-4, -5, np.nan],  # even: avg negatives
            [-1, 2, np.nan],  # even: crossing neg and pos
        ]
    ).transpose()

    X_imputed_median = np.array(
        [
            [0, 0, 0],
            [5, 5, 5],
            [0, 0, 0],
            [-5, 0, -2.5],
            [0, 5, 2.5],
            [4, 5, 4.5],
            [-4, -5, -4.5],
            [-1, 2, 0.5],
        ]
    statistics_median = [0, 5, 0, -2.5, 2.5, 4.5, -4.5, 0.5]

    _check_statistics(
        X, X_imputed_median, "median", statistics_median, np.nan, csc_container
    )

def test_imputation_mean_median_error_invalid_type(strategy, dtype):
    X = np.array([["a", "b", 3], [4, "e", 6], ["g", "h", 9]], dtype=dtype)
    msg = "non-numeric data:\ncould not convert string to float:"
        imputer = SimpleImputer(strategy=strategy)
        imputer.fit_transform(X)

def test_imputation_mean_median_error_invalid_type_list_pandas(strategy, type):
    X = [["a", "b", 3], [4, "e", 6], ["g", "h", 9]]
    if type == "dataframe":
        X = pd.DataFrame(X)

def test_imputation_const_mostf_error_invalid_types(strategy, dtype):
    # Test imputation on non-numeric data using "most_frequent" and "constant"
    # strategy
        [
            [np.nan, np.nan, "a", "f"],
            [np.nan, "c", np.nan, "d"],
            [np.nan, "b", "d", np.nan],
            [np.nan, "c", "d", "h"],
        ],
    )

    err_msg = "SimpleImputer does not support data"
        imputer.fit(X).transform(X)

def test_imputation_most_frequent(csc_container):
    # Test imputation using the most-frequent strategy.
        [
            [-1, -1, 0, 5],
            [-1, 2, -1, 3],
            [-1, 1, 3, -1],
            [-1, 2, 3, 7],
        ]
    )

    X_true = np.array(
        [
            [2, 0, 5],
            [2, 3, 3],
            [1, 3, 3],
            [2, 3, 7],
        ]
    )

    # scipy.stats.mode, used in SimpleImputer, doesn't return the first most
    # frequent as promised in the doc but the lowest most frequent. When this
    # test will fail after an update of scipy, SimpleImputer will need to be
    # updated to be consistent with the new (correct) behaviour
    _check_statistics(X, X_true, "most_frequent", [np.nan, 2, 3, 3], -1, csc_container)

def test_imputation_most_frequent_objects(marker):
        [
            [marker, marker, "a", "f"],
            [marker, "c", marker, "d"],
            [marker, "b", "d", marker],
            [marker, "c", "d", "h"],
        ],
    )

        [
            ["c", "a", "f"],
            ["c", "d", "d"],
            ["b", "d", "d"],
            ["c", "d", "h"],
        ],
    )

    imputer = SimpleImputer(missing_values=marker, strategy="most_frequent")
    X_trans = imputer.fit(X).transform(X)

    assert_array_equal(X_trans, X_true)

def test_imputation_most_frequent_pandas(dtype):
    # Test imputation using the most frequent strategy on pandas df

    f = io.StringIO("Cat1,Cat2,Cat3,Cat4\n,i,x,\na,,y,\na,j,,\nb,j,x,")

    df = pd.read_csv(f, dtype=dtype)

        [["a", "i", "x"], ["a", "j", "y"], ["a", "j", "x"], ["b", "j", "x"]],
    )

    imputer = SimpleImputer(strategy="most_frequent")
    X_trans = imputer.fit_transform(df)

def test_imputation_constant_error_invalid_type(X_data, missing_value):
    # Verify that exceptions are raised on invalid fill_value type
    X = np.full((3, 5), X_data, dtype=float)
    X[0, 0] = missing_value

    fill_value = "x"
    err_msg = f"fill_value={fill_value!r} (of type {type(fill_value)!r}) cannot be cast"
    with pytest.raises(ValueError, match=re.escape(err_msg)):
        imputer = SimpleImputer(
            missing_values=missing_value, strategy="constant", fill_value=fill_value
        )

def test_imputation_constant_integer():
    # Test imputation using the constant strategy on integers
    X = np.array([[-1, 2, 3, -1], [4, -1, 5, -1], [6, 7, -1, -1], [8, 9, 0, -1]])

    X_true = np.array([[0, 2, 3, 0], [4, 0, 5, 0], [6, 7, 0, 0], [8, 9, 0, 0]])

    imputer = SimpleImputer(
        missing_values=-1, strategy="constant", fill_value=0, keep_empty_features=True
    )

def test_imputation_constant_float(array_constructor):
    # Test imputation using the constant strategy on floats
        [
            [np.nan, 1.1, 0, np.nan],
            [1.2, np.nan, 1.3, np.nan],
            [0, 0, np.nan, np.nan],
            [1.4, 1.5, 0, np.nan],
        ]
    )

        [[-1, 1.1, 0, -1], [1.2, -1, 1.3, -1], [0, 0, -1, -1], [1.4, 1.5, 0, -1]]
    )

    X_true = array_constructor(X_true)

        strategy="constant", fill_value=-1, keep_empty_features=True
    )

    assert_allclose_dense_sparse(X_trans, X_true)

def test_imputation_constant_object(marker):
    # Test imputation using the constant strategy on objects
        [
            [marker, "a", "b", marker],
            ["c", marker, "d", marker],
            ["e", "f", marker, marker],
            ["g", "h", "i", marker],
        ],
    )

        [
            ["missing", "a", "b", "missing"],
            ["c", "missing", "d", "missing"],
            ["e", "f", "missing", "missing"],
            ["g", "h", "i", "missing"],
        ],
    )

        missing_values=marker,
        strategy="constant",
        fill_value="missing",
        keep_empty_features=True,
    )

def test_imputation_constant_pandas(dtype):
    # Test imputation using the constant strategy on pandas df

        [
            ["missing_value", "i", "x", "missing_value"],
            ["a", "missing_value", "y", "missing_value"],
            ["a", "j", "missing_value", "missing_value"],
            ["b", "j", "x", "missing_value"],
        ],
    )

    imputer = SimpleImputer(strategy="constant", keep_empty_features=True)

def test_iterative_imputer_one_feature(X):
    # check we exit early when there is a single feature
    imputer = IterativeImputer().fit(X)
    assert imputer.n_iter_ == 0
    imputer = IterativeImputer()
    imputer.fit([[1], [2]])
    imputer.fit([[1], [np.nan]])

def test_imputation_pipeline_grid_search():
    # Test imputation within a pipeline + gridsearch.
    X = _sparse_random_matrix(100, 100, density=0.10)
    missing_values = X.data[0]

    pipeline = Pipeline(
        [
            ("imputer", SimpleImputer(missing_values=missing_values)),
            ("tree", tree.DecisionTreeRegressor(random_state=0)),
        ]
    )

    parameters = {"imputer__strategy": ["mean", "median", "most_frequent"]}

    Y = _sparse_random_matrix(100, 1, density=0.10).toarray()
    gs = GridSearchCV(pipeline, parameters)
    gs.fit(X, Y)

def test_imputation_copy():
    # Test imputation with copy
    X_orig = _sparse_random_matrix(5, 5, density=0.75, random_state=0)

    # copy=True, dense => copy
    X = X_orig.copy().toarray()
    imputer = SimpleImputer(missing_values=0, strategy="mean", copy=True)
    Xt = imputer.fit(X).transform(X)
    Xt[0, 0] = -1
    assert not np.all(X == Xt)

    # copy=True, sparse csr => copy
    X = X_orig.copy()
    imputer = SimpleImputer(missing_values=X.data[0], strategy="mean", copy=True)
    Xt.data[0] = -1
    assert not np.all(X.data == Xt.data)

    # copy=False, dense => no copy
    imputer = SimpleImputer(missing_values=0, strategy="mean", copy=False)
    assert_array_almost_equal(X, Xt)

    # copy=False, sparse csc => no copy
    X = X_orig.copy().tocsc()
    imputer = SimpleImputer(missing_values=X.data[0], strategy="mean", copy=False)
    assert_array_almost_equal(X.data, Xt.data)

    # copy=False, sparse csr => copy

def test_iterative_imputer_zero_iters():

    n = 100
    d = 10
    X = _sparse_random_matrix(n, d, density=0.10, random_state=rng).toarray()
    missing_flag = X == 0
    X[missing_flag] = np.nan

    imputer = IterativeImputer(max_iter=0)
    # with max_iter=0, only initial imputation is performed
    assert_allclose(X_imputed, imputer.initial_imputer_.transform(X))

    # repeat but force n_iter_ to 0
    imputer = IterativeImputer(max_iter=5).fit(X)
    # transformed should not be equal to initial imputation
    assert not np.all(imputer.transform(X) == imputer.initial_imputer_.transform(X))

    imputer.n_iter_ = 0
    # now they should be equal as only initial imputation is done
    assert_allclose(imputer.transform(X), imputer.initial_imputer_.transform(X))

def test_iterative_imputer_verbose():

    n = 100
    d = 3
    imputer = IterativeImputer(missing_values=0, max_iter=1, verbose=1)
    imputer.fit(X)
    imputer.transform(X)
    imputer = IterativeImputer(missing_values=0, max_iter=1, verbose=2)

def test_iterative_imputer_all_missing():
    n = 100
    d = 3
    X = np.zeros((n, d))
    imputer = IterativeImputer(missing_values=0, max_iter=1)

def test_iterative_imputer_imputation_order(imputation_order):
    n = 100
    d = 10
    max_iter = 2
    X[:, 0] = 1  # this column should not be discarded by IterativeImputer

    imputer = IterativeImputer(
        missing_values=0,
        n_nearest_features=5,
        skip_complete=True,
        min_value=0,
        max_value=1,
        verbose=1,
        imputation_order=imputation_order,
    )
    imputer.fit_transform(X)
    ordered_idx = [i.feat_idx for i in imputer.imputation_sequence_]

    assert len(ordered_idx) // imputer.n_iter_ == imputer.n_features_with_missing_

    if imputation_order == "roman":
        assert np.all(ordered_idx[: d - 1] == np.arange(1, d))
    elif imputation_order == "arabic":
        assert np.all(ordered_idx[: d - 1] == np.arange(d - 1, 0, -1))
    elif imputation_order == "random":
        ordered_idx_round_1 = ordered_idx[: d - 1]
        ordered_idx_round_2 = ordered_idx[d - 1 :]
        assert ordered_idx_round_1 != ordered_idx_round_2
    elif "ending" in imputation_order:
        assert len(ordered_idx) == max_iter * (d - 1)

def test_iterative_imputer_estimators(estimator):

    n = 100
    d = 10

        missing_values=0, max_iter=1, estimator=estimator, random_state=rng
    )

    # check that types are correct for estimators
    hashes = []
    for triplet in imputer.imputation_sequence_:
        expected_type = (
            type(estimator) if estimator is not None else type(BayesianRidge())
        )
        assert isinstance(triplet.estimator, expected_type)
        hashes.append(id(triplet.estimator))

    # check that each estimator is unique
    assert len(set(hashes)) == len(hashes)

def test_iterative_imputer_clip():
    n = 100
    d = 10

        missing_values=0, max_iter=1, min_value=0.1, max_value=0.2, random_state=rng
    )

    Xt = imputer.fit_transform(X)
    assert_allclose(np.min(Xt[X == 0]), 0.1)
    assert_allclose(np.max(Xt[X == 0]), 0.2)
    assert_allclose(Xt[X != 0], X[X != 0])

def test_iterative_imputer_clip_truncnorm():
    n = 100
    d = 10
    X[:, 0] = 1

        sample_posterior=True,
        min_value=0.1,
        max_value=0.2,
        imputation_order="random",
    )

def test_iterative_imputer_truncated_normal_posterior():
    #  test that the values that are imputed using `sample_posterior=True`
    #  with boundaries (`min_value` and `max_value` are not None) are drawn
    #  from a distribution that looks gaussian via the Kolmogorov Smirnov test.
    #  note that starting from the wrong random seed will make this test fail
    #  because random sampling doesn't occur at all when the imputation
    #  is outside of the (min_value, max_value) range

    X = rng.normal(size=(5, 5))
    X[0][0] = np.nan

        min_value=0, max_value=0.5, sample_posterior=True, random_state=rng
    )

    # generate multiple imputations for the single missing value
    imputations = np.array([imputer.transform(X)[0][0] for _ in range(100)])

    assert all(imputations >= 0)
    assert all(imputations <= 0.5)

    mu, sigma = imputations.mean(), imputations.std()
    ks_statistic, p_value = kstest((imputations - mu) / sigma, "norm")
    if sigma == 0:
        sigma += 1e-12
    # we want to fail to reject null hypothesis
    # null hypothesis: distributions are the same
    assert ks_statistic < 0.2 or p_value > 0.1, "The posterior does appear to be normal"

def test_iterative_imputer_missing_at_transform(strategy):
    n = 100
    d = 10
    X_train = rng.randint(low=0, high=3, size=(n, d))
    X_test = rng.randint(low=0, high=3, size=(n, d))

    X_train[:, 0] = 1  # definitely no missing values in 0th column
    X_test[0, 0] = 0  # definitely missing value in 0th column

        missing_values=0, max_iter=1, initial_strategy=strategy, random_state=rng
    initial_imputer = SimpleImputer(missing_values=0, strategy=strategy).fit(X_train)

    # if there were no missing values at time of fit, then imputer will
    # only use the initial imputer for that feature at transform
        imputer.transform(X_test)[:, 0], initial_imputer.transform(X_test)[:, 0]
    )

def test_iterative_imputer_transform_stochasticity():
    rng1 = np.random.RandomState(0)
    rng2 = np.random.RandomState(1)
    n = 100
    d = 10
    X = _sparse_random_matrix(n, d, density=0.10, random_state=rng1).toarray()

    # when sample_posterior=True, two transforms shouldn't be equal
        missing_values=0, max_iter=1, sample_posterior=True, random_state=rng1
    )

    X_fitted_1 = imputer.transform(X)
    X_fitted_2 = imputer.transform(X)

    # sufficient to assert that the means are not the same
    assert np.mean(X_fitted_1) != pytest.approx(np.mean(X_fitted_2))

    # when sample_posterior=False, and n_nearest_features=None
    # and imputation_order is not random
    # the two transforms should be identical even if rng are different
    imputer1 = IterativeImputer(
        random_state=rng1,
    )

    imputer2 = IterativeImputer(
        random_state=rng2,
    )
    imputer1.fit(X)
    imputer2.fit(X)

    X_fitted_1a = imputer1.transform(X)
    X_fitted_1b = imputer1.transform(X)
    X_fitted_2 = imputer2.transform(X)

    assert_allclose(X_fitted_1a, X_fitted_1b)
    assert_allclose(X_fitted_1a, X_fitted_2)

def test_iterative_imputer_no_missing():
    X = rng.rand(100, 100)
    m1 = IterativeImputer(max_iter=10, random_state=rng)
    m2 = IterativeImputer(max_iter=10, random_state=rng)
    pred1 = m1.fit(X).transform(X)
    pred2 = m2.fit_transform(X)
    # should exclude the first column entirely
    assert_allclose(X[:, 1:], pred1)
    # fit and fit_transform should both be identical
    assert_allclose(pred1, pred2)

def test_iterative_imputer_rank_one():
    d = 50
    A = rng.rand(d, 1)
    B = rng.rand(1, d)
    X = np.dot(A, B)
    nan_mask = rng.rand(d, d) < 0.5
    X_missing[nan_mask] = np.nan

    imputer = IterativeImputer(max_iter=5, verbose=1, random_state=rng)
    X_filled = imputer.fit_transform(X_missing)
    assert_allclose(X_filled, X, atol=0.02)

def test_iterative_imputer_transform_recovery(rank):
    n = 70
    d = 70
    A = rng.rand(n, rank)
    B = rng.rand(rank, d)
    X_filled = np.dot(A, B)
    nan_mask = rng.rand(n, d) < 0.5
    X_missing = X_filled.copy()

    # split up data in half
    n = n // 2
    X_train = X_missing[:n]
    X_test_filled = X_filled[n:]
    X_test = X_missing[n:]

        max_iter=5, imputation_order="descending", verbose=1, random_state=rng
    X_test_est = imputer.transform(X_test)
    assert_allclose(X_test_filled, X_test_est, atol=0.1)

def test_iterative_imputer_additive_matrix():
    n = 100
    d = 10
    A = rng.randn(n, d)
    B = rng.randn(n, d)
    X_filled = np.zeros(A.shape)
    for i in range(d):
        for j in range(d):
            X_filled[:, (i + j) % d] += (A[:, i] + B[:, j]) / 2
    # a quarter is randomly missing
    nan_mask = rng.rand(n, d) < 0.25

    # split up data
    n = n // 2

    imputer = IterativeImputer(max_iter=10, verbose=1, random_state=rng).fit(X_train)
    assert_allclose(X_test_filled, X_test_est, rtol=1e-3, atol=0.01)

def test_iterative_imputer_early_stopping():
    n = 50
    d = 5
    A = rng.rand(n, 1)

        max_iter=100, tol=1e-2, sample_posterior=False, verbose=1, random_state=rng
    )
    X_filled_100 = imputer.fit_transform(X_missing)
    assert len(imputer.imputation_sequence_) == d * imputer.n_iter_

        max_iter=imputer.n_iter_, sample_posterior=False, verbose=1, random_state=rng
    )
    X_filled_early = imputer.fit_transform(X_missing)
    assert_allclose(X_filled_100, X_filled_early, atol=1e-7)

        max_iter=100, tol=0, sample_posterior=False, verbose=1, random_state=rng
    )
    imputer.fit(X_missing)
    assert imputer.n_iter_ == imputer.max_iter

def test_iterative_imputer_catch_warning():
    # check that we catch a RuntimeWarning due to a division by zero when a
    # feature is constant in the dataset
    X, y = load_diabetes(return_X_y=True)

    # simulate that a feature only contain one category during fit
    X[:, 3] = 1

    # add some missing values
    missing_rate = 0.15
    for feat in range(n_features):
        sample_idx = rng.choice(
            np.arange(n_samples), size=int(n_samples * missing_rate), replace=False
        )
        X[sample_idx, feat] = np.nan

    imputer = IterativeImputer(n_nearest_features=5, sample_posterior=True)
        X_fill = imputer.fit_transform(X, y)
    assert not np.any(np.isnan(X_fill))

def test_iterative_imputer_min_max_array_like(min_value, max_value, correct_output):
    # check that passing scalar or array-like
    # for min_value and max_value in IterativeImputer works
    X = np.random.RandomState(0).randn(10, 3)
    imputer = IterativeImputer(min_value=min_value, max_value=max_value)

    assert isinstance(imputer._min_value, np.ndarray) and isinstance(
        imputer._max_value, np.ndarray
    )
    assert (imputer._min_value.shape[0] == X.shape[1]) and (
        imputer._max_value.shape[0] == X.shape[1]
    )

    assert_allclose(correct_output[0, :], imputer._min_value)
    assert_allclose(correct_output[1, :], imputer._max_value)

def test_iterative_imputer_catch_min_max_error(min_value, max_value, err_msg):

def test_iterative_imputer_min_max_array_like_imputation(min_max_1, min_max_2):
    # Test that None/inf and scalar/vector give the same imputation
        [
            [np.nan, 2, 2, 1],
            [10, np.nan, np.nan, 7],
            [3, 1, np.nan, 1],
            [np.nan, 4, 2, np.nan],
        ]
    )
        [[np.nan, 2, np.nan, 5], [2, 4, np.nan, np.nan], [np.nan, 1, 10, 1]]
    )
        min_value=min_max_1[0], max_value=min_max_1[1], random_state=0
    )
        min_value=min_max_2[0], max_value=min_max_2[1], random_state=0
    )
    X_test_imputed1 = imputer1.fit(X_train).transform(X_test)
    X_test_imputed2 = imputer2.fit(X_train).transform(X_test)
    assert_allclose(X_test_imputed1[:, 0], X_test_imputed2[:, 0])

def test_iterative_imputer_skip_non_missing(skip_complete):
    # check the imputing strategy when missing data are present in the
    # testing set only.
    # taken from: https://github.com/scikit-learn/scikit-learn/issues/14383
    X_train = np.array([[5, 2, 2, 1], [10, 1, 2, 7], [3, 1, 1, 1], [8, 4, 2, 2]])
    X_test = np.array([[np.nan, 2, 4, 5], [np.nan, 4, 1, 2], [np.nan, 1, 10, 1]])
        initial_strategy="mean", skip_complete=skip_complete, random_state=rng
    )
    X_test_est = imputer.fit(X_train).transform(X_test)
    if skip_complete:
        # impute with the initial strategy: 'mean'
        assert_allclose(X_test_est[:, 0], np.mean(X_train[:, 0]))
    else:
        assert_allclose(X_test_est[:, 0], [11, 7, 12], rtol=1e-4)

def test_iterative_imputer_dont_set_random_state(rs_imputer, rs_estimator):
    class ZeroEstimator:
        def __init__(self, random_state):
            self.random_state = random_state

        def fit(self, *args, **kgards):

    estimator = ZeroEstimator(random_state=rs_estimator)
    imputer = IterativeImputer(random_state=rs_imputer)
    X_train = np.zeros((10, 3))
    assert estimator.random_state == rs_estimator

def test_missing_indicator_error(X_fit, X_trans, params, msg_err):
    indicator = MissingIndicator(missing_values=-1)
    indicator.set_params(**params)
    with pytest.raises(ValueError, match=msg_err):
        indicator.fit(X_fit).transform(X_trans)

def _generate_missing_indicator_cases():
    missing_values_dtypes = [(0, np.int32), (np.nan, np.float64), (-1, np.int32)]
    arr_types = (
        [np.array]
        + CSC_CONTAINERS
        + CSR_CONTAINERS
        + COO_CONTAINERS
        + LIL_CONTAINERS
        + BSR_CONTAINERS
    )
    return [
        (arr_type, missing_values, dtype)
        for arr_type, (missing_values, dtype) in product(
            arr_types, missing_values_dtypes
        )
        if not (missing_values == 0 and arr_type is not np.array)
    ]

def test_missing_indicator_new(
    missing_values, arr_type, dtype, param_features, n_features, features_indices
):
    X_fit = np.array([[missing_values, missing_values, 1], [4, 2, missing_values]])
    X_trans = np.array([[missing_values, missing_values, 1], [4, 12, 10]])
    X_fit_expected = np.array([[1, 1, 0], [0, 0, 1]])
    X_trans_expected = np.array([[1, 1, 0], [0, 0, 0]])

    # convert the input to the right array format and right dtype
    X_fit = arr_type(X_fit).astype(dtype)
    X_trans = arr_type(X_trans).astype(dtype)
    X_fit_expected = X_fit_expected.astype(dtype)
    X_trans_expected = X_trans_expected.astype(dtype)

    indicator = MissingIndicator(
        missing_values=missing_values, features=param_features, sparse=False
    )
    X_fit_mask = indicator.fit_transform(X_fit)
    X_trans_mask = indicator.transform(X_trans)

    assert X_fit_mask.shape[1] == n_features
    assert X_trans_mask.shape[1] == n_features

    assert_array_equal(indicator.features_, features_indices)
    assert_allclose(X_fit_mask, X_fit_expected[:, features_indices])
    assert_allclose(X_trans_mask, X_trans_expected[:, features_indices])

    assert X_fit_mask.dtype == bool
    assert X_trans_mask.dtype == bool
    assert isinstance(X_fit_mask, np.ndarray)
    assert isinstance(X_trans_mask, np.ndarray)

    indicator.set_params(sparse=True)
    X_fit_mask_sparse = indicator.fit_transform(X_fit)
    X_trans_mask_sparse = indicator.transform(X_trans)

    assert X_fit_mask_sparse.dtype == bool
    assert X_trans_mask_sparse.dtype == bool
    assert X_fit_mask_sparse.format == "csc"
    assert X_trans_mask_sparse.format == "csc"
    assert_allclose(X_fit_mask_sparse.toarray(), X_fit_mask)
    assert_allclose(X_trans_mask_sparse.toarray(), X_trans_mask)

def test_missing_indicator_raise_on_sparse_with_missing_0(arr_type):
    # test for sparse input and missing_value == 0

    missing_values = 0
    X_fit = np.array([[missing_values, missing_values, 1], [4, missing_values, 2]])

    # convert the input to the right array format
    X_fit_sparse = arr_type(X_fit)
    X_trans_sparse = arr_type(X_trans)

    indicator = MissingIndicator(missing_values=missing_values)

    with pytest.raises(ValueError, match="Sparse input with missing_values=0"):
        indicator.fit_transform(X_fit_sparse)

    indicator.fit_transform(X_fit)
        indicator.transform(X_trans_sparse)

def test_missing_indicator_sparse_param(arr_type, missing_values, param_sparse):
    # check the format of the output with different sparse parameter
    X_fit = arr_type(X_fit).astype(np.float64)
    X_trans = arr_type(X_trans).astype(np.float64)

    indicator = MissingIndicator(missing_values=missing_values, sparse=param_sparse)

    if param_sparse is True:
        assert X_fit_mask.format == "csc"
        assert X_trans_mask.format == "csc"
    elif param_sparse == "auto" and missing_values == 0:
        assert isinstance(X_fit_mask, np.ndarray)
        assert isinstance(X_trans_mask, np.ndarray)
    elif param_sparse is False:
    else:
        if sparse.issparse(X_fit):
            assert X_fit_mask.format == "csc"
            assert X_trans_mask.format == "csc"
        else:
            assert isinstance(X_fit_mask, np.ndarray)
            assert isinstance(X_trans_mask, np.ndarray)

def test_missing_indicator_string():
    X = np.array([["a", "b", "c"], ["b", "c", "a"]], dtype=object)
    indicator = MissingIndicator(missing_values="a", features="all")
    X_trans = indicator.fit_transform(X)
    assert_array_equal(X_trans, np.array([[True, False, False], [False, False, True]]))

def test_missing_indicator_with_imputer(X, missing_values, X_trans_exp):
    trans = make_union(
        SimpleImputer(missing_values=missing_values, strategy="most_frequent"),
        MissingIndicator(missing_values=missing_values),
    )
    X_trans = trans.fit_transform(X)
    assert_array_equal(X_trans, X_trans_exp)

def test_inconsistent_dtype_X_missing_values(
    imputer_constructor, imputer_missing_values, missing_value, err_msg
):
    # regression test for issue #11390. Comparison between incoherent dtype
    # for X and missing_values was not raising a proper error.

    imputer = imputer_constructor(missing_values=imputer_missing_values)

def test_missing_indicator_no_missing():
    # check that all features are dropped if there are no missing values when
    # features='missing-only' (#13491)
    X = np.array([[1, 1], [1, 1]])

    mi = MissingIndicator(features="missing-only", missing_values=-1)
    Xt = mi.fit_transform(X)

    assert Xt.shape[1] == 0

def test_missing_indicator_sparse_no_explicit_zeros(csr_container):
    # Check that non missing values don't become explicit zeros in the mask
    # generated by missing indicator when X is sparse. (#13491)
    X = csr_container([[0, 1, 2], [1, 2, 0], [2, 0, 1]])

    mi = MissingIndicator(features="all", missing_values=1)

    assert Xt.nnz == Xt.sum()

def test_imputer_without_indicator(imputer_constructor):
    imputer = imputer_constructor()

    assert imputer.indicator_ is None

def test_simple_imputation_add_indicator_sparse_matrix(arr_type):
    X_sparse = arr_type([[np.nan, 1, 5], [2, np.nan, 1], [6, 3, np.nan], [1, 2, 9]])
        [
            [3.0, 1.0, 5.0, 1.0, 0.0, 0.0],
            [2.0, 2.0, 1.0, 0.0, 1.0, 0.0],
            [6.0, 3.0, 5.0, 0.0, 0.0, 1.0],
            [1.0, 2.0, 9.0, 0.0, 0.0, 0.0],
        ]
    )

    imputer = SimpleImputer(missing_values=np.nan, add_indicator=True)
    X_trans = imputer.fit_transform(X_sparse)

    assert X_trans.shape == X_true.shape
    assert_allclose(X_trans.toarray(), X_true)

def test_simple_imputation_string_list(strategy, expected):
    X = [["a", "b"], ["c", np.nan]]

    X_true = np.array([["a", "b"], ["c", expected]], dtype=object)

def test_imputation_order(order, idx_order):
    # regression test for #15393
    X = rng.rand(100, 5)
    X[:50, 1] = np.nan
    X[:30, 0] = np.nan
    X[:20, 2] = np.nan
    X[:10, 4] = np.nan

        trs = IterativeImputer(max_iter=1, imputation_order=order, random_state=0).fit(
            X
        )
        idx = [x.feat_idx for x in trs.imputation_sequence_]
        assert idx == idx_order

def test_simple_imputation_inverse_transform(missing_value):
    # Test inverse_transform feature for np.nan
    X_1 = np.array(
        [
            [9, missing_value, 3, -1],
            [4, -1, 5, 4],
            [6, 7, missing_value, -1],
            [8, 9, 0, missing_value],
        ]
    )

    X_2 = np.array(
        [
            [5, 4, 2, 1],
            [2, 1, missing_value, 3],
            [9, missing_value, 7, 1],
            [6, 4, 2, missing_value],
        ]
    )

    X_3 = np.array(
        [
            [1, missing_value, 5, 9],
            [missing_value, 4, missing_value, missing_value],
            [2, missing_value, 7, missing_value],
            [missing_value, 3, missing_value, 8],
        ]
    )

    X_4 = np.array(
        [
            [1, 1, 1, 3],
            [missing_value, 2, missing_value, 1],
            [2, 3, 3, 4],
            [missing_value, 4, missing_value, 2],
        ]
    )

        missing_values=missing_value, strategy="mean", add_indicator=True
    )

    X_1_trans = imputer.fit_transform(X_1)
    X_1_inv_trans = imputer.inverse_transform(X_1_trans)

    X_2_trans = imputer.transform(X_2)  # test on new data
    X_2_inv_trans = imputer.inverse_transform(X_2_trans)

    assert_array_equal(X_1_inv_trans, X_1)
    assert_array_equal(X_2_inv_trans, X_2)

    for X in [X_3, X_4]:
        X_trans = imputer.fit_transform(X)
        X_inv_trans = imputer.inverse_transform(X_trans)
        assert_array_equal(X_inv_trans, X)

def test_simple_imputation_inverse_transform_exceptions(missing_value):
        [
        ]
    )

    imputer = SimpleImputer(missing_values=missing_value, strategy="mean")
        ValueError, match=f"Got 'add_indicator={imputer.add_indicator}'"
    ):
        imputer.inverse_transform(X_1_trans)

def test_most_frequent(expected, array, dtype, extra_value, n_repeat):
    assert expected == _most_frequent(
        np.array(array, dtype=dtype), extra_value, n_repeat
    )

def test_iterative_imputer_keep_empty_features(initial_strategy):
    """Check the behaviour of the iterative imputer with different initial strategy
    and keeping empty features (i.e. features containing only missing values).
    """
    X = np.array([[1, np.nan, 2], [3, np.nan, np.nan]])

        initial_strategy=initial_strategy, keep_empty_features=True
    )
    assert_allclose(X_imputed[:, 1], 0)
    X_imputed = imputer.transform(X)

def test_iterative_imputer_constant_fill_value():
    """Check that we propagate properly the parameter `fill_value`."""

    fill_value = 100
        missing_values=-1,
        initial_strategy="constant",
        fill_value=fill_value,
        max_iter=0,
    )
    assert_array_equal(imputer.initial_imputer_.statistics_, fill_value)

def test_iterative_imputer_min_max_value_remove_empty():
    """Check that we properly apply the empty feature mask to `min_value` and
    `max_value`.

    Non-regression test for https://github.com/scikit-learn/scikit-learn/issues/29355
    """
    # Intentionally make column 2 as a missing column, then the bound of the imputed
    # value of column 3 should be (4, 5)
        [
            [1, 2, np.nan, np.nan],
            [4, 5, np.nan, 6],
            [7, 8, np.nan, np.nan],
            [10, 11, np.nan, 12],
        ]
    )
    min_value = [-np.inf, -np.inf, -np.inf, 4]
    max_value = [np.inf, np.inf, np.inf, 5]

    X_imputed = IterativeImputer(
        min_value=min_value,
        max_value=max_value,

    X_without_missing_column = np.delete(X, 2, axis=1)
    assert X_imputed.shape == X_without_missing_column.shape
    assert np.min(X_imputed[np.isnan(X_without_missing_column)]) == pytest.approx(4)
    assert np.max(X_imputed[np.isnan(X_without_missing_column)]) == pytest.approx(5)

    # Intentionally make column 3 as a missing column, then the bound of the imputed
    # value of column 2 should be (3.5, 6)
        [
            [4, 5, 6, np.nan],
            [10, 11, 12, np.nan],
        ]
    )
    min_value = [-np.inf, -np.inf, 3.5, -np.inf]
    max_value = [np.inf, np.inf, 6, np.inf]

    X_without_missing_column = X[:, :3]
    assert np.min(X_imputed[np.isnan(X_without_missing_column)]) == pytest.approx(3.5)
    assert np.max(X_imputed[np.isnan(X_without_missing_column)]) == pytest.approx(6)

def test_knn_imputer_keep_empty_features(keep_empty_features):
    """Check the behaviour of `keep_empty_features` for `KNNImputer`."""

    imputer = KNNImputer(keep_empty_features=keep_empty_features)

            assert_array_equal(X_imputed[:, 1], 0)
        else:

def test_simple_impute_pd_na():

    # Impute pandas array of string types.
    df = pd.DataFrame({"feature": pd.Series(["abc", None, "de"], dtype="string")})
    imputer = SimpleImputer(missing_values=pd.NA, strategy="constant", fill_value="na")
    _assert_array_equal_and_same_dtype(
        imputer.fit_transform(df), np.array([["abc"], ["na"], ["de"]], dtype=object)
    )

    # Impute pandas array of string types without any missing values.
    df = pd.DataFrame({"feature": pd.Series(["abc", "de", "fgh"], dtype="string")})
    imputer = SimpleImputer(fill_value="ok", strategy="constant")
        imputer.fit_transform(df), np.array([["abc"], ["de"], ["fgh"]], dtype=object)
    )

    # Impute pandas array of integer types.
    df = pd.DataFrame({"feature": pd.Series([1, None, 3], dtype="Int64")})
    imputer = SimpleImputer(missing_values=pd.NA, strategy="constant", fill_value=-1)
    _assert_allclose_and_same_dtype(
        imputer.fit_transform(df), np.array([[1], [-1], [3]], dtype="float64")
    )

    # Use `np.nan` also works.
    imputer = SimpleImputer(missing_values=np.nan, strategy="constant", fill_value=-1)
    )

    # Impute pandas array of integer types with 'median' strategy.
    df = pd.DataFrame({"feature": pd.Series([1, None, 2, 3], dtype="Int64")})
    imputer = SimpleImputer(missing_values=pd.NA, strategy="median")
        imputer.fit_transform(df), np.array([[1], [2], [2], [3]], dtype="float64")
    )

    # Impute pandas array of integer types with 'mean' strategy.
    df = pd.DataFrame({"feature": pd.Series([1, None, 2], dtype="Int64")})
    imputer = SimpleImputer(missing_values=pd.NA, strategy="mean")
        imputer.fit_transform(df), np.array([[1], [1.5], [2]], dtype="float64")
    )

    # Impute pandas array of float types.
    df = pd.DataFrame({"feature": pd.Series([1.0, None, 3.0], dtype="float64")})
    imputer = SimpleImputer(missing_values=pd.NA, strategy="constant", fill_value=-2.0)
        imputer.fit_transform(df), np.array([[1.0], [-2.0], [3.0]], dtype="float64")
    )

    # Impute pandas array of float types with 'median' strategy.
    df = pd.DataFrame({"feature": pd.Series([1.0, None, 2.0, 3.0], dtype="float64")})
        imputer.fit_transform(df),
        np.array([[1.0], [2.0], [2.0], [3.0]], dtype="float64"),
    )

def test_missing_indicator_feature_names_out():
    """Check that missing indicator return the feature names with a prefix."""

        [
        ],
        columns=["a", "b", "c", "d"],
    )

    indicator = MissingIndicator(missing_values=missing_values).fit(X)
    feature_names = indicator.get_feature_names_out()
    expected_names = ["missingindicator_a", "missingindicator_b", "missingindicator_d"]
    assert_array_equal(expected_names, feature_names)

def test_imputer_lists_fit_transform():
    """Check transform uses object dtype when fitted on an object dtype.

    Non-regression test for #19572.
    """

    X = [["a", "b"], ["c", "b"], ["a", "a"]]
    imp_frequent = SimpleImputer(strategy="most_frequent").fit(X)
    X_trans = imp_frequent.transform([[np.nan, np.nan]])
    assert X_trans.dtype == object
    assert_array_equal(X_trans, [["a", "b"]])

def test_imputer_transform_preserves_numeric_dtype(dtype_test):
    """Check transform preserves numeric dtype independent of fit dtype."""
        [[1.2, 3.4, np.nan], [np.nan, 1.2, 1.3], [4.2, 2, 1]], dtype=np.float64
    )
    imp = SimpleImputer().fit(X)

    X_test = np.asarray([[np.nan, np.nan, np.nan]], dtype=dtype_test)
    X_trans = imp.transform(X_test)
    assert X_trans.dtype == dtype_test

def test_simple_imputer_constant_keep_empty_features(array_type, keep_empty_features):
    """Check the behaviour of `keep_empty_features` with `strategy='constant'.
    For backward compatibility, a column full of missing values will always be
    fill and never dropped.
    """
    X = np.array([[np.nan, 2], [np.nan, 3], [np.nan, 6]])
    fill_value = 10
        keep_empty_features=keep_empty_features,
    )

        # TODO(1.8): Remove the condition and still call getattr(imputer, method)(X)
        if method.startswith("fit") and not keep_empty_features:
            warn_msg = '`strategy="constant"`, empty features are not dropped. '
            with pytest.warns(FutureWarning, match=warn_msg):
                X_imputed = getattr(imputer, method)(X)
        else:
            X_imputed = getattr(imputer, method)(X)
        assert X_imputed.shape == X.shape
        constant_feature = (
            X_imputed[:, 0].toarray() if array_type == "sparse" else X_imputed[:, 0]
        )
        assert_array_equal(constant_feature, fill_value)

def test_simple_imputer_keep_empty_features(strategy, array_type, keep_empty_features):
    """Check the behaviour of `keep_empty_features` with all strategies but
    'constant'.
    """
    imputer = SimpleImputer(strategy=strategy, keep_empty_features=keep_empty_features)

            constant_feature = (
                X_imputed[:, 0].toarray() if array_type == "sparse" else X_imputed[:, 0]
            )
            assert_array_equal(constant_feature, 0)
        else:

def test_imputation_custom(csc_container):
        [
            [1.1, 1.1, 1.1],
            [3.9, 1.2, np.nan],
            [np.nan, 1.3, np.nan],
            [0.1, 1.4, 1.4],
            [4.9, 1.5, 1.5],
            [np.nan, 1.6, 1.6],
        ]
    )

        [
            [3.9, 1.2, 1.1],
            [0.1, 1.3, 1.1],
            [0.1, 1.6, 1.6],
        ]
    )

    imputer = SimpleImputer(missing_values=np.nan, strategy=np.min)

    X_trans = imputer.fit_transform(csc_container(X))
    assert_array_equal(X_trans.toarray(), X_true)

def test_simple_imputer_constant_fill_value_casting():
    """Check that we raise a proper error message when we cannot cast the fill value
    to the input data type. Otherwise, check that the casting is done properly.

    https://github.com/scikit-learn/scikit-learn/issues/28309
    """
    # cannot cast fill_value at fit
    fill_value = 1.5
    X_int64 = np.array([[1, 2, 3], [2, 3, 4]], dtype=np.int64)
        strategy="constant", fill_value=fill_value, missing_values=2
    )
        imputer.fit(X_int64)

    # cannot cast fill_value at transform
    X_float64 = np.array([[1, 2, 3], [2, 3, 4]], dtype=np.float64)
    imputer.fit(X_float64)
    err_msg = (
        f"The dtype of the filling value (i.e. {imputer.statistics_.dtype!r}) "
        "cannot be cast"
    )
        imputer.transform(X_int64)

    # check that no error is raised when having the same kind of dtype
    fill_value_list = [np.float64(1.5), 1.5, 1]
    X_float32 = X_float64.astype(np.float32)

    for fill_value in fill_value_list:
            strategy="constant", fill_value=fill_value, missing_values=2
        )
        X_trans = imputer.fit_transform(X_float32)
        assert X_trans.dtype == X_float32.dtype

def test_iterative_imputer_no_empty_features(strategy):
    """Check the behaviour of `keep_empty_features` with no empty features.

    With no-empty features, we should get the same imputation whatever the
    parameter `keep_empty_features`.

    https://github.com/scikit-learn/scikit-learn/issues/29375
    """
    X = np.array([[np.nan, 0, 1], [2, np.nan, 3], [4, 5, np.nan]])

    imputer_drop_empty_features = IterativeImputer(
        initial_strategy=strategy, fill_value=1, keep_empty_features=False
    )

    imputer_keep_empty_features = IterativeImputer(
        initial_strategy=strategy, fill_value=1, keep_empty_features=True
    )

        imputer_drop_empty_features.fit_transform(X),
        imputer_keep_empty_features.fit_transform(X),
    )

def test_iterative_imputer_with_empty_features(strategy, X_test):
    """Check the behaviour of `keep_empty_features` in the presence of empty features.

    With `keep_empty_features=True`, the empty feature will be imputed with the value
    defined by the initial imputation.

    """
        [[np.nan, np.nan, 0, 1], [np.nan, 2, np.nan, 3], [np.nan, 4, 5, np.nan]]
    )

        initial_strategy=strategy, fill_value=0, keep_empty_features=False
    )
    X_train_drop_empty_features = imputer_drop_empty_features.fit_transform(X_train)
    X_test_drop_empty_features = imputer_drop_empty_features.transform(X_test)

        initial_strategy=strategy, fill_value=0, keep_empty_features=True
    )
    X_train_keep_empty_features = imputer_keep_empty_features.fit_transform(X_train)
    X_test_keep_empty_features = imputer_keep_empty_features.transform(X_test)

    assert_allclose(X_train_drop_empty_features, X_train_keep_empty_features[:, 1:])
    assert_allclose(X_train_keep_empty_features[:, 0], 0)

    assert X_train_drop_empty_features.shape[1] == X_test_drop_empty_features.shape[1]
    assert X_train_keep_empty_features.shape[1] == X_test_keep_empty_features.shape[1]

def _update_doc_distribution(
    X,
    exp_topic_word_distr,
    doc_topic_prior,
    max_doc_update_iter,
    mean_change_tol,
    cal_sstats,
):
    """E-step: update document-topic distribution.

        Document word matrix.

    exp_topic_word_distr : ndarray of shape (n_topics, n_features)
        Exponential value of expectation of log topic word distribution.
        In the literature, this is `exp(E[log(beta)])`.

    doc_topic_prior : float
        Prior of document topic distribution `theta`.

    max_doc_update_iter : int
        Max number of iterations for updating document topic distribution in
        the E-step.

    mean_change_tol : float
        Stopping tolerance for updating document topic distribution in E-step.

    cal_sstats : bool
        Parameter that indicate to calculate sufficient statistics or not.
        Set `cal_sstats` to `True` when we need to run M-step.

    random_state : RandomState instance or None
        Parameter that indicate how to initialize document topic distribution.
        Set `random_state` to None will initialize document topic distribution
        to a constant number.

    Returns
    -------
    (doc_topic_distr, suff_stats) :
        `doc_topic_distr` is unnormalized topic distribution for each document.
        In the literature, this is `gamma`. we can calculate `E[log(theta)]`
        from it.
        `suff_stats` is expected sufficient statistics for the M-step.
            When `cal_sstats == False`, this will be None.

    """
    is_sparse_x = sp.issparse(X)
    n_topics = exp_topic_word_distr.shape[0]

    if random_state:
        doc_topic_distr = random_state.gamma(100.0, 0.01, (n_samples, n_topics)).astype(
            X.dtype, copy=False
        )
    else:
        doc_topic_distr = np.ones((n_samples, n_topics), dtype=X.dtype)

    # In the literature, this is `exp(E[log(theta)])`
    exp_doc_topic = np.exp(_dirichlet_expectation_2d(doc_topic_distr))

    # diff on `component_` (only calculate it when `cal_diff` is True)
    suff_stats = (
        np.zeros(exp_topic_word_distr.shape, dtype=X.dtype) if cal_sstats else None
    )

    if is_sparse_x:
        X_data = X.data
        X_indices = X.indices
        X_indptr = X.indptr

    # These cython functions are called in a nested loop on usually very small arrays
    # (length=n_topics). In that case, finding the appropriate signature of the
    # fused-typed function can be more costly than its execution, hence the dispatch
    # is done outside of the loop.
    ctype = "float" if X.dtype == np.float32 else "double"
    mean_change = cy_mean_change[ctype]
    dirichlet_expectation_1d = cy_dirichlet_expectation_1d[ctype]
    eps = np.finfo(X.dtype).eps

    for idx_d in range(n_samples):
        if is_sparse_x:
            ids = X_indices[X_indptr[idx_d] : X_indptr[idx_d + 1]]
            cnts = X_data[X_indptr[idx_d] : X_indptr[idx_d + 1]]
        else:
            ids = np.nonzero(X[idx_d, :])[0]
            cnts = X[idx_d, ids]

        doc_topic_d = doc_topic_distr[idx_d, :]
        # The next one is a copy, since the inner loop overwrites it.
        exp_doc_topic_d = exp_doc_topic[idx_d, :].copy()
        exp_topic_word_d = exp_topic_word_distr[:, ids]

        # Iterate between `doc_topic_d` and `norm_phi` until convergence
        for _ in range(0, max_doc_update_iter):
            last_d = doc_topic_d

            # The optimal phi_{dwk} is proportional to
            # exp(E[log(theta_{dk})]) * exp(E[log(beta_{dw})]).
            norm_phi = np.dot(exp_doc_topic_d, exp_topic_word_d) + eps

            doc_topic_d = exp_doc_topic_d * np.dot(cnts / norm_phi, exp_topic_word_d.T)
            # Note: adds doc_topic_prior to doc_topic_d, in-place.
            dirichlet_expectation_1d(doc_topic_d, doc_topic_prior, exp_doc_topic_d)

            if mean_change(last_d, doc_topic_d) < mean_change_tol:
                break
        doc_topic_distr[idx_d, :] = doc_topic_d

        # Contribution of document d to the expected sufficient
        # statistics for the M step.
        if cal_sstats:
            suff_stats[:, ids] += np.outer(exp_doc_topic_d, cnts / norm_phi)

    return (doc_topic_distr, suff_stats)

class LatentDirichletAllocation(
):
    """Latent Dirichlet Allocation with online variational Bayes algorithm.

    The implementation is based on [1]_ and [2]_.

    Read more in the :ref:`User Guide <LatentDirichletAllocation>`.

    n_components : int, default=10
        Number of topics.

            ``n_topics`` was renamed to ``n_components``

    doc_topic_prior : float, default=None
        Prior of document topic distribution `theta`. If the value is None,
        defaults to `1 / n_components`.
        In [1]_, this is called `alpha`.

    topic_word_prior : float, default=None
        Prior of topic word distribution `beta`. If the value is None, defaults
        to `1 / n_components`.
        In [1]_, this is called `eta`.

    learning_method : {'batch', 'online'}, default='batch'
        Method used to update `_component`. Only used in :meth:`fit` method.
        In general, if the data size is large, the online update will be much
        faster than the batch update.

        Valid options:

        - 'batch': Batch variational Bayes method. Use all training data in each EM
          update. Old `components_` will be overwritten in each iteration.
        - 'online': Online variational Bayes method. In each EM update, use mini-batch
          of training data to update the ``components_`` variable incrementally. The
          learning rate is controlled by the ``learning_decay`` and the
          ``learning_offset`` parameters.

            The default learning method is now ``"batch"``.

    learning_decay : float, default=0.7
        It is a parameter that control learning rate in the online learning
        method. The value should be set between (0.5, 1.0] to guarantee
        asymptotic convergence. When the value is 0.0 and batch_size is
        ``n_samples``, the update method is same as batch learning. In the
        literature, this is called kappa.

    learning_offset : float, default=10.0
        A (positive) parameter that downweights early iterations in online
        learning.  It should be greater than 1.0. In the literature, this is
        called tau_0.

        The maximum number of passes over the training data (aka epochs).
        It only impacts the behavior in the :meth:`fit` method, and not the
        :meth:`partial_fit` method.

    batch_size : int, default=128
        Number of documents to use in each EM iteration. Only used in online
        learning.

    evaluate_every : int, default=-1
        How often to evaluate perplexity. Only used in `fit` method.
        set it to 0 or negative number to not evaluate perplexity in
        training at all. Evaluating perplexity can help you check convergence
        in training process, but it will also increase total training time.
        Evaluating perplexity in every iteration might increase training time
        up to two-fold.

    total_samples : int, default=1e6
        Total number of documents. Only used in the :meth:`partial_fit` method.

    perp_tol : float, default=1e-1
        Perplexity tolerance. Only used when ``evaluate_every`` is greater than 0.

    mean_change_tol : float, default=1e-3

    max_doc_update_iter : int, default=100

        The number of jobs to use in the E-step.

        Variational parameters for topic word distribution. Since the complete
        conditional for topic word distribution is a Dirichlet,
        ``components_[i, j]`` can be viewed as pseudocount that represents the
        number of times word `j` was assigned to topic `i`.
        It can also be viewed as distribution over the words for each topic
        after normalization:
        ``model.components_ / model.components_.sum(axis=1)[:, np.newaxis]``.

    exp_dirichlet_component_ : ndarray of shape (n_components, n_features)

    n_batch_iter_ : int
        Number of iterations of the EM step.

        Number of passes over the dataset.

    bound_ : float
        Final perplexity score on training set.

    doc_topic_prior_ : float
        it is `1 / n_components`.

    topic_word_prior_ : float
        Prior of topic word distribution `beta`. If the value is None, it is
        `1 / n_components`.

    See Also
    --------
    sklearn.discriminant_analysis.LinearDiscriminantAnalysis:
        A classifier with a linear decision boundary, generated by fitting
        class conditional densities to the data and using Bayes' rule.

    .. [1] "Online Learning for Latent Dirichlet Allocation", Matthew D.
           Hoffman, David M. Blei, Francis Bach, 2010
           https://github.com/blei-lab/onlineldavb

    .. [2] "Stochastic Variational Inference", Matthew D. Hoffman,
           David M. Blei, Chong Wang, John Paisley, 2013

    Examples
    --------
    >>> from sklearn.decomposition import LatentDirichletAllocation
    >>> # This produces a feature matrix of token counts, similar to what
    >>> # CountVectorizer would produce on text.
    >>> X, _ = make_multilabel_classification(random_state=0)
    >>> lda = LatentDirichletAllocation(n_components=5,
    ...     random_state=0)
    >>> lda.fit(X)
    LatentDirichletAllocation(...)
    >>> # get topics for some given samples:
    >>> lda.transform(X[-2:])
    array([[0.00360392, 0.25499205, 0.0036211 , 0.64236448, 0.09541846],
           [0.15297572, 0.00362644, 0.44412786, 0.39568399, 0.003586  ]])
    """

        "n_components": [Interval(Integral, 0, None, closed="neither")],
        "doc_topic_prior": [None, Interval(Real, 0, 1, closed="both")],
        "topic_word_prior": [None, Interval(Real, 0, 1, closed="both")],
        "learning_method": [StrOptions({"batch", "online"})],
        "learning_decay": [Interval(Real, 0, 1, closed="both")],
        "learning_offset": [Interval(Real, 1.0, None, closed="left")],
        "batch_size": [Interval(Integral, 0, None, closed="neither")],
        "evaluate_every": [Interval(Integral, None, None, closed="neither")],
        "total_samples": [Interval(Real, 0, None, closed="neither")],
        "perp_tol": [Interval(Real, 0, None, closed="left")],
        "mean_change_tol": [Interval(Real, 0, None, closed="left")],
        "max_doc_update_iter": [Interval(Integral, 0, None, closed="left")],
    }

        self,
        n_components=10,
        *,
        doc_topic_prior=None,
        topic_word_prior=None,
        learning_method="batch",
        learning_decay=0.7,
        learning_offset=10.0,
        batch_size=128,
        evaluate_every=-1,
        total_samples=1e6,
        perp_tol=1e-1,
        mean_change_tol=1e-3,
        max_doc_update_iter=100,
    ):
        self.doc_topic_prior = doc_topic_prior
        self.topic_word_prior = topic_word_prior
        self.learning_method = learning_method
        self.learning_decay = learning_decay
        self.learning_offset = learning_offset
        self.evaluate_every = evaluate_every
        self.total_samples = total_samples
        self.perp_tol = perp_tol
        self.mean_change_tol = mean_change_tol
        self.max_doc_update_iter = max_doc_update_iter

    def _init_latent_vars(self, n_features, dtype=np.float64):
        """Initialize latent variables."""

        self.n_batch_iter_ = 1

        if self.doc_topic_prior is None:
            self.doc_topic_prior_ = 1.0 / self.n_components
        else:
            self.doc_topic_prior_ = self.doc_topic_prior

        if self.topic_word_prior is None:
            self.topic_word_prior_ = 1.0 / self.n_components
        else:
            self.topic_word_prior_ = self.topic_word_prior

        init_gamma = 100.0
        init_var = 1.0 / init_gamma
        # In the literature, this is called `lambda`
        self.components_ = self.random_state_.gamma(
            init_gamma, init_var, (self.n_components, n_features)
        ).astype(dtype, copy=False)

        # In the literature, this is `exp(E[log(beta)])`
        self.exp_dirichlet_component_ = np.exp(
            _dirichlet_expectation_2d(self.components_)
        )

    def _e_step(self, X, cal_sstats, random_init, parallel=None):
        """E-step in EM update.

            Document word matrix.

        cal_sstats : bool
            Parameter that indicate whether to calculate sufficient statistics
            or not. Set ``cal_sstats`` to True when we need to run M-step.

        random_init : bool
            Parameter that indicate whether to initialize document topic
            distribution randomly in the E-step. Set it to True in training
            steps.

        parallel : joblib.Parallel, default=None
            Pre-initialized instance of joblib.Parallel.

        Returns
        -------
        (doc_topic_distr, suff_stats) :
            `doc_topic_distr` is unnormalized topic distribution for each
            document. In the literature, this is called `gamma`.
            `suff_stats` is expected sufficient statistics for the M-step.
            When `cal_sstats == False`, it will be None.

        """

        # Run e-step in parallel
        random_state = self.random_state_ if random_init else None

        # TODO: make Parallel._effective_n_jobs public instead?
        if parallel is None:
            parallel = Parallel(n_jobs=n_jobs, verbose=max(0, self.verbose - 1))
        results = parallel(
            delayed(_update_doc_distribution)(
                X[idx_slice, :],
                self.exp_dirichlet_component_,
                self.doc_topic_prior_,
                self.max_doc_update_iter,
                self.mean_change_tol,
                cal_sstats,
            )
            for idx_slice in gen_even_slices(X.shape[0], n_jobs)
        )

        # merge result
        doc_topics, sstats_list = zip(*results)
        doc_topic_distr = np.vstack(doc_topics)

            # This step finishes computing the sufficient statistics for the
            # M-step.
            suff_stats = np.zeros(self.components_.shape, dtype=self.components_.dtype)
            for sstats in sstats_list:
                suff_stats += sstats
            suff_stats *= self.exp_dirichlet_component_
        else:
            suff_stats = None

        return (doc_topic_distr, suff_stats)

    def _em_step(self, X, total_samples, batch_update, parallel=None):
        """EM update for 1 iteration.

        update `component_` by batch VB or online VB.

        total_samples : int
            Total number of documents. It is only used when
            batch_update is `False`.

        batch_update : bool
            Parameter that controls updating method.
            `True` for batch learning, `False` for online learning.

            Pre-initialized instance of joblib.Parallel

        Returns
        -------
        doc_topic_distr : ndarray of shape (n_samples, n_components)
            Unnormalized document topic distribution.
        """

        # E-step
        _, suff_stats = self._e_step(
            X, cal_sstats=True, random_init=True, parallel=parallel
        )

        # M-step
        if batch_update:
            self.components_ = self.topic_word_prior_ + suff_stats
        else:
            # online update
            # In the literature, the weight is `rho`
            weight = np.power(
                self.learning_offset + self.n_batch_iter_, -self.learning_decay
            )
            doc_ratio = float(total_samples) / X.shape[0]
            self.components_ *= 1 - weight
            self.components_ += weight * (
                self.topic_word_prior_ + doc_ratio * suff_stats
            )

        # update `component_` related variables
        )
        self.n_batch_iter_ += 1
        return

        tags.transformer_tags.preserves_dtype = ["float32", "float64"]

    def _check_non_neg_array(self, X, reset_n_features, whom):
        """check X format

        check X format and make sure no negative value in X.

        X :  array-like or sparse matrix

        """
        dtype = [np.float64, np.float32] if reset_n_features else self.components_.dtype

            self,
            X,
            reset=reset_n_features,
        )
        check_non_negative(X, whom)

        return X

        """Online VB with Mini-Batch update.

        y : Ignored

        Returns
        -------
        self
            Partially fitted estimator.
        """
        first_time = not hasattr(self, "components_")

        X = self._check_non_neg_array(
            X, reset_n_features=first_time, whom="LatentDirichletAllocation.partial_fit"
        )
        batch_size = self.batch_size

        # initialize parameters or check
        if first_time:
            self._init_latent_vars(n_features, dtype=X.dtype)

        if n_features != self.components_.shape[1]:
                "The provided data has %d dimensions while "
                "the model was trained with feature size %d."
                % (n_features, self.components_.shape[1])
            )

        with Parallel(n_jobs=n_jobs, verbose=max(0, self.verbose - 1)) as parallel:
            for idx_slice in gen_batches(n_samples, batch_size):
                self._em_step(
                    X[idx_slice, :],
                    total_samples=self.total_samples,
                    batch_update=False,
                    parallel=parallel,
                )

        """Learn model for the data X with variational Bayes method.

        When `learning_method` is 'online', use mini-batch update.
        Otherwise, use batch update.

        y : Ignored

        Returns
        -------
        self
        """
            X, reset_n_features=True, whom="LatentDirichletAllocation.fit"
        )
        max_iter = self.max_iter
        evaluate_every = self.evaluate_every
        learning_method = self.learning_method

        # initialize parameters
        self._init_latent_vars(n_features, dtype=X.dtype)
        # change to perplexity later
        last_bound = None
            for i in range(max_iter):
                if learning_method == "online":
                    for idx_slice in gen_batches(n_samples, batch_size):
                        self._em_step(
                            X[idx_slice, :],
                            total_samples=n_samples,
                            batch_update=False,
                            parallel=parallel,
                        )
                else:
                    # batch update
                    self._em_step(
                        X, total_samples=n_samples, batch_update=True, parallel=parallel
                    )

                # check perplexity
                if evaluate_every > 0 and (i + 1) % evaluate_every == 0:
                    doc_topics_distr, _ = self._e_step(
                        X, cal_sstats=False, random_init=False, parallel=parallel
                    )
                    bound = self._perplexity_precomp_distr(
                        X, doc_topics_distr, sub_sampling=False
                    )
                        print(
                            "iteration: %d of max_iter: %d, perplexity: %.4f"
                            % (i + 1, max_iter, bound)
                        )

                    if last_bound and abs(last_bound - bound) < self.perp_tol:
                        break
                    last_bound = bound

                elif self.verbose:
                    print("iteration: %d of max_iter: %d" % (i + 1, max_iter))

        # calculate final perplexity value on train set
        doc_topics_distr, _ = self._e_step(
            X, cal_sstats=False, random_init=False, parallel=parallel
        )
        self.bound_ = self._perplexity_precomp_distr(
            X, doc_topics_distr, sub_sampling=False
        )

    def _unnormalized_transform(self, X):
        """Transform data X according to fitted model.

        Returns
        -------
            Document topic distribution for X.
        """
        doc_topic_distr, _ = self._e_step(X, cal_sstats=False, random_init=False)

        return doc_topic_distr

    def transform(self, X, *, normalize=True):
        """Transform data X according to the fitted model.

            `doc_topic_distr` is now normalized.

        normalize : bool, default=True
            Whether to normalize the document topic distribution.

        Returns
        -------
        """
            X, reset_n_features=False, whom="LatentDirichletAllocation.transform"
        )
        doc_topic_distr = self._unnormalized_transform(X)
        if normalize:
            doc_topic_distr /= doc_topic_distr.sum(axis=1)[:, np.newaxis]

    def fit_transform(self, X, y=None, *, normalize=True):
        """

        Fits transformer to `X` and `y` and returns a transformed version of `X`.

            Whether to normalize the document topic distribution in `transform`.

        Returns
        -------
        X_new : ndarray array of shape (n_samples, n_components)
        """
        return self.fit(X, y).transform(X, normalize=normalize)

    def _approx_bound(self, X, doc_topic_distr, sub_sampling):
        """Estimate the variational bound.

        Estimate the variational bound over "all documents" using only the
        documents passed in as X. Since log-likelihood of each word cannot
        be computed directly, we use this bound to estimate it.

            Document topic distribution. In the literature, this is called
            gamma.

        sub_sampling : bool, default=False
            Compensate for subsampling of documents.
            It is used in calculate bound in online learning.

        Returns
        -------

        """

        def _loglikelihood(prior, distr, dirichlet_distr, size):
            # calculate log-likelihood
            score = np.sum((prior - distr) * dirichlet_distr)
            score += np.sum(gammaln(distr) - gammaln(prior))
            score += np.sum(gammaln(prior * size) - gammaln(np.sum(distr, 1)))
            return score

        is_sparse_x = sp.issparse(X)
        n_samples, n_components = doc_topic_distr.shape
        n_features = self.components_.shape[1]
        score = 0

        dirichlet_doc_topic = _dirichlet_expectation_2d(doc_topic_distr)
        dirichlet_component_ = _dirichlet_expectation_2d(self.components_)
        doc_topic_prior = self.doc_topic_prior_
        topic_word_prior = self.topic_word_prior_

            X_data = X.data
            X_indices = X.indices
            X_indptr = X.indptr

        # E[log p(docs | theta, beta)]
        for idx_d in range(0, n_samples):
            if is_sparse_x:
                ids = X_indices[X_indptr[idx_d] : X_indptr[idx_d + 1]]
                cnts = X_data[X_indptr[idx_d] : X_indptr[idx_d + 1]]
            else:
                ids = np.nonzero(X[idx_d, :])[0]
                cnts = X[idx_d, ids]
            temp = (
                dirichlet_doc_topic[idx_d, :, np.newaxis] + dirichlet_component_[:, ids]
            )
            norm_phi = logsumexp(temp, axis=0)
            score += np.dot(cnts, norm_phi)

        # compute E[log p(theta | alpha) - log q(theta | gamma)]
        score += _loglikelihood(
            doc_topic_prior, doc_topic_distr, dirichlet_doc_topic, self.n_components
        )

        # Compensate for the subsampling of the population of documents
        if sub_sampling:
            doc_ratio = float(self.total_samples) / n_samples
            score *= doc_ratio

        # E[log p(beta | eta) - log q (beta | lambda)]
            topic_word_prior, self.components_, dirichlet_component_, n_features
        )

        """Calculate approximate log-likelihood as score.

        y : Ignored

        Returns
        -------
            Use approximate bound as score.
        """
            X, reset_n_features=False, whom="LatentDirichletAllocation.score"
        )

        score = self._approx_bound(X, doc_topic_distr, sub_sampling=False)

    def _perplexity_precomp_distr(self, X, doc_topic_distr=None, sub_sampling=False):
        """Calculate approximate perplexity for data X with ability to accept
        precomputed doc_topic_distr

        Perplexity is defined as exp(-1. * log-likelihood per word)

        doc_topic_distr : ndarray of shape (n_samples, n_components), \
            Document topic distribution.
            If it is None, it will be generated by applying transform on X.

        Returns
        -------
            Perplexity score.
        """
        if doc_topic_distr is None:
            doc_topic_distr = self._unnormalized_transform(X)
        else:
            n_samples, n_components = doc_topic_distr.shape
            if n_samples != X.shape[0]:
                    "Number of samples in X and doc_topic_distr do not match."
                )

            if n_components != self.n_components:
                raise ValueError("Number of topics does not match.")

        current_samples = X.shape[0]
        bound = self._approx_bound(X, doc_topic_distr, sub_sampling)

            word_cnt = X.sum() * (float(self.total_samples) / current_samples)
        else:
            word_cnt = X.sum()
        perword_bound = bound / word_cnt

        return np.exp(-1.0 * perword_bound)

    def perplexity(self, X, sub_sampling=False):
        """Calculate approximate perplexity for data X.

           *doc_topic_distr* argument has been deprecated and is ignored
           because user no longer has access to unnormalized distribution

        sub_sampling : bool
            Do sub-sampling or not.

        Returns
        -------
        """
            X, reset_n_features=True, whom="LatentDirichletAllocation.perplexity"
        )
        return self._perplexity_precomp_distr(X, sub_sampling=sub_sampling)

    @property

        self,
        *,
    ):

        else:

        else:

        )

            steps.

        Returns
        -------

        """

            )
        )

            # M-step.
        else:

        Returns
        -------
        """

        # E-step
        )

        # M-step
        else:
            )
            )

        )
        return

        """

            self,
            X,
        )

        return X

        y : Ignored

        Returns
        -------
        self
        """

        )

            )

                )

        y : Ignored

        Returns
        -------
        self
        """
        )

                        )
                else:
                    )

                    )
                    )
                        print(
                        )

                        break

        )
        )

        Returns
        -------
        """

        Returns
        -------
        """
        )

        """

        Returns
        -------
        """

            gamma.

        Returns
        -------

        """

        score = 0

            else:
            temp = (
            )

        )

        )

        y : Ignored

        Returns
        -------
        """
        )

        Returns
        -------
        """
        else:
                )

        else:

        Returns
        -------
        """
        )

class _BaseSparsePCA(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """Base class for SparsePCA and MiniBatchSparsePCA"""

        "n_components": [None, Interval(Integral, 1, None, closed="left")],
        "alpha": [Interval(Real, 0.0, None, closed="left")],
        "ridge_alpha": [Interval(Real, 0.0, None, closed="left")],
        "method": [StrOptions({"lars", "cd"})],
    }

        self,
        *,
        alpha=1,
        ridge_alpha=0.01,
        tol=1e-8,
        method="lars",
    ):
        self.ridge_alpha = ridge_alpha

        """Fit the model from data in X.

            Training vector, where `n_samples` is the number of samples

        y : Ignored

        Returns
        -------
        """

        self.mean_ = X.mean(axis=0)
        X = X - self.mean_

            n_components = X.shape[1]
        else:

        return self._fit(X, n_components, random_state)

        """Least Squares projection of the data onto the sparse components.

        To avoid instability issues in case the system is under-determined,
        regularization can be applied (Ridge regression) via the
        `ridge_alpha` parameter.

        Note that Sparse PCA components orthogonality is not enforced as in PCA
        hence one cannot use a simple linear projection.

            Test data to be transformed, must have the same number of
            features as the data used to train the model.

        Returns
        -------
        """

        U = ridge_regression(
            self.components_.T, X.T, self.ridge_alpha, solver="cholesky"
        )

        return U

        """Transform data from the latent space to the original space.

        This inversion is an approximation due to the loss of information
        induced by the forward decomposition.

        X : ndarray of shape (n_samples, n_components)
            Data in the latent space.

        Returns
        -------
            Reconstructed data in the original space.
        """
        X = check_array(X)

        return (X @ self.components_) + self.mean_

    @property

class SparsePCA(_BaseSparsePCA):
    """Sparse Principal Components Analysis (SparsePCA).

    Finds the set of sparse components that can optimally reconstruct
    the data.  The amount of sparseness is controllable by the coefficient
    of the L1 penalty, given by the parameter alpha.

    Read more in the :ref:`User Guide <SparsePCA>`.

        Number of sparse atoms to extract. If None, then ``n_components``
        is set to ``n_features``.

    alpha : float, default=1
        Sparsity controlling parameter. Higher values lead to sparser
        components.

    ridge_alpha : float, default=0.01
        Amount of ridge shrinkage to apply in order to improve
        conditioning when calling the transform method.

        Maximum number of iterations to perform.

    tol : float, default=1e-8
        Tolerance for the stopping condition.

    method : {'lars', 'cd'}, default='lars'
        Method to be used for optimization.
        lars: uses the least angle regression method to solve the lasso problem
        (linear_model.lars_path)
        cd: uses the coordinate descent method to compute the
        Lasso solution (linear_model.Lasso). Lars will be faster if
        the estimated components are sparse.

        Number of parallel jobs to run.

    U_init : ndarray of shape (n_samples, n_components), default=None
        Initial values for the loadings for warm restart scenarios. Only used
        if `U_init` and `V_init` are not None.

    V_init : ndarray of shape (n_components, n_features), default=None
        Initial values for the components for warm restart scenarios. Only used

        Controls the verbosity; the higher, the more messages. Defaults to 0.

        Used during dictionary learning. Pass an int for reproducible results
        across multiple function calls.

        Sparse components extracted from the data.

    error_ : ndarray
        Vector of errors at each iteration.

        Estimated number of components.

    mean_ : ndarray of shape (n_features,)
        Per-feature empirical mean, estimated from the training set.
        Equal to ``X.mean(axis=0)``.

    See Also
    --------
    PCA : Principal Component Analysis implementation.
    MiniBatchSparsePCA : Mini batch variant of `SparsePCA` that is faster but less
        accurate.
    DictionaryLearning : Generic dictionary learning problem using a sparse code.

    Examples
    --------
    >>> from sklearn.decomposition import SparsePCA
    >>> X, _ = make_friedman1(n_samples=200, n_features=30, random_state=0)
    >>> transformer = SparsePCA(n_components=5, random_state=0)
    >>> transformer.fit(X)
    SparsePCA(...)
    >>> X_transformed = transformer.transform(X)
    >>> X_transformed.shape
    (200, 5)
    >>> # most values in the components_ are zero (sparsity)
    >>> np.mean(transformer.components_ == 0)
    np.float64(0.9666...)
    """

        **_BaseSparsePCA._parameter_constraints,
        "U_init": [None, np.ndarray],
        "V_init": [None, np.ndarray],
    }

        self,
        *,
        alpha=1,
        tol=1e-8,
        U_init=None,
        V_init=None,
    ):
            ridge_alpha=ridge_alpha,
            tol=tol,
            method=method,
        )
        self.U_init = U_init
        self.V_init = V_init

    def _fit(self, X, n_components, random_state):
        """Specialized `fit` for SparsePCA."""

        code_init = self.V_init.T if self.V_init is not None else None
        dict_init = self.U_init.T if self.U_init is not None else None
        code, dictionary, E, self.n_iter_ = dict_learning(
            X.T,
            n_components,
            method=self.method,
            code_init=code_init,
            dict_init=dict_init,
            return_n_iter=True,
        )
        # flip eigenvectors' sign to enforce deterministic output
        code, dictionary = svd_flip(code, dictionary, u_based_decision=True)
        self.components_ = code.T
        components_norm = np.linalg.norm(self.components_, axis=1)[:, np.newaxis]
        components_norm[components_norm == 0] = 1
        self.components_ /= components_norm
        self.n_components_ = len(self.components_)

        self.error_ = E

class MiniBatchSparsePCA(_BaseSparsePCA):
    """Mini-batch Sparse Principal Components Analysis.

    For an example comparing sparse PCA to PCA, see
    :ref:`sphx_glr_auto_examples_decomposition_plot_faces_decomposition.py`

    alpha : int, default=1

    max_iter : int, default=1_000
        Maximum number of iterations over the complete dataset before
        stopping independently of any early stopping criterion heuristics.

        Callable that gets invoked every five iterations.

    batch_size : int, default=3
        The number of features to take in each mini batch.

        Whether to shuffle the data before splitting it in batches.

        Used for random shuffling when ``shuffle`` is set to ``True``,
        during online dictionary learning. Pass an int for reproducible results

        Control early stopping based on the norm of the differences in the
        dictionary between 2 steps.

        To disable early stopping based on changes in the dictionary, set
        `tol` to 0.0.

    max_no_improvement : int or None, default=10
        Control early stopping based on the consecutive number of mini batches
        that does not yield an improvement on the smoothed cost function.

        To disable convergence detection based on cost function, set
        `max_no_improvement` to `None`.

    See Also
    --------
    DictionaryLearning : Find a dictionary that sparsely encodes data.
    IncrementalPCA : Incremental principal components analysis.
    PCA : Principal component analysis.
    SparsePCA : Sparse Principal Components Analysis.
    TruncatedSVD : Dimensionality reduction using truncated SVD.

    Examples
    --------
    >>> from sklearn.decomposition import MiniBatchSparsePCA
    >>> transformer = MiniBatchSparsePCA(n_components=5, batch_size=50,
    ...                                  max_iter=10, random_state=0)
    MiniBatchSparsePCA(...)
    (200, 5)
    np.float64(0.9...)
    """

        "callback": [None, callable],
        "max_no_improvement": [Interval(Integral, 0, None, closed="left"), None],
    }

        self,
        *,
        alpha=1,
        max_iter=1_000,
        batch_size=3,
        tol=1e-3,
        max_no_improvement=10,
    ):
            tol=tol,
        )
        self.max_no_improvement = max_no_improvement

        """Specialized `fit` for MiniBatchSparsePCA."""

        transform_algorithm = "lasso_" + self.method
        est = MiniBatchDictionaryLearning(
            dict_init=None,
            batch_size=self.batch_size,
            shuffle=self.shuffle,
            fit_algorithm=self.method,
            transform_algorithm=transform_algorithm,
            transform_alpha=self.alpha,
            callback=self.callback,
            max_no_improvement=self.max_no_improvement,
        )
        est.set_output(transform="default")
        est.fit(X.T)

        self.components_, self.n_iter_ = est.transform(X.T).T, est.n_iter_

        self,
        *,
        alpha=1,
        tol=1e-8,
    ):

        y : Ignored

        Returns
        -------
        """

        else:

        Returns
        -------
        """

        )

        return U

        Returns
        -------
        """

        self,
        *,
        alpha=1,
        tol=1e-8,
    ):
            tol=tol,
        )

            X.T,
        )

        self,
        *,
        alpha=1,
        tol=1e-3,
    ):
            tol=tol,
        )

        )

def norm(x):
    """Dot product-based Euclidean norm implementation.

    See: http://fa.bianp.net/blog/2011/computing-the-vector-norm/

    x : array-like
        Vector for which to compute the norm.
    """
    return sqrt(squared_norm(x))

def trace_dot(X, Y):
    """Trace of np.dot(X, Y.T).

    X : array-like
        First matrix.
    Y : array-like
        Second matrix.
    """
    return np.dot(X.ravel(), Y.ravel())

def _check_init(A, shape, whom):
    A = check_array(A)
    if shape[0] != "auto" and A.shape[0] != shape[0]:
            f"Array with wrong first dimension passed to {whom}. Expected {shape[0]}, "
            f"but got {A.shape[0]}."
        )
    if shape[1] != "auto" and A.shape[1] != shape[1]:
            f"Array with wrong second dimension passed to {whom}. Expected {shape[1]}, "
            f"but got {A.shape[1]}."
        )
    check_non_negative(A, whom)
    if np.max(A) == 0:
        raise ValueError(f"Array passed to {whom} is full of zeros.")

def _beta_divergence(X, W, H, beta, square_root=False):
    """Compute the beta-divergence of X and dot(W, H).

    X : float or array-like of shape (n_samples, n_features)

    W : float or array-like of shape (n_samples, n_components)

    H : float or array-like of shape (n_components, n_features)

    beta : float or {'frobenius', 'kullback-leibler', 'itakura-saito'}
        Parameter of the beta-divergence.
        If beta == 2, this is half the Frobenius *squared* norm.
        If beta == 1, this is the generalized Kullback-Leibler divergence.
        If beta == 0, this is the Itakura-Saito divergence.
        Else, this is the general beta-divergence.

    square_root : bool, default=False
        If True, return np.sqrt(2 * res)
        For beta == 2, it corresponds to the Frobenius norm.

    Returns
    -------
        res : float
            Beta divergence of X and np.dot(X, H).
    """
    beta = _beta_loss_to_float(beta)

    # The method can be called with scalars
    if not sp.issparse(X):
    W = np.atleast_2d(W)
    H = np.atleast_2d(H)

    # Frobenius norm
    if beta == 2:
        # Avoid the creation of the dense np.dot(W, H) if X is sparse.
            norm_X = np.dot(X.data, X.data)
            norm_WH = trace_dot(np.linalg.multi_dot([W.T, W, H]), H)
            cross_prod = trace_dot((X @ H.T), W)
            res = (norm_X + norm_WH - 2.0 * cross_prod) / 2.0
        else:
            res = squared_norm(X - np.dot(W, H)) / 2.0

        if square_root:
            return np.sqrt(res * 2)
        else:
            return res

    if sp.issparse(X):
        # compute np.dot(W, H) only where X is nonzero
        WH_data = _special_sparse_dot(W, H, X).data
    else:
        WH = np.dot(W, H)
        WH_data = WH.ravel()
        X_data = X.ravel()

    # do not affect the zeros: here 0 ** (-1) = 0 and not infinity
    indices = X_data > EPSILON
    WH_data = WH_data[indices]
    X_data = X_data[indices]

    # used to avoid division by zero
    WH_data[WH_data < EPSILON] = EPSILON

    # generalized Kullback-Leibler divergence
    if beta == 1:
        # fast and memory efficient computation of np.sum(np.dot(W, H))
        sum_WH = np.dot(np.sum(W, axis=0), np.sum(H, axis=1))
        # computes np.sum(X * log(X / WH)) only where X is nonzero
        div = X_data / WH_data
        res = np.dot(X_data, np.log(div))
        # add full np.sum(np.dot(W, H)) - np.sum(X)
        res += sum_WH - X_data.sum()

    # Itakura-Saito divergence
        res = np.sum(div) - np.prod(X.shape) - np.sum(np.log(div))

    # beta-divergence, beta not in (0, 1, 2)
    else:
            # slow loop, but memory efficient computation of :
            # np.sum(np.dot(W, H) ** beta)
            sum_WH_beta = 0
                sum_WH_beta += np.sum(np.dot(W, H[:, i]) ** beta)

        else:
            sum_WH_beta = np.sum(WH**beta)

        sum_X_WH = np.dot(X_data, WH_data ** (beta - 1))
        res = (X_data**beta).sum() - beta * sum_X_WH
        res += sum_WH_beta * (beta - 1)
        res /= beta * (beta - 1)

    if square_root:
        res = max(res, 0)  # avoid negative number due to rounding errors
        return np.sqrt(2 * res)
    else:
        return res

def _special_sparse_dot(W, H, X):
    """Computes np.dot(W, H), only where X is non zero."""
        ii, jj = X.nonzero()
        n_vals = ii.shape[0]
        dot_vals = np.empty(n_vals)
        n_components = W.shape[1]

        batch_size = max(n_components, n_vals // n_components)
        for start in range(0, n_vals, batch_size):
            batch = slice(start, start + batch_size)
            dot_vals[batch] = np.multiply(W[ii[batch], :], H.T[jj[batch], :]).sum(
                axis=1
            )

        WH = sp.coo_matrix((dot_vals, (ii, jj)), shape=X.shape)
        return WH.tocsr()
    else:
        return np.dot(W, H)

def _beta_loss_to_float(beta_loss):
    """Convert string beta_loss to float."""
    beta_loss_map = {"frobenius": 2, "kullback-leibler": 1, "itakura-saito": 0}
    if isinstance(beta_loss, str):
        beta_loss = beta_loss_map[beta_loss]
    return beta_loss

def _initialize_nmf(X, n_components, init=None, eps=1e-6, random_state=None):
    """Algorithms for NMF initialization.

    Computes an initial guess for the non-negative
    rank k matrix approximation for X: X = WH.

        The data matrix to be decomposed.

        The number of components desired in the approximation.

    init :  {'random', 'nndsvd', 'nndsvda', 'nndsvdar'}, default=None
        Method used to initialize the procedure.

        - None: 'nndsvda' if n_components <= min(n_samples, n_features),
            otherwise 'random'.

        - 'random': non-negative random matrices, scaled with:
            sqrt(X.mean() / n_components)

        - 'nndsvd': Nonnegative Double Singular Value Decomposition (NNDSVD)
            initialization (better for sparseness)

        - 'nndsvda': NNDSVD with zeros filled with the average of X
            (better when sparsity is not desired)

        - 'nndsvdar': NNDSVD with zeros filled with small random values
            (generally faster, less accurate alternative to NNDSVDa
            for when sparsity is not desired)

        - 'custom': use custom matrices W and H

            When `init=None` and n_components is less than n_samples and n_features
            defaults to `nndsvda` instead of `nndsvd`.

    eps : float, default=1e-6
        Truncate all values less then this in output to zero.

        Used when ``init`` == 'nndsvdar' or 'random'. Pass an int for
        reproducible results across multiple function calls.

    Returns
    -------
    W : array-like of shape (n_samples, n_components)
        Initial guesses for solving X ~= WH.

    H : array-like of shape (n_components, n_features)

    C. Boutsidis, E. Gallopoulos: SVD based initialization: A head start for
    nonnegative matrix factorization - Pattern Recognition, 2008
    http://tinyurl.com/nndsvd
    """
    check_non_negative(X, "NMF initialization")

    if (
        init is not None
        and init != "random"
        and n_components > min(n_samples, n_features)
    ):
            "init = '{}' can only be used when "
            "n_components <= min(n_samples, n_features)".format(init)
        )

    if init is None:
        if n_components <= min(n_samples, n_features):
            init = "nndsvda"
        else:
            init = "random"

    # Random initialization
    if init == "random":
        avg = np.sqrt(X.mean() / n_components)
        H = avg * rng.standard_normal(size=(n_components, n_features)).astype(
        )
        W = avg * rng.standard_normal(size=(n_samples, n_components)).astype(
        )
        np.abs(H, out=H)
        np.abs(W, out=W)
        return W, H

    # NNDSVD initialization
    U, S, V = _randomized_svd(X, n_components, random_state=random_state)
    W = np.zeros_like(U)
    H = np.zeros_like(V)

    # The leading singular triplet is non-negative
    # so it can be used as is for initialization.
    W[:, 0] = np.sqrt(S[0]) * np.abs(U[:, 0])
    H[0, :] = np.sqrt(S[0]) * np.abs(V[0, :])

    for j in range(1, n_components):
        x, y = U[:, j], V[j, :]

        # extract positive and negative parts of column vectors
        x_p, y_p = np.maximum(x, 0), np.maximum(y, 0)
        x_n, y_n = np.abs(np.minimum(x, 0)), np.abs(np.minimum(y, 0))

        # and their norms
        x_p_nrm, y_p_nrm = norm(x_p), norm(y_p)
        x_n_nrm, y_n_nrm = norm(x_n), norm(y_n)

        m_p, m_n = x_p_nrm * y_p_nrm, x_n_nrm * y_n_nrm

        # choose update
        if m_p > m_n:
            u = x_p / x_p_nrm
            v = y_p / y_p_nrm
            sigma = m_p
        else:
            u = x_n / x_n_nrm
            v = y_n / y_n_nrm
            sigma = m_n

        lbd = np.sqrt(S[j] * sigma)
        W[:, j] = lbd * u
        H[j, :] = lbd * v

    W[W < eps] = 0
    H[H < eps] = 0

    if init == "nndsvd":
        pass
    elif init == "nndsvda":
        avg = X.mean()
        W[W == 0] = avg
        H[H == 0] = avg
    elif init == "nndsvdar":
        W[W == 0] = abs(avg * rng.standard_normal(size=len(W[W == 0])) / 100)
        H[H == 0] = abs(avg * rng.standard_normal(size=len(H[H == 0])) / 100)
    else:
            "Invalid init parameter: got %r instead of one of %r"
            % (init, (None, "random", "nndsvd", "nndsvda", "nndsvdar"))
        )

    return W, H

def _update_coordinate_descent(X, W, Ht, l1_reg, l2_reg, shuffle, random_state):
    """Helper function for _fit_coordinate_descent.

    Update W to minimize the objective function, iterating once over all
    coordinates. By symmetry, to update H, one can call
    _update_coordinate_descent(X.T, Ht, W, ...).

    """
    n_components = Ht.shape[1]

    HHt = np.dot(Ht.T, Ht)
    XHt = safe_sparse_dot(X, Ht)

    # L2 regularization corresponds to increase of the diagonal of HHt
    if l2_reg != 0.0:
        # adds l2_reg only on the diagonal
        HHt.flat[:: n_components + 1] += l2_reg
    # L1 regularization corresponds to decrease of each element of XHt
    if l1_reg != 0.0:
        XHt -= l1_reg

        permutation = random_state.permutation(n_components)
    else:
        permutation = np.arange(n_components)
    # The following seems to be required on 64-bit Windows w/ Python 3.5.
    permutation = np.asarray(permutation, dtype=np.intp)
    return _update_cdnmf_fast(W, HHt, XHt, permutation)

def _fit_coordinate_descent(
    X,
    W,
    H,
    tol=1e-4,
    max_iter=200,
    l1_reg_W=0,
    l1_reg_H=0,
    l2_reg_W=0,
    l2_reg_H=0,
    update_H=True,
):
    """Compute Non-negative Matrix Factorization (NMF) with Coordinate Descent

    The objective function is minimized with an alternating minimization of W
    and H. Each minimization is done with a cyclic (up to a permutation of the
    features) Coordinate Descent.

        Constant matrix.

        Initial guess for the solution.

        Maximum number of iterations before timing out.

    l1_reg_W : float, default=0.
        L1 regularization parameter for W.

    l1_reg_H : float, default=0.
        L1 regularization parameter for H.

    l2_reg_W : float, default=0.
        L2 regularization parameter for W.

    l2_reg_H : float, default=0.
        L2 regularization parameter for H.

    update_H : bool, default=True
        Set to True, both W and H will be estimated from initial guesses.
        Set to False, only W will be estimated.

        The verbosity level.

        If true, randomize the order of coordinates in the CD solver.

        Used to randomize the coordinates in the CD solver, when
        ``shuffle`` is set to ``True``. Pass an int for reproducible
        results across multiple function calls.

    Returns
    -------
    W : ndarray of shape (n_samples, n_components)
        Solution to the non-negative least squares problem.

    H : ndarray of shape (n_components, n_features)

        The number of iterations done by the algorithm.

    .. [1] :doi:`"Fast local algorithms for large scale nonnegative matrix and tensor
       factorizations" <10.1587/transfun.E92.A.708>`
       Cichocki, Andrzej, and P. H. A. N. Anh-Huy. IEICE transactions on fundamentals
       of electronics, communications and computer sciences 92.3: 708-721, 2009.
    """
    # so W and Ht are both in C order in memory
    Ht = check_array(H.T, order="C")
    X = check_array(X, accept_sparse="csr")

    for n_iter in range(1, max_iter + 1):
        violation = 0.0

        # Update W
        violation += _update_coordinate_descent(
            X, W, Ht, l1_reg_W, l2_reg_W, shuffle, rng
        )
        # Update H
        if update_H:
            violation += _update_coordinate_descent(
                X.T, Ht, W, l1_reg_H, l2_reg_H, shuffle, rng
            )

        if n_iter == 1:
            violation_init = violation

        if violation_init == 0:
            break

            print("violation:", violation / violation_init)

        if violation / violation_init <= tol:
                print("Converged at iteration", n_iter + 1)
            break

    return W, Ht.T, n_iter

def _multiplicative_update_w(
    X,
    W,
    H,
    beta_loss,
    l1_reg_W,
    l2_reg_W,
    gamma,
    H_sum=None,
    HHt=None,
    XHt=None,
):
    """Update W in Multiplicative Update NMF."""
    if beta_loss == 2:
        # Numerator
        if XHt is None:
            XHt = safe_sparse_dot(X, H.T)
            # avoid a copy of XHt, which will be re-computed (update_H=True)
            numerator = XHt
        else:
            # preserve the XHt, which is not re-computed (update_H=False)
            numerator = XHt.copy()

        # Denominator
        if HHt is None:
            HHt = np.dot(H, H.T)
        denominator = np.dot(W, HHt)

    else:
        # if X is sparse, compute WH only where X is non zero
        WH_safe_X = _special_sparse_dot(W, H, X)
            WH_safe_X_data = WH_safe_X.data
        else:
            WH_safe_X_data = WH_safe_X
            X_data = X
            # copy used in the Denominator
            WH = WH_safe_X.copy()
            if beta_loss - 1.0 < 0:
                WH[WH < EPSILON] = EPSILON

        # to avoid taking a negative power of zero
        if beta_loss - 2.0 < 0:
            WH_safe_X_data[WH_safe_X_data < EPSILON] = EPSILON

        if beta_loss == 1:
            np.divide(X_data, WH_safe_X_data, out=WH_safe_X_data)
        elif beta_loss == 0:
            # speeds up computation time
            # refer to /numpy/numpy/issues/9363
            WH_safe_X_data **= -1
            WH_safe_X_data **= 2
            # element-wise multiplication
            WH_safe_X_data *= X_data
        else:
            WH_safe_X_data **= beta_loss - 2

        # here numerator = dot(X * (dot(W, H) ** (beta_loss - 2)), H.T)
        numerator = safe_sparse_dot(WH_safe_X, H.T)

            if H_sum is None:
                H_sum = np.sum(H, axis=1)  # shape(n_components, )
            denominator = H_sum[np.newaxis, :]

        else:
            # computation of WHHt = dot(dot(W, H) ** beta_loss - 1, H.T)
                # memory efficient computation
                # (compute row by row, avoiding the dense matrix WH)
                WHHt = np.empty(W.shape)
                for i in range(X.shape[0]):
                    WHi = np.dot(W[i, :], H)
                    if beta_loss - 1 < 0:
                        WHi[WHi < EPSILON] = EPSILON
                    WHi **= beta_loss - 1
                    WHHt[i, :] = np.dot(WHi, H.T)
            else:
                WH **= beta_loss - 1
                WHHt = np.dot(WH, H.T)
            denominator = WHHt

    # Add L1 and L2 regularization
    if l1_reg_W > 0:
        denominator += l1_reg_W
    if l2_reg_W > 0:
        denominator = denominator + l2_reg_W * W
    denominator[denominator == 0] = EPSILON

    numerator /= denominator
    delta_W = numerator

    # gamma is in ]0, 1]
    if gamma != 1:
        delta_W **= gamma

    W *= delta_W

    return W, H_sum, HHt, XHt

def _multiplicative_update_h(
    X, W, H, beta_loss, l1_reg_H, l2_reg_H, gamma, A=None, B=None, rho=None
):
    """update H in Multiplicative Update NMF."""
        numerator = safe_sparse_dot(W.T, X)
        denominator = np.linalg.multi_dot([W.T, W, H])

    else:
        else:
            X_data = X

        # to avoid division by zero

        else:

        # here numerator = dot(W.T, (dot(W, H) ** (beta_loss - 2)) * X)
        numerator = safe_sparse_dot(W.T, WH_safe_X)

            W_sum = np.sum(W, axis=0)  # shape(n_components, )
            W_sum[W_sum == 0] = 1.0
            denominator = W_sum[:, np.newaxis]

        # beta_loss not in (1, 2)
        else:
            # computation of WtWH = dot(W.T, dot(W, H) ** beta_loss - 1)
                # (compute column by column, avoiding the dense matrix WH)
                WtWH = np.empty(H.shape)
                for i in range(X.shape[1]):
                    WHi = np.dot(W, H[:, i])
                    WtWH[:, i] = np.dot(W.T, WHi)
            else:
                WtWH = np.dot(W.T, WH)
            denominator = WtWH

    if l1_reg_H > 0:
        denominator += l1_reg_H
    if l2_reg_H > 0:
        denominator = denominator + l2_reg_H * H

    if A is not None and B is not None:
        # Updates for the online nmf
        if gamma != 1:
            H **= 1 / gamma
        numerator *= H
        A *= rho
        B *= rho
        A += numerator
        B += denominator
        H = A / B

            H **= gamma
    else:
        delta_H = numerator
        delta_H /= denominator
            delta_H **= gamma
        H *= delta_H

    return H

def _fit_multiplicative_update(
    X,
    W,
    H,
    beta_loss="frobenius",
    tol=1e-4,
):
    """Compute Non-negative Matrix Factorization with Multiplicative Update.

    The objective function is _beta_divergence(X, WH) and is minimized with an
    alternating minimization of W and H. Each minimization is done with a
    Multiplicative Update.

        Constant input matrix.

    beta_loss : float or {'frobenius', 'kullback-leibler', \
            'itakura-saito'}, default='frobenius'
        String must be in {'frobenius', 'kullback-leibler', 'itakura-saito'}.
        Beta divergence to be minimized, measuring the distance between X
        and the dot product WH. Note that values different from 'frobenius'
        (or 2) and 'kullback-leibler' (or 1) lead to significantly slower
        fits. Note that for beta_loss <= 0 (or 'itakura-saito'), the input
        matrix X cannot contain zeros.

        Number of iterations.

    Returns
    -------

    Lee, D. D., & Seung, H., S. (2001). Algorithms for Non-negative Matrix
    Factorization. Adv. Neural Inform. Process. Syst.. 13.
    Fevotte, C., & Idier, J. (2011). Algorithms for nonnegative matrix
    factorization with the beta-divergence. Neural Computation, 23(9).
    """
    start_time = time.time()

    beta_loss = _beta_loss_to_float(beta_loss)

    # gamma for Maximization-Minimization (MM) algorithm [Fevotte 2011]
    if beta_loss < 1:
        gamma = 1.0 / (2.0 - beta_loss)
    elif beta_loss > 2:
        gamma = 1.0 / (beta_loss - 1.0)
    else:
        gamma = 1.0

    # used for the convergence criterion
    error_at_init = _beta_divergence(X, W, H, beta_loss, square_root=True)
    previous_error = error_at_init

    H_sum, HHt, XHt = None, None, None
        # update W
        # H_sum, HHt and XHt are saved and reused if not update_H
        W, H_sum, HHt, XHt = _multiplicative_update_w(
            X,
            W,
            H,
            beta_loss=beta_loss,
            l1_reg_W=l1_reg_W,
            l2_reg_W=l2_reg_W,
            H_sum=H_sum,
            HHt=HHt,
            XHt=XHt,
            update_H=update_H,
        )

        # necessary for stability with beta_loss < 1
        if beta_loss < 1:
            W[W < np.finfo(np.float64).eps] = 0.0

        # update H (only at fit or fit_transform)
            H = _multiplicative_update_h(
                X,
                W,
                H,
                beta_loss=beta_loss,
                l1_reg_H=l1_reg_H,
                l2_reg_H=l2_reg_H,
                gamma=gamma,
            )

            # These values will be recomputed since H changed
            H_sum, HHt, XHt = None, None, None

            # necessary for stability with beta_loss < 1
            if beta_loss <= 1:
                H[H < np.finfo(np.float64).eps] = 0.0

        # test convergence criterion every 10 iterations
        if tol > 0 and n_iter % 10 == 0:
            error = _beta_divergence(X, W, H, beta_loss, square_root=True)

                iter_time = time.time()
                print(
                    "Epoch %02d reached after %.3f seconds, error: %f"
                    % (n_iter, iter_time - start_time, error)
                )

            if (previous_error - error) / error_at_init < tol:
                break
            previous_error = error

    # do not print if we have already printed in the convergence test
    if verbose and (tol == 0 or n_iter % 10 != 0):
        end_time = time.time()
        print(
            "Epoch %02d reached after %.3f seconds." % (n_iter, end_time - start_time)
        )

    return W, H, n_iter

def non_negative_factorization(
    X,
    W=None,
    H=None,
    n_components="auto",
    *,
    init=None,
    solver="cd",
    tol=1e-4,
    alpha_W=0.0,
    alpha_H="same",
    l1_ratio=0.0,
):
    """Compute Non-negative Matrix Factorization (NMF).

    Find two non-negative matrices (W, H) whose product approximates the non-
    negative matrix X. This factorization can be used for example for
    dimensionality reduction, source separation or topic extraction.

    The objective function is:

    .. math::

        L(W, H) &= 0.5 * ||X - WH||_{loss}^2

                &+ alpha\\_W * l1\\_ratio * n\\_features * ||vec(W)||_1

                &+ alpha\\_H * l1\\_ratio * n\\_samples * ||vec(H)||_1

                &+ 0.5 * alpha\\_W * (1 - l1\\_ratio) * n\\_features * ||W||_{Fro}^2

                &+ 0.5 * alpha\\_H * (1 - l1\\_ratio) * n\\_samples * ||H||_{Fro}^2,

    where :math:`||A||_{Fro}^2 = \\sum_{i,j} A_{ij}^2` (Frobenius norm) and
    :math:`||vec(A)||_1 = \\sum_{i,j} abs(A_{ij})` (Elementwise L1 norm)

    The generic norm :math:`||X - WH||_{loss}^2` may represent
    the Frobenius norm or another supported beta-divergence loss.
    The choice between options is controlled by the `beta_loss` parameter.

    The regularization terms are scaled by `n_features` for `W` and by `n_samples` for
    `H` to keep their impact balanced with respect to one another and to the data fit
    term as independent as possible of the size `n_samples` of the training set.

    and H. If H is given and update_H=False, it solves for W only.

    Note that the transformed data is named W and the components matrix is named H. In
    the NMF literature, the naming convention is usually the opposite since the data
    matrix X is transposed.

    W : array-like of shape (n_samples, n_components), default=None
        If `init='custom'`, it is used as initial guess for the solution.
        If `update_H=False`, it is initialised as an array of zeros, unless
        `solver='mu'`, then it is filled with values calculated by
        `np.sqrt(X.mean() / self._n_components)`.
        If `None`, uses the initialisation method specified in `init`.

    H : array-like of shape (n_components, n_features), default=None
        If `update_H=False`, it is used as a constant, to solve for W only.

    n_components : int or {'auto'} or None, default='auto'
        Number of components. If `None`, all features are kept.
        If `n_components='auto'`, the number of components is automatically inferred
        from `W` or `H` shapes.

            Added `'auto'` value.

            Default value changed from `None` to `'auto'`.

    init : {'random', 'nndsvd', 'nndsvda', 'nndsvdar', 'custom'}, default=None

        - None: 'nndsvda' if n_components < n_features, otherwise 'random'.
          `sqrt(X.mean() / n_components)`
          initialization (better for sparseness)
          (better when sparsity is not desired)
          (generally faster, less accurate alternative to NNDSVDa
          for when sparsity is not desired)
        - 'custom': If `update_H=True`, use custom matrices W and H which must both
          be provided. If `update_H=False`, then only custom matrix H is used.

            The default value of `init` changed from 'random' to None in 0.23.

    solver : {'cd', 'mu'}, default='cd'
        Numerical solver to use:

        - 'cd' is a Coordinate Descent solver that uses Fast Hierarchical
          Alternating Least Squares (Fast HALS).
        - 'mu' is a Multiplicative Update solver.

           Coordinate Descent solver.

           Multiplicative Update solver.

        matrix X cannot contain zeros. Used only in 'mu' solver.

    alpha_W : float, default=0.0
        Constant that multiplies the regularization terms of `W`. Set it to zero
        (default) to have no regularization on `W`.

    alpha_H : float or "same", default="same"
        Constant that multiplies the regularization terms of `H`. Set it to zero to
        have no regularization on `H`. If "same" (default), it takes the same value as
        `alpha_W`.

    l1_ratio : float, default=0.0
        The regularization mixing parameter, with 0 <= l1_ratio <= 1.
        For l1_ratio = 0 the penalty is an elementwise L2 penalty
        (aka Frobenius Norm).
        For l1_ratio = 1 it is an elementwise L1 penalty.
        For 0 < l1_ratio < 1, the penalty is a combination of L1 and L2.

        Used for NMF initialisation (when ``init`` == 'nndsvdar' or
        'random'), and in Coordinate Descent. Pass an int for reproducible

    Returns
    -------

        Actual number of iterations.

    .. [2] :doi:`"Algorithms for nonnegative matrix factorization with the
       beta-divergence" <10.1162/NECO_a_00168>`
       Fevotte, C., & Idier, J. (2011). Neural Computation, 23(9).

    Examples
    --------
    >>> X = np.array([[1,1], [2, 1], [3, 1.2], [4, 1], [5, 0.8], [6, 1]])
    >>> from sklearn.decomposition import non_negative_factorization
    >>> W, H, n_iter = non_negative_factorization(
    ...     X, n_components=2, init='random', random_state=0)
    """
    est = NMF(
        n_components=n_components,
        init=init,
        beta_loss=beta_loss,
        tol=tol,
        alpha_W=alpha_W,
        alpha_H=alpha_H,
        l1_ratio=l1_ratio,
        shuffle=shuffle,
    )
    est._validate_params()

    X = check_array(X, accept_sparse=("csr", "csc"), dtype=[np.float64, np.float32])

    with config_context(assume_finite=True):
        W, H, n_iter = est._fit_transform(X, W=W, H=H, update_H=update_H)

class _BaseNMF(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator, ABC):
    """Base class for NMF and MiniBatchNMF."""

            None,
        ],
        "init": [
            StrOptions({"random", "nndsvd", "nndsvda", "nndsvdar", "custom"}),
            None,
        ],
        "beta_loss": [
            StrOptions({"frobenius", "kullback-leibler", "itakura-saito"}),
            Real,
        ],
        "alpha_W": [Interval(Real, 0, None, closed="left")],
        "alpha_H": [Interval(Real, 0, None, closed="left"), StrOptions({"same"})],
        "l1_ratio": [Interval(Real, 0, 1, closed="both")],
    }

        self,
        *,
        beta_loss="frobenius",
        tol=1e-4,
        alpha_W=0.0,
        alpha_H="same",
        l1_ratio=0.0,
    ):
        self.beta_loss = beta_loss
        self.alpha_W = alpha_W
        self.alpha_H = alpha_H
        self.l1_ratio = l1_ratio

    def _check_params(self, X):
        # n_components
        self._n_components = self.n_components
        if self._n_components is None:
            self._n_components = X.shape[1]

        # beta_loss
        self._beta_loss = _beta_loss_to_float(self.beta_loss)

    def _check_w_h(self, X, W, H, update_H):
        """Check W and H, or initialize them."""

        if self.init == "custom" and update_H:
            _check_init(H, (self._n_components, n_features), "NMF (input H)")
            _check_init(W, (n_samples, self._n_components), "NMF (input W)")
            if self._n_components == "auto":
                self._n_components = H.shape[0]

            if H.dtype != X.dtype or W.dtype != X.dtype:
                    "H and W should have the same dtype as X. Got "
                    "H.dtype = {} and W.dtype = {}.".format(H.dtype, W.dtype)
                )

        elif not update_H:
            if W is not None:
                    "When update_H=False, the provided initial W is not used.",
                    RuntimeWarning,
                )

            if H.dtype != X.dtype:
                    "H should have the same dtype as X. Got H.dtype = {}.".format(
                        H.dtype
                    )
                )

            # 'mu' solver should not be initialized by zeros
            if self.solver == "mu":
                avg = np.sqrt(X.mean() / self._n_components)
                W = np.full((n_samples, self._n_components), avg, dtype=X.dtype)
            else:
                W = np.zeros((n_samples, self._n_components), dtype=X.dtype)

        else:
            if W is not None or H is not None:
                    (
                        "When init!='custom', provided W or H are ignored. Set "
                        " init='custom' to use them as initialization."
                    ),
                )

                self._n_components = X.shape[1]

            W, H = _initialize_nmf(
                X, self._n_components, init=self.init, random_state=self.random_state
            )

        return W, H

    def _compute_regularization(self, X):
        """Compute scaled regularization terms."""
        alpha_W = self.alpha_W
        alpha_H = self.alpha_W if self.alpha_H == "same" else self.alpha_H

        l1_reg_W = n_features * alpha_W * self.l1_ratio
        l1_reg_H = n_samples * alpha_H * self.l1_ratio
        l2_reg_W = n_features * alpha_W * (1.0 - self.l1_ratio)
        l2_reg_H = n_samples * alpha_H * (1.0 - self.l1_ratio)

        return l1_reg_W, l1_reg_H, l2_reg_W, l2_reg_H

        """Learn a NMF model for the data X.

        y : Ignored

        **params : kwargs
            Parameters (keyword arguments) and values passed to
            the fit_transform instance.

        Returns
        -------
        """
        # param validation is done in fit_transform

        self.fit_transform(X, **params)

        """Transform data back to its original space.

        X : {ndarray, sparse matrix} of shape (n_samples, n_components)
            Transformed data matrix.

        Returns
        -------
            Returns a data matrix of the original shape.
        """

        return X @ self.components_

    @property

class NMF(_BaseNMF):
    """Non-Negative Matrix Factorization (NMF).

    Find two non-negative matrices, i.e. matrices with all non-negative elements, (W, H)
    whose product approximates the non-negative matrix X. This factorization can be used
    for example for dimensionality reduction, source separation or topic extraction.

    .. math::

    :math:`||vec(A)||_1 = \\sum_{i,j} abs(A_{ij})` (Elementwise L1 norm).

    The generic norm :math:`||X - WH||_{loss}` may represent

    and H.

    Read more in the :ref:`User Guide <NMF>`.

        from W or H shapes.

        - `None`: 'nndsvda' if n_components <= min(n_samples, n_features),
          otherwise random.

        - `'random'`: non-negative random matrices, scaled with:

        - `'nndsvd'`: Nonnegative Double Singular Value Decomposition (NNDSVD)

        - `'nndsvda'`: NNDSVD with zeros filled with the average of X

        - `'nndsvdar'` NNDSVD with zeros filled with small random values

        - `'custom'`: Use custom matrices `W` and `H` which must both be provided.

        - 'cd' is a Coordinate Descent solver.

        Used for initialisation (when ``init`` == 'nndsvdar' or

           Regularization parameter *l1_ratio* used in the Coordinate Descent
           solver.

        Whether to be verbose.

           *shuffle* parameter used in the Coordinate Descent solver.

        Factorization matrix, sometimes called 'dictionary'.

        The number of components. It is same as the `n_components` parameter
        if it was given. Otherwise, it will be same as the number of
        features.

    reconstruction_err_ : float
        Frobenius norm of the matrix difference, or beta-divergence, between
        the training data ``X`` and the reconstructed data ``WH`` from
        the fitted model.

    See Also
    --------
    MiniBatchSparsePCA : Mini-batch Sparse Principal Components Analysis.
    SparseCoder : Find a sparse representation of data from a fixed,
        precomputed dictionary.

    Examples
    --------
    >>> X = np.array([[1, 1], [2, 1], [3, 1.2], [4, 1], [5, 0.8], [6, 1]])
    >>> from sklearn.decomposition import NMF
    >>> model = NMF(n_components=2, init='random', random_state=0)
    >>> W = model.fit_transform(X)
    >>> H = model.components_
    """

        **_BaseNMF._parameter_constraints,
        "solver": [StrOptions({"mu", "cd"})],
    }

        self,
        *,
        solver="cd",
        tol=1e-4,
    ):
            tol=tol,
            alpha_W=alpha_W,
            alpha_H=alpha_H,
            l1_ratio=l1_ratio,
        )

        super()._check_params(X)

        # solver
        if self.solver != "mu" and self.beta_loss not in (2, "frobenius"):
            # 'mu' is the only solver that handles other beta losses than 'frobenius'
                f"Invalid beta_loss parameter: solver {self.solver!r} does not handle "
                f"beta_loss = {self.beta_loss!r}"
            )
        if self.solver == "mu" and self.init == "nndsvd":
                (
                    "The multiplicative update ('mu') solver cannot update "
                    "zeros present in the initialization, and so leads to "
                    "poorer results when used jointly with init='nndsvd'. "
                    "You may try init='nndsvda' or init='nndsvdar' instead."
                ),
            )

    def fit_transform(self, X, y=None, W=None, H=None):
        """Learn a NMF model for the data X and returns the transformed data.

        This is more efficient than calling fit followed by transform.

        y : Ignored

        W : array-like of shape (n_samples, n_components), default=None
            If `init='custom'`, it is used as initial guess for the solution.
            If `None`, uses the initialisation method specified in `init`.

        H : array-like of shape (n_components, n_features), default=None

        Returns
        -------
        W : ndarray of shape (n_samples, n_components)
        """
            self, X, accept_sparse=("csr", "csc"), dtype=[np.float64, np.float32]
        )

            W, H, n_iter = self._fit_transform(X, W=W, H=H)

        self.reconstruction_err_ = _beta_divergence(
            X, W, H, self._beta_loss, square_root=True
        )

        self.n_components_ = H.shape[0]
        self.components_ = H
        self.n_iter_ = n_iter

        return W

    def _fit_transform(self, X, y=None, W=None, H=None, update_H=True):

            Data matrix to be decomposed

        y : Ignored

            If `update_H=False`, it is initialised as an array of zeros, unless
            `solver='mu'`, then it is filled with values calculated by
            `np.sqrt(X.mean() / self._n_components)`.

            If `update_H=False`, it is used as a constant, to solve for W only.

        update_H : bool, default=True
            If True, both W and H will be estimated from initial guesses,
            this corresponds to a call to the 'fit_transform' method.
            If False, only W will be estimated, this corresponds to a call
            to the 'transform' method.

        Returns
        -------

        H : ndarray of shape (n_components, n_features)
            Factorization matrix, sometimes called 'dictionary'.

        n_iter_ : int
            Actual number of iterations.
        """
        # check parameters
        self._check_params(X)

        if X.min() == 0 and self._beta_loss <= 0:
                "When beta_loss <= 0 and X contains zeros, "
                "the solver may diverge. Please add small values "
                "to X, or use a positive beta_loss."
            )

        # initialize or check W and H
        W, H = self._check_w_h(X, W, H, update_H)

        # scale the regularization terms
        l1_reg_W, l1_reg_H, l2_reg_W, l2_reg_H = self._compute_regularization(X)

        if self.solver == "cd":
            W, H, n_iter = _fit_coordinate_descent(
                X,
                W,
                H,
                self.tol,
                self.max_iter,
                l1_reg_W,
                l1_reg_H,
                l2_reg_W,
                l2_reg_H,
                update_H=update_H,
                shuffle=self.shuffle,
            )
        elif self.solver == "mu":
            W, H, n_iter, *_ = _fit_multiplicative_update(
                X,
                W,
                H,
                self._beta_loss,
                self.tol,
                l1_reg_W,
                l1_reg_H,
                l2_reg_W,
                l2_reg_H,
                update_H,
                self.verbose,
            )
        else:
            raise ValueError("Invalid solver parameter '%s'." % self.solver)

        if n_iter == self.max_iter and self.tol > 0:
                "Maximum number of iterations %d reached. Increase "
                "it to improve convergence." % self.max_iter,
            )

        return W, H, n_iter

        """Transform the data X according to the fitted NMF model.

        Returns
        -------
        """
            self,
            X,
            accept_sparse=("csr", "csc"),
            ensure_non_negative=True,
        )

            W, *_ = self._fit_transform(X, H=self.components_, update_H=False)

        return W

class MiniBatchNMF(_BaseNMF):
    """Mini-Batch Non-Negative Matrix Factorization (NMF).

    Find two non-negative matrices, i.e. matrices with all non-negative elements,
    (`W`, `H`) whose product approximates the non-negative matrix `X`. This
    factorization can be used for example for dimensionality reduction, source
    separation or topic extraction.

    .. math::

    The objective function is minimized with an alternating minimization of `W`
    and `H`.

    Note that the transformed data is named `W` and the components matrix is
    named `H`. In the NMF literature, the naming convention is usually the opposite
    since the data matrix `X` is transposed.

    Read more in the :ref:`User Guide <MiniBatchNMF>`.

        - `None`: 'nndsvda' if `n_components <= min(n_samples, n_features)`,

          initialization (better for sparseness).

          (better when sparsity is not desired).

          for when sparsity is not desired).

    batch_size : int, default=1024
        Number of samples in each mini-batch. Large batch sizes
        give better long-term convergence at the cost of a slower start.

        Beta divergence to be minimized, measuring the distance between `X`
        and the dot product `WH`. Note that values different from 'frobenius'
        fits. Note that for `beta_loss <= 0` (or 'itakura-saito'), the input
        matrix `X` cannot contain zeros.

        Control early stopping based on the norm of the differences in `H`
        between 2 steps. To disable early stopping based on changes in `H`, set

    max_no_improvement : int, default=10
        `max_no_improvement` to None.

        timing out.

    forget_factor : float, default=0.7
        Amount of rescaling of past information. Its value could be 1 with
        finite datasets. Choosing values < 1 is recommended with online
        learning as more recent batches will weight more than past batches.

    fresh_restarts : bool, default=False
        Whether to completely solve for W at each step. Doing fresh restarts will likely
        lead to a better solution for a same number of iterations but it is much slower.

    fresh_restarts_max_iter : int, default=30
        Maximum number of iterations when solving for W at each step. Only used when
        doing fresh restarts. These iterations may be stopped early based on a small
        change of W controlled by `tol`.

    transform_max_iter : int, default=None
        Maximum number of iterations when solving for W at transform time.
        If None, it defaults to `max_iter`.

        features.

        the training data `X` and the reconstructed data `WH` from

        Actual number of started iterations over the whole dataset.

    n_steps_ : int
        Number of mini-batches processed.

    See Also
    --------
    NMF : Non-negative matrix factorization.
    MiniBatchDictionaryLearning : Finds a dictionary that can best be used to represent
        data using a sparse code.

    .. [3] :doi:`"Online algorithms for nonnegative matrix factorization with the
       Itakura-Saito divergence" <10.1109/ASPAA.2011.6082314>`
       Lefevre, A., Bach, F., Fevotte, C. (2011). WASPA.

    Examples
    --------
    >>> from sklearn.decomposition import MiniBatchNMF
    >>> model = MiniBatchNMF(n_components=2, init='random', random_state=0)
    """

        "max_no_improvement": [Interval(Integral, 1, None, closed="left"), None],
        "forget_factor": [Interval(Real, 0, 1, closed="both")],
        "fresh_restarts": ["boolean"],
        "fresh_restarts_max_iter": [Interval(Integral, 1, None, closed="left")],
        "transform_max_iter": [Interval(Integral, 1, None, closed="left"), None],
    }

        self,
        *,
        batch_size=1024,
        tol=1e-4,
        forget_factor=0.7,
        fresh_restarts=False,
        fresh_restarts_max_iter=30,
        transform_max_iter=None,
    ):
            tol=tol,
        )

        self.forget_factor = forget_factor
        self.fresh_restarts = fresh_restarts
        self.fresh_restarts_max_iter = fresh_restarts_max_iter
        self.transform_max_iter = transform_max_iter

        # batch_size
        self._batch_size = min(self.batch_size, X.shape[0])

        # forget_factor
        self._rho = self.forget_factor ** (self._batch_size / X.shape[0])

        # gamma for Maximization-Minimization (MM) algorithm [Fevotte 2011]
        if self._beta_loss < 1:
            self._gamma = 1.0 / (2.0 - self._beta_loss)
        elif self._beta_loss > 2:
            self._gamma = 1.0 / (self._beta_loss - 1.0)
        else:
            self._gamma = 1.0

        # transform_max_iter
        self._transform_max_iter = (
            self.max_iter
            if self.transform_max_iter is None
            else self.transform_max_iter
        )

    def _solve_W(self, X, H, max_iter):
        """Minimize the objective function w.r.t W.

        Update W with H being fixed, until convergence. This is the heart
        of `transform` but it's also used during `fit` when doing fresh restarts.
        """
        avg = np.sqrt(X.mean() / self._n_components)
        W = np.full((X.shape[0], self._n_components), avg, dtype=X.dtype)
        W_buffer = W.copy()

        # Get scaled regularization terms. Done for each minibatch to take into account
        # variable sizes of minibatches.
        l1_reg_W, _, l2_reg_W, _ = self._compute_regularization(X)

        for _ in range(max_iter):
            W, *_ = _multiplicative_update_w(
                X, W, H, self._beta_loss, l1_reg_W, l2_reg_W, self._gamma
            )

            W_diff = linalg.norm(W - W_buffer) / linalg.norm(W)
            if self.tol > 0 and W_diff <= self.tol:
                break

            W_buffer[:] = W

        return W

    def _minibatch_step(self, X, W, H, update_H):
        """Perform the update of W and H for one minibatch."""
        batch_size = X.shape[0]

        # get scaled regularization terms. Done for each minibatch to take into account

        # update W
        if self.fresh_restarts or W is None:
            W = self._solve_W(X, H, self.fresh_restarts_max_iter)
        else:
            )

        batch_cost = (
            _beta_divergence(X, W, H, self._beta_loss)
            + l1_reg_W * W.sum()
            + l1_reg_H * H.sum()
            + l2_reg_W * (W**2).sum()
            + l2_reg_H * (H**2).sum()
        ) / batch_size

            H[:] = _multiplicative_update_h(
                X,
                W,
                H,
                beta_loss=self._beta_loss,
                gamma=self._gamma,
                A=self._components_numerator,
                B=self._components_denominator,
                rho=self._rho,
            )

            if self._beta_loss <= 1:

        return batch_cost

    def _minibatch_convergence(
        self, X, batch_cost, H, H_buffer, n_samples, step, n_steps
    ):
        """Helper function to encapsulate the early stopping logic"""

        # counts steps starting from 1 for user friendly verbose mode.
        step = step + 1

        # Ignore first iteration because H is not updated yet.
        if step == 1:
                print(f"Minibatch step {step}/{n_steps}: mean batch cost: {batch_cost}")

        # Compute an Exponentially Weighted Average of the cost function to
        # monitor the convergence while discarding minibatch-local stochastic
        # variability: https://en.wikipedia.org/wiki/Moving_average
        if self._ewa_cost is None:
            self._ewa_cost = batch_cost
        else:
            alpha = batch_size / (n_samples + 1)
            alpha = min(alpha, 1)
            self._ewa_cost = self._ewa_cost * (1 - alpha) + batch_cost * alpha

        # Log progress to be able to monitor convergence
            print(
                f"Minibatch step {step}/{n_steps}: mean batch cost: "
                f"{batch_cost}, ewa cost: {self._ewa_cost}"
            )

        # Early stopping based on change of H
        H_diff = linalg.norm(H - H_buffer) / linalg.norm(H)
        if self.tol > 0 and H_diff <= self.tol:
                print(f"Converged (small H change) at step {step}/{n_steps}")

        # Early stopping heuristic due to lack of improvement on smoothed
        # cost function
        if self._ewa_cost_min is None or self._ewa_cost < self._ewa_cost_min:
            self._no_improvement = 0
            self._ewa_cost_min = self._ewa_cost
        else:
            self._no_improvement += 1

        if (
            self.max_no_improvement is not None
            and self._no_improvement >= self.max_no_improvement
        ):
                print(
                    "Converged (lack of improvement in objective function) "
                    f"at step {step}/{n_steps}"
                )

            Data matrix to be decomposed.

        y : Ignored

        Returns
        -------
        """
        )

            W, H, n_iter, n_steps = self._fit_transform(X, W=W, H=H)

        )

        self.n_steps_ = n_steps

        return W

    def _fit_transform(self, X, W=None, H=None, update_H=True):

            this corresponds to a call to the `fit_transform` method.
            to the `transform` method.

        Returns
        -------

        n_iter : int
            Actual number of started iterations over the whole dataset.

        n_steps : int
            Number of mini-batches processed.
        """
        check_non_negative(X, "MiniBatchNMF (input X)")

            )

        H_buffer = H.copy()

        # Initialize auxiliary matrices
        self._components_numerator = H.copy()
        self._components_denominator = np.ones(H.shape, dtype=H.dtype)

        # Attributes to monitor the convergence
        self._ewa_cost = None
        self._ewa_cost_min = None
        self._no_improvement = 0

        batches = gen_batches(n_samples, self._batch_size)
        batches = itertools.cycle(batches)
        n_steps_per_iter = int(np.ceil(n_samples / self._batch_size))
        n_steps = self.max_iter * n_steps_per_iter

        for i, batch in zip(range(n_steps), batches):
            batch_cost = self._minibatch_step(X[batch], W[batch], H, update_H)

            if update_H and self._minibatch_convergence(
                X[batch], batch_cost, H, H_buffer, n_samples, i, n_steps
            ):
                break

            H_buffer[:] = H

        if self.fresh_restarts:
            W = self._solve_W(X, H, self._transform_max_iter)

        n_steps = i + 1
        n_iter = int(np.ceil(n_steps / n_steps_per_iter))

                (
                    f"Maximum number of iterations {self.max_iter} reached. "
                    "Increase it to improve convergence."
                ),
            )

        return W, H, n_iter, n_steps

        """Transform the data X according to the fitted MiniBatchNMF model.

            Data matrix to be transformed by the model.

        Returns
        -------
        """
            self,
            X,
        )

        W = self._solve_W(X, self.components_, self._transform_max_iter)

        return W

    def partial_fit(self, X, y=None, W=None, H=None):
        """Update the model using the data in `X` as a mini-batch.

        memory at once (see :ref:`scaling_strategies`).

        y : Ignored

            Only used for the first call to `partial_fit`.

        Returns
        -------
        self
        """
        has_components = hasattr(self, "components_")

            self,
            X,
            reset=not has_components,
        )

        if not has_components:
            # This instance has not been fitted yet (fit or partial_fit)
            self._check_params(X)
            _, H = self._check_w_h(X, W=W, H=H, update_H=True)

            self._components_numerator = H.copy()
            self._components_denominator = np.ones(H.shape, dtype=H.dtype)
            self.n_steps_ = 0
        else:
            H = self.components_

        self._minibatch_step(X, None, H, update_H=True)

        self.n_steps_ += 1

        self,
        *,
        tol=1e-4,
    ):

                )

                )

                        H.dtype
                    )
                )

            else:

        else:
                    (
                    ),
                )

            )

        return W, H

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        # solver
            )
                (
                ),
            )

        y : Ignored

        Returns
        -------
        """
        )

        )

        return W

        y : Ignored

        Returns
        -------

        """

            )

                X,
                W,
                H,
                self.tol,
                l1_reg_W,
                l1_reg_H,
                l2_reg_W,
                l2_reg_H,
            )
                X,
                W,
                H,
                self.tol,
                l1_reg_W,
                l1_reg_H,
                l2_reg_W,
                l2_reg_H,
                update_H,
            )
        else:

            )

        Returns
        -------
        """
            self,
            X,
        )

        return W

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        else:

        )

        """

            )

                break

        return W

        # update W
        else:
            )

                X,
                W,
                H,
            )

    ):

        else:

            print(
            )

        else:

        if (
        ):
                print(
                )

        y : Ignored

        Returns
        -------
        """
        )

        )

        return W

        Returns
        -------

        """

            )

            ):
                break

                (
                ),
            )

        Returns
        -------
        """
            self,
            X,
        )

        return W

        y : Ignored

        Returns
        -------
        self
        """

            self,
            X,
        )

        else:

def _assess_dimension(spectrum, rank, n_samples):
    """Compute the log-likelihood of a rank ``rank`` dataset.

    The dataset is assumed to be embedded in gaussian noise of shape(n,
    dimf) having spectrum ``spectrum``. This implements the method of
    T. P. Minka.

    spectrum : ndarray of shape (n_features,)
        Data spectrum.
    rank : int
        Tested rank value. It should be strictly lower than n_features,
        otherwise the method isn't specified (division by zero in equation
        (31) from the paper).

    Returns
    -------
    ll : float
        The log-likelihood.

    This implements the method of `Thomas P. Minka:
    Automatic Choice of Dimensionality for PCA. NIPS 2000: 598-604
    <https://proceedings.neurips.cc/paper/2000/file/7503cfacd12053d309b6bed5c89de212-Paper.pdf>`_
    """
    xp, _ = get_namespace(spectrum)

    n_features = spectrum.shape[0]
    if not 1 <= rank < n_features:
        raise ValueError("the tested rank should be in [1, n_features - 1]")

    eps = 1e-15

    if spectrum[rank - 1] < eps:
        # When the tested rank is associated with a small eigenvalue, there's
        # no point in computing the log-likelihood: it's going to be very
        # small and won't be the max anyway. Also, it can lead to numerical
        # issues below when computing pa, in particular in log((spectrum[i] -
        # spectrum[j]) because this will take the log of something very small.
        return -xp.inf

    pu = -rank * log(2.0)
    for i in range(1, rank + 1):
        pu += (
            lgamma((n_features - i + 1) / 2.0) - log(xp.pi) * (n_features - i + 1) / 2.0
        )

    pl = xp.sum(xp.log(spectrum[:rank]))
    pl = -pl * n_samples / 2.0

    v = max(eps, xp.sum(spectrum[rank:]) / (n_features - rank))
    pv = -log(v) * n_samples * (n_features - rank) / 2.0

    m = n_features * rank - rank * (rank + 1.0) / 2.0
    pp = log(2.0 * xp.pi) * (m + rank) / 2.0

    pa = 0.0
    spectrum_ = xp.asarray(spectrum, copy=True)
    spectrum_[rank:n_features] = v
    for i in range(rank):
        for j in range(i + 1, spectrum.shape[0]):
            pa += log(
                (spectrum[i] - spectrum[j]) * (1.0 / spectrum_[j] - 1.0 / spectrum_[i])
            ) + log(n_samples)

    ll = pu + pl + pv + pp - pa / 2.0 - rank * log(n_samples) / 2.0

    return ll

def _infer_dimension(spectrum, n_samples):
    """Infers the dimension of a dataset with a given spectrum.

    The returned value will be in [1, n_features - 1].
    """

    ll = xp.empty_like(spectrum)
    ll[0] = -xp.inf  # we don't want to return n_components = 0
    for rank in range(1, spectrum.shape[0]):
        ll[rank] = _assess_dimension(spectrum, rank, n_samples)
    return xp.argmax(ll)

class PCA(_BasePCA):
    """Principal component analysis (PCA).

    Linear dimensionality reduction using Singular Value Decomposition of the
    data to project it to a lower dimensional space. The input data is centered
    but not scaled for each feature before applying the SVD.

    It uses the LAPACK implementation of the full SVD or a randomized truncated
    SVD by the method of Halko et al. 2009, depending on the shape of the input
    data and the number of components to extract.

    With sparse inputs, the ARPACK implementation of the truncated SVD can be
    used (i.e. through :func:`scipy.sparse.linalg.svds`). Alternatively, one
    may consider :class:`TruncatedSVD` where the data are not centered.

    Notice that this class only supports sparse inputs for some solvers such as
    "arpack" and "covariance_eigh". See :class:`TruncatedSVD` for an
    alternative with sparse data.

    For a usage example, see
    :ref:`sphx_glr_auto_examples_decomposition_plot_pca_iris.py`

    Read more in the :ref:`User Guide <PCA>`.

    n_components : int, float or 'mle', default=None
        Number of components to keep.
        if n_components is not set all components are kept::

            n_components == min(n_samples, n_features)

        If ``n_components == 'mle'`` and ``svd_solver == 'full'``, Minka's
        MLE is used to guess the dimension. Use of ``n_components == 'mle'``
        will interpret ``svd_solver == 'auto'`` as ``svd_solver == 'full'``.

        If ``0 < n_components < 1`` and ``svd_solver == 'full'``, select the
        number of components such that the amount of variance that needs to be
        explained is greater than the percentage specified by n_components.

        If ``svd_solver == 'arpack'``, the number of components must be
        strictly less than the minimum of n_features and n_samples.

        Hence, the None case results in::

            n_components == min(n_samples, n_features) - 1

        If False, data passed to fit are overwritten and running
        fit(X).transform(X) will not yield the expected results,
        use fit_transform(X) instead.

    whiten : bool, default=False
        When True (False by default) the `components_` vectors are multiplied
        by the square root of n_samples and then divided by the singular values
        to ensure uncorrelated outputs with unit component-wise variances.

        Whitening will remove some information from the transformed signal
        (the relative variance scales of the components) but can sometime
        improve the predictive accuracy of the downstream estimators by
        making their data respect some hard-wired assumptions.

    svd_solver : {'auto', 'full', 'covariance_eigh', 'arpack', 'randomized'},\
        "auto" :
            The solver is selected by a default 'auto' policy is based on `X.shape` and
            `n_components`: if the input data has fewer than 1000 features and
            more than 10 times as many samples, then the "covariance_eigh"
            solver is used. Otherwise, if the input data is larger than 500x500
            and the number of components to extract is lower than 80% of the
            smallest dimension of the data, then the more efficient
            "randomized" method is selected. Otherwise the exact "full" SVD is
            computed and optionally truncated afterwards.
        "full" :
            Run exact full SVD calling the standard LAPACK solver via
            `scipy.linalg.svd` and select the components by postprocessing
        "covariance_eigh" :
            Precompute the covariance matrix (on centered data), run a
            classical eigenvalue decomposition on the covariance matrix
            typically using LAPACK and select the components by postprocessing.
            This solver is very efficient for n_samples >> n_features and small
            n_features. It is, however, not tractable otherwise for large
            n_features (large memory footprint required to materialize the
            covariance matrix). Also note that compared to the "full" solver,
            this solver effectively doubles the condition number and is
            therefore less numerical stable (e.g. on input data with a large
            range of singular values).
        "arpack" :
            Run SVD truncated to `n_components` calling ARPACK solver via
            `scipy.sparse.linalg.svds`. It requires strictly
            `0 < n_components < min(X.shape)`
        "randomized" :
            Run randomized SVD by the method of Halko et al.

        .. versionadded:: 0.18.0

            Added the 'covariance_eigh' solver.

    tol : float, default=0.0
        Tolerance for singular values computed by svd_solver == 'arpack'.
        Must be of range [0.0, infinity).

    iterated_power : int or 'auto', default='auto'
        Number of iterations for the power method computed by
        svd_solver == 'randomized'.
        Must be of range [0, infinity).

    n_oversamples : int, default=10
        This parameter is only relevant when `svd_solver="randomized"`.
        It corresponds to the additional number of random vectors to sample the
        range of `X` so as to ensure proper conditioning. See
        :func:`~sklearn.utils.extmath.randomized_svd` for more details.

    power_iteration_normalizer : {'auto', 'QR', 'LU', 'none'}, default='auto'
        Power iteration normalizer for randomized SVD solver.
        Not used by ARPACK. See :func:`~sklearn.utils.extmath.randomized_svd`

        Used when the 'arpack' or 'randomized' solvers are used. Pass an int

        Principal axes in feature space, representing the directions of
        maximum variance in the data. Equivalently, the right singular
        vectors of the centered input data, parallel to its eigenvectors.
        The components are sorted by decreasing ``explained_variance_``.

    explained_variance_ : ndarray of shape (n_components,)
        The amount of variance explained by each of the selected components.
        The variance estimation uses `n_samples - 1` degrees of freedom.

        Equal to n_components largest eigenvalues
        of the covariance matrix of X.

        sum of the ratios is equal to 1.0.

    singular_values_ : ndarray of shape (n_components,)
        The singular values corresponding to each of the selected components.
        The singular values are equal to the 2-norms of the ``n_components``
        variables in the lower-dimensional space.

        Equal to `X.mean(axis=0)`.

        The estimated number of components. When n_components is set
        to 'mle' or a number between 0 and 1 (with svd_solver == 'full') this
        number is estimated from input data. Otherwise it equals the parameter
        n_components, or the lesser value of n_features and n_samples
        if n_components is None.

    n_samples_ : int
        Number of samples in the training data.

    noise_variance_ : float
        The estimated noise covariance following the Probabilistic PCA model
        from Tipping and Bishop 1999. See "Pattern Recognition and
        Machine Learning" by C. Bishop, 12.2.1 p. 574 or
        http://www.miketipping.com/papers/met-mppca.pdf. It is required to
        compute the estimated data covariance and score samples.

        Equal to the average of (min(n_features, n_samples) - n_components)
        smallest eigenvalues of the covariance matrix of X.

    See Also
    --------
    KernelPCA : Kernel Principal Component Analysis.
    SparsePCA : Sparse Principal Component Analysis.
    IncrementalPCA : Incremental Principal Component Analysis.

    For n_components == 'mle', this class uses the method from:
    `Minka, T. P.. "Automatic choice of dimensionality for PCA".
    In NIPS, pp. 598-604 <https://tminka.github.io/papers/pca/minka-pca.pdf>`_

    Implements the probabilistic PCA model from:
    `Tipping, M. E., and Bishop, C. M. (1999). "Probabilistic principal
    component analysis". Journal of the Royal Statistical Society:
    Series B (Statistical Methodology), 61(3), 611-622.
    <http://www.miketipping.com/papers/met-mppca.pdf>`_
    via the score and score_samples methods.

    For svd_solver == 'arpack', refer to `scipy.sparse.linalg.svds`.

    For svd_solver == 'randomized', see:
    :doi:`Halko, N., Martinsson, P. G., and Tropp, J. A. (2011).
    "Finding structure with randomness: Probabilistic algorithms for
    constructing approximate matrix decompositions".
    SIAM review, 53(2), 217-288.
    <10.1137/090771806>`
    and also
    :doi:`Martinsson, P. G., Rokhlin, V., and Tygert, M. (2011).
    "A randomized algorithm for the decomposition of matrices".
    Applied and Computational Harmonic Analysis, 30(1), 47-68.
    <10.1016/j.acha.2010.02.003>`

    Examples
    --------
    >>> from sklearn.decomposition import PCA
    >>> pca = PCA(n_components=2)
    >>> pca.fit(X)
    PCA(n_components=2)
    >>> print(pca.explained_variance_ratio_)
    [0.9924... 0.0075...]
    >>> print(pca.singular_values_)
    [6.30061... 0.54980...]

    >>> pca = PCA(n_components=2, svd_solver='full')
    PCA(n_components=2, svd_solver='full')
    [0.9924... 0.00755...]

    >>> pca = PCA(n_components=1, svd_solver='arpack')
    PCA(n_components=1, svd_solver='arpack')
    [0.99244...]
    [6.30061...]
    """

            Interval(Integral, 0, None, closed="left"),
            StrOptions({"mle"}),
            None,
        ],
        "whiten": ["boolean"],
        "svd_solver": [
            StrOptions({"auto", "full", "covariance_eigh", "arpack", "randomized"})
        ],
        "iterated_power": [
        ],
        "n_oversamples": [Interval(Integral, 1, None, closed="left")],
        "power_iteration_normalizer": [StrOptions({"auto", "QR", "LU", "none"})],
    }

        self,
        *,
        whiten=False,
        svd_solver="auto",
        tol=0.0,
        iterated_power="auto",
        n_oversamples=10,
        power_iteration_normalizer="auto",
    ):
        self.whiten = whiten
        self.svd_solver = svd_solver
        self.iterated_power = iterated_power
        self.n_oversamples = n_oversamples
        self.power_iteration_normalizer = power_iteration_normalizer

        y : Ignored
            Ignored.

        Returns
        -------
        """

        """Fit the model with X and apply the dimensionality reduction on X.

        y : Ignored
            Ignored.

        Returns
        -------
            Transformed values.

        Notes
        -----
        This method returns a Fortran-ordered array. To convert it to a
        C-ordered array, use 'np.ascontiguousarray'.
        """
        U, S, _, X, x_is_centered, xp = self._fit(X)
        if U is not None:
            U = U[:, : self.n_components_]

            if self.whiten:
                # X_new = X * V / S * sqrt(n_samples) = U * sqrt(n_samples)
                U *= sqrt(X.shape[0] - 1)
            else:
                # X_new = X * V = U * S * Vt * V = U * S
                U *= S[: self.n_components_]

            return U
        else:  # solver="covariance_eigh" does not compute U at fit time.
            return self._transform(X, xp, x_is_centered=x_is_centered)

    def _fit(self, X):
        """Dispatch to the right submethod depending on the chosen solver."""

        # Raise an error for sparse input and unsupported svd_solver
        if issparse(X) and self.svd_solver not in ["auto", "arpack", "covariance_eigh"]:
                'PCA only support sparse inputs with the "arpack" and'
                f' "covariance_eigh" solvers, while "{self.svd_solver}" was passed. See'
                " TruncatedSVD for a possible alternative."
            )
        if self.svd_solver == "arpack" and is_array_api_compliant:
                "PCA with svd_solver='arpack' is not supported for Array API inputs."
            )

        # Validate the data, without ever forcing a copy as any solver that
        # supports sparse input data and the `covariance_eigh` solver are
        # written in a way to avoid the need for any inplace modification of
        # the input data contrary to the other solvers.
        # The copy will happen
        # later, only if needed, once the solver negotiation below is done.
            self,
            X,
            dtype=[xp.float64, xp.float32],
            copy=False,
        )
        self._fit_svd_solver = self.svd_solver
        if self._fit_svd_solver == "auto" and issparse(X):
            self._fit_svd_solver = "arpack"

            if self._fit_svd_solver != "arpack":
                n_components = min(X.shape)
            else:
                n_components = min(X.shape) - 1
        else:

        if self._fit_svd_solver == "auto":
            # Tall and skinny problems are best handled by precomputing the
            # covariance matrix.
            if X.shape[1] <= 1_000 and X.shape[0] >= 10 * X.shape[1]:
                self._fit_svd_solver = "covariance_eigh"
            # Small problem or n_components == 'mle', just call full PCA
            elif max(X.shape) <= 500 or n_components == "mle":
                self._fit_svd_solver = "full"
            elif 1 <= n_components < 0.8 * min(X.shape):
                self._fit_svd_solver = "randomized"
            # This is also the case of n_components in (0, 1)
            else:

        # Call different fits for either full or truncated SVD
        if self._fit_svd_solver in ("full", "covariance_eigh"):
            return self._fit_full(X, n_components, xp, is_array_api_compliant)
        elif self._fit_svd_solver in ["arpack", "randomized"]:
            return self._fit_truncated(X, n_components, xp)

    def _fit_full(self, X, n_components, xp, is_array_api_compliant):
        """Fit the model by computing full SVD on X."""

        if n_components == "mle":
            if n_samples < n_features:
                    "n_components='mle' is only supported if n_samples >= n_features"
                )
        elif not 0 <= n_components <= min(n_samples, n_features):
                f"n_components={n_components} must be between 0 and "
                f"min(n_samples, n_features)={min(n_samples, n_features)} with "
                f"svd_solver={self._fit_svd_solver!r}"
            )

        self.mean_ = xp.mean(X, axis=0)
        # When X is a scipy sparse matrix, self.mean_ is a numpy matrix, so we need
        # to transform it to a 1D array. Note that this is not the case when X
        # is a scipy sparse array.
        # TODO: remove the following two lines when scikit-learn only depends
        # on scipy versions that no longer support scipy.sparse matrices.
        self.mean_ = xp.reshape(xp.asarray(self.mean_), (-1,))

        if self._fit_svd_solver == "full":
            X_centered = xp.asarray(X, copy=True) if self.copy else X
            X_centered -= self.mean_
            x_is_centered = not self.copy

            if not is_array_api_compliant:
                # Use scipy.linalg with NumPy/SciPy inputs for the sake of not
                # introducing unanticipated behavior changes. In the long run we
                # could instead decide to always use xp.linalg.svd for all inputs,
                # but that would make this code rely on numpy's SVD instead of
                # scipy's. It's not 100% clear whether they use the same LAPACK
                # solver by default though (assuming both are built against the
                # same BLAS).
                U, S, Vt = linalg.svd(X_centered, full_matrices=False)
            else:
                U, S, Vt = xp.linalg.svd(X_centered, full_matrices=False)
            explained_variance_ = (S**2) / (n_samples - 1)

        else:
            assert self._fit_svd_solver == "covariance_eigh"
            # In the following, we center the covariance matrix C afterwards
            # (without centering the data X first) to avoid an unnecessary copy
            # of X. Note that the mean_ attribute is still needed to center
            # test data in the transform method.
            #
            # Note: at the time of writing, `xp.cov` does not exist in the
            # Array API standard:
            # https://github.com/data-apis/array-api/issues/43
            #
            # Besides, using `numpy.cov`, as of numpy 1.26.0, would not be
            # memory efficient for our use case when `n_samples >> n_features`:
            # `numpy.cov` centers a copy of the data before computing the
            # matrix product instead of subtracting a small `(n_features,
            # n_features)` square matrix from the gram matrix X.T @ X, as we do
            # below.
            x_is_centered = False
            C = X.T @ X
            C -= (
                n_samples
                * xp.reshape(self.mean_, (-1, 1))
                * xp.reshape(self.mean_, (1, -1))
            )
            C /= n_samples - 1
            eigenvals, eigenvecs = xp.linalg.eigh(C)

            # When X is a scipy sparse matrix, the following two datastructures
            # are returned as instances of the soft-deprecated numpy.matrix
            # class. Note that this problem does not occur when X is a scipy
            # sparse array (or another other kind of supported array).
            # TODO: remove the following two lines when scikit-learn only
            # depends on scipy versions that no longer support scipy.sparse
            # matrices.
            eigenvals = xp.reshape(xp.asarray(eigenvals), (-1,))
            eigenvecs = xp.asarray(eigenvecs)

            eigenvals = xp.flip(eigenvals, axis=0)
            eigenvecs = xp.flip(eigenvecs, axis=1)

            # The covariance matrix C is positive semi-definite by
            # construction. However, the eigenvalues returned by xp.linalg.eigh
            # can be slightly negative due to numerical errors. This would be
            # an issue for the subsequent sqrt, hence the manual clipping.
            eigenvals[eigenvals < 0.0] = 0.0
            explained_variance_ = eigenvals

            # Re-construct SVD of centered X indirectly and make it consistent
            # with the other solvers.
            S = xp.sqrt(eigenvals * (n_samples - 1))
            Vt = eigenvecs.T
            U = None

        U, Vt = svd_flip(U, Vt, u_based_decision=False)

        components_ = Vt

        # Get variance explained by singular values
        total_var = xp.sum(explained_variance_)
        explained_variance_ratio_ = explained_variance_ / total_var
        singular_values_ = xp.asarray(S, copy=True)  # Store the singular values.

        # Postprocess the number of components required
            n_components = _infer_dimension(explained_variance_, n_samples)
        elif 0 < n_components < 1.0:
            # number of components for which the cumulated explained
            # variance percentage is superior to the desired threshold
            # side='right' ensures that number of features selected
            # their variance is always greater than n_components float
            # passed. More discussion in issue: #15669
            if is_array_api_compliant:
                # Convert to numpy as xp.cumsum and xp.searchsorted are not
                # part of the Array API standard yet:
                #
                # https://github.com/data-apis/array-api/issues/597
                # https://github.com/data-apis/array-api/issues/688
                #
                # Furthermore, it's not always safe to call them for namespaces
                # that already implement them: for instance as
                # cupy.searchsorted does not accept a float as second argument.
                explained_variance_ratio_np = _convert_to_numpy(
                    explained_variance_ratio_, xp=xp
                )
            else:
                explained_variance_ratio_np = explained_variance_ratio_
            ratio_cumsum = stable_cumsum(explained_variance_ratio_np)
            n_components = np.searchsorted(ratio_cumsum, n_components, side="right") + 1

        # Compute noise covariance using Probabilistic PCA model
        # The sigma2 maximum likelihood (cf. eq. 12.46)
        if n_components < min(n_features, n_samples):
            self.noise_variance_ = xp.mean(explained_variance_[n_components:])
        else:
            self.noise_variance_ = 0.0

        self.n_samples_ = n_samples
        self.n_components_ = n_components
        # Assign a copy of the result of the truncation of the components in
        # order to:
        # - release the memory used by the discarded components,
        # - ensure that the kept components are allocated contiguously in
        #   memory to make the transform method faster by leveraging cache
        #   locality.
        self.components_ = xp.asarray(components_[:n_components, :], copy=True)

        # We do the same for the other arrays for the sake of consistency.
        self.explained_variance_ = xp.asarray(
            explained_variance_[:n_components], copy=True
        )
        self.explained_variance_ratio_ = xp.asarray(
            explained_variance_ratio_[:n_components], copy=True
        )
        self.singular_values_ = xp.asarray(singular_values_[:n_components], copy=True)

        return U, S, Vt, X, x_is_centered, xp

    def _fit_truncated(self, X, n_components, xp):
        """Fit the model by computing truncated SVD (by ARPACK or randomized)
        on X.
        """

        svd_solver = self._fit_svd_solver
        if isinstance(n_components, str):
                "n_components=%r cannot be a string with svd_solver='%s'"
                % (n_components, svd_solver)
            )
        elif not 1 <= n_components <= min(n_samples, n_features):
                "n_components=%r must be between 1 and "
                "min(n_samples, n_features)=%r with "
                "svd_solver='%s'"
                % (n_components, min(n_samples, n_features), svd_solver)
            )
        elif svd_solver == "arpack" and n_components == min(n_samples, n_features):
                "n_components=%r must be strictly less than "
            )

        # Center data
        total_var = None
            self.mean_, var = mean_variance_axis(X, axis=0)
            total_var = var.sum() * n_samples / (n_samples - 1)  # ddof=1
            X_centered = _implicit_column_offset(X, self.mean_)
        else:
            self.mean_ = xp.mean(X, axis=0)

        if svd_solver == "arpack":
            v0 = _init_arpack_v0(min(X.shape), random_state)
            U, S, Vt = svds(X_centered, k=n_components, tol=self.tol, v0=v0)
            # svds doesn't abide by scipy.linalg.svd/randomized_svd
            # conventions, so reverse its outputs.
            S = S[::-1]
            # flip eigenvectors' sign to enforce deterministic output
            U, Vt = svd_flip(U[:, ::-1], Vt[::-1], u_based_decision=False)

        elif svd_solver == "randomized":
            # sign flipping is done inside
            U, S, Vt = _randomized_svd(
                X_centered,
                n_components=n_components,
                n_oversamples=self.n_oversamples,
                n_iter=self.iterated_power,
                power_iteration_normalizer=self.power_iteration_normalizer,
                flip_sign=False,
            )
            U, Vt = svd_flip(U, Vt, u_based_decision=False)

        self.components_ = Vt

        self.explained_variance_ = (S**2) / (n_samples - 1)

        # Workaround in-place variance calculation since at the time numpy
        # did not have a way to calculate variance in-place.
        #
        # TODO: update this code to either:
        # * Use the array-api variance calculation, unless memory usage suffers
        # * Update sklearn.utils.extmath._incremental_mean_and_var to support array-api
        # See: https://github.com/scikit-learn/scikit-learn/pull/18689#discussion_r1335540991
        if total_var is None:
            N = X.shape[0] - 1
            X_centered **= 2
            total_var = xp.sum(X_centered) / N

        self.explained_variance_ratio_ = self.explained_variance_ / total_var
        self.singular_values_ = xp.asarray(S, copy=True)  # Store the singular values.

        if self.n_components_ < min(n_features, n_samples):
            self.noise_variance_ = total_var - xp.sum(self.explained_variance_)
            self.noise_variance_ /= min(n_features, n_samples) - n_components
        else:

        """Return the log-likelihood of each sample.

        See. "Pattern Recognition and Machine Learning"
        by C. Bishop, 12.2.1 p. 574
        or http://www.miketipping.com/papers/met-mppca.pdf

            The data.

        Returns
        -------
        ll : ndarray of shape (n_samples,)
            Log-likelihood of each sample under the current model.
        """
        X = validate_data(self, X, dtype=[xp.float64, xp.float32], reset=False)
        Xr = X - self.mean_
        log_like = -0.5 * xp.sum(Xr * (Xr @ precision), axis=1)
        log_like -= 0.5 * (n_features * log(2.0 * np.pi) - fast_logdet(precision))
        return log_like

        """Return the average log-likelihood of all samples.

            The data.

        y : Ignored
            Ignored.

        Returns
        -------
        ll : float
            Average log-likelihood of the samples under the current model.
        """
        return float(xp.mean(self.score_samples(X)))

        tags.input_tags.sparse = self.svd_solver in (
            "auto",
            "arpack",
            "covariance_eigh",
        )

        self,
        *,
        tol=0.0,
    ):

        y : Ignored
            Ignored.

        Returns
        -------
        """

        y : Ignored
            Ignored.

        Returns
        -------

        Notes
        -----
        """

            else:

            return U

            )
            )

            self,
            X,
        )

            else:
        else:

            else:

                )
            )

            else:

        else:
            #
            #
            # below.
            C = X.T @ X
            C -= (
                n_samples
            )

            U = None

                #
                #
                )
            else:

        else:

        # order to:

        )
        )

        on X.
        """

            )
            )
            )

        else:

            S = S[::-1]

            )

        #

        else:

            The data.

        Returns
        -------
        """

            The data.

        y : Ignored
            Ignored.

        Returns
        -------
        ll : float
        """

            "auto",
            "arpack",
        )

class _BasePCA(
):
    """Base class for PCA methods.

    """

    def get_covariance(self):
        """Compute data covariance with the generative model.

        ``cov = components_.T * S**2 * components_ + sigma2 * eye(n_features)``
        where S**2 contains the explained variances, and sigma2 contains the
        noise variances.

        Returns
        -------
        cov : array of shape=(n_features, n_features)
            Estimated covariance of data.
        """
        xp, _ = get_namespace(self.components_)

        components_ = self.components_
        exp_var = self.explained_variance_
        if self.whiten:
            components_ = components_ * xp.sqrt(exp_var[:, np.newaxis])
        exp_var_diff = exp_var - self.noise_variance_
        exp_var_diff = xp.where(
            exp_var > self.noise_variance_,
            exp_var_diff,
            xp.asarray(0.0, device=device(exp_var), dtype=exp_var.dtype),
        )
        cov = (components_.T * exp_var_diff) @ components_
        _fill_or_add_to_diagonal(cov, self.noise_variance_, xp)
        return cov

        """Compute data precision matrix with the generative model.

        Equals the inverse of the covariance but computed with
        the matrix inversion lemma for efficiency.

        Returns
        -------
        precision : array, shape=(n_features, n_features)
            Estimated precision of data.
        """
        xp, is_array_api_compliant = get_namespace(self.components_)

        # handle corner cases first
        if self.n_components_ == 0:
            return xp.eye(n_features) / self.noise_variance_

            linalg_inv = xp.linalg.inv
        else:
            linalg_inv = linalg.inv

        if self.noise_variance_ == 0.0:
            return linalg_inv(self.get_covariance())

        # Get precision using matrix inversion lemma
            xp.asarray(0.0, device=device(exp_var)),
        )
        precision = components_ @ components_.T / self.noise_variance_
        _fill_or_add_to_diagonal(precision, 1.0 / exp_var_diff, xp)
        precision = components_.T @ linalg_inv(precision) @ components_
        precision /= -(self.noise_variance_**2)
        _fill_or_add_to_diagonal(precision, 1.0 / self.noise_variance_, xp)

        """Placeholder for fit. Subclasses should implement this method!

        Fit the model with X.

            Training data, where `n_samples` is the number of samples and

        Returns
        -------
        """

        """Apply dimensionality reduction to X.

        X is projected on the first principal components previously extracted
        from a training set.

        Returns
        -------
        X_new : array-like of shape (n_samples, n_components)
            Projection of X in the first principal components, where `n_samples`
            is the number of samples and `n_components` is the number of the components.
        """
        xp, _ = get_namespace(X, self.components_, self.explained_variance_)

            self,
            X,
        )
        return self._transform(X, xp=xp, x_is_centered=False)

    def _transform(self, X, xp, x_is_centered=False):
        X_transformed = X @ self.components_.T
        if not x_is_centered:
            # Apply the centering after the projection.
            # For dense X this avoids copying or mutating the data passed by
            # the caller.
            # For sparse X it keeps sparsity and avoids having to wrap X into
            # a linear operator.
            X_transformed -= xp.reshape(self.mean_, (1, -1)) @ self.components_.T
            # For some solvers (such as "arpack" and "covariance_eigh"), on
            # rank deficient data, some components can have a variance
            # arbitrarily close to zero, leading to non-finite results when
            # whitening. To avoid this problem we clip the variance below.
            scale = xp.sqrt(self.explained_variance_)
            min_scale = xp.finfo(scale.dtype).eps
            scale[scale < min_scale] = min_scale
            X_transformed /= scale
        return X_transformed

        In other words, return an input `X_original` whose transform would be X.

        X : array-like of shape (n_samples, n_components)
            and `n_components` is the number of components.

        Returns
        -------
        X_original array-like of shape (n_samples, n_features)
            Original data, where `n_samples` is the number of samples

        Notes
        -----
        If whitening is enabled, inverse_transform will compute the
        exact inverse operation, which includes reversing whitening.
        """

            scaled_components = (
                xp.sqrt(self.explained_variance_[:, np.newaxis]) * self.components_
            )
            return X @ scaled_components + self.mean_
        else:
            return X @ self.components_ + self.mean_

    @property

        Returns
        -------
        """

        )
        return cov

        Returns
        -------
        """

        else:

        )

        Returns
        -------
        """

        Returns
        -------
        """

            self,
            X,
        )

        Returns
        -------

        Notes
        -----
        """

            )
        else:

class KernelPCA(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """Kernel Principal component analysis (KPCA).

    Non-linear dimensionality reduction through the use of kernels [1]_, see also
    :ref:`metrics`.

    It uses the :func:`scipy.linalg.eigh` LAPACK implementation of the full SVD
    or the :func:`scipy.sparse.linalg.eigsh` ARPACK implementation of the
    truncated SVD, depending on the shape of the input data and the number of
    components to extract. It can also use a randomized truncated SVD by the
    method proposed in [3]_, see `eigen_solver`.

    For a usage example and comparison between
    Principal Components Analysis (PCA) and its kernelized version (KPCA), see
    :ref:`sphx_glr_auto_examples_decomposition_plot_kernel_pca.py`.

    For a usage example in denoising images using KPCA, see
    :ref:`sphx_glr_auto_examples_applications_plot_digits_denoising.py`.

    Read more in the :ref:`User Guide <kernel_PCA>`.

        Number of components. If None, all non-zero components are kept.

    kernel : {'linear', 'poly', 'rbf', 'sigmoid', 'cosine', 'precomputed'} \
            or callable, default='linear'
        Kernel used for PCA.

        Kernel coefficient for rbf, poly and sigmoid kernels. Ignored by other
        kernels. If ``gamma`` is ``None``, then it is set to ``1/n_features``.

        Degree for poly kernels. Ignored by other kernels.

        Independent term in poly and sigmoid kernels.

        Parameters (keyword arguments) and
        values for kernel passed as callable object.

        Hyperparameter of the ridge regression that learns the
        inverse transform (when fit_inverse_transform=True).

    fit_inverse_transform : bool, default=False
        Learn the inverse transform for non-precomputed kernels
        (i.e. learn to find the pre-image of a point). This method is based
        on [2]_.

    eigen_solver : {'auto', 'dense', 'arpack', 'randomized'}, \
        Select eigensolver to use. If `n_components` is much
        less than the number of training samples, randomized (or arpack to a
        smaller extent) may be more efficient than the dense eigensolver.
        Randomized SVD is performed according to the method of Halko et al
        [3]_.

        auto :
            the solver is selected by a default policy based on n_samples
            (the number of training samples) and `n_components`:
            if the number of components to extract is less than 10 (strict) and
            the number of samples is more than 200 (strict), the 'arpack'
            method is enabled. Otherwise the exact full eigenvalue
            decomposition is computed and optionally truncated afterwards
            ('dense' method).
        dense :
            run exact full eigenvalue decomposition calling the standard
            LAPACK solver via `scipy.linalg.eigh`, and select the components
            by postprocessing
        arpack :
            run SVD truncated to n_components calling ARPACK solver using
            `scipy.sparse.linalg.eigsh`. It requires strictly
            0 < n_components < n_samples
        randomized :
            run randomized SVD by the method of Halko et al. [3]_. The current
            implementation selects eigenvalues based on their module; therefore
            using this method can lead to unexpected results if the kernel is
            not positive semi-definite. See also [4]_.

        .. versionchanged:: 1.0
           `'randomized'` was added.

    tol : float, default=0
        Convergence tolerance for arpack.
        If 0, optimal value will be chosen by arpack.

    max_iter : int, default=None
        Maximum number of iterations for arpack.
        If None, optimal value will be chosen by arpack.

    iterated_power : int >= 0, or 'auto', default='auto'
        svd_solver == 'randomized'. When 'auto', it is set to 7 when
        `n_components < 0.1 * min(X.shape)`, other it is set to 4.

    remove_zero_eig : bool, default=False
        If True, then all components with zero eigenvalues are removed, so
        that the number of components in the output may be < n_components
        (and sometimes even zero due to numerical instability).
        When n_components is None, this parameter is ignored and components
        with zero eigenvalues are removed regardless.

        Used when ``eigen_solver`` == 'arpack' or 'randomized'. Pass an int

    copy_X : bool, default=True
        If True, input X is copied and stored by the model in the `X_fit_`
        attribute. If no further changes will be done to X, setting
        `copy_X=False` saves memory by storing a reference.

    eigenvalues_ : ndarray of shape (n_components,)
        Eigenvalues of the centered kernel matrix in decreasing order.
        If `n_components` and `remove_zero_eig` are not set,
        then all values are stored.

    eigenvectors_ : ndarray of shape (n_samples, n_components)
        Eigenvectors of the centered kernel matrix. If `n_components` and
        `remove_zero_eig` are not set, then all components are stored.

    dual_coef_ : ndarray of shape (n_samples, n_features)
        Inverse transform matrix. Only available when
        ``fit_inverse_transform`` is True.

    X_transformed_fit_ : ndarray of shape (n_samples, n_components)
        Projection of the fitted data on the kernel principal components.
        Only available when ``fit_inverse_transform`` is True.

    X_fit_ : ndarray of shape (n_samples, n_features)
        The data used to fit the model. If `copy_X=False`, then `X_fit_` is
        a reference. This attribute is used for the calls to transform.

    gamma_ : float
        Kernel coefficient for rbf, poly and sigmoid kernels. When `gamma`
        is explicitly provided, this is just the same as `gamma`. When `gamma`
        is `None`, this is the actual value of kernel coefficient.

    See Also
    --------
    FastICA : A fast algorithm for Independent Component Analysis.
    NMF : Non-Negative Matrix Factorization.
    PCA : Principal Component Analysis.

    .. [1] `Schölkopf, Bernhard, Alexander Smola, and Klaus-Robert Müller.
       "Kernel principal component analysis."
       International conference on artificial neural networks.
       Springer, Berlin, Heidelberg, 1997.
       <https://people.eecs.berkeley.edu/~wainwrig/stat241b/scholkopf_kernel.pdf>`_

    .. [2] `Bakır, Gökhan H., Jason Weston, and Bernhard Schölkopf.
       "Learning to find pre-images."
       Advances in neural information processing systems 16 (2004): 449-456.
       <https://papers.nips.cc/paper/2003/file/ac1ad983e08ad3304a97e147f522747e-Paper.pdf>`_

    .. [3] :arxiv:`Halko, Nathan, Per-Gunnar Martinsson, and Joel A. Tropp.
       "Finding structure with randomness: Probabilistic algorithms for
       constructing approximate matrix decompositions."
       SIAM review 53.2 (2011): 217-288. <0909.4061>`

    .. [4] `Martinsson, Per-Gunnar, Vladimir Rokhlin, and Mark Tygert.
       "A randomized algorithm for the decomposition of matrices."
       Applied and Computational Harmonic Analysis 30.1 (2011): 47-68.
       <https://www.sciencedirect.com/science/article/pii/S1063520310000242>`_

    Examples
    --------
    >>> from sklearn.decomposition import KernelPCA
    >>> X, _ = load_digits(return_X_y=True)
    >>> transformer = KernelPCA(n_components=7, kernel='linear')
    >>> X_transformed = transformer.fit_transform(X)
    (1797, 7)
    """

            None,
        ],
            StrOptions({"linear", "poly", "rbf", "sigmoid", "cosine", "precomputed"}),
            callable,
        ],
        "gamma": [
            Interval(Real, 0, None, closed="left"),
            None,
        ],
        "fit_inverse_transform": ["boolean"],
        "eigen_solver": [StrOptions({"auto", "dense", "arpack", "randomized"})],
        "max_iter": [
            None,
        ],
        ],
        "remove_zero_eig": ["boolean"],
        "copy_X": ["boolean"],
    }

        self,
        *,
        degree=3,
        coef0=1,
        fit_inverse_transform=False,
        eigen_solver="auto",
        tol=0,
        remove_zero_eig=False,
        copy_X=True,
    ):
        self.fit_inverse_transform = fit_inverse_transform
        self.eigen_solver = eigen_solver
        self.remove_zero_eig = remove_zero_eig
        self.copy_X = copy_X

        else:
            params = {"gamma": self.gamma_, "degree": self.degree, "coef0": self.coef0}
        return pairwise_kernels(
            X, Y, metric=self.kernel, filter_params=True, n_jobs=self.n_jobs, **params
        )

    def _fit_transform_in_place(self, K):
        """Fit's using kernel K"""
        # center kernel in place
        K = self._centerer.fit(K).transform(K, copy=False)

        # adjust n_components according to user inputs
            n_components = K.shape[0]  # use all dimensions
        else:
            n_components = min(K.shape[0], self.n_components)

        # compute eigenvectors
        if self.eigen_solver == "auto":
            if K.shape[0] > 200 and n_components < 10:
                eigen_solver = "arpack"
            else:
                eigen_solver = "dense"
        else:
            eigen_solver = self.eigen_solver

        if eigen_solver == "dense":
            # Note: subset_by_index specifies the indices of smallest/largest to return
            self.eigenvalues_, self.eigenvectors_ = eigh(
                K, subset_by_index=(K.shape[0] - n_components, K.shape[0] - 1)
            )
        elif eigen_solver == "arpack":
            v0 = _init_arpack_v0(K.shape[0], self.random_state)
            self.eigenvalues_, self.eigenvectors_ = eigsh(
                K, n_components, which="LA", tol=self.tol, maxiter=self.max_iter, v0=v0
            )
        elif eigen_solver == "randomized":
            self.eigenvalues_, self.eigenvectors_ = _randomized_eigsh(
                K,
                selection="module",
            )

        # make sure that the eigenvalues are ok and fix numerical issues
        self.eigenvalues_ = _check_psd_eigenvalues(
            self.eigenvalues_, enable_warnings=False
        )

        self.eigenvectors_, _ = svd_flip(u=self.eigenvectors_, v=None)

        # sort eigenvectors in descending order
        indices = self.eigenvalues_.argsort()[::-1]
        self.eigenvalues_ = self.eigenvalues_[indices]
        self.eigenvectors_ = self.eigenvectors_[:, indices]

        # remove eigenvectors with a zero eigenvalue (null space) if required
        if self.remove_zero_eig or self.n_components is None:
            self.eigenvectors_ = self.eigenvectors_[:, self.eigenvalues_ > 0]
            self.eigenvalues_ = self.eigenvalues_[self.eigenvalues_ > 0]

        # Maintenance note on Eigenvectors normalization
        # ----------------------------------------------
        # there is a link between
        # the eigenvectors of K=Phi(X)'Phi(X) and the ones of Phi(X)Phi(X)'
        # if v is an eigenvector of K
        #     then Phi(X)v  is an eigenvector of Phi(X)Phi(X)'
        # if u is an eigenvector of Phi(X)Phi(X)'
        #     then Phi(X)'u is an eigenvector of Phi(X)'Phi(X)
        #
        # At this stage our self.eigenvectors_ (the v) have norm 1, we need to scale
        # them so that eigenvectors in kernel feature space (the u) have norm=1
        # instead
        #
        # We COULD scale them here:
        #       self.eigenvectors_ = self.eigenvectors_ / np.sqrt(self.eigenvalues_)
        #
        # But choose to perform that LATER when needed, in `fit()` and in
        # `transform()`.

        return K

    def _fit_inverse_transform(self, X_transformed, X):
        if hasattr(X, "tocsr"):
                "Inverse transform not implemented for sparse matrices!"
            )

        n_samples = X_transformed.shape[0]
        K = self._get_kernel(X_transformed)
        K.flat[:: n_samples + 1] += self.alpha
        self.dual_coef_ = linalg.solve(K, X, assume_a="pos", overwrite_a=True)
        self.X_transformed_fit_ = X_transformed

        y : Ignored

        Returns
        -------
        """
        if self.fit_inverse_transform and self.kernel == "precomputed":
            raise ValueError("Cannot fit_inverse_transform with a precomputed kernel.")
        X = validate_data(self, X, accept_sparse="csr", copy=self.copy_X)
        self.gamma_ = 1 / X.shape[1] if self.gamma is None else self.gamma
        self._centerer = KernelCenterer().set_output(transform="default")
        # When kernel="precomputed", K is X but it's safe to perform in place operations
        # on K because a copy was made before if requested by copy_X.
        self._fit_transform_in_place(K)

        if self.fit_inverse_transform:
            # no need to use the kernel to transform X, use shortcut expression
            X_transformed = self.eigenvectors_ * np.sqrt(self.eigenvalues_)

            self._fit_inverse_transform(X_transformed, X)

        """Fit the model from data in X and transform X.

        y : Ignored

        Returns
        -------
        """
        self.fit(X, **params)

        # no need to use the kernel to transform X, use shortcut expression
        X_transformed = self.eigenvectors_ * np.sqrt(self.eigenvalues_)

        """Transform X.

        Returns
        -------
        """

        # Compute centered gram matrix between X and training data X_fit_
        K = self._centerer.transform(self._get_kernel(X, self.X_fit_))

        # scale eigenvectors (properly account for null-space for dot product)
        non_zeros = np.flatnonzero(self.eigenvalues_)
        scaled_alphas = np.zeros_like(self.eigenvectors_)
        scaled_alphas[:, non_zeros] = self.eigenvectors_[:, non_zeros] / np.sqrt(
            self.eigenvalues_[non_zeros]
        )

        # Project with a scalar product between K and the scaled eigenvectors
        return np.dot(K, scaled_alphas)

        """Transform X back to original space.

        ``inverse_transform`` approximates the inverse transformation using
        a learned pre-image. The pre-image is learned by kernel ridge
        regression of the original data on their low-dimensional representation
        vectors.

        .. note:
            :meth:`~sklearn.decomposition.fit` internally uses a centered
            kernel. As the centered kernel no longer contains the information
            of the mean of kernel features, such information is not taken into
            account in reconstruction.

        .. note::
            When users want to compute inverse transformation for 'linear'
            kernel, it is recommended that they use
            :class:`~sklearn.decomposition.PCA` instead. Unlike
            :class:`~sklearn.decomposition.PCA`,
            :class:`~sklearn.decomposition.KernelPCA`'s ``inverse_transform``
            does not reconstruct the mean of data when 'linear' kernel is used
            due to the use of centered kernel.

        Returns
        -------
        X_new : ndarray of shape (n_samples, n_features)

        `Bakır, Gökhan H., Jason Weston, and Bernhard Schölkopf.
        "Learning to find pre-images."
        Advances in neural information processing systems 16 (2004): 449-456.
        <https://papers.nips.cc/paper/2003/file/ac1ad983e08ad3304a97e147f522747e-Paper.pdf>`_
        """
        if not self.fit_inverse_transform:
                "The fit_inverse_transform parameter was not"
                " set to True when instantiating and hence "
                "the inverse transform is not available."
            )

        K = self._get_kernel(X, self.X_transformed_fit_)

    @property
        return self.eigenvalues_.shape[0]

        self,
        *,
        degree=3,
        coef0=1,
        tol=0,
    ):

        else:
        )

        else:

            else:
        else:

            )
            )
                K,
            )

        )

        #
        # instead
        #
        #

        return K

            )

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

        )

        vectors.

        .. note:

        .. note::

        Returns
        -------

        """
            )

class FactorAnalysis(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """Factor Analysis (FA).

    A simple linear generative model with Gaussian latent variables.

    The observations are assumed to be caused by a linear transformation of
    lower dimensional latent factors and added Gaussian noise.
    Without loss of generality the factors are distributed according to a
    Gaussian with zero mean and unit covariance. The noise is also zero mean
    and has an arbitrary diagonal covariance matrix.

    If we would restrict the model further, by assuming that the Gaussian
    noise is even isotropic (all diagonal entries are the same) we would obtain
    :class:`PCA`.

    FactorAnalysis performs a maximum likelihood estimate of the so-called
    `loading` matrix, the transformation of the latent variables to the
    observed ones, using SVD based approach.

    Read more in the :ref:`User Guide <FA>`.

        Dimensionality of latent space, the number of components
        of ``X`` that are obtained after ``transform``.
        If None, n_components is set to the number of features.

    tol : float, default=1e-2
        Stopping tolerance for log-likelihood increase.

        Whether to make a copy of X. If ``False``, the input X gets overwritten
        during fitting.

    noise_variance_init : array-like of shape (n_features,), default=None
        The initial guess of the noise variance for each feature.
        If None, it defaults to np.ones(n_features).

    svd_method : {'lapack', 'randomized'}, default='randomized'
        Which SVD method to use. If 'lapack' use standard SVD from
        scipy.linalg, if 'randomized' use fast ``randomized_svd`` function.
        Defaults to 'randomized'. For most applications 'randomized' will
        be sufficiently precise while providing significant speed gains.
        Accuracy can also be improved by setting higher values for
        `iterated_power`. If this is not sufficient, for maximum precision
        you should choose 'lapack'.

    iterated_power : int, default=3
        Number of iterations for the power method. 3 by default. Only used
        if ``svd_method`` equals 'randomized'.

    rotation : {'varimax', 'quartimax'}, default=None
        If not None, apply the indicated rotation. Currently, varimax and
        quartimax are implemented. See
        `"The varimax criterion for analytic rotation in factor analysis"
        <https://link.springer.com/article/10.1007%2FBF02289233>`_
        H. F. Kaiser, 1958.

    random_state : int or RandomState instance, default=0
        Only used when ``svd_method`` equals 'randomized'. Pass an int for

        Components with maximum variance.

    loglike_ : list of shape (n_iterations,)
        The log likelihood at each iteration.

    noise_variance_ : ndarray of shape (n_features,)
        The estimated noise variance for each feature.

    See Also
    --------
    PCA: Principal component analysis is also a latent linear variable model
        which however assumes equal noise variance for each feature.
        This extra assumption makes probabilistic PCA faster as it can be
        computed in closed form.
    FastICA: Independent component analysis, a latent variable model with
        non-Gaussian latent variables.

    - David Barber, Bayesian Reasoning and Machine Learning,
      Algorithm 21.1.

    - Christopher M. Bishop: Pattern Recognition and Machine Learning,
      Chapter 12.2.4.

    Examples
    --------
    >>> from sklearn.decomposition import FactorAnalysis
    >>> transformer = FactorAnalysis(n_components=7, random_state=0)
    (1797, 7)
    """

        "n_components": [Interval(Integral, 0, None, closed="left"), None],
        "noise_variance_init": ["array-like", None],
        "svd_method": [StrOptions({"randomized", "lapack"})],
        "iterated_power": [Interval(Integral, 0, None, closed="left")],
        "rotation": [StrOptions({"varimax", "quartimax"}), None],
    }

        self,
        *,
        tol=1e-2,
        noise_variance_init=None,
        svd_method="randomized",
        iterated_power=3,
        rotation=None,
    ):
        self.svd_method = svd_method

        self.noise_variance_init = noise_variance_init
        self.rotation = rotation

        """Fit the FactorAnalysis model to X using SVD based approach.

        y : Ignored

        Returns
        -------
            FactorAnalysis class instance.
        """
            self, X, copy=self.copy, dtype=np.float64, force_writeable=True
        )

        n_components = self.n_components
        if n_components is None:
            n_components = n_features

        self.mean_ = np.mean(X, axis=0)
        X -= self.mean_

        # some constant terms
        nsqrt = sqrt(n_samples)
        llconst = n_features * log(2.0 * np.pi) + n_components
        var = np.var(X, axis=0)

        if self.noise_variance_init is None:
            psi = np.ones(n_features, dtype=X.dtype)
        else:
            if len(self.noise_variance_init) != n_features:
                    "noise_variance_init dimension does not "
                    "with number of features : %d != %d"
                    % (len(self.noise_variance_init), n_features)
                )
            psi = np.array(self.noise_variance_init)

        loglike = []
        old_ll = -np.inf
        SMALL = 1e-12

        # we'll modify svd outputs to return unexplained variance
        # to allow for unified computation of loglikelihood
        if self.svd_method == "lapack":

            def my_svd(X):
                _, s, Vt = linalg.svd(X, full_matrices=False, check_finite=False)
                return (
                    s[:n_components],
                    Vt[:n_components],
                    squared_norm(s[n_components:]),
                )

        else:  # svd_method == "randomized"
            random_state = check_random_state(self.random_state)

                _, s, Vt = _randomized_svd(
                    X,
                    n_components,
                    n_iter=self.iterated_power,
                )
                return s, Vt, squared_norm(X) - squared_norm(s)

        for i in range(self.max_iter):
            # SMALL helps numerics
            sqrt_psi = np.sqrt(psi) + SMALL
            s, Vt, unexp_var = my_svd(X / (sqrt_psi * nsqrt))
            s **= 2
            # Use 'maximum' here to avoid sqrt problems.
            W = np.sqrt(np.maximum(s - 1.0, 0.0))[:, np.newaxis] * Vt
            del Vt
            W *= sqrt_psi

            # loglikelihood
            ll = llconst + np.sum(np.log(s))
            ll += unexp_var + np.sum(np.log(psi))
            ll *= -n_samples / 2.0
            loglike.append(ll)
            if (ll - old_ll) < self.tol:
                break
            old_ll = ll

            psi = np.maximum(var - np.sum(W**2, axis=0), SMALL)
        else:
                "FactorAnalysis did not converge."
                " You might want"
                " to increase the number of iterations.",
            )

        self.components_ = W
        if self.rotation is not None:
            self.components_ = self._rotate(W)
        self.noise_variance_ = psi
        self.loglike_ = loglike
        self.n_iter_ = i + 1

        """Apply dimensionality reduction to X using the model.

        Compute the expected mean of the latent variables.
        See Barber, 21.2.33 (or Bishop, 12.66).

        Returns
        -------
            The latent variables of X.
        """

        Ih = np.eye(len(self.components_))

        X_transformed = X - self.mean_

        Wpsi = self.components_ / self.noise_variance_
        cov_z = linalg.inv(Ih + np.dot(Wpsi, self.components_.T))
        tmp = np.dot(X_transformed, Wpsi.T)
        X_transformed = np.dot(tmp, cov_z)

        """Compute data covariance with the FactorAnalysis model.

        ``cov = components_.T * components_ + diag(noise_variance)``

        Returns
        -------
        cov : ndarray of shape (n_features, n_features)
        """

        cov = np.dot(self.components_.T, self.components_)
        cov.flat[:: len(cov) + 1] += self.noise_variance_  # modify diag inplace
        return cov

        """Compute data precision matrix with the FactorAnalysis model.

        Returns
        -------
        precision : ndarray of shape (n_features, n_features)
        """

        if self.n_components == 0:
            return np.diag(1.0 / self.noise_variance_)
        if self.n_components == n_features:
            return linalg.inv(self.get_covariance())

        precision = np.dot(components_ / self.noise_variance_, components_.T)
        precision.flat[:: len(precision) + 1] += 1.0
        precision = np.dot(components_.T, np.dot(linalg.inv(precision), components_))
        precision /= self.noise_variance_[:, np.newaxis]
        precision /= -self.noise_variance_[np.newaxis, :]
        precision.flat[:: len(precision) + 1] += 1.0 / self.noise_variance_

        """Compute the log-likelihood of each sample.

            The data.

        Returns
        -------
        """
        log_like = -0.5 * (Xr * (np.dot(Xr, precision))).sum(axis=1)

        """Compute the average log-likelihood of the samples.

            The data.

        y : Ignored

        Returns
        -------
        ll : float
        """
        return np.mean(self.score_samples(X))

    def _rotate(self, components, n_components=None, tol=1e-6):
        "Rotate the factor analysis solution."
        # note that tol is not exposed
        return _ortho_rotation(components.T, method=self.rotation, tol=tol)[
            : self.n_components
        ]

    @property

def _ortho_rotation(components, method="varimax", tol=1e-6, max_iter=100):
    """Return rotated components."""
    nrow, ncol = components.shape
    rotation_matrix = np.eye(ncol)
    var = 0

    for _ in range(max_iter):
        comp_rot = np.dot(components, rotation_matrix)
        if method == "varimax":
            tmp = comp_rot * np.transpose((comp_rot**2).sum(axis=0) / nrow)
        elif method == "quartimax":
            tmp = 0
        u, s, v = np.linalg.svd(np.dot(components.T, comp_rot**3 - tmp))
        rotation_matrix = np.dot(u, v)
        var_new = np.sum(s)
        if var != 0 and var_new < var * (1 + tol):
            break
        var = var_new

    return np.dot(components, rotation_matrix).T

        self,
        *,
        tol=1e-2,
    ):

        y : Ignored

        Returns
        -------
        """
        )

        else:
                )

                return (
                )

                    X,
                )

            s **= 2
            del Vt

                break
            old_ll = ll

        else:
            )

        Returns
        -------
        """

        Returns
        -------
        """

        return cov

        Returns
        -------
        """

            The data.

        Returns
        -------
        """

            The data.

        y : Ignored

        Returns
        -------
        ll : float
        """

        ]

                return (
                )

                    X,
                )

def _gs_decorrelation(w, W, j):
    """
    Orthonormalize w wrt the first j rows of W.

    w : ndarray of shape (n,)
        Array to be orthogonalized

    W : ndarray of shape (p, n)
        Null space definition

    j : int < p
        The no of (from the first) rows of Null space W wrt which w is
        orthogonalized.

    Notes
    -----
    Assumes that W is orthogonal
    w changed in place
    """
    w -= np.linalg.multi_dot([w, W[:j].T, W[:j]])
    return w

def _sym_decorrelation(W):
    """Symmetric decorrelation
    i.e. W <- (W * W.T) ^{-1/2} * W
    """
    s, u = linalg.eigh(np.dot(W, W.T))
    # Avoid sqrt of negative values because of rounding errors. Note that
    # np.sqrt(tiny) is larger than tiny and therefore this clipping also
    # prevents division by zero in the next step.
    s = np.clip(s, a_min=np.finfo(W.dtype).tiny, a_max=None)

    # u (resp. s) contains the eigenvectors (resp. square roots of
    # the eigenvalues) of W * W.T
    return np.linalg.multi_dot([u * (1.0 / np.sqrt(s)), u.T, W])

def _ica_def(X, tol, g, fun_args, max_iter, w_init):
    """Deflationary FastICA using fun approx to neg-entropy function

    Used internally by FastICA.
    """

    n_components = w_init.shape[0]
    W = np.zeros((n_components, n_components), dtype=X.dtype)
    n_iter = []

    # j is the index of the extracted component
    for j in range(n_components):
        w = w_init[j, :].copy()
        w /= np.sqrt((w**2).sum())

            gwtx, g_wtx = g(np.dot(w.T, X), fun_args)

            w1 = (X * gwtx).mean(axis=1) - g_wtx.mean() * w

            _gs_decorrelation(w1, W, j)

            w1 /= np.sqrt((w1**2).sum())

            lim = np.abs(np.abs((w1 * w).sum()) - 1)
            w = w1
            if lim < tol:
                break

        n_iter.append(i + 1)
        W[j, :] = w

    return W, max(n_iter)

def _ica_par(X, tol, g, fun_args, max_iter, w_init):
    """Parallel FastICA.

    Used internally by FastICA --main loop

    """
    W = _sym_decorrelation(w_init)
    del w_init
    p_ = float(X.shape[1])
    for ii in range(max_iter):
        gwtx, g_wtx = g(np.dot(W, X), fun_args)
        W1 = _sym_decorrelation(np.dot(gwtx, X.T) / p_ - g_wtx[:, np.newaxis] * W)
        del gwtx, g_wtx
        # builtin max, abs are faster than numpy counter parts.
        # np.einsum allows having the lowest memory footprint.
        # It is faster than np.diag(np.dot(W1, W.T)).
        lim = max(abs(abs(np.einsum("ij,ij->i", W1, W)) - 1))
        W = W1
        if lim < tol:
            break
    else:
            (
                "FastICA did not converge. Consider increasing "
                "tolerance or the maximum number of iterations."
            ),
            ConvergenceWarning,
        )

    return W, ii + 1

def _logcosh(x, fun_args=None):
    alpha = fun_args.get("alpha", 1.0)  # comment it out?

    x *= alpha
    gx = np.tanh(x, x)  # apply the tanh inplace
    g_x = np.empty(x.shape[0], dtype=x.dtype)
    # XXX compute in chunks to avoid extra allocation
    for i, gx_i in enumerate(gx):  # please don't vectorize.
        g_x[i] = (alpha * (1 - gx_i**2)).mean()
    return gx, g_x

def _exp(x, fun_args):
    exp = np.exp(-(x**2) / 2)
    gx = x * exp
    g_x = (1 - x**2) * exp
    return gx, g_x.mean(axis=-1)

def _cube(x, fun_args):
    return x**3, (3 * x**2).mean(axis=-1)

def fastica(
    X,
    n_components=None,
    *,
    algorithm="parallel",
    whiten="unit-variance",
    fun="logcosh",
    fun_args=None,
    tol=1e-04,
    w_init=None,
    whiten_solver="svd",
    return_X_mean=False,
    compute_sources=True,
):
    """Perform Fast Independent Component Analysis.

    The implementation is based on [1]_.

    Read more in the :ref:`User Guide <ICA>`.

        Training vector, where `n_samples` is the number of samples and

        Number of components to use. If None is passed, all are used.

    algorithm : {'parallel', 'deflation'}, default='parallel'
        Specify which algorithm to use for FastICA.

    whiten : str or bool, default='unit-variance'
        Specify the whitening strategy to use.

        - If 'arbitrary-variance', a whitening with variance
          arbitrary is used.
        - If 'unit-variance', the whitening matrix is rescaled to ensure that
          each recovered source has unit variance.
        - If False, the data is already considered to be whitened, and no
          whitening is performed.

            The default value of `whiten` changed to 'unit-variance' in 1.3.

    fun : {'logcosh', 'exp', 'cube'} or callable, default='logcosh'
        The functional form of the G function used in the
        approximation to neg-entropy. Could be either 'logcosh', 'exp',
        or 'cube'.
        You can also provide your own function. It should return a tuple
        containing the value of the function, and of its derivative, in the
        point. The derivative should be averaged along its last dimension.
        Example::

            def my_g(x):
                return x ** 3, (3 * x ** 2).mean(axis=-1)

    fun_args : dict, default=None
        Arguments to send to the functional form.
        If empty or None and if fun='logcosh', fun_args will take value
        {'alpha' : 1.0}.

        A positive scalar giving the tolerance at which the
        un-mixing matrix is considered to have converged.

    w_init : ndarray of shape (n_components, n_components), default=None
        Initial un-mixing array. If `w_init=None`, then an array of values
        drawn from a normal distribution is used.

    whiten_solver : {"eigh", "svd"}, default="svd"
        The solver to use for whitening.

        - "svd" is more stable numerically if the problem is degenerate, and
          often faster when `n_samples <= n_features`.

        - "eigh" is generally more memory efficient when
          `n_samples >= n_features`, and can be faster when
          `n_samples >= 50 * n_features`.

        Used to initialize ``w_init`` when not specified, with a
        normal distribution. Pass an int, for reproducible results

    return_X_mean : bool, default=False
        If True, X_mean is returned too.

    compute_sources : bool, default=True
        If False, sources are not computed, but only the rotation matrix.
        This can save memory when working with big data. Defaults to True.

    Returns
    -------
    K : ndarray of shape (n_components, n_features) or None
        If whiten is 'True', K is the pre-whitening matrix that projects data
        onto the first n_components principal components. If whiten is 'False',
        K is 'None'.

    W : ndarray of shape (n_components, n_components)
        The square matrix that unmixes the data after whitening.
        The mixing matrix is the pseudo-inverse of matrix ``W K``
        if K is not None, else it is the inverse of W.

    S : ndarray of shape (n_samples, n_components) or None
        Estimated source matrix.

    X_mean : ndarray of shape (n_features,)
        The mean over features. Returned only if return_X_mean is True.

        If the algorithm is "deflation", n_iter is the
        maximum number of iterations run across all components. Else
        they are just the number of iterations taken to converge. This is
        returned only when return_n_iter is set to `True`.

    Notes
    -----
    The data matrix X is considered to be a linear combination of
    non-Gaussian (independent) components i.e. X = AS where columns of S
    contain the independent components and A is a linear mixing
    matrix. In short ICA attempts to `un-mix' the data by estimating an
    un-mixing matrix W where ``S = W K X.``
    While FastICA was proposed to estimate as many sources
    as features, it is possible to estimate less by setting
    n_components < n_features. It this case K is not a square matrix
    and the estimated A is the pseudo-inverse of ``W K``.

    This implementation was originally made for data of shape
    [n_features, n_samples]. Now the input is transposed
    before the algorithm is applied. This makes it slightly
    faster for Fortran-ordered input.

    .. [1] A. Hyvarinen and E. Oja, "Fast Independent Component Analysis",
           Algorithms and Applications, Neural Networks, 13(4-5), 2000,
           pp. 411-430.

    Examples
    --------
    >>> from sklearn.decomposition import fastica
    >>> K, W, S = fastica(X, n_components=7, random_state=0, whiten='unit-variance')
    >>> K.shape
    (7, 64)
    >>> W.shape
    (7, 7)
    >>> S.shape
    (1797, 7)
    """
    est = FastICA(
        whiten=whiten,
        fun=fun,
        fun_args=fun_args,
        tol=tol,
        w_init=w_init,
        whiten_solver=whiten_solver,
    )
    S = est._fit_transform(X, compute_sources=compute_sources)

    if est.whiten in ["unit-variance", "arbitrary-variance"]:
        K = est.whitening_
        X_mean = est.mean_
    else:
        K = None
        X_mean = None

    returned_values = [K, est._unmixing, S]
    if return_X_mean:
        returned_values.append(X_mean)
        returned_values.append(est.n_iter_)

    return returned_values

class FastICA(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """FastICA: a fast algorithm for Independent Component Analysis.

        or 'cube'.
        Example::

        Maximum number of iterations during fit.

    w_init : array-like of shape (n_components, n_components), default=None

        The linear operator to apply to the data to get the independent
        sources. This is equal to the unmixing matrix when ``whiten`` is
        False, and equal to ``np.dot(unmixing_matrix, self.whitening_)`` when
        ``whiten`` is True.

    mixing_ : ndarray of shape (n_features, n_components)
        The pseudo-inverse of ``components_``. It is the linear operator
        that maps independent sources to the data.

    mean_ : ndarray of shape(n_features,)
        The mean over features. Only set if `self.whiten` is True.

        they are just the number of iterations taken to converge.

    whitening_ : ndarray of shape (n_components, n_features)
        Only set if whiten is 'True'. This is the pre-whitening matrix
        that projects data onto the first `n_components` principal components.

    See Also
    --------
    PCA : Principal component analysis (PCA).
    IncrementalPCA : Incremental principal components analysis (IPCA).
    KernelPCA : Kernel Principal component analysis (KPCA).
    SparsePCA : Sparse Principal Components Analysis (SparsePCA).

    .. [1] A. Hyvarinen and E. Oja, Independent Component Analysis:

    Examples
    --------
    >>> from sklearn.decomposition import FastICA
    >>> transformer = FastICA(n_components=7,
    ...         random_state=0,
    ...         whiten='unit-variance')
    (1797, 7)
    """

        "algorithm": [StrOptions({"parallel", "deflation"})],
        "whiten": [
            StrOptions({"arbitrary-variance", "unit-variance"}),
            Options(bool, {False}),
        ],
        "fun": [StrOptions({"logcosh", "exp", "cube"}), callable],
        "fun_args": [dict, None],
        "w_init": ["array-like", None],
        "whiten_solver": [StrOptions({"eigh", "svd"})],
    }

        self,
        *,
        algorithm="parallel",
        whiten="unit-variance",
        fun="logcosh",
        fun_args=None,
        tol=1e-4,
        w_init=None,
        whiten_solver="svd",
    ):
        self.fun = fun
        self.fun_args = fun_args
        self.w_init = w_init
        self.whiten_solver = whiten_solver

    def _fit_transform(self, X, compute_sources=False):

        compute_sources : bool, default=False
            If False, sources are not computes but only the rotation matrix.
            This can save memory when working with big data. Defaults to False.

        Returns
        -------
        S : ndarray of shape (n_samples, n_components) or None
            Sources matrix. `None` if `compute_sources` is `False`.
        """
        XT = validate_data(
            self,
            X,
            copy=self.whiten,
            ensure_min_samples=2,
        ).T
        fun_args = {} if self.fun_args is None else self.fun_args

        alpha = fun_args.get("alpha", 1.0)
        if not 1 <= alpha <= 2:
            raise ValueError("alpha must be in [1,2]")

        if self.fun == "logcosh":
            g = _logcosh
        elif self.fun == "exp":
            g = _exp
        elif self.fun == "cube":
            g = _cube
        elif callable(self.fun):

            def g(x, fun_args):
                return self.fun(x, **fun_args)

        n_features, n_samples = XT.shape
        if not self.whiten and n_components is not None:
            n_components = None
            warnings.warn("Ignoring n_components with whiten=False.")

            n_components = min(n_samples, n_features)
        if n_components > min(n_samples, n_features):
                "n_components is too large: it will be set to %s" % n_components
            )

            # Centering the features of X
            X_mean = XT.mean(axis=-1)
            XT -= X_mean[:, np.newaxis]

            # Whitening and preprocessing by PCA
            if self.whiten_solver == "eigh":
                # Faster when num_samples >> n_features
                d, u = linalg.eigh(XT.dot(X))
                sort_indices = np.argsort(d)[::-1]
                eps = np.finfo(d.dtype).eps * 10
                degenerate_idx = d < eps
                if np.any(degenerate_idx):
                        "There are some small singular values, using "
                        "whiten_solver = 'svd' might lead to more "
                        "accurate results."
                    )
                d[degenerate_idx] = eps  # For numerical issues
                np.sqrt(d, out=d)
                d, u = d[sort_indices], u[:, sort_indices]
            elif self.whiten_solver == "svd":
                u, d = linalg.svd(XT, full_matrices=False, check_finite=False)[:2]

            # Give consistent eigenvectors for both svd solvers
            u *= np.sign(u[0])

            K = (u / d).T[:n_components]  # see (6.33) p.140
            del u, d
            X1 = np.dot(K, XT)
            # see (13.6) p.267 Here X1 is white and data
            # in X has been projected onto a subspace by PCA
            X1 *= np.sqrt(n_samples)
        else:
            # X must be casted to floats to avoid typing issues with numpy
            # 2.0 and the line below
            X1 = as_float_array(XT, copy=False)  # copy has been taken care of

        w_init = self.w_init
        if w_init is None:
            w_init = np.asarray(
                random_state.normal(size=(n_components, n_components)), dtype=X1.dtype
            )

        else:
            w_init = np.asarray(w_init)
            if w_init.shape != (n_components, n_components):
                    "w_init has invalid shape -- should be %(shape)s"
                    % {"shape": (n_components, n_components)}
                )

        kwargs = {
            "g": g,
            "fun_args": fun_args,
            "max_iter": self.max_iter,
            "w_init": w_init,
        }

        if self.algorithm == "parallel":
            W, n_iter = _ica_par(X1, **kwargs)
        elif self.algorithm == "deflation":
            W, n_iter = _ica_def(X1, **kwargs)
        del X1

        if compute_sources:
                S = np.linalg.multi_dot([W, K, XT]).T
            else:
                S = np.dot(W, XT).T
        else:
            S = None

            if self.whiten == "unit-variance":
                if not compute_sources:
                    S = np.linalg.multi_dot([W, K, XT]).T
                S_std = np.std(S, axis=0, keepdims=True)
                S /= S_std
                W /= S_std.T

            self.components_ = np.dot(W, K)
            self.mean_ = X_mean
            self.whitening_ = K
        else:
            self.components_ = W

        self.mixing_ = linalg.pinv(self.components_, check_finite=False)
        self._unmixing = W

        return S

        """Fit the model and recover the sources from X.

        y : Ignored

        Returns
        -------
            Estimated sources obtained by transforming the data with the
            estimated unmixing matrix.
        """
        return self._fit_transform(X, compute_sources=True)

        """Fit the model to X.

        y : Ignored

        Returns
        -------
        """
        self._fit_transform(X, compute_sources=False)

        """Recover the sources from X (apply the unmixing matrix).

            Data to transform, where `n_samples` is the number of samples

            If False, data passed to fit can be overwritten. Defaults to True.

        Returns
        -------
        """

            self,
            X,
            copy=(copy and self.whiten),
        )
            X -= self.mean_

    def inverse_transform(self, X, copy=True):
        """Transform the sources back to the mixed data (apply mixing matrix).

            Sources, where `n_samples` is the number of samples
            If False, data passed to fit are overwritten. Defaults to True.

        Returns
        -------
            Reconstructed data obtained with the mixing matrix.
        """

        X = check_array(X, copy=(copy and self.whiten), dtype=[np.float64, np.float32])
        X = np.dot(X, self.mixing_.T)
            X += self.mean_

        return X

    @property

        self,
        *,
        tol=1e-4,
    ):

        Returns
        -------
        """
            self,
            X,
        ).T

            g = _exp
            g = _cube

            )

                    )

            del u, d
        else:

            )

        else:
                )

        kwargs = {
            "g": g,
        }

        del X1

            else:
        else:
            S = None

                S /= S_std

        else:

        return S

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

            self,
            X,
        )

        Returns
        -------
        """

        return X

class TruncatedSVD(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """Dimensionality reduction using truncated SVD (aka LSA).

    This transformer performs linear dimensionality reduction by means of
    truncated singular value decomposition (SVD). Contrary to PCA, this
    estimator does not center the data before computing the singular value
    decomposition. This means it can work with sparse matrices
    efficiently.

    In particular, truncated SVD works on term count/tf-idf matrices as
    returned by the vectorizers in :mod:`sklearn.feature_extraction.text`. In
    that context, it is known as latent semantic analysis (LSA).

    This estimator supports two algorithms: a fast randomized SVD solver, and
    a "naive" algorithm that uses ARPACK as an eigensolver on `X * X.T` or
    `X.T * X`, whichever is more efficient.

    Read more in the :ref:`User Guide <LSA>`.

    n_components : int, default=2
        Desired dimensionality of output data.
        If algorithm='arpack', must be strictly less than the number of features.
        If algorithm='randomized', must be less than or equal to the number of features.
        The default value is useful for visualisation. For LSA, a value of
        100 is recommended.

    algorithm : {'arpack', 'randomized'}, default='randomized'
        SVD solver to use. Either "arpack" for the ARPACK wrapper in SciPy
        (scipy.sparse.linalg.svds), or "randomized" for the randomized
        algorithm due to Halko (2009).

    n_iter : int, default=5
        Number of iterations for randomized SVD solver. Not used by ARPACK. The
        default is larger than the default in
        :func:`~sklearn.utils.extmath.randomized_svd` to handle sparse
        matrices that may have large slowly decaying spectrum.

        Number of oversamples for randomized SVD solver. Not used by ARPACK.
        See :func:`~sklearn.utils.extmath.randomized_svd` for a complete
        description.

        Used during randomized svd. Pass an int for reproducible results across
        multiple function calls.

        Tolerance for ARPACK. 0 means machine precision. Ignored by randomized
        SVD solver.

        The right singular vectors of the input data.

        The variance of the training samples transformed by a projection to
        each component.

    See Also
    --------
    FactorAnalysis : A simple linear generative model with
        Gaussian latent variables.
    KernelPCA : Kernel Principal component analysis.

    Notes
    -----
    SVD suffers from a problem called "sign indeterminacy", which means the
    sign of the ``components_`` and the output from transform depend on the
    algorithm and random state. To work around this, fit instances of this
    class to data once, then keep the instance around to do transformations.

    :arxiv:`Halko, et al. (2009). "Finding structure with randomness:
    Stochastic algorithms for constructing approximate matrix decompositions"
    <0909.4061>`

    Examples
    --------
    >>> from sklearn.decomposition import TruncatedSVD
    >>> X_dense = np.random.rand(100, 100)
    >>> X_dense[:, 2 * np.arange(50)] = 0
    >>> X = csr_matrix(X_dense)
    >>> svd = TruncatedSVD(n_components=5, n_iter=7, random_state=42)
    >>> svd.fit(X)
    TruncatedSVD(n_components=5, n_iter=7, random_state=42)
    >>> print(svd.explained_variance_ratio_)
    [0.0157... 0.0512... 0.0499... 0.0479... 0.0453...]
    >>> print(svd.explained_variance_ratio_.sum())
    0.2102...
    >>> print(svd.singular_values_)
    [35.2410...  4.5981...   4.5420...  4.4486...  4.3288...]
    """

        "algorithm": [StrOptions({"arpack", "randomized"})],
        "power_iteration_normalizer": [StrOptions({"auto", "OR", "LU", "none"})],
    }

        self,
        n_components=2,
        *,
        algorithm="randomized",
        n_iter=5,
        tol=0.0,
    ):

        """Fit model on training data X.

        y : Ignored

        Returns
        -------
            Returns the transformer object.
        """
        self.fit_transform(X)

        """Fit model to X and perform dimensionality reduction on X.

        y : Ignored

        Returns
        -------
            Reduced version of X. This will always be a dense array.
        """
        X = validate_data(self, X, accept_sparse=["csr", "csc"], ensure_min_features=2)

        if self.algorithm == "arpack":
            U, Sigma, VT = svds(X, k=self.n_components, tol=self.tol, v0=v0)
            Sigma = Sigma[::-1]
            # u_based_decision=False is needed to be consistent with PCA.
            U, VT = svd_flip(U[:, ::-1], VT[::-1], u_based_decision=False)

        elif self.algorithm == "randomized":
            if self.n_components > X.shape[1]:
                    f"n_components({self.n_components}) must be <="
                    f" n_features({X.shape[1]})."
                )
            U, Sigma, VT = _randomized_svd(
                X,
                n_iter=self.n_iter,
            )
            U, VT = svd_flip(U, VT, u_based_decision=False)

        self.components_ = VT

        # As a result of the SVD approximation error on X ~ U @ Sigma @ V.T,
        # X @ V is not the same as U @ Sigma
        if self.algorithm == "randomized" or (
            self.algorithm == "arpack" and self.tol > 0
        ):
            X_transformed = safe_sparse_dot(X, self.components_.T)
        else:
            X_transformed = U * Sigma

        # Calculate explained variance & explained variance ratio
        self.explained_variance_ = exp_var = np.var(X_transformed, axis=0)
            _, full_var = mean_variance_axis(X, axis=0)
            full_var = full_var.sum()
        else:
            full_var = np.var(X, axis=0).sum()
        self.explained_variance_ratio_ = exp_var / full_var
        self.singular_values_ = Sigma  # Store the singular values.

        """Perform dimensionality reduction on X.

            New data.

        Returns
        -------
        """
        X = validate_data(self, X, accept_sparse=["csr", "csc"], reset=False)
        return safe_sparse_dot(X, self.components_.T)

        """Transform X back to its original space.

        Returns an array X_original whose transform would be X.

            New data.

        Returns
        -------
            Note that this is always a dense array.
        """
        return np.dot(X, self.components_)

    @property

        self,
        *,
        n_iter=5,
        tol=0.0,
    ):

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """

                )
                X,
            )

        ):
        else:

        else:

            New data.

        Returns
        -------
        """

            New data.

        Returns
        -------
        """

def _check_positive_coding(method, positive):
    if positive and method in ["omp", "lars"]:
            "Positive constraint not supported for '{}' coding method.".format(method)
        )

def _sparse_encode_precomputed(
    X,
    dictionary,
    *,
    gram=None,
    cov=None,
    algorithm="lasso_lars",
    regularization=None,
    copy_cov=True,
    max_iter=1000,
    positive=False,
):
    """Generic sparse coding with precomputed Gram and/or covariance matrices.

    Each row of the result is the solution to a Lasso problem.

        Data matrix.

        The dictionary matrix against which to solve the sparse coding of
        the data. Some of the algorithms assume normalized rows.

    gram : ndarray of shape (n_components, n_components), default=None
        Precomputed Gram matrix, `dictionary * dictionary'`
        gram can be `None` if method is 'threshold'.

    cov : ndarray of shape (n_components, n_samples), default=None
        Precomputed covariance, `dictionary * X'`.

    algorithm : {'lasso_lars', 'lasso_cd', 'lars', 'omp', 'threshold'}, \
            default='lasso_lars'
        The algorithm used:

        * `'lars'`: uses the least angle regression method
          (`linear_model.lars_path`);
        * `'lasso_lars'`: uses Lars to compute the Lasso solution;
        * `'lasso_cd'`: uses the coordinate descent method to compute the
          Lasso solution (`linear_model.Lasso`). lasso_lars will be faster if
          the estimated components are sparse;
        * `'omp'`: uses orthogonal matching pursuit to estimate the sparse
          solution;
        * `'threshold'`: squashes to zero all coefficients less than
          regularization from the projection `dictionary * data'`.

    regularization : int or float, default=None
        The regularization parameter. It corresponds to alpha when
        algorithm is `'lasso_lars'`, `'lasso_cd'` or `'threshold'`.
        Otherwise it corresponds to `n_nonzero_coefs`.

    init : ndarray of shape (n_samples, n_components), default=None
        Initialization value of the sparse code. Only used if
        `algorithm='lasso_cd'`.

        Maximum number of iterations to perform if `algorithm='lasso_cd'` or
        `'lasso_lars'`.

    copy_cov : bool, default=True
        Whether to copy the precomputed covariance matrix; if `False`, it may
        be overwritten.

        Controls the verbosity; the higher, the more messages.

    positive: bool, default=False
        Whether to enforce a positivity constraint on the sparse code.

    Returns
    -------
    code : ndarray of shape (n_components, n_features)
        The sparse codes.
    """
    n_components = dictionary.shape[0]

    if algorithm == "lasso_lars":
        alpha = float(regularization) / n_features  # account for scaling
        try:
            err_mgt = np.seterr(all="ignore")

            # Not passing in verbose=max(0, verbose-1) because Lars.fit already
            # corrects the verbosity level.
            lasso_lars = LassoLars(
                fit_intercept=False,
                verbose=verbose,
                precompute=gram,
                fit_path=False,
                positive=positive,
            )
            lasso_lars.fit(dictionary.T, X.T, Xy=cov)
            new_code = lasso_lars.coef_
        finally:
            np.seterr(**err_mgt)

    elif algorithm == "lasso_cd":

        # TODO: Make verbosity argument for Lasso?
        # sklearn.linear_model.coordinate_descent.enet_path has a verbosity
        # argument that we could pass in from Lasso.
        clf = Lasso(
            fit_intercept=False,
            precompute=gram,
            positive=positive,
        )

        if init is not None:
            # In some workflows using coordinate descent algorithms:
            #  - users might provide NumPy arrays with read-only buffers
            #  - `joblib` might memmap arrays making their buffer read-only
            # TODO: move this handling (which is currently too broad)
            # closer to the actual private function which need buffers to be writable.
            if not init.flags["WRITEABLE"]:
                init = np.array(init)
            clf.coef_ = init

        clf.fit(dictionary.T, X.T, check_input=False)
        new_code = clf.coef_

    elif algorithm == "lars":
        try:

            lars = Lars(
                n_nonzero_coefs=int(regularization),
            )
            lars.fit(dictionary.T, X.T, Xy=cov)
            new_code = lars.coef_
        finally:

    elif algorithm == "threshold":
        new_code = (np.sign(cov) * np.maximum(np.abs(cov) - regularization, 0)).T
        if positive:
            np.clip(new_code, 0, None, out=new_code)

    elif algorithm == "omp":
        new_code = orthogonal_mp_gram(
            Gram=gram,
            Xy=cov,
            n_nonzero_coefs=int(regularization),
            tol=None,
            norms_squared=row_norms(X, squared=True),
            copy_Xy=copy_cov,
        ).T

    return new_code.reshape(n_samples, n_components)

def sparse_encode(
    X,
    *,
    cov=None,
    n_nonzero_coefs=None,
    alpha=None,
    check_input=True,
):
    """Sparse coding.

    Each row of the result is the solution to a sparse coding problem.
    The goal is to find a sparse array `code` such that::

        X ~= code * dictionary

    Read more in the :ref:`User Guide <SparseCoder>`.

    dictionary : array-like of shape (n_components, n_features)
        the data. Some of the algorithms assume normalized rows for meaningful
        output.

    gram : array-like of shape (n_components, n_components), default=None
        Precomputed Gram matrix, `dictionary * dictionary'`.

    cov : array-like of shape (n_components, n_samples), default=None
        Precomputed covariance, `dictionary' * X`.

          solution;

    n_nonzero_coefs : int, default=None
        Number of nonzero coefficients to target in each column of the
        solution. This is only used by `algorithm='lars'` and `algorithm='omp'`
        and is overridden by `alpha` in the `omp` case. If `None`, then
        `n_nonzero_coefs=int(n_features / 10)`.

    alpha : float, default=None
        If `algorithm='lasso_lars'` or `algorithm='lasso_cd'`, `alpha` is the
        penalty applied to the L1 norm.
        If `algorithm='threshold'`, `alpha` is the absolute value of the
        threshold below which coefficients will be squashed to zero.
        If `algorithm='omp'`, `alpha` is the tolerance parameter: the value of
        the reconstruction error targeted. In this case, it overrides
        `n_nonzero_coefs`.
        If `None`, default to 1.

        Initialization value of the sparse codes. Only used if

    check_input : bool, default=True
        If `False`, the input arrays X and dictionary will not be checked.

    positive : bool, default=False
        Whether to enforce positivity when finding the encoding.

    Returns
    -------

    See Also
    --------
    sklearn.linear_model.lars_path : Compute Least Angle Regression or Lasso
        path using LARS algorithm.
    sklearn.linear_model.orthogonal_mp : Solves Orthogonal Matching Pursuit problems.
    sklearn.linear_model.Lasso : Train Linear Model with L1 prior as regularizer.
    SparseCoder : Find a sparse representation of data from a fixed precomputed
        dictionary.

    Examples
    --------
    >>> from sklearn.decomposition import sparse_encode
    >>> X = np.array([[-1, -1, -1], [0, 0, 3]])
    >>> dictionary = np.array(
    ...     [[0, 1, 0],
    ...      [-1, -1, 2],
    ...      [1, 1, 1],
    ...      [0, 1, 1],
    ...      [0, 2, 1]],
    ...    dtype=np.float64
    ... )
    >>> sparse_encode(X, dictionary, alpha=1e-10)
    array([[ 0.,  0., -1.,  0.,  0.],
           [ 0.,  1.,  1.,  0.,  0.]])
    """
    if check_input:
        if algorithm == "lasso_cd":
            dictionary = check_array(
                dictionary, order="C", dtype=[np.float64, np.float32]
            )
            X = check_array(X, order="C", dtype=[np.float64, np.float32])
        else:
            dictionary = check_array(dictionary)
            X = check_array(X)

    if dictionary.shape[1] != X.shape[1]:
            "Dictionary and X have different numbers of features:"
            "dictionary.shape: {} X.shape{}".format(dictionary.shape, X.shape)
        )

    _check_positive_coding(algorithm, positive)

    return _sparse_encode(
        X,
        dictionary,
        gram=gram,
        cov=cov,
        n_nonzero_coefs=n_nonzero_coefs,
        copy_cov=copy_cov,
        positive=positive,
    )

def _sparse_encode(
    X,
    *,
    cov=None,
):
    """Sparse coding without input/parameter validation."""

    if algorithm in ("lars", "omp"):
        regularization = n_nonzero_coefs
        if regularization is None:
            regularization = min(max(n_features / 10, 1), n_components)
    else:
        regularization = alpha
            regularization = 1.0

    if gram is None and algorithm != "threshold":
        gram = np.dot(dictionary, dictionary.T)

    if cov is None and algorithm != "lasso_cd":
        copy_cov = False
        cov = np.dot(dictionary, X.T)

    if effective_n_jobs(n_jobs) == 1 or algorithm == "threshold":
        code = _sparse_encode_precomputed(
            X,
            dictionary,
            gram=gram,
            cov=cov,
            regularization=regularization,
            copy_cov=copy_cov,
        )
        return code

    # Enter parallel code block
    code = np.empty((n_samples, n_components))
    slices = list(gen_even_slices(n_samples, effective_n_jobs(n_jobs)))

    code_views = Parallel(n_jobs=n_jobs, verbose=verbose)(
        delayed(_sparse_encode_precomputed)(
            X[this_slice],
            cov=cov[:, this_slice] if cov is not None else None,
            init=init[this_slice] if init is not None else None,
        )
        for this_slice in slices
    )
    for this_slice, this_view in zip(slices, code_views):
        code[this_slice] = this_view
    return code

def _update_dict(
    Y,
    code,
    A=None,
    B=None,
):
    """Update the dense dictionary factor in place.

        Value of the dictionary at the previous iteration.

        Sparse coding of the data against which to optimize the dictionary.

    A : ndarray of shape (n_components, n_components), default=None
        Together with `B`, sufficient stats of the online model to update the

    B : ndarray of shape (n_features, n_components), default=None
        Together with `A`, sufficient stats of the online model to update the

    verbose: bool, default=False
        Degree of output the procedure will print.

        Used for randomly initializing the dictionary. Pass an int for

        Whether to enforce positivity when finding the dictionary.

    """
    n_samples, n_components = code.shape

    if A is None:
        A = code.T @ code
    if B is None:
        B = Y.T @ code

    n_unused = 0

    for k in range(n_components):
        if A[k, k] > 1e-6:
            # 1e-6 is arbitrary but consistent with the spams implementation
            dictionary[k] += (B[:, k] - A[k] @ dictionary) / A[k, k]
        else:
            # kth atom is almost never used -> sample a new one from the data
            newd = Y[random_state.choice(n_samples)]

            # add small noise to avoid making the sparse coding ill conditioned
            noise_level = 0.01 * (newd.std() or 1)  # avoid 0 std
            noise = random_state.normal(0, noise_level, size=len(newd))

            dictionary[k] = newd + noise
            code[:, k] = 0
            n_unused += 1

            np.clip(dictionary[k], 0, None, out=dictionary[k])

        # Projection on the constraint set ||V_k|| <= 1
        dictionary[k] /= max(linalg.norm(dictionary[k]), 1)

    if verbose and n_unused > 0:
        print(f"{n_unused} unused atoms resampled.")

def _dict_learning(
    X,
    *,
    alpha,
    max_iter,
    tol,
    method,
    n_jobs,
    dict_init,
    code_init,
    callback,
    verbose,
    return_n_iter,
    positive_dict,
    positive_code,
    method_max_iter,
):
    """Main dictionary learning algorithm"""
    t0 = time.time()
    # Init the code and the dictionary with SVD of Y
    if code_init is not None and dict_init is not None:
        code = np.array(code_init, order="F")
        # Don't copy V, it will happen below
        dictionary = dict_init
    else:
        code, S, dictionary = linalg.svd(X, full_matrices=False)
        # flip the initial code's sign to enforce deterministic output
        code, dictionary = svd_flip(code, dictionary)
        dictionary = S[:, np.newaxis] * dictionary
    r = len(dictionary)
    if n_components <= r:  # True even if n_components=None
        code = code[:, :n_components]
        dictionary = dictionary[:n_components, :]
    else:
        code = np.c_[code, np.zeros((len(code), n_components - r))]
        dictionary = np.r_[
            dictionary, np.zeros((n_components - r, dictionary.shape[1]))
        ]

    # Fortran-order dict better suited for the sparse coding which is the
    # bottleneck of this algorithm.
    dictionary = np.asfortranarray(dictionary)

    errors = []
    current_cost = np.nan

    if verbose == 1:
        print("[dict_learning]", end=" ")

    # If max_iter is 0, number of iterations returned should be zero
    ii = -1

        dt = time.time() - t0
            sys.stdout.write(".")
        elif verbose:
            print(
                "Iteration % 3i (elapsed time: % 3is, % 4.1fmn, current cost % 7.3f)"
                % (ii, dt, dt / 60, current_cost)
            )

        # Update code
        code = sparse_encode(
            X,
            algorithm=method,
            init=code,
            positive=positive_code,
            max_iter=method_max_iter,
        )

        # Update dictionary in place
        _update_dict(
            X,
            code,
            positive=positive_dict,
        )

        # Cost function
        current_cost = 0.5 * np.sum((X - code @ dictionary) ** 2) + alpha * np.sum(
            np.abs(code)
        )
        errors.append(current_cost)

        if ii > 0:
            dE = errors[-2] - errors[-1]
            # assert(dE >= -tol * errors[-1])
            if dE < tol * errors[-1]:
                if verbose == 1:
                    # A line return
                    print("")
                elif verbose:
                    print("--- Convergence reached after %d iterations" % ii)
                break
        if ii % 5 == 0 and callback is not None:
            callback(locals())

        return code, dictionary, errors, ii + 1
    else:
        return code, dictionary, errors

def dict_learning_online(
    X,
    n_components=2,
    *,
    alpha=1,
    return_code=True,
    dict_init=None,
    callback=None,
    batch_size=256,
    method="lars",
    positive_dict=False,
    positive_code=False,
    method_max_iter=1000,
    tol=1e-3,
    max_no_improvement=10,
):
    """Solve a dictionary learning matrix factorization problem online.

    Finds the best dictionary and the corresponding sparse code for
    approximating the data matrix X by solving::

        (U^*, V^*) = argmin 0.5 || X - U V ||_Fro^2 + alpha * || U ||_1,1
                     (U,V)
                     with || V_k ||_2 = 1 for all  0 <= k < n_components

    where V is the dictionary and U is the sparse code. ||.||_Fro stands for
    the Frobenius norm and ||.||_1,1 stands for the entry-wise matrix norm
    which is the sum of the absolute values of all the entries in the matrix.
    This is accomplished by repeatedly iterating over mini-batches by slicing
    the input data.

    Read more in the :ref:`User Guide <DictionaryLearning>`.

    n_components : int or None, default=2
        Number of dictionary atoms to extract. If None, then ``n_components``

        Sparsity controlling parameter.

    return_code : bool, default=True
        Whether to also return the code U or just the dictionary `V`.

    dict_init : ndarray of shape (n_components, n_features), default=None
        Initial values for the dictionary for warm restart scenarios.
        If `None`, the initial values for the dictionary are created
        with an SVD decomposition of the data via
        :func:`~sklearn.utils.extmath.randomized_svd`.

        A callable that gets invoked at the end of each iteration.

    batch_size : int, default=256
        The number of samples to take in each batch.

           The default value of `batch_size` changed from 3 to 256 in version 1.3.

        To control the verbosity of the procedure.

        * `'lars'`: uses the least angle regression method to solve the lasso
          problem (`linear_model.lars_path`);
        * `'cd'`: uses the coordinate descent method to compute the
          Lasso solution (`linear_model.Lasso`). Lars will be faster if
          the estimated components are sparse.

        Used for initializing the dictionary when ``dict_init`` is not
        specified, randomly shuffling the data when ``shuffle`` is set to
        ``True``, and updating the dictionary. Pass an int for reproducible

    positive_dict : bool, default=False

    positive_code : bool, default=False
        Whether to enforce positivity when finding the code.

    method_max_iter : int, default=1000
        Maximum number of iterations to perform when solving the lasso problem.

    Returns
    -------
    code : ndarray of shape (n_samples, n_components),
        The sparse code (only returned if `return_code=True`).

    dictionary : ndarray of shape (n_components, n_features),
        The solutions to the dictionary learning problem.

        Number of iterations run. Returned only if `return_n_iter` is
        set to `True`.

    See Also
    --------
    dict_learning : Solve a dictionary learning matrix factorization problem.
    MiniBatchDictionaryLearning : A faster, less accurate, version of the dictionary
        learning algorithm.

    Examples
    --------
    >>> from sklearn.decomposition import dict_learning_online
    >>> X, _, _ = make_sparse_coded_signal(
    ...     n_samples=30, n_components=15, n_features=20, n_nonzero_coefs=10,
    ...     random_state=42,
    ... )
    >>> U, V = dict_learning_online(
    ...     X, n_components=15, alpha=0.2, max_iter=20, batch_size=3, random_state=42
    ... )

    We can check the level of sparsity of `U`:

    >>> np.mean(U == 0)
    np.float64(0.53...)

    We can compare the average squared euclidean norm of the reconstruction
    error of the sparse coded signal relative to the squared euclidean norm of
    the original signal:

    >>> X_hat = U @ V
    >>> np.mean(np.sum((X_hat - X) ** 2, axis=1) / np.sum(X ** 2, axis=1))
    np.float64(0.05...)
    """
    transform_algorithm = "lasso_" + method

    est = MiniBatchDictionaryLearning(
        fit_algorithm=method,
        batch_size=batch_size,
        dict_init=dict_init,
        transform_algorithm=transform_algorithm,
        transform_alpha=alpha,
        positive_code=positive_code,
        positive_dict=positive_dict,
        transform_max_iter=method_max_iter,
        callback=callback,
        tol=tol,
        max_no_improvement=max_no_improvement,
    ).fit(X)

    if not return_code:
        return est.components_
    else:
        code = est.transform(X)
        return code, est.components_

def dict_learning(
    X,
    *,
    alpha,
    tol=1e-8,
    code_init=None,
):
    """Solve a dictionary learning matrix factorization problem.

                     (U,V)
                    with || V_k ||_2 = 1 for all  0 <= k < n_components

        Number of dictionary atoms to extract.

    alpha : int or float

        The method used:

           problem (`linear_model.lars_path`);

        Initial value for the dictionary for warm restart scenarios. Only used
        if `code_init` and `dict_init` are not None.

    code_init : ndarray of shape (n_samples, n_components), default=None
        Initial value for the sparse code for warm restart scenarios. Only used

    Returns
    -------
        The sparse code factor in the matrix factorization.

        The dictionary factor in the matrix factorization.

    errors : array

        set to True.

    See Also
    --------
    dict_learning_online : Solve a dictionary learning matrix factorization
        problem online.
    MiniBatchDictionaryLearning : A faster, less accurate version
        of the dictionary learning algorithm.

    Examples
    --------
    >>> from sklearn.decomposition import dict_learning
    ... )
    >>> U, V, errors = dict_learning(X, n_components=15, alpha=0.1, random_state=42)

    np.float64(0.6...)

    np.float64(0.01...)
    """
    estimator = DictionaryLearning(
        tol=tol,
        code_init=code_init,
    ).set_output(transform="default")
    code = estimator.fit_transform(X)
        return (
            code,
            estimator.components_,
            estimator.error_,
            estimator.n_iter_,
        )
    return code, estimator.components_, estimator.error_

class _BaseSparseCoding(ClassNamePrefixFeaturesOutMixin, TransformerMixin):
    """Base class from SparseCoder and DictionaryLearning algorithms."""

        self,
        transform_algorithm,
        transform_n_nonzero_coefs,
        transform_alpha,
        split_sign,
        n_jobs,
        positive_code,
        transform_max_iter,
    ):
        self.transform_algorithm = transform_algorithm
        self.transform_n_nonzero_coefs = transform_n_nonzero_coefs
        self.transform_alpha = transform_alpha
        self.split_sign = split_sign
        self.positive_code = positive_code

    def _transform(self, X, dictionary):
        """Private method allowing to accommodate both DictionaryLearning and
        SparseCoder."""

        if hasattr(self, "alpha") and self.transform_alpha is None:
            transform_alpha = self.alpha
        else:
            transform_alpha = self.transform_alpha

            X,
            algorithm=self.transform_algorithm,
            n_nonzero_coefs=self.transform_n_nonzero_coefs,
            alpha=transform_alpha,
            max_iter=self.transform_max_iter,
            positive=self.positive_code,
        )

        if self.split_sign:
            # feature vector is split into a positive and negative side
            n_samples, n_features = code.shape
            split_code = np.empty((n_samples, 2 * n_features))
            split_code[:, :n_features] = np.maximum(code, 0)
            split_code[:, n_features:] = -np.minimum(code, 0)
            code = split_code

        """Encode the data as a sparse combination of the dictionary atoms.

        Coding method is determined by the object parameter
        `transform_algorithm`.

        Returns
        -------
        """
        return self._transform(X, self.components_)

    def _inverse_transform(self, code, dictionary):
        code = check_array(code)
        # compute number of expected features in code
        expected_n_components = dictionary.shape[0]
            expected_n_components += expected_n_components
        if not code.shape[1] == expected_n_components:
                "The number of components in the code is different from the "
                "number of components in the dictionary."
                f"Expected {expected_n_components}, got {code.shape[1]}."
            )
            n_features //= 2
            code = code[:, :n_features] - code[:, n_features:]

        return code @ dictionary

            Data to be transformed back. Must have the same number of
            components as the data used to train the model.

        Returns
        -------
        """
        return self._inverse_transform(X, self.components_)

class SparseCoder(_BaseSparseCoding, BaseEstimator):

    Finds a sparse representation of data against a fixed, precomputed
    dictionary.

        The dictionary atoms used for sparse coding. Lines are assumed to be
        normalized to unit norm.

    transform_algorithm : {'lasso_lars', 'lasso_cd', 'lars', 'omp', \
            'threshold'}, default='omp'
        Algorithm used to transform the data:

        - `'lars'`: uses the least angle regression method
        - `'lasso_lars'`: uses Lars to compute the Lasso solution;
        - `'lasso_cd'`: uses the coordinate descent method to compute the
          Lasso solution (linear_model.Lasso). `'lasso_lars'` will be faster if
        - `'omp'`: uses orthogonal matching pursuit to estimate the sparse
          solution;
        - `'threshold'`: squashes to zero all coefficients less than alpha from
          the projection ``dictionary * X'``.

    transform_n_nonzero_coefs : int, default=None
        `transform_n_nonzero_coefs=int(n_features / 10)`.

    transform_alpha : float, default=None

    split_sign : bool, default=False
        Whether to split the sparse feature vector into the concatenation of
        its negative part and its positive part. This can improve the
        performance of downstream classifiers.

    transform_max_iter : int, default=1000
        `lasso_lars`.

        Number of atoms.

    See Also
    --------
    MiniBatchDictionaryLearning : A faster, less accurate, version of the
        dictionary learning algorithm.
    sparse_encode : Sparse coding where each row of the result is the solution
        to a sparse coding problem.

    Examples
    --------
    >>> from sklearn.decomposition import SparseCoder
    ... )
    >>> coder = SparseCoder(
    ...     dictionary=dictionary, transform_algorithm='lasso_lars',
    ...     transform_alpha=1e-10,
    ... )
    >>> coder.transform(X)
    """

        self,
        *,
        transform_algorithm="omp",
        transform_n_nonzero_coefs=None,
        transform_alpha=None,
        split_sign=False,
        positive_code=False,
        transform_max_iter=1000,
    ):
            transform_algorithm,
            transform_n_nonzero_coefs,
            transform_alpha,
            split_sign,
            n_jobs,
            positive_code,
            transform_max_iter,
        )
        self.dictionary = dictionary

        """Do nothing and return the estimator unchanged.

        This method is just there to implement the usual API and hence
        work in pipelines.

        X : Ignored

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """
        return super()._transform(X, self.dictionary)

        Returns
        -------
        """
        return self._inverse_transform(X, self.dictionary)

    @property
    def n_components_(self):
        """Number of atoms."""
        return self.dictionary.shape[0]

    @property
        """Number of features seen during `fit`."""
        return self.dictionary.shape[1]

    @property
        return self.n_components_

class DictionaryLearning(_BaseSparseCoding, BaseEstimator):
    """Dictionary learning.

    Finds a dictionary (a set of atoms) that performs well at sparsely
    encoding the fitted data.

    Solves the optimization problem::

        (U^*,V^*) = argmin 0.5 || X - U V ||_Fro^2 + alpha * || U ||_1,1
                    (U,V)
                    with || V_k ||_2 <= 1 for all  0 <= k < n_components

    ||.||_Fro stands for the Frobenius norm and ||.||_1,1 stands for
    the entry-wise matrix norm which is the sum of the absolute values
    of all the entries in the matrix.

        Number of dictionary elements to extract. If None, then ``n_components``

        Tolerance for numerical error.

    fit_algorithm : {'lars', 'cd'}, default='lars'
          problem (:func:`~sklearn.linear_model.lars_path`);
          Lasso solution (:class:`~sklearn.linear_model.Lasso`). Lars will be
          faster if the estimated components are sparse.

           *cd* coordinate descent method to improve speed.

          (:func:`~sklearn.linear_model.lars_path`);
        - `'lasso_lars'`: uses Lars to compute the Lasso solution.
          Lasso solution (:class:`~sklearn.linear_model.Lasso`). `'lasso_lars'`
          will be faster if the estimated components are sparse.
          solution.

           *lasso_cd* coordinate descent method to improve speed.

        solution. This is only used by `algorithm='lars'` and
        `algorithm='omp'`. If `None`, then

        If `None`, defaults to `alpha`.

            When None, default value changed from 1.0 to `alpha`.

    n_jobs : int or None, default=None

        Initial value for the code, for warm restart. Only used if `code_init`
        and `dict_init` are not None.

        Initial values for the dictionary, for warm restart. Only used if
        `code_init` and `dict_init` are not None.

        dictionary atoms extracted from the data

    error_ : array
        vector of errors at each iteration

    See Also
    --------
    MiniBatchDictionaryLearning: A faster, less accurate, version of the

    J. Mairal, F. Bach, J. Ponce, G. Sapiro, 2009: Online dictionary learning
    for sparse coding (https://www.di.ens.fr/~fbach/mairal_icml09.pdf)

    Examples
    --------
    >>> from sklearn.decomposition import DictionaryLearning
    >>> X, dictionary, code = make_sparse_coded_signal(
    ... )
    >>> dict_learner = DictionaryLearning(
    ...     n_components=15, transform_algorithm='lasso_lars', transform_alpha=0.1,
    ... )
    >>> X_transformed = dict_learner.fit(X).transform(X)

    We can check the level of sparsity of `X_transformed`:

    >>> np.mean(X_transformed == 0)
    np.float64(0.52...)

    >>> X_hat = X_transformed @ dict_learner.components_
    """

        "fit_algorithm": [StrOptions({"lars", "cd"})],
        "transform_algorithm": [
            StrOptions({"lasso_lars", "lasso_cd", "lars", "omp", "threshold"})
        ],
        "transform_n_nonzero_coefs": [Interval(Integral, 1, None, closed="left"), None],
        "transform_alpha": [Interval(Real, 0, None, closed="left"), None],
        "code_init": [np.ndarray, None],
        "dict_init": [np.ndarray, None],
        "split_sign": ["boolean"],
        "positive_code": ["boolean"],
        "positive_dict": ["boolean"],
        "transform_max_iter": [Interval(Integral, 0, None, closed="left")],
    }

        self,
        *,
        alpha=1,
        tol=1e-8,
        fit_algorithm="lars",
        code_init=None,
        dict_init=None,
        positive_dict=False,
    ):
            n_jobs,
        )
        self.fit_algorithm = fit_algorithm
        self.code_init = code_init
        self.dict_init = dict_init
        self.positive_dict = positive_dict

        y : Ignored

        Returns
        -------
        """

        """Fit the model from data in X and return the transformed data.

        y : Ignored

        Returns
        -------
        V : ndarray of shape (n_samples, n_components)
        """
        _check_positive_coding(method=self.fit_algorithm, positive=self.positive_code)

        method = "lasso_" + self.fit_algorithm

        else:

        V, U, E, self.n_iter_ = _dict_learning(
            X,
            method_max_iter=self.transform_max_iter,
            code_init=self.code_init,
            dict_init=self.dict_init,
            positive_dict=self.positive_dict,
            positive_code=self.positive_code,
        )
        self.components_ = U

        return V

    @property

class MiniBatchDictionaryLearning(_BaseSparseCoding, BaseEstimator):
    """Mini-batch dictionary learning.

       (U^*,V^*) = argmin 0.5 || X - U V ||_Fro^2 + alpha * || U ||_1,1
                    (U,V)

        Number of dictionary elements to extract.

        - `'lars'`: uses the least angle regression method to solve the lasso
          problem (`linear_model.lars_path`)
        - `'cd'`: uses the coordinate descent method to compute the

        Number of samples in each mini-batch.

        Whether to shuffle the samples before forming batches.

        Initial value of the dictionary for warm restart scenarios.

          Lasso solution (`linear_model.Lasso`). `'lasso_lars'` will be faster
          if the estimated components are sparse.
          solution.

    verbose : bool or int, default=False

        Components extracted from the data.

        Number of iterations over the full dataset.

    See Also
    --------

    Examples
    --------
    >>> from sklearn.decomposition import MiniBatchDictionaryLearning
    ...     random_state=42)
    >>> dict_learner = MiniBatchDictionaryLearning(
    ...     n_components=15, batch_size=3, transform_algorithm='lasso_lars',
    ...     transform_alpha=0.1, max_iter=20, random_state=42)
    >>> X_transformed = dict_learner.fit_transform(X)

    >>> np.mean(X_transformed == 0) > 0.5
    np.True_

    np.float64(0.052...)
    """

        "fit_algorithm": [StrOptions({"cd", "lars"})],
        "dict_init": [None, np.ndarray],
        ],
    }

        self,
        *,
        alpha=1,
        batch_size=256,
        tol=1e-3,
    ):
            n_jobs,
        )

        # fit_algorithm
        _check_positive_coding(self.fit_algorithm, self.positive_code)
        self._fit_algorithm = "lasso_" + self.fit_algorithm

    def _initialize_dict(self, X, random_state):
        """Initialization of the dictionary."""
        if self.dict_init is not None:
            dictionary = self.dict_init
        else:
            # Init V with SVD of X
            _, S, dictionary = _randomized_svd(
                X, self._n_components, random_state=random_state
            )
            dictionary = S[:, np.newaxis] * dictionary

        if self._n_components <= len(dictionary):
            dictionary = dictionary[: self._n_components, :]
        else:
            dictionary = np.concatenate(
                (
                    dictionary,
                    np.zeros(
                        (self._n_components - len(dictionary), dictionary.shape[1]),
                        dtype=dictionary.dtype,
                    ),
                )
            )

        dictionary = check_array(dictionary, order="F", dtype=X.dtype, copy=False)
        dictionary = np.require(dictionary, requirements="W")

        return dictionary

    def _update_inner_stats(self, X, code, batch_size, step):
        """Update the inner stats inplace."""
        if step < batch_size - 1:
            theta = (step + 1) * batch_size
        else:
            theta = batch_size**2 + step + 1 - batch_size
        beta = (theta + 1 - batch_size) / (theta + 1)

        self._A *= beta
        self._A += code.T @ code / batch_size
        self._B *= beta
        self._B += X.T @ code / batch_size

    def _minibatch_step(self, X, dictionary, random_state, step):
        """Perform the update on the dictionary for one minibatch."""

        # Compute code for this batch
        code = _sparse_encode(
            X,
            algorithm=self._fit_algorithm,
        )

            0.5 * ((X - code @ dictionary) ** 2).sum()
            + self.alpha * np.sum(np.abs(code))

        # Update inner stats
        self._update_inner_stats(X, code, batch_size, step)

        # Update dictionary
            X,
            code,
            self._A,
            self._B,
            positive=self.positive_dict,
        )

    def _check_convergence(
        self, X, batch_cost, new_dict, old_dict, n_samples, step, n_steps
    ):
        """Helper function to encapsulate the early stopping logic.

        Early stopping is based on two factors:
        - A small change of the dictionary between two minibatch updates. This is
          controlled by the tol parameter.
        - No more improvement on a smoothed estimate of the objective function for a
          a certain number of consecutive minibatch updates. This is controlled by
          the max_no_improvement parameter.
        """

        # Ignore 100 first steps or 1 epoch to avoid initializing the ewa_cost with a
        # too bad value
        if step <= min(100, n_samples / batch_size):

        else:

            print(
            )

        # Early stopping based on change of dictionary
        dict_diff = linalg.norm(new_dict - old_dict) / self._n_components
        if self.tol > 0 and dict_diff <= self.tol:
                print(f"Converged (small dictionary change) at step {step}/{n_steps}")

        else:

        if (
        ):
                print(
                )

        y : Ignored

        Returns
        -------
        """
            self, X, dtype=[np.float64, np.float32], order="C", copy=False
        )

        dictionary = self._initialize_dict(X, self._random_state)
        old_dict = dictionary.copy()

        if self.shuffle:
            X_train = X.copy()
            self._random_state.shuffle(X_train)
        else:
            X_train = X

        n_samples, n_features = X_train.shape

            print("[dict_learning]")

        # Inner stats
        self._A = np.zeros(
            (self._n_components, self._n_components), dtype=X_train.dtype
        )
        self._B = np.zeros((n_features, self._n_components), dtype=X_train.dtype)

        i = -1  # to allow max_iter = 0

            X_batch = X_train[batch]

            batch_cost = self._minibatch_step(
                X_batch, dictionary, self._random_state, i
            )

            if self._check_convergence(
                X_batch, batch_cost, dictionary, old_dict, n_samples, i, n_steps
            ):
                break

            # XXX callback param added for backward compat in #18975 but a common
            # unified callback API should be preferred
            if self.callback is not None:
                self.callback(locals())

            old_dict[:] = dictionary

        self.n_steps_ = i + 1
        self.n_iter_ = np.ceil(self.n_steps_ / n_steps_per_iter)
        self.components_ = dictionary

        """Update the model using the data in X as a mini-batch.

        y : Ignored

        Returns
        -------
            Return the instance itself.
        """

            self, X, dtype=[np.float64, np.float32], order="C", reset=not has_components
        )

            self._random_state = check_random_state(self.random_state)

            dictionary = self._initialize_dict(X, self._random_state)

            self._A = np.zeros((self._n_components, self._n_components), dtype=X.dtype)
            self._B = np.zeros((X.shape[1], self._n_components), dtype=X.dtype)
        else:
            dictionary = self.components_

        self._minibatch_step(X, dictionary, self._random_state, self.n_steps_)

    @property

        self,
        n_jobs,
    ):

        else:

            X,
        )

        Returns
        -------
        """

            )

        Returns
        -------
        """

        self,
        *,
    ):
            n_jobs,
        )

        X : Ignored

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

        self,
        *,
        alpha=1,
        tol=1e-8,
    ):
            n_jobs,
        )

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """

        else:

            X,
        )

        return V

        self,
        *,
        alpha=1,
        tol=1e-3,
    ):
            n_jobs,
        )

        else:
            )

        else:
                (
                    np.zeros(
                    ),
                )
            )

        else:

            X,
        )

            X,
            code,
            self._A,
            self._B,
        )

    ):

        """

        else:

            print(
            )

        else:

        if (
        ):
                print(
                )

        y : Ignored

        Returns
        -------
        """
        )

        else:
            X_train = X

        )

            )

            ):
                break

        y : Ignored

        Returns
        -------
        """

        )

        else:

class IncrementalPCA(_BasePCA):
    """Incremental principal components analysis (IPCA).

    Linear dimensionality reduction using Singular Value Decomposition of
    the data, keeping only the most significant singular vectors to
    project the data to a lower dimensional space. The input data is centered

    Depending on the size of the input data, this algorithm can be much more
    memory efficient than a PCA, and allows sparse input.

    This algorithm has constant memory complexity, on the order
    of ``batch_size * n_features``, enabling use of np.memmap files without
    loading the entire file into memory. For sparse matrices, the input
    is converted to dense in batches (in order to be able to subtract the
    mean) which avoids storing the entire dense matrix at any one time.

    The computational overhead of each SVD is
    ``O(batch_size * n_features ** 2)``, but only 2 * batch_size samples
    remain in memory at a time. There will be ``n_samples / batch_size`` SVD
    computations to get the principal components, versus 1 large SVD of
    complexity ``O(n_samples * n_features ** 2)`` for PCA.

    :ref:`sphx_glr_auto_examples_decomposition_plot_incremental_pca.py`.

    Read more in the :ref:`User Guide <IncrementalPCA>`.

    .. versionadded:: 0.16

        Number of components to keep. If ``n_components`` is ``None``,
        then ``n_components`` is set to ``min(n_samples, n_features)``.

        When True (False by default) the ``components_`` vectors are divided
        by ``n_samples`` times ``components_`` to ensure uncorrelated outputs
        with unit component-wise variances.

        (the relative variance scales of the components) but can sometimes
        making data respect some hard-wired assumptions.

        If False, X will be overwritten. ``copy=False`` can be used to
        save memory but is unsafe for general use.

    batch_size : int, default=None
        The number of samples to use for each batch. Only used when calling
        ``fit``. If ``batch_size`` is ``None``, then ``batch_size``
        is inferred from the data and set to ``5 * n_features``, to provide a
        balance between approximation accuracy and memory consumption.

        Variance explained by each of the selected components.

        If all components are stored, the sum of explained variances is equal
        to 1.0.

        Per-feature empirical mean, aggregate over calls to ``partial_fit``.

    var_ : ndarray of shape (n_features,)
        Per-feature empirical variance, aggregate over calls to
        ``partial_fit``.

        http://www.miketipping.com/papers/met-mppca.pdf.

        The estimated number of components. Relevant when
        ``n_components=None``.

    n_samples_seen_ : int
        The number of samples processed by the estimator. Will be reset on
        new calls to fit, but increments across ``partial_fit`` calls.

    batch_size_ : int
        Inferred batch size from ``batch_size``.

    See Also
    --------

    Notes
    -----
    Implements the incremental PCA model from:
    *D. Ross, J. Lim, R. Lin, M. Yang, Incremental Learning for Robust Visual
    Tracking, International Journal of Computer Vision, Volume 77, Issue 1-3,
    pp. 125-141, May 2008.*
    See https://www.cs.toronto.edu/~dross/ivt/RossLimLinYang_ijcv.pdf

    This model is an extension of the Sequential Karhunen-Loeve Transform from:
    :doi:`A. Levy and M. Lindenbaum, Sequential Karhunen-Loeve Basis Extraction and
    its Application to Images, IEEE Transactions on Image Processing, Volume 9,
    Number 8, pp. 1371-1374, August 2000. <10.1109/83.855432>`

    We have specifically abstained from an optimization used by authors of both
    papers, a QR decomposition used in specific situations to reduce the
    algorithmic complexity of the SVD. The source for this technique is
    *Matrix Computations, Third Edition, G. Holub and C. Van Loan, Chapter 5,
    section 5.4.4, pp 252-253.*. This technique has been omitted because it is
    advantageous only when decomposing a matrix with ``n_samples`` (rows)
    >= 5/3 * ``n_features`` (columns), and hurts the readability of the
    implemented algorithm. This would be a good opportunity for future
    optimization, if it is deemed necessary.

    D. Ross, J. Lim, R. Lin, M. Yang. Incremental Learning for Robust Visual
    Tracking, International Journal of Computer Vision, Volume 77,
    Issue 1-3, pp. 125-141, May 2008.

    G. Golub and C. Van Loan. Matrix Computations, Third Edition, Chapter 5,
    Section 5.4.4, pp. 252-253.

    Examples
    --------
    >>> from sklearn.decomposition import IncrementalPCA
    >>> from scipy import sparse
    >>> transformer = IncrementalPCA(n_components=7, batch_size=200)
    >>> # either partially fit on smaller batches of data
    >>> transformer.partial_fit(X[:100, :])
    IncrementalPCA(batch_size=200, n_components=7)
    >>> # or let the fit function itself divide the data into batches
    >>> X_sparse = sparse.csr_matrix(X)
    >>> X_transformed = transformer.fit_transform(X_sparse)
    (1797, 7)
    """

    __metadata_request__partial_fit = {"check_input": metadata_routing.UNUSED}

        "batch_size": [Interval(Integral, 1, None, closed="left"), None],
    }

    def __init__(self, n_components=None, *, whiten=False, copy=True, batch_size=None):

        """Fit the model with X, using minibatches of size batch_size.

        y : Ignored

        Returns
        -------
        """
        self.components_ = None
        self.n_samples_seen_ = 0
        self.mean_ = 0.0
        self.var_ = 0.0
        self.singular_values_ = None
        self.explained_variance_ = None
        self.explained_variance_ratio_ = None
        self.noise_variance_ = None

            self,
            X,
            accept_sparse=["csr", "csc", "lil"],
        )

        if self.batch_size is None:
            self.batch_size_ = 5 * n_features
        else:
            self.batch_size_ = self.batch_size

        for batch in gen_batches(
            n_samples, self.batch_size_, min_batch_size=self.n_components or 0
        ):
            X_batch = X[batch]
            if sparse.issparse(X_batch):
                X_batch = X_batch.toarray()
            self.partial_fit(X_batch, check_input=False)

    def partial_fit(self, X, y=None, check_input=True):
        """Incremental fit with X. All of X is processed as a single batch.

        y : Ignored

            Run check_array on X.

        Returns
        -------
        """

                    "IncrementalPCA.partial_fit does not support "
                    "sparse input. Either convert data to dense "
                    "or use IncrementalPCA.fit to do so in batches."
                )
                self,
                X,
                dtype=[np.float64, np.float32],
                force_writeable=True,
                reset=first_pass,
            )
            self.components_ = None

            if self.components_ is None:
                self.n_components_ = min(n_samples, n_features)
            else:
                self.n_components_ = self.components_.shape[0]
        elif not self.n_components <= n_features:
                "n_components=%r invalid for n_features=%d, need "
                "more rows than columns for IncrementalPCA "
                "processing" % (self.n_components, n_features)
            )
        elif self.n_components > n_samples and first_pass:
                f"n_components={self.n_components} must be less or equal to "
                f"the batch number of samples {n_samples} for the first "
                "partial_fit call."
            )
        else:

        if (self.components_ is not None) and (
            self.components_.shape[0] != self.n_components_
        ):
                "Number of input features has changed from %i "
                "to %i between calls to partial_fit! Try "
                "setting n_components to a fixed value."
                % (self.components_.shape[0], self.n_components_)
            )

        # This is the first partial_fit
        if not hasattr(self, "n_samples_seen_"):
            self.n_samples_seen_ = 0
            self.mean_ = 0.0
            self.var_ = 0.0

        # Update stats - they are 0 if this is the first step
        col_mean, col_var, n_total_samples = _incremental_mean_and_var(
            X,
            last_mean=self.mean_,
            last_variance=self.var_,
            last_sample_count=np.repeat(self.n_samples_seen_, X.shape[1]),
        )
        n_total_samples = n_total_samples[0]

        # Whitening
        if self.n_samples_seen_ == 0:
            # If it is the first step, simply whiten X
            X -= col_mean
        else:
            col_batch_mean = np.mean(X, axis=0)
            X -= col_batch_mean
            # Build matrix of combined previous basis and new data
            mean_correction = np.sqrt(
                (self.n_samples_seen_ / n_total_samples) * n_samples
            ) * (self.mean_ - col_batch_mean)
            X = np.vstack(
                (
                    self.singular_values_.reshape((-1, 1)) * self.components_,
                    X,
                    mean_correction,
                )
            )

        U, S, Vt = linalg.svd(X, full_matrices=False, check_finite=False)
        explained_variance = S**2 / (n_total_samples - 1)
        explained_variance_ratio = S**2 / np.sum(col_var * n_total_samples)

        self.n_samples_seen_ = n_total_samples
        self.components_ = Vt[: self.n_components_]
        self.singular_values_ = S[: self.n_components_]
        self.mean_ = col_mean
        self.var_ = col_var
        self.explained_variance_ = explained_variance[: self.n_components_]
        self.explained_variance_ratio_ = explained_variance_ratio[: self.n_components_]
        # we already checked `self.n_components <= n_samples` above
        if self.n_components_ not in (n_samples, n_features):
            self.noise_variance_ = explained_variance[self.n_components_ :].mean()
        else:

        from a training set, using minibatches of size batch_size if X is
        sparse.

        Returns
        -------
            Projection of X in the first principal components.

        Examples
        --------

        >>> from sklearn.decomposition import IncrementalPCA
        >>> X = np.array([[-1, -1], [-2, -1], [-3, -2],
        ...               [1, 1], [2, 1], [3, 2]])
        >>> ipca = IncrementalPCA(n_components=2, batch_size=3)
        >>> ipca.fit(X)
        IncrementalPCA(batch_size=3, n_components=2)
        >>> ipca.transform(X) # doctest: +SKIP
        """
        if sparse.issparse(X):
            n_samples = X.shape[0]
            output = []
            for batch in gen_batches(
                n_samples, self.batch_size_, min_batch_size=self.n_components or 0
            ):
                output.append(super().transform(X[batch].toarray()))
            return np.vstack(output)
        else:
            return super().transform(X)

        # Beware that fit accepts sparse data but partial_fit doesn't

        y : Ignored

        Returns
        -------
        """

            self,
            X,
        )

        else:

        ):

        y : Ignored

        Returns
        -------
        """

                )
                self,
                X,
            )

            else:
            )
            )
        else:

        ):
            )

            X,
        )

        else:
                (
                    X,
                )
            )

        else:

        sparse.

        Returns
        -------

        Examples
        --------

        """
            output = []
            ):
        else:

def _check_fitted_pca_close(pca1, pca2, rtol=1e-7, atol=1e-12):
    assert_allclose(pca1.components_, pca2.components_, rtol=rtol, atol=atol)
        pca1.explained_variance_, pca2.explained_variance_, rtol=rtol, atol=atol
    )
    assert_allclose(pca1.singular_values_, pca2.singular_values_, rtol=rtol, atol=atol)
    assert_allclose(pca1.mean_, pca2.mean_, rtol=rtol, atol=atol)
    assert_allclose(pca1.noise_variance_, pca2.noise_variance_, rtol=rtol, atol=atol)

    assert pca1.n_components_ == pca2.n_components_
    assert pca1.n_samples_ == pca2.n_samples_
    assert pca1.n_features_in_ == pca2.n_features_in_

def test_pca(svd_solver, n_components):
    pca = PCA(n_components=n_components, svd_solver=svd_solver)

    # check the shape of fit.transform
    X_r = pca.fit(X).transform(X)
    assert X_r.shape[1] == n_components

    # check the equivalence of fit.transform and fit_transform
    X_r2 = pca.fit_transform(X)
    assert_allclose(X_r, X_r2)
    X_r = pca.transform(X)

    # Test get_covariance and get_precision
    cov = pca.get_covariance()
    precision = pca.get_precision()
    assert_allclose(np.dot(cov, precision), np.eye(X.shape[1]), atol=1e-12)

def test_pca_sparse(
    global_random_seed, svd_solver, sparse_container, n_components, density, scale
):
    """Check that the results are the same for sparse and dense input."""

    # Set atol in addition of the default rtol to account for the very wide range of
    # result values (1e-8 to 1e0).
    atol = 1e-12
    transform_atol = 1e-10

    random_state = np.random.default_rng(global_random_seed)
    X = sparse_container(
        sp.sparse.random(
            SPARSE_M,
            SPARSE_N,
            density=density,
        )
    )
    # Scale the data + vary the column means
    scale_vector = random_state.random(X.shape[1]) * scale
    X = X.multiply(scale_vector)

    pca = PCA(
        svd_solver=svd_solver,
    )
    pca.fit(X)

    Xd = X.toarray()
    pcad = PCA(
    )
    pcad.fit(Xd)

    # Fitted attributes equality
    _check_fitted_pca_close(pca, pcad, atol=atol)

    # Test transform
    X2 = sparse_container(
            SPARSE_M,
            SPARSE_N,
        )
    )
    X2d = X2.toarray()

    assert_allclose(pca.transform(X2), pca.transform(X2d), atol=transform_atol)
    assert_allclose(pca.transform(X2), pcad.transform(X2d), atol=transform_atol)

def test_pca_sparse_fit_transform(global_random_seed, sparse_container):
            SPARSE_M,
            SPARSE_N,
            density=0.01,
        )
    )
            SPARSE_M,
            SPARSE_N,
        )
    )

    pca_fit = PCA(n_components=10, svd_solver="arpack", random_state=global_random_seed)
    pca_fit_transform = PCA(
        n_components=10, svd_solver="arpack", random_state=global_random_seed
    )

    pca_fit.fit(X)
    transformed_X = pca_fit_transform.fit_transform(X)

    _check_fitted_pca_close(pca_fit, pca_fit_transform)
    assert_allclose(transformed_X, pca_fit_transform.transform(X))
    assert_allclose(transformed_X, pca_fit.transform(X))
    assert_allclose(pca_fit.transform(X2), pca_fit_transform.transform(X2))

def test_sparse_pca_solver_error(global_random_seed, svd_solver, sparse_container):
    random_state = np.random.RandomState(global_random_seed)
            SPARSE_M,
            SPARSE_N,
        )
    )
    pca = PCA(n_components=30, svd_solver=svd_solver)
    error_msg_pattern = (
        'PCA only support sparse inputs with the "arpack" and "covariance_eigh"'
        f' solvers, while "{svd_solver}" was passed'
    )
    with pytest.raises(TypeError, match=error_msg_pattern):
        pca.fit(X)

def test_sparse_pca_auto_arpack_singluar_values_consistency(
    global_random_seed, sparse_container
):
    """Check that "auto" and "arpack" solvers are equivalent for sparse inputs."""
            SPARSE_M,
            SPARSE_N,
        )
    )
    pca_arpack = PCA(n_components=10, svd_solver="arpack").fit(X)
    pca_auto = PCA(n_components=10, svd_solver="auto").fit(X)
    assert_allclose(pca_arpack.singular_values_, pca_auto.singular_values_, rtol=5e-3)

def test_no_empty_slice_warning():
    # test if we avoid numpy warnings for computing over empty arrays
    n_features = n_components + 2  # anything > n_comps triggered it in 0.16
    X = np.random.uniform(-1, 1, size=(n_components, n_features))
    pca = PCA(n_components=n_components)

def test_whitening(solver, copy):
    # Check that PCA output has unit-variance
    n_features = 80
    n_components = 30
    rank = 50

    # some low rank data with correlated features
    X = np.dot(
        rng.randn(n_samples, rank),
        np.dot(np.diag(np.linspace(10.0, 1.0, rank)), rng.randn(rank, n_features)),
    )
    # the component-wise variance of the first 50 features is 3 times the
    # mean component-wise variance of the remaining 30 features
    X[:, :50] *= 3

    assert X.shape == (n_samples, n_features)

    # the component-wise variance is thus highly varying:
    assert X.std(axis=0).std() > 43.8

    # whiten the data while projecting to the lower dim subspace
    X_ = X.copy()  # make sure we keep an original across iterations.
    pca = PCA(
        whiten=True,
        svd_solver=solver,
        iterated_power=7,
    )
    # test fit_transform
    X_whitened = pca.fit_transform(X_.copy())
    assert X_whitened.shape == (n_samples, n_components)
    X_whitened2 = pca.transform(X_)
    assert_allclose(X_whitened, X_whitened2, rtol=5e-4)

    assert_allclose(X_whitened.std(ddof=1, axis=0), np.ones(n_components))
    assert_allclose(X_whitened.mean(axis=0), np.zeros(n_components), atol=1e-12)

    X_ = X.copy()
    pca = PCA(
        n_components=n_components, whiten=False, copy=copy, svd_solver=solver
    ).fit(X_.copy())
    X_unwhitened = pca.transform(X_)
    assert X_unwhitened.shape == (n_samples, n_components)

    # in that case the output components still have varying variances
    assert X_unwhitened.std(axis=0).std() == pytest.approx(74.1, rel=1e-1)

def test_pca_solver_equivalence(
    other_svd_solver,
    data_shape,
    rank_deficient,
    whiten,
):
    if data_shape == "tall":
        n_samples, n_features = 100, 30
    else:
        n_samples, n_features = 30, 100
    n_samples_test = 10

    if rank_deficient:
        rng = np.random.default_rng(global_random_seed)
        rank = min(n_samples, n_features) // 2
        X = rng.standard_normal(
            size=(n_samples + n_samples_test, rank)
        ) @ rng.standard_normal(size=(rank, n_features))
    else:
            n_samples=n_samples + n_samples_test,
            tail_strength=0.5,
        )
        # With a non-zero tail strength, the data is actually full-rank.
        rank = min(n_samples, n_features)

    X_train, X_test = X[:n_samples], X[n_samples:]

    if global_dtype == np.float32:
        tols = dict(atol=3e-2, rtol=1e-5)
        variance_threshold = 1e-5
    else:
        tols = dict(atol=1e-10, rtol=1e-12)
        variance_threshold = 1e-12

    extra_other_kwargs = {}
    if other_svd_solver == "randomized":
        # Only check for a truncated result with a large number of iterations
        # to make sure that we can recover precise results.
        n_components = 10
        extra_other_kwargs = {"iterated_power": 50}
    elif other_svd_solver == "arpack":
        # Test all components except the last one which cannot be estimated by
        # arpack.
        n_components = np.minimum(n_samples, n_features) - 1
    else:
        # Test all components to high precision.
        n_components = None

    pca_full = PCA(n_components=n_components, svd_solver="full", whiten=whiten)
    pca_other = PCA(
        svd_solver=other_svd_solver,
        **extra_other_kwargs,
    )
    X_trans_full_train = pca_full.fit_transform(X_train)
    assert np.isfinite(X_trans_full_train).all()
    assert X_trans_full_train.dtype == global_dtype
    X_trans_other_train = pca_other.fit_transform(X_train)
    assert np.isfinite(X_trans_other_train).all()
    assert X_trans_other_train.dtype == global_dtype

    assert (pca_full.explained_variance_ >= 0).all()
    assert_allclose(pca_full.explained_variance_, pca_other.explained_variance_, **tols)
        pca_full.explained_variance_ratio_,
        pca_other.explained_variance_ratio_,
        **tols,
    )
    reference_components = pca_full.components_
    assert np.isfinite(reference_components).all()
    other_components = pca_other.components_
    assert np.isfinite(other_components).all()

    # For some choice of n_components and data distribution, some components
    # might be pure noise, let's ignore them in the comparison:
    stable = pca_full.explained_variance_ > variance_threshold
    assert stable.sum() > 1
    assert_allclose(reference_components[stable], other_components[stable], **tols)

    # As a result the output of fit_transform should be the same:
        X_trans_other_train[:, stable], X_trans_full_train[:, stable], **tols
    )

    # And similarly for the output of transform on new data (except for the
    # last component that can be underdetermined):
    X_trans_full_test = pca_full.transform(X_test)
    assert np.isfinite(X_trans_full_test).all()
    assert X_trans_full_test.dtype == global_dtype
    X_trans_other_test = pca_other.transform(X_test)
    assert np.isfinite(X_trans_other_test).all()
    assert X_trans_other_test.dtype == global_dtype
    assert_allclose(X_trans_other_test[:, stable], X_trans_full_test[:, stable], **tols)

    # Check that inverse transform reconstructions for both solvers are
    # compatible.
    X_recons_full_test = pca_full.inverse_transform(X_trans_full_test)
    assert np.isfinite(X_recons_full_test).all()
    assert X_recons_full_test.dtype == global_dtype
    X_recons_other_test = pca_other.inverse_transform(X_trans_other_test)
    assert np.isfinite(X_recons_other_test).all()
    assert X_recons_other_test.dtype == global_dtype

    if pca_full.components_.shape[0] == pca_full.components_.shape[1]:
        # In this case, the models should have learned the same invertible
        # transform. They should therefore both be able to reconstruct the test
        # data.
        assert_allclose(X_recons_full_test, X_test, **tols)
        assert_allclose(X_recons_other_test, X_test, **tols)
    elif pca_full.components_.shape[0] < rank:
        # In the absence of noisy components, both models should be able to
        # reconstruct the same low-rank approximation of the original data.
        assert pca_full.explained_variance_.min() > variance_threshold
        assert_allclose(X_recons_full_test, X_recons_other_test, **tols)
    else:
        # When n_features > n_samples and n_components is larger than the rank
        # of the training set, the output of the `inverse_transform` function
        # is ill-defined. We can only check that we reach the same fixed point
        # after another round of transform:
            pca_full.transform(X_recons_full_test)[:, stable],
            pca_other.transform(X_recons_other_test)[:, stable],
            **tols,
        )

def test_pca_explained_variance_empirical(X, svd_solver):
    pca = PCA(n_components=2, svd_solver=svd_solver, random_state=0)
    X_pca = pca.fit_transform(X)
    assert_allclose(pca.explained_variance_, np.var(X_pca, ddof=1, axis=0))

    expected_result = np.linalg.eig(np.cov(X, rowvar=False))[0]
    expected_result = sorted(expected_result, reverse=True)[:2]
    assert_allclose(pca.explained_variance_, expected_result, rtol=5e-3)

def test_pca_singular_values_consistency(svd_solver):
    n_samples, n_features = 100, 80

    pca_full = PCA(n_components=2, svd_solver="full", random_state=rng)
    pca_other = PCA(n_components=2, svd_solver=svd_solver, random_state=rng)

    pca_full.fit(X)
    pca_other.fit(X)

    assert_allclose(pca_full.singular_values_, pca_other.singular_values_, rtol=5e-3)

def test_pca_singular_values(svd_solver):

    pca = PCA(n_components=2, svd_solver=svd_solver, random_state=rng)
    X_trans = pca.fit_transform(X)

    # compare to the Frobenius norm
        np.sum(pca.singular_values_**2), np.linalg.norm(X_trans, "fro") ** 2
    )
    # Compare to the 2-norms of the score vectors
    assert_allclose(pca.singular_values_, np.sqrt(np.sum(X_trans**2, axis=0)))

    # set the singular values and see what er get back
    n_samples, n_features = 100, 110

    pca = PCA(n_components=3, svd_solver=svd_solver, random_state=rng)
    X_trans /= np.sqrt(np.sum(X_trans**2, axis=0))
    X_trans[:, 0] *= 3.142
    X_trans[:, 1] *= 2.718
    X_hat = np.dot(X_trans, pca.components_)
    pca.fit(X_hat)
    assert_allclose(pca.singular_values_, [3.142, 2.718, 1.0])

def test_pca_check_projection(svd_solver):
    # Test that the projection of data is correct
    n, p = 100, 3
    X = rng.randn(n, p) * 0.1
    X[:10] += np.array([3, 4, 5])
    Xt = 0.1 * rng.randn(1, p) + np.array([3, 4, 5])

    Yt = PCA(n_components=2, svd_solver=svd_solver).fit(X).transform(Xt)
    Yt /= np.sqrt((Yt**2).sum())

    assert_allclose(np.abs(Yt[0][0]), 1.0, rtol=5e-3)

def test_pca_check_projection_list(svd_solver):
    X = [[1.0, 0.0], [0.0, 1.0]]
    pca = PCA(n_components=1, svd_solver=svd_solver, random_state=0)
    assert X_trans.shape, (2, 1)
    assert_allclose(X_trans.mean(), 0.00, atol=1e-12)
    assert_allclose(X_trans.std(), 0.71, rtol=5e-3)

def test_pca_inverse(svd_solver, whiten):
    # Test that the projection of data can be inverted
    n, p = 50, 3
    X = rng.randn(n, p)  # spherical data
    X[:, 1] *= 0.00001  # make middle component relatively small
    X += [5, 4, 3]  # make a large mean

    # same check that we can find the original data from the transformed
    # signal (since the data is almost of rank n_components)
    pca = PCA(n_components=2, svd_solver=svd_solver, whiten=whiten).fit(X)
    Y = pca.transform(X)
    Y_inverse = pca.inverse_transform(Y)
    assert_allclose(X, Y_inverse, rtol=5e-6)

def test_pca_validation(svd_solver, data, n_components, err_msg):
    # Ensures that solver-specific extreme inputs for the n_components
    # parameter raise errors
    smallest_d = 2  # The smallest dimension
    pca_fitted = PCA(n_components, svd_solver=svd_solver)

        pca_fitted.fit(data)

    # Additional case for arpack
    if svd_solver == "arpack":
        n_components = smallest_d

        err_msg = (
            "n_components={}L? must be strictly less than "
            r"min\(n_samples, n_features\)={}L? with "
            "svd_solver='arpack'".format(n_components, smallest_d)
        )
        with pytest.raises(ValueError, match=err_msg):
            PCA(n_components, svd_solver=svd_solver).fit(data)

def test_n_components_none(data, solver, n_components_):
    pca = PCA(svd_solver=solver)
    pca.fit(data)
    assert pca.n_components_ == n_components_

def test_n_components_mle(svd_solver):
    # Ensure that n_components == 'mle' doesn't raise error for auto/full
    n_samples, n_features = 600, 10
    pca = PCA(n_components="mle", svd_solver=svd_solver)
    assert pca.n_components_ == 1

def test_n_components_mle_error(svd_solver):
    # Ensure that n_components == 'mle' will raise an error for unsupported
    # solvers
    err_msg = "n_components='mle' cannot be a string with svd_solver='{}'".format(
        svd_solver
    )

def test_pca_dim():
    # Check automated dimensionality setting
    n, p = 100, 5
    X[:10] += np.array([3, 4, 5, 1, 2])
    pca = PCA(n_components="mle", svd_solver="full").fit(X)
    assert pca.n_components == "mle"

def test_infer_dim_1():
    # TODO: explain what this is testing
    # Or at least use explicit variable names...
    n, p = 1000, 5
    X = (
        rng.randn(n, p) * 0.1
        + rng.randn(n, 1) * np.array([3, 4, 5, 1, 2])
        + np.array([1, 0, 7, 4, 6])
    )
    pca = PCA(n_components=p, svd_solver="full")
    spect = pca.explained_variance_
    ll = np.array([_assess_dimension(spect, k, n) for k in range(1, p)])
    assert ll[1] > ll.max() - 0.01 * n

def test_infer_dim_2():
    X[10:20] += np.array([6, 0, 7, 2, -1])
    assert _infer_dimension(spect, n) > 1

def test_infer_dim_3():
    n, p = 100, 5
    X[30:40] += 2 * np.array([-1, 1, -1, 1, -1])
    assert _infer_dimension(spect, n) > 2

def test_infer_dim_by_explained_variance(X, n_components, n_components_validated):
    pca = PCA(n_components=n_components, svd_solver="full")
    assert pca.n_components == pytest.approx(n_components)
    assert pca.n_components_ == n_components_validated

def test_pca_score(svd_solver):
    # Test that probabilistic PCA scoring yields a reasonable score
    n, p = 1000, 3
    X = rng.randn(n, p) * 0.1 + np.array([3, 4, 5])
    pca = PCA(n_components=2, svd_solver=svd_solver)

    ll1 = pca.score(X)
    h = -0.5 * np.log(2 * np.pi * np.exp(1) * 0.1**2) * p
    assert_allclose(ll1 / h, 1, rtol=5e-2)

    ll2 = pca.score(rng.randn(n, p) * 0.2 + np.array([3, 4, 5]))
    assert ll1 > ll2

    pca = PCA(n_components=2, whiten=True, svd_solver=svd_solver)
    ll2 = pca.score(X)

def test_pca_score3():
    # Check that probabilistic PCA selects the right model
    n, p = 200, 3
    Xl = rng.randn(n, p) + rng.randn(n, 1) * np.array([3, 4, 5]) + np.array([1, 0, 7])
    Xt = rng.randn(n, p) + rng.randn(n, 1) * np.array([3, 4, 5]) + np.array([1, 0, 7])
    ll = np.zeros(p)
    for k in range(p):
        pca = PCA(n_components=k, svd_solver="full")
        pca.fit(Xl)
        ll[k] = pca.score(Xt)

    assert ll.argmax() == 1

def test_pca_sanity_noise_variance(svd_solver):
    # Sanity check for the noise_variance_. For more details see
    # https://github.com/scikit-learn/scikit-learn/issues/7568
    # https://github.com/scikit-learn/scikit-learn/issues/8541
    # https://github.com/scikit-learn/scikit-learn/issues/8544
    X, _ = datasets.load_digits(return_X_y=True)
    pca = PCA(n_components=30, svd_solver=svd_solver, random_state=0)
    assert np.all((pca.explained_variance_ - pca.noise_variance_) >= 0)

def test_pca_score_consistency_solvers(svd_solver):
    # Check the consistency of score between solvers
    pca_full = PCA(n_components=30, svd_solver="full", random_state=0)
    pca_other = PCA(n_components=30, svd_solver=svd_solver, random_state=0)
    assert_allclose(pca_full.score(X), pca_other.score(X), rtol=5e-6)

def test_pca_zero_noise_variance_edge_cases(svd_solver):
    # ensure that noise_variance_ is 0 in edge cases
    # when n_components == min(n_samples, n_features)
    n, p = 100, 3

    pca = PCA(n_components=p, svd_solver=svd_solver)
    assert pca.noise_variance_ == 0
    # Non-regression test for gh-12489
    # ensure no divide-by-zero error for n_components == n_features < n_samples
    pca.score(X)

    pca.fit(X.T)
    # ensure no divide-by-zero error for n_components == n_samples < n_features
    pca.score(X.T)

def test_pca_svd_solver_auto(n_samples, n_features, n_components, expected_solver):
    data = np.random.RandomState(0).uniform(size=(n_samples, n_features))
    pca_auto = PCA(n_components=n_components, random_state=0)
    pca_test = PCA(
        n_components=n_components, svd_solver=expected_solver, random_state=0
    )
    pca_auto.fit(data)
    assert pca_auto._fit_svd_solver == expected_solver
    pca_test.fit(data)
    assert_allclose(pca_auto.components_, pca_test.components_)

def test_pca_deterministic_output(svd_solver):
    X = rng.rand(10, 10)

    transformed_X = np.zeros((20, 2))
        pca = PCA(n_components=2, svd_solver=svd_solver, random_state=rng)
        transformed_X[i, :] = pca.fit_transform(X)[0]
    assert_allclose(transformed_X, np.tile(transformed_X[0, :], 20).reshape(20, 2))

def test_pca_dtype_preservation(svd_solver, global_random_seed):
    check_pca_float_dtype_preservation(svd_solver, global_random_seed)
    check_pca_int_dtype_upcast_to_double(svd_solver)

def check_pca_float_dtype_preservation(svd_solver, seed):
    # Ensure that PCA does not upscale the dtype when input is float32
    X = np.random.RandomState(seed).rand(1000, 4)
    X_float64 = X.astype(np.float64, copy=False)
    X_float32 = X.astype(np.float32)

    pca_64 = PCA(n_components=3, svd_solver=svd_solver, random_state=seed).fit(
        X_float64
    )
    pca_32 = PCA(n_components=3, svd_solver=svd_solver, random_state=seed).fit(
        X_float32
    )

    assert pca_64.components_.dtype == np.float64
    assert pca_32.components_.dtype == np.float32
    assert pca_64.transform(X_float64).dtype == np.float64
    assert pca_32.transform(X_float32).dtype == np.float32

    # The atol and rtol are set such that the test passes for all random seeds
    # on all supported platforms on our CI and conda-forge with the default
    # random seed.
    assert_allclose(pca_64.components_, pca_32.components_, rtol=1e-3, atol=1e-3)

def check_pca_int_dtype_upcast_to_double(svd_solver):
    # Ensure that all int types will be upcast to float64
    X_i64 = np.random.RandomState(0).randint(0, 1000, (1000, 4))
    X_i64 = X_i64.astype(np.int64, copy=False)
    X_i32 = X_i64.astype(np.int32, copy=False)

    pca_64 = PCA(n_components=3, svd_solver=svd_solver, random_state=0).fit(X_i64)
    pca_32 = PCA(n_components=3, svd_solver=svd_solver, random_state=0).fit(X_i32)

    assert pca_32.components_.dtype == np.float64
    assert pca_64.transform(X_i64).dtype == np.float64
    assert pca_32.transform(X_i32).dtype == np.float64

    assert_allclose(pca_64.components_, pca_32.components_, rtol=1e-4)

def test_pca_n_components_mostly_explained_variance_ratio():
    # when n_components is the second highest cumulative sum of the
    # explained_variance_ratio_, then n_components_ should equal the
    # number of features in the dataset #15669
    pca1 = PCA().fit(X, y)

    n_components = pca1.explained_variance_ratio_.cumsum()[-2]
    pca2 = PCA(n_components=n_components).fit(X, y)
    assert pca2.n_components_ == X.shape[1]

def test_assess_dimension_bad_rank():
    # Test error when tested rank not in [1, n_features - 1]
    spectrum = np.array([1, 1e-30, 1e-30, 1e-30])
    for rank in (0, 5):
        with pytest.raises(ValueError, match=r"should be in \[1, n_features - 1\]"):
            _assess_dimension(spectrum, rank, n_samples)

def test_small_eigenvalues_mle():
    # Test rank associated with tiny eigenvalues are given a log-likelihood of
    # -inf. The inferred rank will be 1

    assert _assess_dimension(spectrum, rank=1, n_samples=10) > -np.inf

    for rank in (2, 3):
        assert _assess_dimension(spectrum, rank, 10) == -np.inf

    assert _infer_dimension(spectrum, 10) == 1

def test_mle_redundant_data():
    # Test 'mle' with pathological X: only one relevant feature should give a
    # rank of 1
    X, _ = datasets.make_classification(
        n_informative=1,
        n_repeated=18,
    )
    pca = PCA(n_components="mle").fit(X)

def test_fit_mle_too_few_samples():
    # Tests that an error is raised when the number of samples is smaller
    # than the number of features during an mle fit
    X, _ = datasets.make_classification(n_samples=20, n_features=21, random_state=42)

    pca = PCA(n_components="mle", svd_solver="full")
        match="n_components='mle' is only supported if n_samples >= n_features",
    ):

def test_mle_simple_case():
    # non-regression test for issue
    # https://github.com/scikit-learn/scikit-learn/issues/16730
    n_samples, n_dim = 1000, 10
    X = np.random.RandomState(0).randn(n_samples, n_dim)
    X[:, -1] = np.mean(X[:, :-1], axis=-1)  # true X dim is ndim - 1
    pca_skl = PCA("mle", svd_solver="full")
    pca_skl.fit(X)
    assert pca_skl.n_components_ == n_dim - 1

def test_assess_dimesion_rank_one():
    # Make sure assess_dimension works properly on a matrix of rank 1
    n_samples, n_features = 9, 6
    X = np.ones((n_samples, n_features))  # rank 1 matrix
    _, s, _ = np.linalg.svd(X, full_matrices=True)
    # except for rank 1, all eigenvalues are 0 resp. close to 0 (FP)
    assert_allclose(s[1:], np.zeros(n_features - 1), atol=1e-12)

    assert np.isfinite(_assess_dimension(s, rank=1, n_samples=n_samples))
    for rank in range(2, n_features):
        assert _assess_dimension(s, rank, n_samples) == -np.inf

def test_pca_randomized_svd_n_oversamples():
    """Check that exposing and setting `n_oversamples` will provide accurate results
    even when `X` as a large number of features.

    https://github.com/scikit-learn/scikit-learn/issues/20589
    """
    n_features = 100
    X = rng.randn(1_000, n_features)

    # The default value of `n_oversamples` will lead to inaccurate results
    # We force it to the number of features.
    pca_randomized = PCA(
        n_components=1,
        svd_solver="randomized",
        n_oversamples=n_features,
    ).fit(X)
    pca_full = PCA(n_components=1, svd_solver="full").fit(X)
    pca_arpack = PCA(n_components=1, svd_solver="arpack", random_state=0).fit(X)

    assert_allclose(np.abs(pca_full.components_), np.abs(pca_arpack.components_))
    assert_allclose(np.abs(pca_randomized.components_), np.abs(pca_arpack.components_))

def test_feature_names_out():
    """Check feature names out for PCA."""
    pca = PCA(n_components=2).fit(iris.data)

    names = pca.get_feature_names_out()
    assert_array_equal([f"pca{i}" for i in range(2)], names)

def test_variance_correctness(copy):
    """Check the accuracy of PCA's internal variance calculation"""
    X = rng.randn(1000, 200)
    pca = PCA().fit(X)
    pca_var = pca.explained_variance_ / pca.explained_variance_ratio_
    true_var = np.var(X, ddof=1, axis=0).sum()
    np.testing.assert_allclose(pca_var, true_var)

def check_array_api_get_precision(name, estimator, array_namespace, device, dtype_name):
    iris_np = iris.data.astype(dtype_name)
    iris_xp = xp.asarray(iris_np, device=device)

    estimator.fit(iris_np)
    precision_np = estimator.get_precision()
    covariance_np = estimator.get_covariance()

    rtol = 2e-4 if iris_np.dtype == "float32" else 2e-7
        estimator_xp = clone(estimator).fit(iris_xp)
        precision_xp = estimator_xp.get_precision()
        assert precision_xp.shape == (4, 4)
        assert precision_xp.dtype == iris_xp.dtype

            _convert_to_numpy(precision_xp, xp=xp),
            precision_np,
        )
        covariance_xp = estimator_xp.get_covariance()
        assert covariance_xp.shape == (4, 4)
        assert covariance_xp.dtype == iris_xp.dtype

            _convert_to_numpy(covariance_xp, xp=xp),
            covariance_np,
        )

def test_pca_array_api_compliance(
    estimator, check, array_namespace, device, dtype_name
):
    name = estimator.__class__.__name__
    check(name, estimator, array_namespace, device=device, dtype_name=dtype_name)

def test_pca_mle_array_api_compliance(
):

    # Simpler variant of the generic check_array_api_input checker tailored for
    # the specific case of PCA with mle-trimmed components.

    X = X.astype(dtype_name, copy=False)
    atol = _atol_for_type(X.dtype)

    est = clone(estimator)

    X_xp = xp.asarray(X, device=device)
    y_xp = xp.asarray(y, device=device)

    components_np = est.components_
    explained_variance_np = est.explained_variance_

    est_xp = clone(est)
        est_xp.fit(X_xp, y_xp)
        components_xp = est_xp.components_
        assert array_device(components_xp) == array_device(X_xp)
        components_xp_np = _convert_to_numpy(components_xp, xp=xp)

        explained_variance_xp = est_xp.explained_variance_
        assert array_device(explained_variance_xp) == array_device(X_xp)
        explained_variance_xp_np = _convert_to_numpy(explained_variance_xp, xp=xp)

    assert components_xp_np.dtype == components_np.dtype
    assert components_xp_np.shape[1] == components_np.shape[1]
    assert explained_variance_xp_np.dtype == explained_variance_np.dtype

    # Check that the explained variance values match for the
    # common components:
    min_components = min(components_xp_np.shape[0], components_np.shape[0])
        explained_variance_xp_np[:min_components],
        explained_variance_np[:min_components],
        atol=atol,
    )

    # If the number of components differ, check that the explained variance of
    # the trimmed components is very small.
    if components_xp_np.shape[0] != components_np.shape[0]:
        reference_variance = explained_variance_np[-1]
        extra_variance_np = explained_variance_np[min_components:]
        extra_variance_xp_np = explained_variance_xp_np[min_components:]
        assert all(np.abs(extra_variance_np - reference_variance) < atol)
        assert all(np.abs(extra_variance_xp_np - reference_variance) < atol)

def test_array_api_error_and_warnings_on_unsupported_params():
    xp = pytest.importorskip("array_api_strict")
    iris_xp = xp.asarray(iris.data)

    pca = PCA(n_components=2, svd_solver="arpack", random_state=0)
        "PCA with svd_solver='arpack' is not supported for Array API inputs."
    )
        with config_context(array_api_dispatch=True):
            pca.fit(iris_xp)

    pca.set_params(svd_solver="randomized", power_iteration_normalizer="LU")
        "Array API does not support LU factorization. Set"
        " `power_iteration_normalizer='QR'` instead."
    )

    pca.set_params(svd_solver="randomized", power_iteration_normalizer="auto")
        "Array API does not support LU factorization, falling back to QR instead. Set"
        " `power_iteration_normalizer='QR'` explicitly to silence this warning."
    )
    with pytest.warns(UserWarning, match=expected_msg):

def test_incremental_pca():
    # Incremental PCA on dense arrays.
    batch_size = X.shape[0] // 3
    ipca = IncrementalPCA(n_components=2, batch_size=batch_size)
    pca = PCA(n_components=2)
    pca.fit_transform(X)

    X_transformed = ipca.fit_transform(X)

    assert X_transformed.shape == (X.shape[0], 2)
        ipca.explained_variance_ratio_.sum(),
        pca.explained_variance_ratio_.sum(),
        rtol=1e-3,
    )

    for n_components in [1, 2, X.shape[1]]:
        ipca = IncrementalPCA(n_components, batch_size=batch_size)
        ipca.fit(X)
        cov = ipca.get_covariance()
        precision = ipca.get_precision()
        np.testing.assert_allclose(
            np.dot(cov, precision), np.eye(X.shape[1]), atol=1e-13
        )

def test_incremental_pca_sparse(sparse_container):
    # Incremental PCA on sparse arrays.
    batch_size = X_sparse.shape[0] // 3

    X_transformed = ipca.fit_transform(X_sparse)

    assert X_transformed.shape == (X_sparse.shape[0], 2)
    )

        ipca.fit(X_sparse)
            np.dot(cov, precision), np.eye(X_sparse.shape[1]), atol=1e-13
        )

        match=(
            "IncrementalPCA.partial_fit does not support "
            "sparse input. Either convert data to dense "
            "or use IncrementalPCA.fit to do so in batches."
        ),
    ):
        ipca.partial_fit(X_sparse)

def test_incremental_pca_check_projection():
    # Test that the projection of data is correct.
    rng = np.random.RandomState(1999)
    n, p = 100, 3

    # Get the reconstruction of the generated data X
    # Note that Xt has the same "components" as X, just separated
    # This is what we want to ensure is recreated correctly
    Yt = IncrementalPCA(n_components=2).fit(X).transform(Xt)

    # Normalize

    # Make sure that the first element of Yt is ~1, this means
    # the reconstruction worked as expected
    assert_almost_equal(np.abs(Yt[0][0]), 1.0, 1)

def test_incremental_pca_inverse():
    # Test that the projection of data can be inverted.
    n, p = 50, 3

    ipca = IncrementalPCA(n_components=2, batch_size=10).fit(X)
    Y = ipca.transform(X)
    Y_inverse = ipca.inverse_transform(Y)
    assert_almost_equal(X, Y_inverse, decimal=3)

def test_incremental_pca_validation():
    # Test that n_components is <= n_features.
    X = np.array([[0, 1, 0], [1, 0, 0]])
    n_components = 4
        match=(
            "n_components={} invalid"
            " for n_features={}, need more rows than"
            " columns for IncrementalPCA"
            " processing".format(n_components, n_features)
        ),
    ):
        IncrementalPCA(n_components, batch_size=10).fit(X)

    # Test that n_components is also <= n_samples in first call to partial fit.
    n_components = 3
        match=(
            f"n_components={n_components} must be less or equal to the batch "
            f"number of samples {n_samples} for the first partial_fit call."
        ),
    ):
        IncrementalPCA(n_components=n_components).partial_fit(X)

def test_n_samples_equal_n_components():
    # Ensures no warning is raised when n_samples==n_components
    # Non-regression test for gh-19050
    ipca = IncrementalPCA(n_components=5)
        ipca.partial_fit(np.random.randn(5, 7))
        ipca.fit(np.random.randn(5, 7))

def test_n_components_none():
    # Ensures that n_components == None is handled correctly
    for n_samples, n_features in [(50, 10), (10, 50)]:
        X = rng.rand(n_samples, n_features)
        ipca = IncrementalPCA(n_components=None)

        # First partial_fit call, ipca.n_components_ is inferred from
        # min(X.shape)
        ipca.partial_fit(X)
        assert ipca.n_components_ == min(X.shape)

        # Second partial_fit call, ipca.n_components_ is inferred from
        # ipca.components_ computed from the first partial_fit call
        assert ipca.n_components_ == ipca.components_.shape[0]

def test_incremental_pca_set_params():
    # Test that components_ sign is stable over batch sizes.
    X3 = rng.randn(n_samples, n_features)
    ipca = IncrementalPCA(n_components=20)
    ipca.fit(X)
    # Decreasing number of components
    ipca.set_params(n_components=10)
        ipca.partial_fit(X2)
    # Increasing number of components
    ipca.set_params(n_components=15)
        ipca.partial_fit(X3)
    # Returning to original setting
    ipca.set_params(n_components=20)
    ipca.partial_fit(X)

def test_incremental_pca_num_features_change():
    # Test that changing n_components will raise an error.
    X = rng.randn(n_samples, 20)
    X2 = rng.randn(n_samples, 50)
    ipca = IncrementalPCA(n_components=None)

def test_incremental_pca_batch_signs():
    all_components = []
    batch_sizes = np.arange(10, 20)
    for batch_size in batch_sizes:
        ipca = IncrementalPCA(n_components=None, batch_size=batch_size).fit(X)
        all_components.append(ipca.components_)

    for i, j in itertools.pairwise(all_components):
        assert_almost_equal(np.sign(i), np.sign(j), decimal=6)

def test_incremental_pca_partial_fit_small_batch():
    # Test that there is no minimum batch size after the first partial_fit
    # Non-regression test
    n, p = 50, 3

    n_components = p
    pipca = IncrementalPCA(n_components=n_components)
    pipca.partial_fit(X[:n_components])
    for idx in range(n_components, n):
        pipca.partial_fit(X[idx : idx + 1])

    assert_allclose(pca.components_, pipca.components_, atol=1e-3)

def test_incremental_pca_batch_values():
    # Test that components_ values are stable over batch sizes.
    batch_sizes = np.arange(20, 40, 3)

        assert_almost_equal(i, j, decimal=1)

def test_incremental_pca_batch_rank():
    # Test sample size in each batch is always larger or equal to n_components
    batch_sizes = np.arange(20, 90, 3)
        ipca = IncrementalPCA(n_components=20, batch_size=batch_size).fit(X)

    for components_i, components_j in itertools.pairwise(all_components):
        assert_allclose_dense_sparse(components_i, components_j)

def test_incremental_pca_partial_fit():
    # Test that fit and partial_fit get equivalent results.
    n, p = 50, 3

    batch_size = 10
    ipca = IncrementalPCA(n_components=2, batch_size=batch_size).fit(X)
    pipca = IncrementalPCA(n_components=2, batch_size=batch_size)
    # Add one to make sure endpoint is included
    batch_itr = np.arange(0, n + 1, batch_size)
    for i, j in itertools.pairwise(batch_itr):
        pipca.partial_fit(X[i:j, :])
    assert_almost_equal(ipca.components_, pipca.components_, decimal=3)

def test_incremental_pca_against_pca_iris():
    # Test that IncrementalPCA and PCA are approximate (to a sign flip).

    Y_pca = PCA(n_components=2).fit_transform(X)
    Y_ipca = IncrementalPCA(n_components=2, batch_size=25).fit_transform(X)

    assert_almost_equal(np.abs(Y_pca), np.abs(Y_ipca), 1)

def test_incremental_pca_against_pca_random_data():
    X = rng.randn(n_samples, n_features) + 5 * rng.rand(1, n_features)

    Y_pca = PCA(n_components=3).fit_transform(X)
    Y_ipca = IncrementalPCA(n_components=3, batch_size=25).fit_transform(X)

def test_explained_variances():
    # Test that PCA and IncrementalPCA calculations match
        1000, 100, tail_strength=0.0, effective_rank=10, random_state=1999
    )
    prec = 3
    for nc in [None, 99]:
        pca = PCA(n_components=nc).fit(X)
        ipca = IncrementalPCA(n_components=nc, batch_size=100).fit(X)
            pca.explained_variance_, ipca.explained_variance_, decimal=prec
        )
            pca.explained_variance_ratio_, ipca.explained_variance_ratio_, decimal=prec
        )
        assert_almost_equal(pca.noise_variance_, ipca.noise_variance_, decimal=prec)

def test_singular_values():
    # Check that the IncrementalPCA output has the correct singular values

        n_samples, n_features, tail_strength=0.0, effective_rank=10, random_state=rng
    )

    pca = PCA(n_components=10, svd_solver="full", random_state=rng).fit(X)
    ipca = IncrementalPCA(n_components=10, batch_size=100).fit(X)
    assert_array_almost_equal(pca.singular_values_, ipca.singular_values_, 2)

    # Compare to the Frobenius norm
    X_pca = pca.transform(X)
    X_ipca = ipca.transform(X)
        np.sum(pca.singular_values_**2.0), np.linalg.norm(X_pca, "fro") ** 2.0, 12
    )
        np.sum(ipca.singular_values_**2.0), np.linalg.norm(X_ipca, "fro") ** 2.0, 2
    )

        pca.singular_values_, np.sqrt(np.sum(X_pca**2.0, axis=0)), 12
    )
        ipca.singular_values_, np.sqrt(np.sum(X_ipca**2.0, axis=0)), 2
    )

    # Set the singular values and see what we get back
    n_features = 110

        n_samples, n_features, tail_strength=0.0, effective_rank=3, random_state=rng
    )

    pca = PCA(n_components=3, svd_solver="full", random_state=rng)
    ipca = IncrementalPCA(n_components=3, batch_size=100)

    X_pca /= np.sqrt(np.sum(X_pca**2.0, axis=0))
    X_pca[:, 0] *= 3.142
    X_pca[:, 1] *= 2.718

    X_hat = np.dot(X_pca, pca.components_)
    ipca.fit(X_hat)
    assert_array_almost_equal(pca.singular_values_, [3.142, 2.718, 1.0], 14)
    assert_array_almost_equal(ipca.singular_values_, [3.142, 2.718, 1.0], 14)

def test_whitening(global_random_seed):
    # Test that PCA and IncrementalPCA transforms match to sign flip.
        1000, 10, tail_strength=0.0, effective_rank=2, random_state=global_random_seed
    )
    atol = 1e-3
    for nc in [None, 9]:
        pca = PCA(whiten=True, n_components=nc).fit(X)
        ipca = IncrementalPCA(whiten=True, n_components=nc, batch_size=250).fit(X)

        # Since the data is rank deficient, some components are pure noise. We
        # should not expect those dimensions to carry any signal and their
        # values might be arbitrarily changed by implementation details of the
        # internal SVD solver. We therefore filter them out before comparison.
        stable_mask = pca.explained_variance_ratio_ > 1e-12

        Xt_pca = pca.transform(X)
        Xt_ipca = ipca.transform(X)
            np.abs(Xt_pca)[:, stable_mask],
            np.abs(Xt_ipca)[:, stable_mask],
        )

        # The noisy dimensions are in the null space of the inverse transform,
        # so they are not influencing the reconstruction. We therefore don't
        # need to apply the mask here.
        Xinv_ipca = ipca.inverse_transform(Xt_ipca)
        Xinv_pca = pca.inverse_transform(Xt_pca)
        assert_allclose(X, Xinv_ipca, atol=atol)
        assert_allclose(X, Xinv_pca, atol=atol)
        assert_allclose(Xinv_pca, Xinv_ipca, atol=atol)

def test_incremental_pca_partial_fit_float_division():
    # Test to ensure float division is used in all versions of Python
    # (non-regression test for issue #9489)

    A = rng.randn(5, 3) + 2
    B = rng.randn(7, 3) + 5

    pca = IncrementalPCA(n_components=2)
    pca.partial_fit(A)
    # Set n_samples_seen_ to be a floating point number instead of an int
    pca.n_samples_seen_ = float(pca.n_samples_seen_)
    pca.partial_fit(B)
    singular_vals_float_samples_seen = pca.singular_values_

    pca2 = IncrementalPCA(n_components=2)
    pca2.partial_fit(A)
    pca2.partial_fit(B)
    singular_vals_int_samples_seen = pca2.singular_values_

        singular_vals_float_samples_seen, singular_vals_int_samples_seen
    )

def test_incremental_pca_fit_overflow_error():
    # Test for overflow error on Windows OS
    # (non-regression test for issue #17693)
    A = rng.rand(500000, 2)

    ipca = IncrementalPCA(n_components=2, batch_size=10000)
    ipca.fit(A)

    pca.fit(A)

    np.testing.assert_allclose(ipca.singular_values_, pca.singular_values_)

def test_incremental_pca_feature_names_out():
    """Check feature names out for IncrementalPCA."""
    ipca = IncrementalPCA(n_components=2).fit(iris.data)

    names = ipca.get_feature_names_out()
    assert_array_equal([f"incrementalpca{i}" for i in range(2)], names)

def generate_toy_data(n_components, n_samples, image_size, random_state=None):
    n_features = image_size[0] * image_size[1]

    U = rng.randn(n_samples, n_components)
    V = rng.randn(n_components, n_features)

    centers = [(3, 3), (6, 7), (8, 1)]
    sz = [1, 2, 1]
        img = np.zeros(image_size)
        xmin, xmax = centers[k][0] - sz[k], centers[k][0] + sz[k]
        ymin, ymax = centers[k][1] - sz[k], centers[k][1] + sz[k]
        img[xmin:xmax][:, ymin:ymax] = 1.0
        V[k, :] = img.ravel()

    # Y is defined by : Y = UV + noise
    Y = np.dot(U, V)
    Y += 0.1 * rng.randn(Y.shape[0], Y.shape[1])  # Add noise
    return Y, U, V

def test_correct_shapes():
    X = rng.randn(12, 10)
    spca = SparsePCA(n_components=8, random_state=rng)
    U = spca.fit_transform(X)
    assert spca.components_.shape == (8, 10)
    assert U.shape == (12, 8)
    # test overcomplete decomposition
    spca = SparsePCA(n_components=13, random_state=rng)
    assert spca.components_.shape == (13, 10)
    assert U.shape == (12, 13)

def test_fit_transform(global_random_seed):
    alpha = 1
    Y, _, _ = generate_toy_data(3, 10, (8, 8), random_state=rng)  # wide array
    spca_lars = SparsePCA(
        n_components=3, method="lars", alpha=alpha, random_state=global_random_seed
    )
    spca_lars.fit(Y)

    # Test that CD gives similar results
    spca_lasso = SparsePCA(
        n_components=3, method="cd", random_state=global_random_seed, alpha=alpha
    )
    spca_lasso.fit(Y)
    assert_array_almost_equal(spca_lasso.components_, spca_lars.components_)

def test_fit_transform_parallel(global_random_seed):
    alpha = 1
    )
    U1 = spca_lars.transform(Y)
    # Test multiple CPUs
    spca = SparsePCA(
        n_components=3,
        n_jobs=2,
    ).fit(Y)
    U2 = spca.transform(Y)
    assert not np.all(spca_lars.components_ == 0)
    assert_array_almost_equal(U1, U2)

def test_transform_nan(global_random_seed):
    # Test that SparsePCA won't return NaN when there is 0 feature in all
    # samples.
    Y[:, 0] = 0
    estimator = SparsePCA(n_components=8, random_state=global_random_seed)
    assert not np.any(np.isnan(estimator.fit_transform(Y)))

def test_fit_transform_tall(global_random_seed):
    Y, _, _ = generate_toy_data(3, 65, (8, 8), random_state=rng)  # tall array
    spca_lars = SparsePCA(n_components=3, method="lars", random_state=rng)
    U1 = spca_lars.fit_transform(Y)
    spca_lasso = SparsePCA(n_components=3, method="cd", random_state=rng)
    U2 = spca_lasso.fit(Y).transform(Y)

def test_initialization(global_random_seed):
    U_init = rng.randn(5, 3)
    V_init = rng.randn(3, 4)
    model = SparsePCA(
        n_components=3, U_init=U_init, V_init=V_init, max_iter=0, random_state=rng
    )
    model.fit(rng.randn(5, 4))

    expected_components = V_init / np.linalg.norm(V_init, axis=1, keepdims=True)
    expected_components = svd_flip(u=expected_components.T, v=None)[0].T
    assert_allclose(model.components_, expected_components)

def test_mini_batch_correct_shapes():
    pca = MiniBatchSparsePCA(n_components=8, max_iter=1, random_state=rng)
    U = pca.fit_transform(X)
    assert pca.components_.shape == (8, 10)
    pca = MiniBatchSparsePCA(n_components=13, max_iter=1, random_state=rng)
    assert pca.components_.shape == (13, 10)

def test_scaling_fit_transform(global_random_seed):
    alpha = 1
    Y, _, _ = generate_toy_data(3, 1000, (8, 8), random_state=rng)
    spca_lars = SparsePCA(n_components=3, method="lars", alpha=alpha, random_state=rng)
    results_train = spca_lars.fit_transform(Y)
    results_test = spca_lars.transform(Y[:10])
    assert_allclose(results_train[0], results_test[0])

def test_pca_vs_spca(global_random_seed):
    Z, _, _ = generate_toy_data(3, 10, (8, 8), random_state=rng)
    spca = SparsePCA(alpha=0, ridge_alpha=0, n_components=2, random_state=rng)
    pca = PCA(n_components=2, random_state=rng)
    pca.fit(Y)
    spca.fit(Y)
    results_test_pca = pca.transform(Z)
    results_test_spca = spca.transform(Z)
        np.abs(spca.components_.dot(pca.components_.T)), np.eye(2), atol=1e-4
    )
    results_test_pca *= np.sign(results_test_pca[0, :])
    results_test_spca *= np.sign(results_test_spca[0, :])
    assert_allclose(results_test_pca, results_test_spca, atol=1e-4)

def test_spca_n_components_(SPCA, n_components):
    n_samples, n_features = 12, 10

    model = SPCA(n_components=n_components).fit(X)

        assert model.n_components_ == n_components
    else:
        assert model.n_components_ == n_features

def test_sparse_pca_dtype_match(SPCA, method, data_type, expected_type):
    # Verify output matrix dtype
    n_samples, n_features, n_components = 12, 10, 3
    input_array = rng.randn(n_samples, n_features).astype(data_type)
    model = SPCA(n_components=n_components, method=method)
    transformed = model.fit_transform(input_array)

    assert transformed.dtype == expected_type
    assert model.components_.dtype == expected_type

def test_sparse_pca_numerical_consistency(SPCA, method, global_random_seed):
    # Verify numericall consistentency among np.float32 and np.float64
    n_samples, n_features, n_components = 20, 20, 5
    input_array = make_low_rank_matrix(
        effective_rank=n_components,
    )

    model_32 = SPCA(
        method=method,
    )
    transformed_32 = model_32.fit_transform(input_array.astype(np.float32))

    model_64 = SPCA(
    )
    transformed_64 = model_64.fit_transform(input_array.astype(np.float64))
    assert_allclose(transformed_64, transformed_32)
    assert_allclose(model_64.components_, model_32.components_)

def test_spca_feature_names_out(SPCA):
    """Check feature names out for *SparsePCA."""

    model = SPCA(n_components=4).fit(X)
    names = model.get_feature_names_out()

    estimator_name = SPCA.__name__.lower()
    assert_array_equal([f"{estimator_name}{i}" for i in range(4)], names)

def test_spca_early_stopping(global_random_seed):
    """Check that `tol` and `max_no_improvement` act as early stopping."""
    n_samples, n_features = 50, 10

    # vary the tolerance to force the early stopping of one of the model
    model_early_stopped = MiniBatchSparsePCA(
        max_iter=100, tol=0.5, random_state=global_random_seed
    ).fit(X)
    model_not_early_stopped = MiniBatchSparsePCA(
        max_iter=100, tol=1e-3, random_state=global_random_seed
    ).fit(X)
    assert model_early_stopped.n_iter_ < model_not_early_stopped.n_iter_

    # force the max number of no improvement to a large value to check that
    # it does help to early stop
        max_iter=100, tol=1e-6, max_no_improvement=2, random_state=global_random_seed
    ).fit(X)
        max_iter=100, tol=1e-6, max_no_improvement=100, random_state=global_random_seed
    ).fit(X)

def test_equivalence_components_pca_spca(global_random_seed):
    """Check the equivalence of the components found by PCA and SparsePCA.

    https://github.com/scikit-learn/scikit-learn/issues/23932
    """
    X = rng.randn(50, 4)

    n_components = 2
    pca = PCA(
    ).fit(X)
        ridge_alpha=0,
        alpha=0,
    ).fit(X)

    assert_allclose(pca.components_, spca.components_)

def test_sparse_pca_inverse_transform(global_random_seed):
    """Check that `inverse_transform` in `SparsePCA` and `PCA` are similar."""

        alpha=1e-12,
        ridge_alpha=1e-12,
    )
    pca = PCA(n_components=n_components, random_state=global_random_seed)
    X_trans_spca = spca.fit_transform(X)
    X_trans_pca = pca.fit_transform(X)
        spca.inverse_transform(X_trans_spca), pca.inverse_transform(X_trans_pca)
    )

def test_transform_inverse_transform_round_trip(SPCA, global_random_seed):
    """Check the `transform` and `inverse_transform` round trip with no loss of
    information.
    """

    n_components = n_features
    spca = SPCA(
    )
    assert_allclose(spca.inverse_transform(X_trans_spca), X)

def test_kernel_pca(global_random_seed):
    """Nominal test for all solvers and all known kernels + a custom one

    It tests
     - that fit_transform is equivalent to fit+transform
     - that the shapes of transforms and inverse transforms are correct
    """
    X_fit = rng.random_sample((5, 4))
    X_pred = rng.random_sample((2, 4))

    def histogram(x, y, **kwargs):
        # Histogram kernel implemented as a callable.
        assert kwargs == {}  # no kernel_params that we didn't ask for
        return np.minimum(x, y).sum()

    for eigen_solver in ("auto", "dense", "arpack", "randomized"):
        for kernel in ("linear", "rbf", "poly", histogram):
            # histogram kernel produces singular matrix inside linalg.solve
            # XXX use a least-squares approximation?
            inv = not callable(kernel)

            # transform fit data
            kpca = KernelPCA(
                4, kernel=kernel, eigen_solver=eigen_solver, fit_inverse_transform=inv
            )
            X_fit_transformed = kpca.fit_transform(X_fit)
            X_fit_transformed2 = kpca.fit(X_fit).transform(X_fit)
                np.abs(X_fit_transformed), np.abs(X_fit_transformed2)
            )

            # non-regression test: previously, gamma would be 0 by default,
            # forcing all eigenvalues to 0 under the poly kernel
            assert X_fit_transformed.size != 0

            # transform new data
            X_pred_transformed = kpca.transform(X_pred)
            assert X_pred_transformed.shape[1] == X_fit_transformed.shape[1]

            # inverse transform
            if inv:
                X_pred2 = kpca.inverse_transform(X_pred_transformed)
                assert X_pred2.shape == X_pred.shape

def test_kernel_pca_invalid_parameters():
    """Check that kPCA raises an error if the parameters are invalid

    Tests fitting inverse transform with a precomputed kernel raises a
    ValueError.
    """
    estimator = KernelPCA(
        n_components=10, fit_inverse_transform=True, kernel="precomputed"
    )
    err_ms = "Cannot fit_inverse_transform with a precomputed kernel"
    with pytest.raises(ValueError, match=err_ms):
        estimator.fit(np.random.randn(10, 10))

def test_kernel_pca_consistent_transform(global_random_seed):
    """Check robustness to mutations in the original training array

    Test that after fitting a kPCA model, it stays independent of any
    mutation of the values of the original data object by relying on an
    internal copy.
    """
    # X_fit_ needs to retain the old, unmodified copy of X
    state = np.random.RandomState(global_random_seed)
    X = state.rand(10, 10)
    kpca = KernelPCA(random_state=state).fit(X)
    transformed1 = kpca.transform(X)

    X_copy = X.copy()
    X[:, 0] = 666
    transformed2 = kpca.transform(X_copy)
    assert_array_almost_equal(transformed1, transformed2)

def test_kernel_pca_deterministic_output(global_random_seed):
    """Test that Kernel PCA produces deterministic output

    Tests that the same inputs and random state produce the same output.
    """
    eigen_solver = ("arpack", "dense")

    for solver in eigen_solver:
        transformed_X = np.zeros((20, 2))
        for i in range(20):
            kpca = KernelPCA(n_components=2, eigen_solver=solver, random_state=rng)
            transformed_X[i, :] = kpca.fit_transform(X)[0]
        assert_allclose(transformed_X, np.tile(transformed_X[0, :], 20).reshape(20, 2))

def test_kernel_pca_sparse(csr_container, global_random_seed):
    """Test that kPCA works on a sparse data input.

    Same test as ``test_kernel_pca except inverse_transform`` since it's not
    implemented for sparse matrices.
    """
    X_fit = csr_container(rng.random_sample((5, 4)))
    X_pred = csr_container(rng.random_sample((2, 4)))

    for eigen_solver in ("auto", "arpack", "randomized"):
        for kernel in ("linear", "rbf", "poly"):
                4,
                kernel=kernel,
                eigen_solver=eigen_solver,
                fit_inverse_transform=False,
            )
            )

            # inverse transform: not available for sparse matrices
            # XXX: should we raise another exception type here? For instance:
            # NotImplementedError.
            with pytest.raises(NotFittedError):
                kpca.inverse_transform(X_pred_transformed)

def test_kernel_pca_linear_kernel(solver, n_features, global_random_seed):
    """Test that kPCA with linear kernel is equivalent to PCA for all solvers.

    KernelPCA with linear kernel should produce the same output as PCA.
    """
    X_fit = rng.random_sample((5, n_features))
    X_pred = rng.random_sample((2, n_features))

    # for a linear kernel, kernel PCA should find the same projection as PCA
    # modulo the sign (direction)
    # fit only the first four components: fifth is near zero eigenvalue, so
    # can be trimmed due to roundoff error
    n_comps = 3 if solver == "arpack" else 4
        np.abs(KernelPCA(n_comps, eigen_solver=solver).fit(X_fit).transform(X_pred)),
        np.abs(
            PCA(n_comps, svd_solver=solver if solver != "dense" else "full")
            .fit(X_fit)
            .transform(X_pred)
        ),
    )

def test_kernel_pca_n_components():
    """Test that `n_components` is correctly taken into account for projections

    For all solvers this tests that the output has the correct shape depending
    on the selected number of components.
    """

    for eigen_solver in ("dense", "arpack", "randomized"):
        for c in [1, 2, 4]:
            kpca = KernelPCA(n_components=c, eigen_solver=eigen_solver)
            shape = kpca.fit(X_fit).transform(X_pred).shape

            assert shape == (2, c)

def test_remove_zero_eig():
    """Check that the ``remove_zero_eig`` parameter works correctly.

    Tests that the null-space (Zero) eigenvalues are removed when
    remove_zero_eig=True, whereas they are not by default.
    """
    X = np.array([[1 - 1e-30, 1], [1, 1], [1, 1 - 1e-20]])

    # n_components=None (default) => remove_zero_eig is True
    kpca = KernelPCA()
    Xt = kpca.fit_transform(X)
    assert Xt.shape == (3, 0)

    kpca = KernelPCA(n_components=2)
    assert Xt.shape == (3, 2)

    kpca = KernelPCA(n_components=2, remove_zero_eig=True)

def test_leave_zero_eig():
    """Non-regression test for issue #12141 (PR #12143)

    This test checks that fit().transform() returns the same result as
    fit_transform() in case of non-removed zero eigenvalue.
    """
    X_fit = np.array([[1, 1], [0, 0]])

    # Assert that even with all np warnings on, there is no div by zero warning
        # There might be warnings about the kernel being badly conditioned,
        # but there should not be warnings about division by zero.
        # (Numpy division by zero warning can have many message variants, but
        # at least we know that it is a RuntimeWarning so lets check only this)
        with np.errstate(all="warn"):
            k = KernelPCA(n_components=2, remove_zero_eig=False, eigen_solver="dense")
            # Fit, then transform
            A = k.fit(X_fit).transform(X_fit)
            # Do both at once
            B = k.fit_transform(X_fit)
            # Compare
            assert_array_almost_equal(np.abs(A), np.abs(B))

def test_kernel_pca_precomputed(global_random_seed):
    """Test that kPCA works with a precomputed kernel, for all solvers"""

        X_kpca = (
            KernelPCA(4, eigen_solver=eigen_solver, random_state=0)
        )

        X_kpca2 = (
            KernelPCA(
                4, eigen_solver=eigen_solver, kernel="precomputed", random_state=0
            )
            .fit(np.dot(X_fit, X_fit.T))
            .transform(np.dot(X_pred, X_fit.T))
        )

        X_kpca_train = KernelPCA(
            4, eigen_solver=eigen_solver, kernel="precomputed", random_state=0
        ).fit_transform(np.dot(X_fit, X_fit.T))

        X_kpca_train2 = (
            )
            .transform(np.dot(X_fit, X_fit.T))
        )

        assert_array_almost_equal(np.abs(X_kpca), np.abs(X_kpca2))

        assert_array_almost_equal(np.abs(X_kpca_train), np.abs(X_kpca_train2))

def test_kernel_pca_precomputed_non_symmetric(solver):
    """Check that the kernel centerer works.

    Tests that a non symmetric precomputed kernel is actually accepted
    because the kernel centerer does its job correctly.
    """

    # a non symmetric gram matrix
    K = [[1, 2], [3, 40]]
    kpca = KernelPCA(
        kernel="precomputed", eigen_solver=solver, n_components=1, random_state=0
    )
    kpca.fit(K)  # no error

    # same test with centered kernel
    Kc = [[9, -9], [-9, 9]]
    kpca_c = KernelPCA(
    )
    kpca_c.fit(Kc)

    # comparison between the non-centered and centered versions
    assert_array_equal(kpca.eigenvectors_, kpca_c.eigenvectors_)
    assert_array_equal(kpca.eigenvalues_, kpca_c.eigenvalues_)

def test_gridsearch_pipeline():
    """Check that kPCA works as expected in a grid search pipeline

    Test if we can do a grid-search to find parameters to separate
    circles with a perceptron model.
    """
    X, y = make_circles(n_samples=400, factor=0.3, noise=0.05, random_state=0)
    kpca = KernelPCA(kernel="rbf", n_components=2)
    pipeline = Pipeline([("kernel_pca", kpca), ("Perceptron", Perceptron(max_iter=5))])
    param_grid = dict(kernel_pca__gamma=2.0 ** np.arange(-2, 2))
    grid_search = GridSearchCV(pipeline, cv=3, param_grid=param_grid)
    grid_search.fit(X, y)
    assert grid_search.best_score_ == 1

def test_gridsearch_pipeline_precomputed():
    """Check that kPCA works as expected in a grid search pipeline (2)

    circles with a perceptron model. This test uses a precomputed kernel.
    """
    kpca = KernelPCA(kernel="precomputed", n_components=2)
    param_grid = dict(Perceptron__max_iter=np.arange(1, 5))
    X_kernel = rbf_kernel(X, gamma=2.0)
    grid_search.fit(X_kernel, y)

def test_nested_circles():
    """Check that kPCA projects in a space where nested circles are separable

    Tests that 2D nested circles become separable with a perceptron when
    projected in the first 2 kPCA using an RBF kernel, while raw samples
    are not directly separable in the original space.
    """

    # 2D nested circles are not linearly separable
    train_score = Perceptron(max_iter=5).fit(X, y).score(X, y)
    assert train_score < 0.8

    # Project the circles data into the first 2 components of a RBF Kernel
    # PCA model.
    # Note that the gamma value is data dependent. If this test breaks
    # and the gamma value has to be updated, the Kernel PCA example will
    # have to be updated too.
        kernel="rbf", n_components=2, fit_inverse_transform=True, gamma=2.0
    )
    X_kpca = kpca.fit_transform(X)

    # The data is perfectly linearly separable in that space
    train_score = Perceptron(max_iter=5).fit(X_kpca, y).score(X_kpca, y)
    assert train_score == 1.0

def test_kernel_conditioning():
    """Check that ``_check_psd_eigenvalues`` is correctly called in kPCA

    Non-regression test for issue #12140 (PR #12145).
    """

    # create a pathological X leading to small non-zero eigenvalue
    X = [[5, 1], [5 + 1e-8, 1e-8], [5 + 1e-8, 0]]
    kpca = KernelPCA(kernel="linear", n_components=2, fit_inverse_transform=True)
    kpca.fit(X)

    # check that the small non-zero eigenvalue was correctly set to zero
    assert kpca.eigenvalues_.min() == 0
    assert np.all(kpca.eigenvalues_ == _check_psd_eigenvalues(kpca.eigenvalues_))

def test_precomputed_kernel_not_psd(solver):
    """Check how KernelPCA works with non-PSD kernels depending on n_components

    Tests for all methods what happens with a non PSD gram matrix (this
    can happen in an isomap scenario, or with custom kernel functions, or
    maybe with ill-posed datasets).

    When ``n_component`` is large enough to capture a negative eigenvalue, an
    error should be raised. Otherwise, KernelPCA should run without error
    since the negative eigenvalues are not selected.
    """

    # a non PSD kernel with large eigenvalues, already centered
    # it was captured from an isomap call and multiplied by 100 for compacity
    K = [
        [4.48, -1.0, 8.07, 2.33, 2.33, 2.33, -5.76, -12.78],
        [-1.0, -6.48, 4.5, -1.24, -1.24, -1.24, -0.81, 7.49],
        [8.07, 4.5, 15.48, 2.09, 2.09, 2.09, -11.1, -23.23],
        [2.33, -1.24, 2.09, 4.0, -3.65, -3.65, 1.02, -0.9],
        [2.33, -1.24, 2.09, -3.65, 4.0, -3.65, 1.02, -0.9],
        [2.33, -1.24, 2.09, -3.65, -3.65, 4.0, 1.02, -0.9],
        [-5.76, -0.81, -11.1, 1.02, 1.02, 1.02, 4.86, 9.75],
        [-12.78, 7.49, -23.23, -0.9, -0.9, -0.9, 9.75, 21.46],
    ]
    # this gram matrix has 5 positive eigenvalues and 3 negative ones
    # [ 52.72,   7.65,   7.65,   5.02,   0.  ,  -0.  ,  -6.13, -15.11]

    # 1. ask for enough components to get a significant negative one
    kpca = KernelPCA(kernel="precomputed", eigen_solver=solver, n_components=7)
    # make sure that the appropriate error is raised
    with pytest.raises(ValueError, match="There are significant negative eigenvalues"):
        kpca.fit(K)

    # 2. ask for a small enough n_components to get only positive ones
    kpca = KernelPCA(kernel="precomputed", eigen_solver=solver, n_components=2)
    if solver == "randomized":
        # the randomized method is still inconsistent with the others on this
        # since it selects the eigenvalues based on the largest 2 modules, not
        # on the largest 2 values.
        #
        # At least we can ensure that we return an error instead of returning
        # the wrong eigenvalues
            ValueError, match="There are significant negative eigenvalues"
        ):
            kpca.fit(K)
    else:
        # general case: make sure that it works

def test_kernel_pca_solvers_equivalence(n_components):
    """Check that 'dense' 'arpack' & 'randomized' solvers give similar results"""

    # Generate random data
    n_train, n_test = 1_000, 100
    X, _ = make_circles(
        n_samples=(n_train + n_test), factor=0.3, noise=0.05, random_state=0
    )
    X_fit, X_pred = X[:n_train, :], X[n_train:, :]

    # reference (full)
    ref_pred = (
        KernelPCA(n_components, eigen_solver="dense", random_state=0)
        .fit(X_fit)
        .transform(X_pred)
    )

    # arpack
    a_pred = (
        KernelPCA(n_components, eigen_solver="arpack", random_state=0)
    )
    # check that the result is still correct despite the approx
    assert_array_almost_equal(np.abs(a_pred), np.abs(ref_pred))

    # randomized
    r_pred = (
        KernelPCA(n_components, eigen_solver="randomized", random_state=0)
    )
    # check that the result is still correct despite the approximation
    assert_array_almost_equal(np.abs(r_pred), np.abs(ref_pred))

def test_kernel_pca_inverse_transform_reconstruction():
    """Test if the reconstruction is a good approximation.

    Note that in general it is not possible to get an arbitrarily good
    reconstruction because of kernel centering that does not
    preserve all the information of the original data.
    """
    X, *_ = make_blobs(n_samples=100, n_features=4, random_state=0)

        n_components=20, kernel="rbf", fit_inverse_transform=True, alpha=1e-3
    )
    X_trans = kpca.fit_transform(X)
    X_reconst = kpca.inverse_transform(X_trans)
    assert np.linalg.norm(X - X_reconst) / np.linalg.norm(X) < 1e-1

def test_kernel_pca_raise_not_fitted_error():
    X = np.random.randn(15).reshape(5, 3)
        kpca.inverse_transform(X)

def test_32_64_decomposition_shape():
    """Test that the decomposition is similar for 32 and 64 bits data

    Non regression test for
    https://github.com/scikit-learn/scikit-learn/issues/18146
    """
        n_samples=30, centers=[[0, 0, 0], [1, 1, 1]], random_state=0, cluster_std=0.1
    )
    X = StandardScaler().fit_transform(X)
    X -= X.min()

    # Compare the shapes (corresponds to the number of non-zero eigenvalues)
    assert kpca.fit_transform(X).shape == kpca.fit_transform(X.astype(np.float32)).shape

def test_kernel_pca_feature_names_out():
    """Check feature names out for KernelPCA."""
    kpca = KernelPCA(n_components=2).fit(X)

    names = kpca.get_feature_names_out()
    assert_array_equal([f"kernelpca{i}" for i in range(2)], names)

def test_kernel_pca_inverse_correct_gamma(global_random_seed):
    """Check that gamma is set correctly when not provided.

    Non-regression test for #26280
    """

    kwargs = {
        "n_components": 2,
        "random_state": rng,
        "fit_inverse_transform": True,
        "kernel": "rbf",
    }

    expected_gamma = 1 / X.shape[1]
    kpca1 = KernelPCA(gamma=None, **kwargs).fit(X)
    kpca2 = KernelPCA(gamma=expected_gamma, **kwargs).fit(X)

    assert kpca1.gamma_ == expected_gamma
    assert kpca2.gamma_ == expected_gamma

    X1_recon = kpca1.inverse_transform(kpca1.transform(X))
    X2_recon = kpca2.inverse_transform(kpca1.transform(X))

    assert_allclose(X1_recon, X2_recon)

def test_kernel_pca_pandas_output():
    """Check that KernelPCA works with pandas output when the solver is arpack.

    https://github.com/scikit-learn/scikit-learn/issues/27579
    """
    X, _ = load_iris(as_frame=True, return_X_y=True)
    with sklearn.config_context(transform_output="pandas"):
        KernelPCA(n_components=2, eigen_solver="arpack").fit_transform(X)

def center_and_norm(x, axis=-1):
    """Centers and norms x **in place**

    -----------
    x: ndarray
        Array with an axis of observations (statistical units) measured on
        random variables.
    axis: int, optional
        Axis along which the mean and variance are calculated.
    """
    x = np.rollaxis(x, axis)
    x -= x.mean(axis=0)
    x /= x.std(axis=0)

def test_gs(global_random_seed):
    # Test gram schmidt orthonormalization
    # generate a random orthogonal  matrix
    W, _, _ = np.linalg.svd(rng.randn(10, 10))
    w = rng.randn(10)
    _gs_decorrelation(w, W, 10)
    assert (w**2).sum() < 1.0e-10
    u = _gs_decorrelation(w, W, 5)
    tmp = np.dot(u, W.T)
    assert (tmp[:5] ** 2).sum() < 1.0e-10

def test_fastica_attributes_dtypes(global_dtype):
    fica = FastICA(
        n_components=5, max_iter=1000, whiten="unit-variance", random_state=0
    ).fit(X)
    assert fica.components_.dtype == global_dtype
    assert fica.mixing_.dtype == global_dtype
    assert fica.mean_.dtype == global_dtype
    assert fica.whitening_.dtype == global_dtype

def test_fastica_return_dtypes(global_dtype):
    k_, mixing_, s_ = fastica(
        X, max_iter=1000, whiten="unit-variance", random_state=rng
    )
    assert k_.dtype == global_dtype
    assert mixing_.dtype == global_dtype
    assert s_.dtype == global_dtype

def test_fastica_simple(add_noise, global_random_seed, global_dtype):
    if (
        global_random_seed == 20
        and global_dtype == np.float32
        and not add_noise
        and os.getenv("DISTRIB") == "ubuntu"
    ):
        pytest.xfail(
            "FastICA instability with Ubuntu Atlas build with float32 "
            "global_dtype. For more details, see "
            "https://github.com/scikit-learn/scikit-learn/issues/24131#issuecomment-1208091119"
        )

    # Test the FastICA algorithm on very simple data.
    # Generate two sources:
    s1 = (2 * np.sin(np.linspace(0, 100, n_samples)) > 0) - 1
    s2 = stats.t.rvs(1, size=n_samples, random_state=global_random_seed)
    s = np.c_[s1, s2].T
    center_and_norm(s)
    s = s.astype(global_dtype)
    s1, s2 = s

    # Mixing angle
    phi = 0.6
    mixing = np.array([[np.cos(phi), np.sin(phi)], [np.sin(phi), -np.cos(phi)]])
    mixing = mixing.astype(global_dtype)
    m = np.dot(mixing, s)

    if add_noise:
        m += 0.1 * rng.randn(2, 1000)

    center_and_norm(m)

    # function as fun arg
    def g_test(x):
        return x**3, (3 * x**2).mean(axis=-1)

    algos = ["parallel", "deflation"]
    nls = ["logcosh", "exp", "cube", g_test]
    whitening = ["arbitrary-variance", "unit-variance", False]
    for algo, nl, whiten in itertools.product(algos, nls, whitening):
        if whiten:
            k_, mixing_, s_ = fastica(
                m.T, fun=nl, whiten=whiten, algorithm=algo, random_state=rng
            )
                fastica(m.T, fun=np.tanh, whiten=whiten, algorithm=algo)
        else:
            pca = PCA(n_components=2, whiten=True, random_state=rng)
            X = pca.fit_transform(m.T)
                X, fun=nl, algorithm=algo, whiten=False, random_state=rng
            )
                fastica(X, fun=np.tanh, algorithm=algo)
        s_ = s_.T
        # Check that the mixing model described in the docstring holds:
        if whiten:
            # XXX: exact reconstruction to standard relative tolerance is not
            # possible. This is probably expected when add_noise is True but we
            # also need a non-trivial atol in float32 when add_noise is False.
            #
            # Note that the 2 sources are non-Gaussian in this test.
            atol = 1e-5 if global_dtype == np.float32 else 0
            assert_allclose(np.dot(np.dot(mixing_, k_), m), s_, atol=atol)

        center_and_norm(s_)
        s1_, s2_ = s_
        # Check to see if the sources have been estimated
        # in the wrong order
        if abs(np.dot(s1_, s2)) > abs(np.dot(s1_, s1)):
            s2_, s1_ = s_
        s1_ *= np.sign(np.dot(s1_, s1))
        s2_ *= np.sign(np.dot(s2_, s2))

        # Check that we have estimated the original sources
        if not add_noise:
            assert_allclose(np.dot(s1_, s1) / n_samples, 1, atol=1e-2)
            assert_allclose(np.dot(s2_, s2) / n_samples, 1, atol=1e-2)
        else:
            assert_allclose(np.dot(s1_, s1) / n_samples, 1, atol=1e-1)
            assert_allclose(np.dot(s2_, s2) / n_samples, 1, atol=1e-1)

    # Test FastICA class
    _, _, sources_fun = fastica(
        m.T, fun=nl, algorithm=algo, random_state=global_random_seed
    )
    ica = FastICA(fun=nl, algorithm=algo, random_state=global_random_seed)
    sources = ica.fit_transform(m.T)
    assert ica.components_.shape == (2, 2)
    assert sources.shape == (1000, 2)

    assert_allclose(sources_fun, sources)
    # Set atol to account for the different magnitudes of the elements in sources
    # (from 1e-4 to 1e1).
    atol = np.max(np.abs(sources)) * (1e-5 if global_dtype == np.float32 else 1e-7)
    assert_allclose(sources, ica.transform(m.T), atol=atol)

    assert ica.mixing_.shape == (2, 2)

    ica = FastICA(fun=np.tanh, algorithm=algo)
        ica.fit(m.T)

def test_fastica_nowhiten():
    m = [[0, 1], [1, 0]]

    # test for issue #697
    ica = FastICA(n_components=1, whiten=False, random_state=0)
    warn_msg = "Ignoring n_components with whiten=False."
        ica.fit(m)
    assert hasattr(ica, "mixing_")

def test_fastica_convergence_fail(global_random_seed):
    # Test the FastICA algorithm on very simple data
    # (see test_non_square_fastica).
    # Ensure a ConvergenceWarning raised if the tolerance is sufficiently low.

    t = np.linspace(0, 100, n_samples)
    s1 = np.sin(t)
    s2 = np.ceil(np.sin(np.pi * t))

    # Mixing matrix
    mixing = rng.randn(6, 2)

    # Do fastICA with tolerance 0. to ensure failing convergence
        "FastICA did not converge. Consider increasing tolerance "
        "or the maximum number of iterations."
    )
        ica = FastICA(
            algorithm="parallel", n_components=2, random_state=rng, max_iter=2, tol=0.0
        )

def test_non_square_fastica(global_random_seed, add_noise):

    s1, s2 = s

        m += 0.1 * rng.randn(6, n_samples)

        m.T, n_components=2, whiten="unit-variance", random_state=rng
    )
    s_ = s_.T

    # Check that the mixing model described in the docstring holds:
    assert_allclose(s_, np.dot(np.dot(mixing_, k_), m))

    center_and_norm(s_)
    s1_, s2_ = s_
    # Check to see if the sources have been estimated
    # in the wrong order
    if abs(np.dot(s1_, s2)) > abs(np.dot(s1_, s1)):
        s2_, s1_ = s_
    s1_ *= np.sign(np.dot(s1_, s1))
    s2_ *= np.sign(np.dot(s2_, s2))

    # Check that we have estimated the original sources
    if not add_noise:
        assert_allclose(np.dot(s1_, s1) / n_samples, 1, atol=1e-3)
        assert_allclose(np.dot(s2_, s2) / n_samples, 1, atol=1e-3)

def test_fit_transform(global_random_seed, global_dtype):
    """Test unit variance of transformed data using FastICA algorithm.

    Check that `fit_transform` gives the same result as applying
    `fit` and then `transform`.

    Bug #13056
    """
    # multivariate uniform data in [0, 1]
    X = rng.random_sample((100, 10)).astype(global_dtype)
    max_iter = 300
    for whiten, n_components in [["unit-variance", 5], [False, None]]:
        n_components_ = n_components if n_components is not None else X.shape[1]

            n_components=n_components, max_iter=max_iter, whiten=whiten, random_state=0
        )
            # make sure that numerical errors do not cause sqrt of negative
            # values
            warnings.simplefilter("error", RuntimeWarning)
            # XXX: for some seeds, the model does not converge.
            # However this is not what we test here.
            Xt = ica.fit_transform(X)
        assert ica.components_.shape == (n_components_, 10)
        assert Xt.shape == (X.shape[0], n_components_)

        ica2 = FastICA(
        )
            # values
            ica2.fit(X)
        assert ica2.components_.shape == (n_components_, 10)
        Xt2 = ica2.transform(X)

        # XXX: we have to set atol for this test to pass for all seeds when
        # fitting with float32 data. Is this revealing a bug?
        if global_dtype:
            atol = np.abs(Xt2).mean() / 1e6
        else:
            atol = 0.0  # the default rtol is enough for float64 data
        assert_allclose(Xt, Xt2, atol=atol)

def test_inverse_transform(
    whiten, n_components, expected_mixing_shape, global_random_seed, global_dtype
):
    # Test FastICA.inverse_transform
    X = rng.random_sample((n_samples, 10)).astype(global_dtype)

    ica = FastICA(n_components=n_components, random_state=rng, whiten=whiten)
        # For some dataset (depending on the value of global_dtype) the model
        # can fail to converge but this should not impact the definition of
        # a valid inverse transform.
        Xt = ica.fit_transform(X)
    assert ica.mixing_.shape == expected_mixing_shape
    X2 = ica.inverse_transform(Xt)
    assert X.shape == X2.shape

    # reversibility test in non-reduction case
    if n_components == X.shape[1]:
            # XXX: dividing by a smaller number makes
            # tests fail for some seeds.
            atol = np.abs(X2).mean() / 1e5
        else:
        assert_allclose(X, X2, atol=atol)

def test_fastica_errors():
    w_init = rng.randn(n_features + 1, n_features + 1)
    with pytest.raises(ValueError, match=r"alpha must be in \[1,2\]"):
        fastica(X, fun_args={"alpha": 0})
        ValueError, match=r"w_init has invalid shape.+should be \(3L?, 3L?\)"
    ):
        fastica(X, w_init=w_init)

def test_fastica_whiten_unit_variance(global_random_seed):

    Bug #13056
    """
    ica = FastICA(n_components=n_components, whiten="unit-variance", random_state=0)
    Xt = ica.fit_transform(X)

    assert np.var(Xt) == pytest.approx(1.0)

def test_fastica_output_shape(whiten, return_X_mean, return_n_iter):

    expected_len = 3 + return_X_mean + return_n_iter

    out = fastica(
        X, whiten=whiten, return_n_iter=return_n_iter, return_X_mean=return_X_mean
    )

    assert len(out) == expected_len
    if not whiten:
        assert out[0] is None

def test_fastica_simple_different_solvers(add_noise, global_random_seed):
    """Test FastICA is consistent between whiten_solvers."""
    s2 = stats.t.rvs(1, size=n_samples, random_state=rng)
    s1, s2 = s

    phi = rng.rand() * 2 * np.pi

    outs = {}
    for solver in ("svd", "eigh"):
        ica = FastICA(random_state=0, whiten="unit-variance", whiten_solver=solver)
        sources = ica.fit_transform(m.T)
        outs[solver] = sources
        assert ica.components_.shape == (2, 2)
        assert sources.shape == (1000, 2)

    # compared numbers are not all on the same magnitude. Using a small atol to
    # make the test less brittle
    assert_allclose(outs["eigh"], outs["svd"], atol=1e-12)

def test_fastica_eigh_low_rank_warning(global_random_seed):
    """Test FastICA eigh solver raises warning for low-rank data."""
    A = rng.randn(10, 2)
    X = A @ A.T
    ica = FastICA(random_state=0, whiten="unit-variance", whiten_solver="eigh")
    msg = "There are some small singular values"

            # The FastICA solver may not converge for some data with specific
            # random seeds but this happens after the whiten step so this is
            # not want we want to test here.
            ica.fit(X)

def test_sparse_encode_shapes_omp():
    algorithms = ["omp", "lasso_lars", "lasso_cd", "lars", "threshold"]
    for n_components, n_samples in itertools.product([1, 5], [1, 9]):
        X_ = rng.randn(n_samples, n_features)
        dictionary = rng.randn(n_components, n_features)
        for algorithm, n_jobs in itertools.product(algorithms, [1, 2]):
            code = sparse_encode(X_, dictionary, algorithm=algorithm, n_jobs=n_jobs)
            assert code.shape == (n_samples, n_components)

def test_dict_learning_shapes():
    n_components = 5
    dico = DictionaryLearning(n_components, random_state=0).fit(X)
    assert dico.components_.shape == (n_components, n_features)

    n_components = 1
    assert dico.transform(X).shape == (X.shape[0], n_components)

def test_dict_learning_overcomplete():
    n_components = 12

def test_max_iter():
    def ricker_function(resolution, center, width):
        """Discrete sub-sampled Ricker (Mexican hat) wavelet"""
        x = np.linspace(0, resolution - 1, resolution)
        x = (
            (2 / (np.sqrt(3 * width) * np.pi**0.25))
            * (1 - (x - center) ** 2 / width**2)
            * np.exp(-((x - center) ** 2) / (2 * width**2))
        )
        return x

    def ricker_matrix(width, resolution, n_components):
        """Dictionary of Ricker (Mexican hat) wavelets"""
        centers = np.linspace(0, resolution - 1, n_components)
        D = np.empty((n_components, resolution))
        for i, center in enumerate(centers):
            D[i] = ricker_function(resolution, center, width)
        D /= np.sqrt(np.sum(D**2, axis=1))[:, np.newaxis]
        return D

    transform_algorithm = "lasso_cd"
    resolution = 1024
    subsampling = 3  # subsampling factor
    n_components = resolution // subsampling

    # Compute a wavelet dictionary
    D_multi = np.r_[
        tuple(
            ricker_matrix(
                width=w, resolution=resolution, n_components=n_components // 5
            )
            for w in (10, 50, 100, 500, 1000)
        )
    ]

    X = np.linspace(0, resolution - 1, resolution)
    first_quarter = X < resolution / 4
    X[first_quarter] = 3.0
    X[np.logical_not(first_quarter)] = -1.0
    X = X.reshape(1, -1)

    # check that the underlying model fails to converge
        model = SparseCoder(
            D_multi, transform_algorithm=transform_algorithm, transform_max_iter=1
        )
        model.fit_transform(X)

    # check that the underlying model converges w/o warnings
            D_multi, transform_algorithm=transform_algorithm, transform_max_iter=2000
        )

def test_dict_learning_lars_positive_parameter():
    alpha = 1
    err_msg = "Positive constraint not supported for 'lars' coding method."
        dict_learning(X, n_components, alpha=alpha, positive_code=True)

def test_dict_learning_positivity(transform_algorithm, positive_code, positive_dict):
    dico = DictionaryLearning(
        n_components,
        fit_algorithm="cd",
    ).fit(X)

    code = dico.transform(X)
    if positive_dict:
        assert (dico.components_ >= 0).all()
    else:
        assert (dico.components_ < 0).any()
    if positive_code:
        assert (code >= 0).all()
    else:
        assert (code < 0).any()

def test_dict_learning_lars_dict_positivity(positive_dict):
        transform_algorithm="lars",
    ).fit(X)

    else:

def test_dict_learning_lars_code_positivity():
        positive_code=True,
    ).fit(X)

    err_msg = "Positive constraint not supported for '{}' coding method."
    err_msg = err_msg.format("lars")
        dico.transform(X)

def test_dict_learning_reconstruction():
        n_components, transform_algorithm="omp", transform_alpha=0.001, random_state=0
    )
    code = dico.fit(X).transform(X)
    assert_array_almost_equal(np.dot(code, dico.components_), X)
    assert_array_almost_equal(dico.inverse_transform(code), X)

    dico.set_params(transform_algorithm="lasso_lars")
    assert_array_almost_equal(np.dot(code, dico.components_), X, decimal=2)
    assert_array_almost_equal(dico.inverse_transform(code), X, decimal=2)

    # test error raised for wrong code size
    with pytest.raises(ValueError, match="Expected 12, got 11."):
        dico.inverse_transform(code[:, :-1])

def test_dict_learning_reconstruction_parallel():
    # regression test that parallel reconstruction works with n_jobs>1
        transform_alpha=0.001,
        n_jobs=4,
    )

def test_dict_learning_lassocd_readonly_data():
    with TempMemmap(X) as X_read_only:
        dico = DictionaryLearning(
            transform_algorithm="lasso_cd",
            transform_alpha=0.001,
            n_jobs=4,
        )
            code = dico.fit(X_read_only).transform(X_read_only)
            np.dot(code, dico.components_), X_read_only, decimal=2
        )

def test_dict_learning_nonzero_coefs():
        transform_n_nonzero_coefs=3,
    )
    code = dico.fit(X).transform(X[np.newaxis, 1])
    assert len(np.flatnonzero(code)) == 3

    dico.set_params(transform_algorithm="omp")
    code = dico.transform(X[np.newaxis, 1])

def test_dict_learning_split():
        n_components, transform_algorithm="threshold", random_state=0
    )
    Xr = dico.inverse_transform(code)

    dico.split_sign = True
    split_code = dico.transform(X)

        split_code[:, :n_components] - split_code[:, n_components:], code
    )

    Xr2 = dico.inverse_transform(split_code)
    assert_array_almost_equal(Xr, Xr2)

def test_dict_learning_online_shapes():
    n_components = 8

    code, dictionary = dict_learning_online(
        X,
        batch_size=4,
        method="cd",
        return_code=True,
    )
    assert code.shape == (n_samples, n_components)
    assert dictionary.shape == (n_components, n_features)
    assert np.dot(code, dictionary).shape == X.shape

    dictionary = dict_learning_online(
        X,
        return_code=False,
    )

def test_dict_learning_online_lars_positive_parameter():
        dict_learning_online(X, batch_size=4, max_iter=10, positive_code=True)

def test_minibatch_dictionary_learning_positivity(
    transform_algorithm, positive_code, positive_dict
):
    dico = MiniBatchDictionaryLearning(
    ).fit(X)

    else:
    else:

def test_minibatch_dictionary_learning_lars(positive_dict):

    ).fit(X)

    else:

def test_dict_learning_online_positivity(positive_code, positive_dict):

        X,
        alpha=1,
    )
        assert (dictionary >= 0).all()
    else:
        assert (dictionary < 0).any()
    else:

def test_dict_learning_online_verbosity():
    # test verbosity for better coverage
    import sys

    try:

        # convergence monitoring verbosity
        dico = MiniBatchDictionaryLearning(
            n_components, batch_size=4, max_iter=5, verbose=1, tol=0.1, random_state=0
        )
        dico.fit(X)
            batch_size=4,
            max_iter=5,
            verbose=1,
            max_no_improvement=2,
        )
        # higher verbosity level
            n_components, batch_size=4, max_iter=5, verbose=2, random_state=0
        )

        # function API verbosity
        dict_learning_online(
            X,
            alpha=1,
        )
            X,
            alpha=1,
            verbose=2,
        )
    finally:

def test_dict_learning_online_estimator_shapes():
        n_components, batch_size=4, max_iter=5, random_state=0
    )
    dico.fit(X)

def test_dict_learning_online_overcomplete():
    ).fit(X)

def test_dict_learning_online_initialization():
        n_components, batch_size=4, max_iter=0, dict_init=V, random_state=0
    ).fit(X)
    assert_array_equal(dico.components_, V)

def test_dict_learning_online_readonly_initialization():
    V.setflags(write=False)
    MiniBatchDictionaryLearning(
        dict_init=V,
    ).fit(X)

def test_dict_learning_online_partial_fit():
    V = rng.randn(n_components, n_features)  # random init
    V /= np.sum(V**2, axis=1)[:, np.newaxis]
    dict1 = MiniBatchDictionaryLearning(
        batch_size=1,
        alpha=1,
        max_no_improvement=None,
        tol=0.0,
    ).fit(X)
    dict2 = MiniBatchDictionaryLearning(
        n_components, alpha=1, dict_init=V, random_state=0
    )
        for sample in X:
            dict2.partial_fit(sample[np.newaxis, :])

    assert not np.all(sparse_encode(X, dict1.components_, alpha=1) == 0)
    assert_array_almost_equal(dict1.components_, dict2.components_, decimal=2)

    # partial_fit should ignore max_iter (#17433)
    assert dict1.n_steps_ == dict2.n_steps_ == 100

def test_sparse_encode_shapes():
    for algo in ("lasso_lars", "lasso_cd", "lars", "omp", "threshold"):
        code = sparse_encode(X, V, algorithm=algo)
        assert code.shape == (n_samples, n_components)

def test_sparse_encode_positivity(algo, positive):
    code = sparse_encode(X, V, algorithm=algo, positive=positive)
    if positive:
    else:

def test_sparse_encode_unavailable_positivity(algo):
    err_msg = err_msg.format(algo)
        sparse_encode(X, V, algorithm=algo, positive=True)

def test_sparse_encode_input():
    n_components = 100
    Xf = check_array(X, order="F")
        a = sparse_encode(X, V, algorithm=algo)
        b = sparse_encode(Xf, V, algorithm=algo)
        assert_array_almost_equal(a, b)

def test_sparse_encode_error():
    code = sparse_encode(X, V, alpha=0.001)
    assert not np.all(code == 0)
    assert np.sqrt(np.sum((np.dot(code, V) - X) ** 2)) < 0.1

def test_sparse_encode_error_default_sparsity():
    X = rng.randn(100, 64)
    D = rng.randn(2, 64)
    code = ignore_warnings(sparse_encode)(X, D, algorithm="omp", n_nonzero_coefs=None)
    assert code.shape == (100, 2)

def test_sparse_coder_estimator():
    coder = SparseCoder(
        dictionary=V, transform_algorithm="lasso_lars", transform_alpha=0.001
    )
    code = coder.fit_transform(X)
    Xr = coder.inverse_transform(code)
    np.testing.assert_allclose(Xr, np.dot(code, V))

def test_sparse_coder_estimator_clone():
    )
    cloned = clone(coder)
    assert id(cloned) != id(coder)
    np.testing.assert_allclose(cloned.dictionary, coder.dictionary)
    assert id(cloned.dictionary) != id(coder.dictionary)
    assert cloned.n_components_ == coder.n_components_
    assert cloned.n_features_in_ == coder.n_features_in_
    data = np.random.rand(n_samples, n_features).astype(np.float32)
    np.testing.assert_allclose(cloned.transform(data), coder.transform(data))

def test_sparse_coder_parallel_mmap():
    # https://github.com/scikit-learn/scikit-learn/issues/5956
    # Test that SparseCoder does not error by passing reading only
    # arrays to child processes

    rng = np.random.RandomState(777)
    n_components, n_features = 40, 64
    init_dict = rng.rand(n_components, n_features)
    # Ensure that `data` is >2M. Joblib memory maps arrays
    # if they are larger than 1MB. The 4 accounts for float32
    # data type
    n_samples = int(2e6) // (4 * n_features)

    sc = SparseCoder(init_dict, transform_algorithm="omp", n_jobs=2)
    sc.fit_transform(data)

def test_sparse_coder_common_transformer():
    n_components, n_features = 40, 3

    sc = SparseCoder(init_dict)

    check_transformer_data_not_an_array(sc.__class__.__name__, sc)
    check_transformer_general(sc.__class__.__name__, sc)
    check_transformer_general_memmap = partial(
        check_transformer_general, readonly_memmap=True
    )
    check_transformer_general_memmap(sc.__class__.__name__, sc)
    check_transformers_unfitted(sc.__class__.__name__, sc)

def test_sparse_coder_n_features_in():
    d = np.array([[1, 2, 3], [1, 2, 3]])
    sc = SparseCoder(d)
    assert sc.n_features_in_ == d.shape[1]

def test_update_dict():
    # Check the dict update in batch mode vs online mode
    # Non-regression test for #4866

    code = np.array([[0.5, -0.5], [0.1, 0.9]])
    dictionary = np.array([[1.0, 0.0], [0.6, 0.8]])

    X = np.dot(code, dictionary) + rng.randn(2, 2)

    # full batch update
    newd_batch = dictionary.copy()
    _update_dict(newd_batch, X, code)

    # online update
    A = np.dot(code.T, code)
    B = np.dot(X.T, code)
    newd_online = dictionary.copy()
    _update_dict(newd_online, X, code, A, B)

    assert_allclose(newd_batch, newd_online)

def test_sparse_encode_dtype_match(data_type, algorithm):
    n_components = 6
    dictionary = rng.randn(n_components, n_features)
    code = sparse_encode(
        X.astype(data_type), dictionary.astype(data_type), algorithm=algorithm
    )
    assert code.dtype == data_type

def test_sparse_encode_numerical_consistency(algorithm):
    # verify numerical consistency among np.float32 and np.float64
    rtol = 1e-4
    code_32 = sparse_encode(
        X.astype(np.float32), dictionary.astype(np.float32), algorithm=algorithm
    )
    code_64 = sparse_encode(
        X.astype(np.float64), dictionary.astype(np.float64), algorithm=algorithm
    )
    assert_allclose(code_32, code_64, rtol=rtol)

def test_sparse_coder_dtype_match(data_type, transform_algorithm):
    # Verify preserving dtype for transform in sparse coder
        dictionary.astype(data_type), transform_algorithm=transform_algorithm
    )
    code = coder.transform(X.astype(data_type))

def test_dictionary_learning_dtype_match(
    data_type,
    expected_type,
    fit_algorithm,
    transform_algorithm,
):
    # Verify preserving dtype for fit and transform in dictionary learning class
    dict_learner = DictionaryLearning(
        fit_algorithm=fit_algorithm,
    )
    dict_learner.fit(X.astype(data_type))
    assert dict_learner.components_.dtype == expected_type
    assert dict_learner.transform(X.astype(data_type)).dtype == expected_type

def test_minibatch_dictionary_learning_dtype_match(
):
    # Verify preserving dtype for fit and transform in minibatch dictionary learning
    dict_learner = MiniBatchDictionaryLearning(
        tol=1e-1,
    )

    assert dict_learner._A.dtype == expected_type
    assert dict_learner._B.dtype == expected_type

def test_dict_learning_dtype_match(data_type, expected_type, method):
    code, dictionary, _ = dict_learning(
        X.astype(data_type),
        alpha=1,
    )
    assert code.dtype == expected_type
    assert dictionary.dtype == expected_type

def test_dict_learning_numerical_consistency(method):
    # verify numerically consistent among np.float32 and np.float64
    rtol = 1e-6
    alpha = 2

    U_64, V_64, _ = dict_learning(
        X.astype(np.float64),
    )
    U_32, V_32, _ = dict_learning(
        X.astype(np.float32),
    )

    # Optimal solution (U*, V*) is not unique.
    # If (U*, V*) is optimal solution, (-U*,-V*) is also optimal,
    # and (column permutated U*, row permutated V*) are also optional
    # as long as holding UV.
    # So here UV, ||U||_1,1 and sum(||V_k||_2^2) are verified
    # instead of comparing directly U and V.
    assert_allclose(np.matmul(U_64, V_64), np.matmul(U_32, V_32), rtol=rtol)
    assert_allclose(np.sum(np.abs(U_64)), np.sum(np.abs(U_32)), rtol=rtol)
    assert_allclose(np.sum(V_64**2), np.sum(V_32**2), rtol=rtol)
    # verify an obtained solution is not degenerate
    assert np.mean(U_64 != 0.0) > 0.05
    assert np.count_nonzero(U_64 != 0.0) == np.count_nonzero(U_32 != 0.0)

def test_dict_learning_online_dtype_match(data_type, expected_type, method):
        alpha=1,
    )

def test_dict_learning_online_numerical_consistency(method):
    rtol = 1e-4
    alpha = 1

    U_64, V_64 = dict_learning_online(
        tol=0.0,
    )
    U_32, V_32 = dict_learning_online(
        tol=0.0,
    )

    # So here UV, ||U||_1,1 and sum(||V_k||_2) are verified

def test_get_feature_names_out(estimator):
    """Check feature names for dict learning estimators."""
    estimator.fit(X)

    feature_names_out = estimator.get_feature_names_out()
    estimator_name = estimator.__class__.__name__.lower()
        feature_names_out,
        [f"{estimator_name}{i}" for i in range(n_components)],
    )

def test_cd_work_on_joblib_memmapped_data(monkeypatch):
        sklearn.decomposition._dict_learning,
    )

    X_train = rng.randn(10, 10)

        n_components=5,
        n_jobs=2,
    )

    # This must run and complete without error.
    dict_learner.fit(X_train)

        x = (
        )
        return x

        return D

def X_sparse():
    # Make an X that looks somewhat like a small tf-idf matrix.
    rng = check_random_state(42)
    X = sp.random(60, 55, density=0.2, format="csr", random_state=rng)
    X.data[:] = 1 + np.log(X.data)
    return X

def test_solvers(X_sparse, solver, kind):
    X = X_sparse if kind == "sparse" else X_sparse.toarray()
    svd_a = TruncatedSVD(30, algorithm="arpack")
    svd = TruncatedSVD(30, algorithm=solver, random_state=42, n_oversamples=100)

    Xa = svd_a.fit_transform(X)[:, :6]
    Xr = svd.fit_transform(X)[:, :6]
    assert_allclose(Xa, Xr, rtol=2e-3)

    comp_a = np.abs(svd_a.components_)
    comp = np.abs(svd.components_)
    # All elements are equal, but some elements are more equal than others.
    assert_allclose(comp_a[:9], comp[:9], rtol=1e-3)
    assert_allclose(comp_a[9:], comp[9:], atol=1e-2)

def test_attributes(n_components, X_sparse):
    n_features = X_sparse.shape[1]
    tsvd = TruncatedSVD(n_components).fit(X_sparse)
    assert tsvd.n_components == n_components
    assert tsvd.components_.shape == (n_components, n_features)

def test_too_many_components(X_sparse, algorithm, n_components):
    tsvd = TruncatedSVD(n_components=n_components, algorithm=algorithm)
        tsvd.fit(X_sparse)

def test_sparse_formats(fmt, X_sparse):
    n_samples = X_sparse.shape[0]
    Xfmt = X_sparse.toarray() if fmt == "dense" else getattr(X_sparse, "to" + fmt)()
    tsvd = TruncatedSVD(n_components=11)
    Xtrans = tsvd.fit_transform(Xfmt)
    assert Xtrans.shape == (n_samples, 11)
    Xtrans = tsvd.transform(Xfmt)

def test_inverse_transform(algo, X_sparse):
    # We need a lot of components for the reconstruction to be "almost
    # equal" in all positions. XXX Test means or sums instead?
    tsvd = TruncatedSVD(n_components=52, random_state=42, algorithm=algo)
    Xt = tsvd.fit_transform(X_sparse)
    Xinv = tsvd.inverse_transform(Xt)
    assert_allclose(Xinv, X_sparse.toarray(), rtol=1e-1, atol=2e-1)

def test_integers(X_sparse):
    Xint = X_sparse.astype(np.int64)
    tsvd = TruncatedSVD(n_components=6)
    Xtrans = tsvd.fit_transform(Xint)
    assert Xtrans.shape == (n_samples, tsvd.n_components)

def test_explained_variance(X_sparse, kind, n_components, solver):
    svd = TruncatedSVD(n_components, algorithm=solver)
    X_tr = svd.fit_transform(X)
    # Assert that all the values are greater than 0
    assert_array_less(0.0, svd.explained_variance_ratio_)

    # Assert that total explained variance is less than 1
    assert_array_less(svd.explained_variance_ratio_.sum(), 1.0)

    # Test that explained_variance is correct
    total_variance = np.var(X_sparse.toarray(), axis=0).sum()
    variances = np.var(X_tr, axis=0)
    true_explained_variance_ratio = variances / total_variance

        svd.explained_variance_ratio_,
        true_explained_variance_ratio,
    )

def test_explained_variance_components_10_20(X_sparse, kind, solver):
    svd_10 = TruncatedSVD(10, algorithm=solver, n_iter=10).fit(X)
    svd_20 = TruncatedSVD(20, algorithm=solver, n_iter=10).fit(X)

    # Assert the 1st component is equal
        svd_10.explained_variance_ratio_,
        svd_20.explained_variance_ratio_[:10],
        rtol=5e-3,
    )

    # Assert that 20 components has higher explained variance than 10
    assert (
        svd_20.explained_variance_ratio_.sum() > svd_10.explained_variance_ratio_.sum()
    )

def test_singular_values_consistency(solver, global_random_seed):
    # Check that the TruncatedSVD output has the correct singular values

    pca = TruncatedSVD(n_components=2, algorithm=solver, random_state=rng).fit(X)

        np.sum(pca.singular_values_**2.0),
        np.linalg.norm(X_pca, "fro") ** 2.0,
        rtol=1e-2,
    )

        pca.singular_values_, np.sqrt(np.sum(X_pca**2.0, axis=0)), rtol=1e-2
    )

def test_singular_values_expected(solver, global_random_seed):

    pca = TruncatedSVD(n_components=3, algorithm=solver, random_state=rng)

    X_hat_pca = np.dot(X_pca, pca.components_)
    pca.fit(X_hat_pca)
    assert_allclose(pca.singular_values_, [3.142, 2.718, 1.0], rtol=1e-14)

def test_truncated_svd_eq_pca(X_sparse):
    # TruncatedSVD should be equal to PCA on centered data

    X_c = X_dense - X_dense.mean(axis=0)

    params = dict(n_components=10, random_state=42)

    svd = TruncatedSVD(algorithm="arpack", **params)
    pca = PCA(svd_solver="arpack", **params)

    Xt_svd = svd.fit_transform(X_c)
    Xt_pca = pca.fit_transform(X_c)

    assert_allclose(Xt_svd, Xt_pca, rtol=1e-9)
    assert_allclose(pca.mean_, 0, atol=1e-9)
    assert_allclose(svd.components_, pca.components_)

def test_fit_transform(X_sparse, algorithm, tol, kind):
    # fit_transform(X) should equal fit(X).transform(X)
    svd = TruncatedSVD(
        n_components=5, n_iter=7, random_state=42, algorithm=algorithm, tol=tol
    )
    X_transformed_1 = svd.fit_transform(X)
    X_transformed_2 = svd.fit(X).transform(X)
    assert_allclose(X_transformed_1, X_transformed_2)

def test_factor_analysis(global_random_seed):
    # Test FactorAnalysis ability to recover the data covariance structure
    n_samples, n_features, n_components = 20, 5, 3

    # Some random settings for the generative model
    W = rng.randn(n_components, n_features)
    # latent variable of dim 3, 20 of it
    h = rng.randn(n_samples, n_components)
    # using gamma to model different noise variance
    # per component
    noise = rng.gamma(1, size=n_features) * rng.randn(n_samples, n_features)

    # generate observations
    # wlog, mean is 0
    X = np.dot(h, W) + noise

    fas = []
    for method in ["randomized", "lapack"]:
        fa = FactorAnalysis(n_components=n_components, svd_method=method)
        fa.fit(X)
        fas.append(fa)

        X_t = fa.transform(X)
        assert X_t.shape == (n_samples, n_components)

        assert_almost_equal(fa.loglike_[-1], fa.score_samples(X).sum())
        assert_almost_equal(fa.score_samples(X).mean(), fa.score(X))

        diff = np.all(np.diff(fa.loglike_))
        assert diff > 0.0, "Log likelihood dif not increase"

        # Sample Covariance
        scov = np.cov(X, rowvar=0.0, bias=1.0)

        # Model Covariance
        mcov = fa.get_covariance()
        diff = np.sum(np.abs(scov - mcov)) / W.size
        assert diff < 0.2, "Mean absolute difference is %f" % diff
        fa = FactorAnalysis(
            n_components=n_components, noise_variance_init=np.ones(n_features)
        )
            fa.fit(X[:, :2])

    def f(x, y):
        return np.abs(getattr(x, y))  # sign will not be equal

    fa1, fa2 = fas
    for attr in ["loglike_", "components_", "noise_variance_"]:
        assert_almost_equal(f(fa1, attr), f(fa2, attr))

    fa1.max_iter = 1
    fa1.verbose = True
        fa1.fit(X)

    # Test get_covariance and get_precision with n_components == n_features
    # with n_components < n_features and with n_components == 0
    for n_components in [0, 2, X.shape[1]]:
        fa.n_components = n_components
        fa.fit(X)
        cov = fa.get_covariance()
        precision = fa.get_precision()
        assert_array_almost_equal(np.dot(cov, precision), np.eye(X.shape[1]), 12)

    # test rotation

    results, projections = {}, {}
    for method in (None, "varimax", "quartimax"):
        fa_var = FactorAnalysis(n_components=n_components, rotation=method)
        results[method] = fa_var.fit_transform(X)
        projections[method] = fa_var.get_covariance()
    for rot1, rot2 in combinations([None, "varimax", "quartimax"], 2):
        assert not np.allclose(results[rot1], results[rot2])
        assert np.allclose(projections[rot1], projections[rot2], atol=3)

    # test against R's psych::principal with rotate="varimax"
    # (i.e., the values below stem from rotating the components in R)
    # R's factor analysis returns quite different values; therefore, we only
    # test the rotation itself
    factors = np.array(
        [
            [0.89421016, -0.35854928, -0.27770122, 0.03773647],
            [-0.45081822, -0.89132754, 0.0932195, -0.01787973],
            [0.99500666, -0.02031465, 0.05426497, -0.11539407],
            [0.96822861, -0.06299656, 0.24411001, 0.07540887],
        ]
    )
    r_solution = np.array(
        [[0.962, 0.052], [-0.141, 0.989], [0.949, -0.300], [0.937, -0.251]]
    )
    rotated = _ortho_rotation(factors[:, :n_components], method="varimax").T
    assert_array_almost_equal(np.abs(rotated), np.abs(r_solution), decimal=3)

def _build_sparse_array(csr_container):
    # Create 3 topics and each topic has 3 distinct words.
    # (Each word only belongs to a single topic.)
    block = np.full((3, 3), n_components, dtype=int)
    blocks = [block] * n_components
    X = block_diag(*blocks)
    return (n_components, X)

def test_lda_default_prior_params(csr_container):
    # default prior parameter should be `1 / topics`
    # and verbose params should not affect result
    n_components, X = _build_sparse_array(csr_container)
    prior = 1.0 / n_components
    lda_1 = LatentDirichletAllocation(
        doc_topic_prior=prior,
        topic_word_prior=prior,
    )
    lda_2 = LatentDirichletAllocation(n_components=n_components, random_state=0)
    topic_distr_1 = lda_1.fit_transform(X)
    topic_distr_2 = lda_2.fit_transform(X)
    assert_almost_equal(topic_distr_1, topic_distr_2)

def test_lda_fit_batch(csr_container):
    # Test LDA batch learning_offset (`fit` method with 'batch' learning)
    lda = LatentDirichletAllocation(
        evaluate_every=1,
    )
    lda.fit(X)

    correct_idx_grps = [(0, 1, 2), (3, 4, 5), (6, 7, 8)]
    for component in lda.components_:
        # Find top 3 words in each LDA component
        top_idx = set(component.argsort()[-3:][::-1])
        assert tuple(sorted(top_idx)) in correct_idx_grps

def test_lda_fit_online(csr_container):
    # Test LDA online learning (`fit` method with 'online' learning)
        learning_method="online",
    )

def test_lda_partial_fit(csr_container):
    # Test LDA online learning (`partial_fit` method)
    # (same as test_lda_batch)
        total_samples=100,
    )
        lda.partial_fit(X)

    for c in lda.components_:
        top_idx = set(c.argsort()[-3:][::-1])

def test_lda_dense_input(csr_container):
    # Test LDA with dense input.
        n_components=n_components, learning_method="batch", random_state=rng
    )
    lda.fit(X.toarray())

def test_lda_transform():
    # Test LDA transform.
    # Transform result cannot be negative and should be normalized by default
    X = rng.randint(5, size=(20, 10))
    lda = LatentDirichletAllocation(n_components=n_components, random_state=rng)
    X_trans = lda.fit_transform(X)
    assert (X_trans > 0.0).any()
    assert_array_almost_equal(np.sum(X_trans, axis=1), np.ones(X_trans.shape[0]))

    X_trans_unnormalized = lda.transform(X, normalize=False)
        X_trans, X_trans_unnormalized / X_trans_unnormalized.sum(axis=1)[:, np.newaxis]
    )

def test_lda_fit_transform(method):
    # Test LDA fit_transform & transform
    # fit_transform and transform result should be the same
    X = rng.randint(10, size=(50, 20))
        n_components=5, learning_method=method, random_state=rng
    )
    X_fit = lda.fit_transform(X)
    X_trans = lda.transform(X)
    assert_array_almost_equal(X_fit, X_trans, 4)

def test_lda_negative_input():
    # test pass dense matrix with sparse negative input.
    X = np.full((5, 10), -1.0)
    lda = LatentDirichletAllocation()
    regex = r"^Negative values in data passed"
    with pytest.raises(ValueError, match=regex):
        lda.fit(X)

def test_lda_no_component_error():
    # test `perplexity` before `fit`
    X = rng.randint(4, size=(20, 10))
    regex = (
        "This LatentDirichletAllocation instance is not fitted yet. "
        "Call 'fit' with appropriate arguments before using this "
        "estimator."
    )
    with pytest.raises(NotFittedError, match=regex):
        lda.perplexity(X)

def test_lda_multi_jobs(method, csr_container):
    # Test LDA batch training with multi CPU
        n_jobs=2,
        learning_method=method,
    )

def test_lda_partial_fit_multi_jobs(csr_container):
    # Test LDA online training with multi CPU
        n_jobs=2,
        learning_offset=5.0,
        total_samples=30,
    )
    for i in range(2):

def test_lda_preplexity_mismatch():
    # test dimension mismatch in `perplexity` method
    n_components = rng.randint(3, 6)
    n_samples = rng.randint(6, 10)
    X = np.random.randint(4, size=(n_samples, 10))
        total_samples=20,
    )
    # invalid samples
    invalid_n_samples = rng.randint(4, size=(n_samples + 1, n_components))
    with pytest.raises(ValueError, match=r"Number of samples"):
        lda._perplexity_precomp_distr(X, invalid_n_samples)
    # invalid topic number
    invalid_n_components = rng.randint(4, size=(n_samples, n_components + 1))
    with pytest.raises(ValueError, match=r"Number of topics"):
        lda._perplexity_precomp_distr(X, invalid_n_components)

def test_lda_perplexity(method, csr_container):
    # Test LDA perplexity for batch training
    # perplexity should be lower after each iteration
    )
    lda_2 = LatentDirichletAllocation(
    )
    lda_1.fit(X)
    perp_1 = lda_1.perplexity(X, sub_sampling=False)

    lda_2.fit(X)
    perp_2 = lda_2.perplexity(X, sub_sampling=False)
    assert perp_1 >= perp_2

    perp_1_subsampling = lda_1.perplexity(X, sub_sampling=True)
    perp_2_subsampling = lda_2.perplexity(X, sub_sampling=True)
    assert perp_1_subsampling >= perp_2_subsampling

def test_lda_score(method, csr_container):
    # Test LDA score for batch training
    # score should be higher after each iteration
    )
    )
    lda_1.fit_transform(X)
    score_1 = lda_1.score(X)

    lda_2.fit_transform(X)
    score_2 = lda_2.score(X)
    assert score_2 >= score_1

def test_perplexity_input_format(csr_container):
    # Test LDA perplexity for sparse and dense input
    # score should be the same for both dense and sparse input
    )
    perp_1 = lda.perplexity(X)
    perp_2 = lda.perplexity(X.toarray())
    assert_almost_equal(perp_1, perp_2)

def test_lda_score_perplexity(csr_container):
    # Test the relationship between LDA score and perplexity
        n_components=n_components, max_iter=10, random_state=0
    )
    perplexity_1 = lda.perplexity(X, sub_sampling=False)

    score = lda.score(X)
    perplexity_2 = np.exp(-1.0 * (score / np.sum(X.data)))
    assert_almost_equal(perplexity_1, perplexity_2)

def test_lda_fit_perplexity(csr_container):
    # Test that the perplexity computed during fit is consistent with what is
    # returned by the perplexity method
    )

    # Perplexity computed at end of fit method
    perplexity1 = lda.bound_

    # Result of perplexity method on the train set
    perplexity2 = lda.perplexity(X)

    assert_almost_equal(perplexity1, perplexity2)

def test_lda_empty_docs(csr_container):
    """Test LDA on empty document (all-zero rows)."""
    Z = np.zeros((5, 4))
    for X in [Z, csr_container(Z)]:
        lda = LatentDirichletAllocation(max_iter=750).fit(X)
            lda.components_.sum(axis=0), np.ones(lda.components_.shape[1])
        )

def test_dirichlet_expectation():
    """Test Cython version of Dirichlet expectation calculation."""
    x = np.logspace(-100, 10, 10000)
    expectation = np.empty_like(x)
    _dirichlet_expectation_1d(x, 0, expectation)
    assert_allclose(expectation, np.exp(psi(x) - psi(np.sum(x))), atol=1e-19)

    x = x.reshape(100, 100)
        _dirichlet_expectation_2d(x),
        psi(x) - psi(np.sum(x, axis=1)[:, np.newaxis]),
        rtol=1e-11,
        atol=3e-9,
    )

def check_verbosity(
    verbose, evaluate_every, expected_lines, expected_perplexities, csr_container
):
        max_iter=3,
        evaluate_every=evaluate_every,
    )
    old_out, sys.stdout = sys.stdout, out
    try:
    finally:
        sys.stdout = old_out

    n_lines = out.getvalue().count("\n")
    n_perplexity = out.getvalue().count("perplexity")
    assert expected_lines == n_lines
    assert expected_perplexities == n_perplexity

def test_verbosity(
):
    check_verbosity(
        verbose, evaluate_every, expected_lines, expected_perplexities, csr_container
    )

def test_lda_feature_names_out(csr_container):
    """Check feature names out for LatentDirichletAllocation."""
    lda = LatentDirichletAllocation(n_components=n_components).fit(X)

    names = lda.get_feature_names_out()
        [f"latentdirichletallocation{i}" for i in range(n_components)], names
    )

def test_lda_dtype_match(learning_method, global_dtype):
    """Check data type preservation of fitted attributes."""
    X = rng.uniform(size=(20, 10)).astype(global_dtype, copy=False)

        n_components=5, random_state=0, learning_method=learning_method
    )
    assert lda.components_.dtype == global_dtype
    assert lda.exp_dirichlet_component_.dtype == global_dtype

def test_lda_numerical_consistency(learning_method, global_random_seed):
    """Check numerical consistency between np.float32 and np.float64."""
    X64 = rng.uniform(size=(20, 10))
    X32 = X64.astype(np.float32)

    lda_64 = LatentDirichletAllocation(
        n_components=5, random_state=global_random_seed, learning_method=learning_method
    ).fit(X64)
    lda_32 = LatentDirichletAllocation(
    ).fit(X32)

    assert_allclose(lda_32.components_, lda_64.components_)
    assert_allclose(lda_32.transform(X32), lda_64.transform(X64))

def test_convergence_warning(Estimator, solver):
    convergence_warning = (
        "Maximum number of iterations 1 reached. Increase it to improve convergence."
    )
    A = np.ones((2, 2))
    with pytest.warns(ConvergenceWarning, match=convergence_warning):
        Estimator(max_iter=1, n_components="auto", **solver).fit(A)

def test_initialize_nn_output():
    # Test that initialization does not return negative values
    rng = np.random.mtrand.RandomState(42)
    data = np.abs(rng.randn(10, 10))
    for init in ("random", "nndsvd", "nndsvda", "nndsvdar"):
        W, H = nmf._initialize_nmf(data, 10, init=init, random_state=0)
        assert not ((W < 0).any() or (H < 0).any())

def test_parameter_checking():
    # Here we only check for invalid parameter values that are not already
    # automatically tested in the common tests.

    msg = "Invalid beta_loss parameter: solver 'cd' does not handle beta_loss = 1.0"
        NMF(solver="cd", beta_loss=1.0).fit(A)
    msg = "Negative values in data passed to"
        NMF().fit(-A)
    clf = NMF(2, tol=0.1).fit(A)
        clf.transform(-A)
        nmf._initialize_nmf(-A, 2, "nndsvd")

    for init in ["nndsvd", "nndsvda", "nndsvdar"]:
        msg = re.escape(
        )
            NMF(3, init=init).fit(A)
            MiniBatchNMF(3, init=init).fit(A)
            nmf._initialize_nmf(A, 3, init)

def test_initialize_close():
    # Test NNDSVD error
    # Test that _initialize_nmf error is less than the standard deviation of
    # the entries in the matrix.
    A = np.abs(rng.randn(10, 10))
    W, H = nmf._initialize_nmf(A, 10, init="nndsvd")
    error = linalg.norm(np.dot(W, H) - A)
    sdev = linalg.norm(A - A.mean())
    assert error <= sdev

def test_initialize_variants():
    # Test NNDSVD variants correctness
    # Test that the variants 'nndsvda' and 'nndsvdar' differ from basic
    # 'nndsvd' only where the basic version has zeros.
    W0, H0 = nmf._initialize_nmf(data, 10, init="nndsvd")
    Wa, Ha = nmf._initialize_nmf(data, 10, init="nndsvda")
    War, Har = nmf._initialize_nmf(data, 10, init="nndsvdar", random_state=0)

    for ref, evl in ((W0, Wa), (W0, War), (H0, Ha), (H0, Har)):
        assert_almost_equal(evl[ref != 0], ref[ref != 0])

def test_nmf_fit_nn_output(Estimator, solver, init, alpha_W, alpha_H):
    # Test that the decomposition does not contain negative values
    A = np.c_[5.0 - np.arange(1, 6), 5.0 + np.arange(1, 6)]
    model = Estimator(
        **solver,
    )
    transf = model.fit_transform(A)
    assert not ((model.components_ < 0).any() or (transf < 0).any())

def test_nmf_fit_close(Estimator, solver):
    # Test that the fit is not too far away
    pnmf = Estimator(
        5,
        init="nndsvdar",
        max_iter=600,
        **solver,
    )
    X = np.abs(rng.randn(6, 5))
    assert pnmf.fit(X).reconstruction_err_ < 0.1

def test_nmf_true_reconstruction():
    # Test that the fit is not too far away from an exact solution
    # (by construction)
    n_samples = 15
    beta_loss = 1
    batch_size = 3
    max_iter = 1000

    W_true = np.zeros([n_samples, n_components])
    W_array = np.abs(rng.randn(n_samples))
        W_true[j % n_samples, j] = W_array[j % n_samples]
    H_true = np.zeros([n_components, n_features])
    H_array = np.abs(rng.randn(n_components))
    for j in range(n_features):
        H_true[j % n_components, j] = H_array[j % n_components]
    X = np.dot(W_true, H_true)

    model = NMF(
        solver="mu",
    )
    transf = model.fit_transform(X)
    X_calc = np.dot(transf, model.components_)

    assert model.reconstruction_err_ < 0.1
    assert_allclose(X, X_calc)

    mbmodel = MiniBatchNMF(
    )
    transf = mbmodel.fit_transform(X)
    X_calc = np.dot(transf, mbmodel.components_)

    assert mbmodel.reconstruction_err_ < 0.1
    assert_allclose(X, X_calc, atol=1)

def test_nmf_transform(solver):
    # Test that fit_transform is equivalent to fit.transform for NMF
    # Test that NMF.transform returns close values
    A = np.abs(rng.randn(6, 5))
    m = NMF(
        init="random",
        tol=1e-6,
    )
    ft = m.fit_transform(A)
    t = m.transform(A)
    assert_allclose(ft, t, atol=1e-1)

def test_minibatch_nmf_transform():
    # Test that fit_transform is equivalent to fit.transform for MiniBatchNMF
    # Only guaranteed with fresh restarts
    m = MiniBatchNMF(
        tol=1e-3,
        fresh_restarts=True,
    )
    assert_allclose(ft, t)

def test_nmf_transform_custom_init(Estimator, solver):
    # Smoke test that checks if NMF.transform works with custom initialization
    A = np.abs(random_state.randn(6, 5))
    avg = np.sqrt(A.mean() / n_components)
    H_init = np.abs(avg * random_state.randn(n_components, 5))
    W_init = np.abs(avg * random_state.randn(6, n_components))

    m = Estimator(
        n_components=n_components, init="custom", random_state=0, tol=1e-3, **solver
    )
    m.fit_transform(A, W=W_init, H=H_init)
    m.transform(A)

def test_nmf_inverse_transform(solver):
    # Test that NMF.inverse_transform returns close values
    A = np.abs(random_state.randn(6, 4))
    m = NMF(
        n_components=4,
    )
    A_new = m.inverse_transform(ft)
    assert_array_almost_equal(A, A_new, decimal=2)

def test_mbnmf_inverse_transform():
    # Test that MiniBatchNMF.transform followed by MiniBatchNMF.inverse_transform
    # is close to the identity
    A = np.abs(rng.randn(6, 4))
    nmf = MiniBatchNMF(
        max_iter=500,
    )
    ft = nmf.fit_transform(A)
    A_new = nmf.inverse_transform(ft)
    assert_allclose(A, A_new, rtol=1e-3, atol=1e-2)

def test_n_components_greater_n_features(Estimator):
    # Smoke test for the case of more components than features.
    A = np.abs(rng.randn(30, 10))
    Estimator(n_components=15, random_state=0, tol=1e-2).fit(A)

def test_nmf_sparse_input(Estimator, solver, sparse_container, alpha_W, alpha_H):
    # Test that sparse matrices are accepted as input
    A[:, 2 * np.arange(5)] = 0
    A_sparse = sparse_container(A)

    est1 = Estimator(
        tol=0,
        **solver,
    )
    est2 = clone(est1)

    W1 = est1.fit_transform(A)
    W2 = est2.fit_transform(A_sparse)
    H1 = est1.components_
    H2 = est2.components_

    assert_allclose(W1, W2)
    assert_allclose(H1, H2)

def test_nmf_sparse_transform(Estimator, solver, csc_container):
    # Test that transform works on sparse data.  Issue #2124
    A = np.abs(rng.randn(3, 2))
    A[1, 1] = 0
    A = csc_container(A)

    model = Estimator(random_state=0, n_components=2, max_iter=400, **solver)
    A_fit_tr = model.fit_transform(A)
    A_tr = model.transform(A)
    assert_allclose(A_fit_tr, A_tr, atol=1e-1)

def test_non_negative_factorization_consistency(init, solver, alpha_W, alpha_H):
    # Test that the function is called in the same way, either directly
    # or through the NMF class
    max_iter = 500

    W_nmf, H, _ = non_negative_factorization(
        A,
        tol=1e-2,
    )
    W_nmf_2, H, _ = non_negative_factorization(
        A,
        H=H,
        update_H=False,
        tol=1e-2,
    )

    model_class = NMF(
        tol=1e-2,
    )
    W_cls = model_class.fit_transform(A)
    W_cls_2 = model_class.transform(A)

    assert_allclose(W_nmf, W_cls)
    assert_allclose(W_nmf_2, W_cls_2)

def test_non_negative_factorization_checking():
    # Note that the validity of parameter types and range of possible values
    # for scalar numerical or str parameters is already checked in the common
    # tests. Here we only check for problems that cannot be captured by simple
    # declarative constraints on the valid parameter values.

    # Test parameters checking in public function
    nnmf = non_negative_factorization
    msg = re.escape("Negative values in data passed to NMF (input H)")
        nnmf(A, A, -A, 2, init="custom")
    msg = re.escape("Negative values in data passed to NMF (input W)")
        nnmf(A, -A, A, 2, init="custom")
    msg = re.escape("Array passed to NMF (input H) is full of zeros")
        nnmf(A, A, 0 * A, 2, init="custom")

def _beta_divergence_dense(X, W, H, beta):
    """Compute the beta-divergence of X and W.H for dense array only.

    Used as a reference for testing nmf._beta_divergence.
    """
    WH = np.dot(W, H)

        return squared_norm(X - WH) / 2

    WH_Xnonzero = WH[X != 0]
    X_nonzero = X[X != 0]
    np.maximum(WH_Xnonzero, 1e-9, out=WH_Xnonzero)

        res = np.sum(X_nonzero * np.log(X_nonzero / WH_Xnonzero))
        res += WH.sum() - X.sum()

        div = X_nonzero / WH_Xnonzero
        res = np.sum(div) - X.size - np.sum(np.log(div))
    else:
        res = (X_nonzero**beta).sum()
        res += (beta - 1) * (WH**beta).sum()
        res -= beta * (X_nonzero * (WH_Xnonzero ** (beta - 1))).sum()

    return res

def test_beta_divergence(csr_container):
    # Compare _beta_divergence with the reference _beta_divergence_dense
    beta_losses = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0]

    # initialization
    np.clip(X, 0, None, out=X)
    W, H = nmf._initialize_nmf(X, n_components, init="random", random_state=42)

    for beta in beta_losses:
        ref = _beta_divergence_dense(X, W, H, beta)
        loss = nmf._beta_divergence(X, W, H, beta)
        loss_csr = nmf._beta_divergence(X_csr, W, H, beta)

        assert_almost_equal(ref, loss, decimal=7)
        assert_almost_equal(ref, loss_csr, decimal=7)

def test_special_sparse_dot(csr_container):
    # Test the function that computes np.dot(W, H), only where X is non zero.

    W = np.abs(rng.randn(n_samples, n_components))
    H = np.abs(rng.randn(n_components, n_features))

    WH_safe = nmf._special_sparse_dot(W, H, X_csr)
    WH = nmf._special_sparse_dot(W, H, X)

    # test that both results have same values, in X_csr nonzero elements
    ii, jj = X_csr.nonzero()
    WH_safe_data = np.asarray(WH_safe[ii, jj]).ravel()
    assert_array_almost_equal(WH_safe_data, WH[ii, jj], decimal=10)

    # test that WH_safe and X_csr have the same sparse structure
    assert_array_equal(WH_safe.indices, X_csr.indices)
    assert_array_equal(WH_safe.indptr, X_csr.indptr)
    assert_array_equal(WH_safe.shape, X_csr.shape)

def test_nmf_multiplicative_update_sparse(csr_container):
    # Compare sparse and dense input in multiplicative update NMF
    # Also test continuity of the results with respect to beta_loss parameter
    alpha = 0.1
    l1_ratio = 0.5
    n_iter = 20

    rng = np.random.mtrand.RandomState(1337)
    X = np.abs(X)
    W0, H0 = nmf._initialize_nmf(X, n_components, init="random", random_state=42)

    for beta_loss in (-1.2, 0, 0.2, 1.0, 2.0, 2.5):
        # Reference with dense array X
        W, H = W0.copy(), H0.copy()
        W1, H1, _ = non_negative_factorization(
            X,
            W,
            H,
            init="custom",
            update_H=True,
            solver="mu",
            max_iter=n_iter,
            alpha_W=alpha,
        )

        # Compare with sparse X
        W2, H2, _ = non_negative_factorization(
            X_csr,
            W,
            H,
        )

        assert_allclose(W1, W2, atol=1e-7)
        assert_allclose(H1, H2, atol=1e-7)

        # Compare with almost same beta_loss, since some values have a specific
        # behavior, but the results should be continuous w.r.t beta_loss
        beta_loss -= 1.0e-5
        W3, H3, _ = non_negative_factorization(
            X_csr,
            W,
            H,
        )

        assert_allclose(W1, W3, atol=1e-4)
        assert_allclose(H1, H3, atol=1e-4)

def test_nmf_negative_beta_loss(csr_container):
    # Test that an error is raised if beta_loss < 0 and X contains zeros.
    # Test that the output has not NaN values when the input contains zeros.

    def _assert_nmf_no_nan(X, beta_loss):
        W, H, _ = non_negative_factorization(
            X,
            max_iter=1000,
        )
        assert not np.any(np.isnan(W))
        assert not np.any(np.isnan(H))

    msg = "When beta_loss <= 0 and X contains zeros, the solver may diverge."
    for beta_loss in (-0.6, 0.0):
            _assert_nmf_no_nan(X, beta_loss)
        _assert_nmf_no_nan(X + 1e-9, beta_loss)

    for beta_loss in (0.2, 1.0, 1.2, 2.0, 2.5):
        _assert_nmf_no_nan(X, beta_loss)
        _assert_nmf_no_nan(X_csr, beta_loss)

def test_minibatch_nmf_negative_beta_loss(beta_loss):
    """Check that an error is raised if beta_loss < 0 and X contains zeros."""
    X = rng.normal(size=(6, 5))
    X[X < 0] = 0

    nmf = MiniBatchNMF(beta_loss=beta_loss, random_state=0)

        nmf.fit(X)

def test_nmf_regularization(Estimator, solver):
    # Test the effect of L1 and L2 regularizations
    X = np.abs(rng.randn(n_samples, n_features))

    # L1 regularization should increase the number of zeros
    l1_ratio = 1.0
    regul = Estimator(
        alpha_W=0.5,
        **solver,
    )
        **solver,
    )

    W_regul = regul.fit_transform(X)
    W_model = model.fit_transform(X)

    H_regul = regul.components_
    H_model = model.components_

    eps = np.finfo(np.float64).eps
    W_regul_n_zeros = W_regul[W_regul <= eps].size
    W_model_n_zeros = W_model[W_model <= eps].size
    H_regul_n_zeros = H_regul[H_regul <= eps].size
    H_model_n_zeros = H_model[H_model <= eps].size

    assert W_regul_n_zeros > W_model_n_zeros
    assert H_regul_n_zeros > H_model_n_zeros

    # L2 regularization should decrease the sum of the squared norm
    # of the matrices W and H
    l1_ratio = 0.0
        **solver,
    )
        **solver,
    )

    assert (linalg.norm(W_model)) ** 2.0 + (linalg.norm(H_model)) ** 2.0 > (
        linalg.norm(W_regul)
    ) ** 2.0 + (linalg.norm(H_regul)) ** 2.0

def test_nmf_decreasing(solver):
    # test that the objective function is decreasing at each iteration
    n_features = 15
    alpha = 0.1
    tol = 0.0

    np.abs(X, X)

        if solver != "mu" and beta_loss != 2:
            # not implemented
            continue
        previous_loss = None
        for _ in range(30):
            # one more iteration starting from the previous results
            W, H, _ = non_negative_factorization(
                X,
                W,
                H,
                init="custom",
                alpha_W=alpha,
                solver=solver,
                tol=tol,
                l1_ratio=l1_ratio,
                verbose=0,
                update_H=True,
            )

            loss = (
                nmf._beta_divergence(X, W, H, beta_loss)
                + alpha * l1_ratio * n_features * W.sum()
                + alpha * l1_ratio * n_samples * H.sum()
                + alpha * (1 - l1_ratio) * n_features * (W**2).sum()
                + alpha * (1 - l1_ratio) * n_samples * (H**2).sum()
            )
            if previous_loss is not None:
                assert previous_loss > loss
            previous_loss = loss

def test_nmf_underflow():
    # Regression test for an underflow issue in _beta_divergence
    n_samples, n_features, n_components = 10, 2, 2
    X = np.abs(rng.randn(n_samples, n_features)) * 10
    W = np.abs(rng.randn(n_samples, n_components)) * 10

    X[0, 0] = 0
    ref = nmf._beta_divergence(X, W, H, beta=1.0)
    X[0, 0] = 1e-323
    res = nmf._beta_divergence(X, W, H, beta=1.0)
    assert_almost_equal(res, ref)

def test_nmf_dtype_match(Estimator, solver, dtype_in, dtype_out):
    # Check that NMF preserves dtype (float32 and float64)
    X = np.random.RandomState(0).randn(20, 15).astype(dtype_in, copy=False)
    np.abs(X, out=X)

    nmf = Estimator(
        alpha_W=1.0,
        alpha_H=1.0,
        tol=1e-2,
        **solver,
    )

    assert nmf.fit(X).transform(X).dtype == dtype_out
    assert nmf.fit_transform(X).dtype == dtype_out
    assert nmf.components_.dtype == dtype_out

def test_nmf_float32_float64_consistency(Estimator, solver):
    # Check that the result of NMF is the same between float32 and float64
    X = np.random.RandomState(0).randn(50, 7)
    nmf32 = Estimator(random_state=0, tol=1e-3, **solver)
    W32 = nmf32.fit_transform(X.astype(np.float32))
    nmf64 = Estimator(random_state=0, tol=1e-3, **solver)
    W64 = nmf64.fit_transform(X)

    assert_allclose(W32, W64, atol=1e-5)

def test_nmf_custom_init_dtype_error(Estimator):
    # Check that an error is raise if custom H and/or W don't have the same
    # dtype as X.
    X = rng.random_sample((20, 15))
    H = rng.random_sample((15, 15)).astype(np.float32)
    W = rng.random_sample((20, 15))

    with pytest.raises(TypeError, match="should have the same dtype as X"):
        Estimator(init="custom").fit(X, H=H, W=W)

        non_negative_factorization(X, H=H, update_H=False)

def test_nmf_minibatchnmf_equivalence(beta_loss):
    # Test that MiniBatchNMF is equivalent to NMF when batch_size = n_samples and
    # forget_factor 0.0 (stopping criterion put aside)
    X = np.abs(rng.randn(48, 5))

    nmf = NMF(
        tol=0,
    )
    mbnmf = MiniBatchNMF(
        tol=0,
        batch_size=X.shape[0],
        forget_factor=0.0,
    )
    W = nmf.fit_transform(X)
    mbW = mbnmf.fit_transform(X)
    assert_allclose(W, mbW)

def test_minibatch_nmf_partial_fit():
    # Check fit / partial_fit equivalence. Applicable only with fresh restarts.
    X = np.abs(rng.randn(100, 5))

    mbnmf1 = MiniBatchNMF(
        init="custom",
        tol=0,
    )
    mbnmf2 = MiniBatchNMF(n_components=n_components, init="custom", random_state=0)

    # Force the same init of H (W is recomputed anyway) to be able to compare results.
    W, H = nmf._initialize_nmf(
        X, n_components=n_components, init="random", random_state=0
    )

    mbnmf1.fit(X, W=W, H=H)
    for i in range(max_iter):
        for j in range(batch_size):
            mbnmf2.partial_fit(X[j : j + batch_size], W=W[:batch_size], H=H)

    assert mbnmf1.n_steps_ == mbnmf2.n_steps_
    assert_allclose(mbnmf1.components_, mbnmf2.components_)

    """Check feature names out for NMF."""
    X = np.abs(random_state.randn(10, 4))
    nmf = NMF(n_components=3).fit(X)

    names = nmf.get_feature_names_out()
    assert_array_equal([f"nmf{i}" for i in range(3)], names)

def test_minibatch_nmf_verbose():
    # Check verbose mode of MiniBatchNMF for better coverage.
    A = np.random.RandomState(0).random_sample((100, 10))
    nmf = MiniBatchNMF(tol=1e-2, random_state=0, verbose=1)
    try:
        nmf.fit(A)
    finally:

def test_nmf_n_components_auto(Estimator):
    # Check that n_components is correctly inferred
    # from the provided custom initialization.
    X = rng.random_sample((6, 5))
    W = rng.random_sample((6, 2))
    H = rng.random_sample((2, 5))
        tol=1e-6,
    )
    est.fit_transform(X, W=W, H=H)
    assert est._n_components == H.shape[0]

def test_nmf_non_negative_factorization_n_components_auto():
    # Check that n_components is correctly inferred from the provided
    # custom initialization.
    W_init = rng.random_sample((6, 2))
    H_init = rng.random_sample((2, 5))
    W, H, _ = non_negative_factorization(
        X, W=W_init, H=H_init, init="custom", n_components="auto"
    )
    assert H.shape == H_init.shape
    assert W.shape == W_init.shape

def test_nmf_n_components_auto_no_h_update():
    # Tests that non_negative_factorization does not fail when setting
    # n_components="auto" also tests that the inferred n_component
    # value is the right one.
    H_true = rng.random_sample((2, 5))
        X, H=H_true, n_components="auto", update_H=False
    )  # should not fail
    assert_allclose(H, H_true)
    assert W.shape == (X.shape[0], H_true.shape[0])

def test_nmf_w_h_not_used_warning():
    # Check that warnings are raised if user provided W and H are not used
    # and initialization overrides value of W or H
        RuntimeWarning,
        match="When init!='custom', provided W or H are ignored",
    ):
        non_negative_factorization(X, H=H_init, update_H=True, n_components="auto")

    ):
        non_negative_factorization(
            X, W=W_init, H=H_init, update_H=True, n_components="auto"
        )

        RuntimeWarning, match="When update_H=False, the provided initial W is not used."
    ):
        # When update_H is False, W is ignored regardless of init
        # TODO: use the provided W when init="custom".
            X, W=W_init, H=H_init, update_H=False, n_components="auto"
        )

def test_nmf_custom_init_shape_error():
    # Check that an informative error is raised when custom initialization does not
    # have the right shape
    nmf = NMF(n_components=2, init="custom", random_state=0)

    with pytest.raises(ValueError, match="Array with wrong first dimension passed"):
        nmf.fit(X, H=H, W=rng.random_sample((5, 2)))

    with pytest.raises(ValueError, match="Array with wrong second dimension passed"):
        nmf.fit(X, H=H, W=rng.random_sample((6, 3)))

            X,
        )

def _pinv2_old(a):
    # Used previous scipy pinv2 that was updated in:
    # https://github.com/scipy/scipy/pull/10067
    # We can not set `cond` or `rcond` for pinv2 in scipy >= 1.3 to keep the
    # same behavior of pinv2 for scipy < 1.3, because the condition used to
    # determine the rank is dependent on the output of svd.
    u, s, vh = svd(a, full_matrices=False, check_finite=False)

    t = u.dtype.char.lower()
    factor = {"f": 1e3, "d": 1e6}
    cond = np.max(s) * factor[t] * np.finfo(t).eps
    rank = np.sum(s > cond)

    u = u[:, :rank]
    u /= s[:rank]
    return np.transpose(np.conjugate(np.dot(u, vh[:rank])))

def _get_first_singular_vectors_power_method(
    X, y, mode="A", max_iter=500, tol=1e-06, norm_y_weights=False
):
    """Return the first left and right singular vectors of X'y.

    Provides an alternative to the svd(X'y) and uses the power method instead.
    With norm_y_weights to True and in mode A, this corresponds to the
    algorithm section 11.3 of the Wegelin's review, except this starts at the
    "update saliences" part.
    """

    try:
        y_score = next(col for col in y.T if np.any(np.abs(col) > eps))
    except StopIteration as e:
        raise StopIteration("y residual is constant") from e

    x_weights_old = 100  # init to big value for first convergence check

    if mode == "B":
        # Precompute pseudo inverse matrices
        # Basically: X_pinv = (X.T X)^-1 X.T
        # Which requires inverting a (n_features, n_features) matrix.
        # As a result, and as detailed in the Wegelin's review, CCA (i.e. mode
        # B) will be unstable if n_features > n_samples or n_targets >
        # n_samples
        X_pinv, y_pinv = _pinv2_old(X), _pinv2_old(y)

        if mode == "B":
            x_weights = np.dot(X_pinv, y_score)
        else:
            x_weights = np.dot(X.T, y_score) / np.dot(y_score, y_score)

        x_weights /= np.sqrt(np.dot(x_weights, x_weights)) + eps
        x_score = np.dot(X, x_weights)

            y_weights = np.dot(y_pinv, x_score)
        else:
            y_weights = np.dot(y.T, x_score) / np.dot(x_score.T, x_score)

        if norm_y_weights:
            y_weights /= np.sqrt(np.dot(y_weights, y_weights)) + eps

        y_score = np.dot(y, y_weights) / (np.dot(y_weights, y_weights) + eps)

        x_weights_diff = x_weights - x_weights_old
        if np.dot(x_weights_diff, x_weights_diff) < tol or y.shape[1] == 1:
            break
        x_weights_old = x_weights

    n_iter = i + 1
    if n_iter == max_iter:
        warnings.warn("Maximum number of iterations reached", ConvergenceWarning)

    return x_weights, y_weights, n_iter

def _get_first_singular_vectors_svd(X, y):

    Here the whole SVD is computed.
    """
    C = np.dot(X.T, y)
    U, _, Vt = svd(C, full_matrices=False)
    return U[:, 0], Vt[0, :]

def _center_scale_xy(X, y, scale=True):
    """Center X, y and scale if the scale parameter==True

    Returns
    -------
        X, y, x_mean, y_mean, x_std, y_std
    """
    # center
    x_mean = X.mean(axis=0)
    X -= x_mean
    y_mean = y.mean(axis=0)
    y -= y_mean
    # scale
    if scale:
        x_std = X.std(axis=0, ddof=1)
        x_std[x_std == 0.0] = 1.0
        X /= x_std
        y_std = y.std(axis=0, ddof=1)
        y_std[y_std == 0.0] = 1.0
        y /= y_std
    else:
        x_std = np.ones(X.shape[1])
        y_std = np.ones(y.shape[1])
    return X, y, x_mean, y_mean, x_std, y_std

def _svd_flip_1d(u, v):
    """Same as svd_flip but works on 1d arrays, and is inplace"""
    # svd_flip would force us to convert to 2d array and would also return 2d
    # arrays. We don't want that.
    biggest_abs_val_idx = np.argmax(np.abs(u))
    sign = np.sign(u[biggest_abs_val_idx])
    u *= sign
    v *= sign

class _PLS(
    RegressorMixin,
    metaclass=ABCMeta,
):
    """Partial Least Squares (PLS)

    This class implements the generic PLS algorithm.

    Main ref: Wegelin, a survey of Partial Least Squares (PLS) methods,
    with emphasis on the two-block case
    https://stat.uw.edu/sites/default/files/files/reports/2000/tr371.pdf
    """

        "scale": ["boolean"],
        "deflation_mode": [StrOptions({"regression", "canonical"})],
        "mode": [StrOptions({"A", "B"})],
        "algorithm": [StrOptions({"svd", "nipals"})],
    }

        self,
        *,
        scale=True,
        deflation_mode="regression",
        mode="A",
        algorithm="nipals",
        tol=1e-06,
    ):
        self.deflation_mode = deflation_mode
        self.scale = scale

        """Fit model to data.

            `n_features` is the number of predictors.

            Target vectors, where `n_samples` is the number of samples and
            `n_targets` is the number of response variables.

        Returns
        -------
            Fitted model.
        """
            self,
            X,
        )
        y = check_array(
            y,
            input_name="y",
        )
            self._predict_1d = True
        else:
            self._predict_1d = False

        n = X.shape[0]
        p = X.shape[1]
        q = y.shape[1]

        # With PLSRegression n_components is bounded by the rank of (X.T X) see
        # Wegelin page 25. With CCA and PLSCanonical, n_components is bounded
        # by the rank of X and the rank of y: see Wegelin page 12
        rank_upper_bound = (
            min(n, p) if self.deflation_mode == "regression" else min(n, p, q)
        )
        if n_components > rank_upper_bound:
                f"`n_components` upper bound is {rank_upper_bound}. "
                f"Got {n_components} instead. Reduce `n_components`."
            )

        self._norm_y_weights = self.deflation_mode == "canonical"  # 1.1
        norm_y_weights = self._norm_y_weights

        # Scale (in place)
        Xk, yk, self._x_mean, self._y_mean, self._x_std, self._y_std = _center_scale_xy(
            X, y, self.scale
        )

        self.x_weights_ = np.zeros((p, n_components))  # U
        self.y_weights_ = np.zeros((q, n_components))  # V
        self._x_scores = np.zeros((n, n_components))  # Xi
        self._y_scores = np.zeros((n, n_components))  # Omega
        self.x_loadings_ = np.zeros((p, n_components))  # Gamma
        self.y_loadings_ = np.zeros((q, n_components))  # Delta
        self.n_iter_ = []

        # This whole thing corresponds to the algorithm in section 4.1 of the
        # review from Wegelin. See above for a notation mapping from code to
        # paper.
        y_eps = np.finfo(yk.dtype).eps
        for k in range(n_components):
            # Find first left and right singular vectors of the X.T.dot(y)
            # cross-covariance matrix.
            if self.algorithm == "nipals":
                # Replace columns that are all close to zero with zeros
                yk_mask = np.all(np.abs(yk) < 10 * y_eps, axis=0)
                yk[:, yk_mask] = 0.0

                try:
                    (
                        x_weights,
                        y_weights,
                        n_iter_,
                    ) = _get_first_singular_vectors_power_method(
                        Xk,
                        yk,
                        max_iter=self.max_iter,
                        norm_y_weights=norm_y_weights,
                    )
                except StopIteration as e:
                    if str(e) != "y residual is constant":
                        raise
                    warnings.warn(f"y residual is constant at iteration {k}")
                    break

                self.n_iter_.append(n_iter_)

            elif self.algorithm == "svd":
                x_weights, y_weights = _get_first_singular_vectors_svd(Xk, yk)

            # inplace sign flip for consistency across solvers and archs
            _svd_flip_1d(x_weights, y_weights)

            # compute scores, i.e. the projections of X and y
            x_scores = np.dot(Xk, x_weights)
            if norm_y_weights:
                y_ss = 1
            else:
                y_ss = np.dot(y_weights, y_weights)
            y_scores = np.dot(yk, y_weights) / y_ss

            # Deflation: subtract rank-one approx to obtain Xk+1 and yk+1
            x_loadings = np.dot(x_scores, Xk) / np.dot(x_scores, x_scores)
            Xk -= np.outer(x_scores, x_loadings)

            if self.deflation_mode == "canonical":
                # regress yk on y_score
                y_loadings = np.dot(y_scores, yk) / np.dot(y_scores, y_scores)
                yk -= np.outer(y_scores, y_loadings)
            if self.deflation_mode == "regression":
                # regress yk on x_score
                y_loadings = np.dot(x_scores, yk) / np.dot(x_scores, x_scores)
                yk -= np.outer(x_scores, y_loadings)

            self.x_weights_[:, k] = x_weights
            self.y_weights_[:, k] = y_weights
            self._x_scores[:, k] = x_scores
            self._y_scores[:, k] = y_scores
            self.x_loadings_[:, k] = x_loadings
            self.y_loadings_[:, k] = y_loadings

        # X was approximated as Xi . Gamma.T + X_(R+1)
        # Xi . Gamma.T is a sum of n_components rank-1 matrices. X_(R+1) is
        # whatever is left to fully reconstruct X, and can be 0 if X is of rank
        # n_components.
        # Similarly, y was approximated as Omega . Delta.T + y_(R+1)

        # Compute transformation matrices (rotations_). See User Guide.
        self.x_rotations_ = np.dot(
            self.x_weights_,
            pinv(np.dot(self.x_loadings_.T, self.x_weights_), check_finite=False),
        )
        self.y_rotations_ = np.dot(
            self.y_weights_,
            pinv(np.dot(self.y_loadings_.T, self.y_weights_), check_finite=False),
        )
        self.coef_ = np.dot(self.x_rotations_, self.y_loadings_.T)
        self.coef_ = (self.coef_ * self._y_std).T / self._x_std
        self.intercept_ = self._y_mean
        self._n_features_out = self.x_rotations_.shape[1]

    def transform(self, X, y=None, copy=True):
        """Apply the dimension reduction.

            Samples to transform.

        y : array-like of shape (n_samples, n_targets), default=None
            Target vectors.

            Whether to copy `X` and `y`, or perform in-place normalization.

        Returns
        -------
        x_scores, y_scores : array-like or tuple of array-like
            Return `x_scores` if `y` is not given, `(x_scores, y_scores)` otherwise.
        """
        X = validate_data(self, X, copy=copy, dtype=FLOAT_DTYPES, reset=False)
        # Normalize
        X -= self._x_mean
        X /= self._x_std
        # Apply rotation
        x_scores = np.dot(X, self.x_rotations_)
        if y is not None:
            y = check_array(
                y, input_name="y", ensure_2d=False, copy=copy, dtype=FLOAT_DTYPES
            )
            if y.ndim == 1:
                y = y.reshape(-1, 1)
            y -= self._y_mean
            y /= self._y_std
            y_scores = np.dot(y, self.y_rotations_)
            return x_scores, y_scores

        return x_scores

    def inverse_transform(self, X, y=None):

            and `n_components` is the number of pls components.

        y : array-like of shape (n_samples,) or (n_samples, n_components)
            New target, where `n_samples` is the number of samples

        Returns
        -------
        X_reconstructed : ndarray of shape (n_samples, n_features)
            Return the reconstructed `X` data.

        y_reconstructed : ndarray of shape (n_samples, n_targets)
            Return the reconstructed `X` target. Only returned when `y` is given.

        Notes
        -----
        This transformation will only be exact if `n_components=n_features`.
        """
        X = check_array(X, input_name="X", dtype=FLOAT_DTYPES)
        # From pls space to original space
        X_reconstructed = np.matmul(X, self.x_loadings_.T)
        # Denormalize
        X_reconstructed *= self._x_std
        X_reconstructed += self._x_mean

            y = check_array(y, input_name="y", dtype=FLOAT_DTYPES)
            # From pls space to original space
            y_reconstructed = np.matmul(y, self.y_loadings_.T)
            # Denormalize
            y_reconstructed *= self._y_std
            y_reconstructed += self._y_mean
            return X_reconstructed, y_reconstructed

        return X_reconstructed

    def predict(self, X, copy=True):
        """Predict targets of given samples.

            Samples.

            Whether to copy `X` or perform in-place normalization.

        Returns
        -------
        y_pred : ndarray of shape (n_samples,) or (n_samples, n_targets)

        Notes
        -----
        This call requires the estimation of a matrix of shape
        `(n_features, n_targets)`, which may be an issue in high dimensional
        space.
        """
        # Only center X but do not scale it since the coefficients are already scaled
        y_pred = X @ self.coef_.T + self.intercept_
        return y_pred.ravel() if self._predict_1d else y_pred

        """Learn and apply the dimension reduction on the train data.

        Returns
        -------
        self : ndarray of shape (n_samples, n_components)
        """
        return self.fit(X, y).transform(X, y)

        tags.target_tags.required = False

class PLSRegression(_PLS):
    """PLS regression.

    PLSRegression is also known as PLS2 or PLS1, depending on the number of
    targets.

    For a comparison between other cross decomposition algorithms, see
    :ref:`sphx_glr_auto_examples_cross_decomposition_plot_compare_cross_decomposition.py`.

    Read more in the :ref:`User Guide <cross_decomposition>`.

    .. versionadded:: 0.8

        Number of components to keep. Should be in `[1, n_features]`.

    scale : bool, default=True
        Whether to scale `X` and `y`.

    max_iter : int, default=500
        The maximum number of iterations of the power method when
        `algorithm='nipals'`. Ignored otherwise.

    tol : float, default=1e-06
        The tolerance used as convergence criteria in the power method: the
        algorithm stops whenever the squared norm of `u_i - u_{i-1}` is less
        than `tol`, where `u` corresponds to the left singular vector.

        Whether to copy `X` and `y` in :term:`fit` before applying centering,
        and potentially scaling. If `False`, these operations will be done
        inplace, modifying both arrays.

    x_weights_ : ndarray of shape (n_features, n_components)
        The left singular vectors of the cross-covariance matrices of each

    y_weights_ : ndarray of shape (n_targets, n_components)
        The right singular vectors of the cross-covariance matrices of each

    x_loadings_ : ndarray of shape (n_features, n_components)
        The loadings of `X`.

    y_loadings_ : ndarray of shape (n_targets, n_components)
        The loadings of `y`.

    x_scores_ : ndarray of shape (n_samples, n_components)
        The transformed training samples.

    y_scores_ : ndarray of shape (n_samples, n_components)
        The transformed training targets.

    x_rotations_ : ndarray of shape (n_features, n_components)
        The projection matrix used to transform `X`.

    y_rotations_ : ndarray of shape (n_targets, n_components)
        The projection matrix used to transform `y`.

    coef_ : ndarray of shape (n_target, n_features)
        The coefficients of the linear model such that `y` is approximated as
        `y = X @ coef_.T + intercept_`.

    intercept_ : ndarray of shape (n_targets,)
        The intercepts of the linear model such that `y` is approximated as

    n_iter_ : list of shape (n_components,)
        Number of iterations of the power method, for each
        component.

    See Also
    --------
    PLSCanonical : Partial Least Squares transformer and regressor.

    Examples
    --------
    >>> from sklearn.cross_decomposition import PLSRegression
    >>> X = [[0., 0., 1.], [1.,0.,0.], [2.,2.,2.], [2.,5.,4.]]
    >>> y = [[0.1, -0.2], [0.9, 1.1], [6.2, 5.9], [11.9, 12.3]]
    >>> pls2 = PLSRegression(n_components=2)
    >>> pls2.fit(X, y)
    PLSRegression()
    >>> y_pred = pls2.predict(X)

    For a comparison between PLS Regression and :class:`~sklearn.decomposition.PCA`, see
    :ref:`sphx_glr_auto_examples_cross_decomposition_plot_pcr_vs_pls.py`.
    """

    _parameter_constraints: dict = {**_PLS._parameter_constraints}
    for param in ("deflation_mode", "mode", "algorithm"):

    # This implementation provides the same results that 3 PLS packages
    # provided in the R language (R-project):
    #     - "mixOmics" with function pls(X, y, mode = "regression")
    #     - "plspm " with function plsreg2(X, y)
    #     - "pls" with function oscorespls.fit(X, y)

        self, n_components=2, *, scale=True, max_iter=500, tol=1e-06, copy=True
    ):
            scale=scale,
            deflation_mode="regression",
            mode="A",
            algorithm="nipals",
            tol=tol,
        )

        Returns
        -------
        """
        super().fit(X, y)
        # expose the fitted attributes `x_scores_` and `y_scores_`
        self.x_scores_ = self._x_scores
        self.y_scores_ = self._y_scores

class PLSCanonical(_PLS):
    """Partial Least Squares transformer and regressor.

        Number of components to keep. Should be in `[1, min(n_samples,
        n_features, n_targets)]`.

    algorithm : {'nipals', 'svd'}, default='nipals'
        The algorithm used to estimate the first singular vectors of the
        cross-covariance matrix. 'nipals' uses the power method while 'svd'
        will compute the whole SVD.

        Whether to copy `X` and `y` in fit before applying centering, and
        potentially scaling. If False, these operations will be done inplace,
        modifying both arrays.

    coef_ : ndarray of shape (n_targets, n_features)

        component. Empty if `algorithm='svd'`.

    See Also
    --------
    CCA : Canonical Correlation Analysis.
    PLSSVD : Partial Least Square SVD.

    Examples
    --------
    >>> from sklearn.cross_decomposition import PLSCanonical
    >>> plsca = PLSCanonical(n_components=2)
    >>> plsca.fit(X, y)
    PLSCanonical()
    >>> X_c, y_c = plsca.transform(X, y)
    """

    for param in ("deflation_mode", "mode"):

    # This implementation provides the same results that the "plspm" package
    # provided in the R language (R-project), using the function plsca(X, y).
    # Results are equal or collinear with the function
    # ``pls(..., mode = "canonical")`` of the "mixOmics" package. The
    # difference relies in the fact that mixOmics implementation does not
    # exactly implement the Wold algorithm since it does not normalize
    # y_weights to one.

        self,
        *,
    ):
            deflation_mode="canonical",
            mode="A",
            tol=tol,
        )

class CCA(_PLS):
    """Canonical Correlation Analysis, also known as "Mode B" PLS.

        The maximum number of iterations of the power method.

    See Also
    --------

    Examples
    --------
    >>> from sklearn.cross_decomposition import CCA
    >>> X = [[0., 0., 1.], [1.,0.,0.], [2.,2.,2.], [3.,5.,4.]]
    >>> cca = CCA(n_components=1)
    >>> cca.fit(X, y)
    CCA(n_components=1)
    >>> X_c, y_c = cca.transform(X, y)
    """

    ):
            mode="B",
            tol=tol,
        )

class PLSSVD(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """Partial Least Square SVD.

    This transformer simply performs a SVD on the cross-covariance matrix
    `X'y`. It is able to project both the training data `X` and the targets
    `y`. The training data `X` is projected on the left singular vectors, while
    the targets are projected on the right singular vectors.

        The number of components to keep. Should be in `[1,
        min(n_samples, n_features, n_targets)]`.

        potentially scaling. If `False`, these operations will be done inplace,

        The left singular vectors of the SVD of the cross-covariance matrix.
        Used to project `X` in :meth:`transform`.

    y_weights_ : ndarray of (n_targets, n_components)
        The right singular vectors of the SVD of the cross-covariance matrix.

    See Also
    --------

    Examples
    --------
    >>> from sklearn.cross_decomposition import PLSSVD
    >>> X = np.array([[0., 0., 1.],
    ...               [1., 0., 0.],
    ...               [2., 2., 2.],
    ...               [2., 5., 4.]])
    >>> y = np.array([[0.1, -0.2],
    ...               [0.9, 1.1],
    ...               [6.2, 5.9],
    ...               [11.9, 12.3]])
    >>> pls = PLSSVD(n_components=2).fit(X, y)
    >>> X_c, y_c = pls.transform(X, y)
    >>> X_c.shape, y_c.shape
    ((4, 2), (4, 2))
    """

    }

    def __init__(self, n_components=2, *, scale=True, copy=True):

            Training samples.

            Targets.

        Returns
        -------
        """
            self,
            X,
        )
            y,
        )

        # we'll compute the SVD of the cross-covariance matrix = X.T.dot(y)
        # This matrix rank is at most min(n_samples, n_features, n_targets) so
        # n_components cannot be bigger than that.
        rank_upper_bound = min(X.shape[0], X.shape[1], y.shape[1])
            )

        X, y, self._x_mean, self._y_mean, self._x_std, self._y_std = _center_scale_xy(
        )

        # Compute SVD of cross-covariance matrix
        C = np.dot(X.T, y)
        U, s, Vt = svd(C, full_matrices=False)
        U = U[:, :n_components]
        Vt = Vt[:n_components]
        U, Vt = svd_flip(U, Vt)
        V = Vt.T

        self.x_weights_ = U
        self.y_weights_ = V
        self._n_features_out = self.x_weights_.shape[1]

        """
        Apply the dimensionality reduction.

            Samples to be transformed.

        y : array-like of shape (n_samples,) or (n_samples, n_targets), \
            Targets.

        Returns
        -------
        x_scores : array-like or tuple of array-like
            The transformed data `X_transformed` if `y is not None`,
            `(X_transformed, y_transformed)` otherwise.
        """
        X = validate_data(self, X, dtype=np.float64, reset=False)
        Xr = (X - self._x_mean) / self._x_std
        x_scores = np.dot(Xr, self.x_weights_)
            y = check_array(y, input_name="y", ensure_2d=False, dtype=np.float64)
            yr = (y - self._y_mean) / self._y_std
            y_scores = np.dot(yr, self.y_weights_)

        """Learn and apply the dimensionality reduction.

            Targets.

        Returns
        -------
        out : array-like or tuple of array-like
        """

        self,
        *,
        mode="A",
    ):

        Returns
        -------
        """
            self,
            X,
        )
            y,
        )
        else:

        )
            )

        )

        # paper.

                try:
                    (
                        n_iter_,
                        Xk,
                        yk,
                    )
                        raise
                    break

                y_ss = 1
            else:

        )
        )

        Returns
        -------
        """
            )

        Returns
        -------

        Notes
        -----
        """

            Samples.

        Returns
        -------

        Notes
        -----
        space.
        """

        Returns
        -------
        """

    ):
            mode="A",
            tol=tol,
        )

        Returns
        -------
        """

        self,
        *,
    ):
            mode="A",
            tol=tol,
        )

    ):
            mode="B",
            tol=tol,
        )

            Targets.

        Returns
        -------
        """
            self,
            X,
        )
            y,
        )

            )

        )

        V = Vt.T

        """

            Targets.

        Returns
        -------
        """

            Targets.

        Returns
        -------
        """

def assert_matrix_orthogonal(M):
    K = np.dot(M.T, M)
    assert_array_almost_equal(K, np.diag(np.diag(K)))

def test_pls_canonical_basics():
    # Basic checks for PLSCanonical
    d = load_linnerud()
    X = d.data
    y = d.target

    pls = PLSCanonical(n_components=X.shape[1])
    pls.fit(X, y)

    assert_matrix_orthogonal(pls.x_weights_)
    assert_matrix_orthogonal(pls.y_weights_)
    assert_matrix_orthogonal(pls._x_scores)
    assert_matrix_orthogonal(pls._y_scores)

    # Check X = TP' and y = UQ'
    T = pls._x_scores
    P = pls.x_loadings_
    U = pls._y_scores
    Q = pls.y_loadings_
    # Need to scale first
    Xc, yc, x_mean, y_mean, x_std, y_std = _center_scale_xy(
        X.copy(), y.copy(), scale=True
    )
    assert_array_almost_equal(Xc, np.dot(T, P.T))
    assert_array_almost_equal(yc, np.dot(U, Q.T))

    # Check that rotations on training data lead to scores
    Xt = pls.transform(X)
    assert_array_almost_equal(Xt, pls._x_scores)
    Xt, yt = pls.transform(X, y)
    assert_array_almost_equal(yt, pls._y_scores)

    # Check that inverse_transform works
    X_back = pls.inverse_transform(Xt)
    assert_array_almost_equal(X_back, X)
    _, y_back = pls.inverse_transform(Xt, yt)
    assert_array_almost_equal(y_back, y)

def test_sanity_check_pls_regression():
    # Sanity check for PLSRegression
    # The results were checked against the R-packages plspm, misOmics and pls

    X = d.data

    pls = PLSRegression(n_components=X.shape[1])
    X_trans, _ = pls.fit_transform(X, y)

    # FIXME: one would expect y_trans == pls.y_scores_ but this is not
    # the case.
    # xref: https://github.com/scikit-learn/scikit-learn/issues/22420
    assert_allclose(X_trans, pls.x_scores_)

    expected_x_weights = np.array(
        [
            [-0.61330704, -0.00443647, 0.78983213],
            [-0.74697144, -0.32172099, -0.58183269],
            [-0.25668686, 0.94682413, -0.19399983],
        ]
    )

    expected_x_loadings = np.array(
        [
            [-0.61470416, -0.24574278, 0.78983213],
            [-0.65625755, -0.14396183, -0.58183269],
            [-0.51733059, 1.00609417, -0.19399983],
        ]
    )

    expected_y_weights = np.array(
        [
            [+0.32456184, 0.29892183, 0.20316322],
            [+0.42439636, 0.61970543, 0.19320542],
            [-0.13143144, -0.26348971, -0.17092916],
        ]
    )

    expected_y_loadings = np.array(
        [
        ]
    )

    assert_array_almost_equal(np.abs(pls.x_loadings_), np.abs(expected_x_loadings))
    assert_array_almost_equal(np.abs(pls.x_weights_), np.abs(expected_x_weights))
    assert_array_almost_equal(np.abs(pls.y_loadings_), np.abs(expected_y_loadings))
    assert_array_almost_equal(np.abs(pls.y_weights_), np.abs(expected_y_weights))

    # The R / Python difference in the signs should be consistent across
    # loadings, weights, etc.
    x_loadings_sign_flip = np.sign(pls.x_loadings_ / expected_x_loadings)
    x_weights_sign_flip = np.sign(pls.x_weights_ / expected_x_weights)
    y_weights_sign_flip = np.sign(pls.y_weights_ / expected_y_weights)
    y_loadings_sign_flip = np.sign(pls.y_loadings_ / expected_y_loadings)
    assert_array_almost_equal(x_loadings_sign_flip, x_weights_sign_flip)
    assert_array_almost_equal(y_loadings_sign_flip, y_weights_sign_flip)

def test_sanity_check_pls_regression_constant_column_y():
    # Check behavior when the first column of y is constant
    # The results are checked against a modified version of plsreg2
    # from the R-package plsdepot
    X = d.data
    y[:, 0] = 1

        [
            [-0.6273573, 0.007081799, 0.7786994],
            [-0.7493417, -0.277612681, -0.6011807],
            [-0.2119194, 0.960666981, -0.1794690],
        ]
    )

        [
            [-0.6273512, -0.22464538, 0.7786994],
            [-0.6643156, -0.09871193, -0.6011807],
            [-0.5125877, 1.01407380, -0.1794690],
        ]
    )

        [
            [0.0000000, 0.0000000, 0.0000000],
            [0.4357300, 0.5828479, 0.2174802],
            [-0.1353739, -0.2486423, -0.1810386],
        ]
    )

    assert_array_almost_equal(np.abs(expected_x_weights), np.abs(pls.x_weights_))
    assert_array_almost_equal(np.abs(expected_x_loadings), np.abs(pls.x_loadings_))
    # For the PLSRegression with default parameters, y_loadings == y_weights
    assert_array_almost_equal(np.abs(pls.y_weights_), np.abs(expected_y_loadings))

    x_loadings_sign_flip = np.sign(expected_x_loadings / pls.x_loadings_)
    x_weights_sign_flip = np.sign(expected_x_weights / pls.x_weights_)
    # we ignore the first full-zeros row for y
    y_loadings_sign_flip = np.sign(expected_y_loadings[1:] / pls.y_loadings_[1:])

    assert_array_equal(x_loadings_sign_flip, x_weights_sign_flip)
    assert_array_equal(x_loadings_sign_flip[1:], y_loadings_sign_flip)

def test_sanity_check_pls_canonical():
    # Sanity check for PLSCanonical
    # The results were checked against the R-package plspm

    X = d.data

        [
            [-0.61330704, 0.25616119, -0.74715187],
            [-0.74697144, 0.11930791, 0.65406368],
            [-0.25668686, -0.95924297, -0.11817271],
        ]
    )

    expected_x_rotations = np.array(
        [
            [-0.61330704, 0.41591889, -0.62297525],
            [-0.74697144, 0.31388326, 0.77368233],
            [-0.25668686, -0.89237972, -0.24121788],
        ]
    )

        [
            [+0.58989127, 0.7890047, 0.1717553],
            [+0.77134053, -0.61351791, 0.16920272],
            [-0.23887670, -0.03267062, 0.97050016],
        ]
    )

    expected_y_rotations = np.array(
        [
            [+0.58989127, 0.7168115, 0.30665872],
            [+0.77134053, -0.70791757, 0.19786539],
            [-0.23887670, -0.00343595, 0.94162826],
        ]
    )

    assert_array_almost_equal(np.abs(pls.x_rotations_), np.abs(expected_x_rotations))
    assert_array_almost_equal(np.abs(pls.y_rotations_), np.abs(expected_y_rotations))

    x_rotations_sign_flip = np.sign(pls.x_rotations_ / expected_x_rotations)
    y_rotations_sign_flip = np.sign(pls.y_rotations_ / expected_y_rotations)
    assert_array_almost_equal(x_rotations_sign_flip, x_weights_sign_flip)
    assert_array_almost_equal(y_rotations_sign_flip, y_weights_sign_flip)

def test_sanity_check_pls_canonical_random():
    # Sanity check for PLSCanonical on random data
    n = 500
    p_noise = 10
    q_noise = 5
    # 2 latents vars:
    rng = check_random_state(11)
    l1 = rng.normal(size=n)
    l2 = rng.normal(size=n)
    latents = np.array([l1, l1, l2, l2]).T
    X = latents + rng.normal(size=4 * n).reshape((n, 4))
    y = latents + rng.normal(size=4 * n).reshape((n, 4))
    X = np.concatenate((X, rng.normal(size=p_noise * n).reshape(n, p_noise)), axis=1)
    y = np.concatenate((y, rng.normal(size=q_noise * n).reshape(n, q_noise)), axis=1)

    pls = PLSCanonical(n_components=3)

        [
            [0.65803719, 0.19197924, 0.21769083],
            [0.7009113, 0.13303969, -0.15376699],
            [0.13528197, -0.68636408, 0.13856546],
            [0.16854574, -0.66788088, -0.12485304],
            [-0.03232333, -0.04189855, 0.40690153],
            [0.1148816, -0.09643158, 0.1613305],
            [0.04792138, -0.02384992, 0.17175319],
            [-0.06781, -0.01666137, -0.18556747],
            [-0.00266945, -0.00160224, 0.11893098],
            [-0.00849528, -0.07706095, 0.1570547],
            [-0.00949471, -0.02964127, 0.34657036],
            [-0.03572177, 0.0945091, 0.3414855],
            [0.05584937, -0.02028961, -0.57682568],
            [0.05744254, -0.01482333, -0.17431274],
        ]
    )

        [
            [0.65649254, 0.1847647, 0.15270699],
            [0.67554234, 0.15237508, -0.09182247],
            [0.19219925, -0.67750975, 0.08673128],
            [0.2133631, -0.67034809, -0.08835483],
            [-0.03178912, -0.06668336, 0.43395268],
            [0.15684588, -0.13350241, 0.20578984],
            [0.03337736, -0.03807306, 0.09871553],
            [-0.06199844, 0.01559854, -0.1881785],
            [0.00406146, -0.00587025, 0.16413253],
            [-0.00374239, -0.05848466, 0.19140336],
            [0.00139214, -0.01033161, 0.32239136],
            [-0.05292828, 0.0953533, 0.31916881],
            [0.04031924, -0.01961045, -0.65174036],
            [0.06172484, -0.06597366, -0.1244497],
        ]
    )

        [
            [0.66101097, 0.18672553, 0.22826092],
            [0.69347861, 0.18463471, -0.23995597],
            [0.14462724, -0.66504085, 0.17082434],
            [0.22247955, -0.6932605, -0.09832993],
            [0.07035859, 0.00714283, 0.67810124],
            [0.07765351, -0.0105204, -0.44108074],
            [-0.00917056, 0.04322147, 0.10062478],
            [-0.01909512, 0.06182718, 0.28830475],
            [0.01756709, 0.04797666, 0.32225745],
        ]
    )

        [
            [0.68568625, 0.1674376, 0.0969508],
            [0.68782064, 0.20375837, -0.1164448],
            [0.11712173, -0.68046903, 0.12001505],
            [0.17860457, -0.6798319, -0.05089681],
            [0.06265739, -0.0277703, 0.74729584],
            [0.0914178, 0.00403751, -0.5135078],
            [-0.02196918, -0.01377169, 0.09564505],
            [-0.03288952, 0.09039729, 0.31858973],
            [0.04287624, 0.05254676, 0.27836841],
        ]
    )

def test_convergence_fail():
    # Make sure ConvergenceWarning is raised if max_iter is too small
    X = d.data
    pls_nipals = PLSCanonical(n_components=X.shape[1], max_iter=2)
        pls_nipals.fit(X, y)

def test_attibutes_shapes(Est):
    # Make sure attributes are of the correct shape depending on n_components
    X = d.data
    pls = Est(n_components=n_components)
        attr.shape[1] == n_components for attr in (pls.x_weights_, pls.y_weights_)
    )

def test_univariate_equivalence(Est):
    # Ensure 2D y with 1 column is equivalent to 1D y
    X = d.data

    est = Est(n_components=1)
    one_d_coeff = est.fit(X, y[:, 0]).coef_
    two_d_coeff = est.fit(X, y[:, :1]).coef_

    assert one_d_coeff.shape == two_d_coeff.shape
    assert_array_almost_equal(one_d_coeff, two_d_coeff)

def test_copy(Est):
    # check that the "copy" keyword works
    X = d.data
    X_orig = X.copy()

    # copy=True won't modify inplace
    pls = Est(copy=True).fit(X, y)
    assert_array_equal(X, X_orig)

    # copy=False will modify inplace
        Est(copy=False).fit(X, y)
        assert_array_almost_equal(X, X_orig)

    if Est is PLSSVD:
        return  # PLSSVD does not support copy param in predict or transform

        pls.transform(X, y, copy=False)

        pls.predict(X, copy=False)

    # Make sure copy=True gives same transform and predictions as predict=False
        pls.transform(X, y, copy=True), pls.transform(X.copy(), y.copy(), copy=False)
    )
        pls.predict(X, copy=True), pls.predict(X.copy(), copy=False)
    )

def _generate_test_scale_and_stability_datasets():
    """Generate dataset for test_scale_and_stability"""
    # dataset for non-regression 7818
    n_targets = 5
    Q = rng.randn(n_targets, n_features)
    y = rng.randn(n_samples, n_targets)
    X = np.dot(y, Q) + 2 * rng.randn(n_samples, n_features) + 1
    X *= 1000
    yield X, y

    # Data set where one of the features is constraint
    X, y = load_linnerud(return_X_y=True)
    # causes X[:, -1].std() to be zero
    X[:, -1] = 1.0
    yield X, y

    X = np.array([[0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [2.0, 2.0, 2.0], [3.0, 5.0, 4.0]])
    y = np.array([[0.1, -0.2], [0.9, 1.1], [6.2, 5.9], [11.9, 12.3]])
    yield X, y

    # Seeds that provide a non-regression test for #18746, where CCA fails
    seeds = [530, 741]
    for seed in seeds:
        X = rng.randn(4, 3)
        y = rng.randn(4, 2)
        yield X, y

def test_scale_and_stability(Est, X, y):
    """scale=True is equivalent to scale=False on centered/scaled data
    This allows to check numerical stability over platforms as well"""

    X_s, y_s, *_ = _center_scale_xy(X, y)

    X_score, y_score = Est(scale=True).fit_transform(X, y)
    X_s_score, y_s_score = Est(scale=False).fit_transform(X_s, y_s)

    assert_allclose(X_s_score, X_score, atol=1e-4)
    assert_allclose(y_s_score, y_score, atol=1e-4)

def test_n_components_upper_bounds(Estimator):
    """Check the validation of `n_components` upper bounds for `PLS` regressors."""
    X = rng.randn(10, 5)
    y = rng.randn(10, 3)
    est = Estimator(n_components=10)
    err_msg = "`n_components` upper bound is .*. Got 10 instead. Reduce `n_components`."

def test_n_components_upper_PLSRegression():
    """Check the validation of `n_components` upper bounds for PLSRegression."""
    X = rng.randn(20, 64)
    y = rng.randn(20, 3)
    est = PLSRegression(n_components=30)
    err_msg = "`n_components` upper bound is 20. Got 30 instead. Reduce `n_components`."

def test_singular_value_helpers(n_samples, n_features, global_random_seed):
    # Make sure SVD and power method give approximately the same results
        n_samples, n_features, n_targets=5, random_state=global_random_seed
    )
    u1, v1, _ = _get_first_singular_vectors_power_method(X, y, norm_y_weights=True)
    u2, v2 = _get_first_singular_vectors_svd(X, y)

    _svd_flip_1d(u1, v1)
    _svd_flip_1d(u2, v2)

    rtol = 1e-3
    # Setting atol because some coordinates are very close to zero
    assert_allclose(u1, u2, atol=u2.max() * rtol)
    assert_allclose(v1, v2, atol=v2.max() * rtol)

def test_one_component_equivalence(global_random_seed):
    # PLSSVD, PLSRegression and PLSCanonical should all be equivalent when
    # n_components is 1
    X, y = make_regression(100, 10, n_targets=5, random_state=global_random_seed)
    svd = PLSSVD(n_components=1).fit(X, y).transform(X)
    reg = PLSRegression(n_components=1).fit(X, y).transform(X)
    canonical = PLSCanonical(n_components=1).fit(X, y).transform(X)

    rtol = 1e-3
    # Setting atol because some entries are very close to zero
    assert_allclose(svd, reg, atol=reg.max() * rtol)
    assert_allclose(svd, canonical, atol=canonical.max() * rtol)

def test_svd_flip_1d():
    # Make sure svd_flip_1d is equivalent to svd_flip
    u = np.array([1, -4, 2])
    v = np.array([1, 2, 3])

    u_expected, v_expected = svd_flip(u.reshape(-1, 1), v.reshape(1, -1))
    _svd_flip_1d(u, v)  # inplace

    assert_allclose(u, u_expected.ravel())
    assert_allclose(u, [-1, 4, -2])

    assert_allclose(v, v_expected.ravel())
    assert_allclose(v, [-1, -2, -3])

def test_loadings_converges(global_random_seed):
    """Test that CCA converges. Non-regression test for #19549."""
        n_samples=200, n_features=20, n_targets=20, random_state=global_random_seed
    )

    cca = CCA(n_components=10, max_iter=500)

        cca.fit(X, y)

    # Loadings converges to reasonable values
    assert np.all(np.abs(cca.x_loadings_) < 1)

def test_pls_constant_y():
    """Checks warning when y is constant. Non-regression test for #19831"""
    x = rng.rand(100, 3)
    y = np.zeros(100)

    pls = PLSRegression()

    msg = "y residual is constant at iteration"
        pls.fit(x, y)

    assert_allclose(pls.x_rotations_, 0)

def test_pls_coef_shape(PLSEstimator):
    """Check the shape of `coef_` attribute.

    https://github.com/scikit-learn/scikit-learn/issues/12410
    """
    X = d.data

    pls = PLSEstimator(copy=True).fit(X, y)

    n_targets, n_features = y.shape[1], X.shape[1]
    assert pls.coef_.shape == (n_targets, n_features)

def test_pls_prediction(PLSEstimator, scale):
    """Check the behaviour of the prediction function."""
    X = d.data

    pls = PLSEstimator(copy=True, scale=scale).fit(X, y)
    y_pred = pls.predict(X, copy=True)

    X_trans = X - X.mean(axis=0)

    assert_allclose(pls.intercept_, y_mean)
    assert_allclose(y_pred, X_trans @ pls.coef_.T + pls.intercept_)

def test_pls_feature_names_out(Klass):
    """Check `get_feature_names_out` cross_decomposition module."""

    est = Klass().fit(X, y)

        [f"{class_name_lower}{i}" for i in range(est.x_weights_.shape[1])],
    )

def test_pls_set_output(Klass):
    """Check `set_output` in cross_decomposition module."""
    X, y = load_linnerud(return_X_y=True, as_frame=True)

    est = Klass().set_output(transform="pandas").fit(X, y)
    X_trans, y_trans = est.transform(X, y)
    assert isinstance(y_trans, np.ndarray)
    assert isinstance(X_trans, pd.DataFrame)
    assert_array_equal(X_trans.columns, est.get_feature_names_out())

def test_pls_regression_fit_1d_y():
    """Check that when fitting with 1d `y`, prediction should also be 1d.

    Non-regression test for Issue #26549.
    """
    X = np.array([[1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36]])
    y = np.array([2, 6, 12, 20, 30, 42])
    expected = y.copy()

    plsr = PLSRegression().fit(X, y)
    y_pred = plsr.predict(X)
    assert y_pred.shape == expected.shape

    # Check that it works in VotingRegressor
    lr = LinearRegression().fit(X, y)
    vr = VotingRegressor([("lr", lr), ("plsr", plsr)])
    y_pred = vr.fit(X, y).predict(X)
    assert_allclose(y_pred, expected)

def test_pls_regression_scaling_coef():
    """Check that when using `scale=True`, the coefficients are using the std. dev. from
    both `X` and `y`.

    https://github.com/scikit-learn/scikit-learn/issues/27964
    """
    # handcrafted data where we can predict y from X with an additional scaling factor
    coef = rng.uniform(size=(3, 5))
    X = rng.normal(scale=10, size=(30, 5))  # add a std of 10
    y = X @ coef.T

    # we need to make sure that the dimension of the latent space is large enough to
    # perfectly predict `y` from `X` (no information loss)
    pls = PLSRegression(n_components=5, scale=True).fit(X, y)
    assert_allclose(pls.coef_, coef)

    # we therefore should be able to predict `y` from `X`
    assert_allclose(pls.predict(X), y)

def _graph_connected_component(graph, node_id):
    """Find the largest graph connected components that contains one
    given node.

    graph : array-like of shape (n_samples, n_samples)
        Adjacency matrix of the graph, non-zero weight means an edge
        between the nodes.

    node_id : int
        The index of the query node of the graph.

    Returns
    -------
    connected_components_matrix : array-like of shape (n_samples,)
        An array of bool value indicating the indexes of the nodes
        belonging to the largest connected components of the given query
        node.
    """
    n_node = graph.shape[0]
    if sparse.issparse(graph):
        # speed up row-wise access to boolean connection mask
        graph = graph.tocsr()
    connected_nodes = np.zeros(n_node, dtype=bool)
    nodes_to_explore = np.zeros(n_node, dtype=bool)
    nodes_to_explore[node_id] = True
    for _ in range(n_node):
        last_num_component = connected_nodes.sum()
        np.logical_or(connected_nodes, nodes_to_explore, out=connected_nodes)
        if last_num_component >= connected_nodes.sum():
            break
        indices = np.where(nodes_to_explore)[0]
        nodes_to_explore.fill(False)
        for i in indices:
            if sparse.issparse(graph):
                # scipy not yet implemented 1D sparse slices; can be changed back to
                # `neighbors = graph[i].toarray().ravel()` once implemented
                neighbors = graph[[i], :].toarray().ravel()
            else:
                neighbors = graph[i]
            np.logical_or(nodes_to_explore, neighbors, out=nodes_to_explore)
    return connected_nodes

def _graph_is_connected(graph):
    """Return whether the graph is connected (True) or Not (False).

    graph : {array-like, sparse matrix} of shape (n_samples, n_samples)

    Returns
    -------
    is_connected : bool
        True means the graph is fully connected and False means not.
    """
        # Before Scipy 1.11.3, `connected_components` only supports 32-bit indices.
        # PR: https://github.com/scipy/scipy/pull/18913
        # First integration in 1.11.3: https://github.com/scipy/scipy/pull/19279
        # TODO(jjerphan): Once SciPy 1.11.3 is the minimum supported version, use
        # `accept_large_sparse=True`.
        accept_large_sparse = sp_version >= parse_version("1.11.3")
        graph = check_array(
            graph, accept_sparse=True, accept_large_sparse=accept_large_sparse
        )
        # sparse graph, find all the connected components
        n_connected_components, _ = connected_components(graph)
        return n_connected_components == 1
    else:
        # dense graph, find all connected components start from node 0
        return _graph_connected_component(graph, 0).sum() == graph.shape[0]

def _set_diag(laplacian, value, norm_laplacian):
    """Set the diagonal of the laplacian matrix and convert it to a
    sparse format well suited for eigenvalue decomposition.

    laplacian : {ndarray, sparse matrix}
        The graph laplacian.

    value : float
        The value of the diagonal.

    norm_laplacian : bool
        Whether the value of the diagonal should be changed or not.

    Returns
    -------
    laplacian : {array, sparse matrix}
        An array of matrix in a form that is well suited to fast
        eigenvalue decomposition, depending on the band width of the
        matrix.
    """
    n_nodes = laplacian.shape[0]
    # We need all entries in the diagonal to values
    if not sparse.issparse(laplacian):
        if norm_laplacian:
            laplacian.flat[:: n_nodes + 1] = value
    else:
        laplacian = laplacian.tocoo()
            diag_idx = laplacian.row == laplacian.col
            laplacian.data[diag_idx] = value
        # If the matrix has a small number of diagonals (as in the
        # case of structured matrices coming from images), the
        # dia format might be best suited for matvec products:
        n_diags = np.unique(laplacian.row - laplacian.col).size
        if n_diags <= 7:
            # 3 or less outer diagonals on each side
            laplacian = laplacian.todia()
        else:
            # csr has the fastest matvec and is thus best suited to
            # arpack
            laplacian = laplacian.tocsr()
    return laplacian

def spectral_embedding(
    adjacency,
    *,
    n_components=8,
    eigen_solver=None,
    eigen_tol="auto",
    norm_laplacian=True,
    drop_first=True,
):
    """Project the sample on the first eigenvectors of the graph Laplacian.

    The adjacency matrix is used to compute a normalized graph Laplacian
    whose spectrum (especially the eigenvectors associated to the
    smallest eigenvalues) has an interpretation in terms of minimal
    number of cuts necessary to split the graph into comparably sized
    components.

    This embedding can also 'work' even if the ``adjacency`` variable is
    not strictly the adjacency matrix of a graph but more generally
    an affinity or similarity matrix between samples (for instance the
    heat kernel of a euclidean distance matrix or a k-NN matrix).

    However care must taken to always make the affinity matrix symmetric
    so that the eigenvector decomposition works as expected.

    Note : Laplacian Eigenmaps is the actual algorithm implemented here.

    Read more in the :ref:`User Guide <spectral_embedding>`.

    adjacency : {array-like, sparse graph} of shape (n_samples, n_samples)
        The adjacency matrix of the graph to embed.

    n_components : int, default=8
        The dimension of the projection subspace.

    eigen_solver : {'arpack', 'lobpcg', 'amg'}, default=None
        The eigenvalue decomposition strategy to use. AMG requires pyamg
        to be installed. It can be faster on very large, sparse problems,
        but may also lead to instabilities. If None, then ``'arpack'`` is
        used.

        A pseudo random number generator used for the initialization
        of the lobpcg eigen vectors decomposition when `eigen_solver ==
        'amg'`, and for the K-Means initialization. Use an int to make
        the results deterministic across calls (See
        :term:`Glossary <random_state>`).

        .. note::
            When using `eigen_solver == 'amg'`,
            it is necessary to also fix the global numpy seed with
            `np.random.seed(int)` to get deterministic results. See
            https://github.com/pyamg/pyamg/issues/139 for further
            information.

    eigen_tol : float, default="auto"
        Stopping criterion for eigendecomposition of the Laplacian matrix.
        If `eigen_tol="auto"` then the passed tolerance will depend on the
        `eigen_solver`:

        - If `eigen_solver="arpack"`, then `eigen_tol=0.0`;
        - If `eigen_solver="lobpcg"` or `eigen_solver="amg"`, then
          `eigen_tol=None` which configures the underlying `lobpcg` solver to
          automatically resolve the value according to their heuristics. See,
          :func:`scipy.sparse.linalg.lobpcg` for details.

        Note that when using `eigen_solver="amg"` values of `tol<1e-5` may lead
        to convergence issues and should be avoided.

           Added 'auto' option.

    norm_laplacian : bool, default=True
        If True, then compute symmetric normalized Laplacian.

    drop_first : bool, default=True
        Whether to drop the first eigenvector. For spectral embedding, this
        should be True as the first eigenvector should be constant vector for
        connected graph, but for spectral clustering, this should be kept as
        False to retain the first eigenvector.

    Returns
    -------
    embedding : ndarray of shape (n_samples, n_components)
        The reduced samples.

    Notes
    -----
    Spectral Embedding (Laplacian Eigenmaps) is most useful when the graph
    has one connected component. If there graph has many components, the first
    few eigenvectors will simply uncover the connected components of the graph.

    * https://en.wikipedia.org/wiki/LOBPCG

    * :doi:`"Toward the Optimal Preconditioned Eigensolver: Locally Optimal
      Block Preconditioned Conjugate Gradient Method",
      Andrew V. Knyazev
      <10.1137/S1064827500366124>`

    Examples
    --------
    >>> from sklearn.manifold import spectral_embedding
    >>> X = X[:100]
    >>> affinity_matrix = kneighbors_graph(
    ...     X, n_neighbors=int(X.shape[0] / 10), include_self=True
    ... )
    >>> # make the matrix symmetric
    >>> affinity_matrix = 0.5 * (affinity_matrix + affinity_matrix.T)
    >>> embedding = spectral_embedding(affinity_matrix, n_components=2, random_state=42)
    >>> embedding.shape
    (100, 2)
    """

    return _spectral_embedding(
        adjacency,
        eigen_solver=eigen_solver,
        eigen_tol=eigen_tol,
        norm_laplacian=norm_laplacian,
        drop_first=drop_first,
    )

def _spectral_embedding(
    *,
):
    adjacency = check_symmetric(adjacency)

    if eigen_solver == "amg":
        try:
            from pyamg import smoothed_aggregation_solver
        except ImportError as e:
                "The eigen_solver was set to 'amg', but pyamg is not available."
            ) from e

    if eigen_solver is None:
        eigen_solver = "arpack"

    n_nodes = adjacency.shape[0]
    # Whether to drop the first eigenvector
    if drop_first:
        n_components = n_components + 1

    if not _graph_is_connected(adjacency):
            "Graph is not fully connected, spectral embedding may not work as expected."
        )

    laplacian, dd = csgraph_laplacian(
        adjacency, normed=norm_laplacian, return_diag=True
    )
    if eigen_solver == "arpack" or (
        eigen_solver != "lobpcg"
        and (not sparse.issparse(laplacian) or n_nodes < 5 * n_components)
    ):
        # lobpcg used with eigen_solver='amg' has bugs for low number of nodes
        # for details see the source code in scipy:
        # https://github.com/scipy/scipy/blob/v0.11.0/scipy/sparse/linalg/eigen
        # /lobpcg/lobpcg.py#L237
        # or matlab:
        # https://www.mathworks.com/matlabcentral/fileexchange/48-lobpcg-m
        laplacian = _set_diag(laplacian, 1, norm_laplacian)

        # Here we'll use shift-invert mode for fast eigenvalues
        # (see https://docs.scipy.org/doc/scipy/reference/tutorial/arpack.html
        #  for a short explanation of what this means)
        # Because the normalized Laplacian has eigenvalues between 0 and 2,
        # I - L has eigenvalues between -1 and 1.  ARPACK is most efficient
        # when finding eigenvalues of largest magnitude (keyword which='LM')
        # and when these eigenvalues are very large compared to the rest.
        # For very large, very sparse graphs, I - L can have many, many
        # eigenvalues very near 1.0.  This leads to slow convergence.  So
        # instead, we'll use ARPACK's shift-invert mode, asking for the
        # eigenvalues near 1.0.  This effectively spreads-out the spectrum
        # near 1.0 and leads to much faster convergence: potentially an
        # orders-of-magnitude speedup over simply using keyword which='LA'
        # in standard mode.
        try:
            # We are computing the opposite of the laplacian inplace so as
            # to spare a memory allocation of a possibly very large array
            tol = 0 if eigen_tol == "auto" else eigen_tol
            laplacian *= -1
            v0 = _init_arpack_v0(laplacian.shape[0], random_state)
            laplacian = check_array(
                laplacian, accept_sparse="csr", accept_large_sparse=False
            )
            _, diffusion_map = eigsh(
                laplacian, k=n_components, sigma=1.0, which="LM", tol=tol, v0=v0
            )
            embedding = diffusion_map.T[n_components::-1]
            if norm_laplacian:
                # recover u = D^-1/2 x from the eigenvector output x
                embedding = embedding / dd
        except RuntimeError:
            # When submatrices are exactly singular, an LU decomposition
            # in arpack fails. We fallback to lobpcg
            eigen_solver = "lobpcg"
            # Revert the laplacian to its opposite to have lobpcg work

    elif eigen_solver == "amg":
        # Use AMG to get a preconditioner and speed up the eigenvalue
        # problem.
        if not sparse.issparse(laplacian):
            warnings.warn("AMG works better for sparse matrices")
        laplacian = check_array(
            laplacian, dtype=[np.float64, np.float32], accept_sparse=True
        )

        # The Laplacian matrix is always singular, having at least one zero
        # eigenvalue, corresponding to the trivial eigenvector, which is a
        # constant. Using a singular matrix for preconditioning may result in
        # random failures in LOBPCG and is not supported by the existing
        # theory:
        #     see https://doi.org/10.1007/s10208-015-9297-1
        # Shift the Laplacian so its diagononal is not all ones. The shift
        # does change the eigenpairs however, so we'll feed the shifted
        # matrix to the solver and afterward set it back to the original.
        diag_shift = 1e-5 * sparse.eye(laplacian.shape[0])
        laplacian += diag_shift
        if hasattr(sparse, "csr_array") and isinstance(laplacian, sparse.csr_array):
            # `pyamg` does not work with `csr_array` and we need to convert it to a
            # `csr_matrix` object.
            laplacian = sparse.csr_matrix(laplacian)
        ml = smoothed_aggregation_solver(check_array(laplacian, accept_sparse="csr"))
        laplacian -= diag_shift

        M = ml.aspreconditioner()
        # Create initial approximation X to eigenvectors
        X = random_state.standard_normal(size=(laplacian.shape[0], n_components + 1))
        X[:, 0] = dd.ravel()
        X = X.astype(laplacian.dtype)

        tol = None if eigen_tol == "auto" else eigen_tol
        _, diffusion_map = lobpcg(laplacian, X, M=M, tol=tol, largest=False)
        embedding = diffusion_map.T
            # recover u = D^-1/2 x from the eigenvector output x
            embedding = embedding / dd
        if embedding.shape[0] == 1:
            raise ValueError

    if eigen_solver == "lobpcg":
        )
        if n_nodes < 5 * n_components + 1:
            # see note above under arpack why lobpcg has problems with small
            # number of nodes
            # lobpcg will fallback to eigh, so we short circuit it
            if sparse.issparse(laplacian):
                laplacian = laplacian.toarray()
            _, diffusion_map = eigh(laplacian, check_finite=False)
            embedding = diffusion_map.T[:n_components]
        else:
            laplacian = _set_diag(laplacian, 1, norm_laplacian)
            # We increase the number of eigenvectors requested, as lobpcg
            # doesn't behave well in low dimension and create initial
            # approximation X to eigenvectors
            X = random_state.standard_normal(
                size=(laplacian.shape[0], n_components + 1)
            )
            X[:, 0] = dd.ravel()
            X = X.astype(laplacian.dtype)
            tol = None if eigen_tol == "auto" else eigen_tol
            _, diffusion_map = lobpcg(
                laplacian, X, tol=tol, largest=False, maxiter=2000
            )
            if embedding.shape[0] == 1:
                raise ValueError

    embedding = _deterministic_vector_sign_flip(embedding)
        return embedding[1:n_components].T
    else:
        return embedding[:n_components].T

class SpectralEmbedding(BaseEstimator):
    """Spectral embedding for non-linear dimensionality reduction.

    Forms an affinity matrix given by the specified function and
    applies spectral decomposition to the corresponding graph laplacian.
    The resulting transformation is given by the value of the
    eigenvectors for each data point.

        The dimension of the projected subspace.

    affinity : {'nearest_neighbors', 'rbf', 'precomputed', \
                'precomputed_nearest_neighbors'} or callable, \
                default='nearest_neighbors'
        How to construct the affinity matrix.
         - 'nearest_neighbors' : construct the affinity matrix by computing a
           graph of nearest neighbors.
         - 'rbf' : construct the affinity matrix by computing a radial basis
           function (RBF) kernel.
         - 'precomputed' : interpret ``X`` as a precomputed affinity matrix.
         - 'precomputed_nearest_neighbors' : interpret ``X`` as a sparse graph
           of precomputed nearest neighbors, and constructs the affinity matrix
           by selecting the ``n_neighbors`` nearest neighbors.
         - callable : use passed in function as affinity
           the function takes in data matrix (n_samples, n_features)
           and return affinity matrix (n_samples, n_samples).

        Kernel coefficient for rbf kernel. If None, gamma will be set to
        1/n_features.

        .. note::

        to be installed. It can be faster on very large, sparse problems.
        If None, then ``'arpack'`` is used.

        Note that when using `eigen_solver="lobpcg"` or `eigen_solver="amg"`
        values of `tol<1e-5` may lead to convergence issues and should be
        avoided.

    n_neighbors : int, default=None
        Number of nearest neighbors for nearest_neighbors graph building.
        If None, n_neighbors will be set to max(n_samples/10, 1).

    embedding_ : ndarray of shape (n_samples, n_components)
        Spectral embedding of the training matrix.

    affinity_matrix_ : ndarray of shape (n_samples, n_samples)
        Affinity_matrix constructed from samples or precomputed.

        Number of nearest neighbors effectively used.

    See Also
    --------
    Isomap : Non-linear dimensionality reduction through Isometric Mapping.

    - :doi:`A Tutorial on Spectral Clustering, 2007
      Ulrike von Luxburg
      <10.1007/s11222-007-9033-z>`

    - `On Spectral Clustering: Analysis and an algorithm, 2001
      Andrew Y. Ng, Michael I. Jordan, Yair Weiss
      <https://citeseerx.ist.psu.edu/doc_view/pid/796c5d6336fc52aa84db575fb821c78918b65f58>`_

    - :doi:`Normalized cuts and image segmentation, 2000
      Jianbo Shi, Jitendra Malik
      <10.1109/34.868688>`

    Examples
    --------
    >>> from sklearn.manifold import SpectralEmbedding
    (1797, 64)
    >>> embedding = SpectralEmbedding(n_components=2)
    >>> X_transformed = embedding.fit_transform(X[:100])
    (100, 2)
    """

        "affinity": [
                {
                    "nearest_neighbors",
                    "rbf",
                    "precomputed",
                    "precomputed_nearest_neighbors",
                },
            ),
            callable,
        ],
        "eigen_solver": [StrOptions({"arpack", "lobpcg", "amg"}), None],
        "eigen_tol": [Interval(Real, 0, None, closed="left"), StrOptions({"auto"})],
    }

        self,
        *,
        affinity="nearest_neighbors",
        eigen_solver=None,
        eigen_tol="auto",
    ):
        self.affinity = affinity
        self.eigen_tol = eigen_tol

        tags.input_tags.pairwise = self.affinity in [
            "precomputed",
            "precomputed_nearest_neighbors",
        ]

    def _get_affinity_matrix(self, X, Y=None):
        """Calculate the affinity matrix from data

            If affinity is "precomputed"
            X : array-like of shape (n_samples, n_samples),
            Interpret X as precomputed adjacency graph computed from
            samples.

        Y: Ignored

        Returns
        -------
        affinity_matrix of shape (n_samples, n_samples)
        """
        if self.affinity == "precomputed":
            self.affinity_matrix_ = X
            return self.affinity_matrix_
        if self.affinity == "precomputed_nearest_neighbors":
            estimator = NearestNeighbors(
                n_neighbors=self.n_neighbors, n_jobs=self.n_jobs, metric="precomputed"
            ).fit(X)
            connectivity = estimator.kneighbors_graph(X=X, mode="connectivity")
            self.affinity_matrix_ = 0.5 * (connectivity + connectivity.T)
        if self.affinity == "nearest_neighbors":
                    "Nearest neighbors affinity currently does "
                    "not support sparse input, falling back to "
                    "rbf affinity"
                )
                self.affinity = "rbf"
            else:
                self.n_neighbors_ = (
                    self.n_neighbors
                    if self.n_neighbors is not None
                    else max(int(X.shape[0] / 10), 1)
                )
                self.affinity_matrix_ = kneighbors_graph(
                    X, self.n_neighbors_, include_self=True, n_jobs=self.n_jobs
                )
                # currently only symmetric affinity_matrix supported
                self.affinity_matrix_ = 0.5 * (
                    self.affinity_matrix_ + self.affinity_matrix_.T
                )
                return self.affinity_matrix_
        if self.affinity == "rbf":
            self.gamma_ = self.gamma if self.gamma is not None else 1.0 / X.shape[1]
            self.affinity_matrix_ = rbf_kernel(X, gamma=self.gamma_)
        self.affinity_matrix_ = self.affinity(X)
        return self.affinity_matrix_

            X : {array-like, sparse matrix}, shape (n_samples, n_samples),
            samples.

        y : Ignored

        Returns
        -------
        """
        X = validate_data(self, X, accept_sparse="csr", ensure_min_samples=2)

        affinity_matrix = self._get_affinity_matrix(X)
        self.embedding_ = _spectral_embedding(
            affinity_matrix,
            n_components=self.n_components,
            eigen_solver=self.eigen_solver,
            eigen_tol=self.eigen_tol,
        )

            X : {array-like, sparse matrix} of shape (n_samples, n_samples),
            samples.

        y : Ignored

        Returns
        -------
            Spectral embedding of the training matrix.
        """
        self.fit(X)
        return self.embedding_

        self,
        *,
    ):

        ]

            samples.

        Y: Ignored

        Returns
        -------
        """
            ).fit(X)
                )
            else:
                )
                )
                )

            samples.

        y : Ignored

        Returns
        -------
        """

        )

            samples.

        y : Ignored

        Returns
        -------
        """

def _joint_probabilities(distances, desired_perplexity, verbose):
    """Compute joint probabilities p_ij from distances.

    distances : ndarray of shape (n_samples * (n_samples-1) / 2,)
        Distances of samples are stored as condensed matrices, i.e.
        we omit the diagonal and duplicate entries and store everything
        in a one-dimensional array.

    desired_perplexity : float
        Desired perplexity of the joint probability distributions.

    Returns
    -------
    P : ndarray of shape (n_samples * (n_samples-1) / 2,)
        Condensed joint probability matrix.
    """
    # Compute conditional probabilities such that they approximately match
    # the desired perplexity
    distances = distances.astype(np.float32, copy=False)
    conditional_P = _utils._binary_search_perplexity(
        distances, desired_perplexity, verbose
    )
    P = conditional_P + conditional_P.T
    sum_P = np.maximum(np.sum(P), MACHINE_EPSILON)
    P = np.maximum(squareform(P) / sum_P, MACHINE_EPSILON)
    return P

def _joint_probabilities_nn(distances, desired_perplexity, verbose):
    """Compute joint probabilities p_ij from distances using just nearest
    neighbors.

    This method is approximately equal to _joint_probabilities. The latter
    is O(N), but limiting the joint probability to nearest neighbors improves
    this substantially to O(uN).

    distances : sparse matrix of shape (n_samples, n_samples)
        Distances of samples to its n_neighbors nearest neighbors. All other
        distances are left to zero (and are not materialized in memory).
        Matrix should be of CSR format.

    Returns
    -------
    P : sparse matrix of shape (n_samples, n_samples)
        Condensed joint probability matrix with only nearest neighbors. Matrix
        will be of CSR format.
    """
    t0 = time()
    distances.sort_indices()
    n_samples = distances.shape[0]
    distances_data = distances.data.reshape(n_samples, -1)
    distances_data = distances_data.astype(np.float32, copy=False)
        distances_data, desired_perplexity, verbose
    )
    assert np.all(np.isfinite(conditional_P)), "All probabilities should be finite"

    # Symmetrize the joint probability distribution using sparse operations
    P = csr_matrix(
        (conditional_P.ravel(), distances.indices, distances.indptr),
        shape=(n_samples, n_samples),
    )
    P = P + P.T

    # Normalize the joint probability distribution
    sum_P = np.maximum(P.sum(), MACHINE_EPSILON)
    P /= sum_P

    assert np.all(np.abs(P.data) <= 1.0)
    if verbose >= 2:
        duration = time() - t0
        print("[t-SNE] Computed conditional probabilities in {:.3f}s".format(duration))
    return P

def _kl_divergence(
    params,
    P,
    degrees_of_freedom,
    skip_num_points=0,
    compute_error=True,
):
    """t-SNE objective function: gradient of the KL divergence
    of p_ijs and q_ijs and the absolute error.

    params : ndarray of shape (n_params,)
        Unraveled embedding.

    degrees_of_freedom : int
        Degrees of freedom of the Student's-t distribution.

        Dimension of the embedded space.

    skip_num_points : int, default=0
        This does not compute the gradient for points with indices below
        `skip_num_points`. This is useful when computing transforms of new
        data where you'd like to keep the old data fixed.

    compute_error: bool, default=True
        If False, the kl_divergence is not computed and returns NaN.

    Returns
    -------
    kl_divergence : float
        Kullback-Leibler divergence of p_ij and q_ij.

    grad : ndarray of shape (n_params,)
        Unraveled gradient of the Kullback-Leibler divergence with respect to
        the embedding.
    """
    X_embedded = params.reshape(n_samples, n_components)

    # Q is a heavy-tailed distribution: Student's t-distribution
    dist = pdist(X_embedded, "sqeuclidean")
    dist /= degrees_of_freedom
    dist += 1.0
    dist **= (degrees_of_freedom + 1.0) / -2.0
    Q = np.maximum(dist / (2.0 * np.sum(dist)), MACHINE_EPSILON)

    # Optimization trick below: np.dot(x, y) is faster than
    # np.sum(x * y) because it calls BLAS

    # Objective: C (Kullback-Leibler divergence of P and Q)
    if compute_error:
        kl_divergence = 2.0 * np.dot(P, np.log(np.maximum(P, MACHINE_EPSILON) / Q))
    else:
        kl_divergence = np.nan

    # Gradient: dC/dY
    # pdist always returns double precision distances. Thus we need to take
    grad = np.ndarray((n_samples, n_components), dtype=params.dtype)
    PQd = squareform((P - Q) * dist)
    for i in range(skip_num_points, n_samples):
        grad[i] = np.dot(np.ravel(PQd[i], order="K"), X_embedded[i] - X_embedded)
    grad = grad.ravel()
    c = 2.0 * (degrees_of_freedom + 1.0) / degrees_of_freedom
    grad *= c

    return kl_divergence, grad

def _kl_divergence_bh(
    params,
    P,
    angle=0.5,
    num_threads=1,
):
    """t-SNE objective function: KL divergence of p_ijs and q_ijs.

    Uses Barnes-Hut tree methods to calculate the gradient that
    runs in O(NlogN) instead of O(N^2).

    P : sparse matrix of shape (n_samples, n_sample)
        Sparse approximate joint probability matrix, computed only for the
        k nearest-neighbors and symmetrized. Matrix should be of CSR format.

    angle : float, default=0.5
        This is the trade-off between speed and accuracy for Barnes-Hut T-SNE.
        'angle' is the angular size (referred to as theta in [3]) of a distant
        node as measured from a point. If this size is below 'angle' then it is
        used as a summary node of all points contained within it.
        This method is not very sensitive to changes in this parameter
        in the range of 0.2 - 0.8. Angle less than 0.2 has quickly increasing
        computation time and angle greater 0.8 has quickly increasing error.

    verbose : int, default=False

    num_threads : int, default=1
        Number of threads used to compute the gradient. This is set here to
        avoid calling _openmp_effective_n_threads for each gradient step.

    Returns
    -------

    """
    params = params.astype(np.float32, copy=False)

    val_P = P.data.astype(np.float32, copy=False)
    neighbors = P.indices.astype(np.int64, copy=False)
    indptr = P.indptr.astype(np.int64, copy=False)

    grad = np.zeros(X_embedded.shape, dtype=np.float32)
    error = _barnes_hut_tsne.gradient(
        val_P,
        X_embedded,
        neighbors,
        indptr,
        grad,
        angle,
        verbose,
        dof=degrees_of_freedom,
        compute_error=compute_error,
        num_threads=num_threads,
    )
    grad *= c

    return error, grad

def _gradient_descent(
    objective,
    p0,
    it,
    max_iter,
    n_iter_check=1,
    n_iter_without_progress=300,
    momentum=0.8,
    learning_rate=200.0,
    min_gain=0.01,
    min_grad_norm=1e-7,
    args=None,
    kwargs=None,
):
    """Batch gradient descent with momentum and individual gains.

    objective : callable
        Should return a tuple of cost and gradient for a given parameter
        vector. When expensive to compute, the cost can optionally
        be None and can be computed every n_iter_check steps using
        the objective_error function.

    p0 : array-like of shape (n_params,)
        Initial parameter vector.

    it : int
        Current number of iterations (this function will be called more than
        once during the optimization).

    max_iter : int
        Maximum number of gradient descent iterations.

    n_iter_check : int, default=1
        Number of iterations before evaluating the global error. If the error
        is sufficiently low, we abort the optimization.

    n_iter_without_progress : int, default=300
        Maximum number of iterations without progress before we abort the
        optimization.

    momentum : float within (0.0, 1.0), default=0.8
        The momentum generates a weight for previous gradients that decays
        exponentially.

    learning_rate : float, default=200.0
        The learning rate for t-SNE is usually in the range [10.0, 1000.0]. If
        the learning rate is too high, the data may look like a 'ball' with any
        point approximately equidistant from its nearest neighbours. If the
        learning rate is too low, most points may look compressed in a dense
        cloud with few outliers.

    min_gain : float, default=0.01
        Minimum individual gain for each parameter.

    min_grad_norm : float, default=1e-7
        If the gradient norm is below this threshold, the optimization will
        be aborted.

    args : sequence, default=None
        Arguments to pass to objective function.

    kwargs : dict, default=None
        Keyword arguments to pass to objective function.

    Returns
    -------
    p : ndarray of shape (n_params,)
        Optimum parameters.

    error : float
        Optimum.

    i : int
        Last iteration.
    """
    if args is None:
        args = []
    if kwargs is None:
        kwargs = {}

    p = p0.copy().ravel()
    update = np.zeros_like(p)
    gains = np.ones_like(p)
    error = np.finfo(float).max
    best_error = np.finfo(float).max
    best_iter = i = it

    tic = time()
    for i in range(it, max_iter):
        check_convergence = (i + 1) % n_iter_check == 0
        # only compute the error when needed
        kwargs["compute_error"] = check_convergence or i == max_iter - 1

        error, grad = objective(p, *args, **kwargs)

        inc = update * grad < 0.0
        dec = np.invert(inc)
        gains[inc] += 0.2
        gains[dec] *= 0.8
        np.clip(gains, min_gain, np.inf, out=gains)
        grad *= gains
        update = momentum * update - learning_rate * grad
        p += update

        if check_convergence:
            toc = time()
            tic = toc
            grad_norm = linalg.norm(grad)

            if verbose >= 2:
                print(
                    "[t-SNE] Iteration %d: error = %.7f,"
                    " gradient norm = %.7f"
                    " (%s iterations in %0.3fs)"
                    % (i + 1, error, grad_norm, n_iter_check, duration)
                )

            if error < best_error:
                best_error = error
                best_iter = i
            elif i - best_iter > n_iter_without_progress:
                if verbose >= 2:
                    print(
                        "[t-SNE] Iteration %d: did not make any progress "
                        "during the last %d episodes. Finished."
                        % (i + 1, n_iter_without_progress)
                    )
                break
            if grad_norm <= min_grad_norm:
                    print(
                        "[t-SNE] Iteration %d: gradient norm %f. Finished."
                        % (i + 1, grad_norm)
                    )
                break

    return p, error, i

def trustworthiness(X, X_embedded, *, n_neighbors=5, metric="euclidean"):
    r"""Indicate to what extent the local structure is retained.

    The trustworthiness is within [0, 1]. It is defined as

    .. math::

        T(k) = 1 - \frac{2}{nk (2n - 3k - 1)} \sum^n_{i=1}
            \sum_{j \in \mathcal{N}_{i}^{k}} \max(0, (r(i, j) - k))

    where for each sample i, :math:`\mathcal{N}_{i}^{k}` are its k nearest
    neighbors in the output space, and every sample j is its :math:`r(i, j)`-th
    nearest neighbor in the input space. In other words, any unexpected nearest
    neighbors in the output space are penalised in proportion to their rank in
    the input space.

    X : {array-like, sparse matrix} of shape (n_samples, n_features) or \
        (n_samples, n_samples)
        If the metric is 'precomputed' X must be a square distance
        matrix. Otherwise it contains a sample per row.

    X_embedded : {array-like, sparse matrix} of shape (n_samples, n_components)
        Embedding of the training data in low-dimensional space.

        The number of neighbors that will be considered. Should be fewer than
        `n_samples / 2` to ensure the trustworthiness to lies within [0, 1], as
        mentioned in [1]_. An error will be raised otherwise.

        Which metric to use for computing pairwise distances between samples
        from the original input space. If metric is 'precomputed', X must be a
        matrix of pairwise distances or squared distances. Otherwise, for a list
        of available metrics, see the documentation of argument metric in
        `sklearn.pairwise.pairwise_distances` and metrics listed in
        `sklearn.metrics.pairwise.PAIRWISE_DISTANCE_FUNCTIONS`. Note that the
        "cosine" metric uses :func:`~sklearn.metrics.pairwise.cosine_distances`.

    Returns
    -------
    trustworthiness : float
        Trustworthiness of the low-dimensional embedding.

    .. [1] Jarkko Venna and Samuel Kaski. 2001. Neighborhood
           Preservation in Nonlinear Projection Methods: An Experimental Study.
           In Proceedings of the International Conference on Artificial Neural Networks
           (ICANN '01). Springer-Verlag, Berlin, Heidelberg, 485-491.

    .. [2] Laurens van der Maaten. Learning a Parametric Embedding by Preserving
           Local Structure. Proceedings of the Twelfth International Conference on
           Artificial Intelligence and Statistics, PMLR 5:384-391, 2009.

    Examples
    --------
    >>> from sklearn.manifold import trustworthiness
    >>> X, _ = make_blobs(n_samples=100, n_features=10, centers=3, random_state=42)
    >>> X_embedded = PCA(n_components=2).fit_transform(X)
    >>> print(f"{trustworthiness(X, X_embedded, n_neighbors=5):.2f}")
    0.92
    """
    n_samples = _num_samples(X)
    if n_neighbors >= n_samples / 2:
            f"n_neighbors ({n_neighbors}) should be less than n_samples / 2"
            f" ({n_samples / 2})"
        )
    dist_X = pairwise_distances(X, metric=metric)
        dist_X = dist_X.copy()
    # we set the diagonal to np.inf to exclude the points themselves from
    # their own neighborhood
    np.fill_diagonal(dist_X, np.inf)
    ind_X = np.argsort(dist_X, axis=1)
    # `ind_X[i]` is the index of sorted distances between i and other samples
    ind_X_embedded = (
        NearestNeighbors(n_neighbors=n_neighbors)
        .fit(X_embedded)
        .kneighbors(return_distance=False)
    )

    # We build an inverted index of neighbors in the input space: For sample i,
    # we define `inverted_index[i]` as the inverted index of sorted distances:
    # inverted_index[i][ind_X[i]] = np.arange(1, n_sample + 1)
    inverted_index = np.zeros((n_samples, n_samples), dtype=int)
    ordered_indices = np.arange(n_samples + 1)
    inverted_index[ordered_indices[:-1, np.newaxis], ind_X] = ordered_indices[1:]
    ranks = (
        inverted_index[ordered_indices[:-1, np.newaxis], ind_X_embedded] - n_neighbors
    )
    t = np.sum(ranks[ranks > 0])
    t = 1.0 - t * (
        2.0 / (n_samples * n_neighbors * (2.0 * n_samples - 3.0 * n_neighbors - 1.0))
    )
    return t

class TSNE(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """T-distributed Stochastic Neighbor Embedding.

    t-SNE [1] is a tool to visualize high-dimensional data. It converts
    similarities between data points to joint probabilities and tries
    to minimize the Kullback-Leibler divergence between the joint
    probabilities of the low-dimensional embedding and the
    high-dimensional data. t-SNE has a cost function that is not convex,
    i.e. with different initializations we can get different results.

    It is highly recommended to use another dimensionality reduction
    method (e.g. PCA for dense data or TruncatedSVD for sparse data)
    to reduce the number of dimensions to a reasonable amount (e.g. 50)
    if the number of features is very high. This will suppress some
    noise and speed up the computation of pairwise distances between
    samples. For more tips see Laurens van der Maaten's FAQ [2].

    Read more in the :ref:`User Guide <t_sne>`.

    perplexity : float, default=30.0
        The perplexity is related to the number of nearest neighbors that
        is used in other manifold learning algorithms. Larger datasets
        usually require a larger perplexity. Consider selecting a value
        between 5 and 50. Different values can result in significantly
        different results. The perplexity must be less than the number
        of samples.

    early_exaggeration : float, default=12.0
        Controls how tight natural clusters in the original space are in
        the embedded space and how much space will be between them. For
        larger values, the space between natural clusters will be larger
        in the embedded space. Again, the choice of this parameter is not
        very critical. If the cost function increases during initial
        optimization, the early exaggeration factor or the learning rate
        might be too high.

    learning_rate : float or "auto", default="auto"
        cloud with few outliers. If the cost function gets stuck in a bad local
        minimum increasing the learning rate may help.
        Note that many other t-SNE implementations (bhtsne, FIt-SNE, openTSNE,
        etc.) use a definition of learning_rate that is 4 times smaller than
        ours. So our learning_rate=200 corresponds to learning_rate=800 in
        those other implementations. The 'auto' option sets the learning_rate
        to `max(N / early_exaggeration / 4, 50)` where N is the sample size,
        following [4] and [5].

           The default value changed to `"auto"`.

        Maximum number of iterations for the optimization. Should be at
        least 250.

            Parameter name changed from `n_iter` to `max_iter`.

        optimization, used after 250 initial iterations with early
        exaggeration. Note that progress is only checked every 50 iterations so
        this value is rounded to the next multiple of 50.

           parameter *n_iter_without_progress* to control stopping criteria.

        be stopped.

        a metric listed in pairwise.PAIRWISE_DISTANCE_FUNCTIONS.
        the distance between them. The default is "euclidean" which is
        interpreted as squared euclidean distance.

    init : {"random", "pca"} or ndarray of shape (n_samples, n_components), \
            default="pca"
        Initialization of embedding.
        PCA initialization cannot be used with precomputed distances and is
        usually more globally stable than random initialization.

           The default value changed to `"pca"`.

        Determines the random number generator. Pass an int for reproducible
        results across multiple function calls. Note that different
        initializations might result in different local minima of the cost
        function. See :term:`Glossary <random_state>`.

    method : {'barnes_hut', 'exact'}, default='barnes_hut'
        By default the gradient calculation algorithm uses Barnes-Hut
        approximation running in O(NlogN) time. method='exact'
        will run on the slower, but exact, algorithm in O(N^2) time. The
        exact algorithm should be used when nearest-neighbor errors need
        to be better than 3%. However, the exact method cannot scale to
        millions of examples.

           Approximate optimization *method* via the Barnes-Hut.

        Only used if method='barnes_hut'

        The number of parallel jobs to run for neighbors search. This parameter
        has no impact when ``metric="precomputed"`` or
        (``metric="euclidean"`` and ``method="exact"``).

    embedding_ : array-like of shape (n_samples, n_components)
        Stores the embedding vectors.

    kl_divergence_ : float
        Kullback-Leibler divergence after optimization.

    learning_rate_ : float
        Effective learning rate.

    See Also
    --------
    sklearn.decomposition.PCA : Principal component analysis that is a linear
        dimensionality reduction method.
    sklearn.decomposition.KernelPCA : Non-linear dimensionality reduction using
        kernels and PCA.
    MDS : Manifold learning using multidimensional scaling.
    Isomap : Manifold learning based on Isometric Mapping.
    LocallyLinearEmbedding : Manifold learning using Locally Linear Embedding.
    SpectralEmbedding : Spectral embedding for non-linear dimensionality.

    Notes
    -----
    For an example of using :class:`~sklearn.manifold.TSNE` in combination with
    :class:`~sklearn.neighbors.KNeighborsTransformer` see

    [1] van der Maaten, L.J.P.; Hinton, G.E. Visualizing High-Dimensional Data
        Using t-SNE. Journal of Machine Learning Research 9:2579-2605, 2008.

    [2] van der Maaten, L.J.P. t-Distributed Stochastic Neighbor Embedding
        https://lvdmaaten.github.io/tsne/

    [3] L.J.P. van der Maaten. Accelerating t-SNE using Tree-Based Algorithms.
        Journal of Machine Learning Research 15(Oct):3221-3245, 2014.
        https://lvdmaaten.github.io/publications/papers/JMLR_2014.pdf

    [4] Belkina, A. C., Ciccolella, C. O., Anno, R., Halpert, R., Spidlen, J.,
        & Snyder-Cappione, J. E. (2019). Automated optimized parameters for
        T-distributed stochastic neighbor embedding improve visualization
        and analysis of large datasets. Nature Communications, 10(1), 1-12.

    [5] Kobak, D., & Berens, P. (2019). The art of using t-SNE for single-cell
        transcriptomics. Nature Communications, 10(1), 1-14.

    Examples
    --------
    >>> from sklearn.manifold import TSNE
    >>> X_embedded = TSNE(n_components=2, learning_rate='auto',
    ...                   init='random', perplexity=3).fit_transform(X)
    >>> X_embedded.shape
    (4, 2)
    """

        "perplexity": [Interval(Real, 0, None, closed="neither")],
        "early_exaggeration": [Interval(Real, 1, None, closed="left")],
        "learning_rate": [
        ],
        "max_iter": [Interval(Integral, 250, None, closed="left")],
        "n_iter_without_progress": [Interval(Integral, -1, None, closed="left")],
        "min_grad_norm": [Interval(Real, 0, None, closed="left")],
        "metric": [StrOptions(set(_VALID_METRICS) | {"precomputed"}), callable],
        "init": [
            StrOptions({"pca", "random"}),
        ],
        "method": [StrOptions({"barnes_hut", "exact"})],
        "angle": [Interval(Real, 0, 1, closed="both")],
    }

    # Control the number of exploration iterations with early_exaggeration on
    _EXPLORATION_MAX_ITER = 250

    # Control the number of iterations between progress checks
    _N_ITER_CHECK = 50

        self,
        *,
        perplexity=30.0,
        early_exaggeration=12.0,
        learning_rate="auto",
        n_iter_without_progress=300,
        min_grad_norm=1e-7,
        init="pca",
        method="barnes_hut",
        angle=0.5,
    ):
        self.perplexity = perplexity
        self.early_exaggeration = early_exaggeration
        self.n_iter_without_progress = n_iter_without_progress
        self.min_grad_norm = min_grad_norm
        self.angle = angle

    def _check_params_vs_input(self, X):
        if self.perplexity >= X.shape[0]:
                f"perplexity ({self.perplexity}) must be less "
                f"than n_samples ({X.shape[0]})"
            )

    def _fit(self, X, skip_num_points=0):
        """Private function to fit the model using X as training data."""

        if isinstance(self.init, str) and self.init == "pca" and issparse(X):
                "PCA initialization is currently not supported "
                "with the sparse input matrix. Use "
                'init="random" instead.'
            )

        if self.learning_rate == "auto":
            # See issue #18018
            self.learning_rate_ = X.shape[0] / self.early_exaggeration / 4
            self.learning_rate_ = np.maximum(self.learning_rate_, 50)
        else:
            self.learning_rate_ = self.learning_rate

        if self.method == "barnes_hut":
                self,
                X,
                accept_sparse=["csr"],
                ensure_min_samples=2,
                dtype=[np.float32, np.float64],
            )
        else:
                self,
                X,
            )
            if isinstance(self.init, str) and self.init == "pca":
                    'The parameter init="pca" cannot be used with metric="precomputed".'
                )
                raise ValueError("X should be a square distance matrix")

            check_non_negative(
                X,
                (
                    "TSNE.fit(). With metric='precomputed', X "
                    "should contain positive distances."
                ),
            )

            if self.method == "exact" and issparse(X):
                    'TSNE with method="exact" does not accept sparse '
                    'precomputed distance matrix. Use method="barnes_hut" '
                    "or provide the dense distance matrix."
                )

        if self.method == "barnes_hut" and self.n_components > 3:
                "'n_components' should be inferior to 4 for the "
                "barnes_hut algorithm as it relies on "
                "quad-tree or oct-tree."
            )

        neighbors_nn = None
        if self.method == "exact":
            # Retrieve the distance matrix, either using the precomputed one or
            # computing it.
                distances = X
            else:
                    print("[t-SNE] Computing pairwise distances...")

                if self.metric == "euclidean":
                    # Euclidean is squared here, rather than using **= 2,
                    # because euclidean_distances already calculates
                    # squared distances, and returns np.sqrt(dist) for
                    # squared=False.
                    # Also, Euclidean is slower for n_jobs>1, so don't set here
                    distances = pairwise_distances(X, metric=self.metric, squared=True)
                else:
                    metric_params_ = self.metric_params or {}
                    distances = pairwise_distances(
                        X, metric=self.metric, n_jobs=self.n_jobs, **metric_params_
                    )

            if np.any(distances < 0):
                    "All distances should be positive, the metric given is not correct"
                )

            if self.metric != "euclidean":
                distances **= 2

            # compute the joint probability distribution for the input space
            P = _joint_probabilities(distances, self.perplexity, self.verbose)
            assert np.all(np.isfinite(P)), "All probabilities should be finite"
            assert np.all(P >= 0), "All probabilities should be non-negative"
            assert np.all(P <= 1), (
                "All probabilities should be less or then equal to one"
            )

        else:
            # Compute the number of nearest neighbors to find.
            # LvdM uses 3 * perplexity as the number of neighbors.
            # In the event that we have very small # of points
            # set the neighbors to n - 1.
            n_neighbors = min(n_samples - 1, int(3.0 * self.perplexity + 1))

                print("[t-SNE] Computing {} nearest neighbors...".format(n_neighbors))

            # Find the nearest neighbors for every point
            knn = NearestNeighbors(
                metric=self.metric,
                metric_params=self.metric_params,
            )
            t0 = time()
            knn.fit(X)
            duration = time() - t0
                print(
                    "[t-SNE] Indexed {} samples in {:.3f}s...".format(
                        n_samples, duration
                    )
                )

            t0 = time()
            distances_nn = knn.kneighbors_graph(mode="distance")
                print(
                    "[t-SNE] Computed neighbors for {} samples in {:.3f}s...".format(
                    )
                )

            # Free the memory used by the ball_tree
            del knn

            # knn return the euclidean distance but we need it squared
            # to be consistent with the 'exact' method. Note that the
            # the method was derived using the euclidean method as in the
            # input space. Not sure of the implication of using a different
            # metric.
            distances_nn.data **= 2

            P = _joint_probabilities_nn(distances_nn, self.perplexity, self.verbose)

        if isinstance(self.init, np.ndarray):
            X_embedded = self.init
        elif self.init == "pca":
            pca = PCA(
                n_components=self.n_components,
                svd_solver="randomized",
            )
            # Always output a numpy array, no matter what is configured globally
            pca.set_output(transform="default")
            X_embedded = pca.fit_transform(X).astype(np.float32, copy=False)
            # PCA is rescaled so that PC1 has standard deviation 1e-4 which is
            # the default value for random initialization. See issue #18018.
            X_embedded = X_embedded / np.std(X_embedded[:, 0]) * 1e-4
        elif self.init == "random":
            # The embedding is initialized with iid samples from Gaussians with
            # standard deviation 1e-4.
            X_embedded = 1e-4 * random_state.standard_normal(
                size=(n_samples, self.n_components)
            ).astype(np.float32)

        # Degrees of freedom of the Student's t-distribution. The suggestion
        # degrees_of_freedom = n_components - 1 comes from
        # "Learning a Parametric Embedding by Preserving Local Structure"
        # Laurens van der Maaten, 2009.
        degrees_of_freedom = max(self.n_components - 1, 1)

        return self._tsne(
            P,
            degrees_of_freedom,
            X_embedded=X_embedded,
            neighbors=neighbors_nn,
            skip_num_points=skip_num_points,
        )

    def _tsne(
        self,
        P,
        degrees_of_freedom,
        neighbors=None,
        skip_num_points=0,
    ):
        """Runs t-SNE."""
        # t-SNE minimizes the Kullback-Leiber divergence of the Gaussians P
        # and the Student's t-distributions Q. The optimization algorithm that
        # we use is batch gradient descent with two stages:
        # * initial optimization with early exaggeration and momentum at 0.5
        # * final optimization with momentum at 0.8
        params = X_embedded.ravel()

        opt_args = {
            "it": 0,
            "n_iter_check": self._N_ITER_CHECK,
            "min_grad_norm": self.min_grad_norm,
            "learning_rate": self.learning_rate_,
            "verbose": self.verbose,
            "kwargs": dict(skip_num_points=skip_num_points),
            "args": [P, degrees_of_freedom, n_samples, self.n_components],
            "n_iter_without_progress": self._EXPLORATION_MAX_ITER,
            "max_iter": self._EXPLORATION_MAX_ITER,
            "momentum": 0.5,
        }
            obj_func = _kl_divergence_bh
            opt_args["kwargs"]["angle"] = self.angle
            # Repeat verbose argument for _kl_divergence_bh
            opt_args["kwargs"]["verbose"] = self.verbose
            # Get the number of threads for gradient computation here to
            # avoid recomputing it at each iteration.
            opt_args["kwargs"]["num_threads"] = _openmp_effective_n_threads()
        else:
            obj_func = _kl_divergence

        # Learning schedule (part 1): do 250 iteration with lower momentum but
        # higher learning rate controlled via the early exaggeration parameter
        P *= self.early_exaggeration
        params, kl_divergence, it = _gradient_descent(obj_func, params, **opt_args)
            print(
                "[t-SNE] KL divergence after %d iterations with early exaggeration: %f"
                % (it + 1, kl_divergence)
            )

        # Learning schedule (part 2): disable early exaggeration and finish
        # optimization with a higher momentum at 0.8
        P /= self.early_exaggeration
        remaining = self.max_iter - self._EXPLORATION_MAX_ITER
        if it < self._EXPLORATION_MAX_ITER or remaining > 0:
            opt_args["max_iter"] = self.max_iter
            opt_args["it"] = it + 1
            opt_args["momentum"] = 0.8
            opt_args["n_iter_without_progress"] = self.n_iter_without_progress
            params, kl_divergence, it = _gradient_descent(obj_func, params, **opt_args)

        # Save the final number of iterations
        self.n_iter_ = it

            print(
                "[t-SNE] KL divergence after %d iterations: %f"
            )

        X_embedded = params.reshape(n_samples, self.n_components)
        self.kl_divergence_ = kl_divergence

        return X_embedded

        # TSNE.metric is not validated yet
    )
        """Fit X into an embedded space and return that transformed output.

            (n_samples, n_samples)
            If the metric is 'precomputed' X must be a square distance
            matrix. Otherwise it contains a sample per row. If the method
            is 'exact', X may be a sparse matrix of type 'csr', 'csc'
            or 'coo'. If the method is 'barnes_hut' and the metric is
            'precomputed', X may be a precomputed sparse graph.

        y : None
            Ignored.

        Returns
        -------
            Embedding of the training data in low-dimensional space.
        """
        self._check_params_vs_input(X)
        embedding = self._fit(X)
        self.embedding_ = embedding

    )
        """Fit X into an embedded space.

        y : None
            Ignored.

        Returns
        -------
        """

    @property
        return self.embedding_.shape[1]

        self,
        *,
    ):

            )

            )

        else:

                self,
                X,
            )
        else:
                self,
                X,
            )
                )

                X,
                (
                ),
            )

                )

            )

            else:

                else:
                    )

                )

            )

        else:

            )
            t0 = time()
                print(
                    )
                )

            t0 = time()
                print(
                    )
                )

            del knn

            # metric.

            pca = PCA(
            )

            P,
        )

    def _tsne(
        self,
        P,
    ):

            "it": 0,
        }
        else:

            print(
            )

            print(
            )

        y : None
            Ignored.

        Returns
        -------
        """

        y : None
            Ignored.

        Returns
        -------
        """

def barycenter_weights(X, Y, indices, reg=1e-3):
    """Compute barycenter weights of X from Y along the first axis

    We estimate the weights to assign to each point in Y[indices] to recover
    the point X[i]. The barycenter weights sum to 1.

    X : array-like, shape (n_samples, n_dim)

    Y : array-like, shape (n_samples, n_dim)

    indices : array-like, shape (n_samples, n_dim)
            Indices of the points in Y used to compute the barycenter

    reg : float, default=1e-3
        Amount of regularization to add for the problem to be
        well-posed in the case of n_neighbors > n_dim

    Returns
    -------
    B : array-like, shape (n_samples, n_neighbors)

    Notes
    -----
    See developers note for more information.
    """
    X = check_array(X, dtype=FLOAT_DTYPES)
    Y = check_array(Y, dtype=FLOAT_DTYPES)
    indices = check_array(indices, dtype=int)

    n_samples, n_neighbors = indices.shape
    assert X.shape[0] == n_samples

    B = np.empty((n_samples, n_neighbors), dtype=X.dtype)
    v = np.ones(n_neighbors, dtype=X.dtype)

    # this might raise a LinalgError if G is singular and has trace
    # zero
    for i, ind in enumerate(indices):
        A = Y[ind]
        C = A - X[i]  # broadcasting
        G = np.dot(C, C.T)
        trace = np.trace(G)
        if trace > 0:
            R = reg * trace
        else:
            R = reg
        G.flat[:: n_neighbors + 1] += R
        w = solve(G, v, assume_a="pos")
        B[i, :] = w / np.sum(w)
    return B

def barycenter_kneighbors_graph(X, n_neighbors, reg=1e-3, n_jobs=None):
    """Computes the barycenter weighted graph of k-Neighbors for points in X

    X : {array-like, NearestNeighbors}
        Sample data, shape = (n_samples, n_features), in the form of a
        numpy array or a NearestNeighbors object.

        Amount of regularization when solving the least-squares
        problem. Only relevant if mode='barycenter'. If None, use the
        default.

    Returns
    -------
    A : sparse matrix in CSR format, shape = [n_samples, n_samples]
        A[i, j] is assigned the weight of edge that connects i to j.

    See Also
    --------
    sklearn.neighbors.kneighbors_graph
    sklearn.neighbors.radius_neighbors_graph
    """
    knn = NearestNeighbors(n_neighbors=n_neighbors + 1, n_jobs=n_jobs).fit(X)
    X = knn._fit_X
    n_samples = knn.n_samples_fit_
    ind = knn.kneighbors(X, return_distance=False)[:, 1:]
    data = barycenter_weights(X, X, ind, reg=reg)
    indptr = np.arange(0, n_samples * n_neighbors + 1, n_neighbors)
    return csr_matrix((data.ravel(), ind.ravel(), indptr), shape=(n_samples, n_samples))

def null_space(
    M, k, k_skip=1, eigen_solver="arpack", tol=1e-6, max_iter=100, random_state=None
):
    """
    Find the null space of a matrix M.

    M : {array, matrix, sparse matrix, LinearOperator}
        Input covariance matrix: should be symmetric positive semi-definite

    k : int
        Number of eigenvalues/vectors to return

    k_skip : int, default=1
        Number of low eigenvalues to skip.

    eigen_solver : {'auto', 'arpack', 'dense'}, default='arpack'
        auto : algorithm will attempt to choose the best method for input data
        arpack : use arnoldi iteration in shift-invert mode.
                    For this method, M may be a dense matrix, sparse matrix,
                    or general linear operator.
                    Warning: ARPACK can be unstable for some problems.  It is
                    best to try several random seeds in order to check results.
        dense  : use standard dense matrix operations for the eigenvalue
                    decomposition.  For this method, M must be an array
                    or matrix type.  This method should be avoided for
                    large problems.

    tol : float, default=1e-6
        Tolerance for 'arpack' method.
        Not used if eigen_solver=='dense'.

        Maximum number of iterations for 'arpack' method.
        Not used if eigen_solver=='dense'

        Determines the random number generator when ``solver`` == 'arpack'.
    """
    if eigen_solver == "auto":
        if M.shape[0] > 200 and k + k_skip < 10:
            eigen_solver = "arpack"
        else:
            eigen_solver = "dense"

    if eigen_solver == "arpack":
        v0 = _init_arpack_v0(M.shape[0], random_state)
        try:
            eigen_values, eigen_vectors = eigsh(
                M, k + k_skip, sigma=0.0, tol=tol, maxiter=max_iter, v0=v0
            )
                "Error in determining null-space with ARPACK. Error message: "
                "'%s'. Note that eigen_solver='arpack' can fail when the "
                "weight matrix is singular or otherwise ill-behaved. In that "
                "case, eigen_solver='dense' is recommended. See online "
                "documentation for more information." % e
            ) from e

        return eigen_vectors[:, k_skip:], np.sum(eigen_values[k_skip:])
    elif eigen_solver == "dense":
        if hasattr(M, "toarray"):
            M = M.toarray()
        eigen_values, eigen_vectors = eigh(
            M, subset_by_index=(k_skip, k + k_skip - 1), overwrite_a=True
        )
        index = np.argsort(np.abs(eigen_values))
        return eigen_vectors[:, index], np.sum(eigen_values)
    else:
        raise ValueError("Unrecognized eigen_solver '%s'" % eigen_solver)

def _locally_linear_embedding(
    X,
    *,
    reg=1e-3,
    eigen_solver="auto",
    tol=1e-6,
    method="standard",
    hessian_tol=1e-4,
    modified_tol=1e-12,
):
    nbrs = NearestNeighbors(n_neighbors=n_neighbors + 1, n_jobs=n_jobs)
    nbrs.fit(X)
    X = nbrs._fit_X

    N, d_in = X.shape

    if n_components > d_in:
            "output dimension must be less than or equal to input dimension"
        )
    if n_neighbors >= N:
            "Expected n_neighbors <= n_samples,  but n_samples = %d, n_neighbors = %d"
            % (N, n_neighbors)
        )

    M_sparse = eigen_solver != "dense"
    M_container_constructor = lil_matrix if M_sparse else np.zeros

    if method == "standard":
        W = barycenter_kneighbors_graph(
            nbrs, n_neighbors=n_neighbors, reg=reg, n_jobs=n_jobs
        )

        # we'll compute M = (I-W)'(I-W)
        # depending on the solver, we'll do this differently
        if M_sparse:
            M = eye(*W.shape, format=W.format) - W
            M = M.T @ M
        else:
            M = (W.T @ W - W.T - W).toarray()
            M.flat[:: M.shape[0] + 1] += 1  # M = W' W - W' - W + I

    elif method == "hessian":
        dp = n_components * (n_components + 1) // 2

        if n_neighbors <= n_components + dp:
                "for method='hessian', n_neighbors must be "
                "greater than "
                "[n_components * (n_components + 3) / 2]"
            )

        neighbors = nbrs.kneighbors(
            X, n_neighbors=n_neighbors + 1, return_distance=False
        )
        neighbors = neighbors[:, 1:]

        Yi = np.empty((n_neighbors, 1 + n_components + dp), dtype=np.float64)
        Yi[:, 0] = 1

        M = M_container_constructor((N, N), dtype=np.float64)

        use_svd = n_neighbors > d_in

        for i in range(N):
            Gi = X[neighbors[i]]
            Gi -= Gi.mean(0)

            # build Hessian estimator
            if use_svd:
                U = svd(Gi, full_matrices=0)[0]
            else:
                Ci = np.dot(Gi, Gi.T)
                U = eigh(Ci)[1][:, ::-1]

            Yi[:, 1 : 1 + n_components] = U[:, :n_components]

            j = 1 + n_components
            for k in range(n_components):
                Yi[:, j : j + n_components - k] = U[:, k : k + 1] * U[:, k:n_components]
                j += n_components - k

            Q, R = qr(Yi)

            w = Q[:, n_components + 1 :]
            S = w.sum(0)

            S[np.where(abs(S) < hessian_tol)] = 1
            w /= S

            nbrs_x, nbrs_y = np.meshgrid(neighbors[i], neighbors[i])
            M[nbrs_x, nbrs_y] += np.dot(w, w.T)

    elif method == "modified":
        if n_neighbors < n_components:
            raise ValueError("modified LLE requires n_neighbors >= n_components")

        )

        # find the eigenvectors and eigenvalues of each local covariance
        # matrix. We want V[i] to be a [n_neighbors x n_neighbors] matrix,
        # where the columns are eigenvectors
        V = np.zeros((N, n_neighbors, n_neighbors))
        nev = min(d_in, n_neighbors)
        evals = np.zeros([N, nev])

        # choose the most efficient way to find the eigenvectors

        if use_svd:
            for i in range(N):
                X_nbrs = X[neighbors[i]] - X[i]
                V[i], evals[i], _ = svd(X_nbrs, full_matrices=True)
            evals **= 2
        else:
                C_nbrs = np.dot(X_nbrs, X_nbrs.T)
                evi, vi = eigh(C_nbrs)
                evals[i] = evi[::-1]
                V[i] = vi[:, ::-1]

        # find regularized weights: this is like normal LLE.
        # because we've already computed the SVD of each covariance matrix,
        # it's faster to use this rather than np.linalg.solve
        reg = 1e-3 * evals.sum(1)

        tmp = np.dot(V.transpose(0, 2, 1), np.ones(n_neighbors))
        tmp[:, :nev] /= evals + reg[:, None]
        tmp[:, nev:] /= reg[:, None]

        w_reg = np.zeros((N, n_neighbors))
            w_reg[i] = np.dot(V[i], tmp[i])
        w_reg /= w_reg.sum(1)[:, None]

        # calculate eta: the median of the ratio of small to large eigenvalues
        # across the points.  This is used to determine s_i, below
        rho = evals[:, n_components:].sum(1) / evals[:, :n_components].sum(1)
        eta = np.median(rho)

        # find s_i, the size of the "almost null space" for each point:
        # this is the size of the largest set of eigenvalues
        # such that Sum[v; v in set]/Sum[v; v not in set] < eta
        s_range = np.zeros(N, dtype=int)
        evals_cumsum = stable_cumsum(evals, 1)
        eta_range = evals_cumsum[:, -1:] / evals_cumsum[:, :-1] - 1
            s_range[i] = np.searchsorted(eta_range[i, ::-1], eta)
        s_range += n_neighbors - nev  # number of zero eigenvalues

        # Now calculate M.
        # This is the [N x N] matrix whose null space is the desired embedding

            s_i = s_range[i]

            # select bottom s_i eigenvectors and calculate alpha
            Vi = V[i, :, n_neighbors - s_i :]
            alpha_i = np.linalg.norm(Vi.sum(0)) / np.sqrt(s_i)

            # compute Householder matrix which satisfies
            #  Hi*Vi.T*ones(n_neighbors) = alpha_i*ones(s)
            # using prescription from paper
            h = np.full(s_i, alpha_i) - np.dot(Vi.T, np.ones(n_neighbors))

            norm_h = np.linalg.norm(h)
            if norm_h < modified_tol:
                h *= 0
            else:
                h /= norm_h

            # Householder matrix is
            #  >> Hi = np.identity(s_i) - 2*np.outer(h,h)
            # Then the weight matrix is
            #  >> Wi = np.dot(Vi,Hi) + (1-alpha_i) * w_reg[i,:,None]
            # We do this much more efficiently:
            Wi = Vi - 2 * np.outer(np.dot(Vi, h), h) + (1 - alpha_i) * w_reg[i, :, None]

            # Update M as follows:
            # >> W_hat = np.zeros( (N,s_i) )
            # >> W_hat[neighbors[i],:] = Wi
            # >> W_hat[i] -= 1
            # >> M += np.dot(W_hat,W_hat.T)
            # We can do this much more efficiently:
            M[nbrs_x, nbrs_y] += np.dot(Wi, Wi.T)
            Wi_sum1 = Wi.sum(1)
            M[i, neighbors[i]] -= Wi_sum1
            M[neighbors[i], [i]] -= Wi_sum1
            M[i, i] += s_i

    elif method == "ltsa":
        )

            Xi = X[neighbors[i]]
            Xi -= Xi.mean(0)

            # compute n_components largest eigenvalues of Xi @ Xi^T
                v = svd(Xi, full_matrices=True)[0]
            else:
                Ci = np.dot(Xi, Xi.T)
                v = eigh(Ci)[1][:, ::-1]

            Gi = np.zeros((n_neighbors, n_components + 1))
            Gi[:, 1:] = v[:, :n_components]
            Gi[:, 0] = 1.0 / np.sqrt(n_neighbors)

            GiGiT = np.dot(Gi, Gi.T)

            M[nbrs_x, nbrs_y] -= GiGiT

            M[neighbors[i], neighbors[i]] += np.ones(shape=n_neighbors)

    if M_sparse:
        M = M.tocsr()

    return null_space(
        M,
        k_skip=1,
        tol=tol,
    )

def locally_linear_embedding(
    X,
    *,
    reg=1e-3,
    tol=1e-6,
):
    """Perform a Locally Linear Embedding analysis on the data.

    Read more in the :ref:`User Guide <locally_linear_embedding>`.

        Number of neighbors to consider for each point.

        Number of coordinates for the manifold.

        Regularization constant, multiplies the trace of the local covariance
        matrix of the distances.

    eigen_solver : {'auto', 'arpack', 'dense'}, default='auto'

        Tolerance for 'arpack' method

        Maximum number of iterations for the arpack solver.

    method : {'standard', 'hessian', 'modified', 'ltsa'}, default='standard'
        standard : use the standard locally linear embedding algorithm.
                   see reference [1]_
        hessian  : use the Hessian eigenmap method.  This method requires
                   n_neighbors > n_components * (1 + (n_components + 1) / 2.
                   see reference [2]_
        modified : use the modified locally linear embedding algorithm.
                   see reference [3]_
        ltsa     : use local tangent space alignment algorithm
                   see reference [4]_

    hessian_tol : float, default=1e-4
        Tolerance for Hessian eigenmapping method.
        Only used if method == 'hessian'.

    modified_tol : float, default=1e-12
        Tolerance for modified LLE method.
        Only used if method == 'modified'.

    Returns
    -------
    Y : ndarray of shape (n_samples, n_components)
        Embedding vectors.

    squared_error : float
        Reconstruction error for the embedding vectors. Equivalent to
        ``norm(Y - W Y, 'fro')**2``, where W are the reconstruction weights.

    .. [1] Roweis, S. & Saul, L. Nonlinear dimensionality reduction
        by locally linear embedding.  Science 290:2323 (2000).
    .. [2] Donoho, D. & Grimes, C. Hessian eigenmaps: Locally
        linear embedding techniques for high-dimensional data.
        Proc Natl Acad Sci U S A.  100:5591 (2003).
    .. [3] `Zhang, Z. & Wang, J. MLLE: Modified Locally Linear
        Embedding Using Multiple Weights.
        <https://citeseerx.ist.psu.edu/doc_view/pid/0b060fdbd92cbcc66b383bcaa9ba5e5e624d7ee3>`_
    .. [4] Zhang, Z. & Zha, H. Principal manifolds and nonlinear
        dimensionality reduction via tangent space alignment.
        Journal of Shanghai Univ.  8:406 (2004)

    Examples
    --------
    >>> from sklearn.manifold import locally_linear_embedding
    (1797, 64)
    >>> embedding, _ = locally_linear_embedding(X[:100],n_neighbors=5, n_components=2)
    (100, 2)
    """
    return _locally_linear_embedding(
        X=X,
        n_neighbors=n_neighbors,
        reg=reg,
        tol=tol,
        hessian_tol=hessian_tol,
        modified_tol=modified_tol,
    )

class LocallyLinearEmbedding(
    _UnstableArchMixin,
):
    """Locally Linear Embedding.

        The solver used to compute the eigenvectors. The available options are:

        - `'auto'` : algorithm will attempt to choose the best method for input
          data.
        - `'arpack'` : use arnoldi iteration in shift-invert mode. For this
          method, M may be a dense matrix, sparse matrix, or general linear
          operator.
        - `'dense'`  : use standard dense matrix operations for the eigenvalue
          decomposition. For this method, M must be an array or matrix type.
          This method should be avoided for large problems.

           ARPACK can be unstable for some problems.  It is best to try several
           random seeds in order to check results.

        - `standard`: use the standard locally linear embedding algorithm. see
          reference [1]_
        - `hessian`: use the Hessian eigenmap method. This method requires
          ``n_neighbors > n_components * (1 + (n_components + 1) / 2``. see
          reference [2]_
        - `modified`: use the modified locally linear embedding algorithm.
          see reference [3]_
        - `ltsa`: use local tangent space alignment algorithm. see
          reference [4]_

        Only used if ``method == 'hessian'``.

        Only used if ``method == 'modified'``.

    neighbors_algorithm : {'auto', 'brute', 'kd_tree', 'ball_tree'}, \
                          default='auto'
        Algorithm to use for nearest neighbors search, passed to
        :class:`~sklearn.neighbors.NearestNeighbors` instance.

        Determines the random number generator when
        ``eigen_solver`` == 'arpack'. Pass an int for reproducible results
        across multiple function calls. See :term:`Glossary <random_state>`.

    embedding_ : array-like, shape [n_samples, n_components]
        Stores the embedding vectors

    reconstruction_error_ : float
        Reconstruction error associated with `embedding_`

    nbrs_ : NearestNeighbors object
        Stores nearest neighbors instance, including BallTree or KDtree
        if applicable.

    See Also
    --------
    SpectralEmbedding : Spectral embedding for non-linear dimensionality
        reduction.
    TSNE : Distributed Stochastic Neighbor Embedding.

    Examples
    --------
    >>> from sklearn.manifold import LocallyLinearEmbedding
    (1797, 64)
    >>> embedding = LocallyLinearEmbedding(n_components=2)
    (100, 2)
    """

        "reg": [Interval(Real, 0, None, closed="left")],
        "eigen_solver": [StrOptions({"auto", "arpack", "dense"})],
        "method": [StrOptions({"standard", "hessian", "modified", "ltsa"})],
        "hessian_tol": [Interval(Real, 0, None, closed="left")],
        "modified_tol": [Interval(Real, 0, None, closed="left")],
        "neighbors_algorithm": [StrOptions({"auto", "brute", "kd_tree", "ball_tree"})],
    }

        self,
        *,
        reg=1e-3,
        tol=1e-6,
        method="standard",
        hessian_tol=1e-4,
        modified_tol=1e-12,
        neighbors_algorithm="auto",
    ):
        self.reg = reg
        self.hessian_tol = hessian_tol
        self.modified_tol = modified_tol
        self.neighbors_algorithm = neighbors_algorithm

    def _fit_transform(self, X):
        self.nbrs_ = NearestNeighbors(
            n_neighbors=self.n_neighbors,
            algorithm=self.neighbors_algorithm,
        )

        X = validate_data(self, X, dtype=float)
        self.nbrs_.fit(X)
        self.embedding_, self.reconstruction_error_ = _locally_linear_embedding(
            X=self.nbrs_,
            hessian_tol=self.hessian_tol,
            modified_tol=self.modified_tol,
            reg=self.reg,
        )
        self._n_features_out = self.embedding_.shape[1]

        """Compute the embedding vectors for data X.

        y : Ignored

        Returns
        -------
            Fitted `LocallyLinearEmbedding` class instance.
        """
        self._fit_transform(X)

        """Compute the embedding vectors for data X and transform X.

        y : Ignored

        Returns
        -------
        """

        """
        Transform new points into embedding space.

        Returns
        -------

        Notes
        -----
        Because of scaling performed by this method, it is discouraged to use
        it together with methods that are not scale-invariant (like SVMs).
        """

        ind = self.nbrs_.kneighbors(
            X, n_neighbors=self.n_neighbors, return_distance=False
        )
        weights = barycenter_weights(X, self.nbrs_._fit_X, ind, reg=self.reg)
        X_new = np.empty((X.shape[0], self.n_components))
            X_new[i] = np.dot(self.embedding_[ind[i]].T, weights[i])
        return X_new

        self,
        *,
        reg=1e-3,
        tol=1e-6,
    ):

        )

        )

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """

        """

        Returns
        -------

        Notes
        -----
        """

        )

def _smacof_single(
    dissimilarities,
    metric=True,
    max_iter=300,
    eps=1e-6,
    normalized_stress=False,
):
    """Computes multidimensional scaling using SMACOF algorithm.

    dissimilarities : ndarray of shape (n_samples, n_samples)
        Pairwise dissimilarities between the points. Must be symmetric.

    metric : bool, default=True
        Compute metric or nonmetric SMACOF algorithm.
        When ``False`` (i.e. non-metric MDS), dissimilarities with 0 are considered as
        missing values.

        Number of dimensions in which to immerse the dissimilarities. If an
        ``init`` array is provided, this option is overridden and the shape of
        ``init`` is used to determine the dimensionality of the embedding
        space.

        Starting configuration of the embedding to initialize the algorithm. By
        default, the algorithm is initialized with a randomly chosen array.

    max_iter : int, default=300
        Maximum number of iterations of the SMACOF algorithm for a single run.

        Level of verbosity.

        The tolerance with respect to stress (normalized by the sum of squared
        embedding distances) at which to declare convergence.

           The default value for `eps` has changed from 1e-3 to 1e-6, as a result
           of a bugfix in the computation of the convergence criterion.

        Determines the random number generator used to initialize the centers.

    normalized_stress : bool, default=False
        Whether to return normalized stress value (Stress-1) instead of raw
        stress.

           Normalized stress is now supported for metric MDS as well.

    Returns
    -------
    X : ndarray of shape (n_samples, n_components)
        Coordinates of the points in a ``n_components``-space.

    stress : float
        The final value of the stress (sum of squared distance of the
        disparities and the distances for all constrained points).
        If `normalized_stress=True`, returns Stress-1.
        A value of 0 indicates "perfect" fit, 0.025 excellent, 0.05 good,
        0.1 fair, and 0.2 poor [1]_.

        The number of iterations corresponding to the best stress.

    .. [1] "Nonmetric multidimensional scaling: a numerical method" Kruskal, J.
           Psychometrika, 29 (1964)

    .. [2] "Multidimensional scaling by optimizing goodness of fit to a nonmetric
           hypothesis" Kruskal, J. Psychometrika, 29, (1964)

    .. [3] "Modern Multidimensional Scaling - Theory and Applications" Borg, I.;
           Groenen P. Springer Series in Statistics (1997)
    """
    dissimilarities = check_symmetric(dissimilarities, raise_exception=True)

    n_samples = dissimilarities.shape[0]

    dissimilarities_flat = ((1 - np.tri(n_samples)) * dissimilarities).ravel()
    dissimilarities_flat_w = dissimilarities_flat[dissimilarities_flat != 0]
        # Randomly choose initial configuration
        X = random_state.uniform(size=n_samples * n_components)
        X = X.reshape((n_samples, n_components))
    else:
        # overrides the parameter p
        n_components = init.shape[1]
        if n_samples != init.shape[0]:
                "init matrix should be of shape (%d, %d)" % (n_samples, n_components)
            )
        X = init

    # Out of bounds condition cannot happen because we are transforming
    # the training set here, but does sometimes get triggered in
    # practice due to machine precision issues. Hence "clip".
    ir = IsotonicRegression(out_of_bounds="clip")

    old_stress = None
    for it in range(max_iter):
        # Compute distance and monotonic regression
        if metric:
            disparities = dissimilarities
        else:
            distances_flat = distances.ravel()
            # dissimilarities with 0 are considered as missing values
            distances_flat_w = distances_flat[dissimilarities_flat != 0]

            # Compute the disparities using isotonic regression.
            # For the first SMACOF iteration, use scaled original dissimilarities.
            # (This choice follows the R implementation described in this paper:
            # https://www.jstatsoft.org/article/view/v102i10)
            if it < 1:
                disparities_flat = dissimilarities_flat_w
            else:
                disparities_flat = ir.fit_transform(
                    dissimilarities_flat_w, distances_flat_w
                )
            disparities = np.zeros_like(distances_flat)
            disparities[dissimilarities_flat != 0] = disparities_flat
            disparities = disparities.reshape((n_samples, n_samples))
            disparities *= np.sqrt(
                (n_samples * (n_samples - 1) / 2) / (disparities**2).sum()
            )
            disparities = disparities + disparities.T

        # Update X using the Guttman transform
        distances[distances == 0] = 1e-5
        ratio = disparities / distances
        B = -ratio
        B[np.arange(len(B)), np.arange(len(B))] += ratio.sum(axis=1)
        X = 1.0 / n_samples * np.dot(B, X)

        # Compute stress
        distances = euclidean_distances(X)
        stress = ((distances.ravel() - disparities.ravel()) ** 2).sum() / 2

        if verbose >= 2:  # pragma: no cover
            print(f"Iteration {it}, stress {stress:.4f}")
        if old_stress is not None:
            sum_squared_distances = (distances.ravel() ** 2).sum()
            if ((old_stress - stress) / (sum_squared_distances / 2)) < eps:
                if verbose:  # pragma: no cover
                    print("Convergence criterion reached.")
                break
        old_stress = stress

    if normalized_stress:
        sum_squared_distances = (distances.ravel() ** 2).sum()
        stress = np.sqrt(stress / (sum_squared_distances / 2))

    return X, stress, it + 1

def smacof(
    *,
    n_init="warn",
    eps=1e-6,
    normalized_stress="auto",
):
    """Compute multidimensional scaling using the SMACOF algorithm.

    The SMACOF (Scaling by MAjorizing a COmplicated Function) algorithm is a
    multidimensional scaling algorithm which minimizes an objective function
    (the *stress*) using a majorization technique. Stress majorization, also
    known as the Guttman Transform, guarantees a monotone convergence of
    stress, and is more powerful than traditional techniques such as gradient
    descent.

    The SMACOF algorithm for metric MDS can be summarized by the following
    steps:

    1. Set an initial start configuration, randomly or not.
    2. Compute the stress
    3. Compute the Guttman Transform
    4. Iterate 2 and 3 until convergence.

    The nonmetric algorithm adds a monotonic regression step before computing
    the stress.

    dissimilarities : array-like of shape (n_samples, n_samples)

        space.

    init : array-like of shape (n_samples, n_components), default=None

    n_init : int, default=8
        Number of times the SMACOF algorithm will be run with different
        initializations. The final results will be the best output of the runs,
        determined by the run with the smallest final stress. If ``init`` is
        provided, this option is overridden and a single run is performed.

        .. versionchanged:: 1.9
           The default value for `n_iter` will change from 8 to 1 in version 1.9.

        The number of jobs to use for the computation. If multiple
        initializations are used (``n_init``), each run of the algorithm is
        computed in parallel.

    normalized_stress : bool or "auto", default="auto"
        stress. By default, metric MDS returns raw stress while non-metric MDS
        returns normalized stress.

           The default value changed from `False` to `"auto"` in version 1.4.

    Returns
    -------

        The number of iterations corresponding to the best stress. Returned
        only if ``return_n_iter`` is set to ``True``.

    Examples
    --------
    >>> from sklearn.manifold import smacof
    >>> from sklearn.metrics import euclidean_distances
    >>> X = np.array([[0, 1, 2], [1, 0, 3], [2, 3, 0]])
    >>> dissimilarities = euclidean_distances(X)
    >>> Z, stress = smacof(
    ...     dissimilarities, n_components=2, n_init=1, eps=1e-6, random_state=42
    ... )
    >>> Z.shape
    (3, 2)
    >>> np.round(stress, 6).item()
    3.2e-05
    """

    if n_init == "warn":
            "The default value of `n_init` will change from 8 to 1 in 1.9.",
        )
        n_init = 8

    dissimilarities = check_array(dissimilarities)

    if normalized_stress == "auto":
        normalized_stress = not metric

    if hasattr(init, "__array__"):
        init = np.asarray(init).copy()
        if not n_init == 1:
                "Explicit initial positions passed: "
                "performing only one init of the MDS instead of %d" % n_init
            )
            n_init = 1

    best_pos, best_stress = None, None

        for it in range(n_init):
            pos, stress, n_iter_ = _smacof_single(
                dissimilarities,
                init=init,
                eps=eps,
                normalized_stress=normalized_stress,
            )
            if best_stress is None or stress < best_stress:
                best_stress = stress
                best_pos = pos.copy()
                best_iter = n_iter_
    else:
        seeds = random_state.randint(np.iinfo(np.int32).max, size=n_init)
        results = Parallel(n_jobs=n_jobs, verbose=max(verbose - 1, 0))(
            delayed(_smacof_single)(
                eps=eps,
            )
            for seed in seeds
        )
        positions, stress, n_iters = zip(*results)
        best = np.argmin(stress)
        best_stress = stress[best]
        best_pos = positions[best]
        best_iter = n_iters[best]

        return best_pos, best_stress, best_iter
    else:
        return best_pos, best_stress

class MDS(BaseEstimator):
    """Multidimensional scaling.

    Read more in the :ref:`User Guide <multidimensional_scaling>`.

        Number of dimensions in which to immerse the dissimilarities.

        If ``True``, perform metric MDS; otherwise, perform nonmetric MDS.

    n_init : int, default=4
        determined by the run with the smallest final stress.

           The default value for `n_init` will change from 4 to 1 in version 1.9.

    dissimilarity : {'euclidean', 'precomputed'}, default='euclidean'
        Dissimilarity measure to use:

        - 'euclidean':
            Pairwise Euclidean distances between points in the dataset.

        - 'precomputed':
            Pre-computed dissimilarities are passed directly to ``fit`` and
            ``fit_transform``.

    normalized_stress : bool or "auto" default="auto"

        Stores the position of the dataset in the embedding space.

    stress_ : float

    dissimilarity_matrix_ : ndarray of shape (n_samples, n_samples)
        Pairwise dissimilarities between the points. Symmetric matrix that:

        - either uses a custom dissimilarity matrix by setting `dissimilarity`
          to 'precomputed';
        - or constructs a dissimilarity matrix from data using
          Euclidean distances.

    See Also
    --------
    TSNE : T-distributed Stochastic Neighbor Embedding.

       Psychometrika, 29 (1964)

       hypothesis" Kruskal, J. Psychometrika, 29, (1964)

       Groenen P. Springer Series in Statistics (1997)

    Examples
    --------
    >>> from sklearn.manifold import MDS
    (1797, 64)
    >>> embedding = MDS(n_components=2, n_init=1)
    (100, 2)

    :ref:`sphx_glr_auto_examples_manifold_plot_mds.py`.

    For a comparison of manifold learning techniques, see
    :ref:`sphx_glr_auto_examples_manifold_plot_compare_methods.py`.
    """

        "metric": ["boolean"],
        "n_init": [Interval(Integral, 1, None, closed="left"), StrOptions({"warn"})],
        "eps": [Interval(Real, 0.0, None, closed="left")],
        "dissimilarity": [StrOptions({"euclidean", "precomputed"})],
        "normalized_stress": ["boolean", StrOptions({"auto"})],
    }

        self,
        *,
        metric=True,
        n_init="warn",
        max_iter=300,
        eps=1e-6,
        dissimilarity="euclidean",
        normalized_stress="auto",
    ):
        self.dissimilarity = dissimilarity
        self.n_init = n_init
        self.normalized_stress = normalized_stress

        tags.input_tags.pairwise = self.dissimilarity == "precomputed"

    def fit(self, X, y=None, init=None):
        """
        Compute the position of the points in the embedding space.

        X : array-like of shape (n_samples, n_features) or \
                (n_samples, n_samples)
            Input data. If ``dissimilarity=='precomputed'``, the input should
            be the dissimilarity matrix.

        y : Ignored

        init : ndarray of shape (n_samples, n_components), default=None
            Starting configuration of the embedding to initialize the SMACOF
            algorithm. By default, the algorithm is initialized with a randomly
            chosen array.

        Returns
        -------
        """
        self.fit_transform(X, init=init)

    def fit_transform(self, X, y=None, init=None):
        """
        Fit the data from `X`, and returns the embedded coordinates.

        y : Ignored

        Returns
        -------
            X transformed in the new space.
        """

        if self.n_init == "warn":
                "The default value of `n_init` will change from 4 to 1 in 1.9.",
            )
            self._n_init = 4
        else:
            self._n_init = self.n_init

        if X.shape[0] == X.shape[1] and self.dissimilarity != "precomputed":
                "The MDS API has changed. ``fit`` now constructs a"
                " dissimilarity matrix from data. To use a custom "
                "dissimilarity matrix, set "
                "``dissimilarity='precomputed'``."
            )

        if self.dissimilarity == "precomputed":
            self.dissimilarity_matrix_ = X
        elif self.dissimilarity == "euclidean":
            self.dissimilarity_matrix_ = euclidean_distances(X)

        self.embedding_, self.stress_, self.n_iter_ = smacof(
            self.dissimilarity_matrix_,
            n_init=self._n_init,
            normalized_stress=self.normalized_stress,
        )

        self,
        *,
        eps=1e-6,
    ):

        """

        y : Ignored

        Returns
        -------
        """

        """

        y : Ignored

        Returns
        -------
        """

            )
        else:

            )

        )

class Isomap(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    """Isomap Embedding.

    Non-linear dimensionality reduction through Isometric Mapping

    Read more in the :ref:`User Guide <isomap>`.

    n_neighbors : int or None, default=5
        Number of neighbors to consider for each point. If `n_neighbors` is an int,
        then `radius` must be `None`.

    radius : float or None, default=None
        Limiting distance of neighbors to return. If `radius` is a float,
        then `n_neighbors` must be set to `None`.

        'auto' : Attempt to choose the most efficient solver
        for the given problem.

        'arpack' : Use Arnoldi decomposition to find the eigenvalues
        and eigenvectors.

        'dense' : Use a direct solver (i.e. LAPACK)
        for the eigenvalue decomposition.

        Convergence tolerance passed to arpack or lobpcg.
        not used if eigen_solver == 'dense'.

    path_method : {'auto', 'FW', 'D'}, default='auto'
        Method to use in finding shortest path.

        'auto' : attempt to choose the best algorithm automatically.

        'FW' : Floyd-Warshall algorithm.

        'D' : Dijkstra's algorithm.

        Algorithm to use for nearest neighbors search,
        passed to neighbors.NearestNeighbors instance.

    metric : str, or callable, default="minkowski"
        feature array. If metric is a string or callable, it must be one of
        the options allowed by :func:`sklearn.metrics.pairwise_distances` for
        its metric parameter.
        must be square. X may be a :term:`Glossary <sparse graph>`.

    embedding_ : array-like, shape (n_samples, n_components)

    kernel_pca_ : object
        :class:`~sklearn.decomposition.KernelPCA` object used to implement the
        embedding.

    nbrs_ : sklearn.neighbors.NearestNeighbors instance

    dist_matrix_ : array-like, shape (n_samples, n_samples)
        Stores the geodesic distance matrix of training data.

    See Also
    --------

    .. [1] Tenenbaum, J.B.; De Silva, V.; & Langford, J.C. A global geometric
           framework for nonlinear dimensionality reduction. Science 290 (5500)

    Examples
    --------
    >>> from sklearn.manifold import Isomap
    (1797, 64)
    >>> embedding = Isomap(n_components=2)
    (100, 2)
    """

        "max_iter": [Interval(Integral, 1, None, closed="left"), None],
        "path_method": [StrOptions({"auto", "FW", "D"})],
        "p": [Interval(Real, 1, None, closed="left")],
    }

        self,
        *,
        tol=0,
        path_method="auto",
        p=2,
    ):
        self.path_method = path_method
        self.p = p

        if self.n_neighbors is not None and self.radius is not None:
                "Both n_neighbors and radius are provided. Use"
                f" Isomap(radius={self.radius}, n_neighbors=None) if intended to use"
                " radius-based neighbors"
            )

            radius=self.radius,
            p=self.p,
            metric_params=self.metric_params,
        )
        self.n_features_in_ = self.nbrs_.n_features_in_
        if hasattr(self.nbrs_, "feature_names_in_"):
            self.feature_names_in_ = self.nbrs_.feature_names_in_

        self.kernel_pca_ = KernelPCA(
            kernel="precomputed",
        ).set_output(transform="default")

        if self.n_neighbors is not None:
            nbg = kneighbors_graph(
                self.nbrs_,
                self.n_neighbors,
                p=self.p,
                mode="distance",
            )
        else:
            nbg = radius_neighbors_graph(
                p=self.p,
            )

        # Compute the number of connected components, and connect the different
        # components to be able to compute a shortest path between all pairs
        # of samples in the graph.
        # Similar fix to cluster._agglomerative._fix_connectivity.
        n_connected_components, labels = connected_components(nbg)
        if n_connected_components > 1:
            if self.metric == "precomputed" and issparse(X):
                    "The number of connected components of the neighbors graph"
                    f" is {n_connected_components} > 1. The graph cannot be "
                    "completed with metric='precomputed', and Isomap cannot be"
                    "fitted. Increase the number of neighbors to avoid this "
                    "issue, or precompute the full distance matrix instead "
                    "of passing a sparse neighbors graph."
                )
                (
                    "The number of connected components of the neighbors graph "
                    f"is {n_connected_components} > 1. Completing the graph to fit"
                    " Isomap might be slow. Increase the number of neighbors to "
                    "avoid this issue."
                ),
            )

            # use array validated by NearestNeighbors
            nbg = _fix_connected_components(
                X=self.nbrs_._fit_X,
                graph=nbg,
                n_connected_components=n_connected_components,
                component_labels=labels,
                metric=self.nbrs_.effective_metric_,
                **self.nbrs_.effective_metric_params_,
            )

        self.dist_matrix_ = shortest_path(nbg, method=self.path_method, directed=False)

        if self.nbrs_._fit_X.dtype == np.float32:
            self.dist_matrix_ = self.dist_matrix_.astype(
                self.nbrs_._fit_X.dtype, copy=False
            )

        G = self.dist_matrix_**2
        G *= -0.5

        self.embedding_ = self.kernel_pca_.fit_transform(G)

    def reconstruction_error(self):
        """Compute the reconstruction error for the embedding.

        Returns
        -------
        reconstruction_error : float
            Reconstruction error.

        Notes
        -----
        The cost function of an isomap embedding is

        ``E = frobenius_norm[K(D) - K(D_fit)] / n_samples``

        Where D is the matrix of distances for the input data X,
        D_fit is the matrix of distances for the output embedding X_fit,
        and K is the isomap kernel:

        ``K(D) = -0.5 * (I - 1/n_samples) * D^2 * (I - 1/n_samples)``
        """
        G = -0.5 * self.dist_matrix_**2
        G_center = KernelCenterer().fit_transform(G)
        evals = self.kernel_pca_.eigenvalues_
        return np.sqrt(np.sum(G_center**2) - np.sum(evals**2)) / G.shape[0]

        # Isomap.metric is not validated yet
    )

        X : {array-like, sparse matrix, BallTree, KDTree, NearestNeighbors}
            Sample data, shape = (n_samples, n_features), in the form of a
            numpy array, sparse matrix, precomputed tree, or NearestNeighbors
            object.

        y : Ignored

        Returns
        -------
        """

    )

        X : {array-like, sparse matrix, BallTree, KDTree}

        y : Ignored

        Returns
        -------
        """

        This is implemented by linking the points X into the graph of geodesic
        distances of the training data. First the `n_neighbors` nearest
        neighbors of X are found in the training data, and from these the
        shortest geodesic distances from each point in X to each point in
        the training data are computed in order to construct the kernel.
        The embedding of X is the projection of this kernel onto the
        embedding vectors of the training set.

        X : {array-like, sparse matrix}, shape (n_queries, n_features)
            If neighbors_algorithm='precomputed', X is assumed to be a
            distance matrix or a sparse graph of shape
            (n_queries, n_samples_fit).

        Returns
        -------
        X_new : array-like, shape (n_queries, n_components)
        """
            distances, indices = self.nbrs_.kneighbors(X, return_distance=True)
        else:
            distances, indices = self.nbrs_.radius_neighbors(X, return_distance=True)

        # Create the graph of shortest distances from X to
        # training data via the nearest neighbors of X.
        # This can be done as a single array operation, but it potentially
        # takes a lot of memory.  To avoid that, use a loop:

        n_samples_fit = self.nbrs_.n_samples_fit_
        n_queries = distances.shape[0]

        if hasattr(X, "dtype") and X.dtype == np.float32:
            dtype = np.float32
        else:

        G_X = np.zeros((n_queries, n_samples_fit), dtype)
        for i in range(n_queries):
            G_X[i] = np.min(self.dist_matrix_[indices[i]] + distances[i][:, None], 0)

        G_X **= 2
        G_X *= -0.5

        return self.kernel_pca_.transform(G_X)

        self,
        *,
        tol=0,
        p=2,
    ):
        self.p = p

            )

            p=self.p,
        )

                p=self.p,
            )
        else:
                p=self.p,
            )

                )
                (
                ),
            )

            )

            )

        G *= -0.5

        Returns
        -------

        Notes
        -----

        """

            object.

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """
        else:

        else:

        G_X **= 2
        G_X *= -0.5

def create_sample_data(dtype, n_pts=25, add_noise=False):
    # grid of equidistant points in 2D, n_components = n_dim
    n_per_side = int(math.sqrt(n_pts))
    X = np.array(list(product(range(n_per_side), repeat=2))).astype(dtype, copy=False)
        # add noise in a third dimension
        noise = 0.1 * rng.randn(n_pts, 1).astype(dtype, copy=False)
        X = np.concatenate((X, noise), 1)
    return X

def test_isomap_simple_grid(
    global_dtype, n_neighbors, radius, eigen_solver, path_method
):
    # Isomap should preserve distances when all neighbors are used
    n_pts = 25
    X = create_sample_data(global_dtype, n_pts=n_pts, add_noise=False)

    # distances from each point to all others
    if n_neighbors is not None:
        G = neighbors.kneighbors_graph(X, n_neighbors, mode="distance")
    else:
        G = neighbors.radius_neighbors_graph(X, radius, mode="distance")

    clf = manifold.Isomap(
        path_method=path_method,
    )

        G_iso = neighbors.kneighbors_graph(clf.embedding_, n_neighbors, mode="distance")
    else:
        G_iso = neighbors.radius_neighbors_graph(
            clf.embedding_, radius, mode="distance"
        )
    atol = 1e-5 if global_dtype == np.float32 else 0
    assert_allclose_dense_sparse(G, G_iso, atol=atol)

def test_isomap_reconstruction_error(
):
    if global_dtype is np.float32:
            "Skipping test due to numerical instabilities on float32 data"
            "from KernelCenterer used in the reconstruction_error method"
        )

    # Same setup as in test_isomap_simple_grid, with an added dimension
    n_pts = 25
    X = create_sample_data(global_dtype, n_pts=n_pts, add_noise=True)

    # compute input kernel
        G = neighbors.kneighbors_graph(X, n_neighbors, mode="distance").toarray()
    else:
        G = neighbors.radius_neighbors_graph(X, radius, mode="distance").toarray()
    centerer = preprocessing.KernelCenterer()
    K = centerer.fit_transform(-0.5 * G**2)

    )

    # compute output kernel
    else:
        )
    G_iso = G_iso.toarray()
    K_iso = centerer.fit_transform(-0.5 * G_iso**2)

    # make sure error agrees
    reconstruction_error = np.linalg.norm(K - K_iso) / n_pts
    assert_allclose(reconstruction_error, clf.reconstruction_error(), atol=atol)

def test_transform(global_dtype, n_neighbors, radius):
    n_samples = 200
    noise_scale = 0.01

    # Create S-curve dataset
    X, y = datasets.make_s_curve(n_samples, random_state=0)

    # Compute isomap embedding
    iso = manifold.Isomap(
        n_components=n_components, n_neighbors=n_neighbors, radius=radius
    )
    X_iso = iso.fit_transform(X)

    # Re-embed a noisy version of the points
    noise = noise_scale * rng.randn(*X.shape)
    X_iso2 = iso.transform(X + noise)

    # Make sure the rms error on re-embedding is comparable to noise_scale
    assert np.sqrt(np.mean((X_iso - X_iso2) ** 2)) < 2 * noise_scale

def test_pipeline(n_neighbors, radius, global_dtype):
    # check that Isomap works fine as a transformer in a Pipeline
    # only checks that no error is raised.
    # TODO check that it actually does something useful
    X, y = datasets.make_blobs(random_state=0)
    clf = pipeline.Pipeline(
        [
            ("isomap", manifold.Isomap(n_neighbors=n_neighbors, radius=radius)),
            ("clf", neighbors.KNeighborsClassifier()),
        ]
    )
    assert 0.9 < clf.score(X, y)

def test_pipeline_with_nearest_neighbors_transformer(global_dtype):
    # Test chaining NearestNeighborsTransformer and Isomap with

    X, _ = datasets.make_blobs(random_state=0)
    X2, _ = datasets.make_blobs(random_state=1)

    X2 = X2.astype(global_dtype, copy=False)

    est_chain = pipeline.make_pipeline(
        neighbors.KNeighborsTransformer(
        ),
        manifold.Isomap(n_neighbors=n_neighbors, metric="precomputed"),
    )
    est_compact = manifold.Isomap(
        n_neighbors=n_neighbors, neighbors_algorithm=algorithm
    )

    assert_allclose(Xt_chain, Xt_compact)

def test_different_metric(global_dtype, metric, p, is_euclidean):
    # Isomap must work on various metric parameters work correctly
    # and must default to euclidean.

    reference = manifold.Isomap().fit_transform(X)
    embedding = manifold.Isomap(metric=metric, p=p).fit_transform(X)

    if is_euclidean:
        assert_allclose(embedding, reference)
    else:
        with pytest.raises(AssertionError, match="Not equal to tolerance"):
            assert_allclose(embedding, reference)

def test_isomap_clone_bug():
    # regression test for bug reported in #6062
    model = manifold.Isomap()
    for n_neighbors in [10, 15, 20]:
        model.set_params(n_neighbors=n_neighbors)
        model.fit(np.random.rand(50, 2))
        assert model.nbrs_.n_neighbors == n_neighbors

def test_sparse_input(
    global_dtype, eigen_solver, path_method, global_random_seed, csr_container
):
        sparse_rand(
            100,
            3,
            density=0.1,
            format="csr",
            dtype=global_dtype,
        )
    )

    iso_dense = manifold.Isomap(
        n_neighbors=8,
    )
    iso_sparse = clone(iso_dense)

    X_trans_dense = iso_dense.fit_transform(X.toarray())
    X_trans_sparse = iso_sparse.fit_transform(X)

    assert_allclose(X_trans_sparse, X_trans_dense, rtol=1e-4, atol=1e-4)

def test_isomap_fit_precomputed_radius_graph(global_dtype):
    # Isomap.fit_transform must yield similar result when using
    # a precomputed distance matrix.

    X, y = datasets.make_s_curve(200, random_state=0)
    radius = 10

    g = neighbors.radius_neighbors_graph(X, radius=radius, mode="distance")
    isomap = manifold.Isomap(n_neighbors=None, radius=radius, metric="precomputed")
    isomap.fit(g)
    precomputed_result = isomap.embedding_

    isomap = manifold.Isomap(n_neighbors=None, radius=radius, metric="minkowski")
    result = isomap.fit_transform(X)
    assert_allclose(precomputed_result, result, atol=atol)

def test_isomap_fitted_attributes_dtype(global_dtype):
    """Check that the fitted attributes are stored accordingly to the
    data type of X."""
    iso = manifold.Isomap(n_neighbors=2)

    X = np.array([[1, 2], [3, 4], [5, 6]], dtype=global_dtype)

    assert iso.dist_matrix_.dtype == global_dtype
    assert iso.embedding_.dtype == global_dtype

def test_isomap_dtype_equivalence():
    iso_32 = manifold.Isomap(n_neighbors=2)
    X_32 = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float32)
    iso_32.fit(X_32)

    iso_64 = manifold.Isomap(n_neighbors=2)
    X_64 = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float64)
    iso_64.fit(X_64)

    assert_allclose(iso_32.dist_matrix_, iso_64.dist_matrix_)

def test_isomap_raise_error_when_neighbor_and_radius_both_set():
    # Isomap.fit_transform must raise a ValueError if
    # radius and n_neighbors are provided.

    isomap = manifold.Isomap(n_neighbors=3, radius=5.5)
    msg = "Both n_neighbors and radius are provided"
        isomap.fit_transform(X)

def test_multiple_connected_components():
    # Test that a warning is raised when the graph has multiple components
    X = np.array([0, 1, 2, 5, 6, 7])[:, None]
    with pytest.warns(UserWarning, match="number of connected components"):
        manifold.Isomap(n_neighbors=2).fit(X)

def test_multiple_connected_components_metric_precomputed(global_dtype):
    # Test that an error is raised when the graph has multiple components
    # and when X is a precomputed neighbors graph.
    X = np.array([0, 1, 2, 5, 6, 7])[:, None].astype(global_dtype, copy=False)

    # works with a precomputed distance matrix (dense)
    X_distances = pairwise_distances(X)
        manifold.Isomap(n_neighbors=1, metric="precomputed").fit(X_distances)

    # does not work with a precomputed neighbors graph (sparse)
    X_graph = neighbors.kneighbors_graph(X, n_neighbors=2, mode="distance")
    with pytest.raises(RuntimeError, match="number of connected components"):
        manifold.Isomap(n_neighbors=1, metric="precomputed").fit(X_graph)

def test_get_feature_names_out():
    """Check get_feature_names_out for Isomap."""
    X, y = make_blobs(random_state=0, n_features=4)

    iso = manifold.Isomap(n_components=n_components)
    iso.fit_transform(X)
    names = iso.get_feature_names_out()
    assert_array_equal([f"isomap{i}" for i in range(n_components)], names)

def _assert_equal_with_sign_flipping(A, B, tol=0.0):
    """Check array A and B are equal with possible sign flipping on
    each column"""
    tol_squared = tol**2
    for A_col, B_col in zip(A.T, B.T):
        assert (
            np.max((A_col - B_col) ** 2) <= tol_squared
            or np.max((A_col + B_col) ** 2) <= tol_squared
        )

def test_sparse_graph_connected_component(coo_container):
    boundaries = [0, 42, 121, 200, n_samples]
    p = rng.permutation(n_samples)
    connections = []

    for start, stop in itertools.pairwise(boundaries):
        group = p[start:stop]
        # Connect all elements within the group at least once via an
        # arbitrary path that spans the group.
        for i in range(len(group) - 1):
            connections.append((group[i], group[i + 1]))

        # Add some more random connections within the group
        min_idx, max_idx = 0, len(group) - 1
        n_random_connections = 1000
        source = rng.randint(min_idx, max_idx, size=n_random_connections)
        target = rng.randint(min_idx, max_idx, size=n_random_connections)
        connections.extend(zip(group[source], group[target]))

    # Build a symmetric affinity matrix
    row_idx, column_idx = tuple(np.array(connections).T)
    data = rng.uniform(0.1, 42, size=len(connections))
    affinity = coo_container((data, (row_idx, column_idx)))
    affinity = 0.5 * (affinity + affinity.T)

        component_1 = _graph_connected_component(affinity, p[start])
        component_size = stop - start
        assert component_1.sum() == component_size

        # We should retrieve the same component mask by starting by both ends
        # of the group
        component_2 = _graph_connected_component(affinity, p[stop - 1])
        assert component_2.sum() == component_size
        assert_array_equal(component_1, component_2)

def test_spectral_embedding_two_components(eigen_solver, dtype, seed=0):
    # Test spectral embedding with two components
    random_state = np.random.RandomState(seed)
    n_sample = 100
    affinity = np.zeros(shape=[n_sample * 2, n_sample * 2])
    # first component
    affinity[0:n_sample, 0:n_sample] = (
        np.abs(random_state.randn(n_sample, n_sample)) + 2
    )
    # second component
    affinity[n_sample::, n_sample::] = (
    )

    # Test of internal _graph_connected_component before connection
    component = _graph_connected_component(affinity, 0)
    assert component[:n_sample].all()
    assert not component[n_sample:].any()
    component = _graph_connected_component(affinity, -1)
    assert not component[:n_sample].any()
    assert component[n_sample:].all()

    # connection
    affinity[0, n_sample + 1] = 1
    affinity[n_sample + 1, 0] = 1
    affinity.flat[:: 2 * n_sample + 1] = 0

    true_label = np.zeros(shape=2 * n_sample)
    true_label[0:n_sample] = 1

    se_precomp = SpectralEmbedding(
        affinity="precomputed",
        random_state=np.random.RandomState(seed),
    )

    embedded_coordinate = se_precomp.fit_transform(affinity.astype(dtype))
    # thresholding on the first components using 0.
    label_ = np.array(embedded_coordinate.ravel() < 0, dtype=np.int64)
    assert normalized_mutual_info_score(true_label, label_) == pytest.approx(1.0)

def test_spectral_embedding_precomputed_affinity(
    sparse_container, eigen_solver, dtype, seed=36
):
    # Test spectral embedding with precomputed kernel
    gamma = 1.0
    X = S if sparse_container is None else sparse_container(S)

    )
    se_rbf = SpectralEmbedding(
        affinity="rbf",
        gamma=gamma,
    )
    embed_precomp = se_precomp.fit_transform(rbf_kernel(X.astype(dtype), gamma=gamma))
    embed_rbf = se_rbf.fit_transform(X.astype(dtype))
    assert_array_almost_equal(se_precomp.affinity_matrix_, se_rbf.affinity_matrix_)
    _assert_equal_with_sign_flipping(embed_precomp, embed_rbf, 0.05)

def test_precomputed_nearest_neighbors_filtering():
    # Test precomputed graph filtering when containing too many neighbors
    n_neighbors = 2
    for additional_neighbors in [0, 10]:
        nn = NearestNeighbors(n_neighbors=n_neighbors + additional_neighbors).fit(S)
        graph = nn.kneighbors_graph(S, mode="connectivity")
        embedding = (
            SpectralEmbedding(
                n_components=2,
                affinity="precomputed_nearest_neighbors",
            )
            .fit(graph)
            .embedding_
        )
        results.append(embedding)

    assert_array_equal(results[0], results[1])

def test_spectral_embedding_callable_affinity(sparse_container, seed=36):
    # Test spectral embedding with callable affinity
    gamma = 0.9
    kern = rbf_kernel(S, gamma=gamma)

    se_callable = SpectralEmbedding(
        affinity=(lambda x: rbf_kernel(x, gamma=gamma)),
    )
    )
    embed_rbf = se_rbf.fit_transform(X)
    embed_callable = se_callable.fit_transform(X)
    assert_array_almost_equal(se_callable.affinity_matrix_, se_rbf.affinity_matrix_)
    assert_array_almost_equal(kern, se_rbf.affinity_matrix_)
    _assert_equal_with_sign_flipping(embed_rbf, embed_callable, 0.05)

def test_spectral_embedding_amg_solver(dtype, coo_container, seed=36):
    se_amg = SpectralEmbedding(
        eigen_solver="amg",
    )
    se_arpack = SpectralEmbedding(
        eigen_solver="arpack",
    )
    embed_amg = se_amg.fit_transform(S.astype(dtype))
    embed_arpack = se_arpack.fit_transform(S.astype(dtype))
    _assert_equal_with_sign_flipping(embed_amg, embed_arpack, 1e-5)

    # same with special case in which amg is not actually used
    # regression test for #10715
    # affinity between nodes
    row = np.array([0, 0, 1, 2, 3, 3, 4], dtype=np.int32)
    col = np.array([1, 2, 2, 3, 4, 5, 5], dtype=np.int32)
    val = np.array([100, 100, 100, 1, 100, 100, 100], dtype=np.int64)

    affinity = coo_container(
        (np.hstack([val, val]), (np.hstack([row, col]), np.hstack([col, row]))),
        shape=(6, 6),
    )
    se_amg.affinity = "precomputed"
    se_arpack.affinity = "precomputed"
    embed_amg = se_amg.fit_transform(affinity.astype(dtype))
    embed_arpack = se_arpack.fit_transform(affinity.astype(dtype))

    # Check that passing a sparse matrix with `np.int64` indices dtype raises an error
    # or is successful based on the version of SciPy which is installed.
    # Use a CSR matrix to avoid any conversion during the validation
    affinity = affinity.tocsr()
    affinity.indptr = affinity.indptr.astype(np.int64)
    affinity.indices = affinity.indices.astype(np.int64)

    # PR: https://github.com/scipy/scipy/pull/18913
    # First integration in 1.11.3: https://github.com/scipy/scipy/pull/19279
    scipy_graph_traversal_supports_int64_index = sp_version >= parse_version("1.11.3")
    if scipy_graph_traversal_supports_int64_index:
        se_amg.fit_transform(affinity)
    else:
        err_msg = "Only sparse matrices with 32-bit integer indices are accepted"
            se_amg.fit_transform(affinity)

def test_spectral_embedding_amg_solver_failure(dtype, seed=36):
    # Non-regression test for amg solver failure (issue #13393 on github)
    num_nodes = 100
    X = sparse.rand(num_nodes, num_nodes, density=0.1, random_state=seed)
    X = X.astype(dtype)
    upper = sparse.triu(X) - sparse.diags(X.diagonal())
    sym_matrix = upper + upper.T
    embedding = spectral_embedding(
        sym_matrix, n_components=10, eigen_solver="amg", random_state=0
    )

    # Check that the learned embedding is stable w.r.t. random solver init:
        new_embedding = spectral_embedding(
            sym_matrix, n_components=10, eigen_solver="amg", random_state=i + 1
        )
        _assert_equal_with_sign_flipping(embedding, new_embedding, tol=0.05)

def test_pipeline_spectral_clustering(seed=36):
    # Test using pipeline to do spectral clustering
        n_components=n_clusters, affinity="rbf", random_state=random_state
    )
    se_knn = SpectralEmbedding(
        n_components=n_clusters,
    )
    for se in [se_rbf, se_knn]:
        km = KMeans(n_clusters=n_clusters, random_state=random_state, n_init=10)
        km.fit(se.fit_transform(S))
            normalized_mutual_info_score(km.labels_, true_labels), 1.0, 2
        )

def test_connectivity(seed=36):
    # Test that graph connectivity test works as expected
    graph = np.array(
        [
            [1, 0, 0, 0, 0],
            [0, 1, 1, 0, 0],
            [0, 1, 1, 1, 0],
            [0, 0, 1, 1, 1],
            [0, 0, 0, 1, 1],
        ]
    )
    assert not _graph_is_connected(graph)
    for csr_container in CSR_CONTAINERS:
        assert not _graph_is_connected(csr_container(graph))
    for csc_container in CSC_CONTAINERS:
        assert not _graph_is_connected(csc_container(graph))

        [
            [1, 1, 0, 0, 0],
            [1, 1, 1, 0, 0],
        ]
    )
    assert _graph_is_connected(graph)
        assert _graph_is_connected(csr_container(graph))
        assert _graph_is_connected(csc_container(graph))

def test_spectral_embedding_deterministic():
    # Test that Spectral Embedding is deterministic
    random_state = np.random.RandomState(36)
    data = random_state.randn(10, 30)
    sims = rbf_kernel(data)
    embedding_1 = spectral_embedding(sims)
    embedding_2 = spectral_embedding(sims)
    assert_array_almost_equal(embedding_1, embedding_2)

def test_spectral_embedding_unnormalized():
    # Test that spectral_embedding is also processing unnormalized laplacian
    # correctly
    embedding_1 = spectral_embedding(
        sims, norm_laplacian=False, n_components=n_components, drop_first=False
    )

    # Verify using manual computation with dense eigh
    laplacian, dd = csgraph_laplacian(sims, normed=False, return_diag=True)
    _, diffusion_map = eigh(laplacian)
    embedding_2 = diffusion_map.T[:n_components]
    embedding_2 = _deterministic_vector_sign_flip(embedding_2).T

def test_spectral_embedding_first_eigen_vector():
    # Test that the first eigenvector of spectral_embedding
    # is constant and that the second is not (for a connected graph)

    for seed in range(10):
        embedding = spectral_embedding(
            sims,
            norm_laplacian=False,
            drop_first=False,
        )

        assert np.std(embedding[:, 0]) == pytest.approx(0)
        assert np.std(embedding[:, 1]) > 1e-3

def test_spectral_embedding_preserves_dtype(eigen_solver, dtype):
    """Check that `SpectralEmbedding is preserving the dtype of the fitted
    attribute and transformed data.

    Ideally, this test should be covered by the common test
    `check_transformer_preserve_dtypes`. However, this test only run
    with transformers implementing `transform` while `SpectralEmbedding`
    implements only `fit_transform`.
    """
    X = S.astype(dtype)
    se = SpectralEmbedding(
        n_components=2, affinity="rbf", eigen_solver=eigen_solver, random_state=0
    )
    X_trans = se.fit_transform(X)

    assert X_trans.dtype == dtype
    assert se.embedding_.dtype == dtype
    assert se.affinity_matrix_.dtype == dtype

def test_error_pyamg_not_available():
    )
    err_msg = "The eigen_solver was set to 'amg', but pyamg is not available."
        se_precomp.fit_transform(S)

def test_spectral_eigen_tol_auto(monkeypatch, solver, csr_container):
    """Test that `eigen_tol="auto"` is resolved correctly"""
    if solver == "amg" and not pyamg_available:
        pytest.skip("PyAMG is not available.")
    X, _ = make_blobs(
        n_samples=200, random_state=0, centers=[[1, 1], [-1, -1]], cluster_std=0.01
    )
    D = pairwise_distances(X)  # Distance matrix
    S = np.max(D) - D  # Similarity matrix

    solver_func = eigsh if solver == "arpack" else lobpcg
    default_value = 0 if solver == "arpack" else None
    if solver == "amg":
        S = csr_container(S)

    mocked_solver = Mock(side_effect=solver_func)

    monkeypatch.setattr(_spectral_embedding, solver_func.__qualname__, mocked_solver)

    spectral_embedding(S, random_state=42, eigen_solver=solver, eigen_tol="auto")
    mocked_solver.assert_called()

    _, kwargs = mocked_solver.call_args
    assert kwargs["tol"] == default_value

def test_smacof():
    # test metric smacof using the data of "Modern Multidimensional Scaling",
    # Borg & Groenen, p 154
    sim = np.array([[0, 5, 3, 4], [5, 0, 2, 2], [3, 2, 0, 1], [4, 2, 1, 0]])
    Z = np.array([[-0.266, -0.539], [0.451, 0.252], [0.016, -0.238], [-0.200, 0.524]])
    X, _ = mds.smacof(sim, init=Z, n_components=2, max_iter=1, n_init=1)
        [[-1.415, -2.471], [1.633, 1.107], [0.249, -0.067], [-0.468, 1.431]]
    )
    assert_array_almost_equal(X, X_true, decimal=3)

def test_nonmetric_lower_normalized_stress():
    # Testing that nonmetric MDS results in lower normalized stress compared
    # compared to metric MDS (non-regression test for issue 27028)

    _, stress1 = mds.smacof(
        sim, init=Z, n_components=2, max_iter=1000, n_init=1, normalized_stress=True
    )

    _, stress2 = mds.smacof(
        sim,
        init=Z,
        n_init=1,
        normalized_stress=True,
        metric=False,
    )
    assert stress1 > stress2

def test_nonmetric_mds_optimization():
    # Test that stress is decreasing during nonmetric MDS optimization
    # (non-regression test for issue 27028)
    X, _ = load_digits(return_X_y=True)
    rng = np.random.default_rng(seed=42)
    ind_subset = rng.choice(len(X), size=200, replace=False)
    X = X[ind_subset]

    mds_est = mds.MDS(
        n_init=1,
    ).fit(X)
    stress_after_2_iter = mds_est.stress_

        n_init=1,
    ).fit(X)
    stress_after_3_iter = mds_est.stress_

    assert stress_after_2_iter > stress_after_3_iter

def test_mds_recovers_true_data(metric):
    X = np.array([[1, 1], [1, 4], [1, 5], [3, 3]])
        n_init=1,
        eps=1e-15,
    ).fit(X)
    stress = mds_est.stress_
    assert_allclose(stress, 0, atol=1e-6)

def test_smacof_error():
    # Not symmetric similarity matrix:
    sim = np.array([[0, 5, 9, 4], [5, 0, 2, 2], [3, 2, 0, 1], [4, 2, 1, 0]])

        mds.smacof(sim, n_init=1)

    # Not squared similarity matrix:
    sim = np.array([[0, 5, 9, 4], [5, 0, 2, 2], [4, 2, 1, 0]])

    # init not None and not correct format:

    Z = np.array([[-0.266, -0.539], [0.016, -0.238], [-0.200, 0.524]])
        mds.smacof(sim, init=Z, n_init=1)

def test_MDS():
    mds_clf = mds.MDS(
        n_jobs=3,
        n_init=3,
        dissimilarity="precomputed",
    )
    mds_clf.fit(sim)

def test_normed_stress(k):
    """Test that non-metric MDS normalized stress is scale-invariant."""

    X1, stress1 = mds.smacof(sim, metric=False, max_iter=5, random_state=0)
    X2, stress2 = mds.smacof(k * sim, metric=False, max_iter=5, random_state=0)

    assert_allclose(stress1, stress2, rtol=1e-5)
    assert_allclose(X1, X2, rtol=1e-5)

def test_normalized_stress_auto(metric, monkeypatch):
    X = rng.randn(4, 3)
    dist = euclidean_distances(X)

    mock = Mock(side_effect=mds._smacof_single)
    monkeypatch.setattr("sklearn.manifold._mds._smacof_single", mock)

    est = mds.MDS(metric=metric, normalized_stress="auto", random_state=rng)
    est.fit_transform(X)
    assert mock.call_args[1]["normalized_stress"] != metric

    mds.smacof(dist, metric=metric, normalized_stress="auto", random_state=rng)

def test_isotonic_outofbounds():
    # This particular configuration can trigger out of bounds error
    # in the isotonic regression (non-regression test for issue 26999)
    dis = np.array(
        [
            [0.0, 1.732050807568877, 1.7320508075688772],
            [1.732050807568877, 0.0, 6.661338147750939e-16],
            [1.7320508075688772, 6.661338147750939e-16, 0.0],
        ]
    )
    init = np.array(
        [
            [0.08665881585055124, 0.7939114643387546],
            [0.9959834154297658, 0.7555546025640025],
            [0.8766008278401566, 0.4227358815811242],
        ]
    )
    mds.smacof(dis, init=init, metric=False, n_init=1)

def test_returned_stress(normalized_stress):
    # Test that the final stress corresponds to the final embedding
    # (non-regression test for issue 16846)
    D = euclidean_distances(X)

        normalized_stress=normalized_stress,
    ).fit(X)

    Z = mds_est.embedding_

    D_mds = euclidean_distances(Z)
    stress_Z = ((D_mds.ravel() - D.ravel()) ** 2).sum() / 2

        stress_Z = np.sqrt(stress_Z / ((D_mds.ravel() ** 2).sum() / 2))

    assert_allclose(stress, stress_Z)

def test_convergence_does_not_depend_on_scale(metric):
    # Test that the number of iterations until convergence does not depend on
    # the scale of the input data

    )

    mds_est.fit(X * 100)
    n_iter1 = mds_est.n_iter_

    mds_est.fit(X / 100)
    n_iter2 = mds_est.n_iter_

    assert_equal(n_iter1, n_iter2)

def test_future_warning_n_init():

    with pytest.warns(FutureWarning):
        mds.smacof(sim)

        mds.MDS().fit(X)

def test_barycenter_kneighbors_graph(global_dtype):
    X = np.array([[0, 1], [1.01, 1.0], [2, 0]], dtype=global_dtype)

    graph = barycenter_kneighbors_graph(X, 1)
    expected_graph = np.array(
        [[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=global_dtype
    )

    assert graph.dtype == global_dtype

    assert_allclose(graph.toarray(), expected_graph)

    graph = barycenter_kneighbors_graph(X, 2)
    # check that columns sum to one
    assert_allclose(np.sum(graph.toarray(), axis=1), np.ones(3))
    pred = np.dot(graph.toarray(), X)
    assert linalg.norm(pred - X) / X.shape[0] < 1

def test_lle_simple_grid(global_dtype):
    # note: ARPACK is numerically unstable, so this test will fail for
    #       some random seeds.  We choose 42 because the tests pass.
    #       for arm64 platforms 2 makes the test fail.
    # TODO: rewrite this test to make less sensitive to the random seed,
    # irrespective of the platform.

    X = np.array(list(product(range(5), repeat=2)))
    X = X + 1e-10 * rng.uniform(size=X.shape)

    clf = manifold.LocallyLinearEmbedding(
        n_neighbors=5, n_components=n_components, random_state=rng
    )
    tol = 0.1

    N = barycenter_kneighbors_graph(X, clf.n_neighbors).toarray()
    reconstruction_error = linalg.norm(np.dot(N, X) - X, "fro")
    assert reconstruction_error < tol

    for solver in eigen_solvers:
        clf.set_params(eigen_solver=solver)
        assert clf.embedding_.shape[1] == n_components
        reconstruction_error = (
            linalg.norm(np.dot(N, clf.embedding_) - clf.embedding_, "fro") ** 2
        )

        assert reconstruction_error < tol
        assert_allclose(clf.reconstruction_error_, reconstruction_error, atol=1e-1)

    # re-embed a noisy version of X using the transform method
    noise = rng.randn(*X.shape).astype(global_dtype, copy=False) / 100
    X_reembedded = clf.transform(X + noise)
    assert linalg.norm(X_reembedded - clf.embedding_) < tol

def test_lle_manifold(global_dtype, method, solver):
    # similar test on a slightly more complex manifold
    X = np.array(list(product(np.arange(18), repeat=2)))
    X = np.c_[X, X[:, 0] ** 2 / 18]

        n_neighbors=6, n_components=n_components, method=method, random_state=0
    )
    tol = 1.5 if method == "standard" else 3

    reconstruction_error = linalg.norm(np.dot(N, X) - X)

    clf.set_params(eigen_solver=solver)
    assert clf.embedding_.shape[1] == n_components
    reconstruction_error = (
        linalg.norm(np.dot(N, clf.embedding_) - clf.embedding_, "fro") ** 2
    )
    details = "solver: %s, method: %s" % (solver, method)
    assert reconstruction_error < tol, details
    assert (
        np.abs(clf.reconstruction_error_ - reconstruction_error)
        < tol * reconstruction_error
    ), details

def test_pipeline():
    # check that LocallyLinearEmbedding works fine as a Pipeline
    from sklearn import datasets, pipeline

        [
            ("filter", manifold.LocallyLinearEmbedding(random_state=0)),
        ]
    )

def test_singular_matrix():
    M = np.ones((200, 3))
    f = ignore_warnings
    with pytest.raises(ValueError, match="Error in determining null-space with ARPACK"):
        f(
            manifold.locally_linear_embedding(
                M,
                n_neighbors=2,
                n_components=1,
                method="standard",
                eigen_solver="arpack",
            )
        )

def test_integer_input():
    rand = np.random.RandomState(0)
    X = rand.randint(0, 100, size=(20, 3))

    for method in ["standard", "hessian", "modified", "ltsa"]:
        clf = manifold.LocallyLinearEmbedding(method=method, n_neighbors=10)
        clf.fit(X)  # this previously raised a TypeError

    """Check get_feature_names_out for LocallyLinearEmbedding."""

    iso = manifold.LocallyLinearEmbedding(n_components=n_components)
        [f"locallylinearembedding{i}" for i in range(n_components)], names
    )

def test_gradient_descent_stops():
    # Test stopping conditions of gradient descent.
    class ObjectiveSmallGradient:
            self.it = -1

        def __call__(self, _, compute_error=True):
            self.it += 1
            return (10 - self.it) / 10.0, np.array([1e-5])

    def flat_function(_, compute_error=True):
        return 0.0, np.ones(1)

    # Gradient norm
    try:
        _, error, it = _gradient_descent(
            ObjectiveSmallGradient(),
            np.zeros(1),
            0,
            n_iter_without_progress=100,
            momentum=0.0,
            learning_rate=0.0,
            min_gain=0.0,
            min_grad_norm=1e-5,
        )
    finally:
        out = sys.stdout.getvalue()
        sys.stdout.close()
    assert error == 1.0
    assert it == 0
    assert "gradient norm" in out

    # Maximum number of iterations without improvement
    try:
            flat_function,
            0,
            n_iter_without_progress=10,
            min_grad_norm=0.0,
        )
    finally:
    assert error == 0.0
    assert it == 11
    assert "did not make any progress" in out

    # Maximum number of iterations
    try:
            0,
            max_iter=11,
        )
    finally:
    assert it == 10
    assert "Iteration 10" in out

def test_binary_search():
    # Test if the binary search finds Gaussians with desired perplexity.
    data = random_state.randn(50, 5)
    distances = pairwise_distances(data).astype(np.float32)
    desired_perplexity = 25.0
    P = _binary_search_perplexity(distances, desired_perplexity, verbose=0)
    P = np.maximum(P, np.finfo(np.double).eps)
    mean_perplexity = np.mean(
        [np.exp(-np.sum(P[i] * np.log(P[i]))) for i in range(P.shape[0])]
    )
    assert_almost_equal(mean_perplexity, desired_perplexity, decimal=3)

def test_binary_search_underflow():
    # A more challenging case than the one above, producing numeric
    # underflow in float precision (see issue #19471 and PR #19472).
    random_state = check_random_state(42)
    data = random_state.randn(1, 90).astype(np.float32) + 100
    desired_perplexity = 30.0
    P = _binary_search_perplexity(data, desired_perplexity, verbose=0)
    perplexity = 2 ** -np.nansum(P[0, 1:] * np.log2(P[0, 1:]))
    assert_almost_equal(perplexity, desired_perplexity, decimal=3)

def test_binary_search_neighbors():
    # Binary perplexity search approximation.
    # Should be approximately equal to the slow method when we use
    # all points as neighbors.
    data = random_state.randn(n_samples, 2).astype(np.float32, copy=False)
    distances = pairwise_distances(data)
    P1 = _binary_search_perplexity(distances, desired_perplexity, verbose=0)

    # Test that when we use all the neighbors the results are identical
    n_neighbors = n_samples - 1
    nn = NearestNeighbors().fit(data)
    distance_graph = nn.kneighbors_graph(n_neighbors=n_neighbors, mode="distance")
    distances_nn = distance_graph.data.astype(np.float32, copy=False)
    distances_nn = distances_nn.reshape(n_samples, n_neighbors)
    P2 = _binary_search_perplexity(distances_nn, desired_perplexity, verbose=0)

    indptr = distance_graph.indptr
    P1_nn = np.array(
        [
            P1[k, distance_graph.indices[indptr[k] : indptr[k + 1]]]
            for k in range(n_samples)
        ]
    )
    assert_array_almost_equal(P1_nn, P2, decimal=4)

    # Test that the highest P_ij are the same when fewer neighbors are used
    for k in np.linspace(150, n_samples - 1, 5):
        k = int(k)
        topn = k * 10  # check the top 10 * k entries out of k * k entries
        distance_graph = nn.kneighbors_graph(n_neighbors=k, mode="distance")
        distances_nn = distance_graph.data.astype(np.float32, copy=False)
        distances_nn = distances_nn.reshape(n_samples, k)
        P2k = _binary_search_perplexity(distances_nn, desired_perplexity, verbose=0)
        assert_array_almost_equal(P1_nn, P2, decimal=2)
        idx = np.argsort(P1.ravel())[::-1]
        P1top = P1.ravel()[idx][:topn]
        idx = np.argsort(P2k.ravel())[::-1]
        P2top = P2k.ravel()[idx][:topn]
        assert_array_almost_equal(P1top, P2top, decimal=2)

def test_binary_perplexity_stability():
    # Binary perplexity search should be stable.
    # The binary_search_perplexity had a bug wherein the P array
    # was uninitialized, leading to sporadically failing tests.
    data = random_state.randn(n_samples, 5)
    distances = distance_graph.data.astype(np.float32, copy=False)
    distances = distances.reshape(n_samples, n_neighbors)
    last_P = None
    desired_perplexity = 3
    for _ in range(100):
        P = _binary_search_perplexity(distances.copy(), desired_perplexity, verbose=0)
        P1 = _joint_probabilities_nn(distance_graph, desired_perplexity, verbose=0)
        # Convert the sparse matrix to a dense one for testing
        P1 = P1.toarray()
        if last_P is None:
            last_P = P
            last_P1 = P1
        else:
            assert_array_almost_equal(P, last_P, decimal=4)
            assert_array_almost_equal(P1, last_P1, decimal=4)

    # Test gradient of Kullback-Leibler divergence.

    alpha = 1.0

    distances = random_state.randn(n_samples, n_features).astype(np.float32)
    distances = np.abs(distances.dot(distances.T))
    np.fill_diagonal(distances, 0.0)
    X_embedded = random_state.randn(n_samples, n_components).astype(np.float32)

    P = _joint_probabilities(distances, desired_perplexity=25.0, verbose=0)

    def fun(params):
        return _kl_divergence(params, P, alpha, n_samples, n_components)[0]

    def grad(params):
        return _kl_divergence(params, P, alpha, n_samples, n_components)[1]

    assert_almost_equal(check_grad(fun, grad, X_embedded.ravel()), 0.0, decimal=5)

def test_trustworthiness():
    # Test trustworthiness score.

    # Affine transformation
    X = random_state.randn(100, 2)
    assert trustworthiness(X, 5.0 + X / 10.0) == 1.0

    # Randomly shuffled
    X = np.arange(100).reshape(-1, 1)
    X_embedded = X.copy()
    random_state.shuffle(X_embedded)
    assert trustworthiness(X, X_embedded) < 0.6

    # Completely different
    X = np.arange(5).reshape(-1, 1)
    X_embedded = np.array([[0], [2], [4], [1], [3]])
    assert_almost_equal(trustworthiness(X, X_embedded, n_neighbors=1), 0.2)

def test_trustworthiness_n_neighbors_error():
    """Raise an error when n_neighbors >= n_samples / 2.

    Non-regression test for #18567.
    """
    regex = "n_neighbors .+ should be less than .+"
    X = rng.rand(7, 4)
    X_embedded = rng.rand(7, 2)
        trustworthiness(X, X_embedded, n_neighbors=5)

    trust = trustworthiness(X, X_embedded, n_neighbors=3)
    assert 0 <= trust <= 1

def test_preserve_trustworthiness_approximately(method, init):
    # Nearest neighbors should be preserved approximately.
    X = random_state.randn(50, n_components).astype(np.float32)
    tsne = TSNE(
        max_iter=700,
    )
    X_embedded = tsne.fit_transform(X)
    t = trustworthiness(X, X_embedded, n_neighbors=1)
    assert t > 0.85

def test_optimization_minimizes_kl_divergence():
    """t-SNE should give a lower KL divergence with more iterations."""
    X, _ = make_blobs(n_features=3, random_state=random_state)
    kl_divergences = []
    for max_iter in [250, 300, 350]:
        tsne = TSNE(
            n_components=2,
            perplexity=10,
            learning_rate=100.0,
        )
        tsne.fit_transform(X)
        kl_divergences.append(tsne.kl_divergence_)
    assert kl_divergences[1] <= kl_divergences[0]
    assert kl_divergences[2] <= kl_divergences[1]

def test_fit_transform_csr_matrix(method, csr_container):
    # X can be a sparse matrix.
    X = rng.randn(50, 2)
    X[(rng.randint(0, 50, 25), rng.randint(0, 2, 25))] = 0.0
        perplexity=10,
        learning_rate=100.0,
        max_iter=750,
    )
    X_embedded = tsne.fit_transform(X_csr)
    assert_allclose(trustworthiness(X_csr, X_embedded, n_neighbors=1), 1.0, rtol=1.1e-1)

def test_preserve_trustworthiness_approximately_with_precomputed_distances():
        X = random_state.randn(80, 2)
        D = squareform(pdist(X), "sqeuclidean")
            perplexity=2,
            early_exaggeration=2.0,
            random_state=i,
            verbose=0,
            max_iter=500,
        )
        X_embedded = tsne.fit_transform(D)
        t = trustworthiness(D, X_embedded, n_neighbors=1, metric="precomputed")
        assert t > 0.95

def test_trustworthiness_not_euclidean_metric():
    # Test trustworthiness with a metric different from 'euclidean' and
    # 'precomputed'
    assert trustworthiness(X, X, metric="cosine") == trustworthiness(
        pairwise_distances(X, metric="cosine"), X, metric="precomputed"
    )

def test_bad_precomputed_distances(method, D, retype, message_regex):
        metric="precomputed",
        perplexity=1,
    )
    with pytest.raises(ValueError, match=message_regex):
        tsne.fit_transform(retype(D))

def test_exact_no_precomputed_sparse(csr_container):
        method="exact",
    )
    with pytest.raises(TypeError, match="sparse"):
        tsne.fit_transform(csr_container([[0, 5], [5, 0]]))

def test_high_perplexity_precomputed_sparse_distances(csr_container):
    # Perplexity should be less than 50
    dist = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]])
    bad_dist = csr_container(dist)
    tsne = TSNE(metric="precomputed", init="random", random_state=42, perplexity=1)
    msg = "3 neighbors per samples are required, but some samples have only 1"
        tsne.fit_transform(bad_dist)

def test_sparse_precomputed_distance(sparse_container):
    """Make sure that TSNE works identically for sparse and dense matrix"""

    D_sparse = kneighbors_graph(X, n_neighbors=100, mode="distance", include_self=True)
    assert sp.issparse(D_sparse)
    assert_almost_equal(D_sparse.toarray(), D)

        metric="precomputed", random_state=0, init="random", learning_rate="auto"
    )
    Xt_dense = tsne.fit_transform(D)

    Xt_sparse = tsne.fit_transform(sparse_container(D_sparse))
    assert_almost_equal(Xt_dense, Xt_sparse)

def test_non_positive_computed_distances():
    # Computed distance matrices must be positive.
    def metric(x, y):
        return -1

    # Negative computed distances should be caught even if result is squared
    tsne = TSNE(metric=metric, method="exact", perplexity=1)
    X = np.array([[0.0, 0.0], [1.0, 1.0]])
    with pytest.raises(ValueError, match="All distances .*metric given.*"):

def test_init_ndarray():
    # Initialize TSNE with ndarray and test fit
    tsne = TSNE(init=np.zeros((100, 2)), learning_rate="auto")
    X_embedded = tsne.fit_transform(np.ones((100, 5)))
    assert_array_equal(np.zeros((100, 2)), X_embedded)

def test_init_ndarray_precomputed():
    # Initialize TSNE with ndarray and metric 'precomputed'
    # Make sure no FutureWarning is thrown from _fit
        init=np.zeros((100, 2)),
        learning_rate=50.0,
    )
    tsne.fit(np.zeros((100, 100)))

def test_pca_initialization_not_compatible_with_precomputed_kernel():
    # Precomputed distance matrices cannot use PCA initialization.
    tsne = TSNE(metric="precomputed", init="pca", perplexity=1)
        match='The parameter init="pca" cannot be used with metric="precomputed".',
    ):
        tsne.fit_transform(np.array([[0.0], [1.0]]))

def test_pca_initialization_not_compatible_with_sparse_input(csr_container):
    # Sparse input matrices cannot use PCA initialization.
    tsne = TSNE(init="pca", learning_rate=100.0, perplexity=1)
    with pytest.raises(TypeError, match="PCA initialization.*"):

def test_n_components_range():
    # barnes_hut method should only be used with n_components <= 3
    tsne = TSNE(n_components=4, method="barnes_hut", perplexity=1)
    with pytest.raises(ValueError, match="'n_components' should be .*"):

def test_early_exaggeration_used():
    # check that the ``early_exaggeration`` parameter has an effect
    methods = ["exact", "barnes_hut"]
    X = random_state.randn(25, n_components).astype(np.float32)
    for method in methods:
            perplexity=1,
            init="pca",
            early_exaggeration=1.0,
            max_iter=250,
        )
        X_embedded1 = tsne.fit_transform(X)
            early_exaggeration=10.0,
        )
        X_embedded2 = tsne.fit_transform(X)

        assert not np.allclose(X_embedded1, X_embedded2)

def test_max_iter_used():
    # check that the ``max_iter`` parameter has an effect
        for max_iter in [251, 500]:
            tsne = TSNE(
                perplexity=1,
                learning_rate=0.5,
                method=method,
                early_exaggeration=1.0,
            )
            tsne.fit_transform(X)

            assert tsne.n_iter_ == max_iter - 1

def test_answer_gradient_two_points(csr_container):
    # Test the tree with only a single set of children.
    #
    # These tests & answers have been checked against the reference
    # implementation by LvdM.
    pos_input = np.array([[1.0, 0.0], [0.0, 1.0]])
    pos_output = np.array(
        [[-4.961291e-05, -1.072243e-04], [9.259460e-05, 2.702024e-04]]
    )
    neighbors = np.array([[1], [0]])
    grad_output = np.array(
        [[-2.37012478e-05, -6.29044398e-05], [2.37012478e-05, 6.29044398e-05]]
    )
    _run_answer_test(pos_input, pos_output, neighbors, grad_output, csr_container)

def test_answer_gradient_four_points(csr_container):
    # Four points tests the tree with multiple levels of children.
    #
    pos_input = np.array([[1.0, 0.0], [0.0, 1.0], [5.0, 2.0], [7.3, 2.2]])
        [
            [6.080564e-05, -7.120823e-05],
            [-1.718945e-04, -4.000536e-05],
            [-2.271720e-04, 8.663310e-05],
            [-1.032577e-04, -3.582033e-05],
        ]
    )
    neighbors = np.array([[1, 2, 3], [0, 2, 3], [1, 0, 3], [1, 2, 0]])
        [
            [5.81128448e-05, -7.78033454e-06],
            [-5.81526851e-05, 7.80976444e-06],
            [4.24275173e-08, -3.69569698e-08],
            [-2.58720939e-09, 7.52706374e-09],
        ]
    )

def test_skip_num_points_gradient(csr_container):
    # Test the kwargs option skip_num_points.
    #
    # Skip num points should make it such that the Barnes_hut gradient
    # is not calculated for indices below skip_num_point.
    # Aside from skip_num_points=2 and the first two gradient rows
    # being set to zero, these data points are the same as in
    # test_answer_gradient_four_points()
        [
        ]
    )
        [
        ]
    )
    _run_answer_test(
        pos_input, pos_output, neighbors, grad_output, csr_container, False, 0.1, 2
    )

def _run_answer_test(
    pos_input,
    pos_output,
    neighbors,
    grad_output,
    perplexity=0.1,
):
    distances = pairwise_distances(pos_input).astype(np.float32)
    args = distances, perplexity, verbose
    pos_output = pos_output.astype(np.float32)
    neighbors = neighbors.astype(np.int64, copy=False)
    pij_input = _joint_probabilities(*args)
    pij_input = squareform(pij_input).astype(np.float32)
    grad_bh = np.zeros(pos_output.shape, dtype=np.float32)

    P = csr_container(pij_input)

    neighbors = P.indices.astype(np.int64)
    indptr = P.indptr.astype(np.int64)

    _barnes_hut_tsne.gradient(
        P.data, pos_output, neighbors, indptr, grad_bh, 0.5, 2, 1, skip_num_points=0
    )
    assert_array_almost_equal(grad_bh, grad_output, decimal=4)

def test_verbose():
    # Verbose options write to stdout.
    tsne = TSNE(verbose=2, perplexity=4)
    X = random_state.randn(5, 2)

    try:
    finally:

    assert "[t-SNE]" in out
    assert "nearest neighbors..." in out
    assert "Computed conditional probabilities" in out
    assert "Mean sigma" in out
    assert "early exaggeration" in out

def test_chebyshev_metric():
    # t-SNE should allow metrics that cannot be squared (issue #3526).
    tsne = TSNE(metric="chebyshev", perplexity=4)
    tsne.fit_transform(X)

def test_reduction_to_one_component():
    # t-SNE should allow reduction to one component (issue #4154).
    tsne = TSNE(n_components=1, perplexity=4)
    X_embedded = tsne.fit(X).embedding_
    assert np.all(np.isfinite(X_embedded))

def test_64bit(method, dt):
    # Ensure 64bit arrays are handled correctly.

    X = random_state.randn(10, 2).astype(dt, copy=False)
        perplexity=2,
    )
    effective_type = X_embedded.dtype

    # tsne cython code is only single precision, so the output will
    # always be single precision, irrespectively of the input dtype
    assert effective_type == np.float32

def test_kl_divergence_not_nan(method):
    # Ensure kl_divergence_ is computed at last iteration
    # even though max_iter % n_iter_check != 0, i.e. 1003 % 50 != 0

    X = random_state.randn(50, 2)
        max_iter=503,
    )

    assert not np.isnan(tsne.kl_divergence_)

def test_barnes_hut_angle():
    # When Barnes-Hut's angle=0 this corresponds to the exact method.
    angle = 0.0
    perplexity = 10
    for n_components in [2, 3]:
        n_features = 5
        degrees_of_freedom = float(n_components - 1.0)

        random_state = check_random_state(0)
        data = random_state.randn(n_samples, n_features)
        distances = pairwise_distances(data)
        params = random_state.randn(n_samples, n_components)
        P = _joint_probabilities(distances, perplexity, verbose=0)
        kl_exact, grad_exact = _kl_divergence(
            params, P, degrees_of_freedom, n_samples, n_components
        )

        n_neighbors = n_samples - 1
        distances_csr = (
            NearestNeighbors()
            .fit(data)
            .kneighbors_graph(n_neighbors=n_neighbors, mode="distance")
        )
        P_bh = _joint_probabilities_nn(distances_csr, perplexity, verbose=0)
        kl_bh, grad_bh = _kl_divergence_bh(
            params,
            P_bh,
            angle=angle,
            skip_num_points=0,
        )

        P = squareform(P)
        P_bh = P_bh.toarray()
        assert_array_almost_equal(P_bh, P, decimal=5)
        assert_almost_equal(kl_exact, kl_bh, decimal=3)

def test_n_iter_without_progress():
    # Use a dummy negative n_iter_without_progress and check output on stdout
    X = random_state.randn(100, 10)
    for method in ["barnes_hut", "exact"]:
            n_iter_without_progress=-1,
            learning_rate=1e8,
            max_iter=351,
        )
        tsne._N_ITER_CHECK = 1
        tsne._EXPLORATION_MAX_ITER = 0

        old_stdout = sys.stdout
        try:
        finally:
            out = sys.stdout.getvalue()
            sys.stdout.close()
            sys.stdout = old_stdout

        # The output needs to contain the value of n_iter_without_progress
        assert "did not make any progress during the last -1 episodes. Finished." in out

def test_min_grad_norm():
    # Make sure that the parameter min_grad_norm is used correctly
    min_grad_norm = 0.002
    tsne = TSNE(min_grad_norm=min_grad_norm, verbose=2, random_state=0, method="exact")

    try:
    finally:

    lines_out = out.split("\n")

    # extract the gradient norm from the verbose output
    gradient_norm_values = []
    for line in lines_out:
        # When the computation is Finished just an old gradient norm value
        # is repeated that we do not need to store
        if "Finished" in line:
            break

        start_grad_norm = line.find("gradient norm")
        if start_grad_norm >= 0:
            line = line[start_grad_norm:]
            line = line.replace("gradient norm = ", "").split(" ")[0]
            gradient_norm_values.append(float(line))

    # Compute how often the gradient norm is smaller than min_grad_norm
    gradient_norm_values = np.array(gradient_norm_values)
    n_smaller_gradient_norms = len(
        gradient_norm_values[gradient_norm_values <= min_grad_norm]
    )

    # The gradient norm can be smaller than min_grad_norm at most once,
    # because in the moment it becomes smaller the optimization stops
    assert n_smaller_gradient_norms <= 1

def test_accessible_kl_divergence():
    # Ensures that the accessible kl_divergence matches the computed value
        n_iter_without_progress=2,
        verbose=2,
    )

    try:
    finally:

    # The output needs to contain the accessible kl_divergence as the error at
    # the last iteration
    for line in out.split("\n")[::-1]:
        if "Iteration" in line:
            _, _, error = line.partition("error = ")
            if error:
                error, _, _ = error.partition(",")
                break
    assert_almost_equal(tsne.kl_divergence_, float(error), decimal=5)

def test_uniform_grid(method):
    """Make sure that TSNE can approximately recover a uniform 2D grid

    Due to ties in distances between point in X_2d_grid, this test is platform
    dependent for ``method='barnes_hut'`` due to numerical imprecision.

    Also, t-SNE is not assured to converge to the right solution because bad
    initialization can lead to convergence to bad local minimum (the
    optimization problem is non-convex). To avoid breaking the test too often,
    we re-run t-SNE from the final point when the convergence is not good
    enough.
    """
    seeds = range(3)
            perplexity=50,
            learning_rate="auto",
        )
        Y = tsne.fit_transform(X_2d_grid)

        try_name = "{}_{}".format(method, seed)
        try:
            assert_uniform_grid(Y, try_name)
        except AssertionError:
            # If the test fails a first time, re-run with init=Y to see if
            # this was caused by a bad initialization. Note that this will
            # also run an early_exaggeration step.
            try_name += ":rerun"
            tsne.init = Y
            Y = tsne.fit_transform(X_2d_grid)

def assert_uniform_grid(Y, try_name=None):
    # Ensure that the resulting embedding leads to approximately
    # uniformly spaced points: the distance to the closest neighbors
    # should be non-zero and approximately constant.
    nn = NearestNeighbors(n_neighbors=1).fit(Y)
    dist_to_nn = nn.kneighbors(return_distance=True)[0].ravel()
    assert dist_to_nn.min() > 0.1

    smallest_to_mean = dist_to_nn.min() / np.mean(dist_to_nn)
    largest_to_mean = dist_to_nn.max() / np.mean(dist_to_nn)

    assert smallest_to_mean > 0.5, try_name
    assert largest_to_mean < 2, try_name

def test_bh_match_exact():
    # check that the ``barnes_hut`` method match the exact one when
    # ``angle = 0`` and ``perplexity > n_samples / 3``
    X = random_state.randn(30, n_features).astype(np.float32)
    X_embeddeds = {}
    max_iter = {}
    for method in ["exact", "barnes_hut"]:
            learning_rate=1.0,
            max_iter=251,
            perplexity=29.5,
            angle=0,
        )
        # Kill the early_exaggeration
        X_embeddeds[method] = tsne.fit_transform(X)
        max_iter[method] = tsne.n_iter_

    assert max_iter["exact"] == max_iter["barnes_hut"]
    assert_allclose(X_embeddeds["exact"], X_embeddeds["barnes_hut"], rtol=1e-4)

def test_gradient_bh_multithread_match_sequential():
    # check that the bh gradient with different num_threads gives the same
    # results

    degrees_of_freedom = 1

    angle = 3

    data = random_state.randn(n_samples, n_features).astype(np.float32)
    params = random_state.randn(n_samples, n_components)

    distances_csr = (
        NearestNeighbors()
        .fit(data)
        .kneighbors_graph(n_neighbors=n_neighbors, mode="distance")
    )
    P_bh = _joint_probabilities_nn(distances_csr, perplexity, verbose=0)
    kl_sequential, grad_sequential = _kl_divergence_bh(
        params,
        P_bh,
        angle=angle,
        num_threads=1,
    )
    for num_threads in [2, 4]:
        kl_multithread, grad_multithread = _kl_divergence_bh(
            params,
            P_bh,
            num_threads=num_threads,
        )

        assert_allclose(kl_multithread, kl_sequential, rtol=1e-6)
        assert_allclose(grad_multithread, grad_multithread)

def test_tsne_with_different_distance_metrics(metric, dist_func, method):
    """Make sure that TSNE works for different distance metrics"""

    if method == "barnes_hut" and metric == "manhattan":
        # The distances computed by `manhattan_distances` differ slightly from those
        # computed internally by NearestNeighbors via the PairwiseDistancesReduction
        # Cython code-based. This in turns causes T-SNE to converge to a different
        # solution but this should not impact the qualitative results as both
        # methods.
        # NOTE: it's probably not valid from a mathematical point of view to use the
        # Manhattan distance for T-SNE...
        # TODO: re-enable this test if/when `manhattan_distances` is refactored to
        # reuse the same underlying Cython code NearestNeighbors.
        # For reference, see:
        # https://github.com/scikit-learn/scikit-learn/pull/23865/files#r925721573
            "Distance computations are different for method == 'barnes_hut' and metric"
            " == 'manhattan', but this is expected."
        )

    n_components_original = 3
    n_components_embedding = 2
    X = random_state.randn(50, n_components_original).astype(np.float32)
    X_transformed_tsne = TSNE(
        n_components=n_components_embedding,
    X_transformed_tsne_precomputed = TSNE(
    ).fit_transform(dist_func(X))
    assert_array_equal(X_transformed_tsne, X_transformed_tsne_precomputed)

def test_tsne_n_jobs(method):
    """Make sure that the n_jobs parameter doesn't impact the output"""
    X = random_state.randn(30, n_features)
    X_tr_ref = TSNE(
        perplexity=25.0,
        angle=0,
        n_jobs=1,
    X_tr = TSNE(
        angle=0,
        n_jobs=2,

    assert_allclose(X_tr_ref, X_tr)

def test_tsne_with_mahalanobis_distance():
    """Make sure that method_parameters works with mahalanobis distance."""
    n_samples, n_features = 300, 10
    X = random_state.randn(n_samples, n_features)
    default_params = {
        "perplexity": 40,
        "max_iter": 250,
        "learning_rate": "auto",
        "init": "random",
        "n_components": 3,
        "random_state": 0,
    }

    tsne = TSNE(metric="mahalanobis", **default_params)
    msg = "Must provide either V or VI for Mahalanobis distance"

    precomputed_X = squareform(pdist(X, metric="mahalanobis"), checks=True)
    X_trans_expected = TSNE(metric="precomputed", **default_params).fit_transform(
        precomputed_X
    )

    X_trans = TSNE(
        metric="mahalanobis", metric_params={"V": np.cov(X.T)}, **default_params
    assert_allclose(X_trans, X_trans_expected)

def test_tsne_perplexity_validation(perplexity):
    """Make sure that perplexity > n_samples results in a ValueError"""

    X = random_state.randn(20, 2)
    est = TSNE(
        perplexity=perplexity,
    )
    msg = re.escape(f"perplexity ({perplexity}) must be less than n_samples (20)")
        est.fit_transform(X)

def test_tsne_works_with_pandas_output():
    """Make sure that TSNE works when the output is set to "pandas".

    Non-regression test for gh-25365.
    """
    with config_context(transform_output="pandas"):
        arr = np.arange(35 * 4).reshape(35, 4)
        TSNE(n_components=2).fit_transform(arr)

def test_imports_strategies():
    # Make sure different import strategies work or fail as expected.

    # Since Python caches the imported modules, we need to run a child process
    # for every test case. Else, the tests would not be independent
    # (manually removing the imports from the cache (sys.modules) is not
    # recommended and can lead to many complications).
    pattern = "IterativeImputer is experimental"
    good_import = """
    from sklearn.experimental import enable_iterative_imputer
    from sklearn.impute import IterativeImputer
    """
    assert_run_python_script_without_output(
        textwrap.dedent(good_import), pattern=pattern
    )

    good_import_with_ensemble_first = """
    import sklearn.ensemble
    """
        textwrap.dedent(good_import_with_ensemble_first),
        pattern=pattern,
    )

    bad_imports = f"""
    import pytest

    with pytest.raises(ImportError, match={pattern!r}):
        from sklearn.impute import IterativeImputer

    import sklearn.experimental
    """
        textwrap.dedent(bad_imports),
    )

    pattern = "Halving(Grid|Random)SearchCV is experimental"
    from sklearn.experimental import enable_halving_search_cv
    from sklearn.model_selection import HalvingGridSearchCV
    from sklearn.model_selection import HalvingRandomSearchCV
    """
    )

    good_import_with_model_selection_first = """
    import sklearn.model_selection
    """
        textwrap.dedent(good_import_with_model_selection_first),
    )

        from sklearn.model_selection import HalvingGridSearchCV

        from sklearn.model_selection import HalvingRandomSearchCV
    """
    )

def test_import_raises_warning():
    code = """
    with pytest.warns(UserWarning, match="it is not needed to import"):
        from sklearn.experimental import enable_hist_gradient_boosting  # noqa
    """
    pattern = "it is not needed to import enable_hist_gradient_boosting anymore"
    assert_run_python_script_without_output(textwrap.dedent(code), pattern=pattern)

def l1_min_c(X, y, *, loss="squared_hinge", fit_intercept=True, intercept_scaling=1.0):
    """Return the lowest bound for `C`.

    The lower bound for `C` is computed such that for `C` in `(l1_min_C, infinity)`
    the model is guaranteed not to be empty. This applies to l1 penalized
    classifiers, such as :class:`sklearn.svm.LinearSVC` with penalty='l1' and
    :class:`sklearn.linear_model.LogisticRegression` with penalty='l1'.

    This value is valid if `class_weight` parameter in `fit()` is not set.

    For an example of how to use this function, see
    :ref:`sphx_glr_auto_examples_linear_model_plot_logistic_path.py`.

        Target vector relative to X.

    loss : {'squared_hinge', 'log'}, default='squared_hinge'
        Specifies the loss function.
        With 'squared_hinge' it is the squared hinge loss (a.k.a. L2 loss).
        With 'log' it is the loss of logistic regression models.

    fit_intercept : bool, default=True
        Specifies if the intercept should be fitted by the model.
        It must match the fit() method parameter.

    intercept_scaling : float, default=1.0
        When fit_intercept is True, instance vector x becomes
        [x, intercept_scaling],
        i.e. a "synthetic" feature with constant value equals to
        intercept_scaling is appended to the instance vector.

    Returns
    -------
    l1_min_c : float
        Minimum value for C.

    Examples
    --------
    >>> from sklearn.svm import l1_min_c
    >>> X, y = make_classification(n_samples=100, n_features=20, random_state=42)
    >>> print(f"{l1_min_c(X, y, loss='squared_hinge', fit_intercept=True):.4f}")
    0.0044
    """

    X = check_array(X, accept_sparse="csc")
    check_consistent_length(X, y)

    Y = LabelBinarizer(neg_label=-1).fit_transform(y).T
    # maximum absolute value over classes and features
    den = np.max(np.abs(safe_sparse_dot(Y, X)))
    if fit_intercept:
        bias = np.full(
            (np.size(y), 1), intercept_scaling, dtype=np.array(intercept_scaling).dtype
        )
        den = max(den, abs(np.dot(Y, bias)).max())

    if den == 0.0:
            "Ill-posed l1_min_c calculation: l1 will always "
            "select zero coefficients for this data"
        )
    if loss == "squared_hinge":
        return 0.5 / den
    else:  # loss == 'log':
        return 2.0 / den

def _validate_dual_parameter(dual, loss, penalty, multi_class, X):
    """Helper function to assign the value of dual parameter."""
    if dual == "auto":
        if X.shape[0] < X.shape[1]:
            try:
                _get_liblinear_solver_type(multi_class, penalty, loss, True)
                return True
            except ValueError:  # dual not supported for the combination
        else:
            try:
                _get_liblinear_solver_type(multi_class, penalty, loss, False)
            except ValueError:  # primal not supported by the combination
    else:
        return dual

class LinearSVC(LinearClassifierMixin, SparseCoefMixin, BaseEstimator):
    """Linear Support Vector Classification.

    Similar to SVC with parameter kernel='linear', but implemented in terms of
    liblinear rather than libsvm, so it has more flexibility in the choice of
    penalties and loss functions and should scale better to large numbers of
    samples.

    The main differences between :class:`~sklearn.svm.LinearSVC` and
    :class:`~sklearn.svm.SVC` lie in the loss function used by default, and in
    the handling of intercept regularization between those two implementations.

    This class supports both dense and sparse input and the multiclass support
    is handled according to a one-vs-the-rest scheme.

    Read more in the :ref:`User Guide <svm_classification>`.

    penalty : {'l1', 'l2'}, default='l2'
        Specifies the norm used in the penalization. The 'l2'
        penalty is the standard used in SVC. The 'l1' leads to ``coef_``
        vectors that are sparse.

    loss : {'hinge', 'squared_hinge'}, default='squared_hinge'
        Specifies the loss function. 'hinge' is the standard SVM loss
        (used e.g. by the SVC class) while 'squared_hinge' is the
        square of the hinge loss. The combination of ``penalty='l1'``
        and ``loss='hinge'`` is not supported.

    dual : "auto" or bool, default="auto"
        Select the algorithm to either solve the dual or primal
        optimization problem. Prefer dual=False when n_samples > n_features.
        `dual="auto"` will choose the value of the parameter automatically,
        based on the values of `n_samples`, `n_features`, `loss`, `multi_class`
        and `penalty`. If `n_samples` < `n_features` and optimizer supports
        chosen `loss`, `multi_class` and `penalty`, then dual will be set to True,
        otherwise it will be set to False.

           The `"auto"` option is added in version 1.3 and will be the default
           in version 1.5.

        Tolerance for stopping criteria.

    C : float, default=1.0
        Regularization parameter. The strength of the regularization is
        inversely proportional to C. Must be strictly positive.
        For an intuitive visualization of the effects of scaling
        the regularization parameter C, see
        :ref:`sphx_glr_auto_examples_svm_plot_svm_scale_c.py`.

    multi_class : {'ovr', 'crammer_singer'}, default='ovr'
        Determines the multi-class strategy if `y` contains more than
        two classes.
        ``"ovr"`` trains n_classes one-vs-rest classifiers, while
        ``"crammer_singer"`` optimizes a joint objective over all classes.
        While `crammer_singer` is interesting from a theoretical perspective
        as it is consistent, it is seldom used in practice as it rarely leads
        to better accuracy and is more expensive to compute.
        If ``"crammer_singer"`` is chosen, the options loss, penalty and dual
        will be ignored.

        Whether or not to fit an intercept. If set to True, the feature vector
        is extended to include an intercept term: `[x_1, ..., x_n, 1]`, where
        1 corresponds to the intercept. If set to False, no intercept will be
        used in calculations (i.e. data is expected to be already centered).

        When `fit_intercept` is True, the instance vector x becomes ``[x_1,
        ..., x_n, intercept_scaling]``, i.e. a "synthetic" feature with a
        constant value equal to `intercept_scaling` is appended to the instance
        vector. The intercept becomes intercept_scaling * synthetic feature
        weight. Note that liblinear internally penalizes the intercept,
        treating it like any other term in the feature vector. To reduce the
        impact of the regularization on the intercept, the `intercept_scaling`
        parameter can be set to a value greater than 1; the higher the value of
        `intercept_scaling`, the lower the impact of regularization on it.
        Then, the weights become `[w_x_1, ..., w_x_n,
        w_intercept*intercept_scaling]`, where `w_x_1, ..., w_x_n` represent
        the feature weights and the intercept weight is scaled by
        `intercept_scaling`. This scaling allows the intercept term to have a
        different regularization behavior compared to the other features.

        Set the parameter C of class i to ``class_weight[i]*C`` for
        SVC. If not given, all classes are supposed to have
        weight one.
        as ``n_samples / (n_classes * np.bincount(y))``.

        Enable verbose output. Note that this setting takes advantage of a
        per-process runtime setting in liblinear that, if enabled, may not work
        properly in a multithreaded context.

        Controls the pseudo random number generation for shuffling the data for
        the dual coordinate descent (if ``dual=True``). When ``dual=False`` the
        underlying implementation of :class:`LinearSVC` is not random and
        ``random_state`` has no effect on the results.

        The maximum number of iterations to be run.

    coef_ : ndarray of shape (1, n_features) if n_classes == 2 \
            else (n_classes, n_features)
        Weights assigned to the features (coefficients in the primal
        problem).

        ``coef_`` is a readonly property derived from ``raw_coef_`` that
        follows the internal memory layout of liblinear.

    intercept_ : ndarray of shape (1,) if n_classes == 2 else (n_classes,)
        Constants in decision function.

        Maximum number of iterations run across all classes.

    See Also
    --------
    SVC : Implementation of Support Vector Machine classifier using libsvm:
        the kernel can be non-linear but its SMO algorithm does not
        scale to large number of samples as LinearSVC does.

        Furthermore SVC multi-class mode is implemented using one
        vs one scheme while LinearSVC uses one vs the rest. It is
        possible to implement one vs the rest with SVC by using the
        :class:`~sklearn.multiclass.OneVsRestClassifier` wrapper.

        Finally SVC can fit dense data without memory copy if the input
        is C-contiguous. Sparse data will still incur memory copy though.

    sklearn.linear_model.SGDClassifier : SGDClassifier can optimize the same
        cost function as LinearSVC
        by adjusting the penalty and loss parameters. In addition it requires
        less memory, allows incremental (online) learning, and implements
        various loss functions and regularization regimes.

    Notes
    -----
    The underlying C implementation uses a random number generator to
    select features when fitting the model. It is thus not uncommon
    to have slightly different results for the same input data. If
    that happens, try with a smaller ``tol`` parameter.

    The underlying implementation, liblinear, uses a sparse internal
    representation for the data that will incur a memory copy.

    Predict output may not match that of standalone liblinear in certain
    cases. See :ref:`differences from liblinear <liblinear_differences>`
    in the narrative documentation.

    `LIBLINEAR: A Library for Large Linear Classification
    <https://www.csie.ntu.edu.tw/~cjlin/liblinear/>`__

    Examples
    --------
    >>> clf = make_pipeline(StandardScaler(),
    ...                     LinearSVC(random_state=0, tol=1e-5))
                    ('linearsvc', LinearSVC(random_state=0, tol=1e-05))])

    >>> print(clf.named_steps['linearsvc'].coef_)
    [[0.141...   0.526... 0.679... 0.493...]]

    >>> print(clf.named_steps['linearsvc'].intercept_)
    [0.1693...]
    [1]
    """

        "penalty": [StrOptions({"l1", "l2"})],
        "loss": [StrOptions({"hinge", "squared_hinge"})],
        "dual": ["boolean", StrOptions({"auto"})],
        "tol": [Interval(Real, 0.0, None, closed="neither")],
        "C": [Interval(Real, 0.0, None, closed="neither")],
        "multi_class": [StrOptions({"ovr", "crammer_singer"})],
        "fit_intercept": ["boolean"],
        "intercept_scaling": [Interval(Real, 0, None, closed="neither")],
        "class_weight": [None, dict, StrOptions({"balanced"})],
    }

        self,
        penalty="l2",
        loss="squared_hinge",
        *,
        dual="auto",
        tol=1e-4,
        C=1.0,
        multi_class="ovr",
        fit_intercept=True,
        intercept_scaling=1,
    ):
        self.dual = dual
        self.C = C
        self.fit_intercept = fit_intercept
        self.intercept_scaling = intercept_scaling
        self.penalty = penalty

            Target vector relative to X.

            Array of weights that are assigned to individual
            samples. If not provided,
            then each sample is given unit weight.

            .. versionadded:: 0.18

        Returns
        -------
            An instance of the estimator.
        """
            self,
            X,
            y,
            order="C",
            accept_large_sparse=False,
        )

        _dual = _validate_dual_parameter(
            self.dual, self.loss, self.penalty, self.multi_class, X
        )

        self.coef_, self.intercept_, n_iter_ = _fit_liblinear(
            X,
            y,
            self.C,
            self.fit_intercept,
            self.intercept_scaling,
            self.class_weight,
            self.penalty,
            _dual,
            self.verbose,
            self.max_iter,
            self.tol,
            self.random_state,
            self.multi_class,
            self.loss,
        )
        # Backward compatibility: _fit_liblinear is used both by LinearSVC/R
        # and LogisticRegression but LogisticRegression sets a structured
        # `n_iter_` attribute with information about the underlying OvR fits
        # while LinearSVC/R only reports the maximum value.
        self.n_iter_ = n_iter_.max().item()

        if self.multi_class == "crammer_singer" and len(self.classes_) == 2:
            self.coef_ = (self.coef_[1] - self.coef_[0]).reshape(1, -1)
            if self.fit_intercept:
                intercept = self.intercept_[1] - self.intercept_[0]
                self.intercept_ = np.array([intercept])

class LinearSVR(RegressorMixin, LinearModel):
    """Linear Support Vector Regression.

    Similar to SVR with parameter kernel='linear', but implemented in terms of
    samples.

    The main differences between :class:`~sklearn.svm.LinearSVR` and
    :class:`~sklearn.svm.SVR` lie in the loss function used by default, and in

    This class supports both dense and sparse input.

    Read more in the :ref:`User Guide <svm_regression>`.

    epsilon : float, default=0.0
        Epsilon parameter in the epsilon-insensitive loss function. Note
        that the value of this parameter depends on the scale of the target
        variable y. If unsure, set ``epsilon=0``.

    loss : {'epsilon_insensitive', 'squared_epsilon_insensitive'}, \
            default='epsilon_insensitive'
        Specifies the loss function. The epsilon-insensitive loss
        (standard SVR) is the L1 loss, while the squared epsilon-insensitive
        loss ('squared_epsilon_insensitive') is the L2 loss.

        When `fit_intercept` is True, the instance vector x becomes `[x_1, ...,
        x_n, intercept_scaling]`, i.e. a "synthetic" feature with a constant
        value equal to `intercept_scaling` is appended to the instance vector.
        The intercept becomes intercept_scaling * synthetic feature weight.
        Note that liblinear internally penalizes the intercept, treating it
        like any other term in the feature vector. To reduce the impact of the
        regularization on the intercept, the `intercept_scaling` parameter can
        be set to a value greater than 1; the higher the value of

        based on the values of `n_samples`, `n_features` and `loss`. If
        `n_samples` < `n_features` and optimizer supports chosen `loss`,
        then dual will be set to True, otherwise it will be set to False.

        Controls the pseudo random number generation for shuffling the data.

    coef_ : ndarray of shape (n_features) if n_classes == 2 \
        problem).

        `coef_` is a readonly property derived from `raw_coef_` that

    intercept_ : ndarray of shape (1) if n_classes == 2 else (n_classes)

    See Also
    --------
    LinearSVC : Implementation of Support Vector Machine classifier using the
        same library as this class (liblinear).

    SVR : Implementation of Support Vector Machine regression using libsvm:
        the kernel can be non-linear but its SMO algorithm does not scale to
        large number of samples as :class:`~sklearn.svm.LinearSVR` does.

    sklearn.linear_model.SGDRegressor : SGDRegressor can optimize the same cost
        function as LinearSVR

    Examples
    --------
    >>> X, y = make_regression(n_features=4, random_state=0)
    >>> regr = make_pipeline(StandardScaler(),
    ...                      LinearSVR(random_state=0, tol=1e-5))
                    ('linearsvr', LinearSVR(random_state=0, tol=1e-05))])

    >>> print(regr.named_steps['linearsvr'].coef_)
    [18.582... 27.023... 44.357... 64.522...]
    >>> print(regr.named_steps['linearsvr'].intercept_)
    [-4...]
    [-2.384...]
    """

        "epsilon": [Real],
        "loss": [StrOptions({"epsilon_insensitive", "squared_epsilon_insensitive"})],
    }

        self,
        *,
        epsilon=0.0,
        tol=1e-4,
        C=1.0,
        loss="epsilon_insensitive",
        intercept_scaling=1.0,
    ):
        self.C = C

        Returns
        -------
        """
            self,
            X,
            y,
        )
        penalty = "l2"  # SVR only accepts l2 penalty

        _dual = _validate_dual_parameter(self.dual, self.loss, penalty, "ovr", X)

            X,
            y,
            self.C,
            None,
            penalty,
            _dual,
            self.tol,
            loss=self.loss,
            epsilon=self.epsilon,
        )
        self.coef_ = self.coef_.ravel()

class SVC(BaseSVC):
    """C-Support Vector Classification.

    The implementation is based on libsvm. The fit time scales at least
    quadratically with the number of samples and may be impractical
    beyond tens of thousands of samples. For large datasets
    consider using :class:`~sklearn.svm.LinearSVC` or
    :class:`~sklearn.linear_model.SGDClassifier` instead, possibly after a
    :class:`~sklearn.kernel_approximation.Nystroem` transformer or
    other :ref:`kernel_approximation`.

    The multiclass support is handled according to a one-vs-one scheme.

    For details on the precise mathematical formulation of the provided
    kernel functions and how `gamma`, `coef0` and `degree` affect each
    other, see the corresponding section in the narrative documentation:
    :ref:`svm_kernels`.

    To learn how to tune SVC's hyperparameters, see the following example:
    :ref:`sphx_glr_auto_examples_model_selection_plot_nested_cross_validation_iris.py`

        inversely proportional to C. Must be strictly positive. The penalty
        is a squared l2 penalty. For an intuitive visualization of the effects
        of scaling the regularization parameter C, see

    kernel : {'linear', 'poly', 'rbf', 'sigmoid', 'precomputed'} or callable,  \
        default='rbf'
        Specifies the kernel type to be used in the algorithm. If
        none is given, 'rbf' will be used. If a callable is given it is used to
        pre-compute the kernel matrix from data matrices; that matrix should be
        an array of shape ``(n_samples, n_samples)``. For an intuitive
        visualization of different kernel types see
        :ref:`sphx_glr_auto_examples_svm_plot_svm_kernels.py`.

    degree : int, default=3
        Degree of the polynomial kernel function ('poly').
        Must be non-negative. Ignored by all other kernels.

    gamma : {'scale', 'auto'} or float, default='scale'
        Kernel coefficient for 'rbf', 'poly' and 'sigmoid'.

        - if ``gamma='scale'`` (default) is passed then it uses
          1 / (n_features * X.var()) as value of gamma,
        - if 'auto', uses 1 / n_features
        - if float, must be non-negative.

           The default value of ``gamma`` changed from 'auto' to 'scale'.

    coef0 : float, default=0.0
        Independent term in kernel function.
        It is only significant in 'poly' and 'sigmoid'.

    shrinking : bool, default=True
        Whether to use the shrinking heuristic.
        See the :ref:`User Guide <shrinking_svm>`.

    probability : bool, default=False
        Whether to enable probability estimates. This must be enabled prior
        to calling `fit`, will slow down that method as it internally uses
        5-fold cross-validation, and `predict_proba` may be inconsistent with
        `predict`. Read more in the :ref:`User Guide <scores_probabilities>`.

        Tolerance for stopping criterion.

    cache_size : float, default=200
        Specify the size of the kernel cache (in MB).

        Set the parameter C of class i to class_weight[i]*C for

        per-process runtime setting in libsvm that, if enabled, may not work

    max_iter : int, default=-1
        Hard limit on iterations within solver, or -1 for no limit.

    decision_function_shape : {'ovo', 'ovr'}, default='ovr'
        Whether to return a one-vs-rest ('ovr') decision function of shape
        (n_samples, n_classes) as all other classifiers, or the original
        one-vs-one ('ovo') decision function of libsvm which has shape
        (n_samples, n_classes * (n_classes - 1) / 2). However, note that
        internally, one-vs-one ('ovo') is always used as a multi-class strategy
        to train models; an ovr matrix is only constructed from the ovo matrix.
        The parameter is ignored for binary classification.

            decision_function_shape is 'ovr' by default.

           *decision_function_shape='ovr'* is recommended.

           Deprecated *decision_function_shape='ovo' and None*.

    break_ties : bool, default=False
        If true, ``decision_function_shape='ovr'``, and number of classes > 2,
        :term:`predict` will break ties according to the confidence values of
        :term:`decision_function`; otherwise the first class among the tied
        classes is returned. Please note that breaking ties comes at a
        relatively high computational cost compared to a simple predict. See
        :ref:`sphx_glr_auto_examples_svm_plot_svm_tie_breaking.py` for an
        example of its usage with ``decision_function_shape='ovr'``.

        probability estimates. Ignored when `probability` is False.

    class_weight_ : ndarray of shape (n_classes,)
        Multipliers of parameter C for each class.
        Computed based on the ``class_weight`` parameter.

    coef_ : ndarray of shape (n_classes * (n_classes - 1) / 2, n_features)
        problem). This is only available in the case of a linear kernel.

        `coef_` is a readonly property derived from `dual_coef_` and
        `support_vectors_`.

    dual_coef_ : ndarray of shape (n_classes -1, n_SV)
        Dual coefficients of the support vector in the decision
        function (see :ref:`sgd_mathematical_formulation`), multiplied by
        their targets.
        For multiclass, coefficient for all 1-vs-1 classifiers.
        The layout of the coefficients in the multiclass case is somewhat
        non-trivial. See the :ref:`multi-class section of the User Guide
        <svm_multi_class>` for details.

    fit_status_ : int
        0 if correctly fitted, 1 otherwise (will raise warning)

    intercept_ : ndarray of shape (n_classes * (n_classes - 1) / 2,)

    n_iter_ : ndarray of shape (n_classes * (n_classes - 1) // 2,)
        Number of iterations run by the optimization routine to fit the model.
        The shape of this attribute depends on the number of models optimized
        which in turn depends on the number of classes.

    support_ : ndarray of shape (n_SV)
        Indices of support vectors.

    support_vectors_ : ndarray of shape (n_SV, n_features)
        Support vectors. An empty array if kernel is precomputed.

    n_support_ : ndarray of shape (n_classes,), dtype=int32
        Number of support vectors for each class.

    probA_ : ndarray of shape (n_classes * (n_classes - 1) / 2)
    probB_ : ndarray of shape (n_classes * (n_classes - 1) / 2)
        If `probability=True`, it corresponds to the parameters learned in
        Platt scaling to produce probability estimates from decision values.
        If `probability=False`, it's an empty array. Platt scaling uses the
        logistic function
        ``1 / (1 + exp(decision_value * probA_ + probB_))``
        where ``probA_`` and ``probB_`` are learned from the dataset [2]_. For
        more information on the multiclass case and training procedure see
        section 8 of [1]_.

    shape_fit_ : tuple of int of shape (n_dimensions_of_X,)
        Array dimensions of training vector ``X``.

    See Also
    --------
    SVR : Support Vector Machine for Regression implemented using libsvm.

    LinearSVC : Scalable Linear Support Vector Machine for classification
        implemented using liblinear. Check the See Also section of
        LinearSVC for more comparison element.

    .. [1] `LIBSVM: A Library for Support Vector Machines
        <http://www.csie.ntu.edu.tw/~cjlin/papers/libsvm.pdf>`_

    .. [2] `Platt, John (1999). "Probabilistic Outputs for Support Vector
        Machines and Comparisons to Regularized Likelihood Methods"
        <https://citeseerx.ist.psu.edu/doc_view/pid/42e5ed832d4310ce4378c44d05570439df28a393>`_

    Examples
    --------
    >>> X = np.array([[-1, -1], [-2, -1], [1, 1], [2, 1]])
    >>> clf = make_pipeline(StandardScaler(), SVC(gamma='auto'))
                    ('svc', SVC(gamma='auto'))])

    [1]

    For a comparison of the SVC with other classifiers see:
    """

    _impl = "c_svc"

        self,
        *,
        C=1.0,
        degree=3,
        gamma="scale",
        coef0=0.0,
        shrinking=True,
        probability=False,
        tol=1e-3,
        cache_size=200,
        max_iter=-1,
        decision_function_shape="ovr",
        break_ties=False,
    ):
            degree=degree,
            coef0=coef0,
            tol=tol,
            C=C,
            nu=0.0,
            shrinking=shrinking,
            probability=probability,
            cache_size=cache_size,
            decision_function_shape=decision_function_shape,
            break_ties=break_ties,
        )

class NuSVC(BaseSVC):
    """Nu-Support Vector Classification.

    Similar to SVC but uses a parameter to control the number of support
    vectors.

    The implementation is based on libsvm.

    nu : float, default=0.5
        An upper bound on the fraction of margin errors (see :ref:`User Guide
        <nu_svc>`) and a lower bound of the fraction of support vectors.
        Should be in the interval (0, 1].

        Specifies the kernel type to be used in the algorithm.
        If none is given, 'rbf' will be used. If a callable is given it is
        used to precompute the kernel matrix. For an intuitive

    class_weight : {dict, 'balanced'}, default=None
        weight one. The "balanced" mode uses the values of y to automatically
        adjust weights inversely proportional to class frequencies as
        ``n_samples / (n_classes * np.bincount(y))``.

        (n_samples, n_classes * (n_classes - 1) / 2). However, one-vs-one
        ('ovo') is always used as multi-class strategy. The parameter is
        ignored for binary classification.

        relatively high computational cost compared to a simple predict.
        See :ref:`sphx_glr_auto_examples_svm_plot_svm_tie_breaking.py` for an

        Multipliers of parameter C of each class.

    coef_ : ndarray of shape (n_classes * (n_classes -1) / 2, n_features)

        `coef_` is readonly property derived from `dual_coef_` and

    dual_coef_ : ndarray of shape (n_classes - 1, n_SV)

        0 if correctly fitted, 1 if the algorithm did not converge.

    support_ : ndarray of shape (n_SV,)

        Support vectors.

    probA_ : ndarray of shape (n_classes * (n_classes - 1) / 2,)

    probB_ : ndarray of shape (n_classes * (n_classes - 1) / 2,)

    See Also
    --------
    SVC : Support Vector Machine for classification using libsvm.

    LinearSVC : Scalable linear Support Vector Machine for classification using
        liblinear.

    Examples
    --------
    >>> from sklearn.svm import NuSVC
    >>> clf = make_pipeline(StandardScaler(), NuSVC())
    Pipeline(steps=[('standardscaler', StandardScaler()), ('nusvc', NuSVC())])
    [1]
    """

    _impl = "nu_svc"

        **BaseSVC._parameter_constraints,
        "nu": [Interval(Real, 0.0, 1.0, closed="right")],
    }
    _parameter_constraints.pop("C")

        self,
        *,
        nu=0.5,
        degree=3,
        tol=1e-3,
    ):
            tol=tol,
            C=0.0,
            nu=nu,
        )

class SVR(RegressorMixin, BaseLibSVM):
    """Epsilon-Support Vector Regression.

    The free parameters in the model are C and epsilon.

    The implementation is based on libsvm. The fit time complexity
    is more than quadratic with the number of samples which makes it hard
    to scale to datasets with more than a couple of 10000 samples. For large
    datasets consider using :class:`~sklearn.svm.LinearSVR` or
    :class:`~sklearn.linear_model.SGDRegressor` instead, possibly after a

         Specifies the kernel type to be used in the algorithm.
         If none is given, 'rbf' will be used. If a callable is given it is
         used to precompute the kernel matrix.
         For an intuitive visualization of different kernel types
         see :ref:`sphx_glr_auto_examples_svm_plot_svm_regression.py`

        The penalty is a squared l2. For an intuitive visualization of the
        effects of scaling the regularization parameter C, see

    epsilon : float, default=0.1
         Epsilon in the epsilon-SVR model. It specifies the epsilon-tube
         within which no penalty is associated in the training loss function
         with points predicted within a distance epsilon from the actual
         value. Must be non-negative.

    coef_ : ndarray of shape (1, n_features)

    dual_coef_ : ndarray of shape (1, n_SV)
        Coefficients of the support vector in the decision function.

    intercept_ : ndarray of shape (1,)

    n_support_ : ndarray of shape (1,), dtype=int32
        Number of support vectors.

    See Also
    --------
    NuSVR : Support Vector Machine for regression implemented using libsvm
        using a parameter to control the number of support vectors.

    LinearSVR : Scalable Linear Support Vector Machine for regression
        implemented using liblinear.

    Examples
    --------
    >>> regr = make_pipeline(StandardScaler(), SVR(C=1.0, epsilon=0.2))
                    ('svr', SVR(epsilon=0.2))])
    """

    _impl = "epsilon_svr"

    _parameter_constraints: dict = {**BaseLibSVM._parameter_constraints}
    for unused_param in ["class_weight", "nu", "probability", "random_state"]:
        _parameter_constraints.pop(unused_param)

        self,
        *,
        degree=3,
        tol=1e-3,
        C=1.0,
        epsilon=0.1,
    ):
            tol=tol,
            C=C,
            nu=0.0,
            probability=False,
            class_weight=None,
            random_state=None,
        )

class NuSVR(RegressorMixin, BaseLibSVM):
    """Nu Support Vector Regression.

    Similar to NuSVC, for regression, uses a parameter nu to control
    the number of support vectors. However, unlike NuSVC, where nu
    replaces C, here nu replaces the parameter epsilon of epsilon-SVR.

        An upper bound on the fraction of training errors and a lower bound of
        the fraction of support vectors. Should be in the interval (0, 1].  By
        default 0.5 will be taken.

        Penalty parameter C of the error term. For an intuitive visualization
        of the effects of scaling the regularization parameter C, see

         For an intuitive visualization of different kernel types see
         See :ref:`sphx_glr_auto_examples_svm_plot_svm_regression.py`

    See Also
    --------
    NuSVC : Support Vector Machine for classification implemented with libsvm
        with a parameter to control the number of support vectors.

    SVR : Epsilon Support Vector Machine for regression implemented with
        libsvm.

    Examples
    --------
    >>> from sklearn.svm import NuSVR
    >>> y = np.random.randn(n_samples)
    >>> X = np.random.randn(n_samples, n_features)
    >>> regr = make_pipeline(StandardScaler(), NuSVR(C=1.0, nu=0.1))
                    ('nusvr', NuSVR(nu=0.1))])
    """

    _impl = "nu_svr"

    for unused_param in ["class_weight", "epsilon", "probability", "random_state"]:

        self,
        *,
        nu=0.5,
        C=1.0,
        degree=3,
        tol=1e-3,
    ):
            tol=tol,
            C=C,
            nu=nu,
            epsilon=0.0,
        )

class OneClassSVM(OutlierMixin, BaseLibSVM):
    """Unsupervised Outlier Detection.

    Estimate the support of a high-dimensional distribution.

        An upper bound on the fraction of training
        errors and a lower bound of the fraction of support
        vectors. Should be in the interval (0, 1]. By default 0.5
        will be taken.

        Coefficients of the support vectors in the decision function.

        Constant in the decision function.

        We have the relation: decision_function = score_samples - `offset_`.
        The offset is the opposite of `intercept_` and is provided for
        consistency with other outlier detection algorithms.

    See Also
    --------
    sklearn.linear_model.SGDOneClassSVM : Solves linear One-Class SVM using
        Stochastic Gradient Descent.
    sklearn.neighbors.LocalOutlierFactor : Unsupervised Outlier Detection using
        Local Outlier Factor (LOF).
    sklearn.ensemble.IsolationForest : Isolation Forest Algorithm.

    Examples
    --------
    >>> from sklearn.svm import OneClassSVM
    >>> X = [[0], [0.44], [0.45], [0.46], [1]]
    >>> clf = OneClassSVM(gamma='auto').fit(X)
    >>> clf.predict(X)
    array([-1,  1,  1,  1, -1])
    >>> clf.score_samples(X)
    array([1.7798..., 2.0547..., 2.0556..., 2.0561..., 1.7332...])

    """

    _impl = "one_class"

    for unused_param in ["C", "class_weight", "epsilon", "probability", "random_state"]:

        self,
        *,
        degree=3,
        tol=1e-3,
        nu=0.5,
    ):
            kernel,
            degree,
            gamma,
            coef0,
            tol,
            0.0,
            nu,
            0.0,
            shrinking,
            False,
            cache_size,
            None,
            verbose,
            max_iter,
        )

        """Detect the soft boundary of the set of samples X.

            Set of samples, where `n_samples` is the number of samples and

        y : Ignored

            Per-sample weights. Rescale C per sample. Higher weights
            force the classifier to put more emphasis on these points.

        Returns
        -------

        Notes
        -----
        If X is not a C-ordered contiguous array it is copied.
        """
        super().fit(X, np.ones(_num_samples(X)), sample_weight=sample_weight)
        self.offset_ = -self._intercept_

        """Signed distance to the separating hyperplane.

        Signed distance is positive for an inlier and negative for an outlier.

        Returns
        -------
        dec : ndarray of shape (n_samples,)
            Returns the decision function of the samples.
        """
        dec = self._decision_function(X).ravel()
        return dec

        """Raw scoring function of the samples.

        Returns
        -------
        score_samples : ndarray of shape (n_samples,)
            Returns the (unshifted) scoring function of the samples.
        """
        return self.decision_function(X) + self.offset_

        """Perform classification on samples in X.

        For a one-class model, +1 or -1 is returned.

                (n_samples_test, n_samples_train)
            For kernel="precomputed", the expected shape of X is
            (n_samples_test, n_samples_train).

        Returns
        -------
            Class labels for samples in X.
        """
        y = super().predict(X)
        return np.asarray(y, dtype=np.intp)

        self,
        *,
        tol=1e-4,
        C=1.0,
    ):
        self.C = C

        Returns
        -------
        """
            self,
            X,
            y,
        )

        )

            X,
            y,
            self.C,
            _dual,
            self.tol,
        )

        self,
        *,
        tol=1e-4,
        C=1.0,
    ):
        self.C = C

        Returns
        -------
        """
            self,
            X,
            y,
        )

            X,
            y,
            self.C,
            None,
            penalty,
            _dual,
            self.tol,
        )

        self,
        *,
        C=1.0,
        degree=3,
        tol=1e-3,
    ):
            tol=tol,
            C=C,
            nu=0.0,
        )

        self,
        *,
        nu=0.5,
        degree=3,
        tol=1e-3,
    ):
            tol=tol,
            C=0.0,
            nu=nu,
        )

        self,
        *,
        degree=3,
        tol=1e-3,
        C=1.0,
    ):
            tol=tol,
            C=C,
            nu=0.0,
        )

        self,
        *,
        nu=0.5,
        C=1.0,
        degree=3,
        tol=1e-3,
    ):
            tol=tol,
            C=C,
            nu=nu,
        )

        self,
        *,
        degree=3,
        tol=1e-3,
        nu=0.5,
    ):
            kernel,
            degree,
            gamma,
            coef0,
            tol,
            0.0,
            nu,
            0.0,
            False,
            None,
            verbose,
            max_iter,
        )

        y : Ignored

        Returns
        -------

        Notes
        -----
        """

        Returns
        -------
        """
        return dec

        Returns
        -------
        """

        Returns
        -------
        """

def _one_vs_one_coef(dual_coef, n_support, support_vectors):
    """Generate primal coefficients from dual coefficients
    for the one-vs-one multi class LibSVM in the case
    of a linear kernel."""

    # get 1vs1 weights for all n*(n-1) classifiers.
    # this is somewhat messy.
    # shape of dual_coef_ is nSV * (n_classes -1)
    # see docs for details
    n_class = dual_coef.shape[0] + 1

    # XXX we could do preallocation of coef but
    # would have to take care in the sparse case
    coef = []
    sv_locs = np.cumsum(np.hstack([[0], n_support]))
    for class1 in range(n_class):
        # SVs for class1:
        sv1 = support_vectors[sv_locs[class1] : sv_locs[class1 + 1], :]
        for class2 in range(class1 + 1, n_class):
            # SVs for class1:
            sv2 = support_vectors[sv_locs[class2] : sv_locs[class2 + 1], :]

            # dual coef for class1 SVs:
            alpha1 = dual_coef[class2 - 1, sv_locs[class1] : sv_locs[class1 + 1]]
            # dual coef for class2 SVs:
            alpha2 = dual_coef[class1, sv_locs[class2] : sv_locs[class2 + 1]]
            # build weight for class1 vs class2

            coef.append(safe_sparse_dot(alpha1, sv1) + safe_sparse_dot(alpha2, sv2))
    return coef

class BaseLibSVM(BaseEstimator, metaclass=ABCMeta):
    """Base class for estimators that use libsvm as backing library.

    This implements support vector machine classification and regression.

    Parameter documentation is in the derived `SVC` class.
    """

            StrOptions({"linear", "poly", "rbf", "sigmoid", "precomputed"}),
            callable,
        ],
        "degree": [Interval(Integral, 0, None, closed="left")],
        "gamma": [
            StrOptions({"scale", "auto"}),
        ],
        "C": [Interval(Real, 0.0, None, closed="right")],
        "epsilon": [Interval(Real, 0.0, None, closed="left")],
        "shrinking": ["boolean"],
        "probability": ["boolean"],
        "cache_size": [Interval(Real, 0, None, closed="neither")],
        "class_weight": [StrOptions({"balanced"}), dict, None],
        "max_iter": [Interval(Integral, -1, None, closed="left")],
    }

    # The order of these must match the integer values in LibSVM.
    # XXX These are actually the same in the dense case. Need to factor
    # this out.
    _sparse_kernels = ["linear", "poly", "rbf", "sigmoid", "precomputed"]

        self,
        kernel,
        degree,
        gamma,
        coef0,
        tol,
        C,
        nu,
        epsilon,
        shrinking,
        probability,
        cache_size,
        class_weight,
        verbose,
        max_iter,
    ):
        if self._impl not in LIBSVM_IMPL:
                "impl should be one of %s, %s was given" % (LIBSVM_IMPL, self._impl)
            )

        self.C = C
        self.shrinking = shrinking
        self.probability = probability
        self.cache_size = cache_size

        # Used by cross_val_score.
        tags.input_tags.sparse = self.kernel != "precomputed"

        """Fit the SVM model according to the given training data.

        X : {array-like, sparse matrix} of shape (n_samples, n_features) \
                or (n_samples, n_samples)
            (n_samples, n_samples).

            Target values (class labels in classification, real numbers in

        Returns
        -------

        Notes
        -----
        If X and y are not C-ordered and contiguous arrays of np.float64 and
        X is not a scipy.sparse.csr_matrix, X and/or y may be copied.

        If X is a dense array, then the other methods will not support sparse
        matrices as input.
        """

        if sparse and self.kernel == "precomputed":
            raise TypeError("Sparse precomputed kernels are not supported.")
        self._sparse = sparse and not callable(self.kernel)

            check_consistent_length(X, y)
        else:
                self,
                X,
                y,
                accept_large_sparse=False,
            )

        y = self._validate_targets(y)

        sample_weight = np.asarray(
            [] if sample_weight is None else sample_weight, dtype=np.float64
        )
        solver_type = LIBSVM_IMPL.index(self._impl)

        # input validation
        if solver_type != 2 and n_samples != y.shape[0]:
                "X and y have incompatible shapes.\n"
                + "X has %s samples, but y has %s." % (n_samples, y.shape[0])
            )

        if self.kernel == "precomputed" and n_samples != X.shape[1]:
                "Precomputed matrix must be a square matrix."
                " Input is a {}x{} matrix.".format(X.shape[0], X.shape[1])
            )

        if sample_weight.shape[0] > 0 and sample_weight.shape[0] != n_samples:
                "sample_weight and X have incompatible shapes: "
                "%r vs %r\n"
                "Note: Sparse matrices cannot be indexed w/"
                "boolean masks (use `indices=True` in CV)."
                % (sample_weight.shape, X.shape)
            )

        kernel = "precomputed" if callable(self.kernel) else self.kernel

        if kernel == "precomputed":
            # unused but needs to be a float for cython code that ignores
            # it anyway
            self._gamma = 0.0
        elif isinstance(self.gamma, str):
            if self.gamma == "scale":
                # var = E[X^2] - E[X]^2 if sparse
                X_var = (X.multiply(X)).mean() - (X.mean()) ** 2 if sparse else X.var()
                self._gamma = 1.0 / (X.shape[1] * X_var) if X_var != 0 else 1.0
            elif self.gamma == "auto":
                self._gamma = 1.0 / X.shape[1]
        elif isinstance(self.gamma, Real):

        fit = self._sparse_fit if self._sparse else self._dense_fit
            print("[LibSVM]", end="")

        seed = rnd.randint(np.iinfo("i").max)
        fit(X, y, sample_weight, solver_type, kernel, random_seed=seed)
        # see comment on the other call to np.iinfo in this file

        self.shape_fit_ = X.shape if hasattr(X, "shape") else (n_samples,)

        # In binary case, we need to flip the sign of coef, intercept and
        # decision function. Use self._intercept_ and self._dual_coef_
        # internally.
        self._intercept_ = self.intercept_.copy()
        self._dual_coef_ = self.dual_coef_
        if self._impl in ["c_svc", "nu_svc"] and len(self.classes_) == 2:
            self.intercept_ *= -1
            self.dual_coef_ = -self.dual_coef_

        dual_coef = self._dual_coef_.data if self._sparse else self._dual_coef_
        intercept_finiteness = np.isfinite(self._intercept_).all()
        dual_coef_finiteness = np.isfinite(dual_coef).all()
        if not (intercept_finiteness and dual_coef_finiteness):
                "The dual coefficients or intercepts are not finite."
                " The input data may contain large values and need to be"
                " preprocessed."
            )

        # Since, in the case of SVC and NuSVC, the number of models optimized by
        # libSVM could be greater than one (depending on the input), `n_iter_`
        # stores an ndarray.
        # For the other sub-classes (SVR, NuSVR, and OneClassSVM), the number of
        # models optimized by libSVM is always one, so `n_iter_` stores an
        # integer.
        if self._impl in ["c_svc", "nu_svc"]:
            self.n_iter_ = self._num_iter
        else:
            self.n_iter_ = self._num_iter.item()

    def _validate_targets(self, y):
        """Validation of y and class_weight.

        Default implementation for SVR and one-class; overridden in BaseSVC.
        """
        return column_or_1d(y, warn=True).astype(np.float64, copy=False)

    def _warn_from_fit_status(self):
        assert self.fit_status_ in (0, 1)
        if self.fit_status_ == 1:
                "Solver terminated early (max_iter=%i)."
                "  Consider pre-processing your data with"
                " StandardScaler or MinMaxScaler." % self.max_iter,
            )

    def _dense_fit(self, X, y, sample_weight, solver_type, kernel, random_seed):
            # you must store a reference to X to compute the kernel in predict
            # TODO: add keyword copy to copy on demand
            self.__Xfit = X
            X = self._compute_kernel(X)

                raise ValueError("X.shape[0] should be equal to X.shape[1]")

        libsvm.set_verbosity_wrap(self.verbose)

        # we don't pass **self.get_params() to allow subclasses to
        # add other parameters to __init__
        (
            self.support_,
            self.support_vectors_,
            self._n_support,
            self.dual_coef_,
            self.intercept_,
            self._probA,
            self._probB,
            self.fit_status_,
            self._num_iter,
        ) = libsvm.fit(
            X,
            y,
            svm_type=solver_type,
            class_weight=getattr(self, "class_weight_", np.empty(0)),
            C=self.C,
            nu=self.nu,
            probability=self.probability,
            degree=self.degree,
            shrinking=self.shrinking,
            cache_size=self.cache_size,
            coef0=self.coef0,
            gamma=self._gamma,
            random_seed=random_seed,
        )

        self._warn_from_fit_status()

    def _sparse_fit(self, X, y, sample_weight, solver_type, kernel, random_seed):
        X.data = np.asarray(X.data, dtype=np.float64, order="C")

        kernel_type = self._sparse_kernels.index(kernel)

        libsvm_sparse.set_verbosity_wrap(self.verbose)

        (
            dual_coef_data,
        ) = libsvm_sparse.libsvm_sparse_train(
            X.data,
            X.indices,
            X.indptr,
            y,
            solver_type,
            kernel_type,
            self.degree,
            self._gamma,
            self.coef0,
            self.tol,
            self.C,
            getattr(self, "class_weight_", np.empty(0)),
            sample_weight,
            self.nu,
            self.cache_size,
            self.epsilon,
            int(self.shrinking),
            int(self.probability),
            random_seed,
        )

        if hasattr(self, "classes_"):
            n_class = len(self.classes_) - 1
        else:  # regression
            n_class = 1
        n_SV = self.support_vectors_.shape[0]

        dual_coef_indices = np.tile(np.arange(n_SV), n_class)
        if not n_SV:
            self.dual_coef_ = sp.csr_matrix([])
        else:
            dual_coef_indptr = np.arange(
                0, dual_coef_indices.size + 1, dual_coef_indices.size / n_class
            )
            self.dual_coef_ = sp.csr_matrix(
                (dual_coef_data, dual_coef_indices, dual_coef_indptr), (n_class, n_SV)
            )

        """Perform regression on samples in X.

        For an one-class model, +1 (inlier) or -1 (outlier) is returned.

        Returns
        -------
        """
        X = self._validate_for_predict(X)
        predict = self._sparse_predict if self._sparse else self._dense_predict
        return predict(X)

    def _dense_predict(self, X):
        X = self._compute_kernel(X)
        if X.ndim == 1:
            X = check_array(X, order="C", accept_large_sparse=False)

        kernel = self.kernel
            kernel = "precomputed"
            if X.shape[1] != self.shape_fit_[0]:
                    "X.shape[1] = %d should be equal to %d, "
                    "the number of samples at training time"
                    % (X.shape[1], self.shape_fit_[0])
                )

        svm_type = LIBSVM_IMPL.index(self._impl)

        return libsvm.predict(
            X,
            self._dual_coef_,
            self._intercept_,
            svm_type=svm_type,
        )

    def _sparse_predict(self, X):
        # Precondition: X is a csr_matrix of dtype np.float64.
        if callable(kernel):

        C = 0.0  # C is not useful here

        return libsvm_sparse.libsvm_sparse_predict(
            X.data,
            X.indptr,
            self.support_vectors_.data,
            self.support_vectors_.indices,
            self.support_vectors_.indptr,
            self._dual_coef_.data,
            LIBSVM_IMPL.index(self._impl),
            self.tol,
            C,
            self.nu,
            self.shrinking,
            self.probability,
        )

    def _compute_kernel(self, X):
        """Return the data transformed by a callable kernel"""
            # in the case of precomputed kernel given as a function, we
            # have to compute explicitly the kernel matrix
            kernel = self.kernel(X, self.__Xfit)
            if sp.issparse(kernel):
                kernel = kernel.toarray()
            X = np.asarray(kernel, dtype=np.float64, order="C")
        return X

        """Evaluates the decision function for the samples in X.

        Returns
        -------
        X : array-like of shape (n_samples, n_class * (n_class-1) / 2)
            Returns the decision function of the sample for each class
            in the model.
        """
        # NOTE: _validate_for_predict contains check for is_fitted
        # hence must be placed before any other attributes are used.

        if self._sparse:
            dec_func = self._sparse_decision_function(X)
        else:
            dec_func = self._dense_decision_function(X)

        # decision function.
            return -dec_func.ravel()

        return dec_func

    def _dense_decision_function(self, X):
        X = check_array(X, dtype=np.float64, order="C", accept_large_sparse=False)

        return libsvm.decision_function(
            X,
            svm_type=LIBSVM_IMPL.index(self._impl),
        )

    def _sparse_decision_function(self, X):

        if hasattr(kernel, "__call__"):

        return libsvm_sparse.libsvm_sparse_decision_function(
            X.data,
            X.indptr,
            self.tol,
            self.C,
            self.nu,
        )

    def _validate_for_predict(self, X):

        if not callable(self.kernel):
                self,
                X,
            )

        if self._sparse and not sp.issparse(X):

        if sp.issparse(X) and not self._sparse and not callable(self.kernel):
                "cannot use sparse input in %r trained on dense data"
                % type(self).__name__
            )

        if self.kernel == "precomputed":
                )
        # Fixes https://nvd.nist.gov/vuln/detail/CVE-2020-28975
        # Check that _n_support is consistent with support_vectors
        sv = self.support_vectors_
        if not self._sparse and sv.size > 0 and self.n_support_.sum() != sv.shape[0]:
                f"The internal representation of {self.__class__.__name__} was altered"
            )
        return X

    @property
    def coef_(self):
        """Weights assigned to the features when `kernel="linear"`.

        Returns
        -------
        ndarray of shape (n_features, n_classes)
        """
        if self.kernel != "linear":
            raise AttributeError("coef_ is only available when using a linear kernel")

        coef = self._get_coef()

        # coef_ being a read-only property, it's better to mark the value as
        # immutable to avoid hiding potential bugs for the unsuspecting user.
        if sp.issparse(coef):
            # sparse matrix do not have global flags
            coef.data.flags.writeable = False
        else:
            # regular dense array
            coef.flags.writeable = False
        return coef

    def _get_coef(self):
        return safe_sparse_dot(self._dual_coef_, self.support_vectors_)

    @property
    def n_support_(self):
        """Number of support vectors for each class."""
        try:
            raise AttributeError

        if svm_type in (0, 1):
            return self._n_support
        else:
            # SVR and OneClass
            # _n_support has size 2, we make it size 1
            return np.array([self._n_support[0]])

class BaseSVC(ClassifierMixin, BaseLibSVM, metaclass=ABCMeta):
    """ABC for LibSVM-based classifiers."""

        **BaseLibSVM._parameter_constraints,
        "decision_function_shape": [StrOptions({"ovr", "ovo"})],
        "break_ties": ["boolean"],
    }
    for unused_param in ["epsilon", "nu"]:

        self,
        kernel,
        degree,
        gamma,
        coef0,
        tol,
        C,
        nu,
        verbose,
        max_iter,
        decision_function_shape,
        break_ties,
    ):
        self.decision_function_shape = decision_function_shape
        self.break_ties = break_ties
            tol=tol,
            C=C,
            nu=nu,
        )

        y_ = column_or_1d(y, warn=True)
        cls, y = np.unique(y_, return_inverse=True)
        self.class_weight_ = compute_class_weight(self.class_weight, classes=cls, y=y_)
        if len(cls) < 2:
                % len(cls)
            )

        self.classes_ = cls

        return np.asarray(y, dtype=np.float64, order="C")

        """Evaluate the decision function for the samples in X.

        Returns
        -------
        X : ndarray of shape (n_samples, n_classes * (n_classes-1) / 2)
            If decision_function_shape='ovr', the shape is (n_samples,
            n_classes).

        Notes
        -----
        If decision_function_shape='ovo', the function values are proportional
        to the distance of the samples X to the separating hyperplane. If the
        exact distances are required, divide the function values by the norm of
        the weight vector (``coef_``). See also `this question
        <https://stats.stackexchange.com/questions/14876/
        interpreting-distance-from-hyperplane-in-svm>`_ for further details.
        If decision_function_shape='ovr', the decision function is a monotonic
        transformation of ovo decision function.
        """
        dec = self._decision_function(X)
        if self.decision_function_shape == "ovr" and len(self.classes_) > 2:
            return _ovr_decision_function(dec < 0, -dec, len(self.classes_))
        return dec

        For an one-class model, +1 or -1 is returned.

        Returns
        -------
        """
        if self.break_ties and self.decision_function_shape == "ovo":
                "break_ties must be False when decision_function_shape is 'ovo'"
            )

        if (
            self.break_ties
            and self.decision_function_shape == "ovr"
            and len(self.classes_) > 2
        ):
            y = np.argmax(self.decision_function(X), axis=1)
        else:
            y = super().predict(X)
        return self.classes_.take(np.asarray(y, dtype=np.intp))

    # Hacky way of getting predict_proba to raise an AttributeError when
    # probability=False using properties. Do not use this in new code; when
    # probabilities are not available depending on a setting, introduce two
    def _check_proba(self):
        if not self.probability:
                "predict_proba is not available when probability=False"
            )
        if self._impl not in ("c_svc", "nu_svc"):
            raise AttributeError("predict_proba only implemented for SVC and NuSVC")

    @available_if(_check_proba)

        The model needs to have probability information computed at training
        time: fit with attribute `probability` set to True.

        Returns
        -------
        T : ndarray of shape (n_samples, n_classes)

        Notes
        -----
        The probability model is created using cross validation, so
        the results can be slightly different than those obtained by
        predict. Also, it will produce meaningless results on very small
        datasets.
        """
        if self.probA_.size == 0 or self.probB_.size == 0:
                "predict_proba is not available when fitted with probability=False"
            )
        pred_proba = (
            self._sparse_predict_proba if self._sparse else self._dense_predict_proba
        )
        return pred_proba(X)

        """Compute log probabilities of possible outcomes for samples in X.

        The model need to have probability information computed at training

        Returns
        -------
            Returns the log-probabilities of the sample for each class in

        Notes
        -----
        datasets.
        """

    def _dense_predict_proba(self, X):

        pprob = libsvm.predict_proba(
            X,
        )

        return pprob

    def _sparse_predict_proba(self, X):

        return libsvm_sparse.libsvm_sparse_predict_proba(
            X.data,
            X.indptr,
            self.tol,
            self.C,
            self.nu,
        )

        if self.dual_coef_.shape[0] == 1:
            # binary classifier
            coef = safe_sparse_dot(self.dual_coef_, self.support_vectors_)
        else:
            # 1vs1 classifier
            coef = _one_vs_one_coef(
                self.dual_coef_, self._n_support, self.support_vectors_
            )
            if sp.issparse(coef[0]):
                coef = sp.vstack(coef).tocsr()
            else:
                coef = np.vstack(coef)

    @property
    def probA_(self):
        """Parameter learned in Platt scaling when `probability=True`.

        Returns
        -------
        ndarray of shape  (n_classes * (n_classes - 1) / 2)
        """
        return self._probA

    @property
    def probB_(self):

        Returns
        -------
        """
        return self._probB

def _get_liblinear_solver_type(multi_class, penalty, loss, dual):
    """Find the liblinear magic number for the solver.

    This number depends on the values of the following attributes:
      - multi_class
      - penalty
      - loss
      - dual

    The same number is also internally used by LibLinear to determine
    which solver to use.
    """
    # nested dicts containing level 1: available loss functions,
    # level2: available penalties for the given loss function,
    # level3: whether the dual solver is available for the specified
    # combination of loss function and penalty
    _solver_type_dict = {
        "logistic_regression": {"l1": {False: 6}, "l2": {False: 0, True: 7}},
        "hinge": {"l2": {True: 3}},
        "squared_hinge": {"l1": {False: 5}, "l2": {False: 2, True: 1}},
        "epsilon_insensitive": {"l2": {True: 13}},
        "squared_epsilon_insensitive": {"l2": {False: 11, True: 12}},
        "crammer_singer": 4,
    }

    if multi_class == "crammer_singer":
        return _solver_type_dict[multi_class]
    elif multi_class != "ovr":
            "`multi_class` must be one of `ovr`, `crammer_singer`, got %r" % multi_class
        )

    _solver_pen = _solver_type_dict.get(loss, None)
    if _solver_pen is None:
        error_string = "loss='%s' is not supported" % loss
    else:
        _solver_dual = _solver_pen.get(penalty, None)
        if _solver_dual is None:
            error_string = (
                "The combination of penalty='%s' and loss='%s' is not supported"
                % (penalty, loss)
            )
        else:
            solver_num = _solver_dual.get(dual, None)
            if solver_num is None:
                error_string = (
                    "The combination of penalty='%s' and "
                    "loss='%s' are not supported when dual=%s" % (penalty, loss, dual)
                )
            else:
                return solver_num
        "Unsupported set of arguments: %s, Parameters: penalty=%r, loss=%r, dual=%r"
        % (error_string, penalty, loss, dual)
    )

def _fit_liblinear(
    X,
    y,
    C,
    fit_intercept,
    intercept_scaling,
    class_weight,
    penalty,
    dual,
    verbose,
    max_iter,
    tol,
    multi_class="ovr",
    loss="logistic_regression",
    epsilon=0.1,
):
    """Used by Logistic Regression (and CV) and LinearSVC/LinearSVR.

    Preprocessing is done in this function before supplying it to liblinear.

        Target vector relative to X

    C : float
        Inverse of cross-validation parameter. The lower the C, the higher
        the penalization.

    fit_intercept : bool
        is extended to include an intercept term: ``[x_1, ..., x_n, 1]``, where

    intercept_scaling : float
        Liblinear internally penalizes the intercept, treating it like any
        other term in the feature vector. To reduce the impact of the

    penalty : {'l1', 'l2'}
        The norm of the penalty used in regularization.

    dual : bool
        Dual or primal formulation,

        Set verbose to any positive number for verbosity.

    tol : float
        Stopping condition.

        `ovr` trains n_classes one-vs-rest classifiers, while `crammer_singer`
        optimizes a joint objective over all classes.
        While `crammer_singer` is interesting from an theoretical perspective
        as it is consistent it is seldom used in practice and rarely leads to
        better accuracy and is more expensive to compute.
        If `crammer_singer` is chosen, the options loss, penalty and dual will

    loss : {'logistic_regression', 'hinge', 'squared_hinge', \
            'epsilon_insensitive', 'squared_epsilon_insensitive}, \
            default='logistic_regression'
        The loss function used to fit the model.

        variable y. If unsure, set epsilon=0.

        Weights assigned to each sample.

    Returns
    -------
    coef_ : ndarray of shape (n_features, n_features + 1)
        The coefficient vector got by minimizing the objective function.

    intercept_ : float
        The intercept term added to the vector.

    n_iter_ : array of int
        Number of iterations run across for each class.
    """
    if loss not in ["epsilon_insensitive", "squared_epsilon_insensitive"]:
        enc = LabelEncoder()
        y_ind = enc.fit_transform(y)
        classes_ = enc.classes_
        if len(classes_) < 2:
                "This solver needs samples of at least 2 classes"
                " in the data, but the data contains only one"
                " class: %r" % classes_[0]
            )
        class_weight_ = compute_class_weight(
            class_weight, classes=classes_, y=y, sample_weight=sample_weight
        )
    else:
        class_weight_ = np.empty(0, dtype=np.float64)
        y_ind = y
    liblinear.set_verbosity_wrap(verbose)
    rnd = check_random_state(random_state)
        print("[LibLinear]", end="")

    # LinearSVC breaks when intercept_scaling is <= 0
    bias = -1.0
        if intercept_scaling <= 0:
                "Intercept scaling is %r but needs to be greater "
                "than 0. To disable fitting an intercept,"
                " set fit_intercept=False." % intercept_scaling
            )
        else:
            bias = intercept_scaling

    libsvm.set_verbosity_wrap(verbose)
    libsvm_sparse.set_verbosity_wrap(verbose)

    # Liblinear doesn't support 64bit sparse matrix indices yet
        _check_large_sparse(X)

    # LibLinear wants targets as doubles, even for classification
    y_ind = np.asarray(y_ind, dtype=np.float64).ravel()
    y_ind = np.require(y_ind, requirements="W")

    sample_weight = _check_sample_weight(sample_weight, X, dtype=np.float64)

    solver_type = _get_liblinear_solver_type(multi_class, penalty, loss, dual)
    raw_coef_, n_iter_ = liblinear.train_wrap(
        X,
        y_ind,
        sp.issparse(X),
        solver_type,
        tol,
        bias,
        C,
        class_weight_,
        max_iter,
        rnd.randint(np.iinfo("i").max),
        epsilon,
    )
    # Regarding rnd.randint(..) in the above signature:
    # seed for srand in range [0..INT_MAX); due to limitations in Numpy
    # on 32-bit platforms, we can't get to the UINT_MAX limit that
    # srand supports
    n_iter_max = max(n_iter_)
    if n_iter_max >= max_iter:
            "Liblinear failed to converge, increase the number of iterations.",
        )

        coef_ = raw_coef_[:, :-1]
        intercept_ = intercept_scaling * raw_coef_[:, -1]
    else:
        coef_ = raw_coef_
        intercept_ = 0.0

    return coef_, intercept_, n_iter_

        self,
        kernel,
        degree,
        gamma,
        coef0,
        tol,
        C,
        nu,
        epsilon,
        verbose,
        max_iter,
    ):
            )

        self.C = C

        Returns
        -------

        Notes
        -----

        """

        else:
                self,
                X,
                y,
            )

        )

            )

            )

            )

            # it anyway

            )

        # integer.
        else:

        """

            )

        (
            X,
            y,
            C=self.C,
        )

        (
            X.data,
            X.indptr,
            y,
            self.tol,
            self.C,
            self.nu,
        )

            n_class = 1

        else:
            )
            )

        Returns
        -------
        """

                )

            X,
        )

            X.data,
            X.indptr,
            self.tol,
            C,
            self.nu,
        )

        return X

        Returns
        -------
        """

        else:

            X,
        )

            X.data,
            X.indptr,
            self.tol,
            self.C,
            self.nu,
        )

                self,
                X,
            )

            )

                )
            )
        return X

        Returns
        -------
        """

        else:

        try:

        else:

        self,
        kernel,
        degree,
        gamma,
        coef0,
        tol,
        C,
        nu,
        verbose,
        max_iter,
    ):
            tol=tol,
            C=C,
            nu=nu,
        )

                % len(cls)
            )

        Returns
        -------

        Notes
        -----
        """
        return dec

        Returns
        -------
        """
            )

        if (
        ):
        else:

            )

        Returns
        -------

        Notes
        -----
        datasets.
        """
            )
        )

        Returns
        -------

        Notes
        -----
        datasets.
        """

            X,
        )

            X.data,
            X.indptr,
            self.tol,
            self.C,
            self.nu,
        )

        else:
            )
            else:

        Returns
        -------
        """

        Returns
        -------
        """

def check_svm_model_equal(dense_svm, X_train, y_train, X_test):
    # Use the original svm model for dense fit and clone an exactly same
    # svm model for sparse fit
    sparse_svm = base.clone(dense_svm)

    dense_svm.fit(X_train.toarray(), y_train)
    if sparse.issparse(X_test):
        X_test_dense = X_test.toarray()
    else:
        X_test_dense = X_test
    sparse_svm.fit(X_train, y_train)
    assert sparse.issparse(sparse_svm.support_vectors_)
    assert sparse.issparse(sparse_svm.dual_coef_)
    assert_allclose(dense_svm.support_vectors_, sparse_svm.support_vectors_.toarray())
    assert_allclose(dense_svm.dual_coef_, sparse_svm.dual_coef_.toarray())
    if dense_svm.kernel == "linear":
        assert sparse.issparse(sparse_svm.coef_)
        assert_array_almost_equal(dense_svm.coef_, sparse_svm.coef_.toarray())
    assert_allclose(dense_svm.support_, sparse_svm.support_)
    assert_allclose(dense_svm.predict(X_test_dense), sparse_svm.predict(X_test))

        dense_svm.decision_function(X_test_dense), sparse_svm.decision_function(X_test)
    )
        dense_svm.decision_function(X_test_dense),
        sparse_svm.decision_function(X_test_dense),
    )
    if isinstance(dense_svm, svm.OneClassSVM):
        msg = "cannot use sparse input in 'OneClassSVM' trained on dense data"
    else:
            dense_svm.predict_proba(X_test_dense),
            sparse_svm.predict_proba(X_test),
            decimal=4,
        )
        msg = "cannot use sparse input in 'SVC' trained on dense data"
            dense_svm.predict(X_test)

def test_svc(X_train, y_train, X_test, kernel, sparse_container):
    """Check that sparse SVC gives the same result as SVC."""
    X_train = sparse_container(X_train)

    clf = svm.SVC(
        gamma=1,
        kernel=kernel,
        probability=True,
        decision_function_shape="ovo",
    )
    check_svm_model_equal(clf, X_train, y_train, X_test)

def test_unsorted_indices(csr_container):
    # test that the result with sorted and unsorted indices in csr is the same
    # we use a subset of digits as iris, blobs or make_classification didn't
    # show the problem
    X, y = load_digits(return_X_y=True)
    X_test = csr_container(X[50:100])
    X, y = X[:50], y[:50]

    coef_dense = (
        svm.SVC(kernel="linear", probability=True, random_state=0).fit(X, y).coef_
    )
    sparse_svc = svm.SVC(kernel="linear", probability=True, random_state=0).fit(
    )
    coef_sorted = sparse_svc.coef_
    # make sure dense and sparse SVM give the same result
    assert_allclose(coef_dense, coef_sorted.toarray())

    # reverse each row's indices
    def scramble_indices(X):
        new_indices = []
        for i in range(1, len(X.indptr)):
            row_slice = slice(*X.indptr[i - 1 : i + 1])
            new_data.extend(X.data[row_slice][::-1])
            new_indices.extend(X.indices[row_slice][::-1])
        return csr_container((new_data, new_indices, X.indptr), shape=X.shape)

    X_sparse_unsorted = scramble_indices(X_sparse)
    X_test_unsorted = scramble_indices(X_test)

    assert not X_sparse_unsorted.has_sorted_indices
    assert not X_test_unsorted.has_sorted_indices

    unsorted_svc = svm.SVC(kernel="linear", probability=True, random_state=0).fit(
        X_sparse_unsorted, y
    )
    coef_unsorted = unsorted_svc.coef_
    # make sure unsorted indices give same result
    assert_allclose(coef_unsorted.toarray(), coef_sorted.toarray())
        sparse_svc.predict_proba(X_test_unsorted), sparse_svc.predict_proba(X_test)
    )

def test_svc_with_custom_kernel(lil_container):
    def kfunc(x, y):
        return safe_sparse_dot(x, y.T)

    X_sp = lil_container(X)
    clf_lin = svm.SVC(kernel="linear").fit(X_sp, Y)
    clf_mylin = svm.SVC(kernel=kfunc).fit(X_sp, Y)
    assert_array_equal(clf_lin.predict(X_sp), clf_mylin.predict(X_sp))

def test_svc_iris(csr_container, kernel):
    # Test the sparse SVC with the iris dataset
    iris_data_sp = csr_container(iris.data)

    sp_clf = svm.SVC(kernel=kernel).fit(iris_data_sp, iris.target)
    clf = svm.SVC(kernel=kernel).fit(iris.data, iris.target)

    assert_allclose(clf.support_vectors_, sp_clf.support_vectors_.toarray())
    assert_allclose(clf.dual_coef_, sp_clf.dual_coef_.toarray())
    assert_allclose(clf.predict(iris.data), sp_clf.predict(iris_data_sp))
    if kernel == "linear":
        assert_allclose(clf.coef_, sp_clf.coef_.toarray())

def test_sparse_decision_function(csr_container):
    # Test decision_function

    # Sanity check, test that decision_function implemented in python
    # returns the same as the one in libsvm

    # multi class:
    svc = svm.SVC(kernel="linear", C=0.1, decision_function_shape="ovo")
    clf = svc.fit(iris_data_sp, iris.target)

    dec = safe_sparse_dot(iris_data_sp, clf.coef_.T) + clf.intercept_

    assert_allclose(dec, clf.decision_function(iris_data_sp))

    # binary:
    dec = np.dot(X, clf.coef_.T) + clf.intercept_
    prediction = clf.predict(X)
    assert_allclose(dec.ravel(), clf.decision_function(X))
        prediction, clf.classes_[(clf.decision_function(X) > 0).astype(int).ravel()]
    )
    expected = np.array([-1.0, -0.66, -1.0, 0.66, 1.0, 1.0])
    assert_array_almost_equal(clf.decision_function(X), expected, decimal=2)

def test_error(lil_container):
    # Test that it gives proper exception on deficient input
    clf = svm.SVC()

    Y2 = Y[:-1]  # wrong dimensions for labels
        clf.fit(X_sp, Y2)

    clf.fit(X_sp, Y)

def test_linearsvc(lil_container, dok_container):
    # Similar to test_SVC
    X2_sp = dok_container(X2)

    clf = svm.LinearSVC(random_state=0).fit(X, Y)
    sp_clf = svm.LinearSVC(random_state=0).fit(X_sp, Y)

    assert sp_clf.fit_intercept

    assert_array_almost_equal(clf.coef_, sp_clf.coef_, decimal=4)
    assert_array_almost_equal(clf.intercept_, sp_clf.intercept_, decimal=4)

    assert_allclose(clf.predict(X), sp_clf.predict(X_sp))

    clf.fit(X2, Y2)
    sp_clf.fit(X2_sp, Y2)

def test_linearsvc_iris(csr_container):
    # Test the sparse LinearSVC with the iris dataset

    sp_clf = svm.LinearSVC(random_state=0).fit(iris_data_sp, iris.target)
    clf = svm.LinearSVC(random_state=0).fit(iris.data, iris.target)

    assert clf.fit_intercept == sp_clf.fit_intercept

    assert_array_almost_equal(clf.coef_, sp_clf.coef_, decimal=1)
    assert_array_almost_equal(clf.intercept_, sp_clf.intercept_, decimal=1)

    # check decision_function
    pred = np.argmax(sp_clf.decision_function(iris_data_sp), axis=1)
    assert_allclose(pred, clf.predict(iris.data))

    # sparsify the coefficients on both models and check that they still
    # produce the same results
    clf.sparsify()
    assert_array_equal(pred, clf.predict(iris_data_sp))
    sp_clf.sparsify()
    assert_array_equal(pred, sp_clf.predict(iris_data_sp))

def test_weight(csr_container):
    # Test class weights
    X_, y_ = make_classification(
        n_samples=200, n_features=100, weights=[0.833, 0.167], random_state=0
    )

    X_ = csr_container(X_)
    for clf in (
        linear_model.LogisticRegression(),
        svm.LinearSVC(random_state=0),
        svm.SVC(),
    ):
        clf.set_params(class_weight={0: 5})
        clf.fit(X_[:180], y_[:180])
        y_pred = clf.predict(X_[180:])
        assert np.sum(y_pred == y_[180:]) >= 11

def test_sample_weights(lil_container):
    # Test weights on individual samples

    assert_array_equal(clf.predict([X[2]]), [1.0])

    sample_weight = [0.1] * 3 + [10] * 3
    clf.fit(X_sp, Y, sample_weight=sample_weight)
    assert_array_equal(clf.predict([X[2]]), [2.0])

def test_sparse_liblinear_intercept_handling():
    # Test that sparse liblinear honours intercept_scaling param
    test_svm.test_dense_liblinear_intercept_handling(svm.LinearSVC)

def test_sparse_oneclasssvm(X_train, y_train, X_test, kernel, sparse_container):
    # Check that sparse OneClassSVM gives the same result as dense OneClassSVM

    clf = svm.OneClassSVM(gamma=1, kernel=kernel)

def test_sparse_realdata(csr_container):
    # Test on a subset from the 20newsgroups dataset.
    # This catches some bugs if input is not correctly converted into
    # sparse format or weights are not correctly initialized.
    data = np.array([0.03771744, 0.1003567, 0.01174647, 0.027069])

    # SVC does not support large sparse, so we specify int32 indices
    # In this case, `csr_matrix` automatically uses int32 regardless of the dtypes of
    # `indices` and `indptr` but `csr_array` may or may not use the same dtype as
    # `indices` and `indptr`, which would be int64 if not specified
    indices = np.array([6, 5, 35, 31], dtype=np.int32)
    indptr = np.array([0] * 8 + [1] * 32 + [2] * 38 + [4] * 3, dtype=np.int32)

    X = csr_container((data, indices, indptr))
        [
            1.0,
            0.0,
            2.0,
            2.0,
            1.0,
            1.0,
            1.0,
            2.0,
            2.0,
            0.0,
            1.0,
            2.0,
            2.0,
            0.0,
            2.0,
            0.0,
            3.0,
            0.0,
            3.0,
            0.0,
            1.0,
            1.0,
            3.0,
            2.0,
            3.0,
            2.0,
            0.0,
            3.0,
            1.0,
            0.0,
            2.0,
            1.0,
            2.0,
            0.0,
            1.0,
            0.0,
            2.0,
            3.0,
            1.0,
            3.0,
            0.0,
            1.0,
            0.0,
            0.0,
            2.0,
            0.0,
            1.0,
            2.0,
            2.0,
            2.0,
            3.0,
            2.0,
            0.0,
            3.0,
            2.0,
            1.0,
            2.0,
            3.0,
            2.0,
            2.0,
            0.0,
            1.0,
            0.0,
            1.0,
            2.0,
            3.0,
            0.0,
            0.0,
            2.0,
            2.0,
            1.0,
            3.0,
            1.0,
            1.0,
            0.0,
            1.0,
            2.0,
            1.0,
            1.0,
            3.0,
        ]
    )

    clf = svm.SVC(kernel="linear").fit(X.toarray(), y)
    sp_clf = svm.SVC(kernel="linear").fit(X.tocoo(), y)

    assert_array_equal(clf.support_vectors_, sp_clf.support_vectors_.toarray())
    assert_array_equal(clf.dual_coef_, sp_clf.dual_coef_.toarray())

def test_sparse_svc_clone_with_callable_kernel(lil_container):
    # Test that the "dense_fit" is called even though we use sparse input
    # meaning that everything works fine.
    a = svm.SVC(C=1, kernel=lambda x, y: x @ y.T, probability=True, random_state=0)
    b = base.clone(a)

    b.fit(X_sp, Y)
    pred = b.predict(X_sp)
    b.predict_proba(X_sp)

    dense_svm = svm.SVC(
        C=1, kernel=lambda x, y: np.dot(x, y.T), probability=True, random_state=0
    )
    pred_dense = dense_svm.fit(X, Y).predict(X)
    assert_array_equal(pred_dense, pred)

def test_timeout(lil_container):
    sp = svm.SVC(
        C=1, kernel=lambda x, y: x @ y.T, probability=True, random_state=0, max_iter=1
    )
    warning_msg = (
        r"Solver terminated early \(max_iter=1\).  Consider pre-processing "
        r"your data with StandardScaler or MinMaxScaler."
    )
    with pytest.warns(ConvergenceWarning, match=warning_msg):
        sp.fit(lil_container(X), Y)

def test_consistent_proba():
    a = svm.SVC(probability=True, max_iter=1, random_state=0)
        proba_1 = a.fit(X, Y).predict_proba(X)
        proba_2 = a.fit(X, Y).predict_proba(X)
    assert_allclose(proba_1, proba_2)

def test_l1_min_c(X_container, loss, Y_label, intercept_label):
    Ys = {"two-classes": Y1, "multi-class": Y2}
    intercepts = {
        "no-intercept": {"fit_intercept": False},
        "fit-intercept": {"fit_intercept": True, "intercept_scaling": 10},
    }

    X = X_container(dense_X)
    Y = Ys[Y_label]
    intercept_params = intercepts[intercept_label]
    check_l1_min_c(X, Y, loss, **intercept_params)

def check_l1_min_c(X, y, loss, fit_intercept=True, intercept_scaling=1.0):
    min_c = l1_min_c(
        X,
        y,
        fit_intercept=fit_intercept,
        intercept_scaling=intercept_scaling,
    )

    clf = {
        "log": LogisticRegression(penalty="l1", solver="liblinear"),
        "squared_hinge": LinearSVC(loss="squared_hinge", penalty="l1", dual=False),
    }[loss]

    clf.fit_intercept = fit_intercept
    clf.intercept_scaling = intercept_scaling

    clf.C = min_c
    assert (np.asarray(clf.coef_) == 0).all()
    assert (np.asarray(clf.intercept_) == 0).all()

    clf.C = min_c * 1.01
    assert (np.asarray(clf.coef_) != 0).any() or (np.asarray(clf.intercept_) != 0).any()

def test_ill_posed_min_c():
    X = [[0, 0], [0, 0]]
    y = [0, 1]
        l1_min_c(X, y)

def test_newrand_default():
    """Test that bounded_rand_int_wrap without seeding respects the range

    Note this test should pass either if executed alone, or in conjunctions
    with other tests that call set_seed explicit in any order: it checks
    invariants on the RNG instead of specific values.
    """
    generated = [bounded_rand_int_wrap(100) for _ in range(10)]
    assert all(0 <= x < 100 for x in generated)
    assert not all(x == generated[0] for x in generated)

def test_newrand_set_seed(seed, expected):
    """Test that `set_seed` produces deterministic results"""
    set_seed_wrap(seed)
    generated = bounded_rand_int_wrap(100)
    assert generated == expected

def test_newrand_set_seed_overflow(seed):
    """Test that `set_seed_wrap` is defined for unsigned 32bits ints"""
    with pytest.raises(OverflowError):
        set_seed_wrap(seed)

def test_newrand_bounded_rand_int(range_, n_pts):
    """Test that `bounded_rand_int` follows a uniform distribution"""
    # XXX: this test is very seed sensitive: either it is wrong (too strict?)
    # or the wrapped RNG is not uniform enough, at least on some platforms.
    set_seed_wrap(42)
    n_iter = 100
    ks_pvals = []
    uniform_dist = stats.uniform(loc=0, scale=range_)
    # perform multiple samplings to make chance of outlier sampling negligible
    for _ in range(n_iter):
        # Deterministic random sampling
        sample = [bounded_rand_int_wrap(range_) for _ in range(n_pts)]
        res = stats.kstest(sample, uniform_dist.cdf)
        ks_pvals.append(res.pvalue)
    # Null hypothesis = samples come from an uniform distribution.
    # Under the null hypothesis, p-values should be uniformly distributed
    # and not concentrated on low values
    # (this may seem counter-intuitive but is backed by multiple refs)
    # So we can do two checks:

    # (1) check uniformity of p-values
    uniform_p_vals_dist = stats.uniform(loc=0, scale=1)
    res_pvals = stats.kstest(ks_pvals, uniform_p_vals_dist.cdf)
    assert res_pvals.pvalue > 0.05, (
        "Null hypothesis rejected: generated random numbers are not uniform."
        " Details: the (meta) p-value of the test of uniform distribution"
        f" of p-values is {res_pvals.pvalue} which is not > 0.05"
    )

    # (2) (safety belt) check that 90% of p-values are above 0.05
    min_10pct_pval = np.percentile(ks_pvals, q=10)
    # lower 10th quantile pvalue <= 0.05 means that the test rejects the
    # null hypothesis that the sample came from the uniform distribution
    assert min_10pct_pval > 0.05, (
        "Null hypothesis rejected: generated random numbers are not uniform. "
        f"Details: lower 10th quantile p-value of {min_10pct_pval} not > 0.05."
    )

def test_newrand_bounded_rand_int_limits(range_):
    """Test that `bounded_rand_int_wrap` is defined for unsigned 32bits ints"""
        bounded_rand_int_wrap(range_)

def test_libsvm_parameters():
    # Test parameters on classes that make use of libsvm.
    clf = svm.SVC(kernel="linear").fit(X, Y)
    assert_array_equal(clf.dual_coef_, [[-0.25, 0.25]])
    assert_array_equal(clf.support_, [1, 3])
    assert_array_equal(clf.support_vectors_, (X[1], X[3]))
    assert_array_equal(clf.intercept_, [0.0])
    assert_array_equal(clf.predict(X), Y)

def test_libsvm_iris():

    # shuffle the dataset so that labels are not ordered
    for k in ("linear", "rbf"):
        clf = svm.SVC(kernel=k).fit(iris.data, iris.target)
        assert np.mean(clf.predict(iris.data) == iris.target) > 0.9
        assert hasattr(clf, "coef_") == (k == "linear")

    assert_array_equal(clf.classes_, np.sort(clf.classes_))

    # check also the low-level API
    # We unpack the values to create a dictionary with some of the return values
    # from Libsvm's fit.
    (
        libsvm_support,
        libsvm_support_vectors,
        libsvm_n_class_SV,
        libsvm_sv_coef,
        libsvm_intercept,
        libsvm_probA,
        libsvm_probB,
        # libsvm_fit_status and libsvm_n_iter won't be used below.
        libsvm_fit_status,
        libsvm_n_iter,
    ) = _libsvm.fit(iris.data, iris.target.astype(np.float64))

    model_params = {
        "support": libsvm_support,
        "SV": libsvm_support_vectors,
        "nSV": libsvm_n_class_SV,
        "sv_coef": libsvm_sv_coef,
        "intercept": libsvm_intercept,
        "probA": libsvm_probA,
        "probB": libsvm_probB,
    }
    pred = _libsvm.predict(iris.data, **model_params)
    assert np.mean(pred == iris.target) > 0.95

    (
    ) = _libsvm.fit(iris.data, iris.target.astype(np.float64), kernel="linear")

    }
    pred = _libsvm.predict(iris.data, **model_params, kernel="linear")

    pred = _libsvm.cross_validation(
        iris.data, iris.target.astype(np.float64), 5, kernel="linear", random_seed=0
    )

    # If random_seed >= 0, the libsvm rng is seeded (by calling `srand`), hence
    # we should get deterministic results (assuming that there is no other
    # thread calling this wrapper calling `srand` concurrently).
    pred2 = _libsvm.cross_validation(
    )
    assert_array_equal(pred, pred2)

def test_precomputed():
    # SVC with a precomputed kernel.
    # We test it with a toy dataset and with iris.
    clf = svm.SVC(kernel="precomputed")
    # Gram matrix for train data (square matrix)
    # (we use just a linear kernel)
    K = np.dot(X, np.array(X).T)
    clf.fit(K, Y)
    # Gram matrix for test data (rectangular matrix)
    KT = np.dot(T, np.array(X).T)
    pred = clf.predict(KT)
        clf.predict(KT.T)

    assert_array_equal(clf.intercept_, [0])
    assert_array_almost_equal(clf.support_, [1, 3])
    assert_array_equal(pred, true_result)

    # Gram matrix for test data but compute KT[i,j]
    # for support vectors j only.
    KT = np.zeros_like(KT)
    for i in range(len(T)):
        for j in clf.support_:
            KT[i, j] = np.dot(T[i], X[j])

    # same as before, but using a callable function instead of the kernel
    # matrix. kernel is just a linear kernel

        return np.dot(x, y.T)

    clf = svm.SVC(kernel=kfunc)
    clf.fit(np.array(X), Y)
    pred = clf.predict(T)

    # test a precomputed kernel with the iris dataset
    # and check parameters against a linear SVC
    clf2 = svm.SVC(kernel="linear")
    K = np.dot(iris.data, iris.data.T)
    clf.fit(K, iris.target)
    pred = clf.predict(K)
    assert_array_almost_equal(clf.support_, clf2.support_)
    assert_array_almost_equal(clf.dual_coef_, clf2.dual_coef_)
    assert_array_almost_equal(clf.intercept_, clf2.intercept_)
    assert_almost_equal(np.mean(pred == iris.target), 0.99, decimal=2)

    K = np.zeros_like(K)
    for i in range(len(iris.data)):
            K[i, j] = np.dot(iris.data[i], iris.data[j])

def test_svr():
    # Test Support Vector Regression

    diabetes = datasets.load_diabetes()
    for clf in (
        svm.NuSVR(kernel="linear", nu=0.4, C=1.0),
        svm.NuSVR(kernel="linear", nu=0.4, C=10.0),
        svm.SVR(kernel="linear", C=10.0),
        svm.LinearSVR(C=10.0),
    ):
        clf.fit(diabetes.data, diabetes.target)
        assert clf.score(diabetes.data, diabetes.target) > 0.02

    # non-regression test; previously, BaseLibSVM would check that
    # len(np.unique(y)) < 2, which must only be done for SVC
    svm.SVR().fit(diabetes.data, np.ones(len(diabetes.data)))
    svm.LinearSVR().fit(diabetes.data, np.ones(len(diabetes.data)))

def test_linearsvr():
    # check that SVR(kernel='linear') and LinearSVC() give
    # comparable results
    lsvr = svm.LinearSVR(C=1e3).fit(diabetes.data, diabetes.target)
    score1 = lsvr.score(diabetes.data, diabetes.target)

    svr = svm.SVR(kernel="linear", C=1e3).fit(diabetes.data, diabetes.target)
    score2 = svr.score(diabetes.data, diabetes.target)

    assert_allclose(np.linalg.norm(lsvr.coef_), np.linalg.norm(svr.coef_), 1, 0.0001)
    assert_almost_equal(score1, score2, 2)

def test_linearsvr_fit_sampleweight():
    # check correct result when sample_weight is 1
    n_samples = len(diabetes.target)
    unit_weight = np.ones(n_samples)
    lsvr = svm.LinearSVR(C=1e3, tol=1e-12, max_iter=10000).fit(
        diabetes.data, diabetes.target, sample_weight=unit_weight
    )

    lsvr_no_weight = svm.LinearSVR(C=1e3, tol=1e-12, max_iter=10000).fit(
        diabetes.data, diabetes.target
    )
    score2 = lsvr_no_weight.score(diabetes.data, diabetes.target)

        np.linalg.norm(lsvr.coef_), np.linalg.norm(lsvr_no_weight.coef_), 1, 0.0001
    )

    # check that fit(X)  = fit([X1, X2, X3], sample_weight = [n1, n2, n3]) where
    # X = X1 repeated n1 times, X2 repeated n2 times and so forth
    random_weight = random_state.randint(0, 10, n_samples)
    lsvr_unflat = svm.LinearSVR(C=1e3, tol=1e-12, max_iter=10000).fit(
        diabetes.data, diabetes.target, sample_weight=random_weight
    )
    score3 = lsvr_unflat.score(
    )

    X_flat = np.repeat(diabetes.data, random_weight, axis=0)
    y_flat = np.repeat(diabetes.target, random_weight, axis=0)
    lsvr_flat = svm.LinearSVR(C=1e3, tol=1e-12, max_iter=10000).fit(X_flat, y_flat)
    score4 = lsvr_flat.score(X_flat, y_flat)

    assert_almost_equal(score3, score4, 2)

def test_svr_errors():
    X = [[0.0], [1.0]]
    y = [0.0, 0.5]

    # Bad kernel
    clf = svm.SVR(kernel=lambda x, y: np.array([[1.0]]))

def test_oneclass():
    # Test OneClassSVM
    clf = svm.OneClassSVM()

    assert_array_equal(pred, [1, -1, -1])
    assert pred.dtype == np.dtype("intp")
    assert_array_almost_equal(clf.intercept_, [-1.218], decimal=3)
    assert_array_almost_equal(clf.dual_coef_, [[0.750, 0.750, 0.750, 0.750]], decimal=3)
        (lambda: clf.coef_)()

def test_oneclass_decision_function():
    # Test OneClassSVM decision function
    rnd = check_random_state(2)

    # Generate train data
    X = 0.3 * rnd.randn(100, 2)
    X_train = np.r_[X + 2, X - 2]

    # Generate some regular novel observations
    X = 0.3 * rnd.randn(20, 2)
    X_test = np.r_[X + 2, X - 2]
    X_outliers = rnd.uniform(low=-4, high=4, size=(20, 2))

    clf = svm.OneClassSVM(nu=0.1, kernel="rbf", gamma=0.1)
    clf.fit(X_train)

    # predict things
    y_pred_test = clf.predict(X_test)
    assert np.mean(y_pred_test == 1) > 0.9
    y_pred_outliers = clf.predict(X_outliers)
    assert np.mean(y_pred_outliers == -1) > 0.9
    dec_func_test = clf.decision_function(X_test)
    assert_array_equal((dec_func_test > 0).ravel(), y_pred_test == 1)
    dec_func_outliers = clf.decision_function(X_outliers)
    assert_array_equal((dec_func_outliers > 0).ravel(), y_pred_outliers == 1)

def test_oneclass_score_samples():
    clf = svm.OneClassSVM(gamma=1).fit(X_train)
        clf.score_samples([[2.0, 2.0]]),
        clf.decision_function([[2.0, 2.0]]) + clf.offset_,
    )

def test_tweak_params():
    # Make sure some tweaking of parameters works.
    # We change clf.dual_coef_ at run time and expect .predict() to change
    # accordingly. Notice that this is not trivial since it involves a lot
    # of C/Python copying in the libsvm bindings.
    # The success of this test ensures that the mapping between libsvm and
    # the python classifier is complete.
    clf = svm.SVC(kernel="linear", C=1.0)
    assert_array_equal(clf.predict([[-0.1, -0.1]]), [1])
    clf._dual_coef_ = np.array([[0.0, 1.0]])
    assert_array_equal(clf.predict([[-0.1, -0.1]]), [2])

    # Predict probabilities using SVC
    # This uses cross validation, so we use a slightly bigger testing set.

    for clf in (
        svm.SVC(probability=True, random_state=0, C=1.0),
        svm.NuSVC(probability=True, random_state=0),
    ):

        assert_array_almost_equal(np.sum(prob_predict, 1), np.ones(iris.data.shape[0]))
        assert np.mean(np.argmax(prob_predict, 1) == clf.predict(iris.data)) > 0.9

            clf.predict_proba(iris.data), np.exp(clf.predict_log_proba(iris.data)), 8
        )

def test_decision_function():
    clf = svm.SVC(kernel="linear", C=0.1, decision_function_shape="ovo").fit(
        iris.data, iris.target
    )

    dec = np.dot(iris.data, clf.coef_.T) + clf.intercept_

    assert_array_almost_equal(dec, clf.decision_function(iris.data))

    # binary:
    assert_array_almost_equal(dec.ravel(), clf.decision_function(X))
        prediction, clf.classes_[(clf.decision_function(X) > 0).astype(int)]
    )
    assert_array_almost_equal(clf.decision_function(X), expected, 2)

    # kernel binary:
    clf = svm.SVC(kernel="rbf", gamma=1, decision_function_shape="ovo")

    rbfs = rbf_kernel(X, clf.support_vectors_, gamma=clf.gamma)
    dec = np.dot(rbfs, clf.dual_coef_.T) + clf.intercept_

def test_decision_function_shape(SVM):
    # check that decision_function_shape='ovr' or 'ovo' gives
    # correct shape and is consistent with predict

    clf = SVM(kernel="linear", decision_function_shape="ovr").fit(
    )
    dec = clf.decision_function(iris.data)
    assert dec.shape == (len(iris.data), 3)
    assert_array_equal(clf.predict(iris.data), np.argmax(dec, axis=1))

    # with five classes:
    X, y = make_blobs(n_samples=80, centers=5, random_state=0)

    clf = SVM(kernel="linear", decision_function_shape="ovr").fit(X_train, y_train)
    dec = clf.decision_function(X_test)
    assert dec.shape == (len(X_test), 5)
    assert_array_equal(clf.predict(X_test), np.argmax(dec, axis=1))

    # check shape of ovo_decition_function=True
    clf = SVM(kernel="linear", decision_function_shape="ovo").fit(X_train, y_train)
    dec = clf.decision_function(X_train)
    assert dec.shape == (len(X_train), 10)

def test_svr_predict():
    # Test SVR's decision_function
    # Sanity check, test that predict implemented in python

    # linear kernel
    reg = svm.SVR(kernel="linear", C=0.1).fit(X, y)

    dec = np.dot(X, reg.coef_.T) + reg.intercept_
    assert_array_almost_equal(dec.ravel(), reg.predict(X).ravel())

    # rbf kernel
    reg = svm.SVR(kernel="rbf", gamma=1).fit(X, y)

    rbfs = rbf_kernel(X, reg.support_vectors_, gamma=reg.gamma)
    dec = np.dot(rbfs, reg.dual_coef_.T) + reg.intercept_

def test_weight():
    clf = svm.SVC(class_weight={1: 0.1})
    # we give a small weights to class 1
    # so all predicted values belong to class 2
    assert_array_almost_equal(clf.predict(X), [2] * 6)

        n_samples=200, n_features=10, weights=[0.833, 0.167], random_state=2
    )

    for clf in (
    ):
        clf.set_params(class_weight={0: 0.1, 1: 10})
        clf.fit(X_[:100], y_[:100])
        y_pred = clf.predict(X_[100:])
        assert f1_score(y_[100:], y_pred) > 0.3

def test_svm_classifier_sided_sample_weight(estimator):
    # fit a linear SVM and check that giving more weight to opposed samples
    # in the space will flip the decision toward these samples.
    X = [[-2, 0], [-1, -1], [0, -2], [0, 2], [1, 1], [2, 0]]
    estimator.set_params(kernel="linear")

    # check that with unit weights, a sample is supposed to be predicted on
    # the boundary
    sample_weight = [1] * 6
    estimator.fit(X, Y, sample_weight=sample_weight)
    y_pred = estimator.decision_function([[-1.0, 1.0]])
    assert y_pred == pytest.approx(0)

    # give more weights to opposed samples
    sample_weight = [10.0, 0.1, 0.1, 0.1, 0.1, 10]
    assert y_pred < 0

    sample_weight = [1.0, 0.1, 10.0, 10.0, 0.1, 0.1]
    assert y_pred > 0

def test_svm_regressor_sided_sample_weight(estimator):
    # similar test to test_svm_classifier_sided_sample_weight but for
    # SVM regressors

    y_pred = estimator.predict([[-1.0, 1.0]])
    assert y_pred == pytest.approx(1.5)

    assert y_pred < 1.5

    assert y_pred > 1.5

def test_svm_equivalence_sample_weight_C():
    # test that rescaling all samples is the same as changing C
    dual_coef_no_weight = clf.dual_coef_
    clf.set_params(C=100)
    clf.fit(X, Y, sample_weight=np.repeat(0.01, len(X)))
    assert_allclose(dual_coef_no_weight, clf.dual_coef_)

def test_negative_sample_weights_mask_all_samples(Estimator, err_msg, sample_weight):
    est = Estimator(kernel="linear")
        est.fit(X, Y, sample_weight=sample_weight)

def test_negative_weights_svc_leave_just_one_label(Classifier, err_msg, sample_weight):
    clf = Classifier(kernel="linear")
        clf.fit(X, Y, sample_weight=sample_weight)

def test_negative_weights_svc_leave_two_labels(
    Classifier, model, sample_weight, mask_side
):
    clf.fit(X, Y, sample_weight=sample_weight)
    assert_allclose(clf.coef_, [model[mask_side]], rtol=1e-3)

def test_negative_weight_equal_coeffs(Estimator, sample_weight):
    # model generates equal coefficients
    est.fit(X, Y, sample_weight=sample_weight)
    coef = np.abs(est.coef_).ravel()
    assert coef[0] == pytest.approx(coef[1], rel=1e-3)

def test_auto_weight():
    # Test class weights for imbalanced data
    from sklearn.linear_model import LogisticRegression

    # We take as dataset the two-dimensional projection of iris so
    # that it is not separable and remove half of predictors from
    # class 1.
    # We add one to the targets as a non-regression test:
    # class_weight="balanced"
    # used to work only when the labels where a range [0..K).
    from sklearn.utils import compute_class_weight

    X, y = iris.data[:, :2], iris.target + 1
    unbalanced = np.delete(np.arange(y.size), np.where(y > 2)[0][::2])

    classes = np.unique(y[unbalanced])
    class_weights = compute_class_weight("balanced", classes=classes, y=y[unbalanced])
    assert np.argmax(class_weights) == 2

    for clf in (
        svm.SVC(kernel="linear"),
    ):
        # check that score is better when class='balanced' is set.
        y_pred = clf.fit(X[unbalanced], y[unbalanced]).predict(X)
        clf.set_params(class_weight="balanced")
        y_pred_balanced = clf.fit(
            X[unbalanced],
            y[unbalanced],
        ).predict(X)
        assert metrics.f1_score(y, y_pred, average="macro") <= metrics.f1_score(
            y, y_pred_balanced, average="macro"
        )

def test_bad_input(lil_container):
    # Test dimensions for labels
        svm.SVC().fit(X, Y2)

    # Test with arrays that are non-contiguous.
    for clf in (svm.SVC(), svm.LinearSVC(random_state=0)):
        assert not Xf.flags["C_CONTIGUOUS"]
        yf = np.ascontiguousarray(np.tile(Y, (2, 1)).T)
        yf = yf[:, -1]
        assert not yf.flags["F_CONTIGUOUS"]
        assert not yf.flags["C_CONTIGUOUS"]
        clf.fit(Xf, yf)
        assert_array_equal(clf.predict(T), true_result)

    # error for precomputed kernelsx
        clf.fit(X, Y)

    # predict with sparse input when trained with dense
    clf = svm.SVC().fit(X, Y)
        clf.predict(lil_container(X))

    Xt = np.array(X).T
    clf.fit(np.dot(X, Xt), Y)

        clf.predict(Xt)

def test_svc_nonfinite_params():
    # Check SVC throws ValueError when dealing with non-finite parameter values
    y = rng.randint(0, 2, size=n_samples)

    msg = "The dual coefficients or intercepts are not finite"

def test_unicode_kernel():
    # Test that a unicode kernel name does not cause a TypeError
    clf = svm.SVC(kernel="linear", probability=True)
    clf.predict_proba(T)
    _libsvm.cross_validation(
    )

def test_sparse_precomputed(csr_container):
    sparse_gram = csr_container([[1, 0], [0, 1]])
    with pytest.raises(TypeError, match="Sparse precomputed"):
        clf.fit(sparse_gram, [0, 1])

def test_sparse_fit_support_vectors_empty(csr_container):
    # Regression test for #14893
    X_train = csr_container([[0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1]])
    y_train = np.array([0.04, 0.04, 0.10, 0.16])
    model = svm.SVR(kernel="linear")
    assert not model.support_vectors_.data.size
    assert not model.dual_coef_.data.size

def test_linearsvc_parameters(loss, penalty, dual):
    # Test possible parameter combinations in LinearSVC
    # Generate list of possible parameter combinations
    X, y = make_classification(n_samples=5, n_features=5, random_state=0)

    clf = svm.LinearSVC(penalty=penalty, loss=loss, dual=dual, random_state=0)
    if (
        (loss, penalty) == ("hinge", "l1")
        or (loss, penalty, dual) == ("hinge", "l2", False)
        or (penalty, dual) == ("l1", True)
    ):
            ValueError,
            match="Unsupported set of arguments.*penalty='%s.*loss='%s.*dual=%s"
            % (penalty, loss, dual),
        ):
    else:

def test_linearsvc():
    # Test basic routines using LinearSVC

    # by default should have intercept
    assert clf.fit_intercept

    assert_array_almost_equal(clf.intercept_, [0], decimal=3)

    # the same with l1 penalty
    clf = svm.LinearSVC(
        penalty="l1", loss="squared_hinge", dual=False, random_state=0
    ).fit(X, Y)

    # l2 penalty with dual formulation
    clf = svm.LinearSVC(penalty="l2", dual=True, random_state=0).fit(X, Y)

    # l2 penalty, l1 loss
    clf = svm.LinearSVC(penalty="l2", loss="hinge", dual=True, random_state=0)

    # test also decision function
    dec = clf.decision_function(T)
    res = (dec > 0).astype(int) + 1
    assert_array_equal(res, true_result)

def test_linearsvc_crammer_singer():
    # Test LinearSVC with crammer_singer multi-class svm
    ovr_clf = svm.LinearSVC(random_state=0).fit(iris.data, iris.target)
    cs_clf = svm.LinearSVC(multi_class="crammer_singer", random_state=0)
    cs_clf.fit(iris.data, iris.target)

    # similar prediction for ovr and crammer-singer:
    assert (ovr_clf.predict(iris.data) == cs_clf.predict(iris.data)).mean() > 0.9

    # classifiers shouldn't be the same
    assert (ovr_clf.coef_ != cs_clf.coef_).all()

    # test decision function
        cs_clf.predict(iris.data),
        np.argmax(cs_clf.decision_function(iris.data), axis=1),
    )
    dec_func = np.dot(iris.data, cs_clf.coef_.T) + cs_clf.intercept_
    assert_array_almost_equal(dec_func, cs_clf.decision_function(iris.data))

def test_linearsvc_fit_sampleweight():
    n_samples = len(X)
    clf_unitweight = svm.LinearSVC(random_state=0, tol=1e-12, max_iter=1000).fit(
        X, Y, sample_weight=unit_weight
    )

    # check if same as sample_weight=None
    assert_array_equal(clf_unitweight.predict(T), clf.predict(T))
    assert_allclose(clf.coef_, clf_unitweight.coef_, 1, 0.0001)

    # check that fit(X)  = fit([X1, X2, X3],sample_weight = [n1, n2, n3]) where

    lsvc_unflat = svm.LinearSVC(random_state=0, tol=1e-12, max_iter=1000).fit(
        X, Y, sample_weight=random_weight
    )

    pred1 = lsvc_unflat.predict(T)

    X_flat = np.repeat(X, random_weight, axis=0)
    y_flat = np.repeat(Y, random_weight, axis=0)
    lsvc_flat = svm.LinearSVC(random_state=0, tol=1e-12, max_iter=1000).fit(
        X_flat, y_flat
    )
    pred2 = lsvc_flat.predict(T)

    assert_allclose(lsvc_unflat.coef_, lsvc_flat.coef_, 1, 0.0001)

def test_crammer_singer_binary():
    # Test Crammer-Singer formulation in the binary case
    X, y = make_classification(n_classes=2, random_state=0)

    for fit_intercept in (True, False):
        acc = (
            svm.LinearSVC(
                fit_intercept=fit_intercept,
                multi_class="crammer_singer",
            )
            .fit(X, y)
            .score(X, y)
        )
        assert acc > 0.9

def test_linearsvc_iris():
    # Test that LinearSVC gives plausible predictions on the iris dataset
    # Also, test symbolic class names (classes_).
    target = iris.target_names[iris.target]
    clf = svm.LinearSVC(random_state=0).fit(iris.data, target)
    assert set(clf.classes_) == set(iris.target_names)
    assert np.mean(clf.predict(iris.data) == target) > 0.8

    pred = iris.target_names[np.argmax(dec, 1)]
    assert_array_equal(pred, clf.predict(iris.data))

def test_dense_liblinear_intercept_handling(classifier=svm.LinearSVC):
    # Test that dense liblinear honours intercept_scaling param
    X = [[2, 1], [3, 1], [1, 3], [2, 3]]
    clf = classifier(
        penalty="l1",
        dual=False,
        C=4,
        tol=1e-7,
    )
    assert clf.intercept_scaling == 1, clf.intercept_scaling

    # when intercept_scaling is low the intercept value is highly "penalized"
    # by regularization
    clf.intercept_scaling = 1
    assert_almost_equal(clf.intercept_, 0, decimal=5)

    # when intercept_scaling is sufficiently high, the intercept value
    # is not affected by regularization
    clf.intercept_scaling = 100
    intercept1 = clf.intercept_
    assert intercept1 < -1

    # doesn't depend on intercept_scaling value
    clf.intercept_scaling = 1000
    intercept2 = clf.intercept_
    assert_array_almost_equal(intercept1, intercept2, decimal=2)

def test_liblinear_set_coef():
    # multi-class case
    clf = svm.LinearSVC().fit(iris.data, iris.target)
    values = clf.decision_function(iris.data)
    clf.coef_ = clf.coef_.copy()
    clf.intercept_ = clf.intercept_.copy()
    values2 = clf.decision_function(iris.data)
    assert_array_almost_equal(values, values2)

    # binary-class case

    clf = svm.LinearSVC().fit(X, y)
    values = clf.decision_function(X)
    values2 = clf.decision_function(X)
    assert_array_equal(values, values2)

def test_immutable_coef_property():
    # Check that primal coef modification are not silently ignored
    svms = [
        svm.SVC(kernel="linear").fit(iris.data, iris.target),
        svm.NuSVC(kernel="linear").fit(iris.data, iris.target),
        svm.SVR(kernel="linear").fit(iris.data, iris.target),
        svm.NuSVR(kernel="linear").fit(iris.data, iris.target),
        svm.OneClassSVM(kernel="linear").fit(iris.data),
    ]
    for clf in svms:
        with pytest.raises(AttributeError):
            clf.__setattr__("coef_", np.arange(3))
        with pytest.raises((RuntimeError, ValueError)):
            clf.coef_.__setitem__((0, 0), 0)

def test_linearsvc_verbose():
    # stdout: redirect
    import os

    stdout = os.dup(1)  # save original stdout
    os.dup2(os.pipe()[1], 1)  # replace it

    # actual call
    clf = svm.LinearSVC(verbose=1)

    # stdout: restore
    os.dup2(stdout, 1)  # restore original stdout

def test_svc_clone_with_callable_kernel():
    # create SVM with callable linear kernel, check that results are the same
    # as with built-in linear kernel
    svm_callable = svm.SVC(
        kernel=lambda x, y: np.dot(x, y.T),
    )
    # clone for checking clonability with lambda functions..
    svm_cloned = base.clone(svm_callable)
    svm_cloned.fit(iris.data, iris.target)

    svm_builtin = svm.SVC(
        kernel="linear", probability=True, random_state=0, decision_function_shape="ovr"
    )
    svm_builtin.fit(iris.data, iris.target)

    assert_array_almost_equal(svm_cloned.dual_coef_, svm_builtin.dual_coef_)
    assert_array_almost_equal(svm_cloned.intercept_, svm_builtin.intercept_)
    assert_array_equal(svm_cloned.predict(iris.data), svm_builtin.predict(iris.data))

        svm_cloned.predict_proba(iris.data),
        svm_builtin.predict_proba(iris.data),
        decimal=4,
    )
        svm_cloned.decision_function(iris.data),
        svm_builtin.decision_function(iris.data),
    )

def test_svc_bad_kernel():
    svc = svm.SVC(kernel=lambda x, y: x)
        svc.fit(X, Y)

def test_libsvm_convergence_warnings():
    a = svm.SVC(
        kernel=lambda x, y: np.dot(x, y.T), probability=True, random_state=0, max_iter=2
    )
        r"Solver terminated early \(max_iter=2\).  Consider pre-processing "
    )
        a.fit(np.array(X), Y)
    assert np.all(a.n_iter_ == 2)

def test_unfitted():
    X = "foo!"  # input validation not required when SVM not fitted

    with pytest.raises(Exception, match=r".*\bSVC\b.*\bnot\b.*\bfitted\b"):

    clf = svm.NuSVR()
    with pytest.raises(Exception, match=r".*\bNuSVR\b.*\bnot\b.*\bfitted\b"):

    proba_1 = a.fit(X, Y).predict_proba(X)
    proba_2 = a.fit(X, Y).predict_proba(X)
    assert_array_almost_equal(proba_1, proba_2)

def test_linear_svm_convergence_warnings():
    # Test that warnings are raised if model does not converge

    lsvc = svm.LinearSVC(random_state=0, max_iter=2)
    warning_msg = "Liblinear failed to converge, increase the number of iterations."
        lsvc.fit(X, Y)
    # Check that we have an n_iter_ attribute with int type as opposed to a
    # numpy array or an np.int32 so as to match the docstring.
    assert isinstance(lsvc.n_iter_, int)
    assert lsvc.n_iter_ == 2

    lsvr = svm.LinearSVR(random_state=0, max_iter=2)
        lsvr.fit(iris.data, iris.target)
    assert isinstance(lsvr.n_iter_, int)
    assert lsvr.n_iter_ == 2

def test_svr_coef_sign():
    # Test that SVR(kernel="linear") has coef_ with the right sign.
    # Non-regression test for #2933.
    X = np.random.RandomState(21).randn(10, 3)
    y = np.random.RandomState(12).randn(10)

    for svr in [
        svm.SVR(kernel="linear"),
        svm.NuSVR(kernel="linear"),
        svm.LinearSVR(),
    ]:
        svr.fit(X, y)
            svr.predict(X), np.dot(X, svr.coef_.ravel()) + svr.intercept_
        )

def test_lsvc_intercept_scaling_zero():
    # Test that intercept_scaling is ignored when fit_intercept is False

    lsvc = svm.LinearSVC(fit_intercept=False)
    lsvc.fit(X, Y)
    assert lsvc.intercept_ == 0.0

def test_hasattr_predict_proba():
    # Method must be (un)available before or after fit, switched by
    # `probability` param

    G = svm.SVC(probability=True)
    assert hasattr(G, "predict_proba")
    G.fit(iris.data, iris.target)

    G = svm.SVC(probability=False)
    assert not hasattr(G, "predict_proba")

    # Switching to `probability=True` after fitting should make
    # predict_proba available, but calling it must not work:
    G.probability = True
    msg = "predict_proba is not available when fitted with probability=False"

    with pytest.raises(NotFittedError, match=msg):
        G.predict_proba(iris.data)

def test_decision_function_shape_two_class():
    for n_classes in [2, 3]:
        X, y = make_blobs(centers=n_classes, random_state=0)
        for estimator in [svm.SVC, svm.NuSVC]:
            clf = OneVsRestClassifier(estimator(decision_function_shape="ovr")).fit(
                X, y
            )
            assert len(clf.predict(X)) == len(y)

def test_ovr_decision_function():
    # One point from each quadrant represents one class
    X_train = np.array([[1, 1], [-1, 1], [-1, -1], [1, -1]])
    y_train = [0, 1, 2, 3]

    # First point is closer to the decision boundaries than the second point
    base_points = np.array([[5, 5], [10, 10]])

    # For all the quadrants (classes)
    X_test = np.vstack(
        (
            base_points * [1, 1],  # Q1
            base_points * [-1, 1],  # Q2
            base_points * [-1, -1],  # Q3
            base_points * [1, -1],  # Q4
        )
    )

    y_test = [0] * 2 + [1] * 2 + [2] * 2 + [3] * 2

    clf = svm.SVC(kernel="linear", decision_function_shape="ovr")

    # Test if the prediction is the same as y

    deci_val = clf.decision_function(X_test)

    # Assert that the predicted class has the maximum value
    assert_array_equal(np.argmax(deci_val, axis=1), y_pred)

    # Get decision value at test points for the predicted class
    pred_class_deci_val = deci_val[range(8), y_pred].reshape((4, 2))

    # Assert pred_class_deci_val > 0 here
    assert np.min(pred_class_deci_val) > 0.0

    # Test if the first point has lower decision value on every quadrant
    # compared to the second point
    assert np.all(pred_class_deci_val[:, 0] < pred_class_deci_val[:, 1])

def test_svc_invalid_break_ties_param(SVCClass):
    X, y = make_blobs(random_state=42)

    svm = SVCClass(
        kernel="linear", decision_function_shape="ovo", break_ties=True, random_state=42

    with pytest.raises(ValueError, match="break_ties must be False"):
        svm.predict(y)

def test_svc_ovr_tie_breaking(SVCClass):
    """Test if predict breaks ties in OVR mode.
    Related issue: https://github.com/scikit-learn/scikit-learn/issues/8277
    """
    if SVCClass.__name__ == "NuSVC" and _IS_32BIT:
        # XXX: known failure to be investigated. Either the code needs to be
        # fixed or the test itself might need to be made less sensitive to
        # random changes in test data and rounding errors more generally.
        # https://github.com/scikit-learn/scikit-learn/issues/29633
        pytest.xfail("Failing test on 32bit OS")

    X, y = make_blobs(random_state=0, n_samples=20, n_features=2)

    xs = np.linspace(X[:, 0].min(), X[:, 0].max(), 100)
    ys = np.linspace(X[:, 1].min(), X[:, 1].max(), 100)
    xx, yy = np.meshgrid(xs, ys)

    common_params = dict(
        kernel="rbf", gamma=1e6, random_state=42, decision_function_shape="ovr"
    )
        **common_params,
    pred = svm.predict(np.c_[xx.ravel(), yy.ravel()])
    dv = svm.decision_function(np.c_[xx.ravel(), yy.ravel()])
    assert not np.all(pred == np.argmax(dv, axis=1))

        break_ties=True,
    assert np.all(pred == np.argmax(dv, axis=1))

def test_gamma_scale():
    X, y = [[0.0], [1.0]], [0, 1]

    assert_almost_equal(clf._gamma, 4)

def test_linearsvm_liblinear_sample_weight(SVM, params):
        [
            [1, 3],
            [1, 3],
            [1, 3],
            [1, 3],
            [2, 1],
            [2, 1],
            [2, 1],
            [2, 1],
            [3, 3],
            [3, 3],
            [3, 3],
            [3, 3],
            [4, 1],
            [4, 1],
            [4, 1],
            [4, 1],
        ],
        dtype=np.dtype("float"),
    )
        [1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2], dtype=np.dtype("int")
    )

    X2 = np.vstack([X, X])
    y2 = np.hstack([y, 3 - y])
    sample_weight = np.ones(shape=len(y) * 2)
    sample_weight[len(y) :] = 0
    X2, y2, sample_weight = shuffle(X2, y2, sample_weight, random_state=0)

    base_estimator = SVM(random_state=42)
    base_estimator.set_params(**params)
    base_estimator.set_params(tol=1e-12, max_iter=1000)
    est_no_weight = base.clone(base_estimator).fit(X, y)
    est_with_weight = base.clone(base_estimator).fit(
        X2, y2, sample_weight=sample_weight
    )

    for method in ("predict", "decision_function"):
        if hasattr(base_estimator, method):
            X_est_no_weight = getattr(est_no_weight, method)(X)
            X_est_with_weight = getattr(est_with_weight, method)(X)
            assert_allclose(X_est_no_weight, X_est_with_weight)

def test_n_support(Klass):
    # Make n_support is correct for oneclass and SVR (used to be
    # non-initialized)
    # this is a non regression test for issue #14774
    X = np.array([[0], [0.44], [0.45], [0.46], [1]])
    est = Klass()
    assert not hasattr(est, "n_support_")
    assert est.n_support_[0] == est.support_vectors_.shape[0]
    assert est.n_support_.size == 1

def test_custom_kernel_not_array_input(Estimator):
    """Test using a custom kernel that is not fed with array-like for floats"""
    data = ["A A", "A", "B", "B B", "A B"]
    X = np.array([[2, 0], [1, 0], [0, 1], [0, 2], [1, 1]])  # count encoding
    y = np.array([1, 1, 2, 2, 1])

    def string_kernel(X1, X2):
        assert isinstance(X1[0], str)
        n_samples1 = _num_samples(X1)
        n_samples2 = _num_samples(X2)
        K = np.zeros((n_samples1, n_samples2))
        for ii in range(n_samples1):
            for jj in range(ii, n_samples2):
                K[ii, jj] = X1[ii].count("A") * X2[jj].count("A")
                K[ii, jj] += X1[ii].count("B") * X2[jj].count("B")
                K[jj, ii] = K[ii, jj]
        return K

    K = string_kernel(data, data)
    assert_array_equal(np.dot(X, X.T), K)

    svc1 = Estimator(kernel=string_kernel).fit(data, y)
    svc2 = Estimator(kernel="linear").fit(X, y)
    svc3 = Estimator(kernel="precomputed").fit(K, y)

    assert svc1.score(data, y) == svc3.score(K, y)
    assert svc1.score(data, y) == svc2.score(X, y)
    if hasattr(svc1, "decision_function"):  # classifier
        assert_allclose(svc1.decision_function(data), svc2.decision_function(X))
        assert_allclose(svc1.decision_function(data), svc3.decision_function(K))
        assert_array_equal(svc1.predict(data), svc2.predict(X))
        assert_array_equal(svc1.predict(data), svc3.predict(K))
    else:  # regressor
        assert_allclose(svc1.predict(data), svc2.predict(X))
        assert_allclose(svc1.predict(data), svc3.predict(K))

def test_svc_raises_error_internal_representation():
    """Check that SVC raises error when internal representation is altered.

    Non-regression test for #18891 and https://nvd.nist.gov/vuln/detail/CVE-2020-28975
    """
    clf._n_support[0] = 1000000

    msg = "The internal representation of SVC was altered"

def test_n_iter_libsvm(estimator, expected_n_iter_type, dataset):
    # Check that the type of n_iter_ is correct for the classes that inherit
    # from BaseSVC.
    # Note that for SVC, and NuSVC this is an ndarray; while for SVR, NuSVR, and
    # OneClassSVM, it is an int.
    # For SVC and NuSVC also check the shape of n_iter_.
    X, y = dataset
    n_iter = estimator(kernel="linear").fit(X, y).n_iter_
    assert type(n_iter) == expected_n_iter_type
    if estimator in [svm.SVC, svm.NuSVC]:
        n_classes = len(np.unique(y))
        assert n_iter.shape == (n_classes * (n_classes - 1) // 2,)

def test_dual_auto(loss):
    # OvR, L2, N > M (6,2)
    dual = _validate_dual_parameter("auto", loss, "l2", "ovr", np.asarray(X))
    assert dual is False
    # OvR, L2, N < M (2,6)
    dual = _validate_dual_parameter("auto", loss, "l2", "ovr", np.asarray(X).T)
    assert dual is True

def test_dual_auto_edge_cases():
    # Hinge, OvR, L2, N > M (6,2)
    dual = _validate_dual_parameter("auto", "hinge", "l2", "ovr", np.asarray(X))
    assert dual is True  # only supports True
    dual = _validate_dual_parameter(
        "auto", "epsilon_insensitive", "l2", "ovr", np.asarray(X)
    )
    # SqHinge, OvR, L1, N < M (2,6)
        "auto", "squared_hinge", "l1", "ovr", np.asarray(X).T
    )
    assert dual is False  # only supports False

def test_svm_with_infinite_C(Estimator, make_dataset, C_inf, global_random_seed):
    """Check that we can pass `C=inf` that is equivalent to a very large C value.

    Non-regression test for
    https://github.com/scikit-learn/scikit-learn/issues/29772
    """
    X, y = make_dataset(random_state=global_random_seed)
    estimator_C_inf = Estimator(C=C_inf).fit(X, y)
    estimator_C_large = Estimator(C=1e10).fit(X, y)

    assert_allclose(estimator_C_large.predict(X), estimator_C_inf.predict(X))

        return K

def process_tempita(fromfile, outfile=None):
    """Process tempita templated file and write out the result.

    The template file is expected to end in `.c.tp` or `.pyx.tp`:
    E.g. processing `template.c.in` generates `template.c`.

    """
    with open(fromfile, "r", encoding="utf-8") as f:
        template_content = f.read()

    template = tempita.Template(template_content)
    content = template.substitute()

    with open(outfile, "w", encoding="utf-8") as f:
        f.write(content)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("infile", type=str, help="Path to the input file")
    parser.add_argument("-o", "--outdir", type=str, help="Path to the output directory")
    parser.add_argument(
        "-i",
        "--ignore",
        type=str,
        help=(
            "An ignored input - may be useful to add a "
            "dependency between custom targets"
        ),
    )
    args = parser.parse_args()

    if not args.infile.endswith(".tp"):
        raise ValueError(f"Unexpected extension: {args.infile}")

    if not args.outdir:
        raise ValueError("Missing `--outdir` argument to tempita.py")

    outdir_abs = os.path.join(os.getcwd(), args.outdir)
    outfile = os.path.join(
        outdir_abs, os.path.splitext(os.path.split(args.infile)[1])[0]
    )

    process_tempita(args.infile, outfile)

def kmeans_plusplus(
    X,
    *,
    x_squared_norms=None,
    n_local_trials=None,
):
    """Init n_clusters seeds according to k-means++.

        The data to pick seeds from.

        The number of centroids to initialize.

        The weights for each observation in `X`. If `None`, all observations
        are assigned equal weight. `sample_weight` is ignored if `init`
        is a callable or a user provided array.

    x_squared_norms : array-like of shape (n_samples,), default=None
        Squared Euclidean norm of each data point.

    random_state : int or RandomState instance, default=None
        Determines random number generation for centroid initialization. Pass
        an int for reproducible output across multiple function calls.

    n_local_trials : int, default=None
        The number of seeding trials for each center (except the first),
        of which the one reducing inertia the most is greedily chosen.
        Set to None to make the number of trials depend logarithmically
        on the number of seeds (2+log(k)) which is the recommended setting.
        Setting to 1 disables the greedy cluster selection and recovers the
        vanilla k-means++ algorithm which was empirically shown to work less
        well than its greedy variant.

    Returns
    -------
    centers : ndarray of shape (n_clusters, n_features)
        The initial centers for k-means.

    indices : ndarray of shape (n_clusters,)
        The index location of the chosen centers in the data array X. For a
        given index and center, X[index] = center.

    Notes
    -----
    Selects initial cluster centers for k-mean clustering in a smart way
    to speed up convergence. see: Arthur, D. and Vassilvitskii, S.
    "k-means++: the advantages of careful seeding". ACM-SIAM symposium
    on Discrete algorithms. 2007

    Examples
    --------

    >>> from sklearn.cluster import kmeans_plusplus
    >>> X = np.array([[1, 2], [1, 4], [1, 0],
    ...               [10, 2], [10, 4], [10, 0]])
    >>> centers, indices = kmeans_plusplus(X, n_clusters=2, random_state=0)
    >>> centers
    array([[10,  2],
           [ 1,  0]])
    >>> indices
    array([3, 2])
    """
    # Check data
    check_array(X, accept_sparse="csr", dtype=[np.float64, np.float32])
    sample_weight = _check_sample_weight(sample_weight, X, dtype=X.dtype)

    if X.shape[0] < n_clusters:
            f"n_samples={X.shape[0]} should be >= n_clusters={n_clusters}."
        )

    # Check parameters
    if x_squared_norms is None:
        x_squared_norms = row_norms(X, squared=True)
    else:
        x_squared_norms = check_array(x_squared_norms, dtype=X.dtype, ensure_2d=False)

    if x_squared_norms.shape[0] != X.shape[0]:
            f"The length of x_squared_norms {x_squared_norms.shape[0]} should "
            f"be equal to the length of n_samples {X.shape[0]}."
        )

    # Call private k-means++
    centers, indices = _kmeans_plusplus(
        X, n_clusters, x_squared_norms, sample_weight, random_state, n_local_trials
    )

    return centers, indices

def _kmeans_plusplus(
    X, n_clusters, x_squared_norms, sample_weight, random_state, n_local_trials=None
):
    """Computational component for initialization of n_clusters by
    k-means++. Prior validation of data is assumed.

    X : {ndarray, sparse matrix} of shape (n_samples, n_features)
        The data to pick seeds for.

        The number of seeds to choose.

        The weights for each observation in `X`.

    x_squared_norms : ndarray of shape (n_samples,)

    random_state : RandomState instance
        The generator used to initialize the centers.

        on the number of seeds (2+log(k)); this is the default.

    Returns
    -------

    """

    centers = np.empty((n_clusters, n_features), dtype=X.dtype)

    # Set the number of local seeding trials if none is given
    if n_local_trials is None:
        # This is what Arthur/Vassilvitskii tried, but did not report
        # specific results for other than mentioning in the conclusion
        # that it helped.
        n_local_trials = 2 + int(np.log(n_clusters))

    # Pick first center randomly and track index of point
    center_id = random_state.choice(n_samples, p=sample_weight / sample_weight.sum())
    indices = np.full(n_clusters, -1, dtype=int)
        centers[0] = X[[center_id]].toarray()
    else:
        centers[0] = X[center_id]
    indices[0] = center_id

    # Initialize list of closest distances and calculate current potential
    closest_dist_sq = _euclidean_distances(
        centers[0, np.newaxis], X, Y_norm_squared=x_squared_norms, squared=True
    )
    current_pot = closest_dist_sq @ sample_weight

    # Pick the remaining n_clusters-1 points
    for c in range(1, n_clusters):
        # Choose center candidates by sampling with probability proportional
        # to the squared distance to the closest existing center
        rand_vals = random_state.uniform(size=n_local_trials) * current_pot
        candidate_ids = np.searchsorted(
            stable_cumsum(sample_weight * closest_dist_sq), rand_vals
        )
        # XXX: numerical imprecision can result in a candidate_id out of range
        np.clip(candidate_ids, None, closest_dist_sq.size - 1, out=candidate_ids)

        # Compute distances to center candidates
        distance_to_candidates = _euclidean_distances(
            X[candidate_ids], X, Y_norm_squared=x_squared_norms, squared=True
        )

        # update closest distances squared and potential for each candidate
        np.minimum(closest_dist_sq, distance_to_candidates, out=distance_to_candidates)
        candidates_pot = distance_to_candidates @ sample_weight.reshape(-1, 1)

        # Decide which candidate is the best
        best_candidate = np.argmin(candidates_pot)
        current_pot = candidates_pot[best_candidate]
        closest_dist_sq = distance_to_candidates[best_candidate]
        best_candidate = candidate_ids[best_candidate]

        # Permanently add best center candidate found in local tries
            centers[c] = X[[best_candidate]].toarray()
        else:
            centers[c] = X[best_candidate]
        indices[c] = best_candidate

def _tolerance(X, tol):
    """Return a tolerance which is dependent on the dataset."""
    if tol == 0:
        return 0
        variances = mean_variance_axis(X, axis=0)[1]
    else:
        variances = np.var(X, axis=0)
    return np.mean(variances) * tol

def k_means(
    X,
    *,
    init="k-means++",
    n_init="auto",
    tol=1e-4,
    copy_x=True,
    algorithm="lloyd",
):
    """Perform K-means clustering algorithm.

    Read more in the :ref:`User Guide <k_means>`.

        The observations to cluster. It must be noted that the data
        will be converted to C ordering, which will cause a memory copy
        if the given data is not C-contiguous.

        The number of clusters to form as well as the number of
        centroids to generate.

        are assigned equal weight. `sample_weight` is not used during
        initialization if `init` is a callable or a user provided array.

    init : {'k-means++', 'random'}, callable or array-like of shape \
            (n_clusters, n_features), default='k-means++'
        Method for initialization:

        - `'k-means++'` : selects initial cluster centers for k-mean
          clustering in a smart way to speed up convergence. See section
          Notes in k_init for more details.
        - `'random'`: choose `n_clusters` observations (rows) at random from data
          for the initial centroids.
        - If an array is passed, it should be of shape `(n_clusters, n_features)`
          and gives the initial centers.
        - If a callable is passed, it should take arguments `X`, `n_clusters` and a
          random state and return an initialization.

    n_init : 'auto' or int, default="auto"
        Number of time the k-means algorithm will be run with different
        centroid seeds. The final results will be the best output of
        n_init consecutive runs in terms of inertia.

        When `n_init='auto'`, the number of runs depends on the value of init:
        10 if using `init='random'` or `init` is a callable;
        1 if using `init='k-means++'` or `init` is an array-like.

           Added 'auto' option for `n_init`.

           Default value for `n_init` changed to `'auto'`.

        Maximum number of iterations of the k-means algorithm to run.

        Verbosity mode.

        Relative tolerance with regards to Frobenius norm of the difference
        in the cluster centers of two consecutive iterations to declare

        Determines random number generation for centroid initialization. Use
        an int to make the randomness deterministic.

    copy_x : bool, default=True
        When pre-computing distances it is more numerically accurate to center
        the data first. If `copy_x` is True (default), then the original data is
        not modified. If False, the original data is modified, and put back
        before the function returns, but small numerical differences may be
        introduced by subtracting and then adding the data mean. Note that if
        the original data is not C-contiguous, a copy will be made even if
        `copy_x` is False. If the original data is sparse, but not in CSR format,
        a copy will be made even if `copy_x` is False.

    algorithm : {"lloyd", "elkan"}, default="lloyd"
        K-means algorithm to use. The classical EM-style algorithm is `"lloyd"`.
        The `"elkan"` variation can be more efficient on some datasets with
        well-defined clusters, by using the triangle inequality. However it's
        more memory intensive due to the allocation of an extra array of shape
        `(n_samples, n_clusters)`.

            Added Elkan algorithm

            Renamed "full" to "lloyd", and deprecated "auto" and "full".
            Changed "auto" to use "lloyd" instead of "elkan".

    Returns
    -------
    centroid : ndarray of shape (n_clusters, n_features)
        Centroids found at the last iteration of k-means.

    label : ndarray of shape (n_samples,)
        The `label[i]` is the code or index of the centroid the
        i'th observation is closest to.

    inertia : float
        The final value of the inertia criterion (sum of squared distances to
        the closest centroid for all observations in the training set).

    best_n_iter : int
        Number of iterations corresponding to the best results.
        Returned only if `return_n_iter` is set to True.

    Examples
    --------
    >>> from sklearn.cluster import k_means
    >>> centroid, label, inertia = k_means(
    ...     X, n_clusters=2, n_init="auto", random_state=0
    ... )
    >>> centroid
    array([[10.,  2.],
           [ 1.,  2.]])
    >>> label
    array([1, 1, 1, 0, 0, 0], dtype=int32)
    >>> inertia
    16.0
    """
    est = KMeans(
        n_clusters=n_clusters,
        n_init=n_init,
        tol=tol,
        copy_x=copy_x,
    ).fit(X, sample_weight=sample_weight)
        return est.cluster_centers_, est.labels_, est.inertia_, est.n_iter_
    else:
        return est.cluster_centers_, est.labels_, est.inertia_

def _kmeans_single_elkan(
    X,
    centers_init,
    tol=1e-4,
    n_threads=1,
):
    """A single run of k-means elkan, assumes preparation completed prior.

        The observations to cluster. If sparse matrix, must be in CSR format.

        The weights for each observation in X.

    centers_init : ndarray of shape (n_clusters, n_features)
        The initial centers.

        It's not advised to set `tol=0` since convergence might never be
        declared due to rounding errors. Use a very small number instead.

    n_threads : int, default=1
        The number of OpenMP threads to use for the computation. Parallelism is
        sample-wise on the main cython loop which assigns each sample to its
        closest center.

    Returns
    -------

        label[i] is the code or index of the centroid the

    """
    n_clusters = centers_init.shape[0]

    # Buffers to avoid new allocations at each iteration.
    centers = centers_init
    centers_new = np.zeros_like(centers)
    weight_in_clusters = np.zeros(n_clusters, dtype=X.dtype)
    labels = np.full(n_samples, -1, dtype=np.int32)
    labels_old = labels.copy()
    center_half_distances = euclidean_distances(centers) / 2
    distance_next_center = np.partition(
        np.asarray(center_half_distances), kth=1, axis=0
    )[1]
    upper_bounds = np.zeros(n_samples, dtype=X.dtype)
    lower_bounds = np.zeros((n_samples, n_clusters), dtype=X.dtype)
    center_shift = np.zeros(n_clusters, dtype=X.dtype)

        init_bounds = init_bounds_sparse
        elkan_iter = elkan_iter_chunked_sparse
        _inertia = _inertia_sparse
    else:
        init_bounds = init_bounds_dense
        elkan_iter = elkan_iter_chunked_dense
        _inertia = _inertia_dense

    init_bounds(
        X,
        centers,
        center_half_distances,
        labels,
        upper_bounds,
        lower_bounds,
    )

    strict_convergence = False

        elkan_iter(
            X,
            centers,
            centers_new,
            weight_in_clusters,
            center_half_distances,
            distance_next_center,
            upper_bounds,
            lower_bounds,
            labels,
            center_shift,
        )

        # compute new pairwise distances between centers and closest other
        # center of each center for next iterations
        center_half_distances = euclidean_distances(centers_new) / 2
        distance_next_center = np.partition(
            np.asarray(center_half_distances), kth=1, axis=0
        )[1]

            inertia = _inertia(X, sample_weight, centers, labels, n_threads)
            print(f"Iteration {i}, inertia {inertia}")

        centers, centers_new = centers_new, centers

        if np.array_equal(labels, labels_old):
            # First check the labels for strict convergence.
                print(f"Converged at iteration {i}: strict convergence.")
            strict_convergence = True
            break
        else:
            # No strict convergence, check for tol based convergence.
            center_shift_tot = (center_shift**2).sum()
            if center_shift_tot <= tol:
                if verbose:
                    print(
                        f"Converged at iteration {i}: center shift "
                        f"{center_shift_tot} within tolerance {tol}."
                    )
                break

        labels_old[:] = labels

    if not strict_convergence:
        # rerun E-step so that predicted labels match cluster centers
            X,
            centers,
            centers,
            labels,
            update_centers=False,
        )

    inertia = _inertia(X, sample_weight, centers, labels, n_threads)

    return labels, inertia, centers, i + 1

def _kmeans_single_lloyd(
    X,
    tol=1e-4,
):
    """A single run of k-means lloyd, assumes preparation completed prior.

        Verbosity mode

    Returns
    -------

    """

    labels = np.full(X.shape[0], -1, dtype=np.int32)

        lloyd_iter = lloyd_iter_chunked_sparse
    else:
        lloyd_iter = lloyd_iter_chunked_dense

        lloyd_iter(
            X,
            centers,
            labels,
        )

            print(f"Iteration {i}, inertia {inertia}.")

            break
        else:
                    print(
                    )
                break

            X,
            centers,
            centers,
            labels,
        )

def _labels_inertia(X, sample_weight, centers, n_threads=1, return_inertia=True):
    """E step of the K-means EM algorithm.

    Compute the labels and the inertia of the given samples and centers.

        The input samples to assign to the labels. If sparse matrix, must
        be in CSR format.

        Precomputed squared euclidean norm of each data point, to speed up
        computations.

        The cluster centers.

    return_inertia : bool, default=True
        Whether to compute and return the inertia.

    Returns
    -------
    labels : ndarray of shape (n_samples,)
        The resulting assignment.

        Sum of squared distances of samples to their closest cluster center.
        Inertia is only returned if return_inertia is True.
    """
    n_clusters = centers.shape[0]

    center_shift = np.zeros(n_clusters, dtype=centers.dtype)

        _labels = lloyd_iter_chunked_sparse
    else:
        _labels = lloyd_iter_chunked_dense

    _labels(
        X,
        centers,
        centers_new=None,
        weight_in_clusters=None,
        center_shift=center_shift,
        update_centers=False,
    )

    if return_inertia:
        inertia = _inertia(X, sample_weight, centers, labels, n_threads)
        return labels, inertia

class _BaseKMeans(
    ClassNamePrefixFeaturesOutMixin, TransformerMixin, ClusterMixin, BaseEstimator, ABC
):
    """Base class for KMeans and MiniBatchKMeans"""

        "n_clusters": [Interval(Integral, 1, None, closed="left")],
        "init": [StrOptions({"k-means++", "random"}), callable, "array-like"],
        "n_init": [
        ],
    }

        self,
        n_clusters,
        *,
        init,
        n_init,
        max_iter,
        tol,
        verbose,
    ):
        self.n_clusters = n_clusters

    def _check_params_vs_input(self, X, default_n_init=None):
        # n_clusters
        if X.shape[0] < self.n_clusters:
                f"n_samples={X.shape[0]} should be >= n_clusters={self.n_clusters}."
            )

        # tol
        self._tol = _tolerance(X, self.tol)

        # n-init
        if self.n_init == "auto":
            if isinstance(self.init, str) and self.init == "k-means++":
                self._n_init = 1
            elif isinstance(self.init, str) and self.init == "random":
                self._n_init = default_n_init
            elif callable(self.init):
            else:  # array-like
        else:

        if _is_arraylike_not_scalar(self.init) and self._n_init != 1:
                (
                    "Explicit initial center position passed: performing only"
                    f" one init in {self.__class__.__name__} instead of "
                    f"n_init={self._n_init}."
                ),
                RuntimeWarning,
            )
            self._n_init = 1

    def _warn_mkl_vcomp(self, n_active_threads):
        """Issue an estimator specific warning when vcomp and mkl are both present

        This method is called by `_check_mkl_vcomp`.
        """

    def _check_mkl_vcomp(self, X, n_samples):
        """Check when vcomp and mkl are both present"""
        # The BLAS call inside a prange in lloyd_iter_chunked_dense is known to
        # cause a small memory leak when there are less chunks than the number
        # of available threads. It only happens when the OpenMP library is
        # vcomp (microsoft OpenMP) and the BLAS library is MKL. see #18653
            return

        n_active_threads = int(np.ceil(n_samples / CHUNK_SIZE))
        if n_active_threads < self._n_threads:
            modules = _get_threadpool_controller().info()
            has_vcomp = "vcomp" in [module["prefix"] for module in modules]
            has_mkl = ("mkl", "intel") in [
                (module["internal_api"], module.get("threading_layer", None))
                for module in modules
            ]
            if has_vcomp and has_mkl:
                self._warn_mkl_vcomp(n_active_threads)

    def _validate_center_shape(self, X, centers):
        """Check if centers is compatible with X and n_clusters."""
        if centers.shape[0] != self.n_clusters:
                f"The shape of the initial centers {centers.shape} does not "
                f"match the number of clusters {self.n_clusters}."
            )
        if centers.shape[1] != X.shape[1]:
                f"match the number of features of the data {X.shape[1]}."
            )

    def _check_test_data(self, X):
            self,
            X,
        )
        return X

    def _init_centroids(
        self,
        X,
        x_squared_norms,
        init,
        init_size=None,
        n_centroids=None,
    ):
        """Compute the initial centroids.

        x_squared_norms : ndarray of shape (n_samples,)
            Squared euclidean norm of each data point. Pass it if you have it
            at hands already to avoid it being recomputed here.

        init : {'k-means++', 'random'}, callable or ndarray of shape \
                (n_clusters, n_features)
            Method for initialization.

            Determines random number generation for centroid initialization.

        sample_weight : ndarray of shape (n_samples,)
            The weights for each observation in X. `sample_weight` is not used
            during initialization if `init` is a callable or a user provided
            array.

        init_size : int, default=None
            Number of samples to randomly sample for speeding up the
            initialization (sometimes at the expense of accuracy).

        n_centroids : int, default=None
            Number of centroids to initialize.
            If left to 'None' the number of centroids will be equal to
            number of clusters to form (self.n_clusters).

        Returns
        -------
        centers : ndarray of shape (n_clusters, n_features)
            Initial centroids of clusters.
        """
        n_clusters = self.n_clusters if n_centroids is None else n_centroids

        if init_size is not None and init_size < n_samples:
            init_indices = random_state.randint(0, n_samples, init_size)
            X = X[init_indices]
            x_squared_norms = x_squared_norms[init_indices]
            sample_weight = sample_weight[init_indices]

        if isinstance(init, str) and init == "k-means++":
            centers, _ = _kmeans_plusplus(
                X,
                n_clusters,
                x_squared_norms=x_squared_norms,
            )
        elif isinstance(init, str) and init == "random":
            seeds = random_state.choice(
                size=n_clusters,
                p=sample_weight / sample_weight.sum(),
            )
            centers = X[seeds]
        elif _is_arraylike_not_scalar(self.init):
            centers = init
        elif callable(init):
            centers = init(X, n_clusters, random_state=random_state)
            centers = check_array(centers, dtype=X.dtype, copy=False, order="C")
            self._validate_center_shape(X, centers)

        if sp.issparse(centers):
            centers = centers.toarray()

        return centers

    def fit_predict(self, X, y=None, sample_weight=None):
        """Compute cluster centers and predict cluster index for each sample.

        Convenience method; equivalent to calling fit(X) followed by
        predict(X).

            New data to transform.

        y : Ignored

            The weights for each observation in X. If None, all observations
            are assigned equal weight.

        Returns
        -------
        labels : ndarray of shape (n_samples,)
            Index of the cluster each sample belongs to.
        """
        return self.fit(X, sample_weight=sample_weight).labels_

        """Predict the closest cluster each sample in X belongs to.

        In the vector quantization literature, `cluster_centers_` is called
        the code book and each value returned by `predict` is the index of
        the closest code in the code book.

            New data to predict.

        Returns
        -------
        """

        X = self._check_test_data(X)

        # sample weights are not used by predict but cython helpers expect an array
        sample_weight = np.ones(X.shape[0], dtype=X.dtype)

        labels = _labels_inertia_threadpool_limit(
            X,
            self.cluster_centers_,
            n_threads=self._n_threads,
            return_inertia=False,
        )

        return labels

        """Compute clustering and transform X to cluster-distance space.

        Equivalent to fit(X).transform(X), but more efficiently implemented.

        y : Ignored

        Returns
        -------
        X_new : ndarray of shape (n_samples, n_clusters)
        """
        return self.fit(X, sample_weight=sample_weight)._transform(X)

        """Transform X to a cluster-distance space.

        In the new space, each dimension is the distance to the cluster
        centers. Note that even if X is sparse, the array returned by
        `transform` will typically be dense.

        Returns
        -------
        """

        """Guts of transform method; no input validation."""
        return euclidean_distances(X, self.cluster_centers_)

    def score(self, X, y=None, sample_weight=None):
        """Opposite of the value of X on the K-means objective.

            New data.

        y : Ignored

        Returns
        -------
            Opposite of the value of X on the K-means objective.
        """

        _, scores = _labels_inertia_threadpool_limit(
            X, sample_weight, self.cluster_centers_, self._n_threads
        )
        return -scores

class KMeans(_BaseKMeans):
    """K-Means clustering.

    n_clusters : int, default=8

        For an example of how to choose an optimal value for `n_clusters` refer to
        :ref:`sphx_glr_auto_examples_cluster_plot_kmeans_silhouette_analysis.py`.

        * 'k-means++' : selects initial cluster centroids using sampling \
            based on an empirical probability distribution of the points' \
            contribution to the overall inertia. This technique speeds up \
            convergence. The algorithm implemented is "greedy k-means++". It \
            differs from the vanilla k-means++ by making several trials at \
            each sampling step and choosing the best centroid among them.

        * 'random': choose `n_clusters` observations (rows) at random from \
        data for the initial centroids.

        * If an array is passed, it should be of shape (n_clusters, n_features)\
        and gives the initial centers.

        * If a callable is passed, it should take arguments X, n_clusters and a\
        random state and return an initialization.

        For an example of how to use the different `init` strategies, see
        :ref:`sphx_glr_auto_examples_cluster_plot_kmeans_digits.py`.

        For an evaluation of the impact of initialization, see the example
        :ref:`sphx_glr_auto_examples_cluster_plot_kmeans_stability_low_dim_dense.py`.

    n_init : 'auto' or int, default='auto'
        Number of times the k-means algorithm is run with different centroid
        seeds. The final results is the best output of `n_init` consecutive runs
        in terms of inertia. Several runs are recommended for sparse
        high-dimensional problems (see :ref:`kmeans_sparse_high_dim`).

        Maximum number of iterations of the k-means algorithm for a
        single run.

        the data first. If copy_x is True (default), then the original data is
        copy_x is False. If the original data is sparse, but not in CSR format,
        a copy will be made even if copy_x is False.

    cluster_centers_ : ndarray of shape (n_clusters, n_features)
        Coordinates of cluster centers. If the algorithm stops before fully
        converging (see ``tol`` and ``max_iter``), these will not be
        consistent with ``labels_``.

    labels_ : ndarray of shape (n_samples,)
        Labels of each point

    inertia_ : float
        Sum of squared distances of samples to their closest cluster center,
        weighted by the sample weights if provided.

    See Also
    --------
    MiniBatchKMeans : Alternative online implementation that does incremental
        updates of the centers positions using mini-batches.
        For large scale learning (say n_samples > 10k) MiniBatchKMeans is
        probably much faster than the default batch implementation.

    Notes
    -----
    The k-means problem is solved using either Lloyd's or Elkan's algorithm.

    The average complexity is given by O(k n T), where n is the number of
    samples and T is the number of iteration.

    The worst case complexity is given by O(n^(k+2/p)) with
    n = n_samples, p = n_features.
    Refer to :doi:`"How slow is the k-means method?" D. Arthur and S. Vassilvitskii -
    SoCG2006.<10.1145/1137856.1137880>` for more details.

    In practice, the k-means algorithm is very fast (one of the fastest
    clustering algorithms available), but it falls in local minima. That's why
    it can be useful to restart it several times.

    If the algorithm stops before fully converging (because of ``tol`` or
    ``max_iter``), ``labels_`` and ``cluster_centers_`` will not be consistent,
    i.e. the ``cluster_centers_`` will not be the means of the points in each
    cluster. Also, the estimator will reassign ``labels_`` after the last
    iteration to make ``labels_`` consistent with ``predict`` on the training
    set.

    Examples
    --------

    >>> kmeans = KMeans(n_clusters=2, random_state=0, n_init="auto").fit(X)
    >>> kmeans.labels_
    >>> kmeans.predict([[0, 0], [12, 3]])
    array([1, 0], dtype=int32)
    >>> kmeans.cluster_centers_
           [ 1.,  2.]])

    For examples of common problems with K-Means and how to address them see
    :ref:`sphx_glr_auto_examples_cluster_plot_kmeans_assumptions.py`.

    For a demonstration of how K-Means can be used to cluster text documents see

    For a comparison between K-Means and MiniBatchKMeans refer to example
    :ref:`sphx_glr_auto_examples_cluster_plot_mini_batch_kmeans.py`.

    For a comparison between K-Means and BisectingKMeans refer to example
    :ref:`sphx_glr_auto_examples_cluster_plot_bisect_kmeans.py`.
    """

        **_BaseKMeans._parameter_constraints,
        "copy_x": ["boolean"],
        "algorithm": [StrOptions({"lloyd", "elkan"})],
    }

        self,
        n_clusters=8,
        *,
        init="k-means++",
        n_init="auto",
        tol=1e-4,
        copy_x=True,
        algorithm="lloyd",
    ):
            n_clusters=n_clusters,
            n_init=n_init,
            tol=tol,
        )

        self.copy_x = copy_x

        super()._check_params_vs_input(X, default_n_init=10)

        self._algorithm = self.algorithm
        if self._algorithm == "elkan" and self.n_clusters == 1:
                (
                    "algorithm='elkan' doesn't make sense for a single "
                    "cluster. Using 'lloyd' instead."
                ),
            )
            self._algorithm = "lloyd"

        """Warn when vcomp and mkl are both present"""
            "KMeans is known to have a memory leak on Windows "
            "with MKL, when there are less chunks than available "
            "threads. You can avoid it by setting the environment"
            f" variable OMP_NUM_THREADS={n_active_threads}."
        )

        """Compute k-means clustering.

            Training instances to cluster. It must be noted that the data
            will be converted to C ordering, which will cause a memory
            copy if the given data is not C-contiguous.
            If a sparse matrix is passed, a copy will be made if it's not in
            CSR format.

        y : Ignored

            are assigned equal weight. `sample_weight` is not used during
            initialization if `init` is a callable or a user provided array.

        Returns
        -------
        """
            self,
            X,
            copy=self.copy_x,
        )

        self._n_threads = _openmp_effective_n_threads()

        # Validate init array
        init_is_array_like = _is_arraylike_not_scalar(init)
        if init_is_array_like:
            init = check_array(init, dtype=X.dtype, copy=True, order="C")
            self._validate_center_shape(X, init)

        # subtract of mean of x for more accurate distance computations
            X_mean = X.mean(axis=0)
            # The copy was already done above
            X -= X_mean

            if init_is_array_like:
                init -= X_mean

        # precompute squared norms of data points

        if self._algorithm == "elkan":
            kmeans_single = _kmeans_single_elkan
        else:
            kmeans_single = _kmeans_single_lloyd
            self._check_mkl_vcomp(X, X.shape[0])

        best_inertia, best_labels = None, None

        for i in range(self._n_init):
            # Initialize centers
            centers_init = self._init_centroids(
                X,
            )
                print("Initialization complete")

            # run a k-means once
            labels, inertia, centers, n_iter_ = kmeans_single(
                X,
                centers_init,
                max_iter=self.max_iter,
                tol=self._tol,
                n_threads=self._n_threads,
            )

            # determine if these results are the best so far
            # we chose a new run if it has a better inertia and the clustering is
            # different from the best so far (it's possible that the inertia is
            # slightly better even if the clustering is the same with potentially
            # permuted labels, due to rounding errors)
            if best_inertia is None or (
                inertia < best_inertia
                and not _is_same_clustering(labels, best_labels, self.n_clusters)
            ):
                best_labels = labels
                best_centers = centers
                best_inertia = inertia
                best_n_iter = n_iter_

            if not self.copy_x:
                X += X_mean
            best_centers += X_mean

        distinct_clusters = len(set(best_labels))
        if distinct_clusters < self.n_clusters:
                "Number of distinct clusters ({}) found smaller than "
                "n_clusters ({}). Possibly due to duplicate points "
                "in X.".format(distinct_clusters, self.n_clusters),
            )

        self.cluster_centers_ = best_centers
        self._n_features_out = self.cluster_centers_.shape[0]
        self.labels_ = best_labels
        self.inertia_ = best_inertia
        self.n_iter_ = best_n_iter

def _mini_batch_step(
    X,
    centers,
    centers_new,
    weight_sums,
    random_reassign=False,
    reassignment_ratio=0.01,
):
    """Incremental update of the centers for the Minibatch K-Means algorithm.

        The original data array. If sparse, must be in CSR format.

        Squared euclidean norm of each data point.

        The cluster centers before the current iteration

    centers_new : ndarray of shape (n_clusters, n_features)
        The cluster centers after the current iteration. Modified in-place.

    weight_sums : ndarray of shape (n_clusters,)
        The vector in which we keep track of the numbers of points in a
        cluster. This array is modified in place.

        Determines random number generation for low count centers reassignment.

    random_reassign : boolean, default=False
        If True, centers with very low counts are randomly reassigned
        to observations.

    reassignment_ratio : float, default=0.01
        Control the fraction of the maximum number of counts for a
        center to be reassigned. A higher value means that low count
        centers are more likely to be reassigned, which means that the
        model will take longer to converge, but should converge in a
        better clustering.

        Controls the verbosity.

        The number of OpenMP threads to use for the computation.

    Returns
    -------
        The inertia is computed after finding the labels and before updating
        the centers.
    """
    # Perform label assignment to nearest centers
    # For better efficiency, it's better to run _mini_batch_step in a
    # threadpool_limit context than using _labels_inertia_threadpool_limit here
    labels, inertia = _labels_inertia(X, sample_weight, centers, n_threads=n_threads)

    # Update centers according to the labels
        _minibatch_update_sparse(
            X, sample_weight, centers, centers_new, weight_sums, labels, n_threads
        )
    else:
        _minibatch_update_dense(
            X,
            centers,
            weight_sums,
            labels,
        )

    # Reassign clusters that have very low weight
    if random_reassign and reassignment_ratio > 0:
        to_reassign = weight_sums < reassignment_ratio * weight_sums.max()

        # pick at most .5 * batch_size samples as new centers
        if to_reassign.sum() > 0.5 * X.shape[0]:
            indices_dont_reassign = np.argsort(weight_sums)[int(0.5 * X.shape[0]) :]
            to_reassign[indices_dont_reassign] = False
        n_reassigns = to_reassign.sum()

        if n_reassigns:
            # Pick new clusters amongst observations with uniform probability
            new_centers = random_state.choice(
                X.shape[0], replace=False, size=n_reassigns
            )
                print(f"[MiniBatchKMeans] Reassigning {n_reassigns} cluster centers.")

                assign_rows_csr(
                    X,
                    new_centers.astype(np.intp, copy=False),
                    np.where(to_reassign)[0].astype(np.intp, copy=False),
                    centers_new,
                )
            else:
                centers_new[to_reassign] = X[new_centers]

        # reset counts of reassigned centers, but don't reset them too small
        # to avoid instant reassignment. This is a pretty dirty hack as it
        # also modifies the learning rates.
        weight_sums[to_reassign] = np.min(weight_sums[~to_reassign])

    return inertia

class MiniBatchKMeans(_BaseKMeans):
    """
    Mini-Batch K-Means clustering.

    Read more in the :ref:`User Guide <mini_batch_kmeans>`.

        'k-means++' : selects initial cluster centroids using sampling based on
        an empirical probability distribution of the points' contribution to the
        overall inertia. This technique speeds up convergence. The algorithm
        implemented is "greedy k-means++". It differs from the vanilla k-means++
        by making several trials at each sampling step and choosing the best centroid
        among them.

        'random': choose `n_clusters` observations (rows) at random from data
        for the initial centroids.

        If an array is passed, it should be of shape (n_clusters, n_features)

        If a callable is passed, it should take arguments X, n_clusters and a

        Size of the mini batches.
        For faster computations, you can set the ``batch_size`` greater than
        256 * number of cores to enable parallelism on all cores.

           `batch_size` default changed from 100 to 1024.

    compute_labels : bool, default=True
        Compute label assignment and inertia for the complete dataset
        once the minibatch optimization has converged in fit.

        Determines random number generation for centroid initialization and
        random reassignment. Use an int to make the randomness deterministic.

        Control early stopping based on the relative center changes as
        measured by a smoothed, variance-normalized of the mean center
        squared position changes. This early stopping heuristics is
        closer to the one used for the batch variant of the algorithms
        but induces a slight computational and memory overhead over the
        inertia heuristic.

        To disable convergence detection based on normalized center
        change, set tol to 0.0 (default).

        Control early stopping based on the consecutive number of mini
        batches that does not yield an improvement on the smoothed inertia.

        To disable convergence detection based on inertia, set
        max_no_improvement to None.

    init_size : int, default=None
        Number of samples to randomly sample for speeding up the
        initialization (sometimes at the expense of accuracy): the
        only algorithm is initialized by running a batch KMeans on a
        random subset of the data. This needs to be larger than n_clusters.

        If `None`, the heuristic is `init_size = 3 * batch_size` if
        `3 * batch_size < n_clusters`, else `init_size = 3 * n_clusters`.

        Number of random initializations that are tried.
        In contrast to KMeans, the algorithm is only run once, using the best of
        the `n_init` initializations as measured by inertia. Several runs are
        recommended for sparse high-dimensional problems (see
        :ref:`kmeans_sparse_high_dim`).

        3 if using `init='random'` or `init` is a callable;

           Default value for `n_init` changed to `'auto'` in version.

        Control the fraction of the maximum number of counts for a center to
        be reassigned. A higher value means that low count centers are more
        easily reassigned, which means that the model will take longer to
        converge, but should converge in a better clustering. However, too high
        a value may cause convergence issues, especially with a small batch
        size.

        Coordinates of cluster centers.

        Labels of each point (if compute_labels is set to True).

        The value of the inertia criterion associated with the chosen
        partition if compute_labels is set to True. If compute_labels is set to
        False, it's an approximation of the inertia based on an exponentially
        weighted average of the batch inertiae.
        The inertia is defined as the sum of square distances of samples to
        their cluster center, weighted by the sample weights if provided.

        Number of minibatches processed.

    See Also
    --------
    KMeans : The classic implementation of the clustering method based on the
        Lloyd's algorithm. It consumes the whole set of input data at each

    Notes
    -----
    See https://www.eecs.tufts.edu/~dsculley/papers/fastkmeans.pdf

    When there are too few points in the dataset, some centers may be
    duplicated, which means that a proper clustering in terms of the number
    of requesting clusters and the number of returned clusters will not
    always match. One solution is to set `reassignment_ratio=0`, which
    prevents reassignments of clusters that are too small.

    See :ref:`sphx_glr_auto_examples_cluster_plot_birch_vs_minibatchkmeans.py` for a
    comparison with :class:`~sklearn.cluster.BIRCH`.

    Examples
    --------
    >>> from sklearn.cluster import MiniBatchKMeans
    ...               [4, 2], [4, 0], [4, 4],
    ...               [4, 5], [0, 1], [2, 2],
    ...               [3, 2], [5, 5], [1, -1]])
    >>> # manually fit on batches
    >>> kmeans = MiniBatchKMeans(n_clusters=2,
    ...                          random_state=0,
    ...                          batch_size=6,
    ...                          n_init="auto")
    >>> kmeans = kmeans.partial_fit(X[0:6,:])
    >>> kmeans = kmeans.partial_fit(X[6:12,:])
    array([[3.375, 3.  ],
           [0.75 , 0.5 ]])
    >>> kmeans.predict([[0, 0], [4, 4]])
    >>> # fit on the whole data
    ...                          max_iter=10,
    ...                          n_init="auto").fit(X)
    array([[3.55102041, 2.48979592],
           [1.06896552, 1.        ]])

    For a comparison of Mini-Batch K-Means clustering with other clustering algorithms,
    see :ref:`sphx_glr_auto_examples_cluster_plot_cluster_comparison.py`
    """

        "compute_labels": ["boolean"],
        "init_size": [Interval(Integral, 1, None, closed="left"), None],
        "reassignment_ratio": [Interval(Real, 0, None, closed="left")],
    }

        self,
        *,
        compute_labels=True,
        tol=0.0,
        reassignment_ratio=0.01,
    ):
            tol=tol,
        )

        self.compute_labels = compute_labels
        self.init_size = init_size
        self.reassignment_ratio = reassignment_ratio

        super()._check_params_vs_input(X, default_n_init=3)

        # init_size
        self._init_size = self.init_size
        if self._init_size is None:
            self._init_size = 3 * self._batch_size
            if self._init_size < self.n_clusters:
                self._init_size = 3 * self.n_clusters
        elif self._init_size < self.n_clusters:
                (
                    f"init_size={self._init_size} should be larger than "
                    f"n_clusters={self.n_clusters}. Setting it to "
                    "min(3*n_clusters, n_samples)"
                ),
            )
            self._init_size = 3 * self.n_clusters
        self._init_size = min(self._init_size, X.shape[0])

        # reassignment_ratio
        if self.reassignment_ratio < 0:
                "reassignment_ratio should be >= 0, got "
                f"{self.reassignment_ratio} instead."
            )

            "MiniBatchKMeans is known to have a memory leak on "
            "Windows with MKL, when there are less chunks than "
            "available threads. You can prevent it by setting "
            f"batch_size >= {self._n_threads * CHUNK_SIZE} or by "
            "setting the environment variable "
            f"OMP_NUM_THREADS={n_active_threads}"
        )

    def _mini_batch_convergence(
        self, step, n_steps, n_samples, centers_squared_diff, batch_inertia
    ):
        # Normalize inertia to be able to compare values when
        # batch_size changes
        batch_inertia /= self._batch_size

        # count steps starting from 1 for user friendly verbose mode.

        # Ignore first iteration because it's inertia from initialization.
                print(
                    f"Minibatch step {step}/{n_steps}: mean batch "
                    f"inertia: {batch_inertia}"
                )

        # Compute an Exponentially Weighted Average of the inertia to
        if self._ewa_inertia is None:
            self._ewa_inertia = batch_inertia
        else:
            alpha = self._batch_size * 2.0 / (n_samples + 1)
            self._ewa_inertia = self._ewa_inertia * (1 - alpha) + batch_inertia * alpha

            print(
                f"Minibatch step {step}/{n_steps}: mean batch inertia: "
                f"{batch_inertia}, ewa inertia: {self._ewa_inertia}"
            )

        # Early stopping based on absolute tolerance on squared change of
        # centers position
        if self._tol > 0.0 and centers_squared_diff <= self._tol:
                print(f"Converged (small centers change) at step {step}/{n_steps}")

        # inertia
        if self._ewa_inertia_min is None or self._ewa_inertia < self._ewa_inertia_min:
            self._ewa_inertia_min = self._ewa_inertia
        else:

        if (
        ):
                print(
                    "Converged (lack of improvement in inertia) at step "
                    f"{step}/{n_steps}"
                )

    def _random_reassign(self):
        """Check if a random reassignment needs to be done.

        Do random reassignments each time 10 * n_clusters samples have been
        processed.

        If there are empty clusters we always want to reassign.
        """
        self._n_since_last_reassign += self._batch_size
        if (self._counts == 0).any() or self._n_since_last_reassign >= (
            10 * self.n_clusters
        ):
            self._n_since_last_reassign = 0

        """Compute the centroids on X by chunking it into mini-batches.

            will be converted to C ordering, which will cause a memory copy
            if the given data is not C-contiguous.

        y : Ignored

        Returns
        -------
        """
            self,
            X,
        )

        if _is_arraylike_not_scalar(init):

        self._check_mkl_vcomp(X, self._batch_size)

        # Validation set for the init
        validation_indices = random_state.randint(0, n_samples, self._init_size)
        X_valid = X[validation_indices]
        sample_weight_valid = sample_weight[validation_indices]

        # perform several inits with random subsets
        best_inertia = None
        for init_idx in range(self._n_init):
                print(f"Init {init_idx + 1}/{self._n_init} with method {init}")

            # Initialize the centers using only a fraction of the data as we
            # expect n_samples to be very large when using MiniBatchKMeans.
            cluster_centers = self._init_centroids(
                X,
                init_size=self._init_size,
            )

            # Compute inertia on a validation set.
            _, inertia = _labels_inertia_threadpool_limit(
                X_valid,
                sample_weight_valid,
                cluster_centers,
            )

                print(f"Inertia for init {init_idx + 1}/{self._n_init}: {inertia}")
            if best_inertia is None or inertia < best_inertia:
                init_centers = cluster_centers

        centers = init_centers
        centers_new = np.empty_like(centers)

        # Initialize counts
        self._counts = np.zeros(self.n_clusters, dtype=X.dtype)

        self._ewa_inertia = None
        self._ewa_inertia_min = None

        # Initialize number of samples seen since last reassignment
        self._n_since_last_reassign = 0

        n_steps = (self.max_iter * n_samples) // self._batch_size

        with _get_threadpool_controller().limit(limits=1, user_api="blas"):
            # Perform the iterative optimization until convergence
            for i in range(n_steps):
                # Sample a minibatch from the full dataset
                minibatch_indices = random_state.randint(0, n_samples, self._batch_size)

                # Perform the actual update step on the minibatch data
                batch_inertia = _mini_batch_step(
                    X=X[minibatch_indices],
                    sample_weight=sample_weight[minibatch_indices],
                    centers=centers,
                    centers_new=centers_new,
                    weight_sums=self._counts,
                    random_reassign=self._random_reassign(),
                    reassignment_ratio=self.reassignment_ratio,
                    n_threads=self._n_threads,
                )

                if self._tol > 0.0:
                    centers_squared_diff = np.sum((centers_new - centers) ** 2)
                else:
                    centers_squared_diff = 0

                centers, centers_new = centers_new, centers

                # Monitor convergence and do early stopping if necessary
                if self._mini_batch_convergence(
                    i, n_steps, n_samples, centers_squared_diff, batch_inertia
                ):
                    break

        self.cluster_centers_ = centers

        self.n_iter_ = int(np.ceil(((i + 1) * self._batch_size) / n_samples))

        if self.compute_labels:
            self.labels_, self.inertia_ = _labels_inertia_threadpool_limit(
                X,
                self.cluster_centers_,
            )
        else:
            self.inertia_ = self._ewa_inertia * n_samples

    def partial_fit(self, X, y=None, sample_weight=None):
        """Update k means estimate on a single mini-batch X.

        y : Ignored

        Returns
        -------
            Return updated estimator.
        """
        has_centers = hasattr(self, "cluster_centers_")

            self,
            X,
            reset=not has_centers,
        )

        self._random_state = getattr(
            self, "_random_state", check_random_state(self.random_state)
        )
        self.n_steps_ = getattr(self, "n_steps_", 0)

        if not has_centers:
            # this instance has not been fitted yet (fit or partial_fit)
            self._check_params_vs_input(X)
            self._n_threads = _openmp_effective_n_threads()

            # Validate init array
            init = self.init
            if _is_arraylike_not_scalar(init):
                init = check_array(init, dtype=X.dtype, copy=True, order="C")
                self._validate_center_shape(X, init)

            # initialize the cluster centers
            self.cluster_centers_ = self._init_centroids(
                X,
                random_state=self._random_state,
            )

            # Initialize counts
            self._counts = np.zeros(self.n_clusters, dtype=X.dtype)

            # Initialize number of samples seen since last reassignment

            _mini_batch_step(
                X,
                centers=self.cluster_centers_,
                centers_new=self.cluster_centers_,
                weight_sums=self._counts,
                random_reassign=self._random_reassign(),
                reassignment_ratio=self.reassignment_ratio,
            )

                X,
            )

        self,
        *,
        init,
        n_init,
        max_iter,
        tol,
        verbose,
    ):

            )

        # tol

        # n-init
        else:

                (
                ),
            )

        """

            return

            ]

            )
            )

            self,
            X,
        )
        return X

        self,
        X,
        init,
    ):

            array.

        Returns
        -------
        """

                X,
            )
            )

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

            X,
        )

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

            New data.

        y : Ignored

        Returns
        -------
        """

        )

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

                (
                ),
            )

        )

        y : Ignored

        Returns
        -------
        """
            self,
            X,
        )

            X -= X_mean

        else:

                X,
            )

                X,
            )

            ):

                X += X_mean

            )

        self,
        *,
        tol=0.0,
    ):
            tol=tol,
        )

                (
                ),
            )

            )

        )

    ):

                print(
                )

        else:

            print(
            )

        # inertia
        else:

        if (
        ):
                print(
                )

        """
        ):

        y : Ignored

        Returns
        -------
        """
            self,
            X,
        )

                X,
            )

                X_valid,
            )

                )

                else:

                ):
                    break

                X,
            )
        else:

        y : Ignored

        Returns
        -------
        """

            self,
            X,
        )

        )

                X,
            )

                X,
            )

                X,
            )

def _equal_similarities_and_preferences(S, preference):
    def all_equal_preferences():
        return np.all(preference == preference.flat[0])

    def all_equal_similarities():
        # Create mask to ignore diagonal of S
        mask = np.ones(S.shape, dtype=bool)
        np.fill_diagonal(mask, 0)

        return np.all(S[mask].flat == S[mask].flat[0])

    return all_equal_preferences() and all_equal_similarities()

def _affinity_propagation(
    S,
    *,
    preference,
    convergence_iter,
    max_iter,
    damping,
    verbose,
):
    """Main affinity propagation algorithm."""
    n_samples = S.shape[0]
    if n_samples == 1 or _equal_similarities_and_preferences(S, preference):
        # It makes no sense to run the algorithm in this case, so return 1 or
        # n_samples clusters, depending on preferences
            "All samples have mutually equal similarities. "
            "Returning arbitrary cluster center(s)."
        )
        if preference.flat[0] > S.flat[n_samples - 1]:
            return (
                (np.arange(n_samples), np.arange(n_samples), 0)
                if return_n_iter
                else (np.arange(n_samples), np.arange(n_samples))
            )
        else:
            return (
                (np.array([0]), np.array([0] * n_samples), 0)
                else (np.array([0]), np.array([0] * n_samples))
            )

    # Place preference on the diagonal of S
    S.flat[:: (n_samples + 1)] = preference

    A = np.zeros((n_samples, n_samples))
    R = np.zeros((n_samples, n_samples))  # Initialize messages
    # Intermediate results
    tmp = np.zeros((n_samples, n_samples))

    # Remove degeneracies
    S += (
        np.finfo(S.dtype).eps * S + np.finfo(S.dtype).tiny * 100
    ) * random_state.standard_normal(size=(n_samples, n_samples))

    # Execute parallel affinity propagation updates
    e = np.zeros((n_samples, convergence_iter))

    ind = np.arange(n_samples)

        # tmp = A + S; compute responsibilities
        np.add(A, S, tmp)
        I = np.argmax(tmp, axis=1)
        Y = tmp[ind, I]  # np.max(A + S, axis=1)
        tmp[ind, I] = -np.inf
        Y2 = np.max(tmp, axis=1)

        # tmp = Rnew
        np.subtract(S, Y[:, None], tmp)
        tmp[ind, I] = S[ind, I] - Y2

        # Damping
        tmp *= 1 - damping
        R *= damping
        R += tmp

        # tmp = Rp; compute availabilities
        np.maximum(R, 0, tmp)
        tmp.flat[:: n_samples + 1] = R.flat[:: n_samples + 1]

        # tmp = -Anew
        tmp -= np.sum(tmp, axis=0)
        dA = np.diag(tmp).copy()
        tmp.clip(0, np.inf, tmp)
        tmp.flat[:: n_samples + 1] = dA

        # Damping
        A *= damping
        A -= tmp

        # Check for convergence
        E = (np.diag(A) + np.diag(R)) > 0
        e[:, it % convergence_iter] = E
        K = np.sum(E, axis=0)

        if it >= convergence_iter:
            se = np.sum(e, axis=1)
            unconverged = np.sum((se == convergence_iter) + (se == 0)) != n_samples
            if (not unconverged and (K > 0)) or (it == max_iter):
                never_converged = False
                    print("Converged after %d iterations." % it)
                break
    else:
        never_converged = True
            print("Did not converge")

    I = np.flatnonzero(E)
    K = I.size  # Identify exemplars

    if K > 0:
        if never_converged:
                (
                    "Affinity propagation did not converge, this model "
                    "may return degenerate cluster centers and labels."
                ),
            )
        c = np.argmax(S[:, I], axis=1)
        c[I] = np.arange(K)  # Identify clusters
        # Refine the final set of exemplars and clusters and return results
        for k in range(K):
            ii = np.asarray(c == k).nonzero()[0]
            j = np.argmax(np.sum(S[ii[:, np.newaxis], ii], axis=0))
            I[k] = ii[j]

        c[I] = np.arange(K)
        labels = I[c]
        # Reduce labels to a sorted, gapless, list
        cluster_centers_indices = np.unique(labels)
        labels = np.searchsorted(cluster_centers_indices, labels)
    else:
            (
                "Affinity propagation did not converge and this model "
                "will not have any cluster centers."
            ),
        )
        labels = np.array([-1] * n_samples)
        cluster_centers_indices = []

        return cluster_centers_indices, labels, it + 1
    else:
        return cluster_centers_indices, labels

def affinity_propagation(
    S,
    *,
    preference=None,
    convergence_iter=15,
    damping=0.5,
):
    """Perform Affinity Propagation Clustering of data.

    Read more in the :ref:`User Guide <affinity_propagation>`.

    S : array-like of shape (n_samples, n_samples)
        Matrix of similarities between points.

    preference : array-like of shape (n_samples,) or float, default=None
        Preferences for each point - points with larger values of
        preferences are more likely to be chosen as exemplars. The number of
        exemplars, i.e. of clusters, is influenced by the input preferences
        value. If the preferences are not passed as arguments, they will be
        set to the median of the input similarities (resulting in a moderate
        number of clusters). For a smaller amount of clusters, this can be set
        to the minimum value of the similarities.

    convergence_iter : int, default=15
        Number of iterations with no change in the number
        of estimated clusters that stops the convergence.

    damping : float, default=0.5
        Damping factor between 0.5 and 1.

        If copy is False, the affinity matrix is modified inplace by the
        algorithm, for memory efficiency.

        Pseudo-random number generator to control the starting state.
        Use an int for reproducible results across function calls.
        See the :term:`Glossary <random_state>`.

            this parameter was previously hardcoded as 0.

    Returns
    -------
    cluster_centers_indices : ndarray of shape (n_clusters,)
        Index of clusters centers.

        Cluster labels for each point.

    Notes
    -----
    For an example usage,
    see :ref:`sphx_glr_auto_examples_cluster_plot_affinity_propagation.py`.
    You may also check out,
    :ref:`sphx_glr_auto_examples_applications_plot_stock_market.py`

    When the algorithm does not converge, it will still return a arrays of
    ``cluster_center_indices`` and labels if there are any exemplars/clusters,
    however they may be degenerate and should be used with caution.

    When all training samples have equal similarities and equal preferences,
    the assignment of cluster centers and labels depends on the preference.
    If the preference is smaller than the similarities, a single cluster center
    and label ``0`` for every sample will be returned. Otherwise, every
    training sample becomes its own cluster center and is assigned a unique
    label.

    Brendan J. Frey and Delbert Dueck, "Clustering by Passing Messages
    Between Data Points", Science Feb. 2007

    Examples
    --------
    >>> from sklearn.cluster import affinity_propagation
    ...               [4, 2], [4, 4], [4, 0]])
    >>> S = -euclidean_distances(X, squared=True)
    >>> cluster_centers_indices, labels = affinity_propagation(S, random_state=0)
    >>> cluster_centers_indices
    array([0, 3])
    >>> labels
    array([0, 0, 0, 1, 1, 1])
    """
    estimator = AffinityPropagation(
        damping=damping,
        convergence_iter=convergence_iter,
        preference=preference,
    ).fit(S)

        return estimator.cluster_centers_indices_, estimator.labels_, estimator.n_iter_
    return estimator.cluster_centers_indices_, estimator.labels_

class AffinityPropagation(ClusterMixin, BaseEstimator):

        Damping factor in the range `[0.5, 1.0)` is the extent to
        which the current value is maintained relative to
        incoming values (weighted 1 - damping). This in order
        to avoid numerical oscillations when updating these
        values (messages).

        Make a copy of input data.

        preferences are more likely to be chosen as exemplars. The number
        of exemplars, ie of clusters, is influenced by the input
        preferences value. If the preferences are not passed as arguments,
        they will be set to the median of the input similarities.

    affinity : {'euclidean', 'precomputed'}, default='euclidean'
        Which affinity to use. At the moment 'precomputed' and
        ``euclidean`` are supported. 'euclidean' uses the
        negative squared euclidean distance between points.

    cluster_centers_indices_ : ndarray of shape (n_clusters,)
        Indices of cluster centers.

        Cluster centers (if affinity != ``precomputed``).

        Labels of each point.

        Stores the affinity matrix used in ``fit``.

        Number of iterations taken to converge.

    See Also
    --------
    AgglomerativeClustering : Recursively merges the pair of
        clusters that minimally increases a given linkage distance.
    FeatureAgglomeration : Similar to AgglomerativeClustering,
        but recursively merges features instead of samples.
    KMeans : K-Means clustering.
    MiniBatchKMeans : Mini-Batch K-Means clustering.
    MeanShift : Mean shift clustering using a flat kernel.
    SpectralClustering : Apply clustering to a projection
        of the normalized Laplacian.

    Notes
    -----
    The algorithmic complexity of affinity propagation is quadratic
    in the number of points.

    When ``fit`` does not converge, ``cluster_centers_`` is still populated
    however it may be degenerate. In such a case, proceed with caution.
    If ``fit`` does not converge and fails to produce any ``cluster_centers_``
    then ``predict`` will label every sample as ``-1``.

    If the preference is smaller than the similarities, ``fit`` will result in
    a single cluster center and label ``0`` for every sample. Otherwise, every
    label.

    Examples
    --------
    >>> from sklearn.cluster import AffinityPropagation
    >>> clustering = AffinityPropagation(random_state=5).fit(X)
    >>> clustering
    AffinityPropagation(random_state=5)
    >>> clustering.labels_
    >>> clustering.predict([[0, 0], [4, 4]])
    >>> clustering.cluster_centers_
    array([[1, 2],
           [4, 2]])

    For a comparison of Affinity Propagation with other clustering algorithms, see
    :ref:`sphx_glr_auto_examples_cluster_plot_cluster_comparison.py`
    """

        "damping": [Interval(Real, 0.5, 1.0, closed="left")],
        "convergence_iter": [Interval(Integral, 1, None, closed="left")],
        "preference": [
            None,
        ],
        "affinity": [StrOptions({"euclidean", "precomputed"})],
    }

        self,
        *,
        damping=0.5,
        convergence_iter=15,
        preference=None,
        affinity="euclidean",
    ):
        self.damping = damping
        self.convergence_iter = convergence_iter
        self.preference = preference

        tags.input_tags.pairwise = self.affinity == "precomputed"
        tags.input_tags.sparse = self.affinity != "precomputed"

        """Fit the clustering from features, or affinity matrix.

        X : {array-like, sparse matrix} of shape (n_samples, n_features), or \
                array-like of shape (n_samples, n_samples)
            Training instances to cluster, or similarities / affinities between
            instances if ``affinity='precomputed'``. If a sparse feature matrix
            is provided, it will be converted into a sparse ``csr_matrix``.

        y : Ignored

        Returns
        -------
        self
        """
            X = validate_data(self, X, copy=self.copy, force_writeable=True)
        else:  # self.affinity == "euclidean"
            X = validate_data(self, X, accept_sparse="csr")
            self.affinity_matrix_ = -euclidean_distances(X, squared=True)

        if self.affinity_matrix_.shape[0] != self.affinity_matrix_.shape[1]:
                "The matrix of similarities must be a square array. "
                f"Got {self.affinity_matrix_.shape} instead."
            )

        if self.preference is None:
            preference = np.median(self.affinity_matrix_)
        else:
            preference = self.preference
        preference = np.asarray(preference)

        (
            self.cluster_centers_indices_,
            self.labels_,
            self.n_iter_,
        ) = _affinity_propagation(
            self.affinity_matrix_,
            convergence_iter=self.convergence_iter,
            preference=preference,
            damping=self.damping,
        )

        if self.affinity != "precomputed":
            self.cluster_centers_ = X[self.cluster_centers_indices_].copy()

            New data to predict. If a sparse matrix is provided, it will be

        Returns
        -------
        """
        X = validate_data(self, X, reset=False, accept_sparse="csr")
        if not hasattr(self, "cluster_centers_"):
                "Predict method is not supported when affinity='precomputed'."
            )

        if self.cluster_centers_.shape[0] > 0:
            with config_context(assume_finite=True):
                return pairwise_distances_argmin(X, self.cluster_centers_)
        else:
                (
                    "This model does not have any cluster centers "
                    "because affinity propagation did not converge. "
                    "Labeling every sample as '-1'."
                ),
            )
            return np.array([-1] * X.shape[0])

        """Fit clustering from features/affinity matrix; return cluster labels.

        y : Ignored

        Returns
        -------
        """
        return super().fit_predict(X, y)

        self,
        *,
    ):

        y : Ignored

        Returns
        -------
        self
        """

            )

        else:

        (
        )

        Returns
        -------
        """
            )

        else:
                (
                ),
            )

        y : Ignored

        Returns
        -------
        """

class AgglomerationTransform(TransformerMixin):
    """
    A class for feature agglomeration via the transform interface.
    """

        """
        Transform a new matrix using the built clustering.

            A M by N array of M observations in N dimensions or a length
            M array of M one-dimensional observations.

        Returns
        -------
        Y : ndarray of shape (n_samples, n_clusters) or (n_clusters,)
            The pooled values for each feature cluster.
        """

        if self.pooling_func == np.mean and not issparse(X):
            size = np.bincount(self.labels_)
            # a fast way to compute the mean of grouped features
            nX = np.array(
                [np.bincount(self.labels_, X[i, :]) / size for i in range(n_samples)]
            )
        else:
            nX = [
                self.pooling_func(X[:, self.labels_ == l], axis=1)
                for l in np.unique(self.labels_)
            ]
            nX = np.array(nX).T
        return nX

        """
        Inverse the transformation and return a vector of size `n_features`.

        X : array-like of shape (n_samples, n_clusters) or (n_clusters,)
            The values to be assigned to each cluster of samples.

        Returns
        -------
        X : ndarray of shape (n_samples, n_features) or (n_features,)
            A vector of size `n_samples` with the values of `Xred` assigned to
            each of the cluster of samples.
        """

        unil, inverse = np.unique(self.labels_, return_inverse=True)
        return X[..., inverse]

        """

        Returns
        -------
        """

            )
        else:
            nX = [
            ]
        return nX

        """

        Returns
        -------
        """

def cluster_qr(vectors):
    """Find the discrete partition closest to the eigenvector embedding.

        This implementation was proposed in [1]_.

        vectors : array-like, shape: (n_samples, n_clusters)
            The embedding space of the samples.

        Returns
        -------
        labels : array of integers, shape: n_samples
            The cluster labels of vectors.

        .. [1] :doi:`Simple, direct, and efficient multi-way spectral clustering, 2019
            Anil Damle, Victor Minden, Lexing Ying
            <10.1093/imaiai/iay008>`

    """

    k = vectors.shape[1]
    _, _, piv = qr(vectors.T, pivoting=True)
    ut, _, v = svd(vectors[piv[:k], :].T)
    vectors = abs(np.dot(vectors, np.dot(ut, v.conj())))
    return vectors.argmax(axis=1)

def discretize(
    vectors, *, copy=True, max_svd_restarts=30, n_iter_max=20, random_state=None
):
    """Search for a partition matrix which is closest to the eigenvector embedding.

    This implementation was proposed in [1]_.

    vectors : array-like of shape (n_samples, n_clusters)
        The embedding space of the samples.

        Whether to copy vectors, or perform in-place normalization.

    max_svd_restarts : int, default=30
        Maximum number of attempts to restart SVD if convergence fails

    n_iter_max : int, default=30
        Maximum number of iterations to attempt in rotation and partition
        matrix search if machine precision convergence is not reached

        Determines random number generation for rotation matrix initialization.
        Use an int to make the randomness deterministic.

    Returns
    -------
    labels : array of integers, shape: n_samples
        The labels of the clusters.

    .. [1] `Multiclass spectral clustering, 2003
           Stella X. Yu, Jianbo Shi
           <https://people.eecs.berkeley.edu/~jordan/courses/281B-spring04/readings/yu-shi.pdf>`_

    Notes
    -----

    The eigenvector embedding is used to iteratively search for the
    closest discrete partition.  First, the eigenvector embedding is
    normalized to the space of partition matrices. An optimal discrete
    partition matrix closest to this normalized embedding multiplied by
    an initial rotation is calculated.  Fixing this discrete partition
    matrix, an optimal rotation matrix is calculated.  These two
    calculations are performed until convergence.  The discrete partition
    matrix is returned as the clustering solution.  Used in spectral
    clustering, this method tends to be faster and more robust to random
    initialization than k-means.

    """

    vectors = as_float_array(vectors, copy=copy)

    eps = np.finfo(float).eps
    n_samples, n_components = vectors.shape

    # Normalize the eigenvectors to an equal length of a vector of ones.
    # Reorient the eigenvectors to point in the negative direction with respect
    # to the first element.  This may have to do with constraining the
    # eigenvectors to lie in a specific quadrant to make the discretization
    # search easier.
    norm_ones = np.sqrt(n_samples)
    for i in range(vectors.shape[1]):
        vectors[:, i] = (vectors[:, i] / np.linalg.norm(vectors[:, i])) * norm_ones
        if vectors[0, i] != 0:
            vectors[:, i] = -1 * vectors[:, i] * np.sign(vectors[0, i])

    # Normalize the rows of the eigenvectors.  Samples should lie on the unit
    # hypersphere centered at the origin.  This transforms the samples in the
    # embedding space to the space of partition matrices.
    vectors = vectors / np.sqrt((vectors**2).sum(axis=1))[:, np.newaxis]

    svd_restarts = 0
    has_converged = False

    # If there is an exception we try to randomize and rerun SVD again
    # do this max_svd_restarts times.
    while (svd_restarts < max_svd_restarts) and not has_converged:
        # Initialize first column of rotation matrix with a row of the
        # eigenvectors
        rotation = np.zeros((n_components, n_components))
        rotation[:, 0] = vectors[random_state.randint(n_samples), :].T

        # To initialize the rest of the rotation matrix, find the rows
        # of the eigenvectors that are as orthogonal to each other as
        # possible
        c = np.zeros(n_samples)
        for j in range(1, n_components):
            # Accumulate c to ensure row is as orthogonal as possible to
            # previous picks as well as current one
            c += np.abs(np.dot(vectors, rotation[:, j - 1]))
            rotation[:, j] = vectors[c.argmin(), :].T

        last_objective_value = 0.0
        n_iter = 0

        while not has_converged:
            n_iter += 1

            t_discrete = np.dot(vectors, rotation)

            labels = t_discrete.argmax(axis=1)
            vectors_discrete = csc_matrix(
                (np.ones(len(labels)), (np.arange(0, n_samples), labels)),
                shape=(n_samples, n_components),
            )

            t_svd = vectors_discrete.T @ vectors

            try:
                U, S, Vh = np.linalg.svd(t_svd)
            except LinAlgError:
                svd_restarts += 1
                print("SVD did not converge, randomizing and trying again")
                break

            ncut_value = 2.0 * (n_samples - S.sum())
            if (abs(ncut_value - last_objective_value) < eps) or (n_iter > n_iter_max):
                has_converged = True
            else:
                # otherwise calculate rotation and continue
                last_objective_value = ncut_value
                rotation = np.dot(Vh.T, U.T)

    if not has_converged:
        raise LinAlgError("SVD did not converge")

def spectral_clustering(
    affinity,
    *,
    n_clusters=8,
    n_init=10,
    assign_labels="kmeans",
):
    """Apply clustering to a projection of the normalized Laplacian.

    In practice Spectral Clustering is very useful when the structure of
    the individual clusters is highly non-convex or more generally when
    a measure of the center and spread of the cluster is not a suitable
    description of the complete cluster. For instance, when clusters are
    nested circles on the 2D plane.

    If affinity is the adjacency matrix of a graph, this method can be
    used to find normalized graph cuts [1]_, [2]_.

    Read more in the :ref:`User Guide <spectral_clustering>`.

    affinity : {array-like, sparse matrix} of shape (n_samples, n_samples)
        The affinity matrix describing the relationship of the samples to
        embed. **Must be symmetric**.

        Possible examples:
          - adjacency matrix of a graph,
          - heat kernel of the pairwise distance matrix of the samples,
          - symmetric k-nearest neighbours connectivity matrix of the samples.

    n_clusters : int, default=None
        Number of clusters to extract.

    n_components : int, default=n_clusters
        Number of eigenvectors to use for the spectral embedding.

    eigen_solver : {None, 'arpack', 'lobpcg', or 'amg'}
        The eigenvalue decomposition method. If None then ``'arpack'`` is used.
        See [4]_ for more details regarding ``'lobpcg'``.
        Eigensolver ``'amg'`` runs ``'lobpcg'`` with optional
        Algebraic MultiGrid preconditioning and requires pyamg to be installed.
        It can be faster on very large sparse problems [6]_ and [7]_.

        of the lobpcg eigenvectors decomposition when `eigen_solver ==

        .. note::

    n_init : int, default=10
        centroid seeds. The final results will be the best output of n_init
        consecutive runs in terms of inertia. Only used if
        ``assign_labels='kmeans'``.

        avoided.

    assign_labels : {'kmeans', 'discretize', 'cluster_qr'}, default='kmeans'
        The strategy to use to assign labels in the embedding
        space.  There are three ways to assign labels after the Laplacian
        embedding.  k-means can be applied and is a popular choice. But it can
        also be sensitive to initialization. Discretization is another
        approach which is less sensitive to random initialization [3]_.
        The cluster_qr method [5]_ directly extracts clusters from eigenvectors
        in spectral clustering. In contrast to k-means and discretization, cluster_qr
        has no tuning parameters and is not an iterative method, yet may outperform
        k-means and discretization in terms of both quality and speed. For a detailed
        comparison of clustering strategies, refer to the following example:
        :ref:`sphx_glr_auto_examples_cluster_plot_coin_segmentation.py`.

           Added new labeling method 'cluster_qr'.

    Returns
    -------

    Notes
    -----
    The graph should contain only one connected component, elsewhere
    the results make little sense.

    This algorithm solves the normalized cut for `k=2`: it is a
    normalized spectral clustering.

    .. [1] :doi:`Normalized cuts and image segmentation, 2000
           Jianbo Shi, Jitendra Malik
           <10.1109/34.868688>`

    .. [2] :doi:`A Tutorial on Spectral Clustering, 2007
           Ulrike von Luxburg
           <10.1007/s11222-007-9033-z>`

    .. [3] `Multiclass spectral clustering, 2003

    .. [4] :doi:`Toward the Optimal Preconditioned Eigensolver:
           Locally Optimal Block Preconditioned Conjugate Gradient Method, 2001
           A. V. Knyazev
           SIAM Journal on Scientific Computing 23, no. 2, pp. 517-541.
           <10.1137/S1064827500366124>`

    .. [5] :doi:`Simple, direct, and efficient multi-way spectral clustering, 2019
           Anil Damle, Victor Minden, Lexing Ying
           <10.1093/imaiai/iay008>`

    .. [6] :doi:`Multiscale Spectral Image Segmentation Multiscale preconditioning
           for computing eigenvalues of graph Laplacians in image segmentation, 2006
           Andrew Knyazev
           <10.13140/RG.2.2.35280.02565>`

    .. [7] :doi:`Preconditioned spectral clustering for stochastic block partition
           streaming graph challenge (Preliminary version at arXiv.)
           David Zhuzhunashvili, Andrew Knyazev
           <10.1109/HPEC.2017.8091045>`

    Examples
    --------
    >>> from sklearn.cluster import spectral_clustering
    >>> affinity = pairwise_kernels(X, metric='rbf')
    >>> spectral_clustering(
    ...     affinity=affinity, n_clusters=2, assign_labels="discretize", random_state=0
    ... )
    array([1, 1, 1, 0, 0, 0])
    """

    clusterer = SpectralClustering(
        assign_labels=assign_labels,
    ).fit(affinity)

    return clusterer.labels_

class SpectralClustering(ClusterMixin, BaseEstimator):

    the individual clusters is highly non-convex, or more generally when
    description of the complete cluster, such as when clusters are

    If the affinity matrix is the adjacency matrix of a graph, this method
    can be used to find normalized graph cuts [1]_, [2]_.

    When calling ``fit``, an affinity matrix is constructed using either
    a kernel function such the Gaussian (aka RBF) kernel with Euclidean
    distance ``d(X, X)``::

            np.exp(-gamma * d(X,X) ** 2)

    or a k-nearest neighbors connectivity matrix.

    Alternatively, a user-provided affinity matrix can be specified by
    setting ``affinity='precomputed'``.

        used. See [4]_ for more details regarding `'lobpcg'`.

        Number of eigenvectors to use for the spectral embedding. If None,
        defaults to `n_clusters`.

        .. note::

        Kernel coefficient for rbf, poly, sigmoid, laplacian and chi2 kernels.
        Ignored for ``affinity='nearest_neighbors'``, ``affinity='precomputed'``
        or ``affinity='precomputed_nearest_neighbors'``.

    affinity : str or callable, default='rbf'
         - 'nearest_neighbors': construct the affinity matrix by computing a
         - 'rbf': construct the affinity matrix using a radial basis function
           (RBF) kernel.
         - 'precomputed': interpret ``X`` as a precomputed affinity matrix,
           where larger values indicate greater similarity between instances.
         - 'precomputed_nearest_neighbors': interpret ``X`` as a sparse graph
           of precomputed distances, and construct a binary affinity matrix
           from the ``n_neighbors`` nearest neighbors of each instance.
         - one of the kernels supported by
           :func:`~sklearn.metrics.pairwise.pairwise_kernels`.

        Only kernels that produce similarity scores (non-negative values that
        increase with similarity) should be used. This property is not checked
        by the clustering algorithm.

    n_neighbors : int, default=10
        Number of neighbors to use when constructing the affinity matrix using
        the nearest neighbors method. Ignored for ``affinity='rbf'``.

        Stopping criterion for eigen decomposition of the Laplacian matrix.

        avoided.

        The strategy for assigning labels in the embedding space. There are two
        ways to assign labels after the Laplacian embedding. k-means is a
        popular choice, but it can be sensitive to initialization.
        Discretization is another approach which is less sensitive to random
        initialization [3]_.
        The cluster_qr method [5]_ directly extract clusters from eigenvectors
        has no tuning parameters and runs no iterations, yet may outperform
        k-means and discretization in terms of both quality and speed.

    kernel_params : dict of str to any, default=None
        Parameters (keyword arguments) and values for kernel passed as
        callable object. Ignored by other kernels.

        The number of parallel jobs to run when `affinity='nearest_neighbors'`
        or `affinity='precomputed_nearest_neighbors'`. The neighbors search
        will be done in parallel.

    affinity_matrix_ : array-like of shape (n_samples, n_samples)
        Affinity matrix used for clustering. Available only after calling
        ``fit``.

    See Also
    --------
    sklearn.cluster.KMeans : K-Means clustering.
    sklearn.cluster.DBSCAN : Density-Based Spatial Clustering of
        Applications with Noise.

    Notes
    -----
    A distance matrix for which 0 indicates identical elements and high values
    indicate very dissimilar elements can be transformed into an affinity /
    similarity matrix that is well-suited for the algorithm by
    applying the Gaussian (aka RBF, heat) kernel::

        np.exp(- dist_matrix ** 2 / (2. * delta ** 2))

    where ``delta`` is a free parameter representing the width of the Gaussian
    kernel.

    An alternative is to take a symmetric version of the k-nearest neighbors
    connectivity matrix of the points.

    If the pyamg package is installed, it is used: this greatly
    speeds up computation.

    Examples
    --------
    >>> from sklearn.cluster import SpectralClustering
    >>> clustering = SpectralClustering(n_clusters=2,
    ...         assign_labels='discretize',
    ...         random_state=0).fit(X)
    SpectralClustering(assign_labels='discretize', n_clusters=2,
        random_state=0)

    For a comparison of Spectral clustering with other clustering algorithms, see
    """

        "n_init": [Interval(Integral, 1, None, closed="left")],
            callable,
                set(KERNEL_PARAMS)
                | {"nearest_neighbors", "precomputed", "precomputed_nearest_neighbors"}
            ),
        ],
        "eigen_tol": [
        ],
        "assign_labels": [StrOptions({"kmeans", "discretize", "cluster_qr"})],
    }

        self,
        *,
        n_init=10,
        n_neighbors=10,
        assign_labels="kmeans",
        degree=3,
        coef0=1,
    ):
        self.assign_labels = assign_labels

        """Perform spectral clustering from features, or affinity matrix.

            Training instances to cluster, similarities / affinities between
            instances if ``affinity='precomputed'``, or distances between
            instances if ``affinity='precomputed_nearest_neighbors``. If a
            sparse matrix is provided in a format other than ``csr_matrix``,
            ``csc_matrix``, or ``coo_matrix``, it will be converted into a
            sparse ``csr_matrix``.

        y : Ignored

        Returns
        -------
            A fitted instance of the estimator.
        """
            self,
            X,
        )
        allow_squared = self.affinity in [
        ]
        if X.shape[0] == X.shape[1] and not allow_squared:
                "The spectral clustering API has changed. ``fit``"
                "now constructs an affinity matrix from data. To use"
                " a custom affinity matrix, "
                "set ``affinity=precomputed``."
            )

            connectivity = kneighbors_graph(
                X, n_neighbors=self.n_neighbors, include_self=True, n_jobs=self.n_jobs
            )
        elif self.affinity == "precomputed_nearest_neighbors":
            ).fit(X)
        elif self.affinity == "precomputed":
        else:
            params = self.kernel_params
            if params is None:
                params = {}
            if not callable(self.affinity):
                params["gamma"] = self.gamma
                params["degree"] = self.degree
                params["coef0"] = self.coef0
            self.affinity_matrix_ = pairwise_kernels(
                X, metric=self.affinity, filter_params=True, **params
            )

        n_components = (
            self.n_clusters if self.n_components is None else self.n_components
        )
        # We now obtain the real valued solution matrix to the
        # relaxed Ncut problem, solving the eigenvalue problem
        # L_sym x = lambda x  and recovering u = D^-1/2 x.
        # The first eigenvector is constant only for fully connected graphs
        # and should be kept for spectral clustering (drop_first = False)
        # See spectral_embedding documentation.
        maps = _spectral_embedding(
        )
            print(f"Computing label assignment using {self.assign_labels}")

        if self.assign_labels == "kmeans":
            _, self.labels_, _ = k_means(
                maps,
                self.n_clusters,
                n_init=self.n_init,
            )
        elif self.assign_labels == "cluster_qr":
            self.labels_ = cluster_qr(maps)
        else:
            self.labels_ = discretize(maps, random_state=random_state)

        """Perform spectral clustering on `X` and return cluster labels.

        y : Ignored

        Returns
        -------
        """

        ]

        self,
        *,
        degree=3,
        coef0=1,
    ):

        y : Ignored

        Returns
        -------
        """
            self,
            X,
        )
        ]
            )

            )
            ).fit(X)
        else:
                params = {}
            )

        )
        )

                maps,
            )
        else:

        y : Ignored

        Returns
        -------
        """

def _scale_normalize(X):
    """Normalize ``X`` by scaling rows and columns independently.

    Returns the normalized matrix and the row and column scaling
    factors.
    """
    X = make_nonnegative(X)
    row_diag = np.asarray(1.0 / np.sqrt(X.sum(axis=1))).squeeze()
    col_diag = np.asarray(1.0 / np.sqrt(X.sum(axis=0))).squeeze()
    row_diag = np.where(np.isnan(row_diag), 0, row_diag)
    col_diag = np.where(np.isnan(col_diag), 0, col_diag)
    if issparse(X):
        n_rows, n_cols = X.shape
        r = dia_matrix((row_diag, [0]), shape=(n_rows, n_rows))
        c = dia_matrix((col_diag, [0]), shape=(n_cols, n_cols))
        an = r @ X @ c
    else:
        an = row_diag[:, np.newaxis] * X * col_diag
    return an, row_diag, col_diag

def _bistochastic_normalize(X, max_iter=1000, tol=1e-5):
    """Normalize rows and columns of ``X`` simultaneously so that all
    rows sum to one constant and all columns sum to a different
    constant.
    """
    # According to paper, this can also be done more efficiently with
    # deviation reduction and balancing algorithms.
    X_scaled = X
        X_new, _, _ = _scale_normalize(X_scaled)
            dist = norm(X_scaled.data - X.data)
        else:
            dist = norm(X_scaled - X_new)
        X_scaled = X_new
        if dist is not None and dist < tol:
            break
    return X_scaled

def _log_normalize(X):
    """Normalize ``X`` according to Kluger's log-interactions scheme."""
    X = make_nonnegative(X, min_value=1)
            "Cannot compute log of a sparse matrix,"
            " because log(x) diverges to -infinity as x"
            " goes to 0."
        )
    L = np.log(X)
    row_avg = L.mean(axis=1)[:, np.newaxis]
    col_avg = L.mean(axis=0)
    avg = L.mean()
    return L - row_avg - col_avg + avg

class BaseSpectral(BiclusterMixin, BaseEstimator, metaclass=ABCMeta):
    """Base class for spectral biclustering."""

        "svd_method": [StrOptions({"randomized", "arpack"})],
        "n_svd_vecs": [Interval(Integral, 0, None, closed="left"), None],
        "mini_batch": ["boolean"],
        "init": [StrOptions({"k-means++", "random"}), np.ndarray],
    }

        self,
        n_clusters=3,
        n_svd_vecs=None,
        mini_batch=False,
    ):
        self.n_svd_vecs = n_svd_vecs
        self.mini_batch = mini_batch

    def _check_parameters(self, n_samples):
        """Validate parameters depending on the input data."""

        """Create a biclustering for X.

        y : Ignored

        Returns
        -------
            SpectralBiclustering instance.
        """
        X = validate_data(self, X, accept_sparse="csr", dtype=np.float64)
        self._check_parameters(X.shape[0])

    def _svd(self, array, n_components, n_discard):
        """Returns first `n_components` left and right singular
        vectors u and v, discarding the first `n_discard`.
        """
        if self.svd_method == "randomized":
            kwargs = {}
            if self.n_svd_vecs is not None:
                kwargs["n_oversamples"] = self.n_svd_vecs
            u, _, vt = _randomized_svd(
                array, n_components, random_state=self.random_state, **kwargs
            )

        elif self.svd_method == "arpack":
            u, _, vt = svds(array, k=n_components, ncv=self.n_svd_vecs)
            if np.any(np.isnan(vt)):
                # some eigenvalues of A * A.T are negative, causing
                # sqrt() to be np.nan. This causes some vectors in vt
                # to be np.nan.
                A = safe_sparse_dot(array.T, array)
                random_state = check_random_state(self.random_state)
                # initialize with [-1,1] as in ARPACK
                v0 = random_state.uniform(-1, 1, A.shape[0])
                _, v = eigsh(A, ncv=self.n_svd_vecs, v0=v0)
                vt = v.T
            if np.any(np.isnan(u)):
                A = safe_sparse_dot(array, array.T)
                _, u = eigsh(A, ncv=self.n_svd_vecs, v0=v0)

        assert_all_finite(u)
        assert_all_finite(vt)
        u = u[:, n_discard:]
        vt = vt[n_discard:]
        return u, vt.T

    def _k_means(self, data, n_clusters):
        if self.mini_batch:
            model = MiniBatchKMeans(
                init=self.init,
            )
        else:
            model = KMeans(
            )
        model.fit(data)
        centroid = model.cluster_centers_
        labels = model.labels_
        return centroid, labels

class SpectralCoclustering(BaseSpectral):
    """Spectral Co-Clustering algorithm (Dhillon, 2001).

    Clusters rows and columns of an array `X` to solve the relaxed
    normalized cut of the bipartite graph created from `X` as follows:
    the edge between row vertex `i` and column vertex `j` has weight
    `X[i, j]`.

    The resulting bicluster structure is block-diagonal, since each
    row and each column belongs to exactly one bicluster.

    Supports sparse matrices, as long as they are nonnegative.

    Read more in the :ref:`User Guide <spectral_coclustering>`.

    n_clusters : int, default=3
        The number of biclusters to find.

    svd_method : {'randomized', 'arpack'}, default='randomized'
        Selects the algorithm for finding singular vectors. May be
        'randomized' or 'arpack'. If 'randomized', use
        :func:`sklearn.utils.extmath.randomized_svd`, which may be faster
        for large matrices. If 'arpack', use
        :func:`scipy.sparse.linalg.svds`, which is more accurate, but
        possibly slower in some cases.

    n_svd_vecs : int, default=None
        Number of vectors to use in calculating the SVD. Corresponds
        to `ncv` when `svd_method=arpack` and `n_oversamples` when
        `svd_method` is 'randomized`.

    mini_batch : bool, default=False
        Whether to use mini-batch k-means, which is faster but may get
        different results.

    init : {'k-means++', 'random'}, or ndarray of shape \
        Method for initialization of k-means algorithm; defaults to
        'k-means++'.

        Number of random initializations that are tried with the
        k-means algorithm.

        If mini-batch k-means is used, the best initialization is
        chosen and the algorithm runs once. Otherwise, the algorithm
        is run for each initialization and the best solution chosen.

        Used for randomizing the singular value decomposition and the k-means
        initialization. Use an int to make the randomness deterministic.

    rows_ : array-like of shape (n_row_clusters, n_rows)
        Results of the clustering. `rows[i, r]` is True if
        cluster `i` contains row `r`. Available only after calling ``fit``.

    columns_ : array-like of shape (n_column_clusters, n_columns)
        Results of the clustering, like `rows`.

    row_labels_ : array-like of shape (n_rows,)
        The bicluster label of each row.

    column_labels_ : array-like of shape (n_cols,)
        The bicluster label of each column.

    biclusters_ : tuple of two ndarrays
        The tuple contains the `rows_` and `columns_` arrays.

    See Also
    --------
    SpectralBiclustering : Partitions rows and columns under the assumption
        that the data has an underlying checkerboard structure.

    * :doi:`Dhillon, Inderjit S, 2001. Co-clustering documents and words using
      bipartite spectral graph partitioning.
      <10.1145/502512.502550>`

    Examples
    --------
    >>> from sklearn.cluster import SpectralCoclustering
    >>> clustering = SpectralCoclustering(n_clusters=2, random_state=0).fit(X)
    >>> clustering.row_labels_ #doctest: +SKIP
    array([0, 1, 1, 0, 0, 0], dtype=int32)
    >>> clustering.column_labels_ #doctest: +SKIP
    array([0, 0], dtype=int32)
    SpectralCoclustering(n_clusters=2, random_state=0)
    """

        **BaseSpectral._parameter_constraints,
    }

        self,
        *,
    ):
            n_clusters, svd_method, n_svd_vecs, mini_batch, init, n_init, random_state
        )

        if self.n_clusters > n_samples:
                f"n_clusters should be <= n_samples={n_samples}. Got"
                f" {self.n_clusters} instead."
            )

        normalized_data, row_diag, col_diag = _scale_normalize(X)
        n_sv = 1 + int(np.ceil(np.log2(self.n_clusters)))
        u, v = self._svd(normalized_data, n_sv, n_discard=1)
        z = np.vstack((row_diag[:, np.newaxis] * u, col_diag[:, np.newaxis] * v))

        _, labels = self._k_means(z, self.n_clusters)

        n_rows = X.shape[0]
        self.row_labels_ = labels[:n_rows]
        self.column_labels_ = labels[n_rows:]

        self.rows_ = np.vstack([self.row_labels_ == c for c in range(self.n_clusters)])
        self.columns_ = np.vstack(
            [self.column_labels_ == c for c in range(self.n_clusters)]
        )

class SpectralBiclustering(BaseSpectral):
    """Spectral biclustering (Kluger, 2003).

    Partitions rows and columns under the assumption that the data has
    an underlying checkerboard structure. For instance, if there are
    two row partitions and three column partitions, each row will
    belong to three biclusters, and each column will belong to two
    biclusters. The outer product of the corresponding row and column
    label vectors gives this checkerboard structure.

    Read more in the :ref:`User Guide <spectral_biclustering>`.

    n_clusters : int or tuple (n_row_clusters, n_column_clusters), default=3
        The number of row and column clusters in the checkerboard
        structure.

    method : {'bistochastic', 'scale', 'log'}, default='bistochastic'
        Method of normalizing and converting singular vectors into
        biclusters. May be one of 'scale', 'bistochastic', or 'log'.
        The authors recommend using 'log'. If the data is sparse,
        however, log normalization will not work, which is why the
        default is 'bistochastic'.

           if `method='log'`, the data must not be sparse.

    n_components : int, default=6
        Number of singular vectors to check.

    n_best : int, default=3
        Number of best singular vectors to which to project the data
        for clustering.

        'randomized' or 'arpack'. If 'randomized', uses
        :func:`~sklearn.utils.extmath.randomized_svd`, which may be faster
        for large matrices. If 'arpack', uses
        `scipy.sparse.linalg.svds`, which is more accurate, but

    init : {'k-means++', 'random'} or ndarray of shape (n_clusters, n_features), \
            default='k-means++'

        Row partition labels.

        Column partition labels.

    See Also
    --------
    SpectralCoclustering : Spectral Co-Clustering algorithm (Dhillon, 2001).

    * :doi:`Kluger, Yuval, et. al., 2003. Spectral biclustering of microarray
      data: coclustering genes and conditions.
      <10.1101/gr.648603>`

    Examples
    --------
    >>> from sklearn.cluster import SpectralBiclustering
    >>> clustering = SpectralBiclustering(n_clusters=2, random_state=0).fit(X)
    >>> clustering.row_labels_
    >>> clustering.column_labels_
    SpectralBiclustering(n_clusters=2, random_state=0)

    For a more detailed example, see
    :ref:`sphx_glr_auto_examples_bicluster_plot_spectral_biclustering.py`
    """

        "n_clusters": [Interval(Integral, 1, None, closed="left"), tuple],
        "method": [StrOptions({"bistochastic", "scale", "log"})],
        "n_best": [Interval(Integral, 1, None, closed="left")],
    }

        self,
        *,
        method="bistochastic",
        n_components=6,
        n_best=3,
    ):
        )
        self.n_best = n_best

        if isinstance(self.n_clusters, Integral):
            if self.n_clusters > n_samples:
                    f"n_clusters should be <= n_samples={n_samples}. Got"
                    f" {self.n_clusters} instead."
                )
        else:  # tuple
            try:
                n_row_clusters, n_column_clusters = self.n_clusters
                    n_row_clusters,
                    "n_row_clusters",
                    target_type=Integral,
                    min_val=1,
                    max_val=n_samples,
                )
                    n_column_clusters,
                    "n_column_clusters",
                )
            except (ValueError, TypeError) as e:
                    "Incorrect parameter n_clusters has value:"
                    f" {self.n_clusters}. It should either be a single integer"
                    " or an iterable with two integers:"
                    " (n_row_clusters, n_column_clusters)"
                    " And the values are should be in the"
                    " range: (1, n_samples)"
                ) from e

        if self.n_best > self.n_components:
                f"n_best={self.n_best} must be <= n_components={self.n_components}."
            )

        n_sv = self.n_components
        if self.method == "bistochastic":
            normalized_data = _bistochastic_normalize(X)
            n_sv += 1
        elif self.method == "scale":
            normalized_data, _, _ = _scale_normalize(X)
            n_sv += 1
        elif self.method == "log":
            normalized_data = _log_normalize(X)
        n_discard = 0 if self.method == "log" else 1
        u, v = self._svd(normalized_data, n_sv, n_discard)
        ut = u.T
        vt = v.T

        try:
            n_row_clusters, n_col_clusters = self.n_clusters
            n_row_clusters = n_col_clusters = self.n_clusters

        best_ut = self._fit_best_piecewise(ut, self.n_best, n_row_clusters)

        best_vt = self._fit_best_piecewise(vt, self.n_best, n_col_clusters)

        self.row_labels_ = self._project_and_cluster(X, best_vt.T, n_row_clusters)

        self.column_labels_ = self._project_and_cluster(X.T, best_ut.T, n_col_clusters)

        self.rows_ = np.vstack(
            [
                self.row_labels_ == label
                for label in range(n_row_clusters)
                for _ in range(n_col_clusters)
            ]
        )
            [
                self.column_labels_ == label
                for _ in range(n_row_clusters)
                for label in range(n_col_clusters)
            ]
        )

    def _fit_best_piecewise(self, vectors, n_best, n_clusters):
        """Find the ``n_best`` vectors that are best approximated by piecewise
        constant vectors.

        The piecewise vectors are found by k-means; the best is chosen
        according to Euclidean distance.

        """

        def make_piecewise(v):
            centroid, labels = self._k_means(v.reshape(-1, 1), n_clusters)
            return centroid[labels].ravel()

        piecewise_vectors = np.apply_along_axis(make_piecewise, axis=1, arr=vectors)
        dists = np.apply_along_axis(norm, axis=1, arr=(vectors - piecewise_vectors))
        result = vectors[np.argsort(dists)[:n_best]]

    def _project_and_cluster(self, data, vectors, n_clusters):
        """Project ``data`` to ``vectors`` and cluster the result."""
        projected = safe_sparse_dot(data, vectors)
        _, labels = self._k_means(projected, n_clusters)

        self,
    ):

        y : Ignored

        Returns
        -------
        """

        """
            kwargs = {}
            )

                vt = v.T

            )
        else:
            )

        self,
        *,
    ):
        )

            )

        )

        self,
        *,
        n_best=3,
    ):
        )

                )
            try:
                )
                )
                ) from e

            )

            n_sv += 1
            n_sv += 1
        ut = u.T
        vt = v.T

        try:

            [
            ]
        )
            [
            ]
        )

        """

def dbscan(
    X,
    eps=0.5,
    *,
    min_samples=5,
    algorithm="auto",
    leaf_size=30,
    p=2,
):
    """Perform DBSCAN clustering from vector array or distance matrix.

    Read more in the :ref:`User Guide <dbscan>`.

    X : {array-like, sparse (CSR) matrix} of shape (n_samples, n_features) or \
        A feature array, or array of distances between samples if
        ``metric='precomputed'``.

    eps : float, default=0.5
        The maximum distance between two samples for one to be considered
        as in the neighborhood of the other. This is not a maximum bound
        on the distances of points within a cluster. This is the most
        important DBSCAN parameter to choose appropriately for your data set
        and distance function.

    min_samples : int, default=5
        The number of samples (or total weight) in a neighborhood for a point
        to be considered as a core point. This includes the point itself.

        must be square during fit.
        X may be a :term:`sparse graph <sparse graph>`,
        in which case only "nonzero" elements may be considered neighbors.

        The algorithm to be used by the NearestNeighbors module
        to compute pointwise distances and find nearest neighbors.
        See NearestNeighbors module documentation for details.

        Leaf size passed to BallTree or cKDTree. This can affect the speed
        of the construction and query, as well as the memory required
        to store the tree. The optimal value depends
        on the nature of the problem.

        The power of the Minkowski metric to be used to calculate distance
        between points.

        Weight of each sample, such that a sample with a weight of at least
        ``min_samples`` is by itself a core sample; a sample with negative
        weight may inhibit its eps-neighbor from being core.
        Note that weights are absolute, and default to 1.

        The number of parallel jobs to run for neighbors search. ``None`` means
        1 unless in a :obj:`joblib.parallel_backend` context. ``-1`` means
        If precomputed distance are used, parallel execution is not available
        and thus n_jobs will have no effect.

    Returns
    -------
    core_samples : ndarray of shape (n_core_samples,)
        Indices of core samples.

        Cluster labels for each point.  Noisy samples are given the label -1.

    See Also
    --------
    DBSCAN : An estimator interface for this clustering algorithm.
    OPTICS : A similar estimator interface clustering at multiple values of
        eps. Our implementation is optimized for memory usage.

    Notes
    -----
    For an example, see :ref:`sphx_glr_auto_examples_cluster_plot_dbscan.py`.

    This implementation bulk-computes all neighborhood queries, which increases
    the memory complexity to O(n.d) where d is the average number of neighbors,
    while original DBSCAN had memory complexity O(n). It may attract a higher
    memory complexity when querying these nearest neighborhoods, depending
    on the ``algorithm``.

    One way to avoid the query complexity is to pre-compute sparse
    neighborhoods in chunks using
    :func:`NearestNeighbors.radius_neighbors_graph
    <sklearn.neighbors.NearestNeighbors.radius_neighbors_graph>` with
    ``mode='distance'``, then using ``metric='precomputed'`` here.

    Another way to reduce memory and computation time is to remove
    (near-)duplicate points and use ``sample_weight`` instead.

    :class:`~sklearn.cluster.OPTICS` provides a similar clustering with lower
    memory usage.

    Ester, M., H. P. Kriegel, J. Sander, and X. Xu, `"A Density-Based
    Algorithm for Discovering Clusters in Large Spatial Databases with Noise"
    <https://www.dbs.ifi.lmu.de/Publikationen/Papers/KDD-96.final.frame.pdf>`_.
    In: Proceedings of the 2nd International Conference on Knowledge Discovery
    and Data Mining, Portland, OR, AAAI Press, pp. 226-231. 1996

    Schubert, E., Sander, J., Ester, M., Kriegel, H. P., & Xu, X. (2017).
    :doi:`"DBSCAN revisited, revisited: why and how you should (still) use DBSCAN."
    <10.1145/3068335>`
    ACM Transactions on Database Systems (TODS), 42(3), 19.

    Examples
    --------
    >>> from sklearn.cluster import dbscan
    >>> X = [[1, 2], [2, 2], [2, 3], [8, 7], [8, 8], [25, 80]]
    >>> core_samples, labels = dbscan(X, eps=3, min_samples=2)
    >>> core_samples
    array([0, 1, 2, 3, 4])
    >>> labels
    array([ 0,  0,  0,  1,  1, -1])
    """

    est = DBSCAN(
        eps=eps,
        min_samples=min_samples,
        metric_params=metric_params,
        leaf_size=leaf_size,
        p=p,
    )
    est.fit(X, sample_weight=sample_weight)
    return est.core_sample_indices_, est.labels_

class DBSCAN(ClusterMixin, BaseEstimator):

    DBSCAN - Density-Based Spatial Clustering of Applications with Noise.
    Finds core samples of high density and expands clusters from them.
    Good for data which contains clusters of similar density.

    This implementation has a worst case memory complexity of :math:`O({n}^2)`,
    which can occur when the `eps` param is large and `min_samples` is low,
    while the original DBSCAN only uses linear memory.
    For further details, see the Notes below.

        The number of samples (or total weight) in a neighborhood for a point to
        be considered as a core point. This includes the point itself. If
        `min_samples` is set to a higher value, DBSCAN will find denser clusters,
        whereas if it is set to a lower value, the found clusters will be more
        sparse.

    metric : str, or callable, default='euclidean'
        must be square. X may be a :term:`sparse graph`, in which
        case only "nonzero" elements may be considered neighbors for DBSCAN.

           metric *precomputed* to accept precomputed sparse matrix.

    p : float, default=None
        between points. If None, then ``p=2`` (equivalent to the Euclidean
        distance).

    core_sample_indices_ : ndarray of shape (n_core_samples,)

    components_ : ndarray of shape (n_core_samples, n_features)
        Copy of each core sample found by training.

    labels_ : ndarray of shape (n_samples)
        Cluster labels for each point in the dataset given to fit().
        Noisy samples are given the label -1.

    See Also
    --------
    OPTICS : A similar clustering at multiple values of eps. Our implementation
        is optimized for memory usage.

    Notes
    -----

    :class:`~sklearn.cluster.OPTICS` provides a similar clustering with lower memory
    usage.

    Examples
    --------
    >>> X = np.array([[1, 2], [2, 2], [2, 3],
    ...               [8, 7], [8, 8], [25, 80]])
    >>> clustering = DBSCAN(eps=3, min_samples=2).fit(X)
    DBSCAN(eps=3, min_samples=2)

    For an example, see
    :ref:`sphx_glr_auto_examples_cluster_plot_dbscan.py`.

    For a comparison of DBSCAN with other clustering algorithms, see
    """

        "eps": [Interval(Real, 0.0, None, closed="neither")],
        "min_samples": [Interval(Integral, 1, None, closed="left")],
            StrOptions(set(_VALID_METRICS) | {"precomputed"}),
            callable,
        ],
        "p": [Interval(Real, 0.0, None, closed="left"), None],
    }

        self,
        eps=0.5,
        *,
        min_samples=5,
        p=None,
    ):
        self.min_samples = min_samples
        self.p = p

        # DBSCAN.metric is not validated yet
    )
        """Perform DBSCAN clustering from features, or distance matrix.

            Training instances to cluster, or distances between instances if
            ``metric='precomputed'``. If a sparse matrix is provided, it will
            be converted into a sparse ``csr_matrix``.

        y : Ignored

            Weight of each sample, such that a sample with a weight of at least
            ``min_samples`` is by itself a core sample; a sample with a
            negative weight may inhibit its eps-neighbor from being core.
            Note that weights are absolute, and default to 1.

        Returns
        -------
        """

        # Calculate neighborhood for all samples. This leaves the original
        # point in, which needs to be considered later (i.e. point i is in the
        # neighborhood of point i. While True, its useless information)
        if self.metric == "precomputed" and sparse.issparse(X):
            # set the diagonal to explicit values, as a point is its own
            # neighbor
            X = X.copy()  # copy to avoid in-place modification
                warnings.simplefilter("ignore", sparse.SparseEfficiencyWarning)
                X.setdiag(X.diagonal())

        neighbors_model = NearestNeighbors(
            radius=self.eps,
            algorithm=self.algorithm,
            p=self.p,
        )
        neighbors_model.fit(X)
        # This has worst case O(n^2) memory complexity
        neighborhoods = neighbors_model.radius_neighbors(X, return_distance=False)

            n_neighbors = np.array([len(neighbors) for neighbors in neighborhoods])
        else:
            n_neighbors = np.array(
                [np.sum(sample_weight[neighbors]) for neighbors in neighborhoods]
            )

        # Initially, all samples are noise.
        labels = np.full(X.shape[0], -1, dtype=np.intp)

        # A list of all core samples found.
        core_samples = np.asarray(n_neighbors >= self.min_samples, dtype=np.uint8)
        dbscan_inner(core_samples, neighborhoods, labels)

        self.core_sample_indices_ = np.where(core_samples)[0]
        self.labels_ = labels

        if len(self.core_sample_indices_):
            # fix for scipy sparse indexing issue
            self.components_ = X[self.core_sample_indices_].copy()
        else:
            # no core samples
            self.components_ = np.empty((0, X.shape[1]))

        """Compute clusters from a data or distance matrix and predict labels.

        y : Ignored

        Returns
        -------
            Cluster labels. Noisy samples are given the label -1.
        """
        self.fit(X, sample_weight=sample_weight)

        self,
        eps=0.5,
        *,
        p=None,
    ):
        self.p = p

        y : Ignored

        Returns
        -------
        """

            # neighbor

            p=self.p,
        )

        else:
            )

        else:

        y : Ignored

        Returns
        -------
        """

def _iterate_sparse_X(X):
    """This little hack returns a densified row when iterating over a sparse
    matrix, instead of constructing a sparse matrix for every row that is
    expensive.
    """
    X_indices = X.indices
    X_data = X.data
    X_indptr = X.indptr

        row = np.zeros(X.shape[1])
        startptr, endptr = X_indptr[i], X_indptr[i + 1]
        nonzero_indices = X_indices[startptr:endptr]
        row[nonzero_indices] = X_data[startptr:endptr]
        yield row

def _split_node(node, threshold, branching_factor):
    """The node has to be split if there is no place for a new subcluster
    in the node.
    1. Two empty nodes and two empty subclusters are initialized.
    2. The pair of distant subclusters are found.
    3. The properties of the empty subclusters and nodes are updated
       according to the nearest distance between the subclusters to the
       pair of distant subclusters.
    4. The two nodes are set as children to the two subclusters.
    """
    new_subcluster1 = _CFSubcluster()
    new_subcluster2 = _CFSubcluster()
    new_node1 = _CFNode(
        threshold=threshold,
        branching_factor=branching_factor,
        is_leaf=node.is_leaf,
        n_features=node.n_features,
        dtype=node.init_centroids_.dtype,
    )
    new_node2 = _CFNode(
    )
    new_subcluster1.child_ = new_node1
    new_subcluster2.child_ = new_node2

    if node.is_leaf:
        if node.prev_leaf_ is not None:
            node.prev_leaf_.next_leaf_ = new_node1
        new_node1.prev_leaf_ = node.prev_leaf_
        new_node1.next_leaf_ = new_node2
        new_node2.prev_leaf_ = new_node1
        new_node2.next_leaf_ = node.next_leaf_
        if node.next_leaf_ is not None:
            node.next_leaf_.prev_leaf_ = new_node2

    dist = euclidean_distances(
        node.centroids_, Y_norm_squared=node.squared_norm_, squared=True
    )
    n_clusters = dist.shape[0]

    farthest_idx = np.unravel_index(dist.argmax(), (n_clusters, n_clusters))
    node1_dist, node2_dist = dist[(farthest_idx,)]

    node1_closer = node1_dist < node2_dist
    # make sure node1 is closest to itself even if all distances are equal.
    # This can only happen when all node.centroids_ are duplicates leading to all
    # distances between centroids being zero.
    node1_closer[farthest_idx[0]] = True

    for idx, subcluster in enumerate(node.subclusters_):
        if node1_closer[idx]:
            new_node1.append_subcluster(subcluster)
            new_subcluster1.update(subcluster)
        else:
            new_node2.append_subcluster(subcluster)
            new_subcluster2.update(subcluster)
    return new_subcluster1, new_subcluster2

class _CFNode:
    """Each node in a CFTree is called a CFNode.

    The CFNode can have a maximum of branching_factor
    number of CFSubclusters.

    threshold : float
        Threshold needed for a new subcluster to enter a CFSubcluster.

    branching_factor : int
        Maximum number of CF subclusters in each node.

        We need to know if the CFNode is a leaf or not, in order to
        retrieve the final subclusters.

    subclusters_ : list
        List of subclusters for a particular CFNode.

    prev_leaf_ : _CFNode
        Useful only if is_leaf is True.

    next_leaf_ : _CFNode
        next_leaf. Useful only if is_leaf is True.
        the final subclusters.

    init_centroids_ : ndarray of shape (branching_factor + 1, n_features)
        Manipulate ``init_centroids_`` throughout rather than centroids_ since
        the centroids are just a view of the ``init_centroids_`` .

    init_sq_norm_ : ndarray of shape (branching_factor + 1,)
        manipulate init_sq_norm_ throughout. similar to ``init_centroids_``.

    centroids_ : ndarray of shape (branching_factor + 1, n_features)
        View of ``init_centroids_``.

    squared_norm_ : ndarray of shape (branching_factor + 1,)
        View of ``init_sq_norm_``.

    """

    def __init__(self, *, threshold, branching_factor, is_leaf, n_features, dtype):
        self.branching_factor = branching_factor
        self.is_leaf = is_leaf

        # The list of subclusters, centroids and squared norms
        # to manipulate throughout.
        self.subclusters_ = []
        self.init_centroids_ = np.zeros((branching_factor + 1, n_features), dtype=dtype)
        self.init_sq_norm_ = np.zeros((branching_factor + 1), dtype)
        self.squared_norm_ = []
        self.prev_leaf_ = None
        self.next_leaf_ = None

    def append_subcluster(self, subcluster):
        n_samples = len(self.subclusters_)
        self.subclusters_.append(subcluster)
        self.init_centroids_[n_samples] = subcluster.centroid_
        self.init_sq_norm_[n_samples] = subcluster.sq_norm_

        # Keep centroids and squared norm as views. In this way
        # if we change init_centroids and init_sq_norm_, it is
        # sufficient,
        self.centroids_ = self.init_centroids_[: n_samples + 1, :]
        self.squared_norm_ = self.init_sq_norm_[: n_samples + 1]

    def update_split_subclusters(self, subcluster, new_subcluster1, new_subcluster2):
        """Remove a subcluster from a node and update it with the
        split subclusters.
        """
        ind = self.subclusters_.index(subcluster)
        self.subclusters_[ind] = new_subcluster1
        self.init_centroids_[ind] = new_subcluster1.centroid_
        self.init_sq_norm_[ind] = new_subcluster1.sq_norm_
        self.append_subcluster(new_subcluster2)

    def insert_cf_subcluster(self, subcluster):
        """Insert a new subcluster into the node."""
        if not self.subclusters_:
            self.append_subcluster(subcluster)

        threshold = self.threshold
        branching_factor = self.branching_factor
        # We need to find the closest subcluster among all the
        # subclusters so that we can insert our new subcluster.
        dist_matrix = np.dot(self.centroids_, subcluster.centroid_)
        dist_matrix *= -2.0
        dist_matrix += self.squared_norm_
        closest_index = np.argmin(dist_matrix)
        closest_subcluster = self.subclusters_[closest_index]

        # If the subcluster has a child, we need a recursive strategy.
        if closest_subcluster.child_ is not None:
            split_child = closest_subcluster.child_.insert_cf_subcluster(subcluster)

            if not split_child:
                # If it is determined that the child need not be split, we
                # can just update the closest_subcluster
                closest_subcluster.update(subcluster)
                self.init_centroids_[closest_index] = self.subclusters_[
                    closest_index
                ].centroid_
                self.init_sq_norm_[closest_index] = self.subclusters_[
                ].sq_norm_

            # things not too good. we need to redistribute the subclusters in
            # our child node, and add a new subcluster in the parent
            # subcluster to accommodate the new child.
            else:
                new_subcluster1, new_subcluster2 = _split_node(
                    closest_subcluster.child_,
                    threshold,
                    branching_factor,
                )
                self.update_split_subclusters(
                    closest_subcluster, new_subcluster1, new_subcluster2
                )

                if len(self.subclusters_) > self.branching_factor:
                    return True

        # good to go!
        else:
            merged = closest_subcluster.merge_subcluster(subcluster, self.threshold)
            if merged:
                self.init_centroids_[closest_index] = closest_subcluster.centroid_
                self.init_sq_norm_[closest_index] = closest_subcluster.sq_norm_

            # not close to any other subclusters, and we still
            # have space, so add.
            elif len(self.subclusters_) < self.branching_factor:
                self.append_subcluster(subcluster)

            # We do not have enough space nor is it closer to an
            # other subcluster. We need to split.
            else:

class _CFSubcluster:
    """Each subcluster in a CFNode is called a CFSubcluster.

    A CFSubcluster can have a CFNode has its child.

    linear_sum : ndarray of shape (n_features,), default=None
        Sample. This is kept optional to allow initialization of empty
        subclusters.

        Number of samples that belong to each subcluster.

    linear_sum_ : ndarray
        Linear sum of all the samples in a subcluster. Prevents holding
        all sample data in memory.

    squared_sum_ : float
        Sum of the squared l2 norms of all samples belonging to a subcluster.

    centroid_ : ndarray of shape (branching_factor + 1, n_features)
        Centroid of the subcluster. Prevent recomputing of centroids when
        ``CFNode.centroids_`` is called.

    child_ : _CFNode
        Child Node of the subcluster. Once a given _CFNode is set as the child
        of the _CFNode, it is set to ``self.child_``.

    sq_norm_ : ndarray of shape (branching_factor + 1,)
        Squared norm of the subcluster. Used to prevent recomputing when
        pairwise minimum distances are computed.
    """

    def __init__(self, *, linear_sum=None):
        if linear_sum is None:
            self.n_samples_ = 0
            self.squared_sum_ = 0.0
            self.centroid_ = self.linear_sum_ = 0
        else:
            self.n_samples_ = 1
            self.centroid_ = self.linear_sum_ = linear_sum
            self.squared_sum_ = self.sq_norm_ = np.dot(
                self.linear_sum_, self.linear_sum_
            )
        self.child_ = None

    def update(self, subcluster):
        self.n_samples_ += subcluster.n_samples_
        self.linear_sum_ += subcluster.linear_sum_
        self.squared_sum_ += subcluster.squared_sum_
        self.centroid_ = self.linear_sum_ / self.n_samples_
        self.sq_norm_ = np.dot(self.centroid_, self.centroid_)

    def merge_subcluster(self, nominee_cluster, threshold):
        """Check if a cluster is worthy enough to be merged. If
        yes then merge.
        """
        new_ss = self.squared_sum_ + nominee_cluster.squared_sum_
        new_ls = self.linear_sum_ + nominee_cluster.linear_sum_
        new_n = self.n_samples_ + nominee_cluster.n_samples_
        new_centroid = (1 / new_n) * new_ls
        new_sq_norm = np.dot(new_centroid, new_centroid)

        # The squared radius of the cluster is defined:
        #   r^2  = sum_i ||x_i - c||^2 / n
        # with x_i the n points assigned to the cluster and c its centroid:
        #   c = sum_i x_i / n
        # This can be expanded to:
        #   r^2 = sum_i ||x_i||^2 / n - 2 < sum_i x_i / n, c> + n ||c||^2 / n
        # and therefore simplifies to:
        #   r^2 = sum_i ||x_i||^2 / n - ||c||^2
        sq_radius = new_ss / new_n - new_sq_norm

        if sq_radius <= threshold**2:
            (
                self.n_samples_,
                self.linear_sum_,
                self.squared_sum_,
                self.centroid_,
                self.sq_norm_,
            ) = (new_n, new_ls, new_ss, new_centroid, new_sq_norm)

    @property
    def radius(self):
        """Return radius of the subcluster"""
        # Because of numerical issues, this could become negative
        sq_radius = self.squared_sum_ / self.n_samples_ - self.sq_norm_
        return sqrt(max(0, sq_radius))

class Birch(
    ClassNamePrefixFeaturesOutMixin, ClusterMixin, TransformerMixin, BaseEstimator
):
    """Implements the BIRCH clustering algorithm.

    It is a memory-efficient, online-learning algorithm provided as an
    alternative to :class:`MiniBatchKMeans`. It constructs a tree
    data structure with the cluster centroids being read off the leaf.
    These can be either the final cluster centroids or can be provided as input
    to another clustering algorithm such as :class:`AgglomerativeClustering`.

    Read more in the :ref:`User Guide <birch>`.

    threshold : float, default=0.5
        The radius of the subcluster obtained by merging a new sample and the
        closest subcluster should be lesser than the threshold. Otherwise a new
        subcluster is started. Setting this value to be very low promotes
        splitting and vice-versa.

    branching_factor : int, default=50
        Maximum number of CF subclusters in each node. If a new samples enters
        such that the number of subclusters exceed the branching_factor then
        that node is split into two nodes with the subclusters redistributed
        in each. The parent subcluster of that node is removed and two new
        subclusters are added as parents of the 2 split nodes.

    n_clusters : int, instance of sklearn.cluster model or None, default=3
        Number of clusters after the final clustering step, which treats the
        subclusters from the leaves as new samples.

        - `None` : the final clustering step is not performed and the
          subclusters are returned as they are.

        - :mod:`sklearn.cluster` Estimator : If a model is provided, the model
          is fit treating the subclusters as new samples and the initial data
          is mapped to the label of the closest subcluster.

        - `int` : the model fit is :class:`AgglomerativeClustering` with
          `n_clusters` set to be equal to the int.

        Whether or not to compute labels for each fit.

        Whether or not to make a copy of the given data. If set to False,
        the initial data will be overwritten.

            `copy` was deprecated in 1.6 and will be removed in 1.8. It has no effect
            as the estimator does not perform in-place operations on the input data.

    root_ : _CFNode
        Root of the CFTree.

    dummy_leaf_ : _CFNode
        Start pointer to all the leaves.

    subcluster_centers_ : ndarray
        Centroids of all subclusters read directly from the leaves.

    subcluster_labels_ : ndarray
        Labels assigned to the centroids of the subclusters after
        they are clustered globally.

        Array of labels assigned to the input data.
        if partial_fit is used instead of fit, they are assigned to the
        last batch of data.

    See Also
    --------
    MiniBatchKMeans : Alternative implementation that does incremental updates
        of the centers' positions using mini-batches.

    Notes
    -----
    The tree data structure consists of nodes with each node consisting of
    a number of subclusters. The maximum number of subclusters in a node
    is determined by the branching factor. Each subcluster maintains a
    linear sum, squared sum and the number of samples in that subcluster.
    In addition, each subcluster can also have a node as its child, if the
    subcluster is not a member of a leaf node.

    For a new point entering the root, it is merged with the subcluster closest
    to it and the linear sum, squared sum and the number of samples of that
    subcluster are updated. This is done recursively till the properties of
    the leaf node are updated.

    comparison with :class:`~sklearn.cluster.MiniBatchKMeans`.

    * Tian Zhang, Raghu Ramakrishnan, Maron Livny
      BIRCH: An efficient data clustering method for large databases.
      https://www.cs.sfu.ca/CourseCentral/459/han/papers/zhang96.pdf

    * Roberto Perdisci
      JBirch - Java implementation of BIRCH clustering algorithm
      https://code.google.com/archive/p/jbirch

    Examples
    --------
    >>> from sklearn.cluster import Birch
    >>> X = [[0, 1], [0.3, 1], [-0.3, 1], [0, -1], [0.3, -1], [-0.3, -1]]
    >>> brc = Birch(n_clusters=None)
    >>> brc.fit(X)
    Birch(n_clusters=None)
    >>> brc.predict(X)

    For a comparison of the BIRCH clustering algorithm with other clustering algorithms,
    """

        "threshold": [Interval(Real, 0.0, None, closed="neither")],
        "branching_factor": [Interval(Integral, 1, None, closed="neither")],
        "n_clusters": [None, ClusterMixin, Interval(Integral, 1, None, closed="left")],
        "copy": ["boolean", Hidden(StrOptions({"deprecated"}))],
    }

        self,
        *,
        threshold=0.5,
        branching_factor=50,
        copy="deprecated",
    ):

        """
        Build a CF Tree for the input data.

        y : Ignored

        Returns
        -------
        self
        """
        return self._fit(X, partial=False)

    def _fit(self, X, partial):
        has_root = getattr(self, "root_", None)
        first_call = not (partial and has_root)

        if self.copy != "deprecated" and first_call:
                "`copy` was deprecated in 1.6 and will be removed in 1.8 since it "
                "has no effect internally. Simply leave this parameter to its default "
                "value to avoid this warning.",
            )

            self,
            X,
        )

        # If partial_fit is called for the first time or fit is called, we
        # start a new tree.
            # The first root is the leaf. Manipulate this object throughout.
            self.root_ = _CFNode(
                threshold=threshold,
                branching_factor=branching_factor,
                is_leaf=True,
                n_features=n_features,
                dtype=X.dtype,
            )

            # To enable getting back subclusters.
            self.dummy_leaf_ = _CFNode(
            )
            self.dummy_leaf_.next_leaf_ = self.root_
            self.root_.prev_leaf_ = self.dummy_leaf_

        # Cannot vectorize. Enough to convince to use cython.
        if not sparse.issparse(X):
            iter_func = iter
        else:
            iter_func = _iterate_sparse_X

        for sample in iter_func(X):
            subcluster = _CFSubcluster(linear_sum=sample)
            split = self.root_.insert_cf_subcluster(subcluster)

            if split:
                    self.root_, threshold, branching_factor
                )
                del self.root_
                self.root_ = _CFNode(
                    threshold=threshold,
                    branching_factor=branching_factor,
                    is_leaf=False,
                    n_features=n_features,
                    dtype=X.dtype,
                )
                self.root_.append_subcluster(new_subcluster1)
                self.root_.append_subcluster(new_subcluster2)

        centroids = np.concatenate([leaf.centroids_ for leaf in self._get_leaves()])
        self.subcluster_centers_ = centroids
        self._n_features_out = self.subcluster_centers_.shape[0]

        self._global_clustering(X)

    def _get_leaves(self):
        """
        Retrieve the leaves of the CF Node.

        Returns
        -------
        leaves : list of shape (n_leaves,)
            List of the leaf nodes.
        """
        leaf_ptr = self.dummy_leaf_.next_leaf_
        leaves = []
        while leaf_ptr is not None:
            leaves.append(leaf_ptr)
            leaf_ptr = leaf_ptr.next_leaf_

    def partial_fit(self, X=None, y=None):
        """
        Online learning. Prevents rebuilding of CFTree from scratch.

        X : {array-like, sparse matrix} of shape (n_samples, n_features), \
            Input data. If X is not provided, only the global clustering
            step is done.

        y : Ignored

        Returns
        -------
        self
        """
            # Perform just the final global clustering step.
            self._global_clustering()
        else:
            return self._fit(X, partial=True)

        """
        Predict data using the ``centroids_`` of subclusters.

        Avoid computation of the row norms of X.

        Returns
        -------
        labels : ndarray of shape(n_samples,)
            Labelled data.
        """

        """Predict data using the ``centroids_`` of subclusters."""
        kwargs = {"Y_norm_squared": self._subcluster_norms}

            argmin = pairwise_distances_argmin(
                X, self.subcluster_centers_, metric_kwargs=kwargs
            )
        return self.subcluster_labels_[argmin]

        """
        Transform X into subcluster centroids dimension.

        Each dimension represents the distance from the sample point to each
        cluster centroid.

        Returns
        -------
        X_trans : {array-like, sparse matrix} of shape (n_samples, n_clusters)
        """
            return euclidean_distances(X, self.subcluster_centers_)

    def _global_clustering(self, X=None):
        """
        Global clustering for the subclusters obtained after fitting
        """
        clusterer = self.n_clusters
        centroids = self.subcluster_centers_
        compute_labels = (X is not None) and self.compute_labels

        # Preprocessing for the global clustering.
        not_enough_centroids = False
        if isinstance(clusterer, Integral):
            clusterer = AgglomerativeClustering(n_clusters=self.n_clusters)
            # There is no need to perform the global clustering step.
            if len(centroids) < self.n_clusters:
                not_enough_centroids = True

        # To use in predict to avoid recalculation.
        self._subcluster_norms = row_norms(self.subcluster_centers_, squared=True)

        if clusterer is None or not_enough_centroids:
            self.subcluster_labels_ = np.arange(len(centroids))
            if not_enough_centroids:
                    "Number of subclusters found (%d) by BIRCH is less "
                    "than (%d). Decrease the threshold."
                    % (len(centroids), self.n_clusters),
                )
        else:
            # The global clustering step that clusters the subclusters of
            # the leaves. It assumes the centroids of the subclusters as
            # samples and finds the final centroids.
            self.subcluster_labels_ = clusterer.fit_predict(self.subcluster_centers_)

        if compute_labels:
            self.labels_ = self._predict(X)

        """

            else:
                )
                )

        else:
            if merged:

            else:

        else:
            )

        """

            (

        self,
        *,
    ):

        """

        y : Ignored

        Returns
        -------
        self
        """

            )

            self,
            X,
        )

            )

            )

        else:

            if split:
                )
                )

        """

        Returns
        -------
        """
        leaves = []

        """

        y : Ignored

        Returns
        -------
        self
        """
        else:

        """

        Returns
        -------
        """

            )

        """

        Returns
        -------
        """

        """
        """

                )
        else:

class _BisectingTree:
    """Tree structure representing the hierarchical clusters of BisectingKMeans."""

    def __init__(self, center, indices, score):
        """Create a new cluster node in the tree.

        The node holds the center of this cluster and the indices of the data points
        that belong to it.
        """
        self.center = center
        self.indices = indices
        self.score = score

        self.left = None
        self.right = None

    def split(self, labels, centers, scores):
        """Split the cluster node into two subclusters."""
        self.left = _BisectingTree(
            indices=self.indices[labels == 0], center=centers[0], score=scores[0]
        )
        self.right = _BisectingTree(
            indices=self.indices[labels == 1], center=centers[1], score=scores[1]
        )

        # reset the indices attribute to save memory
        self.indices = None

    def get_cluster_to_bisect(self):
        """Return the cluster node to bisect next.

        It's based on the score of the cluster, which can be either the number of
        data points assigned to that cluster or the inertia of that cluster
        (see `bisecting_strategy` for details).
        """
        max_score = None

        for cluster_leaf in self.iter_leaves():
            if max_score is None or cluster_leaf.score > max_score:
                max_score = cluster_leaf.score
                best_cluster_leaf = cluster_leaf

        return best_cluster_leaf

    def iter_leaves(self):
        """Iterate over all the cluster leaves in the tree."""
        if self.left is None:
            yield self
        else:
            yield from self.left.iter_leaves()
            yield from self.right.iter_leaves()

class BisectingKMeans(_BaseKMeans):
    """Bisecting K-Means clustering.

    Read more in the :ref:`User Guide <bisect_k_means>`.

    init : {'k-means++', 'random'} or callable, default='random'

        'k-means++' : selects initial cluster centers for k-mean
        clustering in a smart way to speed up convergence. See section
        Notes in k_init for more details.

    n_init : int, default=1
        Number of time the inner k-means algorithm will be run with different
        centroid seeds in each bisection.
        That will result producing for each bisection best output of n_init
        consecutive runs in terms of inertia.

        Determines random number generation for centroid initialization
        in inner K-Means. Use an int to make the randomness deterministic.

        Maximum number of iterations of the inner k-means algorithm at each
        bisection.

        in the cluster centers of two consecutive iterations  to declare
        convergence. Used in inner k-means algorithm at each bisection to pick
        best possible clusters.

        Inner K-means algorithm used in bisection.
        The classical EM-style algorithm is `"lloyd"`.

    bisecting_strategy : {"biggest_inertia", "largest_cluster"},\
            default="biggest_inertia"
        Defines how bisection should be performed:

        - "biggest_inertia" means that BisectingKMeans will always check
          all calculated cluster for cluster with biggest SSE
          (Sum of squared errors) and bisect it. This approach concentrates on
          precision, but may be costly in terms of execution time (especially for
          larger amount of data points).

        - "largest_cluster" - BisectingKMeans will always split cluster with
          largest amount of points assigned to it from all clusters
          previously calculated. That should work faster than picking by SSE
          ('biggest_inertia') and may produce similar results in most cases.

    See Also
    --------
    KMeans : Original implementation of K-Means algorithm.

    Notes
    -----
    It might be inefficient when n_cluster is less than 3, due to unnecessary
    calculations for that case.

    Examples
    --------
    >>> from sklearn.cluster import BisectingKMeans
    >>> X = np.array([[1, 1], [10, 1], [3, 1],
    ...               [10, 0], [2, 1], [10, 2],
    ...               [10, 8], [10, 9], [10, 10]])
    >>> bisect_means = BisectingKMeans(n_clusters=3, random_state=0).fit(X)
    >>> bisect_means.labels_
    array([0, 2, 0, 2, 0, 2, 1, 1, 1], dtype=int32)
    >>> bisect_means.predict([[0, 0], [12, 3]])
    array([0, 2], dtype=int32)
    >>> bisect_means.cluster_centers_
    array([[ 2., 1.],
           [10., 9.],
           [10., 1.]])

    For a comparison between BisectingKMeans and K-Means refer to example
    """

        "init": [StrOptions({"k-means++", "random"}), callable],
        "bisecting_strategy": [StrOptions({"biggest_inertia", "largest_cluster"})],
    }

        self,
        *,
        n_init=1,
        tol=1e-4,
        bisecting_strategy="biggest_inertia",
    ):
            tol=tol,
        )

        self.bisecting_strategy = bisecting_strategy

            "BisectingKMeans is known to have a memory leak on Windows "
        )

    def _inertia_per_cluster(self, X, centers, labels, sample_weight):
        """Calculate the sum of squared errors (inertia) per cluster.

        X : {ndarray, csr_matrix} of shape (n_samples, n_features)

        centers : ndarray of shape (n_clusters=2, n_features)
            The cluster centers.

            The weights for each observation in X.

        Returns
        -------
        inertia_per_cluster : ndarray of shape (n_clusters=2,)
            Sum of squared errors (inertia) for each cluster.
        """
        n_clusters = centers.shape[0]  # = 2 since centers comes from a bisection
        _inertia = _inertia_sparse if sp.issparse(X) else _inertia_dense

        inertia_per_cluster = np.empty(n_clusters)
        for label in range(n_clusters):
            inertia_per_cluster[label] = _inertia(
                X, sample_weight, centers, labels, self._n_threads, single_label=label
            )

        return inertia_per_cluster

    def _bisect(self, X, x_squared_norms, sample_weight, cluster_to_bisect):
        """Split a cluster into 2 subsclusters.

            Training instances to cluster.

            Squared euclidean norm of each data point.

        cluster_to_bisect : _BisectingTree node object
            The cluster node to split.
        """
        X = X[cluster_to_bisect.indices]
        x_squared_norms = x_squared_norms[cluster_to_bisect.indices]
        sample_weight = sample_weight[cluster_to_bisect.indices]

        # Split samples in X into 2 clusters.
        # Repeating `n_init` times to obtain best clusters
        for _ in range(self.n_init):
                X,
                n_centroids=2,
            )

            labels, inertia, centers, _ = self._kmeans_single(
                X,
                tol=self.tol,
            )

            # allow small tolerance on the inertia to accommodate for
            # non-deterministic rounding errors due to parallel computation
            if best_inertia is None or inertia < best_inertia * (1 - 1e-6):

            print(f"New centroids from bisection: {best_centers}")

        if self.bisecting_strategy == "biggest_inertia":
            scores = self._inertia_per_cluster(
                X, best_centers, best_labels, sample_weight
            )
        else:  # bisecting_strategy == "largest_cluster"
            # Using minlength to make sure that we have the counts for both labels even
            # if all samples are labelled 0.
            scores = np.bincount(best_labels, minlength=2)

        cluster_to_bisect.split(best_labels, best_centers, scores)

        """Compute bisecting k-means clustering.

            .. note:: The data will be converted to C ordering,
                which will cause a memory copy
                if the given data is not C-contiguous.

        y : Ignored

            initialization if `init` is a callable.

        Returns
        -------
        self
        """
            self,
            X,
        )

        if self.algorithm == "lloyd" or self.n_clusters == 1:
            self._kmeans_single = _kmeans_single_lloyd
        else:
            self._kmeans_single = _kmeans_single_elkan

        # Subtract of mean of X for more accurate distance computations
            self._X_mean = X.mean(axis=0)
            X -= self._X_mean

        # Initialize the hierarchical clusters tree
        self._bisecting_tree = _BisectingTree(
            indices=np.arange(X.shape[0]),
            center=X.mean(axis=0),
            score=0,
        )

        for _ in range(self.n_clusters - 1):
            # Chose cluster to bisect
            cluster_to_bisect = self._bisecting_tree.get_cluster_to_bisect()

            # Split this cluster into 2 subclusters
            self._bisect(X, x_squared_norms, sample_weight, cluster_to_bisect)

        # Aggregate final labels and centers from the bisecting tree
        self.labels_ = np.full(X.shape[0], -1, dtype=np.int32)
        self.cluster_centers_ = np.empty((self.n_clusters, X.shape[1]), dtype=X.dtype)

        for i, cluster_node in enumerate(self._bisecting_tree.iter_leaves()):
            self.labels_[cluster_node.indices] = i
            self.cluster_centers_[i] = cluster_node.center
            cluster_node.label = i  # label final clusters for future prediction
            cluster_node.indices = None  # release memory

        # Restore original data
            X += self._X_mean
            self.cluster_centers_ += self._X_mean

        self.inertia_ = _inertia(
            X, sample_weight, self.cluster_centers_, self.labels_, self._n_threads
        )

        """Predict which cluster each sample in X belongs to.

        Prediction is made by going down the hierarchical tree
        in searching of closest leaf cluster.

        Returns
        -------
        """

        # sample weights are unused but necessary in cython helpers
        sample_weight = np.ones_like(x_squared_norms)

        labels = self._predict_recursive(X, sample_weight, self._bisecting_tree)

    def _predict_recursive(self, X, sample_weight, cluster_node):
        """Predict recursively by going down the hierarchical tree.

            The data points, currently assigned to `cluster_node`, to predict between
            the subclusters of this node.

        cluster_node : _BisectingTree node object
            The cluster node of the hierarchical tree.

        Returns
        -------
        """
        if cluster_node.left is None:
            # This cluster has no subcluster. Labels are just the label of the cluster.
            return np.full(X.shape[0], cluster_node.label, dtype=np.int32)

        # Determine if data points belong to the left or right subcluster
        centers = np.vstack((cluster_node.left.center, cluster_node.right.center))
        if hasattr(self, "_X_mean"):
            centers += self._X_mean

        cluster_labels = _labels_inertia_threadpool_limit(
            X,
            centers,
            self._n_threads,
        )
        mask = cluster_labels == 0

        # Compute the labels for each subset of the data points.
        labels = np.full(X.shape[0], -1, dtype=np.int32)

        labels[mask] = self._predict_recursive(
            X[mask], sample_weight[mask], cluster_node.left
        )

        labels[~mask] = self._predict_recursive(
            X[~mask], sample_weight[~mask], cluster_node.right
        )

        """

        )
        )

        """

            yield self
        else:

        self,
        *,
        n_init=1,
        tol=1e-4,
    ):
            tol=tol,
        )

        )

        Returns
        -------
        """

            )

        """

                X,
            )

                X,
            )

            )

        y : Ignored

        Returns
        -------
        self
        """
            self,
            X,
        )

        else:

            score=0,
        )

        )

        Returns
        -------
        """

        Returns
        -------
        """

            X,
            centers,
        )

        )

        )

def estimate_bandwidth(X, *, quantile=0.3, n_samples=None, random_state=0, n_jobs=None):
    """Estimate the bandwidth to use with the mean-shift algorithm.

    This function takes time at least quadratic in `n_samples`. For large
    datasets, it is wise to subsample by setting `n_samples`. Alternatively,
    the parameter `bandwidth` can be set to a small value without estimating
    it.

        Input points.

    quantile : float, default=0.3
        Should be between [0, 1]
        0.5 means that the median of all pairwise distances is used.

    n_samples : int, default=None
        The number of samples to use. If not given, all samples are used.

        The generator used to randomly select the samples from input points
        for bandwidth estimation. Use an int to make the randomness

    Returns
    -------
    bandwidth : float
        The bandwidth parameter.

    Examples
    --------
    >>> from sklearn.cluster import estimate_bandwidth
    >>> estimate_bandwidth(X, quantile=0.5)
    np.float64(1.61...)
    """

    if n_samples is not None:
        idx = random_state.permutation(X.shape[0])[:n_samples]
        X = X[idx]
    n_neighbors = int(X.shape[0] * quantile)
    if n_neighbors < 1:  # cannot fit NearestNeighbors with n_neighbors = 0
        n_neighbors = 1
    nbrs = NearestNeighbors(n_neighbors=n_neighbors, n_jobs=n_jobs)

    bandwidth = 0.0
    for batch in gen_batches(len(X), 500):
        d, _ = nbrs.kneighbors(X[batch, :], return_distance=True)
        bandwidth += np.max(d, axis=1).sum()

    return bandwidth / X.shape[0]

def _mean_shift_single_seed(my_mean, X, nbrs, max_iter):
    # For each seed, climb gradient until convergence or max_iter
    bandwidth = nbrs.get_params()["radius"]
    stop_thresh = 1e-3 * bandwidth  # when mean has converged
    completed_iterations = 0
        # Find mean of points within bandwidth
        i_nbrs = nbrs.radius_neighbors([my_mean], bandwidth, return_distance=False)[0]
        points_within = X[i_nbrs]
        if len(points_within) == 0:
            break  # Depending on seeding strategy this condition may occur
        my_old_mean = my_mean  # save the old mean
        my_mean = np.mean(points_within, axis=0)
        # If converged or at max_iter, adds the cluster
        if (
            np.linalg.norm(my_mean - my_old_mean) <= stop_thresh
            or completed_iterations == max_iter
        ):
            break
        completed_iterations += 1
    return tuple(my_mean), len(points_within), completed_iterations

def mean_shift(
    X,
    *,
    bandwidth=None,
    seeds=None,
    bin_seeding=False,
    min_bin_freq=1,
    cluster_all=True,
):
    """Perform mean shift clustering of data using a flat kernel.

    Read more in the :ref:`User Guide <mean_shift>`.

    bandwidth : float, default=None
        Kernel bandwidth. If not None, must be in the range [0, +inf).

        If None, the bandwidth is determined using a heuristic based on
        the median of all pairwise distances. This will take quadratic time in
        the number of samples. The sklearn.cluster.estimate_bandwidth function
        can be used to do this more efficiently.

    seeds : array-like of shape (n_seeds, n_features) or None
        Point used as initial kernel locations. If None and bin_seeding=False,
        each data point is used as a seed. If None and bin_seeding=True,
        see bin_seeding.

    bin_seeding : bool, default=False
        If true, initial kernel locations are not locations of all
        points, but rather the location of the discretized version of
        points, where points are binned onto a grid whose coarseness
        corresponds to the bandwidth. Setting this option to True will speed
        up the algorithm because fewer seeds will be initialized.
        Ignored if seeds argument is not None.

    min_bin_freq : int, default=1
       To speed up the algorithm, accept only those bins with at least
       min_bin_freq points as seeds.

    cluster_all : bool, default=True
        If true, then all points are clustered, even those orphans that are
        not within any kernel. Orphans are assigned to the nearest kernel.
        If false, then orphans are given cluster label -1.

        Maximum number of iterations, per seed point before the clustering
        operation terminates (for that seed point), if has not converged yet.

        The number of jobs to use for the computation. The following tasks benefit
        from the parallelization:

        - The search of nearest neighbors for bandwidth estimation and label
          assignments. See the details in the docstring of the
          ``NearestNeighbors`` class.
        - Hill-climbing optimization for all seeds.

           Parallel Execution using *n_jobs*.

    Returns
    -------

    cluster_centers : ndarray of shape (n_clusters, n_features)

    Notes
    -----
    :ref:`sphx_glr_auto_examples_cluster_plot_mean_shift.py`.

    Examples
    --------
    >>> from sklearn.cluster import mean_shift
    >>> cluster_centers, labels = mean_shift(X, bandwidth=2)
    >>> cluster_centers
    array([[3.33..., 6.     ],
           [1.33..., 0.66...]])
    >>> labels
    """
    model = MeanShift(
        bandwidth=bandwidth,
        seeds=seeds,
        min_bin_freq=min_bin_freq,
        bin_seeding=bin_seeding,
        cluster_all=cluster_all,
    ).fit(X)
    return model.cluster_centers_, model.labels_

def get_bin_seeds(X, bin_size, min_bin_freq=1):
    """Find seeds for mean_shift.

    Finds seeds by first binning data onto a grid whose lines are
    spaced bin_size apart, and then choosing those bins with at least
    min_bin_freq points.

        Input points, the same points that will be used in mean_shift.

    bin_size : float
        Controls the coarseness of the binning. Smaller values lead
        to more seeding (which is computationally more expensive). If you're
        not sure how to set this, set it to the value of the bandwidth used
        in clustering.mean_shift.

        Only bins with at least min_bin_freq will be selected as seeds.
        Raising this value decreases the number of seeds found, which
        makes mean_shift computationally cheaper.

    Returns
    -------
    bin_seeds : array-like of shape (n_samples, n_features)
        Points used as initial kernel positions in clustering.mean_shift.
    """
    if bin_size == 0:
        return X

    # Bin points
    bin_sizes = defaultdict(int)
    for point in X:
        binned_point = np.round(point / bin_size)
        bin_sizes[tuple(binned_point)] += 1

    # Select only those bins as seeds which have enough members
    bin_seeds = np.array(
        [point for point, freq in bin_sizes.items() if freq >= min_bin_freq],
        dtype=np.float32,
    )
    if len(bin_seeds) == len(X):
            "Binning data failed with provided bin_size=%f, using data points as seeds."
            % bin_size
        )
        return X
    bin_seeds = bin_seeds * bin_size
    return bin_seeds

class MeanShift(ClusterMixin, BaseEstimator):
    """Mean shift clustering using a flat kernel.

    Mean shift clustering aims to discover "blobs" in a smooth density of
    samples. It is a centroid-based algorithm, which works by updating
    candidates for centroids to be the mean of the points within a given
    region. These candidates are then filtered in a post-processing stage to
    eliminate near-duplicates to form the final set of centroids.

    Seeding is performed using a binning technique for scalability.

    For an example of how to use MeanShift clustering, refer to:

        Bandwidth used in the flat kernel.

        If not given, the bandwidth is estimated using
        sklearn.cluster.estimate_bandwidth; see the documentation for that
        function for hints on scalability (see also the Notes, below).

    seeds : array-like of shape (n_samples, n_features), default=None
        Seeds used to initialize kernels. If not set,
        the seeds are calculated by clustering.get_bin_seeds
        with bandwidth as the grid size and default values for
        other parameters.

        Maximum number of iterations performed on each seed.

    See Also
    --------

    Notes
    -----

    Scalability:

    Because this implementation uses a flat kernel and
    a Ball Tree to look up members of each kernel, the complexity will tend
    towards O(T*n*log(n)) in lower dimensions, with n the number of samples
    and T the number of points. In higher dimensions the complexity will
    tend towards O(T*n^2).

    Scalability can be boosted by using fewer seeds, for example by using
    a higher value of min_bin_freq in the get_bin_seeds function.

    Note that the estimate_bandwidth function is much less scalable than the
    mean shift algorithm and will be the bottleneck if it is used.

    Dorin Comaniciu and Peter Meer, "Mean Shift: A robust approach toward
    feature space analysis". IEEE Transactions on Pattern Analysis and
    Machine Intelligence. 2002. pp. 603-619.

    Examples
    --------
    >>> from sklearn.cluster import MeanShift
    >>> clustering = MeanShift(bandwidth=2).fit(X)
    >>> clustering.predict([[0, 0], [5, 5]])
    array([1, 0])
    MeanShift(bandwidth=2)

    For a comparison of Mean Shift clustering with other clustering algorithms, see
    """

        "bandwidth": [Interval(Real, 0, None, closed="neither"), None],
        "seeds": ["array-like", None],
        "bin_seeding": ["boolean"],
        "min_bin_freq": [Interval(Integral, 1, None, closed="left")],
        "cluster_all": ["boolean"],
    }

        self,
        *,
        bandwidth=None,
        seeds=None,
        bin_seeding=False,
        min_bin_freq=1,
        cluster_all=True,
    ):
        self.seeds = seeds
        self.bin_seeding = bin_seeding
        self.cluster_all = cluster_all
        self.min_bin_freq = min_bin_freq

        """Perform clustering.

            Samples to cluster.

        y : Ignored

        Returns
        -------
               Fitted instance.
        """
        bandwidth = self.bandwidth
        if bandwidth is None:
            bandwidth = estimate_bandwidth(X, n_jobs=self.n_jobs)

        seeds = self.seeds
        if seeds is None:
            if self.bin_seeding:
                seeds = get_bin_seeds(X, bandwidth, self.min_bin_freq)
            else:
                seeds = X
        center_intensity_dict = {}

        # We use n_jobs=1 because this will be used in nested calls under
        # parallel calls to _mean_shift_single_seed so there is no need for
        # for further parallelism.
        nbrs = NearestNeighbors(radius=bandwidth, n_jobs=1).fit(X)

        # execute iterations on all seeds in parallel
        all_res = Parallel(n_jobs=self.n_jobs)(
            delayed(_mean_shift_single_seed)(seed, X, nbrs, self.max_iter)
        )
        # copy results in a dictionary
        for i in range(len(seeds)):
            if all_res[i][1]:  # i.e. len(points_within) > 0
                center_intensity_dict[all_res[i][0]] = all_res[i][1]

        self.n_iter_ = max([x[2] for x in all_res])

        if not center_intensity_dict:
            # nothing near seeds
                "No point was within bandwidth=%f of any seed. Try a different seeding"
                " strategy                              or increase the bandwidth."
                % bandwidth
            )

        # POST PROCESSING: remove near duplicate points
        # If the distance between two kernels is less than the bandwidth,
        # then we have to remove one because it is a duplicate. Remove the
        # one with fewer points.

        sorted_by_intensity = sorted(
            center_intensity_dict.items(),
            key=lambda tup: (tup[1], tup[0]),
            reverse=True,
        )
        sorted_centers = np.array([tup[0] for tup in sorted_by_intensity])
        unique = np.ones(len(sorted_centers), dtype=bool)
        nbrs = NearestNeighbors(radius=bandwidth, n_jobs=self.n_jobs).fit(
            sorted_centers
        )
        for i, center in enumerate(sorted_centers):
            if unique[i]:
                neighbor_idxs = nbrs.radius_neighbors([center], return_distance=False)[
                    0
                ]
                unique[neighbor_idxs] = 0
                unique[i] = 1  # leave the current point as unique
        cluster_centers = sorted_centers[unique]

        # ASSIGN LABELS: a point belongs to the cluster that it is closest to
        nbrs = NearestNeighbors(n_neighbors=1, n_jobs=self.n_jobs).fit(cluster_centers)
        labels = np.zeros(n_samples, dtype=int)
        distances, idxs = nbrs.kneighbors(X)
        if self.cluster_all:
            labels = idxs.flatten()
        else:
            labels.fill(-1)
            bool_selector = distances.flatten() <= bandwidth
            labels[bool_selector] = idxs.flatten()[bool_selector]

        self.cluster_centers_, self.labels_ = cluster_centers, labels

        Returns
        -------
        """
            return pairwise_distances_argmin(X, self.cluster_centers_)

        self,
        *,
    ):

        y : Ignored

        Returns
        -------
        """

            else:
                seeds = X

        )

            )

        )
        )
                    0
                ]

        else:

        Returns
        -------
        """

def _fix_connectivity(X, connectivity, affinity):
    """
    Fixes the connectivity matrix.

    The different steps are:

    - copies it
    - makes it symmetric
    - converts it to LIL if necessary
    - completes it if necessary.

        Feature matrix representing `n_samples` samples to be clustered.

    connectivity : sparse matrix, default=None
        Connectivity matrix. Defines for each sample the neighboring samples
        following a given structure of the data. The matrix is assumed to
        be symmetric and only the upper triangular half is used.
        Default is `None`, i.e, the Ward algorithm is unstructured.

    affinity : {"euclidean", "precomputed"}, default="euclidean"
        Which affinity to use. At the moment `precomputed` and
        ``euclidean`` are supported. `euclidean` uses the
        negative squared Euclidean distance between points.

    Returns
    -------
    connectivity : sparse matrix
        The fixed connectivity matrix.

    n_connected_components : int
        The number of connected components in the graph.
    """
    if connectivity.shape[0] != n_samples or connectivity.shape[1] != n_samples:
            "Wrong shape for connectivity matrix: %s when X is %s"
            % (connectivity.shape, X.shape)
        )

    # Make the connectivity matrix symmetric:
    connectivity = connectivity + connectivity.T

    # Convert connectivity matrix to LIL
    if not sparse.issparse(connectivity):
        connectivity = sparse.lil_matrix(connectivity)

    # `connectivity` is a sparse matrix at this point
    if connectivity.format != "lil":
        connectivity = connectivity.tolil()

    # Compute the number of nodes
    n_connected_components, labels = connected_components(connectivity)

    if n_connected_components > 1:
            "the number of connected components of the "
            "connectivity matrix is %d > 1. Completing it to avoid "
            "stopping the tree early." % n_connected_components,
            stacklevel=2,
        )
        # XXX: Can we do without completing the matrix?
        connectivity = _fix_connected_components(
            X=X,
            graph=connectivity,
            n_connected_components=n_connected_components,
            component_labels=labels,
            metric=affinity,
            mode="connectivity",
        )

    return connectivity, n_connected_components

def _single_linkage_tree(
    connectivity,
    n_nodes,
    n_connected_components,
    return_distance,
):
    """
    Perform single linkage clustering on sparse data via the minimum
    spanning tree from scipy.sparse.csgraph, then using union-find to label.
    The parent array is then generated by walking through the tree.
    """
    from scipy.sparse.csgraph import minimum_spanning_tree

    # explicitly cast connectivity to ensure safety
    connectivity = connectivity.astype(np.float64, copy=False)

    # Ensure zero distances aren't ignored by setting them to "epsilon"
    epsilon_value = np.finfo(dtype=connectivity.data.dtype).eps
    connectivity.data[connectivity.data == 0] = epsilon_value

    # Use scipy.sparse.csgraph to generate a minimum spanning tree
    mst = minimum_spanning_tree(connectivity.tocsr())

    # Convert the graph to scipy.cluster.hierarchy array format
    mst = mst.tocoo()

    # Undo the epsilon values
    mst.data[mst.data == epsilon_value] = 0

    mst_array = np.vstack([mst.row, mst.col, mst.data]).T

    # Sort edges of the min_spanning_tree by weight
    mst_array = mst_array[np.argsort(mst_array.T[2], kind="mergesort"), :]

    # Convert edge list into standard hierarchical clustering format
    single_linkage_tree = _hierarchical._single_linkage_label(mst_array)
    children_ = single_linkage_tree[:, :2].astype(int)

    # Compute parents
    parent = np.arange(n_nodes, dtype=np.intp)
    for i, (left, right) in enumerate(children_, n_samples):
        if n_clusters is not None and i >= n_nodes:
            break
        if left < n_nodes:
            parent[left] = i
        if right < n_nodes:
            parent[right] = i

        distances = single_linkage_tree[:, 2]
        return children_, n_connected_components, n_samples, parent, distances
    return children_, n_connected_components, n_samples, parent

def ward_tree(X, *, connectivity=None, n_clusters=None, return_distance=False):
    """Ward clustering based on a Feature matrix.

    Recursively merges the pair of clusters that minimally increases
    within-cluster variance.

    The inertia matrix uses a Heapq-based representation.

    This is the structured version, that takes into account some topological
    structure between samples.

    Read more in the :ref:`User Guide <hierarchical_clustering>`.

    connectivity : {array-like, sparse matrix}, default=None
        Default is None, i.e, the Ward algorithm is unstructured.

        `n_clusters` should be less than `n_samples`.  Stop early the
        construction of the tree at `n_clusters.` This is useful to decrease
        computation time if the number of clusters is not small compared to the
        number of samples. In this case, the complete tree is not computed, thus
        the 'children' output is of limited use, and the 'parents' output should
        rather be used. This option is valid only when specifying a connectivity
        matrix.

    return_distance : bool, default=False
        If `True`, return the distance between the clusters.

    Returns
    -------
    children : ndarray of shape (n_nodes-1, 2)
        The children of each non-leaf node. Values less than `n_samples`
        correspond to leaves of the tree which are the original samples.
        A node `i` greater than or equal to `n_samples` is a non-leaf
        node and has children `children_[i - n_samples]`. Alternatively
        at the i-th iteration, children[i][0] and children[i][1]
        are merged to form node `n_samples + i`.

    n_leaves : int
        The number of leaves in the tree.

    parents : ndarray of shape (n_nodes,) or None
        The parent of each node. Only returned when a connectivity matrix
        is specified, elsewhere 'None' is returned.

    distances : ndarray of shape (n_nodes-1,)
        Only returned if `return_distance` is set to `True` (for compatibility).
        The distances between the centers of the nodes. `distances[i]`
        corresponds to a weighted Euclidean distance between
        the nodes `children[i, 1]` and `children[i, 2]`. If the nodes refer to
        leaves of the tree, then `distances[i]` is their unweighted Euclidean
        distance. Distances are updated in the following way
        (from scipy.hierarchy.linkage):

        The new entry :math:`d(u,v)` is computed as follows,

        .. math::

           d(u,v) = \\sqrt{\\frac{|v|+|s|}
                               {T}d(v,s)^2
                        + \\frac{|v|+|t|}
                               {T}d(v,t)^2
                        - \\frac{|v|}
                               {T}d(s,t)^2}

        where :math:`u` is the newly joined cluster consisting of
        clusters :math:`s` and :math:`t`, :math:`v` is an unused
        cluster in the forest, :math:`T=|v|+|s|+|t|`, and
        :math:`|*|` is the cardinality of its argument. This is also
        known as the incremental algorithm.

    Examples
    --------
    >>> from sklearn.cluster import ward_tree
    >>> children, n_connected_components, n_leaves, parents = ward_tree(X)
    >>> children
    array([[0, 1],
           [3, 5],
           [2, 6],
           [4, 7],
           [8, 9]])
    >>> n_connected_components
    1
    >>> n_leaves
    6
    """
        X = np.reshape(X, (-1, 1))

    if connectivity is None:
        from scipy.cluster import hierarchy  # imports PIL

        if n_clusters is not None:
                (
                    "Partial build of the tree is implemented "
                    "only for structured clustering (i.e. with "
                    "explicit connectivity). The algorithm "
                    "will build the full tree and only "
                    "retain the lower branches required "
                    "for the specified number of clusters"
                ),
            )
        X = np.require(X, requirements="W")
        out = hierarchy.ward(X)
        children_ = out[:, :2].astype(np.intp)

            distances = out[:, 2]
            return children_, 1, n_samples, None, distances
        else:
            return children_, 1, n_samples, None

    connectivity, n_connected_components = _fix_connectivity(
        X, connectivity, affinity="euclidean"
    )
    if n_clusters is None:
        n_nodes = 2 * n_samples - 1
    else:
        if n_clusters > n_samples:
                "Cannot provide more clusters than samples. "
                "%i n_clusters was asked, and there are %i "
                "samples." % (n_clusters, n_samples)
            )
        n_nodes = 2 * n_samples - n_clusters

    # create inertia matrix
    coord_row = []
    coord_col = []
    A = []
    for ind, row in enumerate(connectivity.rows):
        A.append(row)
        # We keep only the upper triangular for the moments
        # Generator expressions are faster than arrays on the following
        row = [i for i in row if i < ind]
        coord_row.extend(
            len(row)
            * [
                ind,
            ]
        )
        coord_col.extend(row)

    coord_row = np.array(coord_row, dtype=np.intp, order="C")
    coord_col = np.array(coord_col, dtype=np.intp, order="C")

    # build moments as a list
    moments_1 = np.zeros(n_nodes, order="C")
    moments_1[:n_samples] = 1
    moments_2 = np.zeros((n_nodes, n_features), order="C")
    moments_2[:n_samples] = X
    inertia = np.empty(len(coord_row), dtype=np.float64, order="C")
    _hierarchical.compute_ward_dist(moments_1, moments_2, coord_row, coord_col, inertia)
    inertia = list(zip(inertia, coord_row, coord_col))
    heapify(inertia)

    # prepare the main fields
    used_node = np.ones(n_nodes, dtype=bool)
    children = []
        distances = np.empty(n_nodes - n_samples)

    not_visited = np.empty(n_nodes, dtype=bool, order="C")

    # recursive merge loop
    for k in range(n_samples, n_nodes):
        # identify the merge
            inert, i, j = heappop(inertia)
            if used_node[i] and used_node[j]:
                break
        parent[i], parent[j] = k, k
        children.append((i, j))
        used_node[i] = used_node[j] = False
        if return_distance:  # store inertia value
            distances[k - n_samples] = inert

        # update the moments
        moments_1[k] = moments_1[i] + moments_1[j]
        moments_2[k] = moments_2[i] + moments_2[j]

        # update the structure matrix A and the inertia matrix
        coord_col = []
        not_visited.fill(1)
        not_visited[k] = 0
        _hierarchical._get_parents(A[i], coord_col, parent, not_visited)
        _hierarchical._get_parents(A[j], coord_col, parent, not_visited)
        # List comprehension is faster than a for loop
        [A[col].append(k) for col in coord_col]
        A.append(coord_col)
        coord_col = np.array(coord_col, dtype=np.intp, order="C")
        coord_row = np.empty(coord_col.shape, dtype=np.intp, order="C")
        coord_row.fill(k)
        n_additions = len(coord_row)
        ini = np.empty(n_additions, dtype=np.float64, order="C")

        _hierarchical.compute_ward_dist(moments_1, moments_2, coord_row, coord_col, ini)

        [heappush(inertia, (ini[idx], k, coord_col[idx])) for idx in range(n_additions)]

    # Separate leaves in children (empty lists up to now)
    n_leaves = n_samples
    # sort children to get consistent output with unstructured version
    children = [c[::-1] for c in children]
    children = np.array(children)  # return numpy array for efficient caching

        # 2 is scaling factor to compare w/ unstructured version
        distances = np.sqrt(2.0 * distances)
        return children, n_connected_components, n_leaves, parent, distances
    else:
        return children, n_connected_components, n_leaves, parent

def linkage_tree(
    X,
    connectivity=None,
    n_clusters=None,
    linkage="complete",
    affinity="euclidean",
    return_distance=False,
):
    """Linkage agglomerative clustering based on a Feature matrix.

        Stop early the construction of the tree at `n_clusters`. This is
        useful to decrease computation time if the number of clusters is
        not small compared to the number of samples. In this case, the
        complete tree is not computed, thus the 'children' output is of
        limited use, and the 'parents' output should rather be used.
        This option is valid only when specifying a connectivity matrix.

    linkage : {"average", "complete", "single"}, default="complete"
        Which linkage criteria to use. The linkage criterion determines which
        distance to use between sets of observation.
            - "average" uses the average of the distances of each observation of
              the two sets.
            - "complete" or maximum linkage uses the maximum distances between
              all observations of the two sets.
            - "single" uses the minimum of the distances between all
              observations of the two sets.

    affinity : str or callable, default='euclidean'
        Which metric to use. Can be 'euclidean', 'manhattan', or any
        distance known to paired distance (see metric.pairwise).

        Whether or not to return the distances between the clusters.

    Returns
    -------

    parents : ndarray of shape (n_nodes, ) or None

        Returned when `return_distance` is set to `True`.

        distances[i] refers to the distance between children[i][0] and
        children[i][1] when they are merged.

    See Also
    --------
    ward_tree : Hierarchical clustering with ward linkage.
    """

    linkage_choices = {
        "complete": _hierarchical.max_merge,
        "average": _hierarchical.average_merge,
        "single": None,
    }  # Single linkage is handled differently
    try:
        join_func = linkage_choices[linkage]
    except KeyError as e:
            "Unknown linkage option, linkage should be one of %s, but %s was given"
            % (linkage_choices.keys(), linkage)
        ) from e

    if affinity == "cosine" and np.any(~np.any(X, axis=1)):
        raise ValueError("Cosine affinity cannot be used when X contains zero vectors")

                (
                ),
            )

        if affinity == "precomputed":
            # for the linkage function of hierarchy to work on precomputed
            # data, provide as first argument an ndarray of the shape returned
            # by sklearn.metrics.pairwise_distances.
                    f"Distance matrix should be square, got matrix of shape {X.shape}"
                )
            i, j = np.triu_indices(X.shape[0], k=1)
            X = X[i, j]
        elif affinity == "l2":
            # Translate to something understood by scipy
            affinity = "euclidean"
        elif affinity in ("l1", "manhattan"):
            affinity = "cityblock"
        elif callable(affinity):
            X = affinity(X)
            X = X[i, j]
        if (
            linkage == "single"
            and affinity != "precomputed"
            and not callable(affinity)
            and affinity in METRIC_MAPPING64
        ):
            # We need the fast cythonized metric from neighbors
            dist_metric = DistanceMetric.get_metric(affinity)

            # The Cython routines used require contiguous arrays
            X = np.ascontiguousarray(X, dtype=np.double)

            mst = _hierarchical.mst_linkage_core(X, dist_metric)
            # Sort edges of the min_spanning_tree by weight
            mst = mst[np.argsort(mst.T[2], kind="mergesort"), :]

            # Convert edge list into standard hierarchical clustering format
            out = _hierarchical.single_linkage_label(mst)
        else:
            out = hierarchy.linkage(X, method=linkage, metric=affinity)
        children_ = out[:, :2].astype(int, copy=False)

        return children_, 1, n_samples, None

        X, connectivity, affinity=affinity
    )
    connectivity = connectivity.tocoo()
    # Put the diagonal to zero
    diag_mask = connectivity.row != connectivity.col
    connectivity.row = connectivity.row[diag_mask]
    connectivity.col = connectivity.col[diag_mask]
    connectivity.data = connectivity.data[diag_mask]
    del diag_mask

    if affinity == "precomputed":
        distances = X[connectivity.row, connectivity.col].astype(np.float64, copy=False)
    else:
        # FIXME We compute all the distances, while we could have only computed
        # the "interesting" distances
        distances = paired_distances(
            X[connectivity.row], X[connectivity.col], metric=affinity
        )
    connectivity.data = distances

    else:
        assert n_clusters <= n_samples

    if linkage == "single":
        return _single_linkage_tree(
            connectivity,
            n_nodes,
            n_clusters,
            n_connected_components,
            return_distance,
        )

    # create inertia heap and connection matrix
    A = np.empty(n_nodes, dtype=object)
    inertia = list()

    # LIL seems to the best format to access the rows quickly,
    # without the numpy overhead of slicing CSR indices and data.
    connectivity = connectivity.tolil()
    # We are storing the graph in a list of IntFloatDict
    for ind, (data, row) in enumerate(zip(connectivity.data, connectivity.rows)):
        A[ind] = IntFloatDict(
            np.asarray(row, dtype=np.intp), np.asarray(data, dtype=np.float64)
        )
        # We keep only the upper triangular for the heap
        inertia.extend(
            _hierarchical.WeightedEdge(d, ind, r) for r, d in zip(row, data) if r < ind
        )
    del connectivity

    used_node = np.ones(n_nodes, dtype=np.intp)

            edge = heappop(inertia)
            if used_node[edge.a] and used_node[edge.b]:
                break
        i = edge.a
        j = edge.b

            # store distances
            distances[k - n_samples] = edge.weight

        parent[i] = parent[j] = k
        # Keep track of the number of elements per cluster
        n_i = used_node[i]
        n_j = used_node[j]
        used_node[k] = n_i + n_j

        # a clever 'min', or 'max' operation between A[i] and A[j]
        coord_col = join_func(A[i], A[j], used_node, n_i, n_j)
        for col, d in coord_col:
            A[col].append(k, d)
            # Here we use the information from coord_col (containing the
            # distances) to update the heap
            heappush(inertia, _hierarchical.WeightedEdge(d, k, col))
        A[k] = coord_col
        # Clear A[i] and A[j] to save memory
        A[i] = A[j] = 0

    # # return numpy array for efficient caching
    children = np.array(children)[:, ::-1]

    return children, n_connected_components, n_leaves, parent

def _complete_linkage(*args, **kwargs):
    kwargs["linkage"] = "complete"
    return linkage_tree(*args, **kwargs)

def _average_linkage(*args, **kwargs):
    kwargs["linkage"] = "average"

def _single_linkage(*args, **kwargs):
    kwargs["linkage"] = "single"

def _hc_cut(n_clusters, children, n_leaves):
    """Function cutting the ward tree for a given number of clusters.

    n_clusters : int or ndarray
        The number of clusters to form.

        Number of leaves of the tree.

    Returns
    -------
    labels : array [n_samples]
    """
    if n_clusters > n_leaves:
            "Cannot extract more clusters than samples: "
            f"{n_clusters} clusters were given for a tree with {n_leaves} leaves."
        )
    # In this function, we store nodes as a heap to avoid recomputing
    # the max of the nodes: the first element is always the smallest
    # We use negated indices as heaps work on smallest elements, and we
    # are interested in largest elements
    # children[-1] is the root of the tree
    nodes = [-(max(children[-1]) + 1)]
    for _ in range(n_clusters - 1):
        # As we have a heap, nodes[0] is the smallest element
        these_children = children[-nodes[0] - n_leaves]
        # Insert the 2 children and remove the largest node
        heappush(nodes, -these_children[0])
        heappushpop(nodes, -these_children[1])
    label = np.zeros(n_leaves, dtype=np.intp)
    for i, node in enumerate(nodes):
        label[_hierarchical._hc_get_descendent(-node, children, n_leaves)] = i
    return label

class AgglomerativeClustering(ClusterMixin, BaseEstimator):
    """
    Agglomerative Clustering.

    Recursively merges pair of clusters of sample data; uses linkage distance.

    n_clusters : int or None, default=2
        The number of clusters to find. It must be ``None`` if
        ``distance_threshold`` is not ``None``.

        Metric used to compute the linkage. Can be "euclidean", "l1", "l2",
        "manhattan", "cosine", or "precomputed". If linkage is "ward", only
        "euclidean" is accepted. If "precomputed", a distance matrix is needed
        as input for the fit method. If connectivity is None, linkage is
        "single" and affinity is not "precomputed" any valid pairwise distance
        metric can be assigned.

        For an example of agglomerative clustering with different metrics, see
        :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_clustering_metrics.py`.

        Used to cache the output of the computation of the tree.
        By default, no caching is done. If a string is given, it is the
        path to the caching directory.

    connectivity : array-like, sparse matrix, or callable, default=None
        Connectivity matrix. Defines for each sample the neighboring
        samples following a given structure of the data.
        This can be a connectivity matrix itself or a callable that transforms
        the data into a connectivity matrix, such as derived from
        `kneighbors_graph`. Default is ``None``, i.e, the
        hierarchical clustering algorithm is unstructured.

        For an example of connectivity matrix using
        :class:`~sklearn.neighbors.kneighbors_graph`, see
        :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_clustering.py`.

    compute_full_tree : 'auto' or bool, default='auto'
        Stop early the construction of the tree at ``n_clusters``. This is
        useful to decrease computation time if the number of clusters is not
        small compared to the number of samples. This option is useful only
        when specifying a connectivity matrix. Note also that when varying the
        number of clusters and using caching, it may be advantageous to compute
        the full tree. It must be ``True`` if ``distance_threshold`` is not
        ``None``. By default `compute_full_tree` is "auto", which is equivalent
        to `True` when `distance_threshold` is not `None` or that `n_clusters`
        is inferior to the maximum between 100 or `0.02 * n_samples`.
        Otherwise, "auto" is equivalent to `False`.

    linkage : {'ward', 'complete', 'average', 'single'}, default='ward'
        Which linkage criterion to use. The linkage criterion determines which
        distance to use between sets of observation. The algorithm will merge
        the pairs of cluster that minimize this criterion.

        - 'ward' minimizes the variance of the clusters being merged.
        - 'average' uses the average of the distances of each observation of
          the two sets.
        - 'complete' or 'maximum' linkage uses the maximum distances between
          all observations of the two sets.
        - 'single' uses the minimum of the distances between all observations
          of the two sets.

            Added the 'single' option

        For examples comparing different `linkage` criteria, see
        :ref:`sphx_glr_auto_examples_cluster_plot_linkage_comparison.py`.

    distance_threshold : float, default=None
        The linkage distance threshold at or above which clusters will not be
        merged. If not ``None``, ``n_clusters`` must be ``None`` and
        ``compute_full_tree`` must be ``True``.

    compute_distances : bool, default=False
        Computes distances between clusters even if `distance_threshold` is not
        used. This can be used to make dendrogram visualization, but introduces
        a computational and memory overhead.

        For an example of dendrogram visualization, see
        :ref:`sphx_glr_auto_examples_cluster_plot_agglomerative_dendrogram.py`.

    n_clusters_ : int
        The number of clusters found by the algorithm. If
        ``distance_threshold=None``, it will be equal to the given
        ``n_clusters``.

    n_leaves_ : int
        Number of leaves in the hierarchical tree.

    n_connected_components_ : int
        The estimated number of connected components in the graph.

            ``n_connected_components_`` was added to replace ``n_components_``.

    children_ : array-like of shape (n_samples-1, 2)

    distances_ : array-like of shape (n_nodes-1,)
        Distances between nodes in the corresponding place in `children_`.
        Only computed if `distance_threshold` is used or `compute_distances`
        is set to `True`.

    See Also
    --------
    FeatureAgglomeration : Agglomerative clustering but for features instead of
        samples.

    Examples
    --------
    >>> from sklearn.cluster import AgglomerativeClustering
    >>> clustering = AgglomerativeClustering().fit(X)
    AgglomerativeClustering()

    For a comparison of Agglomerative clustering with other clustering algorithms, see
    """

        "n_clusters": [Interval(Integral, 1, None, closed="left"), None],
            callable,
        ],
        "memory": [str, HasMethods("cache"), None],
        "connectivity": ["array-like", "sparse matrix", callable, None],
        "compute_full_tree": [StrOptions({"auto"}), "boolean"],
        "linkage": [StrOptions(set(_TREE_BUILDERS.keys()))],
        "distance_threshold": [Interval(Real, 0, None, closed="left"), None],
        "compute_distances": ["boolean"],
    }

        self,
        n_clusters=2,
        *,
        memory=None,
        connectivity=None,
        compute_full_tree="auto",
        linkage="ward",
        distance_threshold=None,
        compute_distances=False,
    ):
        self.distance_threshold = distance_threshold
        self.connectivity = connectivity
        self.compute_full_tree = compute_full_tree
        self.linkage = linkage
        self.compute_distances = compute_distances

        """Fit the hierarchical clustering from features, or distance matrix.

        X : array-like, shape (n_samples, n_features) or \
            ``metric='precomputed'``.

        y : Ignored

        Returns
        -------
            Returns the fitted instance.
        """
        X = validate_data(self, X, ensure_min_samples=2)

        """Fit without validation

        X : ndarray of shape (n_samples, n_features) or (n_samples, n_samples)

        Returns
        -------
        """

        if not ((self.n_clusters is None) ^ (self.distance_threshold is None)):
                "Exactly one of n_clusters and "
                "distance_threshold has to be set, and the other "
                "needs to be None."
            )

        if self.distance_threshold is not None and not self.compute_full_tree:
                "compute_full_tree must be True if distance_threshold is set."
            )

        if self.linkage == "ward" and self.metric != "euclidean":
                f"{self.metric} was provided as metric. Ward can only "
                "work with euclidean distances."
            )

        tree_builder = _TREE_BUILDERS[self.linkage]

        connectivity = self.connectivity
        if self.connectivity is not None:
            if callable(self.connectivity):
                connectivity = self.connectivity(X)
            connectivity = check_array(
                connectivity, accept_sparse=["csr", "coo", "lil"]
            )

        n_samples = len(X)
        compute_full_tree = self.compute_full_tree
        if self.connectivity is None:
            compute_full_tree = True
        if compute_full_tree == "auto":
            if self.distance_threshold is not None:
                compute_full_tree = True
            else:
                # Early stopping is likely to give a speed up only for
                # a large number of clusters. The actual threshold
                # implemented here is heuristic
                compute_full_tree = self.n_clusters < max(100, 0.02 * n_samples)
        n_clusters = self.n_clusters
        if compute_full_tree:
            n_clusters = None

        # Construct the tree
        kwargs = {}
        if self.linkage != "ward":
            kwargs["linkage"] = self.linkage
            kwargs["affinity"] = self.metric

        distance_threshold = self.distance_threshold

        return_distance = (distance_threshold is not None) or self.compute_distances

        out = memory.cache(tree_builder)(
            X,
            connectivity=connectivity,
            return_distance=return_distance,
            **kwargs,
        )
        (self.children_, self.n_connected_components_, self.n_leaves_, parents) = out[
            :4
        ]

            self.distances_ = out[-1]

        if self.distance_threshold is not None:  # distance_threshold is used
            self.n_clusters_ = (
                np.count_nonzero(self.distances_ >= distance_threshold) + 1
            )
        else:  # n_clusters is used
            self.n_clusters_ = self.n_clusters

        # Cut the tree
            self.labels_ = _hc_cut(self.n_clusters_, self.children_, self.n_leaves_)
        else:
            labels = _hierarchical.hc_get_heads(parents, copy=False)
            # copy to avoid holding a reference on the original array
            labels = np.copy(labels[:n_samples])
            # Reassign cluster numbers
            self.labels_ = np.searchsorted(np.unique(labels), labels)

        """Fit and return the result of each sample's clustering assignment.

        In addition to fitting, this method also return the result of the
        clustering assignment for each sample in the training set.

            ``affinity='precomputed'``.

        y : Ignored

        Returns
        -------
        """

class FeatureAgglomeration(
    ClassNamePrefixFeaturesOutMixin, AgglomerationTransform, AgglomerativeClustering
):
    """Agglomerate features.

    Recursively merges pair of clusters of features.

    Refer to
    :ref:`sphx_glr_auto_examples_cluster_plot_feature_agglomeration_vs_univariate_selection.py`
    for an example comparison of :class:`FeatureAgglomeration` strategy with a
    univariate feature selection strategy (based on ANOVA).

        as input for the fit method.

        Connectivity matrix. Defines for each feature the neighboring
        features following a given structure of the data.
        `kneighbors_graph`. Default is `None`, i.e, the

        Stop early the construction of the tree at `n_clusters`. This is useful
        to decrease computation time if the number of clusters is not small
        compared to the number of features. This option is useful only when
        specifying a connectivity matrix. Note also that when varying the

    linkage : {"ward", "complete", "average", "single"}, default="ward"
        distance to use between sets of features. The algorithm will merge

        - "ward" minimizes the variance of the clusters being merged.
        - "complete" or maximum linkage uses the maximum distances between
          all features of the two sets.
        - "average" uses the average of the distances of each feature of
        - "single" uses the minimum of the distances between all features

    pooling_func : callable, default=np.mean
        This combines the values of agglomerated features into a single
        value, and should accept an array of shape [M, N] and the keyword
        argument `axis=1`, and reduce it to an array of size [M].

    labels_ : array-like of (n_features,)
        Cluster labels for each feature.

    children_ : array-like of shape (n_nodes-1, 2)
        The children of each non-leaf node. Values less than `n_features`
        A node `i` greater than or equal to `n_features` is a non-leaf
        node and has children `children_[i - n_features]`. Alternatively
        are merged to form node `n_features + i`.

    See Also
    --------
    AgglomerativeClustering : Agglomerative clustering samples instead of
        features.

    Examples
    --------
    >>> from sklearn import datasets, cluster
    >>> digits = datasets.load_digits()
    >>> images = digits.images
    >>> X = np.reshape(images, (len(images), -1))
    >>> agglo = cluster.FeatureAgglomeration(n_clusters=32)
    >>> agglo.fit(X)
    FeatureAgglomeration(n_clusters=32)
    >>> X_reduced = agglo.transform(X)
    >>> X_reduced.shape
    (1797, 32)
    """

            callable,
        ],
        "pooling_func": [callable],
    }

        self,
        *,
        pooling_func=np.mean,
    ):
            memory=memory,
            compute_full_tree=compute_full_tree,
            linkage=linkage,
            distance_threshold=distance_threshold,
            compute_distances=compute_distances,
        )
        self.pooling_func = pooling_func

        """Fit the hierarchical clustering on the data.

            The data.

        y : Ignored

        Returns
        -------
        """
        super()._fit(X.T)
        self._n_features_out = self.n_clusters_

    @property
    def fit_predict(self):
        """Fit and return the result of each sample's clustering assignment."""
        raise AttributeError

        self,
        *,
    ):

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

            )

            )

            )

            )

            else:

        kwargs = {}

            X,
            **kwargs,
        )
            :4
        ]

            )

        else:

        y : Ignored

        Returns
        -------
        """

        self,
        *,
    ):
        )

            The data.

        y : Ignored

        Returns
        -------
        """

class OPTICS(ClusterMixin, BaseEstimator):
    """Estimate clustering structure from vector array.

    OPTICS (Ordering Points To Identify the Clustering Structure), closely
    related to DBSCAN, finds core sample of high density and expands clusters
    from them [1]_. Unlike DBSCAN, keeps cluster hierarchy for a variable
    neighborhood radius. Better suited for usage on large datasets than the
    current sklearn implementation of DBSCAN.

    Clusters are then extracted using a DBSCAN-like method
    (cluster_method = 'dbscan') or an automatic
    technique proposed in [1]_ (cluster_method = 'xi').

    This implementation deviates from the original OPTICS by first performing
    k-nearest-neighborhood searches on all points to identify core sizes, then
    computing only the distances to unprocessed points when constructing the
    cluster order. Note that we do not employ a heap to manage the expansion
    candidates, so the time complexity will be O(n^2).

    Read more in the :ref:`User Guide <optics>`.

    min_samples : int > 1 or float between 0 and 1, default=5
        The number of samples in a neighborhood for a point to be considered as
        a core point. Also, up and down steep regions can't have more than
        ``min_samples`` consecutive non-steep points. Expressed as an absolute
        number or a fraction of the number of samples (rounded to be at least
        2).

    max_eps : float, default=np.inf
        The maximum distance between two samples for one to be considered as
        in the neighborhood of the other. Default value of ``np.inf`` will
        identify clusters across all scales; reducing ``max_eps`` will result
        in shorter run times.

        efficient than passing the metric name as a string. If metric is
        "precomputed", `X` is assumed to be a distance matrix and must be
        square.

          'manhattan']

          'yule']

        Sparse matrices are only supported by scikit-learn metrics.
        metrics.

        .. note::

        :class:`~sklearn.metrics.pairwise_distances`. When p = 1, this is

    cluster_method : str, default='xi'
        The extraction method used to extract clusters using the calculated
        reachability and ordering. Possible values are "xi" and "dbscan".

        in the neighborhood of the other. By default it assumes the same value
        as ``max_eps``.
        Used only when ``cluster_method='dbscan'``.

    xi : float between 0 and 1, default=0.05
        Determines the minimum steepness on the reachability plot that
        constitutes a cluster boundary. For example, an upwards point in the
        reachability plot is defined by the ratio from one point to its
        successor being at most 1-xi.
        Used only when ``cluster_method='xi'``.

    predecessor_correction : bool, default=True
        Correct clusters according to the predecessors calculated by OPTICS
        [2]_. This parameter has minimal effect on most datasets.

    min_cluster_size : int > 1 or float between 0 and 1, default=None
        Minimum number of samples in an OPTICS cluster, expressed as an
        absolute number or a fraction of the number of samples (rounded to be
        at least 2). If ``None``, the value of ``min_samples`` is used instead.

        - 'ball_tree' will use :class:`~sklearn.neighbors.BallTree`.
        - 'kd_tree' will use :class:`~sklearn.neighbors.KDTree`.
        - 'auto' (default) will attempt to decide the most appropriate
          algorithm based on the values passed to :meth:`fit` method.

        Leaf size passed to :class:`~sklearn.neighbors.BallTree` or
        :class:`~sklearn.neighbors.KDTree`. This can affect the speed of the
        construction and query, as well as the memory required to store the
        tree. The optimal value depends on the nature of the problem.

        Noisy samples and points which are not included in a leaf cluster
        of ``cluster_hierarchy_`` are labeled as -1.

    reachability_ : ndarray of shape (n_samples,)
        Reachability distances per sample, indexed by object order. Use
        ``clust.reachability_[clust.ordering_]`` to access in cluster order.

    ordering_ : ndarray of shape (n_samples,)
        The cluster ordered list of sample indices.

    core_distances_ : ndarray of shape (n_samples,)
        Distance at which each sample becomes a core point, indexed by object
        order. Points which will never be core have a distance of inf. Use
        ``clust.core_distances_[clust.ordering_]`` to access in cluster order.

    predecessor_ : ndarray of shape (n_samples,)
        Point that a sample was reached from, indexed by object order.
        Seed points have a predecessor of -1.

    cluster_hierarchy_ : ndarray of shape (n_clusters, 2)
        The list of clusters in the form of ``[start, end]`` in each row, with
        all indices inclusive. The clusters are ordered according to
        ``(end, -start)`` (ascending) so that larger clusters encompassing
        smaller clusters come after those smaller ones. Since ``labels_`` does
        not reflect the hierarchy, usually
        ``len(cluster_hierarchy_) > np.unique(optics.labels_)``. Please also
        note that these indices are of the ``ordering_``, i.e.
        ``X[ordering_][start:end + 1]`` form a cluster.
        Only available when ``cluster_method='xi'``.

    See Also
    --------
    DBSCAN : A similar clustering for a specified neighborhood radius (eps).
        Our implementation is optimized for runtime.

    .. [1] Ankerst, Mihael, Markus M. Breunig, Hans-Peter Kriegel,
       and Jörg Sander. "OPTICS: ordering points to identify the clustering
       structure." ACM SIGMOD Record 28, no. 2 (1999): 49-60.

    .. [2] Schubert, Erich, Michael Gertz.
       "Improving the Cluster Structure Extracted from OPTICS Plots." Proc. of
       the Conference "Lernen, Wissen, Daten, Analysen" (LWDA) (2018): 318-329.

    Examples
    --------
    >>> from sklearn.cluster import OPTICS
    >>> X = np.array([[1, 2], [2, 5], [3, 6],
    ...               [8, 7], [8, 8], [7, 3]])
    >>> clustering = OPTICS(min_samples=2).fit(X)

    :ref:`sphx_glr_auto_examples_cluster_plot_optics.py`.

    For a comparison of OPTICS with other clustering algorithms, see
    """

        "min_samples": [
        ],
        "max_eps": [Interval(Real, 0, None, closed="both")],
        "cluster_method": [StrOptions({"dbscan", "xi"})],
        "eps": [Interval(Real, 0, None, closed="both"), None],
        "xi": [Interval(Real, 0, 1, closed="both")],
        "predecessor_correction": ["boolean"],
        "min_cluster_size": [
            None,
        ],
        "algorithm": [StrOptions({"auto", "brute", "ball_tree", "kd_tree"})],
    }

        self,
        *,
        max_eps=np.inf,
        p=2,
        cluster_method="xi",
        eps=None,
        xi=0.05,
        predecessor_correction=True,
        min_cluster_size=None,
    ):
        self.max_eps = max_eps
        self.min_cluster_size = min_cluster_size
        self.p = p
        self.cluster_method = cluster_method
        self.xi = xi
        self.predecessor_correction = predecessor_correction

        # Optics.metric is not validated yet
    )
        """Perform OPTICS clustering.

        Extracts an ordered list of points and reachability distances, and
        performs initial clustering using ``max_eps`` distance specified at
        OPTICS object instantiation.

        X : {ndarray, sparse matrix} of shape (n_samples, n_features), or \
            A feature array, or array of distances between samples if
            metric='precomputed'. If a sparse matrix is provided, it will be
            converted into CSR format.

        y : Ignored

        Returns
        -------
        """
        dtype = bool if self.metric in PAIRWISE_BOOLEAN_FUNCTIONS else float
        if dtype is bool and X.dtype != bool:
            msg = (
                "Data will be converted to boolean for"
                f" metric {self.metric}, to avoid this warning,"
                " you may convert the data prior to calling fit."
            )

        X = validate_data(self, X, dtype=dtype, accept_sparse="csr")
        if self.metric == "precomputed" and issparse(X):
                warnings.simplefilter("ignore", SparseEfficiencyWarning)
                # Set each diagonal to an explicit value so each point is its
                # own neighbor

        (
            self.ordering_,
            self.core_distances_,
            self.reachability_,
            self.predecessor_,
        ) = memory.cache(compute_optics_graph)(
            X=X,
            min_samples=self.min_samples,
            p=self.p,
            max_eps=self.max_eps,
        )

        # Extract clusters from the calculated orders and reachability
        if self.cluster_method == "xi":
            labels_, clusters_ = cluster_optics_xi(
                reachability=self.reachability_,
                predecessor=self.predecessor_,
                ordering=self.ordering_,
                min_samples=self.min_samples,
                min_cluster_size=self.min_cluster_size,
                xi=self.xi,
                predecessor_correction=self.predecessor_correction,
            )
            self.cluster_hierarchy_ = clusters_
        elif self.cluster_method == "dbscan":
            if self.eps is None:
                eps = self.max_eps
            else:
                eps = self.eps

            if eps > self.max_eps:
                    "Specify an epsilon smaller than %s. Got %s." % (self.max_eps, eps)
                )

            labels_ = cluster_optics_dbscan(
                core_distances=self.core_distances_,
                eps=eps,
            )

        self.labels_ = labels_

def _validate_size(size, n_samples, param_name):
    if size > n_samples:
            "%s must be no greater than the number of samples (%d). Got %d"
            % (param_name, n_samples, size)
        )

def _compute_core_distances_(X, neighbors, min_samples, working_memory):
    """Compute the k-th nearest neighbor of each sample.

    Equivalent to neighbors.kneighbors(X, self.min_samples)[0][:, -1]
    but with more memory efficiency.

        The data.
    neighbors : NearestNeighbors instance
        The fitted nearest neighbors estimator.

    Returns
    -------
    core_distances : ndarray of shape (n_samples,)
        Distance at which each sample becomes a core point.
        Points which will never be core have a distance of inf.
    """
    core_distances = np.empty(n_samples)
    core_distances.fill(np.nan)

    chunk_n_rows = get_chunk_n_rows(
        row_bytes=16 * min_samples, max_n_rows=n_samples, working_memory=working_memory
    )
    slices = gen_batches(n_samples, chunk_n_rows)
        core_distances[sl] = neighbors.kneighbors(X[sl], min_samples)[0][:, -1]
    return core_distances

def compute_optics_graph(
    X, *, min_samples, max_eps, metric, p, metric_params, algorithm, leaf_size, n_jobs
):
    """Compute the OPTICS reachability graph.

    X : {ndarray, sparse matrix} of shape (n_samples, n_features), or \
            (n_samples, n_samples) if metric='precomputed'
        metric='precomputed'.

    min_samples : int > 1 or float between 0 and 1
        The number of samples in a neighborhood for a point to be considered
        as a core point. Expressed as an absolute number or a fraction of the
        number of samples (rounded to be at least 2).

        "precomputed", X is assumed to be a distance matrix and must be square.

          'yule']

        metrics.

        .. note::

          based on the values passed to `fit` method. (default)

    Returns
    -------
    ordering_ : array of shape (n_samples,)

    core_distances_ : array of shape (n_samples,)

    reachability_ : array of shape (n_samples,)

    predecessor_ : array of shape (n_samples,)

    Examples
    --------
    >>> from sklearn.cluster import compute_optics_graph
    >>> ordering, core_distances, reachability, predecessor = compute_optics_graph(
    ...     X,
    ...     min_samples=2,
    ...     max_eps=np.inf,
    ...     metric="minkowski",
    ...     p=2,
    ...     metric_params=None,
    ...     algorithm="auto",
    ...     leaf_size=30,
    ...     n_jobs=None,
    ... )
    >>> ordering
    array([0, 1, 2, 5, 3, 4])
    >>> core_distances
    array([3.16..., 1.41..., 1.41..., 1.        , 1.        ,
           4.12...])
    >>> reachability
    array([       inf, 3.16..., 1.41..., 4.12..., 1.        ,
           5.        ])
    >>> predecessor
    array([-1,  0,  1,  5,  3,  2])
    """
    _validate_size(min_samples, n_samples, "min_samples")
    if min_samples <= 1:
        min_samples = max(2, int(min_samples * n_samples))

    # Start all points as 'unprocessed' ##
    reachability_ = np.empty(n_samples)
    reachability_.fill(np.inf)
    predecessor_ = np.empty(n_samples, dtype=int)
    predecessor_.fill(-1)

    nbrs = NearestNeighbors(
        n_neighbors=min_samples,
        p=p,
    )

    # Here we first do a kNN query for each point, this differs from
    # the original OPTICS that only used epsilon range queries.
    # TODO: handle working_memory somehow?
    core_distances_ = _compute_core_distances_(
        X=X, neighbors=nbrs, min_samples=min_samples, working_memory=None
    )
    # OPTICS puts an upper limit on these, use inf for undefined.
    core_distances_[core_distances_ > max_eps] = np.inf
    np.around(
        core_distances_,
        decimals=np.finfo(core_distances_.dtype).precision,
        out=core_distances_,
    )

    # Main OPTICS loop. Not parallelizable. The order that entries are
    # written to the 'ordering_' list is important!
    # Note that this implementation is O(n^2) theoretically, but
    # supposedly with very low constant factors.
    processed = np.zeros(X.shape[0], dtype=bool)
    ordering = np.zeros(X.shape[0], dtype=int)
    for ordering_idx in range(X.shape[0]):
        # Choose next based on smallest reachability distance
        # (And prefer smaller ids on ties, possibly np.inf!)
        index = np.where(processed == 0)[0]
        point = index[np.argmin(reachability_[index])]

        processed[point] = True
        ordering[ordering_idx] = point
        if core_distances_[point] != np.inf:
            _set_reach_dist(
                core_distances_=core_distances_,
                reachability_=reachability_,
                predecessor_=predecessor_,
                point_index=point,
                processed=processed,
                X=X,
                nbrs=nbrs,
                p=p,
                max_eps=max_eps,
            )
    if np.all(np.isinf(reachability_)):
            (
                "All reachability values are inf. Set a larger"
                " max_eps or all data will be considered outliers."
            ),
        )
    return ordering, core_distances_, reachability_, predecessor_

def _set_reach_dist(
    core_distances_,
    reachability_,
    predecessor_,
    point_index,
    processed,
    X,
    nbrs,
    metric,
    metric_params,
    p,
    max_eps,
):
    P = X[point_index : point_index + 1]
    # Assume that radius_neighbors is faster without distances
    # and we don't need all distances, nevertheless, this means
    # we may be doing some work twice.
    indices = nbrs.radius_neighbors(P, radius=max_eps, return_distance=False)[0]

    # Getting indices of neighbors that have not been processed
    unproc = np.compress(~np.take(processed, indices), indices)
    # Neighbors of current point are already processed.
    if not unproc.size:
        return

    # Only compute distances to unprocessed neighbors:
        dists = X[[point_index], unproc]
        if isinstance(dists, np.matrix):
            dists = np.asarray(dists)
        dists = dists.ravel()
    else:
        _params = dict() if metric_params is None else metric_params.copy()
        if metric == "minkowski" and "p" not in _params:
            # the same logic as neighbors, p is ignored if explicitly set
            # in the dict params
            _params["p"] = p
        dists = pairwise_distances(P, X[unproc], metric, n_jobs=None, **_params).ravel()

    rdists = np.maximum(dists, core_distances_[point_index])
    np.around(rdists, decimals=np.finfo(rdists.dtype).precision, out=rdists)
    improved = np.where(rdists < np.take(reachability_, unproc))
    reachability_[unproc[improved]] = rdists[improved]
    predecessor_[unproc[improved]] = point_index

def cluster_optics_dbscan(*, reachability, core_distances, ordering, eps):
    """Perform DBSCAN extraction for an arbitrary epsilon.

    Extracting the clusters runs in linear time. Note that this results in
    ``labels_`` which are close to a :class:`~sklearn.cluster.DBSCAN` with
    similar settings and ``eps``, only if ``eps`` is close to ``max_eps``.

    reachability : ndarray of shape (n_samples,)
        Reachability distances calculated by OPTICS (``reachability_``).

        Distances at which points become core (``core_distances_``).

    ordering : ndarray of shape (n_samples,)
        OPTICS ordered point indices (``ordering_``).

    eps : float
        DBSCAN ``eps`` parameter. Must be set to < ``max_eps``. Results
        will be close to DBSCAN algorithm if ``eps`` and ``max_eps`` are close
        to one another.

    Returns
    -------
    labels_ : array of shape (n_samples,)
        The estimated labels.

    Examples
    --------
    >>> from sklearn.cluster import cluster_optics_dbscan, compute_optics_graph
    ...     X,
    ...     p=2,
    ... )
    >>> eps = 4.5
    >>> labels = cluster_optics_dbscan(
    ...     reachability=reachability,
    ...     core_distances=core_distances,
    ...     ordering=ordering,
    ...     eps=eps,
    ... )
    >>> labels
    """
    n_samples = len(core_distances)
    labels = np.zeros(n_samples, dtype=int)

    far_reach = reachability > eps
    near_core = core_distances <= eps
    labels[ordering] = np.cumsum(far_reach[ordering] & near_core[ordering]) - 1
    labels[far_reach & ~near_core] = -1

def cluster_optics_xi(
    *,
    reachability,
    predecessor,
    ordering,
    min_samples,
    min_cluster_size=None,
    xi=0.05,
    predecessor_correction=True,
):
    """Automatically extract clusters according to the Xi-steep method.

        Reachability distances calculated by OPTICS (`reachability_`).

    predecessor : ndarray of shape (n_samples,)
        Predecessors calculated by OPTICS.

        OPTICS ordered point indices (`ordering_`).

        The same as the min_samples given to OPTICS. Up and down steep regions
        can't have more then ``min_samples`` consecutive non-steep points.
        Expressed as an absolute number or a fraction of the number of samples
        (rounded to be at least 2).

        Correct clusters based on the calculated predecessors.

    Returns
    -------
        The labels assigned to samples. Points which are not included
        in any cluster are labeled as -1.

    clusters : ndarray of shape (n_clusters, 2)
        all indices inclusive. The clusters are ordered according to ``(end,
        -start)`` (ascending) so that larger clusters encompassing smaller
        clusters come after such nested smaller clusters. Since ``labels`` does
        not reflect the hierarchy, usually ``len(clusters) >
        np.unique(labels)``.

    Examples
    --------
    >>> from sklearn.cluster import cluster_optics_xi, compute_optics_graph
    ...     X,
    ...     p=2,
    ...     n_jobs=None
    ... )
    >>> min_samples = 2
    >>> labels, clusters = cluster_optics_xi(
    ...     predecessor=predecessor,
    ...     min_samples=min_samples,
    ... )
    >>> labels
    >>> clusters
    array([[0, 2],
           [3, 5],
           [0, 5]])
    """
    n_samples = len(reachability)
    if min_cluster_size is None:
        min_cluster_size = min_samples
    _validate_size(min_cluster_size, n_samples, "min_cluster_size")
    if min_cluster_size <= 1:
        min_cluster_size = max(2, int(min_cluster_size * n_samples))

    clusters = _xi_cluster(
        reachability[ordering],
        predecessor[ordering],
        ordering,
        xi,
        min_samples,
        min_cluster_size,
        predecessor_correction,
    )
    labels = _extract_xi_labels(ordering, clusters)
    return labels, clusters

def _extend_region(steep_point, xward_point, start, min_samples):
    """Extend the area until it's maximal.

    It's the same function for both upward and downward reagions, depending on
    the given input parameters. Assuming:

        - steep_{upward/downward}: bool array indicating whether a point is a
          steep {upward/downward};
        - upward/downward: bool array indicating whether a point is
          upward/downward;

    To extend an upward reagion, ``steep_point=steep_upward`` and
    ``xward_point=downward`` are expected, and to extend a downward region,
    ``steep_point=steep_downward`` and ``xward_point=upward``.

    steep_point : ndarray of shape (n_samples,), dtype=bool
        True if the point is steep downward (upward).

    xward_point : ndarray of shape (n_samples,), dtype=bool
        True if the point is an upward (respectively downward) point.

    start : int
        The start of the xward region.

    min_samples : int
       The same as the min_samples given to OPTICS. Up and down steep
       regions can't have more then ``min_samples`` consecutive non-steep
       points.

    Returns
    -------
    index : int
        The current index iterating over all the samples, i.e. where we are up
        to in our search.

    end : int
        The end of the region, which can be behind the index. The region
        includes the ``end`` index.
    """
    n_samples = len(steep_point)
    non_xward_points = 0
    index = start
    end = start
    # find a maximal area
    while index < n_samples:
        if steep_point[index]:
            non_xward_points = 0
            end = index
        elif not xward_point[index]:
            # it's not a steep point, but still goes up.
            non_xward_points += 1
            # region should include no more than min_samples consecutive
            # non steep xward points.
            if non_xward_points > min_samples:
                break
        else:
            return end
        index += 1
    return end

def _update_filter_sdas(sdas, mib, xi_complement, reachability_plot):
    """Update steep down areas (SDAs) using the new maximum in between (mib)
    value, and the given complement of xi, i.e. ``1 - xi``.
    """
    if np.isinf(mib):
        return []
    res = [
        sda for sda in sdas if mib <= reachability_plot[sda["start"]] * xi_complement
    ]
    for sda in res:
        sda["mib"] = max(sda["mib"], mib)
    return res

def _correct_predecessor(reachability_plot, predecessor_plot, ordering, s, e):
    """Correct for predecessors.

    Applies Algorithm 2 of [1]_.

    Input parameters are ordered by the computer OPTICS ordering.

    .. [1] Schubert, Erich, Michael Gertz.
    """
    while s < e:
        if reachability_plot[s] > reachability_plot[e]:
            return s, e
        p_e = predecessor_plot[e]
        for i in range(s, e):
            if p_e == ordering[i]:
                return s, e
        e -= 1
    return None, None

def _xi_cluster(
    reachability_plot,
    predecessor_plot,
    ordering,
    xi,
    min_cluster_size,
    predecessor_correction,
):

    This is rouphly an implementation of Figure 19 of the OPTICS paper.

    reachability_plot : array-like of shape (n_samples,)
        The reachability plot, i.e. reachability ordered according to
        the calculated ordering, all computed by OPTICS.

    predecessor_plot : array-like of shape (n_samples,)
        Predecessors ordered according to the calculated ordering.

    xi : float, between 0 and 1

    min_samples : int > 1

    min_cluster_size : int > 1
        Minimum number of samples in an OPTICS cluster.

    predecessor_correction : bool

    Returns
    -------
        The list of clusters in the form of [start, end] in each row, with all
        indices inclusive. The clusters are ordered in a way that larger
        clusters encompassing smaller clusters come after those smaller
        clusters.
    """

    # Our implementation adds an inf to the end of reachability plot
    # this helps to find potential clusters at the end of the
    # reachability plot even if there's no upward region at the end of it.
    reachability_plot = np.hstack((reachability_plot, np.inf))

    xi_complement = 1 - xi
    sdas = []  # steep down areas, introduced in section 4.3.2 of the paper
    clusters = []
    index = 0
    mib = 0.0  # maximum in between, section 4.3.2

    # Our implementation corrects a mistake in the original
    # paper, i.e., in Definition 9 steep downward point,
    # r(p) * (1 - x1) <= r(p + 1) should be
    # r(p) * (1 - x1) >= r(p + 1)
    with np.errstate(invalid="ignore"):
        ratio = reachability_plot[:-1] / reachability_plot[1:]
        steep_upward = ratio <= xi_complement
        steep_downward = ratio >= 1 / xi_complement
        downward = ratio > 1
        upward = ratio < 1

    # the following loop is almost exactly as Figure 19 of the paper.
    # it jumps over the areas which are not either steep down or up areas
    for steep_index in iter(np.flatnonzero(steep_upward | steep_downward)):
        # just continue if steep_index has been a part of a discovered xward
        # area.
        if steep_index < index:
            continue

        mib = max(mib, np.max(reachability_plot[index : steep_index + 1]))

        # steep downward areas
        if steep_downward[steep_index]:
            sdas = _update_filter_sdas(sdas, mib, xi_complement, reachability_plot)
            D_start = steep_index
            D_end = _extend_region(steep_downward, upward, D_start, min_samples)
            D = {"start": D_start, "end": D_end, "mib": 0.0}
            sdas.append(D)
            index = D_end + 1
            mib = reachability_plot[index]

        # steep upward areas
        else:
            U_start = steep_index
            U_end = _extend_region(steep_upward, downward, U_start, min_samples)
            index = U_end + 1

            U_clusters = []
            for D in sdas:
                c_start = D["start"]
                c_end = U_end

                # line (**), sc2*
                if reachability_plot[c_end + 1] * xi_complement < D["mib"]:
                    continue

                # Definition 11: criterion 4
                D_max = reachability_plot[D["start"]]
                if D_max * xi_complement >= reachability_plot[c_end + 1]:
                    # Find the first index from the left side which is almost
                    # at the same level as the end of the detected cluster.
                    while (
                        reachability_plot[c_start + 1] > reachability_plot[c_end + 1]
                        and c_start < D["end"]
                    ):
                        c_start += 1
                elif reachability_plot[c_end + 1] * xi_complement >= D_max:
                    # Find the first index from the right side which is almost
                    # at the same level as the beginning of the detected
                    # cluster.
                    # Our implementation corrects a mistake in the original
                    # paper, i.e., in Definition 11 4c, r(x) < r(sD) should be
                    # r(x) > r(sD).
                    while reachability_plot[c_end - 1] > D_max and c_end > U_start:
                        c_end -= 1

                # predecessor correction
                if predecessor_correction:
                    c_start, c_end = _correct_predecessor(
                        reachability_plot, predecessor_plot, ordering, c_start, c_end
                    )
                if c_start is None:
                    continue

                # Definition 11: criterion 3.a
                if c_end - c_start + 1 < min_cluster_size:
                    continue

                # Definition 11: criterion 1
                if c_start > D["end"]:
                    continue

                # Definition 11: criterion 2
                if c_end < U_start:
                    continue

                U_clusters.append((c_start, c_end))

            # add smaller clusters first.
            U_clusters.reverse()
            clusters.extend(U_clusters)

    return np.array(clusters)

def _extract_xi_labels(ordering, clusters):
    """Extracts the labels from the clusters returned by `_xi_cluster`.
    We rely on the fact that clusters are stored
    with the smaller clusters coming before the larger ones.

    ordering : array-like of shape (n_samples,)
        The ordering of points calculated by OPTICS

    clusters : array-like of shape (n_clusters, 2)
        List of clusters i.e. (start, end) tuples,
        as returned by `_xi_cluster`.

    Returns
    -------
    """

    labels = np.full(len(ordering), -1, dtype=int)
    label = 0
    for c in clusters:
        if not np.any(labels[c[0] : (c[1] + 1)] != -1):
            labels[c[0] : (c[1] + 1)] = label
            label += 1
    labels[ordering] = labels.copy()

        self,
        *,
        p=2,
        eps=None,
        xi=0.05,
    ):
        self.p = p

        y : Ignored

        Returns
        -------
        """
            msg = (
            )

        (
            X=X,
            p=self.p,
        )

            )
            else:

                )

                eps=eps,
            )

def _brute_mst(mutual_reachability, min_samples):
    """
    Builds a minimum spanning tree (MST) from the provided mutual-reachability
    values. This function dispatches to a custom Cython implementation for
    dense arrays, and `scipy.sparse.csgraph.minimum_spanning_tree` for sparse
    arrays/matrices.

    mututal_reachability_graph: {ndarray, sparse matrix} of shape \
        Weighted adjacency matrix of the mutual reachability graph.

    min_samples : int, default=None
        The number of samples in a neighborhood for a point

    Returns
    -------
    mst : ndarray of shape (n_samples - 1,), dtype=MST_edge_dtype
        The MST representation of the mutual-reachability graph. The MST is
        represented as a collection of edges.
    """
    if not issparse(mutual_reachability):
        return mst_from_mutual_reachability(mutual_reachability)

    # Check if the mutual reachability matrix has any rows which have
    # less than `min_samples` non-zero elements.
    indptr = mutual_reachability.indptr
    num_points = mutual_reachability.shape[0]
    if any((indptr[i + 1] - indptr[i]) < min_samples for i in range(num_points)):
            f"There exists points with fewer than {min_samples} neighbors. Ensure"
            " your distance matrix has non-zero values for at least"
            f" `min_sample`={min_samples} neighbors for each points (i.e. K-nn"
            " graph), or specify a `max_distance` in `metric_params` to use when"
            " distances are missing."
        )
    # Check connected component on mutual reachability.
    # If more than one connected component is present,
    # it means that the graph is disconnected.
    n_components = csgraph.connected_components(
        mutual_reachability, directed=False, return_labels=False
    )
    if n_components > 1:
            f"Sparse mutual reachability matrix has {n_components} connected"
            " components. HDBSCAN cannot be performed on a disconnected graph. Ensure"
            " that the sparse distance matrix has only one connected component."
        )

    # Compute the minimum spanning tree for the sparse graph
    sparse_min_spanning_tree = csgraph.minimum_spanning_tree(mutual_reachability)
    rows, cols = sparse_min_spanning_tree.nonzero()
    mst = np.rec.fromarrays(
        [rows, cols, sparse_min_spanning_tree.data],
        dtype=MST_edge_dtype,
    )
    return mst

def _process_mst(min_spanning_tree):
    """
    Builds a single-linkage tree (SLT) from the provided minimum spanning tree
    (MST). The MST is first sorted then processed by a custom Cython routine.

    min_spanning_tree : ndarray of shape (n_samples - 1,), dtype=MST_edge_dtype

    Returns
    -------
    single_linkage : ndarray of shape (n_samples - 1,), dtype=HIERARCHY_dtype
        The single-linkage tree tree (dendrogram) built from the MST.
    """
    row_order = np.argsort(min_spanning_tree["distance"])
    min_spanning_tree = min_spanning_tree[row_order]
    return make_single_linkage(min_spanning_tree)

def _hdbscan_brute(
    X,
    **metric_params,
):
    """
    Builds a single-linkage tree (SLT) from the input data `X`. If
    `metric="precomputed"` then `X` must be a symmetric array of distances.
    Otherwise, the pairwise distances are calculated directly and passed to
    `mutual_reachability_graph`.

    X : ndarray of shape (n_samples, n_features) or (n_samples, n_samples)
        Either the raw data from which to compute the pairwise distances,
        or the precomputed distances.

        A distance scaling parameter as used in robust single linkage.

        feature array.

        - If metric is a string or callable, it must be one of
          the options allowed by :func:`~sklearn.metrics.pairwise_distances`
          for its metric parameter.

        - If metric is "precomputed", X is assumed to be a distance matrix and
          must be square.

        The number of jobs to use for computing the pairwise distances. This
        works by breaking down the pairwise matrix into n_jobs even slices and
        computing them in parallel. This parameter is passed directly to
        :func:`~sklearn.metrics.pairwise_distances`.

        If `copy=True` then any time an in-place modifications would be made
        that would overwrite `X`, a copy will first be made, guaranteeing that
        the original data will be unchanged. Currently, it only applies when
        `metric="precomputed"`, when passing a dense array or a CSR sparse
        array/matrix.

        Arguments passed to the distance metric.

    Returns
    -------
    """
        if X.shape[0] != X.shape[1]:
                "The precomputed distance matrix is expected to be symmetric, however"
                f" it has shape {X.shape}. Please verify that the"
                " distance matrix was constructed correctly."
            )
        if not _allclose_dense_sparse(X, X.T):
                " its values appear to be asymmetric. Please verify that the distance"
                " matrix was constructed correctly."
            )

        distance_matrix = X.copy() if copy else X
    else:
        distance_matrix = pairwise_distances(
            X, metric=metric, n_jobs=n_jobs, **metric_params
        )
    distance_matrix /= alpha

    max_distance = metric_params.get("max_distance", 0.0)
    if issparse(distance_matrix) and distance_matrix.format != "csr":
        # we need CSR format to avoid a conversion in `_brute_mst` when calling
        # `csgraph.connected_components`
        distance_matrix = distance_matrix.tocsr()

    # Note that `distance_matrix` is manipulated in-place, however we do not
    # need it for anything else past this point, hence the operation is safe.
    mutual_reachability_ = mutual_reachability_graph(
        distance_matrix, min_samples=min_samples, max_distance=max_distance
    )
    min_spanning_tree = _brute_mst(mutual_reachability_, min_samples=min_samples)
    # Warn if the MST couldn't be constructed around the missing distances
    if np.isinf(min_spanning_tree["distance"]).any():
        warn(
            (
                "The minimum spanning tree contains edge weights with value "
                "infinity. Potentially, you are missing too many distances "
                "in the initial distance matrix for the given neighborhood "
                "size."
            ),
        )
    return _process_mst(min_spanning_tree)

def _hdbscan_prims(
    X,
    algo,
    alpha=1.0,
    leaf_size=40,
):
    """

        The raw data.

        feature array. `metric` must be one of the options allowed by
        :func:`~sklearn.metrics.pairwise_distances` for its metric
        parameter.

    Returns
    -------
    """
    # The Cython routines used require contiguous arrays
    X = np.asarray(X, order="C")

    # Get distance to kth nearest neighbour
        algorithm=algo,
        p=None,
    ).fit(X)

    neighbors_distances, _ = nbrs.kneighbors(X, min_samples, return_distance=True)
    core_distances = np.ascontiguousarray(neighbors_distances[:, -1])
    dist_metric = DistanceMetric.get_metric(metric, **metric_params)

    # Mutual reachability distance is implicit in mst_from_data_matrix
    min_spanning_tree = mst_from_data_matrix(X, core_distances, dist_metric, alpha)

def remap_single_linkage_tree(tree, internal_to_raw, non_finite):
    """
    Takes an internal single_linkage_tree structure and adds back in a set of points
    that were initially detected as non-finite and returns that new tree.
    These points will all be merged into the final node at np.inf distance and
    considered noise points.

    tree : ndarray of shape (n_samples - 1,), dtype=HIERARCHY_dtype
    internal_to_raw: dict
        A mapping from internal integer index to the raw integer index
    non_finite : ndarray
        Boolean array of which entries in the raw data are non-finite
    """
    finite_count = len(internal_to_raw)

    outlier_count = len(non_finite)
    for i, _ in enumerate(tree):
        left = tree[i]["left_node"]
        right = tree[i]["right_node"]

        if left < finite_count:
            tree[i]["left_node"] = internal_to_raw[left]
        else:
            tree[i]["left_node"] = left + outlier_count
        if right < finite_count:
            tree[i]["right_node"] = internal_to_raw[right]
        else:
            tree[i]["right_node"] = right + outlier_count

    outlier_tree = np.zeros(len(non_finite), dtype=HIERARCHY_dtype)
    last_cluster_id = max(
        tree[tree.shape[0] - 1]["left_node"], tree[tree.shape[0] - 1]["right_node"]
    )
    last_cluster_size = tree[tree.shape[0] - 1]["cluster_size"]
    for i, outlier in enumerate(non_finite):
        outlier_tree[i] = (outlier, last_cluster_id + 1, np.inf, last_cluster_size + 1)
        last_cluster_id += 1
        last_cluster_size += 1
    tree = np.concatenate([tree, outlier_tree])

def _get_finite_row_indices(matrix):
    """
    Returns the indices of the purely finite rows of a
    sparse matrix or dense ndarray
    """
    if issparse(matrix):
        row_indices = np.array(
            [i for i, row in enumerate(matrix.tolil().data) if np.all(np.isfinite(row))]
        )
    else:
        (row_indices,) = np.isfinite(matrix.sum(axis=1)).nonzero()
    return row_indices

class HDBSCAN(ClusterMixin, BaseEstimator):
    """Cluster data using hierarchical density-based clustering.

    HDBSCAN - Hierarchical Density-Based Spatial Clustering of Applications
    with Noise. Performs :class:`~sklearn.cluster.DBSCAN` over varying epsilon
    values and integrates the result to find a clustering that gives the best
    stability over epsilon.
    This allows HDBSCAN to find clusters of varying densities (unlike
    :class:`~sklearn.cluster.DBSCAN`), and be more robust to parameter selection.
    Read more in the :ref:`User Guide <hdbscan>`.

    min_cluster_size : int, default=5
        The minimum number of samples in a group for that group to be
        considered a cluster; groupings smaller than this size will be left
        as noise.

        The parameter `k` used to calculate the distance between a point
        `x_p` and its k-th nearest neighbor.
        When `None`, defaults to `min_cluster_size`.

    cluster_selection_epsilon : float, default=0.0
        A distance threshold. Clusters below this value will be merged.
        See [5]_ for more information.

    max_cluster_size : int, default=None
        A limit to the size of clusters returned by the `"eom"` cluster
        selection algorithm. There is no limit when `max_cluster_size=None`.
        Has no effect if `cluster_selection_method="leaf"`.

        See [3]_ for more information.

    algorithm : {"auto", "brute", "kd_tree", "ball_tree"}, default="auto"
        Exactly which algorithm to use for computing core distances; By default
        this is set to `"auto"` which attempts to use a
        :class:`~sklearn.neighbors.KDTree` tree if possible, otherwise it uses
        a :class:`~sklearn.neighbors.BallTree` tree. Both `"kd_tree"` and
        `"ball_tree"` algorithms use the
        :class:`~sklearn.neighbors.NearestNeighbors` estimator.

        If the `X` passed during `fit` is sparse or `metric` is invalid for
        both :class:`~sklearn.neighbors.KDTree` and
        :class:`~sklearn.neighbors.BallTree`, then it resolves to use the
        `"brute"` algorithm.

        Leaf size for trees responsible for fast nearest neighbour queries when
        a KDTree or a BallTree are used as core-distance algorithms. A large
        dataset size and small `leaf_size` may induce excessive memory usage.
        If you are running out of memory consider increasing the `leaf_size`
        parameter. Ignored for `algorithm="brute"`.

        Number of jobs to run in parallel to calculate distances.
        `None` means 1 unless in a :obj:`joblib.parallel_backend` context.
        `-1` means using all processors. See :term:`Glossary <n_jobs>`

    cluster_selection_method : {"eom", "leaf"}, default="eom"
        The method used to select clusters from the condensed tree. The
        standard approach for HDBSCAN* is to use an Excess of Mass (`"eom"`)
        algorithm to find the most persistent clusters. Alternatively you can
        instead select the clusters at the leaves of the tree -- this provides
        the most fine grained and homogeneous clusters.

    allow_single_cluster : bool, default=False
        By default HDBSCAN* will not produce a single cluster, setting this
        to True will override this and allow single cluster results in
        the case that you feel this is a valid result for your dataset.

    store_centers : str, default=None
        Which, if any, cluster centers to compute and store. The options are:

        - `None` which does not compute nor store any centers.
        - `"centroid"` which calculates the center by taking the weighted
          average of their positions. Note that the algorithm uses the
          euclidean metric and does not guarantee that the output will be
          an observed data point.
        - `"medoid"` which calculates the center by taking the point in the
          fitted data which minimizes the distance to all other points in
          the cluster. This is slower than "centroid" since it requires
          computing additional pairwise distances between points of the
          same cluster but guarantees the output is an observed data point.
          The medoid is also well-defined for arbitrary metrics, and does not
          depend on a euclidean metric.
        - `"both"` which computes and stores both forms of centers.

        that would overwrite data passed to :term:`fit`, a copy will first be
        made, guaranteeing that the original data will be unchanged.
        Currently, it only applies when `metric="precomputed"`, when passing
        a dense array or a CSR sparse matrix and when `algorithm="brute"`.

        Cluster labels for each point in the dataset given to :term:`fit`.
        Outliers are labeled as follows:

        - Noisy samples are given the label -1.
        - Samples with infinite elements (+/- np.inf) are given the label -2.
        - Samples with missing data are given the label -3, even if they
          also have infinite elements.

    probabilities_ : ndarray of shape (n_samples,)
        The strength with which each sample is a member of its assigned
        cluster.

        - Clustered samples have probabilities proportional to the degree that
          they persist as part of the cluster.
        - Noisy samples have probability zero.
        - Samples with infinite elements (+/- np.inf) have probability 0.
        - Samples with missing data have probability `np.nan`.

    centroids_ : ndarray of shape (n_clusters, n_features)
        A collection containing the centroid of each cluster calculated under
        the standard euclidean metric. The centroids may fall "outside" their
        respective clusters if the clusters themselves are non-convex.

        Note that `n_clusters` only counts non-outlier clusters. That is to
        say, the `-1, -2, -3` labels for the outlier clusters are excluded.

    medoids_ : ndarray of shape (n_clusters, n_features)
        A collection containing the medoid of each cluster calculated under
        the whichever metric was passed to the `metric` parameter. The
        medoids are points in the original cluster which minimize the average
        distance to all other points in that cluster under the chosen metric.
        These can be thought of as the result of projecting the `metric`-based
        centroid back onto the cluster.

    See Also
    --------
    DBSCAN : Density-Based Spatial Clustering of Applications
        with Noise.
    OPTICS : Ordering Points To Identify the Clustering Structure.
    Birch : Memory-efficient, online-learning algorithm.

    Notes
    -----
    The `min_samples` parameter includes the point itself, whereas the implementation in
    `scikit-learn-contrib/hdbscan <https://github.com/scikit-learn-contrib/hdbscan>`_
    does not. To get the same results in both versions, the value of `min_samples` here
    must be 1 greater than the value used in `scikit-learn-contrib/hdbscan
    <https://github.com/scikit-learn-contrib/hdbscan>`_.

    .. [1] :doi:`Campello, R. J., Moulavi, D., & Sander, J. Density-based clustering
      based on hierarchical density estimates.
      <10.1007/978-3-642-37456-2_14>`
    .. [2] :doi:`Campello, R. J., Moulavi, D., Zimek, A., & Sander, J.
       Hierarchical density estimates for data clustering, visualization,
       and outlier detection.<10.1145/2733381>`

    .. [3] `Chaudhuri, K., & Dasgupta, S. Rates of convergence for the
       cluster tree.
       <https://papers.nips.cc/paper/2010/hash/
       b534ba68236ba543ae44b22bd110a1d6-Abstract.html>`_

    .. [4] `Moulavi, D., Jaskowiak, P.A., Campello, R.J., Zimek, A. and
       Sander, J. Density-Based Clustering Validation.
       <https://www.dbs.ifi.lmu.de/~zimek/publications/SDM2014/DBCV.pdf>`_

    .. [5] :arxiv:`Malzer, C., & Baum, M. "A Hybrid Approach To Hierarchical
       Density-based Cluster Selection."<1911.02282>`.

    Examples
    --------
    >>> from sklearn.cluster import HDBSCAN
    >>> hdb = HDBSCAN(min_cluster_size=20)
    >>> hdb.fit(X)
    HDBSCAN(min_cluster_size=20)
    >>> hdb.labels_.shape == (X.shape[0],)
    True
    >>> np.unique(hdb.labels_).tolist()
    [-1, 0, 1, 2, 3, 4, 5, 6, 7]
    """

        "min_cluster_size": [Interval(Integral, left=2, right=None, closed="left")],
        "min_samples": [Interval(Integral, left=1, right=None, closed="left"), None],
        "cluster_selection_epsilon": [
            Interval(Real, left=0, right=None, closed="left")
        ],
        "max_cluster_size": [
            None,
            Interval(Integral, left=1, right=None, closed="left"),
        ],
            StrOptions(FAST_METRICS | set(_VALID_METRICS) | {"precomputed"}),
            callable,
        ],
        "alpha": [Interval(Real, left=0, right=None, closed="neither")],
        "algorithm": [StrOptions({"auto", "brute", "kd_tree", "ball_tree"})],
        "leaf_size": [Interval(Integral, left=1, right=None, closed="left")],
        "cluster_selection_method": [StrOptions({"eom", "leaf"})],
        "allow_single_cluster": ["boolean"],
        "store_centers": [None, StrOptions({"centroid", "medoid", "both"})],
    }

        self,
        min_cluster_size=5,
        min_samples=None,
        cluster_selection_epsilon=0.0,
        max_cluster_size=None,
        cluster_selection_method="eom",
        allow_single_cluster=False,
        store_centers=None,
        copy=False,
    ):
        self.max_cluster_size = max_cluster_size
        self.cluster_selection_epsilon = cluster_selection_epsilon
        self.cluster_selection_method = cluster_selection_method
        self.allow_single_cluster = allow_single_cluster
        self.store_centers = store_centers

        # HDBSCAN.metric is not validated yet
    )
        """Find clusters based on hierarchical density-based clustering.

                ndarray of shape (n_samples, n_samples)
            `metric='precomputed'`.

        y : None
            Ignored.

        Returns
        -------
            Returns self.
        """
        if self.metric == "precomputed" and self.store_centers is not None:
                "Cannot store centers when using a precomputed distance matrix."
            )

        self._metric_params = self.metric_params or {}
        if self.metric != "precomputed":
            # Non-precomputed matrices may contain non-finite values.
                self,
                X,
                accept_sparse=["csr", "lil"],
            )
            self._raw_data = X
            all_finite = True
            try:
                _assert_all_finite(X.data if issparse(X) else X)
            except ValueError:
                all_finite = False

            if not all_finite:
                # Pass only the purely finite indices into hdbscan
                # We will later assign all non-finite points their
                # corresponding labels, as specified in `_OUTLIER_ENCODING`

                # Reduce X to make the checks for missing/outlier samples more
                # convenient.
                reduced_X = X.sum(axis=1)

                # Samples with missing data are denoted by the presence of
                # `np.nan`
                missing_index = np.isnan(reduced_X).nonzero()[0]

                # Outlier samples are denoted by the presence of `np.inf`
                infinite_index = np.isinf(reduced_X).nonzero()[0]

                # Continue with only finite samples
                finite_index = _get_finite_row_indices(X)
                internal_to_raw = {x: y for x, y in enumerate(finite_index)}
                X = X[finite_index]
        elif issparse(X):
            # Handle sparse precomputed distance matrices separately
                self,
                X,
            )
        else:
            # Only non-sparse, precomputed distance matrices are handled here
            # and thereby allowed to contain numpy.inf for missing distances

            # Perform data validation after removing infinite values (numpy.inf)
            # from the given distance matrix.
                self, X, ensure_all_finite=False, dtype=np.float64, force_writeable=True
            )
            if np.isnan(X).any():
                # TODO: Support np.nan in Cython implementation for precomputed
                # dense HDBSCAN
                raise ValueError("np.nan values found in precomputed-dense")
        if X.shape[0] == 1:
            raise ValueError("n_samples=1 while HDBSCAN requires more than one sample")
        self._min_samples = (
            self.min_cluster_size if self.min_samples is None else self.min_samples
        )

        if self._min_samples > X.shape[0]:
                f"min_samples ({self._min_samples}) must be at most the number of"
                f" samples in X ({X.shape[0]})"
            )

        mst_func = None
            X=X,
            min_samples=self._min_samples,
            **self._metric_params,
        )
        if self.algorithm == "kd_tree" and self.metric not in KDTree.valid_metrics:
                f"{self.metric} is not a valid metric for a KDTree-based algorithm."
                " Please select a different metric."
            )
        elif (
            self.algorithm == "ball_tree" and self.metric not in BallTree.valid_metrics
        ):
                f"{self.metric} is not a valid metric for a BallTree-based algorithm."
            )

        if self.algorithm != "auto":
            if (
                self.metric != "precomputed"
                and issparse(X)
                and self.algorithm != "brute"
            ):
                raise ValueError("Sparse data matrices only support algorithm `brute`.")

            if self.algorithm == "brute":
                mst_func = _hdbscan_brute
                kwargs["copy"] = self.copy
            elif self.algorithm == "kd_tree":
                mst_func = _hdbscan_prims
                kwargs["algo"] = "kd_tree"
                kwargs["leaf_size"] = self.leaf_size
            else:
                kwargs["algo"] = "ball_tree"
        else:
            if issparse(X) or self.metric not in FAST_METRICS:
                # We can't do much with sparse matrices ...
            elif self.metric in KDTree.valid_metrics:
                # TODO: Benchmark KD vs Ball Tree efficiency
            else:
                # Metric is a valid BallTree metric

        self._single_linkage_tree_ = mst_func(**kwargs)

        self.labels_, self.probabilities_ = tree_to_labels(
            self._single_linkage_tree_,
            self.min_cluster_size,
            self.cluster_selection_method,
            self.allow_single_cluster,
            self.cluster_selection_epsilon,
            self.max_cluster_size,
        )
        if self.metric != "precomputed" and not all_finite:
            # Remap indices to align with original data in the case of
            # non-finite entries. Samples with np.inf are mapped to -1 and
            # those with np.nan are mapped to -2.
            self._single_linkage_tree_ = remap_single_linkage_tree(
                self._single_linkage_tree_,
                internal_to_raw,
                # There may be overlap for points w/ both `np.inf` and `np.nan`
                non_finite=set(np.hstack([infinite_index, missing_index])),
            )
            new_labels = np.empty(self._raw_data.shape[0], dtype=np.int32)
            new_labels[finite_index] = self.labels_
            new_labels[infinite_index] = _OUTLIER_ENCODING["infinite"]["label"]
            new_labels[missing_index] = _OUTLIER_ENCODING["missing"]["label"]
            self.labels_ = new_labels

            new_probabilities = np.zeros(self._raw_data.shape[0], dtype=np.float64)
            new_probabilities[finite_index] = self.probabilities_
            # Infinite outliers have probability 0 by convention, though this
            # is arbitrary.
            new_probabilities[infinite_index] = _OUTLIER_ENCODING["infinite"]["prob"]
            new_probabilities[missing_index] = _OUTLIER_ENCODING["missing"]["prob"]
            self.probabilities_ = new_probabilities

        if self.store_centers:
            self._weighted_cluster_center(X)

        """Cluster X and return the associated cluster labels.

        y : None
            Ignored.

        Returns
        -------
        """

    def _weighted_cluster_center(self, X):
        """Calculate and store the centroids/medoids of each cluster.

        This requires `X` to be a raw feature array, not precomputed
        distances. Rather than return outputs directly, this helper method
        instead stores them in the `self.{centroids, medoids}_` attributes.
        The choice for which attributes are calculated and stored is mediated
        by the value of `self.store_centers`.

            The feature array that the estimator was fit with.

        """
        # Number of non-noise clusters
        n_clusters = len(set(self.labels_) - {-1, -2})
        mask = np.empty((X.shape[0],), dtype=np.bool_)
        make_centroids = self.store_centers in ("centroid", "both")
        make_medoids = self.store_centers in ("medoid", "both")

        if make_centroids:
            self.centroids_ = np.empty((n_clusters, X.shape[1]), dtype=np.float64)
        if make_medoids:
            self.medoids_ = np.empty((n_clusters, X.shape[1]), dtype=np.float64)

        # Need to handle iteratively seen each cluster may have a different
        # number of samples, hence we can't create a homogeneous 3D array.
        for idx in range(n_clusters):
            mask = self.labels_ == idx
            data = X[mask]
            strength = self.probabilities_[mask]
            if make_centroids:
                self.centroids_[idx] = np.average(data, weights=strength, axis=0)
            if make_medoids:
                # TODO: Implement weighted argmin PWD backend
                dist_mat = pairwise_distances(
                    data, metric=self.metric, **self._metric_params
                )
                dist_mat = dist_mat * strength
                medoid_index = np.argmin(dist_mat.sum(axis=1))
                self.medoids_[idx] = data[medoid_index]
        return

    def dbscan_clustering(self, cut_distance, min_cluster_size=5):
        """Return clustering given by DBSCAN without border points.

        Return clustering that would be equivalent to running DBSCAN* for a
        particular cut_distance (or epsilon) DBSCAN* can be thought of as
        DBSCAN without the border points.  As such these results may differ
        slightly from `cluster.DBSCAN` due to the difference in implementation
        over the non-core points.

        This can also be thought of as a flat clustering derived from constant
        height cut through the single linkage tree.

        This represents the result of selecting a cut value for robust single linkage
        clustering. The `min_cluster_size` allows the flat clustering to declare noise
        points (and cluster smaller than `min_cluster_size`).

        cut_distance : float
            The mutual reachability distance cut value to use to generate a
            flat clustering.

        min_cluster_size : int, default=5
            Clusters smaller than this value with be called 'noise' and remain
            unclustered in the resulting flat clustering.

        Returns
        -------
            An array of cluster labels, one per datapoint.
            Outliers are labeled as follows:

            - Noisy samples are given the label -1.
            - Samples with infinite elements (+/- np.inf) are given the label -2.
            - Samples with missing data are given the label -3, even if they
              also have infinite elements.
        """
        labels = labelling_at_cut(
            self._single_linkage_tree_, cut_distance, min_cluster_size
        )
        # Infer indices from labels generated during `fit`
        infinite_index = self.labels_ == _OUTLIER_ENCODING["infinite"]["label"]
        missing_index = self.labels_ == _OUTLIER_ENCODING["missing"]["label"]

        # Overwrite infinite/missing outlier samples (otherwise simple noise)
        labels[infinite_index] = _OUTLIER_ENCODING["infinite"]["label"]
        labels[missing_index] = _OUTLIER_ENCODING["missing"]["label"]

        tags.input_tags.allow_nan = self.metric != "precomputed"

        self,
    ):

        y : None
            Ignored.

        Returns
        -------
        """
            )

                self,
                X,
            )
            try:

                # `np.nan`

                self,
                X,
            )
        else:

            )
        )

            )

            X=X,
        )
            )
        elif (
        ):
            )

            if (
            ):

            else:
        else:
            else:

        )
            )

        y : None
            Ignored.

        Returns
        -------
        """

        """

                )
        return

        Returns
        -------

        """
        )

def test_mutual_reachability_graph_error_sparse_format():
    """Check that we raise an error if the sparse format is not CSR."""
    X = X.T @ X
    np.fill_diagonal(X, 0.0)
    X = _convert_container(X, "sparse_csc")

    err_msg = "Only sparse CSR matrices are supported"
        mutual_reachability_graph(X)

def test_mutual_reachability_graph_inplace(array_type):
    """Check that the operation is happening inplace."""
    X = X.T @ X

    mr_graph = mutual_reachability_graph(X)

    assert id(mr_graph) == id(X)

def test_mutual_reachability_graph_equivalence_dense_sparse():
    """Check that we get the same results for dense and sparse implementation."""
    X = rng.randn(5, 5)
    X_dense = X.T @ X
    X_sparse = _convert_container(X_dense, "sparse_csr")

    mr_graph_dense = mutual_reachability_graph(X_dense, min_samples=3)
    mr_graph_sparse = mutual_reachability_graph(X_sparse, min_samples=3)

    assert_allclose(mr_graph_dense, mr_graph_sparse.toarray())

def test_mutual_reachability_graph_preserves_dtype(array_type, dtype):
    """Check that the computation preserve dtype thanks to fused types."""
    X = (X.T @ X).astype(dtype)

    assert X.dtype == dtype
    assert mr_graph.dtype == dtype

def test_n_samples_leaves_roots(global_random_seed, global_dtype):
    # Sanity check for the number of samples in leaves and roots
    X, y = make_blobs(n_samples=10, random_state=global_random_seed)
    brc = Birch()
    brc.fit(X)
    n_samples_root = sum([sc.n_samples_ for sc in brc.root_.subclusters_])
    n_samples_leaves = sum(
        [sc.n_samples_ for leaf in brc._get_leaves() for sc in leaf.subclusters_]
    )
    assert n_samples_leaves == X.shape[0]
    assert n_samples_root == X.shape[0]

def test_partial_fit(global_random_seed, global_dtype):
    # Test that fit is equivalent to calling partial_fit multiple times
    X, y = make_blobs(n_samples=100, random_state=global_random_seed)
    brc = Birch(n_clusters=3)
    brc_partial = Birch(n_clusters=None)
    brc_partial.partial_fit(X[:50])
    brc_partial.partial_fit(X[50:])
    assert_allclose(brc_partial.subcluster_centers_, brc.subcluster_centers_)

    # Test that same global labels are obtained after calling partial_fit
    # with None
    brc_partial.set_params(n_clusters=3)
    brc_partial.partial_fit(None)
    assert_array_equal(brc_partial.subcluster_labels_, brc.subcluster_labels_)

def test_birch_predict(global_random_seed, global_dtype):
    # Test the predict method predicts the nearest centroid.
    X = generate_clustered_data(n_clusters=3, n_features=3, n_samples_per_cluster=10)

    # n_samples * n_samples_per_cluster
    shuffle_indices = np.arange(30)
    rng.shuffle(shuffle_indices)
    X_shuffle = X[shuffle_indices, :]
    brc = Birch(n_clusters=4, threshold=1.0)
    brc.fit(X_shuffle)

    # Birch must preserve inputs' dtype
    assert brc.subcluster_centers_.dtype == global_dtype

    assert_array_equal(brc.labels_, brc.predict(X_shuffle))
    centroids = brc.subcluster_centers_
    nearest_centroid = brc.subcluster_labels_[
        pairwise_distances_argmin(X_shuffle, centroids)
    ]
    assert_allclose(v_measure_score(nearest_centroid, brc.labels_), 1.0)

def test_n_clusters(global_random_seed, global_dtype):
    # Test that n_clusters param works properly
    X, y = make_blobs(n_samples=100, centers=10, random_state=global_random_seed)
    brc1 = Birch(n_clusters=10)
    brc1.fit(X)
    assert len(brc1.subcluster_centers_) > 10
    assert len(np.unique(brc1.labels_)) == 10

    # Test that n_clusters = Agglomerative Clustering gives
    # the same results.
    gc = AgglomerativeClustering(n_clusters=10)
    brc2 = Birch(n_clusters=gc)
    brc2.fit(X)
    assert_array_equal(brc1.subcluster_labels_, brc2.subcluster_labels_)
    assert_array_equal(brc1.labels_, brc2.labels_)

    # Test that a small number of clusters raises a warning.
    brc4 = Birch(threshold=10000.0)
        brc4.fit(X)

def test_sparse_X(global_random_seed, global_dtype, csr_container):
    # Test that sparse and dense data give same results
    brc = Birch(n_clusters=10)

    csr = csr_container(X)
    brc_sparse = Birch(n_clusters=10)
    brc_sparse.fit(csr)

    assert brc_sparse.subcluster_centers_.dtype == global_dtype

    assert_array_equal(brc.labels_, brc_sparse.labels_)
    assert_allclose(brc.subcluster_centers_, brc_sparse.subcluster_centers_)

def test_partial_fit_second_call_error_checks():
    # second partial fit calls will error when n_features is not consistent
    # with the first call
    X, y = make_blobs(n_samples=100)
    brc.partial_fit(X, y)

    msg = "X has 1 features, but Birch is expecting 2 features"
        brc.partial_fit(X[:, [0]], y)

def check_branching_factor(node, branching_factor):
    subclusters = node.subclusters_
    assert branching_factor >= len(subclusters)
    for cluster in subclusters:
        if cluster.child_:
            check_branching_factor(cluster.child_, branching_factor)

def test_branching_factor(global_random_seed, global_dtype):
    # Test that nodes have at max branching_factor number of subclusters
    X, y = make_blobs(random_state=global_random_seed)
    branching_factor = 9

    # Purposefully set a low threshold to maximize the subclusters.
    brc = Birch(n_clusters=None, branching_factor=branching_factor, threshold=0.01)
    check_branching_factor(brc.root_, branching_factor)
    brc = Birch(n_clusters=3, branching_factor=branching_factor, threshold=0.01)

def check_threshold(birch_instance, threshold):
    """Use the leaf linked list for traversal"""
    current_leaf = birch_instance.dummy_leaf_.next_leaf_
    while current_leaf:
        subclusters = current_leaf.subclusters_
        for sc in subclusters:
            assert threshold >= sc.radius
        current_leaf = current_leaf.next_leaf_

def test_threshold(global_random_seed, global_dtype):
    # Test that the leaf subclusters have a threshold lesser than radius
    X, y = make_blobs(n_samples=80, centers=4, random_state=global_random_seed)
    brc = Birch(threshold=0.5, n_clusters=None)
    check_threshold(brc, 0.5)

    brc = Birch(threshold=5.0, n_clusters=None)
    check_threshold(brc, 5.0)

def test_birch_n_clusters_long_int():
    # Check that birch supports n_clusters with np.int64 dtype, for instance
    # coming from np.arange. #16484
    n_clusters = np.int64(5)
    Birch(n_clusters=n_clusters).fit(X)

    """Check `get_feature_names_out` for `Birch`."""
    X, _ = make_blobs(n_samples=80, n_features=4, random_state=0)
    brc = Birch(n_clusters=4)
    n_clusters = brc.subcluster_centers_.shape[0]

    names_out = brc.get_feature_names_out()
    assert_array_equal([f"birch{i}" for i in range(n_clusters)], names_out)

def test_transform_match_across_dtypes(global_random_seed):
    X, _ = make_blobs(n_samples=80, n_features=4, random_state=global_random_seed)
    brc = Birch(n_clusters=4, threshold=1.1)
    Y_64 = brc.fit_transform(X)
    Y_32 = brc.fit_transform(X.astype(np.float32))

    assert_allclose(Y_64, Y_32, atol=1e-6)

def test_subcluster_dtype(global_dtype):
    X = make_blobs(n_samples=80, n_features=4, random_state=0)[0].astype(
    )
    assert brc.fit(X).subcluster_centers_.dtype == global_dtype

def test_both_subclusters_updated():
    """Check that both subclusters are updated when a node a split, even when there are
    duplicated data points. Non-regression test for #23269.
    """

        [
            [-2.6192791, -1.5053215],
            [-2.9993038, -1.6863596],
            [-2.3724914, -1.3438171],
            [-2.336792, -1.3417323],
            [-2.4089134, -1.3290224],
            [-3.364009, -1.8846745],
            [-2.617677, -1.5003285],
            [-2.2960556, -1.3260119],
            [-2.5459878, -1.4533926],
            [-2.25979, -1.3003055],
            [-2.9720619, -1.7058647],
        ],
    )

    # no error
    Birch(branching_factor=5, threshold=1e-5, n_clusters=None).fit(X)

def test_birch_copy_deprecated():
    brc = Birch(n_clusters=4, copy=True)
    with pytest.warns(FutureWarning, match="`copy` was deprecated"):
        brc.fit(X)

def test_convergence_of_1d_constant_data():
    # Test convergence using 1D constant data
    # https://github.com/scikit-learn/scikit-learn/issues/28926
    model = MeanShift()
    n_iter = model.fit(np.ones(10).reshape(-1, 1)).n_iter_
    assert n_iter < model.max_iter

def test_estimate_bandwidth():
    # Test estimate_bandwidth
    bandwidth = estimate_bandwidth(X, n_samples=200)
    assert 0.9 <= bandwidth <= 1.5

def test_estimate_bandwidth_1sample(global_dtype):
    # Test estimate_bandwidth when n_samples=1 and quantile<1, so that
    # n_neighbors is set to 1.
    bandwidth = estimate_bandwidth(
        X.astype(global_dtype, copy=False), n_samples=1, quantile=0.3
    )

    assert bandwidth.dtype == X.dtype
    assert bandwidth == pytest.approx(0.0, abs=1e-5)

def test_mean_shift(
    global_dtype, bandwidth, cluster_all, expected, first_cluster_label
):
    # Test MeanShift algorithm
    X_with_global_dtype = X.astype(global_dtype, copy=False)
    ms = MeanShift(bandwidth=bandwidth, cluster_all=cluster_all)
    labels = ms.fit(X_with_global_dtype).labels_
    labels_unique = np.unique(labels)
    n_clusters_ = len(labels_unique)
    assert n_clusters_ == expected
    assert labels_unique[0] == first_cluster_label
    assert ms.cluster_centers_.dtype == global_dtype

    cluster_centers, labels_mean_shift = mean_shift(
        X_with_global_dtype, cluster_all=cluster_all
    )
    labels_mean_shift_unique = np.unique(labels_mean_shift)
    n_clusters_mean_shift = len(labels_mean_shift_unique)
    assert n_clusters_mean_shift == expected
    assert labels_mean_shift_unique[0] == first_cluster_label
    assert cluster_centers.dtype == global_dtype

def test_parallel(global_dtype, global_random_seed):
    centers = np.array([[1, 1], [-1, -1], [1, -1]]) + 10
        cluster_std=0.4,
    )

    ms1 = MeanShift(n_jobs=2)
    ms1.fit(X)

    ms2 = MeanShift()
    ms2.fit(X)

    assert_allclose(ms1.cluster_centers_, ms2.cluster_centers_)
    assert ms1.cluster_centers_.dtype == ms2.cluster_centers_.dtype
    assert_array_equal(ms1.labels_, ms2.labels_)

def test_meanshift_predict(global_dtype):
    # Test MeanShift.predict
    ms = MeanShift(bandwidth=1.2)
    labels = ms.fit_predict(X_with_global_dtype)
    labels2 = ms.predict(X_with_global_dtype)
    assert_array_equal(labels, labels2)

def test_meanshift_all_orphans():
    # init away from the data, crash with a sensible warning
    ms = MeanShift(bandwidth=0.1, seeds=[[-9, -9], [-10, -10]])
    msg = "No point was within bandwidth=0.1"
        ms.fit(
            X,
        )

    # Non-regression: before fit, there should be not fitted attributes.
    ms = MeanShift()
    assert not hasattr(ms, "cluster_centers_")
    assert not hasattr(ms, "labels_")

def test_cluster_intensity_tie(global_dtype):
    X = np.array([[1, 1], [2, 1], [1, 0], [4, 7], [3, 5], [3, 6]], dtype=global_dtype)
    c1 = MeanShift(bandwidth=2).fit(X)

    X = np.array([[4, 7], [3, 5], [3, 6], [1, 1], [2, 1], [1, 0]], dtype=global_dtype)
    c2 = MeanShift(bandwidth=2).fit(X)
    assert_array_equal(c1.labels_, [1, 1, 1, 0, 0, 0])
    assert_array_equal(c2.labels_, [0, 0, 0, 1, 1, 1])

def test_bin_seeds(global_dtype):
    # Test the bin seeding technique which can be used in the mean shift
    # algorithm
    # Data is just 6 points in the plane
        [[1.0, 1.0], [1.4, 1.4], [1.8, 1.2], [2.0, 1.0], [2.1, 1.1], [0.0, 0.0]],
    )

    # With a bin coarseness of 1.0 and min_bin_freq of 1, 3 bins should be
    # found
    ground_truth = {(1.0, 1.0), (2.0, 1.0), (0.0, 0.0)}
    test_bins = get_bin_seeds(X, 1, 1)
    test_result = set(tuple(p) for p in test_bins)
    assert len(ground_truth.symmetric_difference(test_result)) == 0

    # With a bin coarseness of 1.0 and min_bin_freq of 2, 2 bins should be
    # found
    ground_truth = {(1.0, 1.0), (2.0, 1.0)}
    test_bins = get_bin_seeds(X, 1, 2)

    # With a bin size of 0.01 and min_bin_freq of 1, 6 bins should be found
    # we bail and use the whole data here.
    with warnings.catch_warnings(record=True):
        test_bins = get_bin_seeds(X, 0.01, 1)
    assert_allclose(test_bins, X)

    # tight clusters around [0, 0] and [1, 1], only get two bins
        centers=[[0, 0], [1, 1]],
        cluster_std=0.1,
    )
    test_bins = get_bin_seeds(X, 1)
    assert_array_equal(test_bins, [[0, 0], [1, 1]])

def test_max_iter(max_iter):
    clusters1, _ = mean_shift(X, max_iter=max_iter)
    ms = MeanShift(max_iter=max_iter).fit(X)
    clusters2 = ms.cluster_centers_

    assert ms.n_iter_ <= ms.max_iter
    assert len(clusters1) == len(clusters2)

    for c1, c2 in zip(clusters1, clusters2):
        assert np.allclose(c1, c2)

def test_mean_shift_zero_bandwidth(global_dtype):
    # Check that mean shift works when the estimated bandwidth is 0.
    X = np.array([1, 1, 1, 2, 2, 2, 3, 3], dtype=global_dtype).reshape(-1, 1)

    # estimate_bandwidth with default args returns 0 on this dataset
    bandwidth = estimate_bandwidth(X)
    assert bandwidth == 0

    # get_bin_seeds with a 0 bin_size should return the dataset itself
    assert get_bin_seeds(X, bin_size=bandwidth) is X

    # MeanShift with binning and a 0 estimated bandwidth should be equivalent
    # to no binning.
    ms_binning = MeanShift(bin_seeding=True, bandwidth=None).fit(X)
    ms_nobinning = MeanShift(bin_seeding=False).fit(X)
    expected_labels = np.array([0, 0, 0, 1, 1, 1, 2, 2])

    assert v_measure_score(ms_binning.labels_, expected_labels) == pytest.approx(1)
    assert v_measure_score(ms_nobinning.labels_, expected_labels) == pytest.approx(1)
    assert_allclose(ms_binning.cluster_centers_, ms_nobinning.cluster_centers_)

def test_affinity_propagation(global_random_seed, global_dtype):
    """Test consistency of the affinity propagations."""
    S = -euclidean_distances(X.astype(global_dtype, copy=False), squared=True)
    preference = np.median(S) * 10
    cluster_centers_indices, labels = affinity_propagation(
        S, preference=preference, random_state=global_random_seed
    )

    n_clusters_ = len(cluster_centers_indices)

    assert n_clusters == n_clusters_

def test_affinity_propagation_precomputed():
    """Check equality of precomputed affinity matrix to internally computed affinity
    matrix.
    """
    S = -euclidean_distances(X, squared=True)
    af = AffinityPropagation(
        preference=preference, affinity="precomputed", random_state=28
    )
    labels_precomputed = af.fit(S).labels_

    af = AffinityPropagation(preference=preference, verbose=True, random_state=37)
    labels = af.fit(X).labels_

    assert_array_equal(labels, labels_precomputed)

    cluster_centers_indices = af.cluster_centers_indices_

    assert np.unique(labels).size == n_clusters_

def test_affinity_propagation_no_copy():
    """Check behaviour of not copying the input data."""
    S_original = S.copy()
    assert not np.allclose(S.diagonal(), preference)

    # with copy=True S should not be modified
    affinity_propagation(S, preference=preference, copy=True, random_state=0)
    assert_allclose(S, S_original)
    assert_allclose(S.diagonal(), np.zeros(S.shape[0]))

    # with copy=False S will be modified inplace
    affinity_propagation(S, preference=preference, copy=False, random_state=0)
    assert_allclose(S.diagonal(), preference)

    # test that copy=True and copy=False lead to the same result
    S = S_original.copy()
    af = AffinityPropagation(preference=preference, verbose=True, random_state=0)

    _, labels_no_copy = affinity_propagation(
        S, preference=preference, copy=False, random_state=74
    )
    assert_array_equal(labels, labels_no_copy)

def test_affinity_propagation_affinity_shape():
    """Check the shape of the affinity matrix when using `affinity_propagation."""
    err_msg = "The matrix of similarities must be a square array"
        affinity_propagation(S[:, :-1])

def test_affinity_propagation_precomputed_with_sparse_input(csr_container):
    err_msg = "Sparse data was passed for X, but dense data is required"
        AffinityPropagation(affinity="precomputed").fit(csr_container((3, 3)))

def test_affinity_propagation_predict(global_random_seed, global_dtype):
    # Test AffinityPropagation.predict
    af = AffinityPropagation(affinity="euclidean", random_state=global_random_seed)
    X_ = X.astype(global_dtype, copy=False)
    labels = af.fit_predict(X_)
    labels2 = af.predict(X_)

def test_affinity_propagation_predict_error():
    # Test exception in AffinityPropagation.predict
    # Not fitted.
    af = AffinityPropagation(affinity="euclidean")
        af.predict(X)

    # Predict not supported when affinity="precomputed".
    S = np.dot(X, X.T)
    af = AffinityPropagation(affinity="precomputed", random_state=57)
    af.fit(S)
    with pytest.raises(ValueError, match="expecting 60 features as input"):

def test_affinity_propagation_fit_non_convergence(global_dtype):
    # In case of non-convergence of affinity_propagation(), the cluster
    # centers should be an empty array and training samples should be labelled
    # as noise (-1)
    X = np.array([[0, 0], [1, 1], [-2, -2]], dtype=global_dtype)

    # Force non-convergence by allowing only a single iteration
    af = AffinityPropagation(preference=-10, max_iter=1, random_state=82)

        af.fit(X)
    assert_allclose(np.empty((0, 2)), af.cluster_centers_)
    assert_array_equal(np.array([-1, -1, -1]), af.labels_)

def test_affinity_propagation_equal_mutual_similarities(global_dtype):
    X = np.array([[-1, 1], [1, -1]], dtype=global_dtype)

    # setting preference > similarity
    with pytest.warns(UserWarning, match="mutually equal"):
        cluster_center_indices, labels = affinity_propagation(S, preference=0)

    # expect every sample to become an exemplar
    assert_array_equal([0, 1], cluster_center_indices)
    assert_array_equal([0, 1], labels)

    # setting preference < similarity
        cluster_center_indices, labels = affinity_propagation(S, preference=-10)

    # expect one cluster, with arbitrary (first) sample as exemplar
    assert_array_equal([0], cluster_center_indices)
    assert_array_equal([0, 0], labels)

    # setting different preferences
        cluster_center_indices, labels = affinity_propagation(
            S, preference=[-20, -10], random_state=37
        )

    # expect one cluster, with highest-preference sample as exemplar
    assert_array_equal([1], cluster_center_indices)

def test_affinity_propagation_predict_non_convergence(global_dtype):
    # centers should be an empty array

        af = AffinityPropagation(preference=-10, max_iter=1, random_state=75).fit(X)

    # At prediction time, consider new samples as noise since there are no
    # clusters
    to_predict = np.array([[2, 2], [3, 3], [4, 4]])
        y = af.predict(to_predict)
    assert_array_equal(np.array([-1, -1, -1]), y)

def test_affinity_propagation_non_convergence_regressiontest(global_dtype):
        [[1, 0, 0, 0, 0, 0], [0, 1, 1, 1, 0, 0], [0, 0, 1, 0, 0, 1]], dtype=global_dtype
    )
    af = AffinityPropagation(affinity="euclidean", max_iter=2, random_state=34)
    msg = (
        "Affinity propagation did not converge, this model may return degenerate"
        " cluster centers and labels."
    )
    with pytest.warns(ConvergenceWarning, match=msg):
        af.fit(X)

    assert_array_equal(np.array([0, 0, 0]), af.labels_)

def test_equal_similarities_and_preferences(global_dtype):
    # Unequal distances

    assert not _equal_similarities_and_preferences(S, np.array(0))
    assert not _equal_similarities_and_preferences(S, np.array([0, 0]))
    assert not _equal_similarities_and_preferences(S, np.array([0, 1]))

    # Equal distances
    X = np.array([[0, 0], [1, 1]], dtype=global_dtype)

    # Different preferences

    # Same preferences
    assert _equal_similarities_and_preferences(S, np.array([0, 0]))
    assert _equal_similarities_and_preferences(S, np.array(0))

def test_affinity_propagation_random_state():
    """Check that different random states lead to different initialisations
    by looking at the center locations after two iterations.
    """
    centers = [[1, 1], [-1, -1], [1, -1]]
    X, labels_true = make_blobs(
        n_samples=300, centers=centers, cluster_std=0.5, random_state=0
    )
    # random_state = 0
    ap = AffinityPropagation(convergence_iter=1, max_iter=2, random_state=0)
    ap.fit(X)
    centers0 = ap.cluster_centers_

    # random_state = 76
    ap = AffinityPropagation(convergence_iter=1, max_iter=2, random_state=76)
    ap.fit(X)
    centers76 = ap.cluster_centers_
    # check that the centers have not yet converged to the same solution
    assert np.mean((centers0 - centers76) ** 2) > 1

def test_affinity_propagation_convergence_warning_dense_sparse(container, global_dtype):
    """
    Check that having sparse or dense `centers` format should not
    influence the convergence.
    Non-regression test for gh-13334.
    """
    centers = container(np.zeros((1, 10)))
    X = rng.rand(40, 10).astype(global_dtype, copy=False)
    y = (4 * rng.rand(40)).astype(int)
    ap = AffinityPropagation(random_state=46)
    ap.fit(X, y)
    ap.cluster_centers_ = centers
        assert_array_equal(ap.predict(X), np.zeros(X.shape[0], dtype=int))

def test_correct_clusters(global_dtype):
    # Test to fix incorrect clusters due to dtype change
    # (non-regression test for issue #10832)
        [[1, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 1]], dtype=global_dtype
    )
    afp = AffinityPropagation(preference=1, affinity="precomputed", random_state=0).fit(
        X
    )
    expected = np.array([0, 1, 1, 2])
    assert_array_equal(afp.labels_, expected)

def test_sparse_input_for_predict(csr_container):
    # Test to make sure sparse inputs are accepted for predict
    # (non-regression test for issue #20049)
    af = AffinityPropagation(affinity="euclidean", random_state=42)
    af.fit(X)
    labels = af.predict(csr_container((2, 2)))
    assert_array_equal(labels, (2, 2))

def test_sparse_input_for_fit_predict(csr_container):
    # Test to make sure sparse inputs are accepted for fit_predict
    X = csr_container(rng.randint(0, 2, size=(5, 5)))
    labels = af.fit_predict(X)
    assert_array_equal(labels, (0, 1, 1, 2, 3))

def test_affinity_propagation_equal_points():
    """Make sure we do not assign multiple clusters to equal points.

    https://github.com/scikit-learn/scikit-learn/pull/20043
    """
    X = np.zeros((8, 1))
    af = AffinityPropagation(affinity="euclidean", damping=0.5, random_state=42).fit(X)
    assert np.all(af.labels_ == 0)

def generate_clustered_data(
    seed=0, n_clusters=3, n_features=2, n_samples_per_cluster=20, std=0.4
):
    prng = np.random.RandomState(seed)

    # the data is voluntary shifted away from zero to check clustering
    # algorithm robustness with regards to non centered data
    means = (
        np.array(
            [
                [1, 1, 1, 0],
                [-1, -1, 0, 1],
                [1, -1, 1, 1],
                [-1, 1, 1, 0],
            ]
        )
        + 10
    )

    X = np.empty((0, n_features))
        X = np.r_[
            X,
            means[i][:n_features] + std * prng.randn(n_samples_per_cluster, n_features),
        ]
    return X

def test_extend_downward(r_plot, end):
    r_plot = np.array(r_plot)
    ratio = r_plot[:-1] / r_plot[1:]
    steep_downward = ratio >= 1 / 0.9
    upward = ratio < 1

    e = _extend_region(steep_downward, upward, 0, 2)
    assert e == end

def test_extend_upward(r_plot, end):
    steep_upward = ratio <= 0.9
    downward = ratio > 1

    e = _extend_region(steep_upward, downward, 0, 2)

def test_the_extract_xi_labels(ordering, clusters, expected):

    assert_array_equal(labels, expected)

def test_extract_xi(global_dtype):
    # small and easy test (no clusters around other clusters)
    # but with a clear noise data.
    # global_random_seed is not used here since the expected labels
    # are hardcoded for these specific data.
    n_points_per_cluster = 5

    C1 = [-5, -2] + 0.8 * rng.randn(n_points_per_cluster, 2)
    C2 = [4, -1] + 0.1 * rng.randn(n_points_per_cluster, 2)
    C3 = [1, -2] + 0.2 * rng.randn(n_points_per_cluster, 2)
    C4 = [-2, 3] + 0.3 * rng.randn(n_points_per_cluster, 2)
    C5 = [3, -2] + 0.6 * rng.randn(n_points_per_cluster, 2)
    C6 = [5, 6] + 0.2 * rng.randn(n_points_per_cluster, 2)

    X = np.vstack((C1, C2, C3, C4, C5, np.array([[100, 100]]), C6)).astype(
    )
    expected_labels = np.r_[[2] * 5, [0] * 5, [1] * 5, [3] * 5, [1] * 5, -1, [4] * 5]
    X, expected_labels = shuffle(X, expected_labels, random_state=rng)

    clust = OPTICS(
        min_samples=3, min_cluster_size=2, max_eps=20, cluster_method="xi", xi=0.4
    ).fit(X)
    assert_array_equal(clust.labels_, expected_labels)

    # check float min_samples and min_cluster_size
        min_samples=0.1, min_cluster_size=0.08, max_eps=20, cluster_method="xi", xi=0.4
    ).fit(X)

    X = np.vstack((C1, C2, C3, C4, C5, np.array([[100, 100]] * 2), C6)).astype(
    )
    expected_labels = np.r_[
        [1] * 5, [3] * 5, [2] * 5, [0] * 5, [2] * 5, -1, -1, [4] * 5
    ]

        min_samples=3, min_cluster_size=3, max_eps=20, cluster_method="xi", xi=0.3
    ).fit(X)
    # this may fail if the predecessor correction is not at work!

    C1 = [[0, 0], [0, 0.1], [0, -0.1], [0.1, 0]]
    C2 = [[10, 10], [10, 9], [10, 11], [9, 10]]
    C3 = [[100, 100], [100, 90], [100, 110], [90, 100]]
    X = np.vstack((C1, C2, C3)).astype(global_dtype, copy=False)
    expected_labels = np.r_[[0] * 4, [1] * 4, [2] * 4]

        min_samples=2, min_cluster_size=2, max_eps=np.inf, cluster_method="xi", xi=0.04
    ).fit(X)

def test_cluster_hierarchy(global_dtype, global_random_seed):
    n_points_per_cluster = 100
    C1 = [0, 0] + 2 * rng.randn(n_points_per_cluster, 2).astype(
    )
    C2 = [0, 0] + 50 * rng.randn(n_points_per_cluster, 2).astype(
    )
    X = np.vstack((C1, C2))
    X = shuffle(X, random_state=rng)

    clusters = OPTICS(min_samples=20, xi=0.2).fit(X).cluster_hierarchy_
    assert clusters.shape == (2, 2)

    # The first cluster should contain all point from C1 but due to how the data is
    # generated, some points from C2 may end up in it.
    assert 100 <= np.diff(clusters[0]) + 1 <= 115
    # The second cluster should contain all points from C1 and C2.
    assert np.diff(clusters[-1]) + 1 == 200

def test_correct_number_of_clusters(metric, csr_container):
    # in 'auto' mode

    # Parameters chosen specifically for this task.
    # Compute OPTICS
    clust = OPTICS(max_eps=5.0 * 6.0, min_samples=4, xi=0.1, metric=metric)
    clust.fit(csr_container(X) if csr_container is not None else X)
    # number of clusters, ignoring noise if present
    n_clusters_1 = len(set(clust.labels_)) - int(-1 in clust.labels_)
    assert n_clusters_1 == n_clusters

    # check attribute types and sizes
    assert clust.labels_.shape == (len(X),)
    assert clust.labels_.dtype.kind == "i"

    assert clust.reachability_.shape == (len(X),)
    assert clust.reachability_.dtype.kind == "f"

    assert clust.core_distances_.shape == (len(X),)
    assert clust.core_distances_.dtype.kind == "f"

    assert clust.ordering_.shape == (len(X),)
    assert clust.ordering_.dtype.kind == "i"
    assert set(clust.ordering_) == set(range(len(X)))

def test_minimum_number_of_sample_check():
    # test that we check a minimum number of samples
    msg = "min_samples must be no greater than"

    X = [[1, 1]]
    clust = OPTICS(max_eps=5.0 * 0.3, min_samples=10, min_cluster_size=1.0)

    # Run the fit
        clust.fit(X)

def test_bad_extract():
    # Test an extraction of eps too close to original eps
    msg = "Specify an epsilon smaller than 0.15. Got 0.3."
        n_samples=750, centers=centers, cluster_std=0.4, random_state=0
    )

    clust = OPTICS(max_eps=5.0 * 0.03, cluster_method="dbscan", eps=0.3, min_samples=10)

def test_bad_reachability():
    msg = "All reachability values are inf. Set a larger max_eps."
    )

        clust = OPTICS(max_eps=5.0 * 0.003, min_samples=10, eps=0.015)

def test_nowarn_if_metric_bool_data_bool():
    # make sure no warning is raised if metric and data are both boolean
    # https://github.com/scikit-learn/scikit-learn/issues/18996

    pairwise_metric = "rogerstanimoto"
    X = np.random.randint(2, size=(5, 2), dtype=bool)

        OPTICS(metric=pairwise_metric).fit(X)

def test_warn_if_metric_bool_data_no_bool():
    # make sure a *single* conversion warning is raised if metric is boolean
    # but data isn't

    X = np.random.randint(2, size=(5, 2), dtype=np.int32)
    msg = f"Data will be converted to boolean for metric {pairwise_metric}"

    with pytest.warns(DataConversionWarning, match=msg) as warn_record:
        assert len(warn_record) == 1

def test_nowarn_if_metric_no_bool():
    # make sure no conversion warning is raised if
    # metric isn't boolean, no matter what the data type is
    pairwise_metric = "minkowski"
    X_bool = np.random.randint(2, size=(5, 2), dtype=bool)
    X_num = np.random.randint(2, size=(5, 2), dtype=np.int32)

        # fit boolean data
        OPTICS(metric=pairwise_metric).fit(X_bool)
        # fit numeric data
        OPTICS(metric=pairwise_metric).fit(X_num)

def test_close_extract():
    # Test extract where extraction eps is close to scaled max_eps

    )

    clust = OPTICS(max_eps=1.0, cluster_method="dbscan", eps=0.3, min_samples=10).fit(X)
    # Cluster ordering starts at 0; max cluster label = 2 is 3 clusters
    assert max(clust.labels_) == 2

def test_dbscan_optics_parity(eps, min_samples, metric, global_dtype, csr_container):
    # Test that OPTICS clustering labels are <= 5% difference of DBSCAN

        n_samples=150, centers=centers, cluster_std=0.4, random_state=0
    )
    X = csr_container(X) if csr_container is not None else X

    # calculate optics with dbscan extract at 0.3 epsilon
    op = OPTICS(
        min_samples=min_samples, cluster_method="dbscan", eps=eps, metric=metric
    ).fit(X)

    # calculate dbscan labels
    db = DBSCAN(eps=eps, min_samples=min_samples).fit(X)

    contingency = contingency_matrix(db.labels_, op.labels_)
    agree = min(
        np.sum(np.max(contingency, axis=0)), np.sum(np.max(contingency, axis=1))
    )
    disagree = X.shape[0] - agree

    percent_mismatch = np.round((disagree - 1) / X.shape[0], 2)

    # verify label mismatch is <= 5% labels
    assert percent_mismatch <= 0.05

def test_min_samples_edge_case(global_dtype):
    C1 = [[0, 0], [0, 0.1], [0, -0.1]]
    C2 = [[10, 10], [10, 9], [10, 11]]
    C3 = [[100, 100], [100, 96], [100, 106]]

    expected_labels = np.r_[[0] * 3, [1] * 3, [2] * 3]
    clust = OPTICS(min_samples=3, max_eps=7, cluster_method="xi", xi=0.04).fit(X)

    expected_labels = np.r_[[0] * 3, [1] * 3, [-1] * 3]
    clust = OPTICS(min_samples=3, max_eps=3, cluster_method="xi", xi=0.04).fit(X)

    expected_labels = np.r_[[-1] * 9]
    with pytest.warns(UserWarning, match="All reachability values"):
        clust = OPTICS(min_samples=4, max_eps=3, cluster_method="xi", xi=0.04).fit(X)
        assert_array_equal(clust.labels_, expected_labels)

def test_min_cluster_size(min_cluster_size, global_dtype):
    redX = X[::2].astype(global_dtype, copy=False)  # reduce for speed
    clust = OPTICS(min_samples=9, min_cluster_size=min_cluster_size).fit(redX)
    cluster_sizes = np.bincount(clust.labels_[clust.labels_ != -1])
    if cluster_sizes.size:
        assert min(cluster_sizes) >= min_cluster_size
    # check behaviour is the same when min_cluster_size is a fraction
    clust_frac = OPTICS(
        min_samples=9,
        min_cluster_size=min_cluster_size / redX.shape[0],
    )
    clust_frac.fit(redX)
    assert_array_equal(clust.labels_, clust_frac.labels_)

def test_min_cluster_size_invalid2(csr_container):
    clust = OPTICS(min_cluster_size=len(X) + 1)
    with pytest.raises(ValueError, match="must be no greater than the "):

    clust = OPTICS(min_cluster_size=len(X) + 1, metric="euclidean")
        clust.fit(csr_container(X))

def test_processing_order():
    # Ensure that we consider all unprocessed points,
    # not only direct neighbors. when picking the next point.
    Y = [[0], [10], [-10], [25]]

    clust = OPTICS(min_samples=3, max_eps=15).fit(Y)
    assert_array_equal(clust.reachability_, [np.inf, 10, 10, 15])
    assert_array_equal(clust.core_distances_, [10, 15, np.inf, np.inf])
    assert_array_equal(clust.ordering_, [0, 1, 2, 3])

def test_compare_to_ELKI():
    # Expected values, computed with (future) ELKI 0.7.5 using:
    # java -jar elki.jar cli -dbc.in csv -dbc.filter FixedDBIDsFilter
    #   -algorithm clustering.optics.OPTICSHeap -optics.minpts 5
    # where the FixedDBIDsFilter gives 0-indexed ids.
    r1 = [
        np.inf,
        1.0574896366427478,
        0.7587934993548423,
        0.7290174038973836,
        0.6861627576116127,
        0.9280118450166668,
        1.1748022534146194,
        3.3355455741292257,
        0.49618389254482587,
        0.2552805046961355,
        0.24944622248445714,
        0.3086779122185853,
        4.163024452756142,
        1.623152630340929,
        0.45315840475822655,
        0.25468325192031926,
        0.2254004358159971,
        0.18765711877083036,
        0.1821471333893275,
        0.2240202988740153,
        1.154337614548715,
        1.342604473837069,
        1.323308536402633,
        0.8607514948648837,
        0.27219111215810565,
        0.13260875220533205,
        0.09890587675958984,
        0.13548790801634494,
        0.1575483940837384,
        0.17515137170530226,
        0.17575920159442388,
        0.6101447895405373,
        1.3189208094864302,
        2.2509184159764577,
        2.4517810628594527,
        3.675977064404973,
        3.8264795626020365,
        2.9130735341510614,
        2.8459300127258036,
        3.0321982337972537,
    ]
    o1 = [
        0,
        3,
        6,
        4,
        7,
        8,
        2,
        9,
        5,
        1,
        31,
        30,
        32,
        34,
        33,
        38,
        39,
        35,
        37,
        36,
        44,
        21,
        23,
        24,
        22,
        25,
        27,
        29,
        26,
        28,
        20,
        40,
        45,
        46,
        10,
        15,
        11,
        13,
        17,
        19,
        18,
        12,
        16,
        14,
        47,
        49,
        43,
        48,
        42,
        41,
        53,
        57,
        51,
        52,
        56,
        59,
        54,
        55,
        58,
        50,
    ]
    p1 = [
        -1,
        0,
        3,
        6,
        6,
        6,
        8,
        3,
        7,
        5,
        1,
        31,
        30,
        30,
        34,
        34,
        34,
        32,
        32,
        37,
        36,
        44,
        21,
        23,
        24,
        22,
        25,
        25,
        22,
        22,
        22,
        21,
        40,
        45,
        46,
        10,
        15,
        15,
        13,
        13,
        15,
        11,
        19,
        15,
        10,
        47,
        12,
        45,
        14,
        43,
        42,
        53,
        57,
        57,
        57,
        57,
        59,
        59,
        59,
        58,
    ]

    # Tests against known extraction array
    # Does NOT work with metric='euclidean', because sklearn euclidean has
    # worse numeric precision. 'minkowski' is slower but more accurate.
    clust1 = OPTICS(min_samples=5).fit(X)

    assert_array_equal(clust1.ordering_, np.array(o1))
    assert_array_equal(clust1.predecessor_[clust1.ordering_], np.array(p1))
    assert_allclose(clust1.reachability_[clust1.ordering_], np.array(r1))
    # ELKI currently does not print the core distances (which are not used much
    # in literature, but we can at least ensure to have this consistency:
    for i in clust1.ordering_[1:]:
        assert clust1.reachability_[i] >= clust1.core_distances_[clust1.predecessor_[i]]

    # Expected values, computed with (future) ELKI 0.7.5 using
    r2 = [
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        0.4928068613197889,
        np.inf,
        0.2666183922512113,
        0.18715928772277457,
        np.inf,
        0.34466409325984865,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
        np.inf,
    ]
    o2 = [
        0,
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        8,
        9,
        10,
        15,
        11,
        13,
        17,
        19,
        18,
        12,
        16,
        14,
        47,
        46,
        20,
        22,
        25,
        23,
        27,
        29,
        24,
        26,
        28,
        21,
        30,
        32,
        34,
        33,
        38,
        39,
        35,
        37,
        36,
        31,
        40,
        41,
        42,
        43,
        44,
        45,
        48,
        49,
        50,
        51,
        52,
        53,
        54,
        55,
        56,
        57,
        58,
        59,
    ]
    p2 = [
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        10,
        15,
        15,
        13,
        13,
        15,
        11,
        19,
        15,
        10,
        47,
        -1,
        20,
        22,
        25,
        25,
        25,
        25,
        22,
        22,
        23,
        -1,
        30,
        30,
        34,
        34,
        34,
        32,
        32,
        37,
        38,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
        -1,
    ]
    clust2 = OPTICS(min_samples=5, max_eps=0.5).fit(X)

    assert_array_equal(clust2.ordering_, np.array(o2))
    assert_array_equal(clust2.predecessor_[clust2.ordering_], np.array(p2))
    assert_allclose(clust2.reachability_[clust2.ordering_], np.array(r2))

    index = np.where(clust1.core_distances_ <= 0.5)[0]
    assert_allclose(clust1.core_distances_[index], clust2.core_distances_[index])

def test_extract_dbscan(global_dtype, global_random_seed):
    # testing an easy dbscan case. Not including clusters with different
    # densities.
    n_points_per_cluster = 20
    C1 = [-5, -2] + 0.2 * rng.randn(n_points_per_cluster, 2)
    C2 = [4, -1] + 0.2 * rng.randn(n_points_per_cluster, 2)
    C3 = [1, 2] + 0.2 * rng.randn(n_points_per_cluster, 2)
    C4 = [-2, 3] + 0.2 * rng.randn(n_points_per_cluster, 2)
    X = np.vstack((C1, C2, C3, C4)).astype(global_dtype, copy=False)

    clust = OPTICS(cluster_method="dbscan", eps=0.5).fit(X)
        np.sort(np.unique(clust.labels_[clust.labels_ != -1])), [0, 1, 2, 3]
    )

def test_precomputed_dists(global_dtype, csr_container):
    redX = X[::2].astype(global_dtype, copy=False)
    dists = pairwise_distances(redX, metric="euclidean")
    dists = csr_container(dists) if csr_container is not None else dists
        warnings.simplefilter("ignore", EfficiencyWarning)
        clust1 = OPTICS(min_samples=10, algorithm="brute", metric="precomputed").fit(
            dists
        )
    clust2 = OPTICS(min_samples=10, algorithm="brute", metric="euclidean").fit(redX)

    assert_allclose(clust1.reachability_, clust2.reachability_)
    assert_array_equal(clust1.labels_, clust2.labels_)

def test_optics_input_not_modified_precomputed_sparse_nodiag(
    csr_container, global_random_seed
):
    """Check that we don't modify in-place the pre-computed sparse matrix.
    https://github.com/scikit-learn/scikit-learn/issues/27508
    """
    X = np.random.RandomState(global_random_seed).rand(6, 6)
    # Add zeros on the diagonal that will be implicit when creating
    # the sparse matrix. If `X` is modified in-place, the zeros from
    # the diagonal will be made explicit.
    np.fill_diagonal(X, 0)
    assert all(row != col for row, col in zip(*X.nonzero()))
    OPTICS(metric="precomputed").fit(X)
    # Make sure that we did not modify `X` in-place even by creating
    # explicit 0s values.
    assert X.nnz == X_copy.nnz
    assert_array_equal(X.toarray(), X_copy.toarray())

def test_optics_predecessor_correction_ordering():
    """Check that cluster correction using predecessor is working as expected.

    In the following example, the predecessor correction was not working properly
    since it was not using the right indices.

    This non-regression test check that reordering the data does not change the results.

    https://github.com/scikit-learn/scikit-learn/issues/26324
    """
    X_1 = np.array([1, 2, 3, 1, 8, 8, 7, 100]).reshape(-1, 1)
    reorder = [0, 1, 2, 4, 5, 6, 7, 3]
    X_2 = X_1[reorder]

    optics_1 = OPTICS(min_samples=3, metric="euclidean").fit(X_1)
    optics_2 = OPTICS(min_samples=3, metric="euclidean").fit(X_2)

    assert_array_equal(optics_1.labels_[reorder], optics_2.labels_)

def test_dbscan_similarity():
    # Tests the DBSCAN algorithm with a similarity array.
    eps = 0.15
    min_samples = 10
    # Compute similarities
    D = distance.squareform(distance.pdist(X))
    D /= np.max(D)
    # Compute DBSCAN
    core_samples, labels = dbscan(
        D, metric="precomputed", eps=eps, min_samples=min_samples
    )
    n_clusters_1 = len(set(labels)) - (1 if -1 in labels else 0)

    db = DBSCAN(metric="precomputed", eps=eps, min_samples=min_samples)
    labels = db.fit(D).labels_

    n_clusters_2 = len(set(labels)) - int(-1 in labels)
    assert n_clusters_2 == n_clusters

def test_dbscan_feature():
    # Tests the DBSCAN algorithm with a feature vector array.
    # Different eps to other test, because distance is not normalised.
    eps = 0.8
    # parameters chosen for task
    core_samples, labels = dbscan(X, metric=metric, eps=eps, min_samples=min_samples)

    n_clusters_1 = len(set(labels)) - int(-1 in labels)

    db = DBSCAN(metric=metric, eps=eps, min_samples=min_samples)
    labels = db.fit(X).labels_

def test_dbscan_sparse(lil_container):
    core_sparse, labels_sparse = dbscan(lil_container(X), eps=0.8, min_samples=10)
    core_dense, labels_dense = dbscan(X, eps=0.8, min_samples=10)
    assert_array_equal(core_dense, core_sparse)
    assert_array_equal(labels_dense, labels_sparse)

def test_dbscan_sparse_precomputed(include_self):
    nn = NearestNeighbors(radius=0.9).fit(X)
    X_ = X if include_self else None
    D_sparse = nn.radius_neighbors_graph(X=X_, mode="distance")
    # Ensure it is sparse not merely on diagonals:
    assert D_sparse.nnz < D.shape[0] * (D.shape[0] - 1)
    core_sparse, labels_sparse = dbscan(
        D_sparse, eps=0.8, min_samples=10, metric="precomputed"
    )
    core_dense, labels_dense = dbscan(D, eps=0.8, min_samples=10, metric="precomputed")

def test_dbscan_sparse_precomputed_different_eps():
    # test that precomputed neighbors graph is filtered if computed with
    # a radius larger than DBSCAN's eps.
    lower_eps = 0.2
    nn = NearestNeighbors(radius=lower_eps).fit(X)
    D_sparse = nn.radius_neighbors_graph(X, mode="distance")
    dbscan_lower = dbscan(D_sparse, eps=lower_eps, metric="precomputed")

    higher_eps = lower_eps + 0.7
    nn = NearestNeighbors(radius=higher_eps).fit(X)
    dbscan_higher = dbscan(D_sparse, eps=lower_eps, metric="precomputed")

    assert_array_equal(dbscan_lower[0], dbscan_higher[0])
    assert_array_equal(dbscan_lower[1], dbscan_higher[1])

def test_dbscan_input_not_modified(metric, csr_container):
    # test that the input is not modified by dbscan
    X = np.random.RandomState(0).rand(10, 10)
    dbscan(X, metric=metric)

    if csr_container is not None:
        assert_array_equal(X.toarray(), X_copy.toarray())
    else:
        assert_array_equal(X, X_copy)

def test_dbscan_input_not_modified_precomputed_sparse_nodiag(csr_container):

    """
    dbscan(X, metric="precomputed")

def test_dbscan_no_core_samples(csr_container):
    X = rng.rand(40, 10)

    for X_ in [X, csr_container(X)]:
        db = DBSCAN(min_samples=6).fit(X_)
        assert_array_equal(db.components_, np.empty((0, X_.shape[1])))
        assert_array_equal(db.labels_, -1)
        assert db.core_sample_indices_.shape == (0,)

def test_dbscan_callable():
    # Tests the DBSCAN algorithm with a callable metric.
    eps = 0.8
    # metric is the function reference, not the string key.
    metric = distance.euclidean
        X, metric=metric, eps=eps, min_samples=min_samples, algorithm="ball_tree"
    )

    db = DBSCAN(metric=metric, eps=eps, min_samples=min_samples, algorithm="ball_tree")

def test_dbscan_metric_params():
    # Tests that DBSCAN works with the metrics_params argument.
    eps = 0.8
    p = 1

    # Compute DBSCAN with metric_params arg

    with warnings.catch_warnings(record=True) as warns:
        db = DBSCAN(
            metric="minkowski",
            metric_params={"p": p},
            eps=eps,
            p=None,
            min_samples=min_samples,
            algorithm="ball_tree",
        ).fit(X)
    assert not warns, warns[0].message
    core_sample_1, labels_1 = db.core_sample_indices_, db.labels_

    # Test that sample labels are the same as passing Minkowski 'p' directly
    db = DBSCAN(
        metric="minkowski", eps=eps, min_samples=min_samples, algorithm="ball_tree", p=p
    ).fit(X)
    core_sample_2, labels_2 = db.core_sample_indices_, db.labels_

    assert_array_equal(core_sample_1, core_sample_2)
    assert_array_equal(labels_1, labels_2)

    # Minkowski with p=1 should be equivalent to Manhattan distance
        metric="manhattan", eps=eps, min_samples=min_samples, algorithm="ball_tree"
    ).fit(X)
    core_sample_3, labels_3 = db.core_sample_indices_, db.labels_

    assert_array_equal(core_sample_1, core_sample_3)
    assert_array_equal(labels_1, labels_3)

        SyntaxWarning,
        match=(
            "Parameter p is found in metric_params. "
            "The corresponding parameter from __init__ "
            "is ignored."
        ),
    ):
        # Test that checks p is ignored in favor of metric_params={'p': <val>}
            eps=eps,
            p=p + 1,
        ).fit(X)
        core_sample_4, labels_4 = db.core_sample_indices_, db.labels_

    assert_array_equal(core_sample_1, core_sample_4)
    assert_array_equal(labels_1, labels_4)

def test_dbscan_balltree():
    # Tests the DBSCAN algorithm with balltree for neighbor calculation.
    eps = 0.8

    )

    db = DBSCAN(p=2.0, eps=eps, min_samples=min_samples, algorithm="ball_tree")

    db = DBSCAN(p=2.0, eps=eps, min_samples=min_samples, algorithm="kd_tree")

    n_clusters_3 = len(set(labels)) - int(-1 in labels)
    assert n_clusters_3 == n_clusters

    db = DBSCAN(p=1.0, eps=eps, min_samples=min_samples, algorithm="ball_tree")

    n_clusters_4 = len(set(labels)) - int(-1 in labels)
    assert n_clusters_4 == n_clusters

    db = DBSCAN(leaf_size=20, eps=eps, min_samples=min_samples, algorithm="ball_tree")

    n_clusters_5 = len(set(labels)) - int(-1 in labels)
    assert n_clusters_5 == n_clusters

    # DBSCAN.fit should accept a list of lists.
    X = [[1.0, 2.0], [3.0, 4.0]]
    DBSCAN().fit(X)  # must not raise exception

    obj = DBSCAN()
    assert type(pickle.loads(s)) is obj.__class__

def test_boundaries():
    # ensure min_samples is inclusive of core point
    core, _ = dbscan([[0], [1]], eps=2, min_samples=2)
    assert 0 in core
    # ensure eps is inclusive of circumference
    core, _ = dbscan([[0], [1], [1]], eps=1, min_samples=2)
    core, _ = dbscan([[0], [1], [1]], eps=0.99, min_samples=2)
    assert 0 not in core

def test_weighted_dbscan(global_random_seed):
    # ensure sample_weight is validated
        dbscan([[0], [1]], sample_weight=[2])
        dbscan([[0], [1]], sample_weight=[2, 3, 4])

    # ensure sample_weight has an effect
    assert_array_equal([], dbscan([[0], [1]], sample_weight=None, min_samples=6)[0])
    assert_array_equal([], dbscan([[0], [1]], sample_weight=[5, 5], min_samples=6)[0])
    assert_array_equal([0], dbscan([[0], [1]], sample_weight=[6, 5], min_samples=6)[0])
        [0, 1], dbscan([[0], [1]], sample_weight=[6, 6], min_samples=6)[0]
    )

    # points within eps of each other:
        [0, 1], dbscan([[0], [1]], eps=1.5, sample_weight=[5, 1], min_samples=6)[0]
    )
    # and effect of non-positive and non-integer sample_weight:
        [], dbscan([[0], [1]], sample_weight=[5, 0], eps=1.5, min_samples=6)[0]
    )
        [0, 1], dbscan([[0], [1]], sample_weight=[5.9, 0.1], eps=1.5, min_samples=6)[0]
    )
        [0, 1], dbscan([[0], [1]], sample_weight=[6, 0], eps=1.5, min_samples=6)[0]
    )
        [], dbscan([[0], [1]], sample_weight=[6, -1], eps=1.5, min_samples=6)[0]
    )

    # for non-negative sample_weight, cores should be identical to repetition
    sample_weight = rng.randint(0, 5, X.shape[0])
    core1, label1 = dbscan(X, sample_weight=sample_weight)
    assert len(label1) == len(X)

    X_repeated = np.repeat(X, sample_weight, axis=0)
    core_repeated, label_repeated = dbscan(X_repeated)
    core_repeated_mask = np.zeros(X_repeated.shape[0], dtype=bool)
    core_repeated_mask[core_repeated] = True
    core_mask = np.zeros(X.shape[0], dtype=bool)
    core_mask[core1] = True
    assert_array_equal(np.repeat(core_mask, sample_weight), core_repeated_mask)

    # sample_weight should work with precomputed distance matrix
    core3, label3 = dbscan(D, sample_weight=sample_weight, metric="precomputed")
    assert_array_equal(core1, core3)
    assert_array_equal(label1, label3)

    # sample_weight should work with estimator
    est = DBSCAN().fit(X, sample_weight=sample_weight)
    core4 = est.core_sample_indices_
    label4 = est.labels_
    assert_array_equal(core1, core4)
    assert_array_equal(label1, label4)

    est = DBSCAN()
    label5 = est.fit_predict(X, sample_weight=sample_weight)
    core5 = est.core_sample_indices_
    assert_array_equal(core1, core5)
    assert_array_equal(label1, label5)
    assert_array_equal(label1, est.labels_)

def test_dbscan_core_samples_toy(algorithm):
    X = [[0], [2], [3], [4], [6], [8], [10]]

    # Degenerate case: every sample is a core sample, either with its own
    # cluster or including other close core samples.
    core_samples, labels = dbscan(X, algorithm=algorithm, eps=1, min_samples=1)
    assert_array_equal(core_samples, np.arange(n_samples))
    assert_array_equal(labels, [0, 1, 1, 1, 2, 3, 4])

    # With eps=1 and min_samples=2 only the 3 samples from the denser area
    # are core samples. All other points are isolated and considered noise.
    core_samples, labels = dbscan(X, algorithm=algorithm, eps=1, min_samples=2)
    assert_array_equal(core_samples, [1, 2, 3])
    assert_array_equal(labels, [-1, 0, 0, 0, -1, -1, -1])

    # Only the sample in the middle of the dense area is core. Its two
    # neighbors are edge samples. Remaining samples are noise.
    core_samples, labels = dbscan(X, algorithm=algorithm, eps=1, min_samples=3)
    assert_array_equal(core_samples, [2])

    # It's no longer possible to extract core samples with eps=1:
    # everything is noise.
    core_samples, labels = dbscan(X, algorithm=algorithm, eps=1, min_samples=4)
    assert_array_equal(core_samples, [])
    assert_array_equal(labels, np.full(n_samples, -1.0))

def test_dbscan_precomputed_metric_with_degenerate_input_arrays():
    # see https://github.com/scikit-learn/scikit-learn/issues/4641 for
    X = np.eye(10)
    labels = DBSCAN(eps=0.5, metric="precomputed").fit(X).labels_
    assert len(set(labels)) == 1

def test_dbscan_precomputed_metric_with_initial_rows_zero(csr_container):
    # sample matrix with initial two row all zero
    ar = np.array(
        [
            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0],
            [0.0, 0.0, 0.1, 0.1, 0.0, 0.0, 0.3],
            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1],
            [0.0, 0.0, 0.0, 0.0, 0.3, 0.1, 0.0],
        ]
    )
    matrix = csr_container(ar)
    labels = DBSCAN(eps=0.2, metric="precomputed", min_samples=2).fit(matrix).labels_
    assert_array_equal(labels, [-1, -1, 0, 0, 0, 1, 1])

def test_linkage_misc():
    # Misc tests on linkage

        linkage_tree(X, linkage="foo")

        linkage_tree(X, connectivity=np.ones((4, 4)))

    # Smoke test FeatureAgglomeration
    FeatureAgglomeration().fit(X)

    # test hierarchical clustering on a precomputed distances matrix
    dis = cosine_distances(X)

    res = linkage_tree(dis, affinity="precomputed")
    assert_array_equal(res[0], linkage_tree(X, affinity="cosine")[0])

    res = linkage_tree(X, affinity=manhattan_distances)
    assert_array_equal(res[0], linkage_tree(X, affinity="manhattan")[0])

def test_structured_linkage_tree():
    # Check that we obtain the correct solution for structured linkage trees.
    mask = np.ones([10, 10], dtype=bool)
    # Avoiding a mask with only 'True' entries
    mask[4:7, 4:7] = 0
    X = rng.randn(50, 100)
    connectivity = grid_to_graph(*mask.shape)
    for tree_builder in _TREE_BUILDERS.values():
        children, n_components, n_leaves, parent = tree_builder(
            X.T, connectivity=connectivity
        )
        n_nodes = 2 * X.shape[1] - 1
        assert len(children) + n_leaves == n_nodes
        # Check that ward_tree raises a ValueError with a connectivity matrix
        # of the wrong shape
            tree_builder(X.T, connectivity=np.ones((4, 4)))
        # Check that fitting with no samples raises an error
            tree_builder(X.T[:0], connectivity=connectivity)

def test_unstructured_linkage_tree():
    # Check that we obtain the correct solution for unstructured linkage trees.
    for this_X in (X, X[0]):
        # With specified a number of clusters just for the sake of
        # raising a warning and testing the warning code
        with ignore_warnings():
            with pytest.warns(UserWarning):
                children, n_nodes, n_leaves, parent = ward_tree(this_X.T, n_clusters=10)

        for this_X in (X, X[0]):
            with ignore_warnings():
                with pytest.warns(UserWarning):
                    children, n_nodes, n_leaves, parent = tree_builder(
                        this_X.T, n_clusters=10
                    )
            n_nodes = 2 * X.shape[1] - 1
            assert len(children) + n_leaves == n_nodes

def test_height_linkage_tree():
    # Check that the height of the results of linkage tree is sorted.
    for linkage_func in _TREE_BUILDERS.values():
        children, n_nodes, n_leaves, parent = linkage_func(
        )

def test_zero_cosine_linkage_tree():
    # Check that zero vectors in X produce an error when
    # 'cosine' affinity is used
    X = np.array([[0, 1], [0, 0]])
    msg = "Cosine affinity cannot be used when X contains zero vectors"
        linkage_tree(X, affinity="cosine")

def test_agglomerative_clustering_distances(
    n_clusters, compute_distances, distance_threshold, linkage
):
    # Check that when `compute_distances` is True or `distance_threshold` is
    # given, the fitted model has an attribute `distances_`.
    X = rng.randn(n_samples, 50)

    clustering = AgglomerativeClustering(
        connectivity=connectivity,
        linkage=linkage,
        distance_threshold=distance_threshold,
        compute_distances=compute_distances,
    )
    clustering.fit(X)
    if compute_distances or (distance_threshold is not None):
        assert hasattr(clustering, "distances_")
        n_children = clustering.children_.shape[0]
        n_nodes = n_children + 1
        assert clustering.distances_.shape == (n_nodes - 1,)
    else:
        assert not hasattr(clustering, "distances_")

def test_agglomerative_clustering(global_random_seed, lil_container):
    # Check that we obtain the correct number of clusters with
    # agglomerative clustering.
    for linkage in ("ward", "complete", "average", "single"):
        clustering = AgglomerativeClustering(
            n_clusters=10, connectivity=connectivity, linkage=linkage
        )
        clustering.fit(X)
        # test caching
        try:
            tempdir = mkdtemp()
            clustering = AgglomerativeClustering(
                n_clusters=10,
                connectivity=connectivity,
                memory=tempdir,
                linkage=linkage,
            )
            clustering.fit(X)
            labels = clustering.labels_
            assert np.size(np.unique(labels)) == 10
        finally:
            shutil.rmtree(tempdir)
        # Turn caching off now
        )
        # Check that we obtain the same solution with early-stopping of the
        # tree building
        clustering.compute_full_tree = False
        assert_almost_equal(normalized_mutual_info_score(clustering.labels_, labels), 1)
        clustering.connectivity = None
        assert np.size(np.unique(clustering.labels_)) == 10
        # Check that we raise a TypeError on dense matrices
            n_clusters=10,
            connectivity=lil_container(connectivity.toarray()[:10, :10]),
        )

    # Test that using ward with another metric than euclidean raises an
    # exception
        n_clusters=10,
        connectivity=connectivity.toarray(),
    )

    # Test using another metric than euclidean works with linkage complete
    for metric in PAIRED_DISTANCES.keys():
        # Compare our (structured) implementation to scipy
            connectivity=np.ones((n_samples, n_samples)),
            linkage="complete",
        )
        clustering2 = AgglomerativeClustering(
            n_clusters=10, connectivity=None, metric=metric, linkage="complete"
        )
        clustering2.fit(X)
            normalized_mutual_info_score(clustering2.labels_, clustering.labels_), 1
        )

    # Test that using a distance matrix (affinity = 'precomputed') has same
    # results (with connectivity constraints)
        n_clusters=10, connectivity=connectivity, linkage="complete"
    )
    X_dist = pairwise_distances(X)
    clustering2 = AgglomerativeClustering(
        linkage="complete",
    )
    clustering2.fit(X_dist)
    assert_array_equal(clustering.labels_, clustering2.labels_)

def test_agglomerative_clustering_memory_mapped():
    """AgglomerativeClustering must work on mem-mapped dataset.

    Non-regression test for issue #19875.
    """
    Xmm = create_memmap_backed_data(rng.randn(50, 100))
    AgglomerativeClustering(metric="euclidean", linkage="single").fit(Xmm)

def test_ward_agglomeration(global_random_seed):
    # Check that we obtain the correct solution in a simplistic case
    agglo = FeatureAgglomeration(n_clusters=5, connectivity=connectivity)
    agglo.fit(X)
    assert np.size(np.unique(agglo.labels_)) == 5

    X_red = agglo.transform(X)
    assert X_red.shape[1] == 5
    X_full = agglo.inverse_transform(X_red)
    assert np.unique(X_full[0]).size == 5
    assert_array_almost_equal(agglo.transform(X_full), X_red)

    # Check that fitting with no samples raises a ValueError
        agglo.fit(X[:0])

def test_single_linkage_clustering():
    # Check that we get the correct result in two emblematic cases
    moons, moon_labels = make_moons(noise=0.05, random_state=42)
    clustering = AgglomerativeClustering(n_clusters=2, linkage="single")
    clustering.fit(moons)
        normalized_mutual_info_score(clustering.labels_, moon_labels), 1
    )

    circles, circle_labels = make_circles(factor=0.5, noise=0.025, random_state=42)
    clustering.fit(circles)
        normalized_mutual_info_score(clustering.labels_, circle_labels), 1
    )

def assess_same_labelling(cut1, cut2):
    """Util for comparison with scipy"""
    co_clust = []
    for cut in [cut1, cut2]:
        n = len(cut)
        k = cut.max() + 1
        ecut = np.zeros((n, k))
        ecut[np.arange(n), cut] = 1
        co_clust.append(np.dot(ecut, ecut.T))
    assert (co_clust[0] == co_clust[1]).all()

def test_sparse_scikit_vs_scipy(global_random_seed):
    # Test scikit linkage with full connectivity (i.e. unstructured) vs scipy
    n, p, k = 10, 5, 3

    # Not using a lil_matrix here, just to check that non sparse
    # matrices are well handled
    connectivity = np.ones((n, n))
    for linkage in _TREE_BUILDERS.keys():
            X = 0.1 * rng.normal(size=(n, p))
            X -= 4.0 * np.arange(n)[:, np.newaxis]
            X -= X.mean(axis=1)[:, np.newaxis]

            out = hierarchy.linkage(X, method=linkage)

            children_ = out[:, :2].astype(int, copy=False)
            children, _, n_leaves, _ = _TREE_BUILDERS[linkage](
                X, connectivity=connectivity
            )

            # Sort the order of child nodes per row for consistency
            children.sort(axis=1)
                children,
                children_,
                "linkage tree differs from scipy impl for linkage: " + linkage,
            )

            cut = _hc_cut(k, children, n_leaves)
            cut_ = _hc_cut(k, children_, n_leaves)
            assess_same_labelling(cut, cut_)

    # Test error management in _hc_cut
        _hc_cut(n_leaves + 1, children, n_leaves)

def test_vector_scikit_single_vs_scipy_single(global_random_seed):
    n_samples, n_features, n_clusters = 10, 5, 3
    X = 0.1 * rng.normal(size=(n_samples, n_features))
    X -= 4.0 * np.arange(n_samples)[:, np.newaxis]
    X -= X.mean(axis=1)[:, np.newaxis]

    out = hierarchy.linkage(X, method="single")
    children_scipy = out[:, :2].astype(int)

    children, _, n_leaves, _ = _TREE_BUILDERS["single"](X)

    # Sort the order of child nodes per row for consistency
    children.sort(axis=1)
        children,
        children_scipy,
        "linkage tree differs from scipy impl for single linkage.",
    )

    cut = _hc_cut(n_clusters, children, n_leaves)
    cut_scipy = _hc_cut(n_clusters, children_scipy, n_leaves)
    assess_same_labelling(cut, cut_scipy)

def test_mst_linkage_core_memory_mapped(metric_param_grid):
    """The MST-LINKAGE-CORE algorithm must work on mem-mapped dataset.

    """
    rng = np.random.RandomState(seed=1)
    X = rng.normal(size=(20, 4))
    Xmm = create_memmap_backed_data(X)
        distance_metric = DistanceMetric.get_metric(metric, **kwargs)
        mst = mst_linkage_core(X, distance_metric)
        mst_mm = mst_linkage_core(Xmm, distance_metric)
        np.testing.assert_equal(mst, mst_mm)

def test_identical_points():
    # Ensure identical points are handled correctly when using mst with
    # a sparse connectivity matrix
    X = np.array([[0, 0, 0], [0, 0, 0], [1, 1, 1], [1, 1, 1], [2, 2, 2], [2, 2, 2]])
    true_labels = np.array([0, 0, 1, 1, 2, 2])
    connectivity = kneighbors_graph(X, n_neighbors=3, include_self=False)
    connectivity = 0.5 * (connectivity + connectivity.T)
    connectivity, n_components = _fix_connectivity(X, connectivity, "euclidean")

    for linkage in ("single", "average", "average", "ward"):
            n_clusters=3, linkage=linkage, connectivity=connectivity
        )

            normalized_mutual_info_score(clustering.labels_, true_labels), 1
        )

def test_connectivity_propagation():
    # Check that connectivity in the ward tree is propagated correctly during
    # merging.
        [
            (0.014, 0.120),
            (0.014, 0.099),
            (0.014, 0.097),
            (0.017, 0.153),
            (0.018, 0.153),
            (0.018, 0.152),
            (0.018, 0.149),
            (0.018, 0.144),
        ]
    )
    connectivity = kneighbors_graph(X, 10, include_self=False)
    ward = AgglomerativeClustering(
        n_clusters=4, connectivity=connectivity, linkage="ward"
    )
    # If changes are not propagated correctly, fit crashes with an
    # IndexError
    ward.fit(X)

def test_ward_tree_children_order(global_random_seed):
    # Check that children are ordered in the same way for both structured and
    # unstructured versions of ward_tree.

    # test on five random datasets
    n, p = 10, 5

    for i in range(5):
        X = 0.1 * rng.normal(size=(n, p))
        X -= 4.0 * np.arange(n)[:, np.newaxis]
        X -= X.mean(axis=1)[:, np.newaxis]

        out_unstructured = ward_tree(X)
        out_structured = ward_tree(X, connectivity=connectivity)

        assert_array_equal(out_unstructured[0], out_structured[0])

def test_ward_linkage_tree_return_distance(global_random_seed):
    # Test return_distance option on linkage and ward trees

    # test that return_distance when set true, gives same
    # output on both structured and unstructured clustering.
    n, p = 10, 5

        out_unstructured = ward_tree(X, return_distance=True)
        out_structured = ward_tree(X, connectivity=connectivity, return_distance=True)

        # get children
        children_unstructured = out_unstructured[0]
        children_structured = out_structured[0]

        # check if we got the same clusters
        assert_array_equal(children_unstructured, children_structured)

        # check if the distances are the same
        dist_unstructured = out_unstructured[-1]
        dist_structured = out_structured[-1]

        assert_array_almost_equal(dist_unstructured, dist_structured)

        for linkage in ["average", "complete", "single"]:
            structured_items = linkage_tree(
                X, connectivity=connectivity, linkage=linkage, return_distance=True
            )[-1]
            unstructured_items = linkage_tree(X, linkage=linkage, return_distance=True)[
                -1
            ]
            structured_dist = structured_items[-1]
            unstructured_dist = unstructured_items[-1]
            structured_children = structured_items[0]
            unstructured_children = unstructured_items[0]
            assert_array_almost_equal(structured_dist, unstructured_dist)
            assert_array_almost_equal(structured_children, unstructured_children)

    # test on the following dataset where we know the truth
    # taken from scipy/cluster/tests/hierarchy_test_data.py
        [
            [1.43054825, -7.5693489],
            [6.95887839, 6.82293382],
            [2.87137846, -9.68248579],
            [7.87974764, -6.05485803],
            [8.24018364, -6.09495602],
            [7.39020262, 8.54004355],
        ]
    )
    # truth
    linkage_X_ward = np.array(
        [
            [3.0, 4.0, 0.36265956, 2.0],
            [1.0, 5.0, 1.77045373, 2.0],
            [0.0, 2.0, 2.55760419, 2.0],
            [6.0, 8.0, 9.10208346, 4.0],
            [7.0, 9.0, 24.7784379, 6.0],
        ]
    )

    linkage_X_complete = np.array(
        [
            [6.0, 8.0, 6.96742194, 4.0],
            [7.0, 9.0, 18.77445997, 6.0],
        ]
    )

    linkage_X_average = np.array(
        [
            [6.0, 8.0, 6.55832839, 4.0],
            [7.0, 9.0, 15.44089605, 6.0],
        ]
    )

    n_samples, n_features = np.shape(X)
    connectivity_X = np.ones((n_samples, n_samples))

    out_X_unstructured = ward_tree(X, return_distance=True)
    out_X_structured = ward_tree(X, connectivity=connectivity_X, return_distance=True)

    # check that the labels are the same
    assert_array_equal(linkage_X_ward[:, :2], out_X_unstructured[0])
    assert_array_equal(linkage_X_ward[:, :2], out_X_structured[0])

    # check that the distances are correct
    assert_array_almost_equal(linkage_X_ward[:, 2], out_X_unstructured[4])
    assert_array_almost_equal(linkage_X_ward[:, 2], out_X_structured[4])

    linkage_options = ["complete", "average", "single"]
    X_linkage_truth = [linkage_X_complete, linkage_X_average]
    for linkage, X_truth in zip(linkage_options, X_linkage_truth):
        out_X_unstructured = linkage_tree(X, return_distance=True, linkage=linkage)
        out_X_structured = linkage_tree(
            X, connectivity=connectivity_X, linkage=linkage, return_distance=True
        )

        # check that the labels are the same
        assert_array_equal(X_truth[:, :2], out_X_unstructured[0])
        assert_array_equal(X_truth[:, :2], out_X_structured[0])

        # check that the distances are correct
        assert_array_almost_equal(X_truth[:, 2], out_X_unstructured[4])
        assert_array_almost_equal(X_truth[:, 2], out_X_structured[4])

def test_connectivity_fixing_non_lil():
    # Check non regression of a bug if a non item assignable connectivity is
    # provided with more than one component.
    # create dummy data
    x = np.array([[0, 0], [1, 1]])
    # create a mask with several components to force connectivity fixing
    m = np.array([[True, False], [False, True]])
    c = grid_to_graph(n_x=2, n_y=2, mask=m)
    w = AgglomerativeClustering(connectivity=c, linkage="ward")
        w.fit(x)

def test_int_float_dict():
    keys = np.unique(rng.randint(100, size=10).astype(np.intp, copy=False))
    values = rng.rand(len(keys))

    d = IntFloatDict(keys, values)
    for key, value in zip(keys, values):
        assert d[key] == value

    other_keys = np.arange(50, dtype=np.intp)[::2]
    other_values = np.full(50, 0.5)[::2]
    other = IntFloatDict(other_keys, other_values)
    # Complete smoke test
    max_merge(d, other, mask=np.ones(100, dtype=np.intp), n_a=1, n_b=1)
    average_merge(d, other, mask=np.ones(100, dtype=np.intp), n_a=1, n_b=1)

def test_connectivity_callable():
    X = rng.rand(20, 5)
    connectivity = kneighbors_graph(X, 3, include_self=False)
    aglc1 = AgglomerativeClustering(connectivity=connectivity)
    aglc2 = AgglomerativeClustering(
        connectivity=partial(kneighbors_graph, n_neighbors=3, include_self=False)
    )
    aglc1.fit(X)
    aglc2.fit(X)
    assert_array_equal(aglc1.labels_, aglc2.labels_)

def test_connectivity_ignores_diagonal():
    connectivity_include_self = kneighbors_graph(X, 3, include_self=True)
    aglc2 = AgglomerativeClustering(connectivity=connectivity_include_self)

def test_compute_full_tree():
    # Test that the full tree is computed if n_clusters is small
    connectivity = kneighbors_graph(X, 5, include_self=False)

    # When n_clusters is less, the full tree should be built
    # that is the number of merges should be n_samples - 1
    agc = AgglomerativeClustering(n_clusters=2, connectivity=connectivity)
    agc.fit(X)
    n_nodes = agc.children_.shape[0]
    assert n_nodes == n_samples - 1

    # When n_clusters is large, greater than max of 100 and 0.02 * n_samples.
    # we should stop when there are n_clusters.
    n_clusters = 101
    X = rng.randn(200, 2)
    agc = AgglomerativeClustering(n_clusters=n_clusters, connectivity=connectivity)
    assert n_nodes == n_samples - n_clusters

    # Test n_components returned by linkage, average and ward tree

    # Connectivity matrix having five components.
    connectivity = np.eye(5)

        assert ignore_warnings(linkage_func)(X, connectivity=connectivity)[1] == 5

def test_affinity_passed_to_fix_connectivity():
    # Test that the affinity parameter is actually passed to the pairwise
    # function

    size = 2
    X = rng.randn(size, size)
    mask = np.array([True, False, False, True])

    connectivity = grid_to_graph(n_x=size, n_y=size, mask=mask, return_as=np.ndarray)

    class FakeAffinity:
            self.counter = 0

        def increment(self, *args, **kwargs):
            self.counter += 1
            return self.counter

    fa = FakeAffinity()

    linkage_tree(X, connectivity=connectivity, affinity=fa.increment)

    assert fa.counter == 3

def test_agglomerative_clustering_with_distance_threshold(linkage, global_random_seed):
    # agglomerative clustering with distance_threshold.
    # test when distance threshold is set to 10
    distance_threshold = 10
    for conn in [None, connectivity]:
            n_clusters=None,
            connectivity=conn,
        )
        clusters_produced = clustering.labels_
        num_clusters_produced = len(np.unique(clustering.labels_))
        # test if the clusters produced match the point in the linkage tree
        # where the distance exceeds the threshold
        tree_builder = _TREE_BUILDERS[linkage]
        children, n_components, n_leaves, parent, distances = tree_builder(
            X, connectivity=conn, n_clusters=None, return_distance=True
        )
        num_clusters_at_threshold = (
            np.count_nonzero(distances >= distance_threshold) + 1
        )
        # test number of clusters produced
        assert num_clusters_at_threshold == num_clusters_produced
        # test clusters produced
        clusters_at_threshold = _hc_cut(
            n_clusters=num_clusters_produced, children=children, n_leaves=n_leaves
        )
        assert np.array_equiv(clusters_produced, clusters_at_threshold)

def test_small_distance_threshold(global_random_seed):
    X = rng.randint(-300, 300, size=(n_samples, 3))
    # this should result in all data in their own clusters, given that
    # their pairwise distances are bigger than .1 (which may not be the case
    # with a different random seed).
        n_clusters=None, distance_threshold=1.0, linkage="single"
    ).fit(X)
    # check that the pairwise distances are indeed all larger than .1
    all_distances = pairwise_distances(X, metric="minkowski", p=2)
    np.fill_diagonal(all_distances, np.inf)
    assert np.all(all_distances > 0.1)
    assert clustering.n_clusters_ == n_samples

def test_cluster_distances_with_distance_threshold(global_random_seed):
    X = rng.randint(-10, 10, size=(n_samples, 3))
    # check the distances within the clusters and with other clusters
    distance_threshold = 4
        n_clusters=None, distance_threshold=distance_threshold, linkage="single"
    ).fit(X)
    labels = clustering.labels_
    D = pairwise_distances(X, metric="minkowski", p=2)
    # to avoid taking the 0 diagonal in min()
    np.fill_diagonal(D, np.inf)
    for label in np.unique(labels):
        in_cluster_mask = labels == label
        max_in_cluster_distance = (
            D[in_cluster_mask][:, in_cluster_mask].min(axis=0).max()
        )
        min_out_cluster_distance = (
            D[in_cluster_mask][:, ~in_cluster_mask].min(axis=0).min()
        )
        # single data point clusters only have that inf diagonal here
        if in_cluster_mask.sum() > 1:
            assert max_in_cluster_distance < distance_threshold
        assert min_out_cluster_distance >= distance_threshold

def test_agglomerative_clustering_with_distance_threshold_edge_case(
    linkage, threshold, y_true
):
    # test boundary case of distance_threshold matching the distance
    clusterer = AgglomerativeClustering(
        n_clusters=None, distance_threshold=threshold, linkage=linkage
    )
    y_pred = clusterer.fit_predict(X)
    assert adjusted_rand_score(y_true, y_pred) == 1

def test_dist_threshold_invalid_parameters():
    with pytest.raises(ValueError, match="Exactly one of "):
        AgglomerativeClustering(n_clusters=None, distance_threshold=None).fit(X)

        AgglomerativeClustering(n_clusters=2, distance_threshold=1).fit(X)

    with pytest.raises(ValueError, match="compute_full_tree must be True if"):
        AgglomerativeClustering(
            n_clusters=None, distance_threshold=1, compute_full_tree=False
        ).fit(X)

def test_invalid_shape_precomputed_dist_matrix():
    # Check that an error is raised when affinity='precomputed'
    # and a non square matrix is passed (PR #16257).
    X = rng.rand(5, 3)
        match=r"Distance matrix should be square, got matrix of shape \(5, 3\)",
    ):
        AgglomerativeClustering(metric="precomputed", linkage="complete").fit(X)

def test_precomputed_connectivity_metric_with_2_connected_components():
    """Check that connecting components works when connectivity and
    affinity are both precomputed and the number of connected components is
    greater than 1. Non-regression test for #16151.
    """

    connectivity_matrix = np.array(
        [
            [0, 0, 1, 0, 0],
            [0, 0, 0, 0, 0],
            [0, 0, 0, 0, 1],
        ]
    )
    # ensure that connectivity_matrix has two connected components
    assert connected_components(connectivity_matrix)[0] == 2

    X = rng.randn(5, 10)

    clusterer_precomputed = AgglomerativeClustering(
        metric="precomputed", connectivity=connectivity_matrix, linkage="complete"
    )
    msg = "Completing it to avoid stopping the tree early"
        clusterer_precomputed.fit(X_dist)

        connectivity=connectivity_matrix, linkage="complete"
    )
        clusterer.fit(X)

    assert_array_equal(clusterer.labels_, clusterer_precomputed.labels_)
    assert_array_equal(clusterer.children_, clusterer_precomputed.children_)

def test_three_clusters(bisecting_strategy, init):
    """Tries to perform bisect k-means for three clusters to check
    if splitting data is performed correctly.
    """
        [[1, 1], [10, 1], [3, 1], [10, 0], [2, 1], [10, 2], [10, 8], [10, 9], [10, 10]]
    )
    bisect_means = BisectingKMeans(
        bisecting_strategy=bisecting_strategy,
    )
    bisect_means.fit(X)

    expected_centers = [[2, 1], [10, 1], [10, 9]]
    expected_labels = [0, 1, 0, 1, 0, 1, 2, 2, 2]

        sorted(expected_centers), sorted(bisect_means.cluster_centers_.tolist())
    )
    assert_allclose(v_measure_score(expected_labels, bisect_means.labels_), 1.0)

    """Test Bisecting K-Means with sparse data.

    Checks if labels and centers are the same between dense and sparse.
    """

    bisect_means = BisectingKMeans(n_clusters=3, random_state=0)

    bisect_means.fit(X_csr)
    sparse_centers = bisect_means.cluster_centers_

    normal_centers = bisect_means.cluster_centers_

    # Check if results is the same for dense and sparse data
    assert_allclose(normal_centers, sparse_centers, atol=1e-8)

def test_n_clusters(n_clusters):
    """Test if resulting labels are in range [0, n_clusters - 1]."""

    X = rng.rand(10, 2)

    bisect_means = BisectingKMeans(n_clusters=n_clusters, random_state=0)

    assert_array_equal(np.unique(bisect_means.labels_), np.arange(n_clusters))

def test_one_cluster():
    """Test single cluster."""

    X = np.array([[1, 2], [10, 2], [10, 8]])

    bisect_means = BisectingKMeans(n_clusters=1, random_state=0).fit(X)

    # All labels from fit or predict should be equal 0
    assert all(bisect_means.labels_ == 0)
    assert all(bisect_means.predict(X) == 0)

    assert_allclose(bisect_means.cluster_centers_, X.mean(axis=0).reshape(1, -1))

def test_fit_predict(csr_container):
    """Check if labels from fit(X) method are same as from fit(X).predict(X)."""

        X[X < 0.8] = 0
        X = csr_container(X)

    assert_array_equal(bisect_means.labels_, bisect_means.predict(X))

def test_dtype_preserved(csr_container, global_dtype):
    """Check that centers dtype is the same as input data dtype."""
    X = rng.rand(10, 2).astype(global_dtype, copy=False)

    km = BisectingKMeans(n_clusters=3, random_state=0)
    km.fit(X)

    assert km.cluster_centers_.dtype == global_dtype

def test_float32_float64_equivalence(csr_container):
    """Check that the results are the same between float32 and float64."""

    km64 = BisectingKMeans(n_clusters=3, random_state=0).fit(X)
    km32 = BisectingKMeans(n_clusters=3, random_state=0).fit(X.astype(np.float32))

    assert_allclose(km32.cluster_centers_, km64.cluster_centers_)
    assert_array_equal(km32.labels_, km64.labels_)

def test_no_crash_on_empty_bisections(algorithm):
    # https://github.com/scikit-learn/scikit-learn/issues/27081
    X_train = rng.rand(3000, 10)
    bkm = BisectingKMeans(n_clusters=10, algorithm=algorithm).fit(X_train)

    # predict on scaled data to trigger pathologic case
    # where the inner mask leads to empty bisections.
    X_test = 50 * rng.rand(100, 10)
    labels = bkm.predict(X_test)  # should not crash with idiv by 0
    assert np.isin(np.unique(labels), np.arange(10)).all()

def test_one_feature():
    # Check that no error is raised when there is only one feature
    # https://github.com/scikit-learn/scikit-learn/issues/27236
    X = np.random.normal(size=(128, 1))
    BisectingKMeans(bisecting_strategy="biggest_inertia", random_state=0).fit(X)

def test_kmeans_results(array_constr, algo, dtype):
    # Checks that KMeans works as intended on toy dataset by comparing with
    # expected results computed by hand.
    X = array_constr([[0, 0], [0.5, 0], [0.5, 1], [1, 1]], dtype=dtype)
    sample_weight = [3, 1, 1, 3]
    init_centers = np.array([[0, 0], [1, 1]], dtype=dtype)

    expected_labels = [0, 0, 1, 1]
    expected_inertia = 0.375
    expected_centers = np.array([[0.125, 0], [0.875, 1]], dtype=dtype)
    expected_n_iter = 2

    kmeans = KMeans(n_clusters=2, n_init=1, init=init_centers, algorithm=algo)
    kmeans.fit(X, sample_weight=sample_weight)

    assert_array_equal(kmeans.labels_, expected_labels)
    assert_allclose(kmeans.inertia_, expected_inertia)
    assert_allclose(kmeans.cluster_centers_, expected_centers)
    assert kmeans.n_iter_ == expected_n_iter

def test_kmeans_relocated_clusters(array_constr, algo):
    # check that empty clusters are relocated as expected
    X = array_constr([[0, 0], [0.5, 0], [0.5, 1], [1, 1]])

    # second center too far from others points will be empty at first iter
    init_centers = np.array([[0.5, 0.5], [3, 3]])

    kmeans.fit(X)

    expected_n_iter = 3
    expected_inertia = 0.25

    # There are two acceptable ways of relocating clusters in this example, the output
    # depends on how the argpartition strategy breaks ties. We accept both outputs.
    try:
        expected_labels = [0, 0, 1, 1]
        expected_centers = [[0.25, 0], [0.75, 1]]
        assert_array_equal(kmeans.labels_, expected_labels)
        assert_allclose(kmeans.cluster_centers_, expected_centers)
    except AssertionError:
        expected_labels = [1, 1, 0, 0]
        expected_centers = [[0.75, 1.0], [0.25, 0.0]]

def test_relocate_empty_clusters(array_constr):
    # test for the _relocate_empty_clusters_(dense/sparse) helpers

    # Synthetic dataset with 3 obvious clusters of different sizes
    X = np.array([-10.0, -9.5, -9, -8.5, -8, -1, 1, 9, 9.5, 10]).reshape(-1, 1)
    X = array_constr(X)
    sample_weight = np.ones(10)

    # centers all initialized to the first point of X
    centers_old = np.array([-10.0, -10, -10]).reshape(-1, 1)

    # With this initialization, all points will be assigned to the first center
    # At this point a center in centers_new is the weighted sum of the points
    # it contains if it's not empty, otherwise it is the same as before.
    centers_new = np.array([-16.5, -10, -10]).reshape(-1, 1)
    weight_in_clusters = np.array([10.0, 0, 0])
    labels = np.zeros(10, dtype=np.int32)

    if array_constr is np.array:
        _relocate_empty_clusters_dense(
            X, sample_weight, centers_old, centers_new, weight_in_clusters, labels
        )
    else:
        _relocate_empty_clusters_sparse(
            X.data,
            X.indptr,
            centers_old,
            labels,
        )

    # The relocation scheme will take the 2 points farthest from the center and
    # assign them to the 2 empty clusters, i.e. points at 10 and at 9.9. The
    # first center will be updated to contain the other 8 points.
    assert_array_equal(weight_in_clusters, [8, 1, 1])
    assert_allclose(centers_new, [[-36], [10], [9.5]])

def test_kmeans_elkan_results(distribution, array_constr, tol, global_random_seed):
    # Check that results are identical between lloyd and elkan algorithms
    if distribution == "normal":
        X = rnd.normal(size=(5000, 10))
    else:
        X, _ = make_blobs(random_state=rnd)
    X[X < 0] = 0

    km_lloyd = KMeans(n_clusters=5, random_state=global_random_seed, n_init=1, tol=tol)
    km_elkan = KMeans(
        algorithm="elkan",
        n_clusters=5,
        n_init=1,
        tol=tol,
    )

    km_lloyd.fit(X)
    km_elkan.fit(X)
    assert_allclose(km_elkan.cluster_centers_, km_lloyd.cluster_centers_)
    assert_array_equal(km_elkan.labels_, km_lloyd.labels_)
    assert km_elkan.n_iter_ == km_lloyd.n_iter_
    assert km_elkan.inertia_ == pytest.approx(km_lloyd.inertia_, rel=1e-6)

def test_kmeans_convergence(algorithm, global_random_seed):
    # Check that KMeans stops when convergence is reached when tol=0. (#16075)
    X = rnd.normal(size=(5000, 10))

    km = KMeans(
        n_init=1,
        tol=0,
    ).fit(X)

    assert km.n_iter_ < max_iter

def test_minibatch_update_consistency(X_csr, global_random_seed):
    # Check that dense and sparse minibatch update give the same results

    centers_old = centers + rng.normal(size=centers.shape)
    centers_old_csr = centers_old.copy()

    centers_new = np.zeros_like(centers_old)
    centers_new_csr = np.zeros_like(centers_old_csr)

    weight_sums = np.zeros(centers_old.shape[0], dtype=X.dtype)
    weight_sums_csr = np.zeros(centers_old.shape[0], dtype=X.dtype)

    sample_weight = np.ones(X.shape[0], dtype=X.dtype)

    # extract a small minibatch
    X_mb = X[:10]
    X_mb_csr = X_csr[:10]
    sample_weight_mb = sample_weight[:10]

    # step 1: compute the dense minibatch update
    old_inertia = _mini_batch_step(
        X_mb,
        sample_weight_mb,
        centers_old,
        centers_new,
        weight_sums,
        np.random.RandomState(global_random_seed),
        random_reassign=False,
    )
    assert old_inertia > 0.0

    # compute the new inertia on the same batch to check that it decreased
    labels, new_inertia = _labels_inertia(X_mb, sample_weight_mb, centers_new)
    assert new_inertia > 0.0
    assert new_inertia < old_inertia

    # step 2: compute the sparse minibatch update
    old_inertia_csr = _mini_batch_step(
        X_mb_csr,
        centers_old_csr,
        centers_new_csr,
        weight_sums_csr,
    )
    assert old_inertia_csr > 0.0

    labels_csr, new_inertia_csr = _labels_inertia(
        X_mb_csr, sample_weight_mb, centers_new_csr
    )
    assert new_inertia_csr > 0.0
    assert new_inertia_csr < old_inertia_csr

    # step 3: check that sparse and dense updates lead to the same results
    assert_array_equal(labels, labels_csr)
    assert_allclose(centers_new, centers_new_csr)
    assert_allclose(old_inertia, old_inertia_csr)
    assert_allclose(new_inertia, new_inertia_csr)

def _check_fitted_model(km):
    # check that the number of clusters centers and distinct labels match
    # the expectation
    centers = km.cluster_centers_
    assert centers.shape == (n_clusters, n_features)

    labels = km.labels_
    assert np.unique(labels).shape[0] == n_clusters

    # check that the labels assignment are perfect (up to a permutation)
    assert_allclose(v_measure_score(true_labels, labels), 1.0)
    assert km.inertia_ > 0.0

def test_all_init(Estimator, input_data, init):
    # Check KMeans and MiniBatchKMeans with all possible init.
    n_init = 10 if isinstance(init, str) else 1
    km = Estimator(
        init=init, n_clusters=n_clusters, random_state=42, n_init=n_init
    ).fit(input_data)
    _check_fitted_model(km)

def test_minibatch_kmeans_partial_fit_init(init):
    # Check MiniBatchKMeans init with partial_fit
    km = MiniBatchKMeans(
        init=init, n_clusters=n_clusters, random_state=0, n_init=n_init
    )
        # "random" init requires many batches to recover the true labels.
        km.partial_fit(X)

def test_kmeans_init_auto_with_initial_centroids(Estimator, init, expected_n_init):
    """Check that `n_init="auto"` chooses the right number of initializations.
    Non-regression test for #26657:
    https://github.com/scikit-learn/scikit-learn/pull/26657
    """
    n_sample, n_features, n_clusters = 100, 10, 5
    X = np.random.randn(n_sample, n_features)
    if init == "array-like":
        init = np.random.randn(n_clusters, n_features)
    if expected_n_init == "default":
        expected_n_init = 3 if Estimator is MiniBatchKMeans else 10

    kmeans = Estimator(n_clusters=n_clusters, init=init, n_init="auto").fit(X)
    assert kmeans._n_init == expected_n_init

def test_fortran_aligned_data(Estimator, global_random_seed):
    # Check that KMeans works with fortran-aligned data.
    centers_fortran = np.asfortranarray(centers)

    km_c = Estimator(
        n_clusters=n_clusters, init=centers, n_init=1, random_state=global_random_seed
    ).fit(X)
    km_f = Estimator(
        init=centers_fortran,
        n_init=1,
    ).fit(X_fortran)
    assert_allclose(km_c.cluster_centers_, km_f.cluster_centers_)
    assert_array_equal(km_c.labels_, km_f.labels_)

def test_minibatch_kmeans_verbose():
    # Check verbose mode of MiniBatchKMeans for better coverage.
    km = MiniBatchKMeans(n_clusters=n_clusters, random_state=42, verbose=1)
    try:
        km.fit(X)
    finally:

def test_kmeans_verbose(algorithm, tol, capsys):
    # Check verbose mode of KMeans for better coverage.
    X = np.random.RandomState(0).normal(size=(5000, 10))

    KMeans(
        n_init=1,
        tol=tol,
    ).fit(X)

    assert re.search(r"Initialization complete", captured.out)
    assert re.search(r"Iteration [0-9]+, inertia", captured.out)

    if tol == 0:
        assert re.search(r"strict convergence", captured.out)
    else:
        assert re.search(r"center shift .* within tolerance", captured.out)

def test_minibatch_kmeans_warning_init_size():
    # Check that a warning is raised when init_size is smaller than n_clusters
        RuntimeWarning, match=r"init_size.* should be larger than n_clusters"
    ):
        MiniBatchKMeans(init_size=10, n_clusters=20).fit(X)

def test_warning_n_init_precomputed_centers(Estimator):
    # Check that a warning is raised when n_init > 1 and an array is passed for
    # the init parameter.
        match="Explicit initial center position passed: performing only one init",
    ):
        Estimator(init=centers, n_clusters=n_clusters, n_init=10).fit(X)

def test_minibatch_sensible_reassign(global_random_seed):
    # check that identical initial clusters are reassigned
    # also a regression test for when there are more desired reassignments than
    # samples.
    zeroed_X, true_labels = make_blobs(
        n_samples=100, centers=5, random_state=global_random_seed
    )
    zeroed_X[::2, :] = 0

        n_clusters=20, batch_size=10, random_state=global_random_seed, init="random"
    ).fit(zeroed_X)
    # there should not be too many exact zero cluster centers
    num_non_zero_clusters = km.cluster_centers_.any(axis=1).sum()
    assert num_non_zero_clusters > 9, f"{num_non_zero_clusters=} is too small"

    # do the same with batch-size > X.shape[0] (regression test)
        n_clusters=20, batch_size=200, random_state=global_random_seed, init="random"

    # do the same with partial_fit API
    km = MiniBatchKMeans(n_clusters=20, random_state=global_random_seed, init="random")
        km.partial_fit(zeroed_X)

def test_minibatch_reassign(input_data, global_random_seed):
    # Check the reassignment part of the minibatch step with very high or very
    # low reassignment ratio.
    perfect_centers = np.empty((n_clusters, n_features))
        perfect_centers[i] = X[true_labels == i].mean(axis=0)

    sample_weight = np.ones(n_samples)
    centers_new = np.empty_like(perfect_centers)

    # Give a perfect initialization, but a large reassignment_ratio, as a
    # result many centers should be reassigned and the model should no longer
    # be good
    score_before = -_labels_inertia(input_data, sample_weight, perfect_centers, 1)[1]

    _mini_batch_step(
        input_data,
        perfect_centers,
        np.zeros(n_clusters),
        random_reassign=True,
        reassignment_ratio=1,
    )

    score_after = -_labels_inertia(input_data, sample_weight, centers_new, 1)[1]

    assert score_before > score_after

    # Give a perfect initialization, with a small reassignment_ratio,
    # no center should be reassigned.
        reassignment_ratio=1e-15,
    )

    assert_allclose(centers_new, perfect_centers)

def test_minibatch_with_many_reassignments():
    # Test for the case that the number of clusters to reassign is bigger
    # than the batch_size. Run the test with 100 clusters and a batch_size of
    # 10 because it turned out that these values ensure that the number of
    # clusters to reassign is always bigger than the batch_size.
    MiniBatchKMeans(
        n_clusters=100,
        init_size=n_samples,
    ).fit(X)

def test_minibatch_kmeans_init_size():
    # Check the internal _init_size attribute of MiniBatchKMeans

    # default init size should be 3 * batch_size
    km = MiniBatchKMeans(n_clusters=10, batch_size=5, n_init=1).fit(X)
    assert km._init_size == 15

    # if 3 * batch size < n_clusters, it should then be 3 * n_clusters
    km = MiniBatchKMeans(n_clusters=10, batch_size=1, n_init=1).fit(X)
    assert km._init_size == 30

    # it should not be larger than n_samples
        n_clusters=10, batch_size=5, n_init=1, init_size=n_samples + 1
    ).fit(X)
    assert km._init_size == n_samples

def test_minibatch_declared_convergence(capsys, tol, max_no_improvement):
    # Check convergence detection based on ewa batch inertia or on
    # small center change.
    X, _, centers = make_blobs(centers=3, random_state=0, return_centers=True)

        init=centers,
        batch_size=20,
        tol=tol,
        n_init=1,
    )

    km.fit(X)
    assert 1 < km.n_iter_ < 10

    if max_no_improvement is None:
        assert "Converged (small centers change)" in captured.out
    if tol == 0:
        assert "Converged (lack of improvement in inertia)" in captured.out

def test_minibatch_iter_steps():
    # Check consistency of n_iter_ and n_steps_ attributes.
    batch_size = 30
    km = MiniBatchKMeans(n_clusters=3, batch_size=batch_size, random_state=0).fit(X)

    # n_iter_ is the number of started epochs
    assert km.n_iter_ == np.ceil((km.n_steps_ * batch_size) / n_samples)
    assert isinstance(km.n_iter_, int)

    # without stopping condition, max_iter should be reached
        tol=0,
    ).fit(X)

    assert km.n_iter_ == 10
    assert km.n_steps_ == (10 * n_samples) // batch_size
    assert isinstance(km.n_steps_, int)

def test_kmeans_copyx():
    # Check that copy_x=False returns nearly equal X after de-centering.
    my_X = X.copy()
    km = KMeans(copy_x=False, n_clusters=n_clusters, random_state=42)
    km.fit(my_X)

    # check that my_X is de-centered
    assert_allclose(my_X, X)

def test_score_max_iter(Estimator, global_random_seed):
    # Check that fitting KMeans or MiniBatchKMeans with more iterations gives
    # better score
    X = np.random.RandomState(global_random_seed).randn(100, 10)

    km1 = Estimator(n_init=1, random_state=global_random_seed, max_iter=1)
    s1 = km1.fit(X).score(X)
    km2 = Estimator(n_init=1, random_state=global_random_seed, max_iter=10)
    s2 = km2.fit(X).score(X)
    assert s2 > s1

def test_kmeans_predict(
    Estimator, algorithm, array_constr, max_iter, global_dtype, global_random_seed
):
    # Check the predict method and the equivalence between fit.predict and
    # fit_predict.
        n_samples=200, n_features=10, centers=10, random_state=global_random_seed
    )
    X = array_constr(X, dtype=global_dtype)

    )
    if algorithm is not None:
        km.set_params(algorithm=algorithm)
    km.fit(X)

    # re-predict labels for training set using predict
    pred = km.predict(X)
    assert_array_equal(pred, labels)

    # re-predict labels for training set using fit_predict
    pred = km.fit_predict(X)

    # predict centroid labels
    pred = km.predict(km.cluster_centers_)
    assert_array_equal(pred, np.arange(10))

def test_dense_sparse(Estimator, X_csr, global_random_seed):
    # Check that the results are the same for dense and sparse input.
    sample_weight = np.random.RandomState(global_random_seed).random_sample(
        (n_samples,)
    )
    km_dense = Estimator(
        n_clusters=n_clusters, random_state=global_random_seed, n_init=1
    )
    km_dense.fit(X, sample_weight=sample_weight)
    km_sparse = Estimator(
    )
    km_sparse.fit(X_csr, sample_weight=sample_weight)

    assert_array_equal(km_dense.labels_, km_sparse.labels_)
    assert_allclose(km_dense.cluster_centers_, km_sparse.cluster_centers_)

def test_predict_dense_sparse(Estimator, init, X_csr):
    # check that models trained on sparse input also works for dense input at
    # predict time and vice versa.
    km = Estimator(n_clusters=n_clusters, init=init, n_init=n_init, random_state=0)

    km.fit(X_csr)
    assert_array_equal(km.predict(X), km.labels_)

    km.fit(X)
    assert_array_equal(km.predict(X_csr), km.labels_)

def test_integer_input(Estimator, array_constr, dtype, init, global_random_seed):
    # Check that KMeans and MiniBatchKMeans work with integer input.
    X_dense = np.array([[0, 0], [10, 10], [12, 9], [-1, 1], [2, 0], [8, 10]])
    X = array_constr(X_dense, dtype=dtype)

    n_init = 1 if init == "ndarray" else 10
    init = X_dense[:2] if init == "ndarray" else init

        n_clusters=2, init=init, n_init=n_init, random_state=global_random_seed
    )
    if Estimator is MiniBatchKMeans:
        km.set_params(batch_size=2)

    km.fit(X)

    # Internally integer input should be converted to float64
    assert km.cluster_centers_.dtype == np.float64

    expected_labels = [0, 1, 1, 0, 0, 1]
    assert_allclose(v_measure_score(km.labels_, expected_labels), 1.0)

    # Same with partial_fit (#14314)
        km = clone(km).partial_fit(X)
        assert km.cluster_centers_.dtype == np.float64

def test_transform(Estimator, global_random_seed):
    # Check the transform method
    km = Estimator(n_clusters=n_clusters, random_state=global_random_seed).fit(X)

    # Transorfming cluster_centers_ should return the pairwise distances
    # between centers
    Xt = km.transform(km.cluster_centers_)
    assert_allclose(Xt, pairwise_distances(km.cluster_centers_))
    # In particular, diagonal must be 0
    assert_array_equal(Xt.diagonal(), np.zeros(n_clusters))

    # Transorfming X should return the pairwise distances between X and the
    # centers
    Xt = km.transform(X)
    assert_allclose(Xt, pairwise_distances(X, km.cluster_centers_))

def test_fit_transform(Estimator, global_random_seed):
    # Check equivalence between fit.transform and fit_transform
    X1 = Estimator(random_state=global_random_seed, n_init=1).fit(X).transform(X)
    X2 = Estimator(random_state=global_random_seed, n_init=1).fit_transform(X)
    assert_allclose(X1, X2)

def test_n_init(global_random_seed):
    # Check that increasing the number of init increases the quality
    previous_inertia = np.inf
    for n_init in [1, 5, 10]:
        # set max_iter=1 to avoid finding the global minimum and get the same
        # inertia each time
        km = KMeans(
        ).fit(X)
        assert km.inertia_ <= previous_inertia

def test_k_means_function(global_random_seed):
    # test calling the k_means function directly
    cluster_centers, labels, inertia = k_means(
        X, n_clusters=n_clusters, sample_weight=None, random_state=global_random_seed
    )

    assert cluster_centers.shape == (n_clusters, n_features)

    assert inertia > 0.0

def test_float_precision(Estimator, input_data, global_random_seed):
    # Check that the results are the same for single and double precision.
    km = Estimator(n_init=1, random_state=global_random_seed)

    inertia = {}
    Xt = {}
    centers = {}
    labels = {}

    for dtype in [np.float64, np.float32]:
        X = input_data.astype(dtype, copy=False)
        km.fit(X)

        inertia[dtype] = km.inertia_
        Xt[dtype] = km.transform(X)
        centers[dtype] = km.cluster_centers_
        labels[dtype] = km.labels_

        # dtype of cluster centers has to be the dtype of the input data
        assert km.cluster_centers_.dtype == dtype

        # same with partial_fit
        if Estimator is MiniBatchKMeans:
            km.partial_fit(X[0:3])
            assert km.cluster_centers_.dtype == dtype

    # compare arrays with low precision since the difference between 32 and
    # 64 bit comes from an accumulation of rounding errors.
    assert_allclose(inertia[np.float32], inertia[np.float64], rtol=1e-4)
    assert_allclose(Xt[np.float32], Xt[np.float64], atol=Xt[np.float64].max() * 1e-4)
        centers[np.float32], centers[np.float64], atol=centers[np.float64].max() * 1e-4
    )
    assert_array_equal(labels[np.float32], labels[np.float64])

def test_centers_not_mutated(Estimator, dtype):
    # Check that KMeans and MiniBatchKMeans won't mutate the user provided
    # init centers silently even if input data and init centers have the same
    # type.
    X_new_type = X.astype(dtype, copy=False)
    centers_new_type = centers.astype(dtype, copy=False)

    km = Estimator(init=centers_new_type, n_clusters=n_clusters, n_init=1)
    km.fit(X_new_type)

    assert not np.may_share_memory(km.cluster_centers_, centers_new_type)

def test_kmeans_init_fitted_centers(input_data):
    # Check that starting fitting from a local optimum shouldn't change the
    # solution
    km1 = KMeans(n_clusters=n_clusters).fit(input_data)
    km2 = KMeans(n_clusters=n_clusters, init=km1.cluster_centers_, n_init=1).fit(
        input_data
    )

    assert_allclose(km1.cluster_centers_, km2.cluster_centers_)

def test_kmeans_warns_less_centers_than_unique_points(global_random_seed):
    # Check KMeans when the number of found clusters is smaller than expected
    X = np.asarray([[0, 0], [0, 1], [1, 0], [1, 0]])  # last point is duplicated
    km = KMeans(n_clusters=4, random_state=global_random_seed)

    # KMeans should warn that fewer labels than cluster centers have been used
    msg = (
        r"Number of distinct clusters \(3\) found smaller than "
        r"n_clusters \(4\). Possibly due to duplicate points in X."
    )
        km.fit(X)
        # only three distinct points, so only three clusters
        # can have points assigned to them
        assert set(km.labels_) == set(range(3))

def test_weighted_vs_repeated(global_random_seed):
    # Check that a sample weight of N should yield the same result as an N-fold
    # repetition of the sample. Valid only if init is precomputed, otherwise
    # rng produces different results. Not valid for MinibatchKMeans due to rng
    # to extract minibatches.
    sample_weight = np.random.RandomState(global_random_seed).randint(
        1, 5, size=n_samples
    )
    X_repeat = np.repeat(X, sample_weight, axis=0)

        init=centers, n_init=1, n_clusters=n_clusters, random_state=global_random_seed
    )

    km_weighted = clone(km).fit(X, sample_weight=sample_weight)
    repeated_labels = np.repeat(km_weighted.labels_, sample_weight)
    km_repeated = clone(km).fit(X_repeat)

    assert_array_equal(km_repeated.labels_, repeated_labels)
    assert_allclose(km_weighted.inertia_, km_repeated.inertia_)
        _sort_centers(km_weighted.cluster_centers_),
        _sort_centers(km_repeated.cluster_centers_),
    )

def test_unit_weights_vs_no_weights(Estimator, input_data, global_random_seed):
    # Check that not passing sample weights should be equivalent to passing
    # sample weights all equal to one.

    km = Estimator(n_clusters=n_clusters, random_state=global_random_seed, n_init=1)
    km_none = clone(km).fit(input_data, sample_weight=None)
    km_ones = clone(km).fit(input_data, sample_weight=sample_weight)

    assert_array_equal(km_none.labels_, km_ones.labels_)
    assert_allclose(km_none.cluster_centers_, km_ones.cluster_centers_)

def test_scaled_weights(Estimator, input_data, global_random_seed):
    # Check that scaling all sample weights by a common factor
    # shouldn't change the result
    sample_weight = np.random.RandomState(global_random_seed).uniform(size=n_samples)

    km_orig = clone(km).fit(input_data, sample_weight=sample_weight)
    km_scaled = clone(km).fit(input_data, sample_weight=0.5 * sample_weight)

    assert_array_equal(km_orig.labels_, km_scaled.labels_)
    assert_allclose(km_orig.cluster_centers_, km_scaled.cluster_centers_)

def test_kmeans_elkan_iter_attribute():
    # Regression test on bad n_iter_ value. Previous bug n_iter_ was one off
    # it's right value (#11340).
    km = KMeans(algorithm="elkan", max_iter=1).fit(X)
    assert km.n_iter_ == 1

def test_kmeans_empty_cluster_relocated(array_constr):
    # check that empty clusters are correctly relocated when using sample
    # weights (#13486)
    X = array_constr([[-1], [1]])
    sample_weight = [1.9, 0.1]
    init = np.array([[-1], [10]])

    km = KMeans(n_clusters=2, init=init, n_init=1)
    km.fit(X, sample_weight=sample_weight)

    assert len(set(km.labels_)) == 2
    assert_allclose(km.cluster_centers_, [[-1], [1]])

def test_result_equal_in_diff_n_threads(Estimator, global_random_seed):
    # Check that KMeans/MiniBatchKMeans give the same results in parallel mode
    # than in sequential mode.
    X = rnd.normal(size=(50, 10))

        result_1 = (
            Estimator(n_clusters=n_clusters, random_state=global_random_seed)
            .fit(X)
            .labels_
        )
    with _get_threadpool_controller().limit(limits=2, user_api="openmp"):
        result_2 = (
            .fit(X)
            .labels_
        )
    assert_array_equal(result_1, result_2)

def test_warning_elkan_1_cluster():
    # Check warning messages specific to KMeans
        match="algorithm='elkan' doesn't make sense for a single cluster",
    ):
        KMeans(n_clusters=1, algorithm="elkan").fit(X)

def test_k_means_1_iteration(array_constr, algo, global_random_seed):
    # check the results after a single iteration (E-step M-step E-step) by
    # comparing against a pure python implementation.
    X = np.random.RandomState(global_random_seed).uniform(size=(100, 5))
    init_centers = X[:5]

    def py_kmeans(X, init):
        new_centers = init.copy()
        labels = pairwise_distances_argmin(X, init)
        for label in range(init.shape[0]):
            new_centers[label] = X[labels == label].mean(axis=0)
        labels = pairwise_distances_argmin(X, new_centers)
        return labels, new_centers

    py_labels, py_centers = py_kmeans(X, init_centers)

    cy_kmeans = KMeans(
        n_clusters=5, n_init=1, init=init_centers, algorithm=algo, max_iter=1
    ).fit(X)
    cy_labels = cy_kmeans.labels_
    cy_centers = cy_kmeans.cluster_centers_

    assert_array_equal(py_labels, cy_labels)
    assert_allclose(py_centers, cy_centers)

def test_euclidean_distance(dtype, squared, global_random_seed):
    # Check that the _euclidean_(dense/sparse)_dense helpers produce correct
    # results
    a_sparse = sp.random(
        1, 100, density=0.5, format="csr", random_state=rng, dtype=dtype
    )
    a_dense = a_sparse.toarray().reshape(-1)
    b = rng.randn(100).astype(dtype, copy=False)
    b_squared_norm = (b**2).sum()

    expected = ((a_dense - b) ** 2).sum()
    expected = expected if squared else np.sqrt(expected)

    distance_dense_dense = _euclidean_dense_dense_wrapper(a_dense, b, squared)
    distance_sparse_dense = _euclidean_sparse_dense_wrapper(
        a_sparse.data, a_sparse.indices, b, b_squared_norm, squared
    )

    rtol = 1e-4 if dtype == np.float32 else 1e-7
    assert_allclose(distance_dense_dense, distance_sparse_dense, rtol=rtol)
    assert_allclose(distance_dense_dense, expected, rtol=rtol)
    assert_allclose(distance_sparse_dense, expected, rtol=rtol)

def test_inertia(dtype, global_random_seed):
    # Check that the _inertia_(dense/sparse) helpers produce correct results.
    X_sparse = sp.random(
        100, 10, density=0.5, format="csr", random_state=rng, dtype=dtype
    )
    sample_weight = rng.randn(100).astype(dtype, copy=False)
    centers = rng.randn(5, 10).astype(dtype, copy=False)
    labels = rng.randint(5, size=100, dtype=np.int32)

    distances = ((X_dense - centers[labels]) ** 2).sum(axis=1)
    expected = np.sum(distances * sample_weight)

    inertia_dense = _inertia_dense(X_dense, sample_weight, centers, labels, n_threads=1)
    inertia_sparse = _inertia_sparse(
        X_sparse, sample_weight, centers, labels, n_threads=1
    )

    rtol = 1e-4 if dtype == np.float32 else 1e-6
    assert_allclose(inertia_dense, inertia_sparse, rtol=rtol)
    assert_allclose(inertia_dense, expected, rtol=rtol)
    assert_allclose(inertia_sparse, expected, rtol=rtol)

    # Check the single_label parameter.
    label = 1
    mask = labels == label
    distances = ((X_dense[mask] - centers[label]) ** 2).sum(axis=1)
    expected = np.sum(distances * sample_weight[mask])

    inertia_dense = _inertia_dense(
        X_dense, sample_weight, centers, labels, n_threads=1, single_label=label
    )
        X_sparse, sample_weight, centers, labels, n_threads=1, single_label=label
    )

def test_n_init_auto(Klass, default_n_init):
    est = Klass(n_init="auto", init="k-means++")
    est.fit(X)
    assert est._n_init == 1

    est = Klass(n_init="auto", init="random")
    assert est._n_init == 10 if Klass.__name__ == "KMeans" else 3

def test_sample_weight_unchanged(Estimator):
    # Check that sample_weight is not modified in place by KMeans (#17204)
    X = np.array([[1], [2], [4]])
    sample_weight = np.array([0.5, 0.2, 0.3])
    Estimator(n_clusters=2, random_state=0).fit(X, sample_weight=sample_weight)

    assert_array_equal(sample_weight, np.array([0.5, 0.2, 0.3]))

def test_wrong_params(Estimator, param, match):
    # Check that error are raised with clear error message when wrong values
    # are passed for the parameters
    # Set n_init=1 by default to avoid warning with precomputed init
    km = Estimator(n_init=1)
        km.set_params(**param).fit(X)

def test_kmeans_plusplus_wrong_params(param, match):
        kmeans_plusplus(X, n_clusters, **param)

def test_kmeans_plusplus_output(input_data, dtype, global_random_seed):
    # Check for the correct number of seeds and all positive values
    data = input_data.astype(dtype)
    centers, indices = kmeans_plusplus(
        data, n_clusters, random_state=global_random_seed
    )

    # Check there are the correct number of indices and that all indices are
    # positive and within the number of samples
    assert indices.shape[0] == n_clusters
    assert (indices >= 0).all()
    assert (indices <= data.shape[0]).all()

    # Check for the correct number of seeds and that they are bound by the data
    assert centers.shape[0] == n_clusters
    assert (centers.max(axis=0) <= data.max(axis=0)).all()
    assert (centers.min(axis=0) >= data.min(axis=0)).all()

    # Check that indices correspond to reported centers
    # Use X for comparison rather than data, test still works against centers
    # calculated with sparse data.
    assert_allclose(X[indices].astype(dtype), centers)

def test_kmeans_plusplus_norms(x_squared_norms):
    # Check that defining x_squared_norms returns the same as default=None.
    centers, indices = kmeans_plusplus(X, n_clusters, x_squared_norms=x_squared_norms)

    assert_allclose(X[indices], centers)

def test_kmeans_plusplus_dataorder(global_random_seed):
    # Check that memory layout does not effect result
    centers_c, _ = kmeans_plusplus(X, n_clusters, random_state=global_random_seed)

    centers_fortran, _ = kmeans_plusplus(
        X_fortran, n_clusters, random_state=global_random_seed
    )

    assert_allclose(centers_c, centers_fortran)

def test_is_same_clustering():
    # Sanity check for the _is_same_clustering utility function
    labels1 = np.array([1, 0, 0, 1, 2, 0, 2, 1], dtype=np.int32)
    assert _is_same_clustering(labels1, labels1, 3)

    # these other labels represent the same clustering since we can retrieve the first
    # labels by simply renaming the labels: 0 -> 1, 1 -> 2, 2 -> 0.
    labels2 = np.array([0, 2, 2, 0, 1, 2, 1, 0], dtype=np.int32)
    assert _is_same_clustering(labels1, labels2, 3)

    # these other labels do not represent the same clustering since not all ones are
    # mapped to a same value
    labels3 = np.array([1, 0, 0, 2, 2, 0, 2, 1], dtype=np.int32)
    assert not _is_same_clustering(labels1, labels3, 3)

def test_kmeans_with_array_like_or_np_scalar_init(kwargs):
    """Check that init works with numpy scalar strings.

    Non-regression test for #21964.
    """
    X = np.asarray([[0, 0], [0.5, 0], [0.5, 1], [1, 1]], dtype=np.float64)

    clustering = KMeans(n_clusters=2, **kwargs)

def test_feature_names_out(Klass, method):
    """Check `feature_names_out` for `KMeans` and `MiniBatchKMeans`."""
    class_name = Klass.__name__.lower()
    kmeans = Klass()
    getattr(kmeans, method)(X)
    n_clusters = kmeans.cluster_centers_.shape[0]

    names_out = kmeans.get_feature_names_out()
    assert_array_equal([f"{class_name}{i}" for i in range(n_clusters)], names_out)

def test_predict_does_not_change_cluster_centers(csr_container):
    """Check that predict does not change cluster centers.

    Non-regression test for gh-24253.
    """
    X, _ = make_blobs(n_samples=200, n_features=10, centers=10, random_state=0)

    kmeans = KMeans()
    y_pred1 = kmeans.fit_predict(X)
    # Make cluster_centers readonly
    kmeans.cluster_centers_ = create_memmap_backed_data(kmeans.cluster_centers_)
    kmeans.labels_ = create_memmap_backed_data(kmeans.labels_)

    y_pred2 = kmeans.predict(X)
    assert_array_equal(y_pred1, y_pred2)

def test_sample_weight_init(init, global_random_seed):
    """Check that sample weight is used during init.

    `_init_centroids` is shared across all classes inheriting from _BaseKMeans so
    it's enough to check for KMeans.
    """
    )
    x_squared_norms = row_norms(X, squared=True)

    clusters_weighted = kmeans._init_centroids(
        X=X,
        x_squared_norms=x_squared_norms,
        sample_weight=rng.uniform(size=X.shape[0]),
        n_centroids=5,
        random_state=np.random.RandomState(global_random_seed),
    )
    clusters = kmeans._init_centroids(
        X=X,
        sample_weight=np.ones(X.shape[0]),
    )
        assert_allclose(clusters_weighted, clusters)

def test_sample_weight_zero(init, global_random_seed):
    """Check that if sample weight is 0, this sample won't be chosen.

    """
        n_samples=100, n_features=5, centers=5, random_state=global_random_seed
    )
    sample_weight = rng.uniform(size=X.shape[0])
    sample_weight[::2] = 0

        X=X,
        n_centroids=10,
    )
    # No center should be one of the 0 sample weight point
    # (i.e. be at a distance=0 from it)
    d = euclidean_distances(X[::2], clusters_weighted)
    assert not np.any(np.isclose(d, 0))

def test_relocating_with_duplicates(algorithm, array_constr):
    """Check that kmeans stops when there are more centers than non-duplicate samples

    Non-regression test for issue:
    https://github.com/scikit-learn/scikit-learn/issues/28055
    """
    X = np.array([[0, 0], [1, 1], [1, 1], [1, 0], [0, 1]])
    km = KMeans(n_clusters=5, init=X, algorithm=algorithm)

    msg = r"Number of distinct clusters \(4\) found smaller than n_clusters \(5\)"
        km.fit(array_constr(X))

def test_feature_agglomeration():
    n_clusters = 1
    X = np.array([0, 0, 1]).reshape(1, 3)  # (n_samples, n_features)

    agglo_mean = FeatureAgglomeration(n_clusters=n_clusters, pooling_func=np.mean)
    agglo_median = FeatureAgglomeration(n_clusters=n_clusters, pooling_func=np.median)
    agglo_mean.fit(X)
    agglo_median.fit(X)

    assert np.size(np.unique(agglo_mean.labels_)) == n_clusters
    assert np.size(np.unique(agglo_median.labels_)) == n_clusters
    assert np.size(agglo_mean.labels_) == X.shape[1]
    assert np.size(agglo_median.labels_) == X.shape[1]

    Xt_mean = agglo_mean.transform(X)
    Xt_median = agglo_median.transform(X)
    assert Xt_mean.shape[1] == n_clusters
    assert Xt_median.shape[1] == n_clusters
    assert Xt_mean == np.array([1 / 3.0])
    assert Xt_median == np.array([0.0])

    # Test inverse transform
    X_full_mean = agglo_mean.inverse_transform(Xt_mean)
    X_full_median = agglo_median.inverse_transform(Xt_median)
    assert np.unique(X_full_mean[0]).size == n_clusters
    assert np.unique(X_full_median[0]).size == n_clusters

    assert_array_almost_equal(agglo_mean.transform(X_full_mean), Xt_mean)
    assert_array_almost_equal(agglo_median.transform(X_full_median), Xt_median)

def test_feature_agglomeration_feature_names_out():
    """Check `get_feature_names_out` for `FeatureAgglomeration`."""
    X, _ = make_blobs(n_features=6, random_state=0)
    agglo = FeatureAgglomeration(n_clusters=3)
    n_clusters = agglo.n_clusters_

    names_out = agglo.get_feature_names_out()
        [f"featureagglomeration{i}" for i in range(n_clusters)], names_out
    )

class MockBiclustering(BiclusterMixin, BaseEstimator):
    # Mock object for testing get_submatrix.
        pass

        # Overridden to reproduce old get_submatrix test.
        return (
            np.where([True, True, False, False, True])[0],
            np.where([False, False, True, True])[0],
        )

def test_get_submatrix(csr_container):
    data = np.arange(20).reshape(5, 4)
    model = MockBiclustering()

    for X in (data, csr_container(data), data.tolist()):
        submatrix = model.get_submatrix(0, X)
        if issparse(submatrix):
            submatrix = submatrix.toarray()
        assert_array_equal(submatrix, [[2, 3], [6, 7], [18, 19]])
        submatrix[:] = -1
        assert np.all(X != -1)

def _test_shape_indices(model):
    # Test get_shape and get_indices on fitted model.
    for i in range(model.n_clusters):
        m, n = model.get_shape(i)
        i_ind, j_ind = model.get_indices(i)
        assert len(i_ind) == m
        assert len(j_ind) == n

def test_spectral_coclustering(global_random_seed, csr_container):
    # Test Dhillon's Spectral CoClustering on a simple problem.
    param_grid = {
        "svd_method": ["randomized", "arpack"],
        "n_svd_vecs": [None, 20],
        "mini_batch": [False, True],
        "init": ["k-means++"],
        "n_init": [10],
    }
    S, rows, cols = make_biclusters(
        (30, 30), 3, noise=0.1, random_state=global_random_seed
    )
    S -= S.min()  # needs to be nonnegative before making it sparse
    S = np.where(S < 1, 0, S)  # threshold some values
    for mat in (S, csr_container(S)):
        for kwargs in ParameterGrid(param_grid):
            model = SpectralCoclustering(
                n_clusters=3, random_state=global_random_seed, **kwargs
            )
            model.fit(mat)

            assert model.rows_.shape == (3, 30)
            assert_array_equal(model.rows_.sum(axis=0), np.ones(30))
            assert_array_equal(model.columns_.sum(axis=0), np.ones(30))
            assert consensus_score(model.biclusters_, (rows, cols)) == 1

            _test_shape_indices(model)

def test_spectral_biclustering(global_random_seed, csr_container):
    # Test Kluger methods on a checkerboard dataset.
    S, rows, cols = make_checkerboard(
        (30, 30), 3, noise=0.5, random_state=global_random_seed
    )

    non_default_params = {
        "method": ["scale", "log"],
        "svd_method": ["arpack"],
        "n_svd_vecs": [20],
        "mini_batch": [True],
    }

        for param_name, param_values in non_default_params.items():
            for param_value in param_values:
                model = SpectralBiclustering(
                    n_clusters=3,
                    n_init=3,
                    init="k-means++",
                    random_state=global_random_seed,
                )
                model.set_params(**dict([(param_name, param_value)]))

                if issparse(mat) and model.get_params().get("method") == "log":
                    # cannot take log of sparse matrix
                    with pytest.raises(ValueError):
                        model.fit(mat)
                    continue
                else:
                    model.fit(mat)

                assert model.rows_.shape == (9, 30)
                assert model.columns_.shape == (9, 30)
                assert_array_equal(model.rows_.sum(axis=0), np.repeat(3, 30))
                assert_array_equal(model.columns_.sum(axis=0), np.repeat(3, 30))
                assert consensus_score(model.biclusters_, (rows, cols)) == 1

                _test_shape_indices(model)

def _do_scale_test(scaled):
    """Check that rows sum to one constant, and columns to another."""
    row_sum = scaled.sum(axis=1)
    col_sum = scaled.sum(axis=0)
    if issparse(scaled):
        row_sum = np.asarray(row_sum).squeeze()
        col_sum = np.asarray(col_sum).squeeze()
    assert_array_almost_equal(row_sum, np.tile(row_sum.mean(), 100), decimal=1)
    assert_array_almost_equal(col_sum, np.tile(col_sum.mean(), 100), decimal=1)

def _do_bistochastic_test(scaled):
    """Check that rows and columns sum to the same constant."""
    _do_scale_test(scaled)
    assert_almost_equal(scaled.sum(axis=0).mean(), scaled.sum(axis=1).mean(), decimal=1)

def test_scale_normalize(global_random_seed, csr_container):
    generator = np.random.RandomState(global_random_seed)
    X = generator.rand(100, 100)
    for mat in (X, csr_container(X)):
        scaled, _, _ = _scale_normalize(mat)
        _do_scale_test(scaled)
        if issparse(mat):
            assert issparse(scaled)

def test_bistochastic_normalize(global_random_seed, csr_container):
        scaled = _bistochastic_normalize(mat)
        _do_bistochastic_test(scaled)

def test_log_normalize(global_random_seed):
    # adding any constant to a log-scaled matrix should make it
    # bistochastic
    mat = generator.rand(100, 100)
    scaled = _log_normalize(mat) + 1
    _do_bistochastic_test(scaled)

def test_fit_best_piecewise(global_random_seed):
    model = SpectralBiclustering(random_state=global_random_seed)
    vectors = np.array([[0, 0, 0, 1, 1, 1], [2, 2, 2, 3, 3, 3], [0, 1, 2, 3, 4, 5]])
    best = model._fit_best_piecewise(vectors, n_best=2, n_clusters=2)
    assert_array_equal(best, vectors[:2])

def test_project_and_cluster(global_random_seed, csr_container):
    data = np.array([[1, 1, 1], [1, 1, 1], [3, 6, 3], [3, 6, 3]])
    vectors = np.array([[1, 0], [0, 1], [0, 0]])
    for mat in (data, csr_container(data)):
        labels = model._project_and_cluster(mat, vectors, n_clusters=2)
        assert_almost_equal(v_measure_score(labels, [0, 0, 1, 1]), 1.0)

def test_perfect_checkerboard(global_random_seed):
    # XXX Previously failed on build bot (not reproducible)
    model = SpectralBiclustering(
        3, svd_method="arpack", random_state=global_random_seed
    )

        (30, 30), 3, noise=0, random_state=global_random_seed
    )
    model.fit(S)
    assert consensus_score(model.biclusters_, (rows, cols)) == 1

        (40, 30), 3, noise=0, random_state=global_random_seed
    )

        (30, 40), 3, noise=0, random_state=global_random_seed
    )

def test_spectralbiclustering_parameter_validation(params, type_err, err_msg):
    """Check parameters validation in `SpectralBiClustering`"""
    data = np.arange(25).reshape((5, 5))
    model = SpectralBiclustering(**params)
    with pytest.raises(type_err, match=err_msg):

def test_n_features_in_(est):
    X, _, _ = make_biclusters((3, 3), 3, random_state=0)

    assert est.n_features_in_ == 3

        return (
        )

def check_label_quality(labels, threshold=0.99):
    n_clusters = len(set(labels) - OUTLIER_SET)
    assert n_clusters == 3
    assert fowlkes_mallows_score(labels, y) > threshold

def test_outlier_data(outlier_type):
    """
    Tests if np.inf and np.nan data are each treated as special outliers.
    """
    outlier = {
        "infinite": np.inf,
        "missing": np.nan,
    }[outlier_type]
    prob_check = {
        "infinite": lambda x, y: x == y,
        "missing": lambda x, y: np.isnan(x),
    label = _OUTLIER_ENCODING[outlier_type]["label"]
    prob = _OUTLIER_ENCODING[outlier_type]["prob"]

    X_outlier = X.copy()
    X_outlier[0] = [outlier, 1]
    X_outlier[5] = [outlier, outlier]
    model = HDBSCAN().fit(X_outlier)

    (missing_labels_idx,) = (model.labels_ == label).nonzero()
    assert_array_equal(missing_labels_idx, [0, 5])

    (missing_probs_idx,) = (prob_check(model.probabilities_, prob)).nonzero()
    assert_array_equal(missing_probs_idx, [0, 5])

    clean_indices = list(range(1, 5)) + list(range(6, 200))
    clean_model = HDBSCAN().fit(X_outlier[clean_indices])
    assert_array_equal(clean_model.labels_, model.labels_[clean_indices])

def test_hdbscan_distance_matrix():
    """
    Tests that HDBSCAN works with precomputed distance matrices, and throws the
    appropriate errors when needed.
    """
    D_original = D.copy()
    labels = HDBSCAN(metric="precomputed", copy=True).fit_predict(D)

    assert_allclose(D, D_original)
    check_label_quality(labels)

    msg = r"The precomputed distance matrix.*has shape"
        HDBSCAN(metric="precomputed", copy=True).fit_predict(X)

    msg = r"The precomputed distance matrix.*values"
    # Ensure the matrix is not symmetric
    D[0, 1] = 10
    D[1, 0] = 1
        HDBSCAN(metric="precomputed").fit_predict(D)

def test_hdbscan_sparse_distance_matrix(sparse_constructor):
    """
    Tests that HDBSCAN works with sparse distance matrices.
    """

    threshold = stats.scoreatpercentile(D.flatten(), 50)

    D[D >= threshold] = 0.0
    D = sparse_constructor(D)
    D.eliminate_zeros()

    labels = HDBSCAN(metric="precomputed").fit_predict(D)

def test_hdbscan_feature_array():
    """
    Tests that HDBSCAN works with feature array, including an arbitrary
    goodness of fit check. Note that the check is a simple heuristic.
    """
    labels = HDBSCAN().fit_predict(X)

    # Check that clustering is arbitrarily good
    # This is a heuristic to guard against regression

def test_hdbscan_algorithms(algo, metric):
    """
    Tests that HDBSCAN works with the expected combinations of algorithms and
    metrics, or raises the expected errors.
    """
    labels = HDBSCAN(algorithm=algo).fit_predict(X)

    # Validation for brute is handled by `pairwise_distances`
    if algo in ("brute", "auto"):
        return

    ALGOS_TREES = {
        "kd_tree": KDTree,
        "ball_tree": BallTree,
    }
    metric_params = {
        "mahalanobis": {"V": np.eye(X.shape[1])},
        "seuclidean": {"V": np.ones(X.shape[1])},
        "minkowski": {"p": 2},
        "wminkowski": {"p": 2, "w": np.ones(X.shape[1])},
    }.get(metric, None)

    hdb = HDBSCAN(
    )

    if metric not in ALGOS_TREES[algo].valid_metrics:
            hdb.fit(X)
    elif metric == "wminkowski":
        with pytest.warns(FutureWarning):
    else:
        hdb.fit(X)

def test_dbscan_clustering():
    """
    Tests that HDBSCAN can generate a sufficiently accurate dbscan clustering.
    This test is more of a sanity check than a rigorous evaluation.
    """
    clusterer = HDBSCAN().fit(X)
    labels = clusterer.dbscan_clustering(0.3)

    # We use a looser threshold due to dbscan producing a more constrained
    # clustering representation
    check_label_quality(labels, threshold=0.92)

def test_dbscan_clustering_outlier_data(cut_distance):
    """
    """
    missing_label = _OUTLIER_ENCODING["missing"]["label"]
    infinite_label = _OUTLIER_ENCODING["infinite"]["label"]

    X_outlier[0] = [np.inf, 1]
    X_outlier[2] = [1, np.nan]
    X_outlier[5] = [np.inf, np.nan]
    labels = model.dbscan_clustering(cut_distance=cut_distance)

    missing_labels_idx = np.flatnonzero(labels == missing_label)
    assert_array_equal(missing_labels_idx, [2, 5])

    infinite_labels_idx = np.flatnonzero(labels == infinite_label)
    assert_array_equal(infinite_labels_idx, [0])

    clean_idx = list(set(range(200)) - set(missing_labels_idx + infinite_labels_idx))
    clean_model = HDBSCAN().fit(X_outlier[clean_idx])
    clean_labels = clean_model.dbscan_clustering(cut_distance=cut_distance)
    assert_array_equal(clean_labels, labels[clean_idx])

def test_hdbscan_best_balltree_metric():
    """
    Tests that HDBSCAN using `BallTree` works.
    """
    labels = HDBSCAN(
        metric="seuclidean", metric_params={"V": np.ones(X.shape[1])}
    ).fit_predict(X)

def test_hdbscan_no_clusters():
    """
    Tests that HDBSCAN correctly does not generate a valid cluster when the
    `min_cluster_size` is too large for the data.
    """
    labels = HDBSCAN(min_cluster_size=len(X) - 1).fit_predict(X)
    assert set(labels).issubset(OUTLIER_SET)

def test_hdbscan_min_cluster_size():
    """
    Test that the smallest non-noise cluster has at least `min_cluster_size`
    many points
    """
    for min_cluster_size in range(2, len(X), 1):
        labels = HDBSCAN(min_cluster_size=min_cluster_size).fit_predict(X)
        true_labels = [label for label in labels if label != -1]
        if len(true_labels) != 0:
            assert np.min(np.bincount(true_labels)) >= min_cluster_size

def test_hdbscan_callable_metric():
    """
    Tests that HDBSCAN works when passed a callable metric.
    """
    labels = HDBSCAN(metric=metric).fit_predict(X)

def test_hdbscan_precomputed_non_brute(tree):
    """
    Tests that HDBSCAN correctly raises an error when passing precomputed data
    while requesting a tree-based algorithm.
    """
    hdb = HDBSCAN(metric="precomputed", algorithm=tree)
    msg = "precomputed is not a valid metric for"

def test_hdbscan_sparse(csr_container):
    """
    Tests that HDBSCAN works correctly when passing sparse feature data.
    Evaluates correctness by comparing against the same data passed as a dense
    array.
    """

    dense_labels = HDBSCAN().fit(X).labels_
    check_label_quality(dense_labels)

    _X_sparse = csr_container(X)
    X_sparse = _X_sparse.copy()
    sparse_labels = HDBSCAN().fit(X_sparse).labels_
    assert_array_equal(dense_labels, sparse_labels)

    # Compare that the sparse and dense non-precomputed routines return the same labels
    # where the 0th observation contains the outlier.
    for outlier_val, outlier_type in ((np.inf, "infinite"), (np.nan, "missing")):
        X_dense = X.copy()
        X_dense[0, 0] = outlier_val
        dense_labels = HDBSCAN().fit(X_dense).labels_
        check_label_quality(dense_labels)
        assert dense_labels[0] == _OUTLIER_ENCODING[outlier_type]["label"]

        X_sparse = _X_sparse.copy()
        X_sparse[0, 0] = outlier_val
        sparse_labels = HDBSCAN().fit(X_sparse).labels_
        assert_array_equal(dense_labels, sparse_labels)

    msg = "Sparse data matrices only support algorithm `brute`."
        HDBSCAN(metric="euclidean", algorithm="ball_tree").fit(X_sparse)

def test_hdbscan_centers(algorithm):
    """
    Tests that HDBSCAN centers are calculated and stored properly, and are
    accurate to the data.
    """
    centers = [(0.0, 0.0), (3.0, 3.0)]
    H, _ = make_blobs(n_samples=2000, random_state=0, centers=centers, cluster_std=0.5)
    hdb = HDBSCAN(store_centers="both").fit(H)

    for center, centroid, medoid in zip(centers, hdb.centroids_, hdb.medoids_):
        assert_allclose(center, centroid, rtol=1, atol=0.05)
        assert_allclose(center, medoid, rtol=1, atol=0.05)

    # Ensure that nothing is done for noise
        algorithm=algorithm, store_centers="both", min_cluster_size=X.shape[0]
    ).fit(X)
    assert hdb.centroids_.shape[0] == 0
    assert hdb.medoids_.shape[0] == 0

def test_hdbscan_allow_single_cluster_with_epsilon():
    """
    Tests that HDBSCAN single-cluster selection with epsilon works correctly.
    """
    no_structure = rng.rand(150, 2)
    # without epsilon we should see many noise points as children of root.
        allow_single_cluster=True,
    ).fit_predict(no_structure)
    unique_labels, counts = np.unique(labels, return_counts=True)
    assert len(unique_labels) == 2

    # Arbitrary heuristic. Would prefer something more precise.
    assert counts[unique_labels == -1] > 30

    # for this random seed an epsilon of 0.18 will produce exactly 2 noise
    # points at that cut in single linkage.
        cluster_selection_epsilon=0.18,
        algorithm="kd_tree",
    assert counts[unique_labels == -1] == 2

def test_hdbscan_better_than_dbscan():
    """
    Validate that HDBSCAN can properly cluster this difficult synthetic
    dataset. Note that DBSCAN fails on this (see HDBSCAN plotting
    example)
    """
    centers = [[-0.85, -0.85], [-0.85, 0.85], [3, 3], [3, -3]]
        n_samples=750,
        cluster_std=[0.2, 0.35, 1.35, 1.35],
    )
    labels = HDBSCAN().fit(X).labels_

    n_clusters = len(set(labels)) - int(-1 in labels)
    assert n_clusters == 4
    fowlkes_mallows_score(labels, y) > 0.99

def test_hdbscan_usable_inputs(X, kwargs):
    """
    Tests that HDBSCAN works correctly for array-likes and precomputed inputs
    with non-finite points.
    """
    HDBSCAN(min_samples=1, **kwargs).fit(X)

def test_hdbscan_sparse_distances_too_few_nonzero(csr_container):
    """
    Tests that HDBSCAN raises the correct error when there are too few
    non-zero distances.
    """
    X = csr_container(np.zeros((10, 10)))

    msg = "There exists points with fewer than"
        HDBSCAN(metric="precomputed").fit(X)

def test_hdbscan_sparse_distances_disconnected_graph(csr_container):
    """
    Tests that HDBSCAN raises the correct error when the distance matrix
    has multiple connected components.
    """
    # Create symmetric sparse matrix with 2 connected components
    X = np.zeros((20, 20))
    X[:5, :5] = 1
    X[5:, 15:] = 1
    X = X + X.T
    msg = "HDBSCAN cannot be performed on a disconnected graph"

def test_hdbscan_tree_invalid_metric():
    """
    Tests that HDBSCAN correctly raises an error for invalid metric choices.
    """
    metric_callable = lambda x: x
    msg = (
        ".* is not a valid metric for a .*-based algorithm\\. Please select a different"
        " metric\\."
    )

    # Callables are not supported for either
        HDBSCAN(algorithm="kd_tree", metric=metric_callable).fit(X)
        HDBSCAN(algorithm="ball_tree", metric=metric_callable).fit(X)

    # The set of valid metrics for KDTree at the time of writing this test is a
    # strict subset of those supported in BallTree
    metrics_not_kd = list(set(BallTree.valid_metrics) - set(KDTree.valid_metrics))
    if len(metrics_not_kd) > 0:
            HDBSCAN(algorithm="kd_tree", metric=metrics_not_kd[0]).fit(X)

def test_hdbscan_too_many_min_samples():
    """
    Tests that HDBSCAN correctly raises an error when setting `min_samples`
    larger than the number of samples.
    """
    hdb = HDBSCAN(min_samples=len(X) + 1)
    msg = r"min_samples (.*) must be at most"

def test_hdbscan_precomputed_dense_nan():
    """
    Tests that HDBSCAN correctly raises an error when providing precomputed
    distances with `np.nan` values.
    """
    X_nan = X.copy()
    X_nan[0, 0] = np.nan
    msg = "np.nan values found in precomputed-dense"
    hdb = HDBSCAN(metric="precomputed")
        hdb.fit(X_nan)

def test_labelling_distinct(global_random_seed, allow_single_cluster, epsilon):
    """
    Tests that the `_do_labelling` helper function correctly assigns labels.
    """
    n_samples = 48
        # Ensure the clusters are distinct with no overlap
        centers=[
            [0, 0],
            [10, 0],
            [0, 10],
        ],
    )

    est = HDBSCAN().fit(X)
    condensed_tree = _condense_tree(
        est._single_linkage_tree_, min_cluster_size=est.min_cluster_size
    )
    clusters = {n_samples + 2, n_samples + 3, n_samples + 4}
    cluster_label_map = {n_samples + 2: 0, n_samples + 3: 1, n_samples + 4: 2}
    labels = _do_labelling(
        condensed_tree=condensed_tree,
        clusters=clusters,
        cluster_label_map=cluster_label_map,
        allow_single_cluster=allow_single_cluster,
        cluster_selection_epsilon=epsilon,
    )

    first_with_label = {_y: np.where(y == _y)[0][0] for _y in list(set(y))}
    y_to_labels = {_y: labels[first_with_label[_y]] for _y in list(set(y))}
    aligned_target = np.vectorize(y_to_labels.get)(y)
    assert_array_equal(labels, aligned_target)

def test_labelling_thresholding():
    """
    Tests that the `_do_labelling` helper function correctly thresholds the
    incoming lambda values given various `cluster_selection_epsilon` values.
    """
    MAX_LAMBDA = 1.5
    condensed_tree = np.array(
        [
            (5, 2, MAX_LAMBDA, 1),
            (5, 1, 0.1, 1),
            (5, 0, MAX_LAMBDA, 1),
            (5, 3, 0.2, 1),
            (5, 4, 0.3, 1),
        ],
        dtype=CONDENSED_dtype,
    )
        clusters={n_samples},
        cluster_label_map={n_samples: 0, n_samples + 1: 1},
        cluster_selection_epsilon=1,
    )
    num_noise = condensed_tree["value"] < 1
    assert sum(num_noise) == sum(labels == -1)

        cluster_selection_epsilon=0,
    )
    # The threshold should be calculated per-sample based on the largest
    # lambda of any simbling node. In this case, all points are siblings
    # and the largest value is exactly MAX_LAMBDA.
    num_noise = condensed_tree["value"] < MAX_LAMBDA

def test_hdbscan_error_precomputed_and_store_centers(store_centers):
    """Check that we raise an error if the centers are requested together with
    a precomputed input matrix.

    https://github.com/scikit-learn/scikit-learn/issues/27893
    """
    X = rng.random((100, 2))
    X_dist = euclidean_distances(X)
    err_msg = "Cannot store centers when using a precomputed distance matrix."
        HDBSCAN(metric="precomputed", store_centers=store_centers).fit(X_dist)

def test_hdbscan_cosine_metric_valid_algorithm(valid_algo):
    """Test that HDBSCAN works with the "cosine" metric when the algorithm is set
    to "brute" or "auto".

    Non-regression test for issue #28631
    """
    HDBSCAN(metric="cosine", algorithm=valid_algo).fit_predict(X)

def test_hdbscan_cosine_metric_invalid_algorithm(invalid_algo):
    """Test that HDBSCAN raises an informative error is raised when an unsupported
    algorithm is used with the "cosine" metric.
    """
    hdbscan = HDBSCAN(metric="cosine", algorithm=invalid_algo)
    with pytest.raises(ValueError, match="cosine is not a valid metric"):
        hdbscan.fit_predict(X)

def test_spectral_clustering(
    eigen_solver, assign_labels, csr_container, global_random_seed
):
    S = np.array(
        [
            [1.0, 1.0, 1.0, 0.2, 0.0, 0.0, 0.0],
            [0.2, 0.2, 0.2, 1.0, 1.0, 1.0, 1.0],
            [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
        ]
    )

        model = SpectralClustering(
            n_clusters=2,
            affinity="precomputed",
            eigen_solver=eigen_solver,
            assign_labels=assign_labels,
        ).fit(mat)
        if labels[0] == 0:
            labels = 1 - labels

        assert adjusted_rand_score(labels, [1, 1, 1, 0, 0, 0, 0]) == 1

        model_copy = pickle.loads(pickle.dumps(model))
        assert model_copy.n_clusters == model.n_clusters
        assert model_copy.eigen_solver == model.eigen_solver
        assert_array_equal(model_copy.labels_, model.labels_)

def test_spectral_clustering_sparse(assign_labels, coo_container, global_random_seed):
        centers=[[1, 1], [-1, -1]],
        cluster_std=0.01,
    )

    S = rbf_kernel(X, gamma=1)
    S = np.maximum(S - 1e-4, 0)
    S = coo_container(S)

    labels = (
        )
        .fit(S)
        .labels_
    )
    assert adjusted_rand_score(y, labels) == 1

def test_precomputed_nearest_neighbors_filtering(global_random_seed):
        n_samples=250,
    )

        nn = NearestNeighbors(n_neighbors=n_neighbors + additional_neighbors).fit(X)
        graph = nn.kneighbors_graph(X, mode="connectivity")
        labels = (
            SpectralClustering(
                random_state=global_random_seed,
                n_clusters=2,
            )
            .labels_
        )
        results.append(labels)

def test_affinities(global_random_seed):
    # Note: in the following, random_state has been selected to have
    # a dataset that yields a stable eigen decomposition both when built
    # on OSX and Linux
        n_samples=20, random_state=0, centers=[[1, 1], [-1, -1]], cluster_std=0.01
    )
    # nearest neighbors affinity
    sp = SpectralClustering(n_clusters=2, affinity="nearest_neighbors", random_state=0)
    with pytest.warns(UserWarning, match="not fully connected"):
        sp.fit(X)
    assert adjusted_rand_score(y, sp.labels_) == 1

    sp = SpectralClustering(n_clusters=2, gamma=2, random_state=global_random_seed)
    labels = sp.fit(X).labels_

    X = check_random_state(10).rand(10, 5) * 10

    kernels_available = kernel_metrics()
    for kern in kernels_available:
        # Additive chi^2 gives a negative similarity matrix which
        # doesn't make sense for spectral clustering
        if kern != "additive_chi2":
            sp = SpectralClustering(n_clusters=2, affinity=kern, random_state=0)
            labels = sp.fit(X).labels_
            assert (X.shape[0],) == labels.shape

    sp = SpectralClustering(n_clusters=2, affinity=lambda x, y: 1, random_state=0)
    assert (X.shape[0],) == labels.shape

    sp = SpectralClustering(n_clusters=2, affinity=histogram, random_state=0)

def test_cluster_qr(global_random_seed):
    # cluster_qr by itself should not be used for clustering generic data
    # other than the rows of the eigenvectors within spectral clustering,
    # but cluster_qr must still preserve the labels for different dtypes
    # of the generic fixed input even if the labels may be meaningless.
    random_state = np.random.RandomState(seed=global_random_seed)
    n_samples, n_components = 10, 5
    data = random_state.randn(n_samples, n_components)
    labels_float64 = cluster_qr(data.astype(np.float64))
    # Each sample is assigned a cluster identifier
    assert labels_float64.shape == (n_samples,)
    # All components should be covered by the assignment
    assert np.array_equal(np.unique(labels_float64), np.arange(n_components))
    # Single precision data should yield the same cluster assignments
    labels_float32 = cluster_qr(data.astype(np.float32))
    assert np.array_equal(labels_float64, labels_float32)

def test_cluster_qr_permutation_invariance(global_random_seed):
    # cluster_qr must be invariant to sample permutation.
    n_samples, n_components = 100, 5
    perm = random_state.permutation(n_samples)
    assert np.array_equal(
        cluster_qr(data)[perm],
        cluster_qr(data[perm]),
    )

def test_discretize(n_samples, coo_container, global_random_seed):
    # Test the discretize using a noise assignment matrix
    for n_class in range(2, 10):
        # random class labels
        y_true = random_state.randint(0, n_class + 1, n_samples)
        y_true = np.array(y_true, float)
        # noise class assignment matrix
        y_indicator = coo_container(
            (np.ones(n_samples), (np.arange(n_samples), y_true)),
            shape=(n_samples, n_class + 1),
        )
        y_true_noisy = y_indicator.toarray() + 0.1 * random_state.randn(
            n_samples, n_class + 1
        )
        y_pred = discretize(y_true_noisy, random_state=random_state)
        assert adjusted_rand_score(y_true, y_pred) > 0.8

def test_spectral_clustering_with_arpack_amg_solvers(global_random_seed):
    # Test that spectral_clustering is the same for arpack and amg solver
    # Based on toy example from plot_segmentation_toy.py

    # a small two coin image
    x, y = np.indices((40, 40))

    center1, center2 = (14, 12), (20, 25)
    radius1, radius2 = 8, 7

    circle1 = (x - center1[0]) ** 2 + (y - center1[1]) ** 2 < radius1**2
    circle2 = (x - center2[0]) ** 2 + (y - center2[1]) ** 2 < radius2**2

    circles = circle1 | circle2
    mask = circles.copy()
    img = circles.astype(float)

    graph = img_to_graph(img, mask=mask)
    graph.data = np.exp(-graph.data / graph.data.std())

    labels_arpack = spectral_clustering(
        graph, n_clusters=2, eigen_solver="arpack", random_state=global_random_seed
    )

    assert len(np.unique(labels_arpack)) == 2

    if amg_loaded:
        labels_amg = spectral_clustering(
            graph, n_clusters=2, eigen_solver="amg", random_state=global_random_seed
        )
        assert adjusted_rand_score(labels_arpack, labels_amg) == 1
    else:
            spectral_clustering(graph, n_clusters=2, eigen_solver="amg", random_state=0)

def test_n_components(global_random_seed):
    # Test that after adding n_components, result is different and
    # n_components = n_clusters by default
    )
    sp = SpectralClustering(n_clusters=2, random_state=global_random_seed)
    # set n_components = n_cluster and test if result is the same
    labels_same_ncomp = (
            n_clusters=2, n_components=2, random_state=global_random_seed
        )
        .fit(X)
        .labels_
    )
    # test that n_components=n_clusters by default
    assert_array_equal(labels, labels_same_ncomp)

    # test that n_components affect result
    # n_clusters=8 by default, and set n_components=2
    labels_diff_ncomp = (
        SpectralClustering(n_components=2, random_state=global_random_seed)
        .fit(X)
        .labels_
    )
    assert not np.array_equal(labels, labels_diff_ncomp)

def test_verbose(assign_labels, capsys):
    )

    SpectralClustering(n_clusters=2, random_state=42, verbose=1).fit(X)

    assert re.search(r"Computing label assignment using", captured.out)

    if assign_labels == "kmeans":
        assert re.search(r"Initialization complete", captured.out)
        assert re.search(r"Iteration [0-9]+, inertia", captured.out)

def test_spectral_clustering_np_matrix_raises():
    """Check that spectral_clustering raises an informative error when passed
    a np.matrix. See #10993"""
    X = np.matrix([[0.0, 2.0], [2.0, 0.0]])

    msg = r"np\.matrix is not supported. Please convert to a numpy array"
        spectral_clustering(X)

def test_spectral_clustering_not_infinite_loop(capsys, monkeypatch):
    """Check that discretize raises LinAlgError when svd never converges.

    Non-regression test for #21380
    """

    def new_svd(*args, **kwargs):
        raise LinAlgError()

    monkeypatch.setattr(np.linalg, "svd", new_svd)
    vectors = np.ones((10, 4))

    with pytest.raises(LinAlgError, match="SVD did not converge"):
        discretize(vectors)

def test_all_estimator_no_base_class():
    # test that all_estimators doesn't find abstract classes.
    for name, Estimator in all_estimators():
        msg = (
            "Base estimators such as {0} should not be included in all_estimators"
        ).format(name)
        assert not name.lower().startswith("base"), msg

class CallableEstimator(BaseEstimator):
    """Dummy development stub for an estimator.

    This is to make sure a callable estimator passes common tests.
    """

    def __call__(self):
        pass  # pragma: nocover

def test_get_check_estimator_ids(val, expected):
    assert _get_check_estimator_ids(val) == expected

def test_estimators(estimator, check, request):
    # Common tests for estimator instances
    with ignore_warnings(
        category=(FutureWarning, ConvergenceWarning, UserWarning, LinAlgWarning)
    ):
        check(estimator)

def test_check_estimator_generate_only_deprecation():
    """Check that check_estimator with generate_only=True raises a deprecation
    warning."""
    with pytest.warns(FutureWarning, match="`generate_only` is deprecated in 1.6"):
        all_instance_gen_checks = check_estimator(
            LogisticRegression(), generate_only=True
        )
    assert isgenerator(all_instance_gen_checks)

def test_import_all_consistency():
    sklearn_path = [os.path.dirname(sklearn.__file__)]
    # Smoke test to check that any name in a __all__ list is actually defined
    # in the namespace of the module or package.
    pkgs = pkgutil.walk_packages(
        path=sklearn_path, prefix="sklearn.", onerror=lambda _: None
    )
    submods = [modname for _, modname, _ in pkgs]
    for modname in submods + ["sklearn"]:
        if ".tests." in modname or "sklearn.externals" in modname:
            continue
        # Avoid test suite depending on build dependencies, for example Cython
        if "sklearn._build_utils" in modname:
            continue
        package = __import__(modname, fromlist="dummy")
        for name in getattr(package, "__all__", ()):
            assert hasattr(package, name), "Module '{0}' has no attribute '{1}'".format(
                modname, name
            )

def test_root_import_all_completeness():
    EXCEPTIONS = ("utils", "tests", "base", "conftest")
    for _, modname, _ in pkgutil.walk_packages(
        path=sklearn_path, onerror=lambda _: None
    ):
        if "." in modname or modname.startswith("_") or modname in EXCEPTIONS:
            continue
        assert modname in sklearn.__all__

def test_all_tests_are_importable():
    # Ensure that for each contentful subpackage, there is a test directory
    # within it that is also a subpackage (i.e. a directory with __init__.py)

    HAS_TESTS_EXCEPTIONS = re.compile(
        r"""(?x)
                                      \.externals(\.|$)|
                                      \.tests(\.|$)|
                                      \._
                                      """
    )
    resource_modules = {
        "sklearn.datasets.data",
        "sklearn.datasets.descr",
        "sklearn.datasets.images",
    }
    lookup = {
        name: ispkg
        for _, name, ispkg in pkgutil.walk_packages(sklearn_path, prefix="sklearn.")
    }
    missing_tests = [
        name
        for name, ispkg in lookup.items()
        if ispkg
        and name not in resource_modules
        and not HAS_TESTS_EXCEPTIONS.search(name)
        and name + ".tests" not in lookup
    ]
    assert missing_tests == [], (
        "{0} do not have `tests` subpackages. "
        "Perhaps they require "
        "__init__.py or a meson.build "
        "in the parent "
        "directory".format(missing_tests)
    )

def test_class_support_removed():
    # Make sure passing classes to check_estimator or parametrize_with_checks
    # raises an error

    msg = "Passing a class was deprecated.* isn't supported anymore"
        check_estimator(LogisticRegression)

        parametrize_with_checks([LogisticRegression])

def _estimators_that_predict_in_fit():
    for estimator in _tested_estimators():
        est_params = set(estimator.get_params())
        if "oob_score" in est_params:
            yield estimator.set_params(oob_score=True, bootstrap=True)
        elif "early_stopping" in est_params:
            est = estimator.set_params(early_stopping=True, n_iter_no_change=1)
            if est.__class__.__name__ in {"MLPClassifier", "MLPRegressor"}:
                # TODO: FIX MLP to not check validation set during MLP
                yield pytest.param(
                    est, marks=pytest.mark.xfail(msg="MLP still validates in fit")
                )
            else:
                yield est
        elif "n_iter_no_change" in est_params:
            yield estimator.set_params(n_iter_no_change=1)

def test_pandas_column_name_consistency(estimator):
    if isinstance(estimator, ColumnTransformer):
        pytest.skip("ColumnTransformer is not tested here")
    if "check_dataframe_column_names_consistency" in _get_expected_failed_checks(
        estimator
    ):
            "Estimator does not support check_dataframe_column_names_consistency"
        )
    with ignore_warnings(category=(FutureWarning)):
        with warnings.catch_warnings(record=True) as record:
            check_dataframe_column_names_consistency(
                estimator.__class__.__name__, estimator
            )
        for warning in record:
            assert "was fitted without feature names" not in str(warning.message)

def _include_in_get_feature_names_out_check(transformer):
    if hasattr(transformer, "get_feature_names_out"):
    module = transformer.__module__.split(".")[1]
    return module not in GET_FEATURES_OUT_MODULES_TO_IGNORE

def test_transformers_get_feature_names_out(transformer):
        check_transformer_get_feature_names_out(
            transformer.__class__.__name__, transformer
        )
        check_transformer_get_feature_names_out_pandas(
        )

def test_estimators_get_feature_names_out_error(estimator):
    estimator_name = estimator.__class__.__name__
    check_get_feature_names_out_error(estimator_name, estimator)

def test_check_param_validation(estimator):
    if isinstance(estimator, FeatureUnion):
        pytest.skip("FeatureUnion is not tested here")
    check_param_validation(name, estimator)

def test_set_output_transform(estimator):
    if not hasattr(estimator, "set_output"):
            f"Skipping check_set_output_transform for {name}: Does not support"
            " set_output API"
        )
        check_set_output_transform(estimator.__class__.__name__, estimator)

def test_set_output_transform_configured(estimator, check_func):
            f"Skipping {check_func.__name__} for {name}: Does not support"
            " set_output API yet"
        )
        check_func(estimator.__class__.__name__, estimator)

def test_check_inplace_ensure_writeable(estimator):

    if hasattr(estimator, "copy"):
        estimator.set_params(copy=False)
    elif hasattr(estimator, "copy_X"):
        estimator.set_params(copy_X=False)
    else:
        raise SkipTest(f"{name} doesn't require writeable input.")

    # The following estimators can work inplace only with certain settings
    if name == "HDBSCAN":
        estimator.set_params(metric="precomputed", algorithm="brute")

    if name == "PCA":
        estimator.set_params(svd_solver="full")

    if name == "KernelPCA":
        estimator.set_params(kernel="precomputed")

    check_inplace_ensure_writeable(name, estimator)

def test_raise_build_error():
    with pytest.raises(ImportError):
        raise_build_error(ImportError())

def test_multi_target_regression():
    X, y = datasets.make_regression(n_targets=3, random_state=0)
    X_train, y_train = X[:50], y[:50]
    X_test, y_test = X[50:], y[50:]

    references = np.zeros_like(y_test)
    for n in range(3):
        rgr = GradientBoostingRegressor(random_state=0)
        rgr.fit(X_train, y_train[:, n])
        references[:, n] = rgr.predict(X_test)

    rgr = MultiOutputRegressor(GradientBoostingRegressor(random_state=0))
    rgr.fit(X_train, y_train)
    y_pred = rgr.predict(X_test)

    assert_almost_equal(references, y_pred)

def test_multi_target_regression_partial_fit():

    half_index = 25
        sgr = SGDRegressor(random_state=0, max_iter=5)
        sgr.partial_fit(X_train[:half_index], y_train[:half_index, n])
        sgr.partial_fit(X_train[half_index:], y_train[half_index:, n])
        references[:, n] = sgr.predict(X_test)

    sgr = MultiOutputRegressor(SGDRegressor(random_state=0, max_iter=5))

    sgr.partial_fit(X_train[:half_index], y_train[:half_index])
    sgr.partial_fit(X_train[half_index:], y_train[half_index:])

    y_pred = sgr.predict(X_test)
    assert not hasattr(MultiOutputRegressor(Lasso), "partial_fit")

def test_multi_target_regression_one_target():
    # Test multi target regression raises
    X, y = datasets.make_regression(n_targets=1, random_state=0)
    msg = "at least two dimensions"
        rgr.fit(X, y)

def test_multi_target_sparse_regression(sparse_container):
    X_test = X[50:]

    rgr = MultiOutputRegressor(Lasso(random_state=0))
    rgr_sparse = MultiOutputRegressor(Lasso(random_state=0))

    rgr_sparse.fit(sparse_container(X_train), y_train)

        rgr.predict(X_test), rgr_sparse.predict(sparse_container(X_test))
    )

def test_multi_target_sample_weights_api():
    X = [[1, 2, 3], [4, 5, 6]]
    y = [[3.141, 2.718], [2.718, 3.141]]
    w = [0.8, 0.6]

    rgr = MultiOutputRegressor(OrthogonalMatchingPursuit())
    msg = "does not support sample weights"
        rgr.fit(X, y, w)

    # no exception should be raised if the base estimator supports weights
    rgr.fit(X, y, w)

def test_multi_target_sample_weight_partial_fit():
    # weighted regressor
    w = [2.0, 1.0]
    rgr_w = MultiOutputRegressor(SGDRegressor(random_state=0, max_iter=5))
    rgr_w.partial_fit(X, y, w)

    # weighted with different weights
    w = [2.0, 2.0]
    rgr = MultiOutputRegressor(SGDRegressor(random_state=0, max_iter=5))
    rgr.partial_fit(X, y, w)

    assert rgr.predict(X)[0][0] != rgr_w.predict(X)[0][0]

def test_multi_target_sample_weights():
    Xw = [[1, 2, 3], [4, 5, 6]]
    yw = [[3.141, 2.718], [2.718, 3.141]]
    rgr_w = MultiOutputRegressor(GradientBoostingRegressor(random_state=0))
    rgr_w.fit(Xw, yw, w)

    # unweighted, but with repeated samples
    X = [[1, 2, 3], [1, 2, 3], [4, 5, 6]]
    y = [[3.141, 2.718], [3.141, 2.718], [2.718, 3.141]]
    rgr.fit(X, y)

    X_test = [[1.5, 2.5, 3.5], [3.5, 4.5, 5.5]]
    assert_almost_equal(rgr.predict(X_test), rgr_w.predict(X_test))

def test_multi_output_classification_partial_fit_parallelism():
    sgd_linear_clf = SGDClassifier(loss="log_loss", random_state=1, max_iter=5)
    mor = MultiOutputClassifier(sgd_linear_clf, n_jobs=4)
    mor.partial_fit(X, y, classes)
    est1 = mor.estimators_[0]
    mor.partial_fit(X, y)
    est2 = mor.estimators_[0]
    if cpu_count() > 1:
        # parallelism requires this to be the case for a sane implementation
        assert est1 is not est2

def test_hasattr_multi_output_predict_proba():
    # default SGDClassifier has loss='hinge'
    # which does not expose a predict_proba method
    sgd_linear_clf = SGDClassifier(random_state=1, max_iter=5)
    multi_target_linear = MultiOutputClassifier(sgd_linear_clf)
    multi_target_linear.fit(X, y)
    assert not hasattr(multi_target_linear, "predict_proba")

    # case where predict_proba attribute exists
    assert hasattr(multi_target_linear, "predict_proba")

def test_multi_output_predict_proba():
    param = {"loss": ("hinge", "log_loss", "modified_huber")}

    # inner function for custom scoring
    def custom_scorer(estimator, X, y):
            return 1.0
        else:
            return 0.0

    grid_clf = GridSearchCV(
        sgd_linear_clf,
        param_grid=param,
        scoring=custom_scorer,
        cv=3,
        error_score="raise",
    )
    multi_target_linear = MultiOutputClassifier(grid_clf)

    multi_target_linear.predict_proba(X)

    # SGDClassifier defaults to loss='hinge' which is not a probabilistic
    # loss function; therefore it does not expose a predict_proba method

    inner2_msg = "probability estimates are not available for loss='hinge'"
    inner1_msg = "'SGDClassifier' has no attribute 'predict_proba'"
    outer_msg = "'MultiOutputClassifier' has no attribute 'predict_proba'"
        multi_target_linear.predict_proba(X)

    assert inner1_msg in str(exec_info.value.__cause__)

    assert isinstance(exec_info.value.__cause__.__cause__, AttributeError)
    assert inner2_msg in str(exec_info.value.__cause__.__cause__)

def test_multi_output_classification_partial_fit():
    # test if multi_target initializes correctly with base estimator and fit
    # assert predictions work as expected for predict

    # train the multi_target_linear and also get the predictions.
    half_index = X.shape[0] // 2
    multi_target_linear.partial_fit(X[:half_index], y[:half_index], classes=classes)

    first_predictions = multi_target_linear.predict(X)
    assert (n_samples, n_outputs) == first_predictions.shape

    multi_target_linear.partial_fit(X[half_index:], y[half_index:])
    second_predictions = multi_target_linear.predict(X)
    assert (n_samples, n_outputs) == second_predictions.shape

    # train the linear classification with each column and assert that
    # predictions are equal after first partial_fit and second partial_fit
        # create a clone with the same state
        sgd_linear_clf = clone(sgd_linear_clf)
        sgd_linear_clf.partial_fit(
            X[:half_index], y[:half_index, i], classes=classes[i]
        )
        assert_array_equal(sgd_linear_clf.predict(X), first_predictions[:, i])
        sgd_linear_clf.partial_fit(X[half_index:], y[half_index:, i])
        assert_array_equal(sgd_linear_clf.predict(X), second_predictions[:, i])

def test_multi_output_classification_partial_fit_no_first_classes_exception():
    msg = "classes must be passed on the first call to partial_fit."
        multi_target_linear.partial_fit(X, y)

def test_multi_output_classification():
    # assert predictions work as expected for predict, prodict_proba and score

    forest = RandomForestClassifier(n_estimators=10, random_state=1)
    multi_target_forest = MultiOutputClassifier(forest)

    # train the multi_target_forest and also get the predictions.
    multi_target_forest.fit(X, y)

    predictions = multi_target_forest.predict(X)
    assert (n_samples, n_outputs) == predictions.shape

    predict_proba = multi_target_forest.predict_proba(X)

    assert len(predict_proba) == n_outputs
    for class_probabilities in predict_proba:
        assert (n_samples, n_classes) == class_probabilities.shape

    assert_array_equal(np.argmax(np.dstack(predict_proba), axis=1), predictions)

    # train the forest with each column and assert that predictions are equal
        forest_ = clone(forest)  # create a clone with the same state
        forest_.fit(X, y[:, i])
        assert list(forest_.predict(X)) == list(predictions[:, i])
        assert_array_equal(list(forest_.predict_proba(X)), list(predict_proba[i]))

def test_multiclass_multioutput_estimator():
    # test to check meta of meta estimators
    svc = LinearSVC(random_state=0)
    multi_class_svc = OneVsRestClassifier(svc)
    multi_target_svc = MultiOutputClassifier(multi_class_svc)

    multi_target_svc.fit(X, y)

    predictions = multi_target_svc.predict(X)

        multi_class_svc_ = clone(multi_class_svc)  # create a clone
        multi_class_svc_.fit(X, y[:, i])
        assert list(multi_class_svc_.predict(X)) == list(predictions[:, i])

def test_multiclass_multioutput_estimator_predict_proba():
    seed = 542

    # make test deterministic

    # random features

    # random labels
    y1 = np.array(["b", "a", "a", "b", "a"]).reshape(5, 1)  # 2 classes
    y2 = np.array(["d", "e", "f", "e", "d"]).reshape(5, 1)  # 3 classes

    Y = np.concatenate([y1, y2], axis=1)

    clf = MultiOutputClassifier(
        LogisticRegression(solver="liblinear", random_state=seed)
    )

    y_result = clf.predict_proba(X)
    y_actual = [
        np.array(
            [
                [0.23481764, 0.76518236],
                [0.67196072, 0.32803928],
                [0.54681448, 0.45318552],
                [0.34883923, 0.65116077],
                [0.73687069, 0.26312931],
            ]
        ),
        np.array(
            [
                [0.5171785, 0.23878628, 0.24403522],
                [0.22141451, 0.64102704, 0.13755846],
                [0.16751315, 0.18256843, 0.64991843],
                [0.27357372, 0.55201592, 0.17441036],
                [0.65745193, 0.26062899, 0.08191907],
            ]
        ),
    ]

    for i in range(len(y_actual)):
        assert_almost_equal(y_result[i], y_actual[i])

def test_multi_output_classification_sample_weights():
    # weighted classifier
    yw = [[3, 2], [2, 3]]
    w = np.asarray([2.0, 1.0])
    clf_w = MultiOutputClassifier(forest)
    clf_w.fit(Xw, yw, w)

    y = [[3, 2], [3, 2], [2, 3]]
    clf = MultiOutputClassifier(forest)

    assert_almost_equal(clf.predict(X_test), clf_w.predict(X_test))

def test_multi_output_classification_partial_fit_sample_weights():
    Xw = [[1, 2, 3], [4, 5, 6], [1.5, 2.5, 3.5]]
    yw = [[3, 2], [2, 3], [3, 2]]
    w = np.asarray([2.0, 1.0, 1.0])
    sgd_linear_clf = SGDClassifier(random_state=1, max_iter=20)
    clf_w = MultiOutputClassifier(sgd_linear_clf)

    X = [[1, 2, 3], [1, 2, 3], [4, 5, 6], [1.5, 2.5, 3.5]]
    y = [[3, 2], [3, 2], [2, 3], [3, 2]]
    clf = MultiOutputClassifier(sgd_linear_clf)
    X_test = [[1.5, 2.5, 3.5]]
    assert_array_almost_equal(clf.predict(X_test), clf_w.predict(X_test))

def test_multi_output_exceptions():
    # NotFittedError when fit is not done but score, predict and
    # and predict_proba are called
    moc = MultiOutputClassifier(LinearSVC(random_state=0))
        moc.score(X, y)

    # ValueError when number of outputs is different
    # for fit and score
    y_new = np.column_stack((y1, y2))
    moc.fit(X, y)
        moc.score(X, y_new)

    # ValueError when y is continuous
    msg = "Unknown label type"
        moc.fit(X, X[:, 1])

def test_multi_output_not_fitted_error(response_method):
    """Check that we raise the proper error when the estimator is not fitted"""
    moc = MultiOutputClassifier(LogisticRegression())
        getattr(moc, response_method)(X)

def test_multi_output_delegate_predict_proba():
    """Check the behavior for the delegation of predict_proba to the underlying
    estimator"""

    # A base estimator with `predict_proba`should expose the method even before fit
    assert hasattr(moc, "predict_proba")

    # A base estimator without `predict_proba` should raise an AttributeError
    moc = MultiOutputClassifier(LinearSVC())
    assert not hasattr(moc, "predict_proba")

    inner_msg = "'LinearSVC' object has no attribute 'predict_proba'"
        moc.predict_proba(X)
    assert inner_msg == str(exec_info.value.__cause__)

def generate_multilabel_dataset_with_correlations():
    # Generate a multilabel data set from a multiclass dataset as a way of
    # by representing the integer number of the original class using a binary
    # encoding.
        n_samples=1000, n_features=100, n_classes=16, n_informative=10, random_state=0
    )

    Y_multi = np.array([[int(yyy) for yyy in format(yy, "#06b")[2:]] for yy in y])
    return X, Y_multi

def test_classifier_chain_fit_and_predict_with_linear_svc(chain_method):
    # Fit classifier chain and verify predict performance using LinearSVC
    X, Y = generate_multilabel_dataset_with_correlations()
    classifier_chain = ClassifierChain(
        LinearSVC(),
        chain_method=chain_method,

    Y_pred = classifier_chain.predict(X)
    assert Y_pred.shape == Y.shape

    Y_decision = classifier_chain.decision_function(X)

    Y_binary = Y_decision >= 0
    assert_array_equal(Y_binary, Y_pred)
    assert not hasattr(classifier_chain, "predict_proba")

def test_classifier_chain_fit_and_predict_with_sparse_data(csr_container):
    # Fit classifier chain with sparse data

    classifier_chain = ClassifierChain(LogisticRegression()).fit(X_sparse, Y)
    Y_pred_sparse = classifier_chain.predict(X_sparse)

    classifier_chain = ClassifierChain(LogisticRegression()).fit(X, Y)
    Y_pred_dense = classifier_chain.predict(X)

    assert_array_equal(Y_pred_sparse, Y_pred_dense)

def test_classifier_chain_vs_independent_models():
    # Verify that an ensemble of classifier chains (each of length
    # N) can achieve a higher Jaccard similarity score than N independent
    # models
    X_train = X[:600, :]
    X_test = X[600:, :]
    Y_train = Y[:600, :]
    Y_test = Y[600:, :]

    ovr = OneVsRestClassifier(LogisticRegression())
    ovr.fit(X_train, Y_train)
    Y_pred_ovr = ovr.predict(X_test)

    chain = ClassifierChain(LogisticRegression())
    chain.fit(X_train, Y_train)
    Y_pred_chain = chain.predict(X_test)

    assert jaccard_score(Y_test, Y_pred_chain, average="samples") > jaccard_score(
        Y_test, Y_pred_ovr, average="samples"
    )

def test_classifier_chain_fit_and_predict(chain_method, response_method):
    # Fit classifier chain and verify predict performance
    chain = ClassifierChain(LogisticRegression(), chain_method=chain_method)
    chain.fit(X, Y)
    Y_pred = chain.predict(X)
    assert [c.coef_.size for c in chain.estimators_] == list(
        range(X.shape[1], X.shape[1] + Y.shape[1])
    )

    Y_prob = getattr(chain, response_method)(X)
    if response_method == "predict_log_proba":
        Y_prob = np.exp(Y_prob)
    Y_binary = Y_prob >= 0.5

    assert isinstance(chain, ClassifierMixin)

def test_regressor_chain_fit_and_predict():
    # Fit regressor chain and verify Y and estimator coefficients shape
    chain = RegressorChain(Ridge())
    )

def test_base_chain_fit_and_predict_with_sparse_data_and_cv(csr_container):
    # Fit base chain with sparse data cross_val_predict
    base_chains = [
        ClassifierChain(LogisticRegression(), cv=3),
        RegressorChain(Ridge(), cv=3),
    ]
    for chain in base_chains:
        chain.fit(X_sparse, Y)
        Y_pred = chain.predict(X_sparse)
        assert Y_pred.shape == Y.shape

def test_base_chain_random_order():
    # Fit base chain with random order
    for chain in [ClassifierChain(LogisticRegression()), RegressorChain(Ridge())]:
        chain_random = clone(chain).set_params(order="random", random_state=42)
        chain_random.fit(X, Y)
        chain_fixed = clone(chain).set_params(order=chain_random.order_)
        chain_fixed.fit(X, Y)
        assert_array_equal(chain_fixed.order_, chain_random.order_)
        assert list(chain_random.order) != list(range(4))
        assert len(chain_random.order_) == 4
        assert len(set(chain_random.order_)) == 4
        # Randomly ordered chain should behave identically to a fixed order
        # chain with the same order.
        for est1, est2 in zip(chain_random.estimators_, chain_fixed.estimators_):
            assert_array_almost_equal(est1.coef_, est2.coef_)

def test_base_chain_crossval_fit_and_predict(chain_type, chain_method):
    # Fit chain with cross_val_predict and verify predict
    # performance

    if chain_type == "classifier":
        chain = ClassifierChain(LogisticRegression(), chain_method=chain_method)
    else:
        chain = RegressorChain(Ridge())
    chain_cv = clone(chain).set_params(cv=3)
    chain_cv.fit(X, Y)
    Y_pred_cv = chain_cv.predict(X)

    assert Y_pred_cv.shape == Y_pred.shape
    assert not np.all(Y_pred == Y_pred_cv)
    if isinstance(chain, ClassifierChain):
        assert jaccard_score(Y, Y_pred_cv, average="samples") > 0.4
    else:
        assert mean_squared_error(Y, Y_pred_cv) < 0.25

def test_multi_output_classes_(estimator):
    # Tests classes_ attribute of multioutput classifiers
    # RandomForestClassifier supports multioutput out-of-the-box
    assert isinstance(estimator.classes_, list)
    assert len(estimator.classes_) == n_outputs
    for estimator_classes, expected_classes in zip(classes, estimator.classes_):
        assert_array_equal(estimator_classes, expected_classes)

class DummyRegressorWithFitParams(DummyRegressor):
        self._fit_params = fit_params
        return super().fit(X, y, sample_weight)

class DummyClassifierWithFitParams(DummyClassifier):

def test_multioutput_estimator_with_fit_params(estimator, dataset):
    some_param = np.zeros_like(X)
    estimator.fit(X, y, some_param=some_param)
    for dummy_estimator in estimator.estimators_:
        assert "some_param" in dummy_estimator._fit_params

def test_regressor_chain_w_fit_params():
    # Make sure fit_params are properly propagated to the sub-estimators
    weight = rng.rand(y.shape[0])

    class MySGD(SGDRegressor):
        def fit(self, X, y, **fit_params):
            self.sample_weight_ = fit_params["sample_weight"]
            super().fit(X, y, **fit_params)

    model = RegressorChain(MySGD())

    # Fitting with params
    fit_param = {"sample_weight": weight}
    model.fit(X, y, **fit_param)

    for est in model.estimators_:
        assert est.sample_weight_ is weight

def test_support_missing_values(MultiOutputEstimator, Estimator):
    # smoke test to check that pipeline MultioutputEstimators are letting
    # the validation of missing values to
    # the underlying pipeline, regressor or classifier
    X, y = rng.randn(50, 2), rng.binomial(1, 0.5, (50, 3))
    mask = rng.choice([1, 0], X.shape, p=[0.01, 0.99]).astype(bool)

    MultiOutputEstimator(pipe).fit(X, y).score(X, y)

def test_classifier_chain_tuple_order(order_type):
    X = [[1, 2, 3], [4, 5, 6], [1.5, 2.5, 3.5]]
    y = [[3, 2], [2, 3], [3, 2]]
    order = order_type([1, 0])

    chain = ClassifierChain(
        RandomForestClassifier(n_estimators=2, random_state=0), order=order
    )

    chain.fit(X, y)
    y_test = [[3, 2]]
    assert_array_almost_equal(chain.predict(X_test), y_test)

def test_classifier_chain_tuple_invalid_order():
    order = tuple([1, 2])

    chain = ClassifierChain(RandomForestClassifier(), order=order)

    with pytest.raises(ValueError, match="invalid order"):
        chain.fit(X, y)

def test_classifier_chain_verbose(capsys):
        n_samples=100, n_features=5, n_classes=3, n_labels=3, random_state=0
    )

    pattern = (
        r"\[Chain\].*\(1 of 3\) Processing order 0, total=.*\n"
        r"\[Chain\].*\(2 of 3\) Processing order 1, total=.*\n"
        r"\[Chain\].*\(3 of 3\) Processing order 2, total=.*\n$"
    )

    classifier = ClassifierChain(
        DecisionTreeClassifier(),
        order=[0, 1, 2],
    )

def test_regressor_chain_verbose(capsys):
    X, y = make_regression(n_samples=125, n_targets=3, random_state=0)

    pattern = (
        r"\[Chain\].*\(1 of 3\) Processing order 1, total=.*\n"
        r"\[Chain\].*\(2 of 3\) Processing order 0, total=.*\n"
    )
    regressor = RegressorChain(
        LinearRegression(),
        order=[1, 0, 2],
    )

def test_multioutputregressor_ducktypes_fitted_estimator():
    """Test that MultiOutputRegressor checks the fitted estimator for
    predict. Non-regression test for #16549."""
    stacker = StackingRegressor(
        estimators=[("sgd", SGDRegressor(random_state=1))],
        final_estimator=Ridge(),
        cv=2,
    )

    reg = MultiOutputRegressor(estimator=stacker).fit(X, y)

    reg.predict(X)

def test_fit_params_no_routing(Cls, method):
    """Check that we raise an error when passing metadata not requested by the
    underlying classifier.
    """
    X, y = make_classification(n_samples=50)
    clf = Cls(PassiveAggressiveClassifier())

    with pytest.raises(ValueError, match="is only supported if"):
        getattr(clf, method)(X, y, test=1)

def test_multioutput_regressor_has_partial_fit():
    # Test that an unfitted MultiOutputRegressor handles available_if for
    # partial_fit correctly
    est = MultiOutputRegressor(LinearRegression())
    msg = "This 'MultiOutputRegressor' has no attribute 'partial_fit'"
        getattr(est, "partial_fit")

def test_base_estimator_deprecation(Estimator):
    """Check that we warn about the deprecation of `base_estimator`."""
    y = np.array([[1, 0], [0, 1]])

    estimator = LogisticRegression()

        Estimator(base_estimator=estimator).fit(X, y)

        Estimator(base_estimator=estimator, estimator=estimator).fit(X, y)

            return 1.0
        else:
            return 0.0

class NoFit(BaseEstimator):
    """Small class to test parameter dispatching."""

    def __init__(self, a=None, b=None):
        self.a = a
        self.b = b

class NoTrans(NoFit):

    def get_params(self, deep=False):
        return {"a": self.a, "b": self.b}

        self.a = params["a"]

class NoInvTransf(TransformerMixin, NoTrans):
        return X

class Transf(NoInvTransf):
        return X

        return X

class TransfFitParams(Transf):
        self.fit_params = fit_params

class Mult(TransformerMixin, BaseEstimator):
    def __init__(self, mult=1):
        self.mult = mult

        return np.asarray(X) * self.mult

        return np.asarray(X) / self.mult

        return (np.asarray(X) * self.mult).sum(axis=1)

    predict_proba = predict_log_proba = decision_function = predict

        return np.sum(X)

class FitParamT(BaseEstimator):
    """Mock classifier"""

        self.successful = False

    def fit(self, X, y, should_succeed=False):
        self.successful = should_succeed
        self.fitted_ = True

        return self.successful

    def fit_predict(self, X, y, should_succeed=False):
        self.fit(X, y, should_succeed=should_succeed)
        return self.predict(X)

            X = X * sample_weight

class DummyTransf(Transf):
    """Transformer which store the column means"""

        self.means_ = np.mean(X, axis=0)
        # store timestamp to figure out whether the result of 'fit' has been
        # cached or not
        self.timestamp_ = time.time()

class DummyEstimatorParams(BaseEstimator):
    """Mock classifier that takes params on predict"""

    def predict(self, X, got_attribute=False):
        self.got_attribute = got_attribute

    def predict_proba(self, X, got_attribute=False):

    def predict_log_proba(self, X, got_attribute=False):

def test_pipeline_invalid_parameters():
    # Test the various init parameters of the pipeline in fit
    # method
    pipeline = Pipeline([(1, 1)])
        pipeline.fit([[1]], [1])

    # Check that we can't fit pipelines with objects without fit
    # method
    msg = (
        "Last step of Pipeline should implement fit "
        "or be the string 'passthrough'"
        ".*NoFit.*"
    )
    pipeline = Pipeline([("clf", NoFit())])

    # Smoke test with only an estimator
    clf = NoTrans()
    pipe = Pipeline([("svc", clf)])
    assert pipe.get_params(deep=True) == dict(
        svc__a=None, svc__b=None, svc=clf, **pipe.get_params(deep=False)
    )

    # Check that params are set
    pipe.set_params(svc__a=0.1)
    assert clf.a == 0.1
    assert clf.b is None
    # Smoke test the repr:
    repr(pipe)

    # Test with two objects
    clf = SVC()
    filter1 = SelectKBest(f_classif)
    pipe = Pipeline([("anova", filter1), ("svc", clf)])

    # Check that estimators are not cloned on pipeline construction
    assert pipe.named_steps["anova"] is filter1
    assert pipe.named_steps["svc"] is clf

    # Check that we can't fit with non-transformers on the way
    # Note that NoTrans implements fit, but not transform
    msg = "All intermediate steps should be transformers.*\\bNoTrans\\b.*"
    pipeline = Pipeline([("t", NoTrans()), ("svc", clf)])

    pipe.set_params(svc__C=0.1)
    assert clf.C == 0.1

    # Check that params are not set when naming them wrong
        "Invalid parameter 'C' for estimator SelectKBest(). Valid parameters are: ['k',"
        " 'score_func']."
    )
        pipe.set_params(anova__C=0.1)

    # Test clone
    pipe2 = clone(pipe)
    assert pipe.named_steps["svc"] is not pipe2.named_steps["svc"]

    # Check that apart from estimators, the parameters are the same
    params = pipe.get_params(deep=True)
    params2 = pipe2.get_params(deep=True)

    for x in pipe.get_params(deep=False):
        params.pop(x)

    for x in pipe2.get_params(deep=False):
        params2.pop(x)

    # Remove estimators that where copied
    params.pop("svc")
    params.pop("anova")
    params2.pop("svc")
    params2.pop("anova")
    assert params == params2

def test_pipeline_init_tuple():
    # Pipeline accepts steps as tuple
    X = np.array([[1, 2]])
    pipe = Pipeline((("transf", Transf()), ("clf", FitParamT())))
    pipe.fit(X, y=None)
    pipe.score(X)

    pipe.set_params(transf="passthrough")

def test_pipeline_methods_anova():
    # Test the various methods of the pipeline (anova).
    # Test with Anova + LogisticRegression
    clf = LogisticRegression()
    filter1 = SelectKBest(f_classif, k=2)
    pipe = Pipeline([("anova", filter1), ("logistic", clf)])
    pipe.fit(X, y)
    pipe.predict(X)
    pipe.predict_proba(X)
    pipe.predict_log_proba(X)
    pipe.score(X, y)

def test_pipeline_fit_params():
    # Test that the pipeline can take fit parameters
    pipe = Pipeline([("transf", Transf()), ("clf", FitParamT())])
    pipe.fit(X=None, y=None, clf__should_succeed=True)
    # classifier should return True
    assert pipe.predict(None)
    # and transformer params should not be changed
    assert pipe.named_steps["transf"].a is None
    assert pipe.named_steps["transf"].b is None
    # invalid parameters should raise an error message

    msg = re.escape("fit() got an unexpected keyword argument 'bad'")
        pipe.fit(None, None, clf__bad=True)

def test_pipeline_sample_weight_supported():
    # Pipeline should pass sample_weight
    assert pipe.score(X) == 3
    assert pipe.score(X, y=None) == 3
    assert pipe.score(X, y=None, sample_weight=None) == 3
    assert pipe.score(X, sample_weight=np.array([2, 3])) == 8

def test_pipeline_sample_weight_unsupported():
    # When sample_weight is None it shouldn't be passed
    pipe = Pipeline([("transf", Transf()), ("clf", Mult())])
    assert pipe.score(X, sample_weight=None) == 3

    msg = re.escape("score() got an unexpected keyword argument 'sample_weight'")
        pipe.score(X, sample_weight=np.array([2, 3]))

def test_pipeline_raise_set_params_error():
    # Test pipeline raises set params error message for nested models.
    pipe = Pipeline([("cls", LinearRegression())])

    # expected error message
    error_msg = re.escape(
        "Invalid parameter 'fake' for estimator Pipeline(steps=[('cls',"
        " LinearRegression())]). Valid parameters are: ['memory', 'steps',"
        " 'transform_input', 'verbose']."
    )
        pipe.set_params(fake="nope")

    # invalid outer parameter name for compound parameter: the expected error message
    # is the same as above.
        pipe.set_params(fake__estimator="nope")

    # expected error message for invalid inner parameter
        "Invalid parameter 'invalid_param' for estimator LinearRegression(). Valid"
        " parameters are: ['copy_X', 'fit_intercept', 'n_jobs', 'positive', 'tol']."
    )
        pipe.set_params(cls__invalid_param="nope")

def test_pipeline_methods_pca_svm():
    # Test the various methods of the pipeline (pca + svm).
    # Test with PCA + SVC
    clf = SVC(probability=True, random_state=0)
    pca = PCA(svd_solver="full", n_components="mle", whiten=True)
    pipe = Pipeline([("pca", pca), ("svc", clf)])

def test_pipeline_score_samples_pca_lof():
    # Test that the score_samples method is implemented on a pipeline.
    # Test that the score_samples method on pipeline yields same results as
    # applying transform and score_samples steps separately.
    lof = LocalOutlierFactor(novelty=True)
    pipe = Pipeline([("pca", pca), ("lof", lof)])
    pipe.fit(X)
    # Check the shapes
    assert pipe.score_samples(X).shape == (X.shape[0],)
    # Check the values
    lof.fit(pca.fit_transform(X))
    assert_allclose(pipe.score_samples(X), lof.score_samples(pca.transform(X)))

def test_score_samples_on_pipeline_without_score_samples():
    X = np.array([[1], [2]])
    # Test that a pipeline does not have score_samples method when the final
    # step of the pipeline does not have score_samples defined.
    pipe = make_pipeline(LogisticRegression())

    inner_msg = "'LogisticRegression' object has no attribute 'score_samples'"
    outer_msg = "'Pipeline' has no attribute 'score_samples'"
        pipe.score_samples(X)

def test_pipeline_methods_preprocessing_svm():
    # Test the various methods of the pipeline (preprocessing + svm).
    n_classes = len(np.unique(y))
    scaler = StandardScaler()
    pca = PCA(n_components=2, svd_solver="randomized", whiten=True)
    clf = SVC(probability=True, random_state=0, decision_function_shape="ovr")

    for preprocessing in [scaler, pca]:
        pipe = Pipeline([("preprocess", preprocessing), ("svc", clf)])
        pipe.fit(X, y)

        # check shapes of various prediction functions
        predict = pipe.predict(X)
        assert predict.shape == (n_samples,)

        proba = pipe.predict_proba(X)
        assert proba.shape == (n_samples, n_classes)

        log_proba = pipe.predict_log_proba(X)
        assert log_proba.shape == (n_samples, n_classes)

        decision_function = pipe.decision_function(X)
        assert decision_function.shape == (n_samples, n_classes)

        pipe.score(X, y)

def test_fit_predict_on_pipeline():
    # test that the fit_predict method is implemented on a pipeline
    # test that the fit_predict on pipeline yields same results as applying
    # transform and clustering steps separately
    km = KMeans(random_state=0, n_init="auto")
    # As pipeline doesn't clone estimators on construction,
    # it must have its own estimators
    scaler_for_pipeline = StandardScaler()
    km_for_pipeline = KMeans(random_state=0, n_init="auto")

    # first compute the transform and clustering step separately
    scaled = scaler.fit_transform(iris.data)
    separate_pred = km.fit_predict(scaled)

    # use a pipeline to do the transform and clustering in one step
    pipe = Pipeline([("scaler", scaler_for_pipeline), ("Kmeans", km_for_pipeline)])
    pipeline_pred = pipe.fit_predict(iris.data)

    assert_array_almost_equal(pipeline_pred, separate_pred)

def test_fit_predict_on_pipeline_without_fit_predict():
    # tests that a pipeline does not have fit_predict method when final
    # step of pipeline does not have fit_predict defined
    pca = PCA(svd_solver="full")
    pipe = Pipeline([("scaler", scaler), ("pca", pca)])

    outer_msg = "'Pipeline' has no attribute 'fit_predict'"
    inner_msg = "'PCA' object has no attribute 'fit_predict'"
        getattr(pipe, "fit_predict")

def test_fit_predict_with_intermediate_fit_params():
    # tests that Pipeline passes fit_params to intermediate steps
    # when fit_predict is invoked
    pipe = Pipeline([("transf", TransfFitParams()), ("clf", FitParamT())])
    pipe.fit_predict(
        X=None, y=None, transf__should_get_this=True, clf__should_succeed=True
    )
    assert pipe.named_steps["transf"].fit_params["should_get_this"]
    assert pipe.named_steps["clf"].successful
    assert "should_succeed" not in pipe.named_steps["transf"].fit_params

def test_predict_methods_with_predict_params(method_name):
    # tests that Pipeline passes predict_* to the final estimator
    # when predict_* is invoked
    pipe = Pipeline([("transf", Transf()), ("clf", DummyEstimatorParams())])
    pipe.fit(None, None)
    method = getattr(pipe, method_name)
    method(X=None, got_attribute=True)

    assert pipe.named_steps["clf"].got_attribute

def test_feature_union(csr_container):
    # basic sanity check for feature union
    X = iris.data.copy()
    X -= X.mean(axis=0)
    svd = TruncatedSVD(n_components=2, random_state=0)
    select = SelectKBest(k=1)
    fs = FeatureUnion([("svd", svd), ("select", select)])
    fs.fit(X, y)
    X_transformed = fs.transform(X)
    assert X_transformed.shape == (X.shape[0], 3)

    # check if it does the expected thing
    assert_array_almost_equal(X_transformed[:, :-1], svd.fit_transform(X))
    assert_array_equal(X_transformed[:, -1], select.fit_transform(X, y).ravel())

    # test if it also works for sparse input
    # We use a different svd object to control the random_state stream
    X_sp = csr_container(X)
    X_sp_transformed = fs.fit_transform(X_sp, y)
    assert_array_almost_equal(X_transformed, X_sp_transformed.toarray())

    fs2 = clone(fs)
    assert fs.transformer_list[0][1] is not fs2.transformer_list[0][1]

    # test setting parameters
    fs.set_params(select__k=2)
    assert fs.fit_transform(X, y).shape == (X.shape[0], 4)

    # test it works with transformers missing fit_transform
    fs = FeatureUnion([("mock", Transf()), ("svd", svd), ("select", select)])
    X_transformed = fs.fit_transform(X, y)
    assert X_transformed.shape == (X.shape[0], 8)

    # test error if some elements do not support transform
    msg = "All estimators should implement fit and transform.*\\bNoTrans\\b"
    fs = FeatureUnion([("transform", Transf()), ("no_transform", NoTrans())])
        fs.fit(X)

    # test that init accepts tuples
    fs = FeatureUnion((("svd", svd), ("select", select)))

def test_feature_union_named_transformers():
    """Check the behaviour of `named_transformers` attribute."""
    transf = Transf()
    noinvtransf = NoInvTransf()
    fs = FeatureUnion([("transf", transf), ("noinvtransf", noinvtransf)])
    assert fs.named_transformers["transf"] == transf
    assert fs.named_transformers["noinvtransf"] == noinvtransf

    # test named attribute
    assert fs.named_transformers.transf == transf
    assert fs.named_transformers.noinvtransf == noinvtransf

def test_make_union():
    mock = Transf()
    fu = make_union(pca, mock)
    names, transformers = zip(*fu.transformer_list)
    assert names == ("pca", "transf")
    assert transformers == (pca, mock)

def test_make_union_kwargs():
    fu = make_union(pca, mock, n_jobs=3)
    assert fu.transformer_list == make_union(pca, mock).transformer_list
    assert 3 == fu.n_jobs

    # invalid keyword parameters should raise an error message
        "make_union() got an unexpected keyword argument 'transformer_weights'"
    )
        make_union(pca, mock, transformer_weights={"pca": 10, "Transf": 1})

def create_mock_transformer(base_name, n_features=3):
    """Helper to create a mock transformer with custom feature names."""
    mock.get_feature_names_out = lambda input_features: [
        f"{base_name}{i}" for i in range(n_features)
    ]
    return mock

def test_make_union_passes_verbose_feature_names_out():
    # Test that make_union passes verbose_feature_names_out
    # to the FeatureUnion.

    pca = PCA()
    mock = create_mock_transformer("transf")
    union = make_union(pca, mock, verbose_feature_names_out=False)

    assert not union.verbose_feature_names_out

    fu_union = make_union(pca, mock, verbose_feature_names_out=True)
    fu_union.fit(X, y)

        [
            "pca__pca0",
            "pca__pca1",
            "pca__pca2",
            "pca__pca3",
            "transf__transf0",
            "transf__transf1",
            "transf__transf2",
        ],
        fu_union.get_feature_names_out(),
    )

def test_pipeline_transform():
    # Test whether pipeline works with a transformer at the end.
    # Also test pipeline.transform and pipeline.inverse_transform
    pca = PCA(n_components=2, svd_solver="full")
    pipeline = Pipeline([("pca", pca)])

    # test transform and fit_transform:
    X_trans = pipeline.fit(X).transform(X)
    X_trans2 = pipeline.fit_transform(X)
    X_trans3 = pca.fit_transform(X)
    assert_array_almost_equal(X_trans, X_trans2)
    assert_array_almost_equal(X_trans, X_trans3)

    X_back = pipeline.inverse_transform(X_trans)
    X_back2 = pca.inverse_transform(X_trans)
    assert_array_almost_equal(X_back, X_back2)

def test_pipeline_fit_transform():
    # Test whether pipeline works with a transformer missing fit_transform
    pipeline = Pipeline([("mock", transf)])

    # test fit_transform:
    X_trans = pipeline.fit_transform(X, y)
    X_trans2 = transf.fit(X, y).transform(X)

def test_pipeline_slice(start, end):
        [("transf1", Transf()), ("transf2", Transf()), ("clf", FitParamT())],
        memory="123",
    )
    pipe_slice = pipe[start:end]
    # Test class
    assert isinstance(pipe_slice, Pipeline)
    # Test steps
    assert pipe_slice.steps == pipe.steps[start:end]
    # Test named_steps attribute
    assert (
        list(pipe_slice.named_steps.items())
        == list(pipe.named_steps.items())[start:end]
    )
    # Test the rest of the parameters
    pipe_params = pipe.get_params(deep=False)
    pipe_slice_params = pipe_slice.get_params(deep=False)
    del pipe_params["steps"]
    del pipe_slice_params["steps"]
    assert pipe_params == pipe_slice_params
    # Test exception
    msg = "Pipeline slicing only supports a step of 1"
        pipe[start:end:-1]

def test_pipeline_index():
    clf = FitParamT()
    pipe = Pipeline([("transf", transf), ("clf", clf)])
    assert pipe[0] == transf
    assert pipe["transf"] == transf
    assert pipe[-1] == clf
    assert pipe["clf"] == clf

    # should raise an error if slicing out of range
        pipe[3]

    # should raise an error if indexing with wrong element name
    with pytest.raises(KeyError):
        pipe["foobar"]

def test_set_pipeline_steps():
    transf1 = Transf()
    transf2 = Transf()
    pipeline = Pipeline([("mock", transf1)])
    assert pipeline.named_steps["mock"] is transf1

    # Directly setting attr
    pipeline.steps = [("mock2", transf2)]
    assert "mock" not in pipeline.named_steps
    assert pipeline.named_steps["mock2"] is transf2
    assert [("mock2", transf2)] == pipeline.steps

    # Using set_params
    pipeline.set_params(steps=[("mock", transf1)])
    assert [("mock", transf1)] == pipeline.steps

    # Using set_params to replace single step
    pipeline.set_params(mock=transf2)
    assert [("mock", transf2)] == pipeline.steps

    # With invalid data
    pipeline.set_params(steps=[("junk", ())])
        "Last step of Pipeline should implement fit or be the string 'passthrough'."
    )

    msg = "This 'Pipeline' has no attribute 'fit_transform'"
        pipeline.fit_transform([[1]], [1])

def test_pipeline_named_steps():
    mult2 = Mult(mult=2)
    pipeline = Pipeline([("mock", transf), ("mult", mult2)])

    # Test access via named_steps bunch object
    assert "mock" in pipeline.named_steps
    assert "mock2" not in pipeline.named_steps
    assert pipeline.named_steps.mock is transf
    assert pipeline.named_steps.mult is mult2

    # Test bunch with conflict attribute of dict
    pipeline = Pipeline([("values", transf), ("mult", mult2)])
    assert pipeline.named_steps.values is not transf

def test_pipeline_correctly_adjusts_steps(passthrough):
    X = np.array([[1]])
    y = np.array([1])
    mult3 = Mult(mult=3)
    mult5 = Mult(mult=5)

        [("m2", mult2), ("bad", passthrough), ("m3", mult3), ("m5", mult5)]
    )

    pipeline.fit(X, y)
    expected_names = ["m2", "bad", "m3", "m5"]
    actual_names = [name for name, _ in pipeline.steps]
    assert expected_names == actual_names

def test_set_pipeline_step_passthrough(passthrough):

    def make():
        return Pipeline([("m2", mult2), ("m3", mult3), ("last", mult5)])

    pipeline = make()

    exp = 2 * 3 * 5
    assert_array_equal([[exp]], pipeline.fit_transform(X, y))
    assert_array_equal([exp], pipeline.fit(X).predict(X))
    assert_array_equal(X, pipeline.inverse_transform([[exp]]))

    pipeline.set_params(m3=passthrough)
    exp = 2 * 5
    assert pipeline.get_params(deep=True) == {
        "steps": pipeline.steps,
        "m2": mult2,
        "m3": passthrough,
        "last": mult5,
        "memory": None,
        "m2__mult": 2,
        "last__mult": 5,
        "transform_input": None,
        "verbose": False,
    }

    pipeline.set_params(m2=passthrough)
    exp = 5

    # for other methods, ensure no AttributeErrors on None:
    other_methods = [
        "predict_proba",
        "predict_log_proba",
        "decision_function",
        "transform",
        "score",
    ]
    for method in other_methods:
        getattr(pipeline, method)(X)

    pipeline.set_params(m2=mult2)
    exp = 2 * 5

    pipeline.set_params(last=passthrough)
    # mult2 and mult3 are active
    exp = 6
    assert_array_equal([[exp]], pipeline.fit(X, y).transform(X))

    inner_msg = "'str' object has no attribute 'predict'"
    outer_msg = "This 'Pipeline' has no attribute 'predict'"
        getattr(pipeline, "predict")

    # Check 'passthrough' step at construction time
    exp = 2 * 5
    pipeline = Pipeline([("m2", mult2), ("m3", passthrough), ("last", mult5)])

def test_pipeline_ducktyping():
    pipeline = make_pipeline(Mult(5))
    pipeline.predict
    pipeline.transform
    pipeline.inverse_transform

    pipeline = make_pipeline(Transf())
    assert not hasattr(pipeline, "predict")

    pipeline = make_pipeline("passthrough")
    assert pipeline.steps[0] == ("passthrough", "passthrough")

    pipeline = make_pipeline(Transf(), NoInvTransf())
    assert not hasattr(pipeline, "inverse_transform")

    pipeline = make_pipeline(NoInvTransf(), Transf())

def test_make_pipeline():
    t1 = Transf()
    t2 = Transf()
    pipe = make_pipeline(t1, t2)
    assert isinstance(pipe, Pipeline)
    assert pipe.steps[0][0] == "transf-1"
    assert pipe.steps[1][0] == "transf-2"

    pipe = make_pipeline(t1, t2, FitParamT())
    assert pipe.steps[2][0] == "fitparamt"

def test_pipeline_estimator_type(pipeline, check_estimator_type):
    """Check that the estimator type returned by the pipeline is correct.

    Non-regression test as part of:
    https://github.com/scikit-learn/scikit-learn/issues/30197
    """
    # Smoke test the repr
    repr(pipeline)
    assert check_estimator_type(pipeline)

def test_sklearn_tags_with_empty_pipeline():
    """Check that we propagate properly the tags in a Pipeline.

    """
    empty_pipeline = Pipeline(steps=[])
    be = BaseEstimator()

    expected_tags = be.__sklearn_tags__()
    assert empty_pipeline.__sklearn_tags__() == expected_tags

def test_feature_union_weights():
    # test feature union with transformer weights
    pca = PCA(n_components=2, svd_solver="randomized", random_state=0)
    # test using fit followed by transform
    fs = FeatureUnion(
        [("pca", pca), ("select", select)], transformer_weights={"pca": 10}
    )
    # test using fit_transform
    )
    X_fit_transformed = fs.fit_transform(X, y)
        [("mock", Transf()), ("pca", pca), ("select", select)],
        transformer_weights={"mock": 10},
    )
    X_fit_transformed_wo_method = fs.fit_transform(X, y)
    # check against expected result

    # We use a different pca object to control the random_state stream
    assert_array_almost_equal(X_transformed[:, :-1], 10 * pca.fit_transform(X))
    assert_array_almost_equal(X_fit_transformed[:, :-1], 10 * pca.fit_transform(X))
    assert_array_equal(X_fit_transformed[:, -1], select.fit_transform(X, y).ravel())
    assert X_fit_transformed_wo_method.shape == (X.shape[0], 7)

def test_feature_union_parallel():
    # test that n_jobs work for FeatureUnion
    X = JUNK_FOOD_DOCS

        [
            ("words", CountVectorizer(analyzer="word")),
            ("chars", CountVectorizer(analyzer="char")),
        ]
    )

    fs_parallel = FeatureUnion(
        [
        ],
        n_jobs=2,
    )

    fs_parallel2 = FeatureUnion(
        [
        ],
        n_jobs=2,
    )

    fs.fit(X)
    assert X_transformed.shape[0] == len(X)

    fs_parallel.fit(X)
    X_transformed_parallel = fs_parallel.transform(X)
    assert X_transformed.shape == X_transformed_parallel.shape
    assert_array_equal(X_transformed.toarray(), X_transformed_parallel.toarray())

    # fit_transform should behave the same
    X_transformed_parallel2 = fs_parallel2.fit_transform(X)
    assert_array_equal(X_transformed.toarray(), X_transformed_parallel2.toarray())

    # transformers should stay fit after fit_transform
    X_transformed_parallel2 = fs_parallel2.transform(X)

def test_feature_union_feature_names():
    word_vect = CountVectorizer(analyzer="word")
    char_vect = CountVectorizer(analyzer="char_wb", ngram_range=(3, 3))
    ft = FeatureUnion([("chars", char_vect), ("words", word_vect)])
    ft.fit(JUNK_FOOD_DOCS)
    feature_names = ft.get_feature_names_out()
    for feat in feature_names:
        assert "chars__" in feat or "words__" in feat
    assert len(feature_names) == 35

    ft = FeatureUnion([("tr1", Transf())]).fit([[1]])

        "Transformer tr1 (type Transf) does not provide get_feature_names_out"
    )
        ft.get_feature_names_out()

def test_classes_property():

    reg = make_pipeline(SelectKBest(k=1), LinearRegression())
        getattr(reg, "classes_")

    clf = make_pipeline(SelectKBest(k=1), LogisticRegression(random_state=0))
        getattr(clf, "classes_")
    assert_array_equal(clf.classes_, np.unique(y))

def test_set_feature_union_steps():
    mult2 = Mult(2)
    mult3 = Mult(3)
    mult5 = Mult(5)

    mult3.get_feature_names_out = lambda input_features: ["x3"]
    mult2.get_feature_names_out = lambda input_features: ["x2"]
    mult5.get_feature_names_out = lambda input_features: ["x5"]

    ft = FeatureUnion([("m2", mult2), ("m3", mult3)])
    assert_array_equal([[2, 3]], ft.transform(np.asarray([[1]])))
    assert_array_equal(["m2__x2", "m3__x3"], ft.get_feature_names_out())

    ft.transformer_list = [("m5", mult5)]
    assert_array_equal([[5]], ft.transform(np.asarray([[1]])))
    assert_array_equal(["m5__x5"], ft.get_feature_names_out())

    ft.set_params(transformer_list=[("mock", mult3)])
    assert_array_equal([[3]], ft.transform(np.asarray([[1]])))
    assert_array_equal(["mock__x3"], ft.get_feature_names_out())

    ft.set_params(mock=mult5)
    assert_array_equal(["mock__x5"], ft.get_feature_names_out())

def test_set_feature_union_step_drop():

    X = np.asarray([[1]])

    assert_array_equal([[2, 3]], ft.fit(X).transform(X))
    assert_array_equal([[2, 3]], ft.fit_transform(X))

    ft.set_params(m2="drop")
    assert_array_equal([[3]], ft.fit(X).transform(X))
    assert_array_equal([[3]], ft.fit_transform(X))
    assert_array_equal(["m3__x3"], ft.get_feature_names_out())

    ft.set_params(m3="drop")
    assert_array_equal([[]], ft.fit(X).transform(X))
    assert_array_equal([[]], ft.fit_transform(X))
    assert_array_equal([], ft.get_feature_names_out())

    # check we can change back
    ft.set_params(m3=mult3)

    # Check 'drop' step at construction time
    ft = FeatureUnion([("m2", "drop"), ("m3", mult3)])

def test_set_feature_union_passthrough():
    """Check the behaviour of setting a transformer to `"passthrough"`."""

    # We only test get_features_names_out, as get_feature_names is unsupported by
    # FunctionTransformer, and hence unsupported by FeatureUnion passthrough.

    ft.set_params(m2="passthrough")
    assert_array_equal([[1, 3]], ft.fit(X).transform(X))
    assert_array_equal([[1, 3]], ft.fit_transform(X))
    assert_array_equal(["m2__myfeat", "m3__x3"], ft.get_feature_names_out(["myfeat"]))

    ft.set_params(m3="passthrough")
    assert_array_equal([[1, 1]], ft.fit(X).transform(X))
    assert_array_equal([[1, 1]], ft.fit_transform(X))
        ["m2__myfeat", "m3__myfeat"], ft.get_feature_names_out(["myfeat"])
    )

    ft = FeatureUnion([("m2", "passthrough"), ("m3", mult3)])

    columns = X.shape[1]

    ft = FeatureUnion([("passthrough", "passthrough"), ("pca", pca)])
    assert_array_equal(X, ft.fit(X).transform(X)[:, :columns])
    assert_array_equal(X, ft.fit_transform(X)[:, :columns])
        [
            "passthrough__f0",
            "passthrough__f1",
            "passthrough__f2",
            "passthrough__f3",
        ],
        ft.get_feature_names_out(["f0", "f1", "f2", "f3"]),
    )

    ft.set_params(pca="passthrough")
    X_ft = ft.fit(X).transform(X)
    assert_array_equal(X_ft, np.hstack([X, X]))
    X_ft = ft.fit_transform(X)
        [
            "pca__f0",
            "pca__f1",
            "pca__f2",
            "pca__f3",
        ],
    )

    ft.set_params(passthrough=pca)
    assert_array_equal(X, ft.fit(X).transform(X)[:, -columns:])
    assert_array_equal(X, ft.fit_transform(X)[:, -columns:])
        [
            "passthrough__pca0",
            "passthrough__pca1",
        ],
    )

    ft = FeatureUnion(
        [("passthrough", "passthrough"), ("pca", pca)],
        transformer_weights={"passthrough": 2},
    )
    assert_array_equal(X * 2, ft.fit(X).transform(X)[:, :columns])
    assert_array_equal(X * 2, ft.fit_transform(X)[:, :columns])
        [
        ],
    )

def test_feature_union_passthrough_get_feature_names_out_true():
    """Check feature_names_out for verbose_feature_names_out=True (default)"""

    ft = FeatureUnion([("pca", pca), ("passthrough", "passthrough")])
    ft.fit(X)
        [
            "passthrough__x0",
            "passthrough__x1",
            "passthrough__x2",
            "passthrough__x3",
        ],
        ft.get_feature_names_out(),
    )

def test_feature_union_passthrough_get_feature_names_out_false():
    """Check feature_names_out for verbose_feature_names_out=False"""

        [("pca", pca), ("passthrough", "passthrough")], verbose_feature_names_out=False
    )
    ft.fit(X)
        [
            "pca0",
            "pca1",
            "x0",
            "x1",
            "x2",
            "x3",
        ],
    )

def test_feature_union_passthrough_get_feature_names_out_false_errors():
    """Check get_feature_names_out and non-verbose names and colliding names."""
    X = pd.DataFrame([[1, 2], [2, 3]], columns=["a", "b"])

    select_a = FunctionTransformer(
        lambda X: X[["a"]], feature_names_out=lambda self, _: np.asarray(["a"])
    )
    union = FeatureUnion(
        [("t1", StandardScaler()), ("t2", select_a)],
        verbose_feature_names_out=False,
    )
    union.fit(X)

        "Output feature names: ['a'] are not unique. "
        "Please set verbose_feature_names_out=True to add prefixes to feature names"
    )

        union.get_feature_names_out()

def test_feature_union_passthrough_get_feature_names_out_false_errors_overlap_over_5():
    """Check get_feature_names_out with non-verbose names and >= 5 colliding names."""
    X = pd.DataFrame([list(range(10))], columns=[f"f{i}" for i in range(10)])

        [("t1", "passthrough"), ("t2", "passthrough")],
    )

        "Output feature names: ['f0', 'f1', 'f2', 'f3', 'f4', ...] "
        "are not unique. Please set verbose_feature_names_out=True to add prefixes to"
        " feature names"
    )

def test_step_name_validation():
    error_message_1 = r"Estimator names must not contain __: got \['a__q'\]"
    error_message_2 = r"Names provided are not unique: \['a', 'a'\]"
    error_message_3 = r"Estimator names conflict with constructor arguments: \['%s'\]"
    bad_steps1 = [("a__q", Mult(2)), ("b", Mult(3))]
    bad_steps2 = [("a", Mult(2)), ("a", Mult(3))]
    for cls, param in [(Pipeline, "steps"), (FeatureUnion, "transformer_list")]:
        # we validate in construction (despite scikit-learn convention)
        bad_steps3 = [("a", Mult(2)), (param, Mult(3))]
        for bad_steps, message in [
            (bad_steps1, error_message_1),
            (bad_steps2, error_message_2),
            (bad_steps3, error_message_3 % param),
        ]:
            # three ways to make invalid:
            # - construction
            with pytest.raises(ValueError, match=message):
                cls(**{param: bad_steps}).fit([[1]], [1])

            # - setattr
            est = cls(**{param: [("a", Mult(1))]})
            setattr(est, param, bad_steps)
                est.fit([[1]], [1])

                est.fit_transform([[1]], [1])

            # - set_params
            est.set_params(**{param: bad_steps})

def test_set_params_nested_pipeline():
    estimator = Pipeline([("a", Pipeline([("b", DummyRegressor())]))])
    estimator.set_params(a__b__alpha=0.001, a__b=Lasso())
    estimator.set_params(a__steps=[("b", LogisticRegression())], a__b__C=5)

def test_pipeline_memory():
    cachedir = mkdtemp()
    try:
        memory = joblib.Memory(location=cachedir, verbose=10)
        # Test with Transformer + SVC
        clf = SVC(probability=True, random_state=0)
        transf = DummyTransf()
        pipe = Pipeline([("transf", clone(transf)), ("svc", clf)])
        cached_pipe = Pipeline([("transf", transf), ("svc", clf)], memory=memory)

        # Memoize the transformer at the first fit
        cached_pipe.fit(X, y)
        # Get the time stamp of the transformer in the cached pipeline
        ts = cached_pipe.named_steps["transf"].timestamp_
        # Check that cached_pipe and pipe yield identical results
        assert_array_equal(pipe.predict(X), cached_pipe.predict(X))
        assert_array_equal(pipe.predict_proba(X), cached_pipe.predict_proba(X))
        assert_array_equal(pipe.predict_log_proba(X), cached_pipe.predict_log_proba(X))
        assert_array_equal(pipe.score(X, y), cached_pipe.score(X, y))
            pipe.named_steps["transf"].means_, cached_pipe.named_steps["transf"].means_
        )
        assert not hasattr(transf, "means_")
        # Check that we are reading the cache while fitting
        # a second time
        )
        assert ts == cached_pipe.named_steps["transf"].timestamp_
        # Create a new pipeline with cloned estimators
        # Check that even changing the name step does not affect the cache hit
        clf_2 = SVC(probability=True, random_state=0)
        transf_2 = DummyTransf()
        cached_pipe_2 = Pipeline(
            [("transf_2", transf_2), ("svc", clf_2)], memory=memory
        )
        cached_pipe_2.fit(X, y)

        assert_array_equal(pipe.predict(X), cached_pipe_2.predict(X))
        assert_array_equal(pipe.predict_proba(X), cached_pipe_2.predict_proba(X))
            pipe.predict_log_proba(X), cached_pipe_2.predict_log_proba(X)
        )
        assert_array_equal(pipe.score(X, y), cached_pipe_2.score(X, y))
            pipe.named_steps["transf"].means_,
            cached_pipe_2.named_steps["transf_2"].means_,
        )
        assert ts == cached_pipe_2.named_steps["transf_2"].timestamp_
    finally:
        shutil.rmtree(cachedir)

def test_make_pipeline_memory():
    memory = joblib.Memory(location=cachedir, verbose=10)
    pipeline = make_pipeline(DummyTransf(), SVC(), memory=memory)
    assert pipeline.memory is memory
    pipeline = make_pipeline(DummyTransf(), SVC())
    assert pipeline.memory is None
    assert len(pipeline) == 2

    shutil.rmtree(cachedir)

class FeatureNameSaver(BaseEstimator):
        _check_feature_names(self, X, reset=True)

        return X

        return input_features

def test_features_names_passthrough():
    """Check pipeline.get_feature_names_out with passthrough"""
        steps=[
            ("names", FeatureNameSaver()),
            ("pass", "passthrough"),
            ("clf", LogisticRegression()),
        ]
    )
    pipe.fit(iris.data, iris.target)
        pipe[:-1].get_feature_names_out(iris.feature_names), iris.feature_names
    )

def test_feature_names_count_vectorizer():
    """Check pipeline.get_feature_names_out with vectorizers"""
    pipe = Pipeline(steps=[("vect", CountVectorizer()), ("clf", LogisticRegression())])
    y = ["pizza" in x for x in JUNK_FOOD_DOCS]
    pipe.fit(JUNK_FOOD_DOCS, y)
        pipe[:-1].get_feature_names_out(),
        ["beer", "burger", "coke", "copyright", "pizza", "the"],
    )
        pipe[:-1].get_feature_names_out("nonsense_is_ignored"),
    )

def test_pipeline_feature_names_out_error_without_definition():
    """Check that error is raised when a transformer does not define
    `get_feature_names_out`."""
    pipe = Pipeline(steps=[("notrans", NoTrans())])

    msg = "does not provide get_feature_names_out"
        pipe.get_feature_names_out()

def test_pipeline_param_error():
    clf = make_pipeline(LogisticRegression())
        ValueError, match="Pipeline.fit does not accept the sample_weight parameter"
    ):
        clf.fit([[0], [0]], [0, 1], sample_weight=[1, 1])

def test_verbose(est, method, pattern, capsys):
    func = getattr(est, method)

    y = [[7], [8]]

    est.set_params(verbose=False)
    func(X, y)
    assert not capsys.readouterr().out, "Got output for verbose=False"

    est.set_params(verbose=True)
    func(X, y)
    assert re.match(pattern, capsys.readouterr().out)

def test_n_features_in_pipeline():
    # make sure pipelines delegate n_features_in to the first step

    y = [0, 1, 2]

    ss = StandardScaler()
    gbdt = HistGradientBoostingClassifier()
    pipe = make_pipeline(ss, gbdt)
    assert not hasattr(pipe, "n_features_in_")
    assert pipe.n_features_in_ == ss.n_features_in_ == 2

    # if the first step has the n_features_in attribute then the pipeline also
    # has it, even though it isn't fitted.
    ss.fit(X, y)
    assert not hasattr(gbdt, "n_features_in_")

def test_n_features_in_feature_union():
    # make sure FeatureUnion delegates n_features_in to the first transformer

    y = [0, 1, 2]

    fu = make_union(ss)
    assert not hasattr(fu, "n_features_in_")
    fu.fit(X, y)
    assert fu.n_features_in_ == ss.n_features_in_ == 2

    # if the first step has the n_features_in attribute then the feature_union
    # also has it, even though it isn't fitted.

def test_feature_union_fit_params():
    # Regression test for issue: #15117
    class DummyTransformer(TransformerMixin, BaseEstimator):
        def fit(self, X, y=None, **fit_params):
            if fit_params != {"a": 0}:

        def transform(self, X, y=None):
            return X

    t = FeatureUnion([("dummy0", DummyTransformer()), ("dummy1", DummyTransformer())])
        t.fit(X, y)

        t.fit_transform(X, y)

    t.fit(X, y, a=0)
    t.fit_transform(X, y, a=0)

def test_feature_union_fit_params_without_fit_transform():
    # Test that metadata is passed correctly to underlying transformers that don't
    # implement a `fit_transform` method when SLEP6 is not enabled.

    class DummyTransformer(ConsumingNoFitTransformTransformer):
            if fit_params != {"metadata": 1}:

    t = FeatureUnion(
        [
            ("nofittransform0", DummyTransformer()),
            ("nofittransform1", DummyTransformer()),
        ]
    )

        t.fit_transform(X, y, metadata=0)

    t.fit_transform(X, y, metadata=1)

def test_pipeline_missing_values_leniency():
    # check that pipeline let the missing values validation to
    # the underlying transformers and predictors.
    X, y = iris.data.copy(), iris.target.copy()
    pipe = make_pipeline(SimpleImputer(), LogisticRegression())
    assert pipe.fit(X, y).score(X, y) > 0.4

def test_feature_union_warns_unknown_transformer_weight():
    # Warn user when transformer_weights containers a key not present in
    # transformer_list
    y = [0, 1, 2]

    transformer_list = [("transf", Transf())]
    # Transformer weights dictionary with incorrect name
    weights = {"transformer": 1}
        'Attempting to weight transformer "transformer", '
        "but it is not present in transformer_list."
    )
    union = FeatureUnion(transformer_list, transformer_weights=weights)
        union.fit(X, y)

def test_pipeline_get_tags_none(passthrough):
    # Checks that tags are set correctly when the first transformer is None or
    # 'passthrough'
    # https://github.com/scikit-learn/scikit-learn/issues/18815
    pipe = make_pipeline(passthrough, SVC())
    assert not pipe.__sklearn_tags__().input_tags.pairwise

def test_search_cv_using_minimal_compatible_estimator(Predictor):
    # Check that third-party library estimators can be part of a pipeline
    # and tuned by grid-search without inheriting from BaseEstimator.
    X, y = rng.randn(25, 2), np.array([0] * 5 + [1] * 20)

    model = Pipeline(
        [("transformer", MinimalTransformer()), ("predictor", Predictor())]
    )

    y_pred = model.predict(X)
    if is_classifier(model):
        assert_array_equal(y_pred, 1)
        assert model.score(X, y) == pytest.approx(accuracy_score(y, y_pred))
    else:
        assert_allclose(y_pred, y.mean())
        assert model.score(X, y) == pytest.approx(r2_score(y, y_pred))

def test_pipeline_check_if_fitted():

    pipeline = Pipeline([("clf", Estimator())])
        check_is_fitted(pipeline)
    pipeline.fit(iris.data, iris.target)
    check_is_fitted(pipeline)

def test_feature_union_check_if_fitted():
    """Check __sklearn_is_fitted__ is defined correctly."""

    y = [0, 1, 2]

    union = FeatureUnion([("clf", MinimalTransformer())])
        check_is_fitted(union)

    union.fit(X, y)
    check_is_fitted(union)

    # passthrough is stateless
    union = FeatureUnion([("pass", "passthrough")])

    union = FeatureUnion([("clf", MinimalTransformer()), ("pass", "passthrough")])

def test_pipeline_get_feature_names_out_passes_names_through():
    """Check that pipeline passes names through.

    Non-regresion test for #21349.
    """

    class AddPrefixStandardScalar(StandardScaler):
        def get_feature_names_out(self, input_features=None):
            names = super().get_feature_names_out(input_features=input_features)
            return np.asarray([f"my_prefix_{name}" for name in names], dtype=object)

    pipe = make_pipeline(AddPrefixStandardScalar(), StandardScaler())

    input_names = iris.feature_names
    feature_names_out = pipe.get_feature_names_out(input_names)

    assert_array_equal(feature_names_out, [f"my_prefix_{name}" for name in input_names])

def test_pipeline_set_output_integration():
    """Test pipeline's set_output with feature names."""

    X, y = load_iris(as_frame=True, return_X_y=True)

    pipe = make_pipeline(StandardScaler(), LogisticRegression())
    pipe.set_output(transform="pandas")

    feature_names_in_ = pipe[:-1].get_feature_names_out()
    log_reg_feature_names = pipe[-1].feature_names_in_

    assert_array_equal(feature_names_in_, log_reg_feature_names)

def test_feature_union_set_output():
    """Test feature union with set_output API."""

    X_train, X_test = train_test_split(X, random_state=0)
    union = FeatureUnion([("scalar", StandardScaler()), ("pca", PCA())])
    union.set_output(transform="pandas")
    union.fit(X_train)

    X_trans = union.transform(X_test)
    assert_array_equal(X_trans.columns, union.get_feature_names_out())
    assert_array_equal(X_trans.index, X_test.index)

def test_feature_union_getitem():
    """Check FeatureUnion.__getitem__ returns expected results."""
    scalar = StandardScaler()
    pca = PCA()
        [
            ("scalar", scalar),
            ("pca", pca),
            ("drop_me", "drop"),
        ]
    )
    assert union["scalar"] is scalar
    assert union["pca"] is pca
    assert union["pass"] == "passthrough"
    assert union["drop_me"] == "drop"

def test_feature_union_getitem_error(key):
    """Raise error when __getitem__ gets a non-string input."""

    msg = "Only string keys are supported"
    with pytest.raises(KeyError, match=msg):
        union[key]

def test_feature_union_feature_names_in_():
    """Ensure feature union has `.feature_names_in_` attribute if `X` has a
    `columns` attribute.

    Test for #24754.
    """

    # FeatureUnion should have the feature_names_in_ attribute if the
    # first transformer also has it
    scaler.fit(X)
    union = FeatureUnion([("scale", scaler)])
    assert hasattr(union, "feature_names_in_")
    assert_array_equal(X.columns, union.feature_names_in_)
    assert_array_equal(scaler.feature_names_in_, union.feature_names_in_)

    # fit with pandas.DataFrame

    # fit with numpy array
    X_array = X.to_numpy()
    union.fit(X_array)
    assert not hasattr(union, "feature_names_in_")

def test_transform_input_pipeline(method):
    """Test that with transform_input, data is correctly transformed for each step."""

    def get_transformer(registry, sample_weight, metadata):
        """Get a transformer with requests set."""
        return (
            ConsumingTransformer(registry=registry)
            .set_fit_request(sample_weight=sample_weight, metadata=metadata)
            .set_transform_request(sample_weight=sample_weight, metadata=metadata)
        )

    def get_pipeline():
        """Get a pipeline and corresponding registries.

        The pipeline has 4 steps, with different request values set to test different
        cases. One is aliased.
        """
        registry_1, registry_2, registry_3, registry_4 = (
            _Registry(),
        )
        pipe = make_pipeline(
            get_transformer(registry_1, sample_weight=True, metadata=True),
            get_transformer(registry_2, sample_weight=False, metadata=False),
            get_transformer(registry_3, sample_weight=True, metadata=True),
            get_transformer(registry_4, sample_weight="other_weights", metadata=True),
            transform_input=["sample_weight"],
        )
        return pipe, registry_1, registry_2, registry_3, registry_4

    def check_metadata(registry, methods, **metadata):
        """Check that the right metadata was recorded for the given methods."""
        assert registry
        for estimator in registry:
            for method in methods:
                check_recorded_metadata(
                    estimator,
                    method=method,
                    parent=method,
                    **metadata,
                )

    sample_weight = np.array([[1, 2]])
    other_weights = np.array([[30, 40]])
    metadata = np.array([[100, 200]])

    pipe, registry_1, registry_2, registry_3, registry_4 = get_pipeline()
    pipe.fit(
        X,
        y,
        other_weights=other_weights,
    )

    check_metadata(
        registry_1, ["fit", "transform"], sample_weight=sample_weight, metadata=metadata
    )
    check_metadata(registry_2, ["fit", "transform"])
        registry_3,
        ["fit", "transform"],
        sample_weight=sample_weight + 2,
    )
        registry_4,
        method.split("_"),  # ["fit", "transform"] if "fit_transform", ["fit"] otherwise
        sample_weight=other_weights + 3,
    )

def test_transform_input_explicit_value_check():
    """Test that the right transformed values are passed to `fit`."""

    class Transformer(TransformerMixin, BaseEstimator):

            return X + 1

    class Estimator(ClassifierMixin, BaseEstimator):
        def fit(self, X, y, X_val=None, y_val=None):
            assert_array_equal(X, np.array([[1, 2]]))
            assert_array_equal(y, np.array([0, 1]))
            assert_array_equal(X_val, np.array([[2, 3]]))
            assert_array_equal(y_val, np.array([0, 1]))

    X = np.array([[0, 1]])
    X_val = np.array([[1, 2]])
    y_val = np.array([0, 1])
        [
            ("transformer", Transformer()),
            ("estimator", Estimator().set_fit_request(X_val=True, y_val=True)),
        ],
        transform_input=["X_val"],
    )
    pipe.fit(X, y, X_val=X_val, y_val=y_val)

def test_transform_input_no_slep6():
    """Make sure the right error is raised if slep6 is not enabled."""
    msg = "The `transform_input` parameter can only be set if metadata"
        make_pipeline(DummyTransf(), transform_input=["blah"]).fit(X, y)

def test_transform_tuple_input():
    """Test that if metadata is a tuple of arrays, both arrays are transformed."""

            assert isinstance(X_val, tuple)
            assert isinstance(y_val, tuple)
            # Here we make sure that each X_val is transformed by the transformer
            assert_array_equal(X_val[0], np.array([[2, 3]]))
            assert_array_equal(y_val[0], np.array([0, 1]))
            assert_array_equal(X_val[1], np.array([[11, 12]]))
            assert_array_equal(y_val[1], np.array([1, 2]))

            return X + 1

    X_val0 = np.array([[1, 2]])
    y_val0 = np.array([0, 1])
    X_val1 = np.array([[10, 11]])
    y_val1 = np.array([1, 2])
        [
        ],
    )
    pipe.fit(X, y, X_val=(X_val0, X_val1), y_val=(y_val0, y_val1))

def test_pipeline_warns_not_fitted(method):
    class StatelessEstimator(BaseEstimator):
        """Stateless estimator that doesn't check if it's fitted.

        Stateless estimators that don't require fit, should properly set the
        `requires_fit` flag and implement a `__sklearn_check_is_fitted__` returning
        `True`.
        """

            return self  # pragma: no cover

            return X

        def predict_log_proba(self, X):
            return np.zeros(len(X))

        def score(self, X, y):
            return 1

        def score_samples(self, X):

        def inverse_transform(self, X):
            return X

    pipe = Pipeline([("estimator", StatelessEstimator())])
    with pytest.warns(FutureWarning, match="This Pipeline instance is not fitted yet."):
        getattr(pipe, method)([[1]])

class SimpleEstimator(BaseEstimator):
    # This class is used in this section for testing routing in the pipeline.
    # This class should have every set_{method}_request

    def fit(self, X, y, sample_weight=None, prop=None):
        assert sample_weight is not None, sample_weight
        assert prop is not None, prop

    def fit_transform(self, X, y, sample_weight=None, prop=None):
        assert prop is not None
        return X + 1

    def fit_predict(self, X, y, sample_weight=None, prop=None):
        return np.ones(len(X))

    def predict(self, X, sample_weight=None, prop=None):

    def predict_proba(self, X, sample_weight=None, prop=None):

    def predict_log_proba(self, X, sample_weight=None, prop=None):
        return np.zeros(len(X))

    def decision_function(self, X, sample_weight=None, prop=None):

    def score(self, X, y, sample_weight=None, prop=None):
        return 1

    def transform(self, X, sample_weight=None, prop=None):
        return X + 1

    def inverse_transform(self, X, sample_weight=None, prop=None):
        return X - 1

def test_metadata_routing_for_pipeline(method):
    """Test that metadata is routed correctly for pipelines."""

    def set_request(est, method, **kwarg):
        """Set requests for a given method.

        If the given method is a composite method, set the same requests for
        all the methods that compose it.
        """
        if method in COMPOSITE_METHODS:
            methods = COMPOSITE_METHODS[method]
        else:
            methods = [method]

        for method in methods:
            getattr(est, f"set_{method}_request")(**kwarg)
        return est

    X, y = np.array([[1]]), np.array([1])
    sample_weight, prop, metadata = [1], "a", "b"

    # test that metadata is routed correctly for pipelines when requested
    est = SimpleEstimator()
    est = set_request(est, method, sample_weight=True, prop=True)
    est = set_request(est, "fit", sample_weight=True, prop=True)
    trs = (
        ConsumingTransformer()
        .set_fit_request(sample_weight=True, metadata=True)
        .set_transform_request(sample_weight=True, metadata=True)
        .set_inverse_transform_request(sample_weight=True, metadata=True)
    )
    pipeline = Pipeline([("trs", trs), ("estimator", est)])

    if "fit" not in method:
        pipeline = pipeline.fit(X, y, sample_weight=sample_weight, prop=prop)

    try:
        getattr(pipeline, method)(
            X, y, sample_weight=sample_weight, prop=prop, metadata=metadata
        )
        # Some methods don't accept y
            X, sample_weight=sample_weight, prop=prop, metadata=metadata
        )

    # Make sure the transformer has received the metadata
    # For the transformer, always only `fit` and `transform` are called.
        obj=trs,
        method="fit",
        parent="fit",
    )
        obj=trs,
        method="transform",
        parent="transform",
    )

def test_metadata_routing_error_for_pipeline(method):
    """Test that metadata is not routed for pipelines when not requested."""
    X, y = [[1]], [1]
    sample_weight, prop = [1], "a"
    # here not setting sample_weight request and leaving it as None
    pipeline = Pipeline([("estimator", est)])
        "[sample_weight, prop] are passed but are not explicitly set as requested"
        f" or not requested for SimpleEstimator.{method}"
    )
        try:
            # passing X, y positional as the first two arguments
            getattr(pipeline, method)(X, y, sample_weight=sample_weight, prop=prop)
            # not all methods accept y (like `predict`), so here we only
            # pass X as a positional arg.
            getattr(pipeline, method)(X, sample_weight=sample_weight, prop=prop)

    pipe = Pipeline([("estimator", SimpleEstimator())])

    ):
        getattr(pipe, method)([[1]], sample_weight=[1], prop="a")

def test_pipeline_with_estimator_with_len():
    """Test that pipeline works with estimators that have a `__len__` method."""
        [("trs", RandomTreesEmbedding()), ("estimator", RandomForestClassifier())]
    )
    pipe.fit([[1]], [1])
    pipe.predict([[1]])

def test_pipeline_with_no_last_step(last_step):
    """Test that the pipeline works when there is not last step.

    It should just ignore and pass through the data on transform.
    """
    pipe = Pipeline([("trs", FunctionTransformer()), ("estimator", last_step)])
    assert pipe.fit([[1]], [1]).transform([[1], [2], [3]]) == [[1], [2], [3]]

def test_feature_union_metadata_routing_error():
    y = [1, 2, 3]

    # test lacking set_fit_request
    feature_union = FeatureUnion([("sub_transformer", ConsumingTransformer())])

        f" or not requested for {ConsumingTransformer.__name__}.fit"
    )

    with pytest.raises(UnsetMetadataPassedError, match=re.escape(error_message)):
        feature_union.fit(X, y, sample_weight=sample_weight, metadata=metadata)

    # test lacking set_transform_request
    feature_union = FeatureUnion(
        [
            (
                "sub_transformer",
                ConsumingTransformer().set_fit_request(
                    sample_weight=True, metadata=True
                ),
            )
        ]
    )

        "[sample_weight, metadata] are passed but are not explicitly set as requested "
        f"or not requested for {ConsumingTransformer.__name__}.transform"
    )

        feature_union.fit(
            X, y, sample_weight=sample_weight, metadata=metadata
        ).transform(X, sample_weight=sample_weight, metadata=metadata)

def test_feature_union_get_metadata_routing_without_fit():
    """Test that get_metadata_routing() works regardless of the Child's
    consumption of any metadata."""
    feature_union.get_metadata_routing()

def test_feature_union_metadata_routing(transformer):
    """Test that metadata is routed correctly for FeatureUnion."""
    y = [1, 2, 3]

        [
            (
                "sub_trans1",
                transformer(registry=_Registry())
                .set_fit_request(sample_weight=True, metadata=True)
                .set_transform_request(sample_weight=True, metadata=True),
            ),
            (
                "sub_trans2",
            ),
        ]
    )

    kwargs = {"sample_weight": sample_weight, "metadata": metadata}
    feature_union.fit(X, y, **kwargs)
    feature_union.fit_transform(X, y, **kwargs)
    feature_union.fit(X, y, **kwargs).transform(X, **kwargs)

    for transformer in feature_union.transformer_list:
        # access sub-transformer in (name, trans) with transformer[1]
        registry = transformer[1].registry
        for sub_trans in registry:
                obj=sub_trans,
                **kwargs,
            )

        self.a = a
        self.b = b

            return X

        return (
        )

        """
        )
        )

                )

            return X + 1

        `True`.
        """

            return X

            return 1

            return X

        return X + 1

        return 1

        return X + 1

        return X - 1

        """
        else:

        return est

class SimplePipeline(BaseEstimator):
    """A very simple pipeline, assuming the last step is always a predictor.

    steps : iterable of objects
        An iterable of transformers with the last step being a predictor.
    """

    def __init__(self, steps):

        self.steps_ = []
        params = process_routing(self, "fit", **fit_params)
        X_transformed = X
        for i, step in enumerate(self.steps[:-1]):
            transformer = clone(step).fit(
                X_transformed, y, **params.get(f"step_{i}").fit
            )
            self.steps_.append(transformer)
            X_transformed = transformer.transform(
                X_transformed, **params.get(f"step_{i}").transform
            )

        self.steps_.append(
            clone(self.steps[-1]).fit(X_transformed, y, **params.predictor.fit)
        )

        params = process_routing(self, "predict", **predict_params)
        for i, step in enumerate(self.steps_[:-1]):
            X_transformed = step.transform(X, **params.get(f"step_{i}").transform)

        return self.steps_[-1].predict(X_transformed, **params.predictor.predict)

                **{f"step_{i}": step},
                .add(caller="fit", callee="transform")
                .add(caller="predict", callee="transform"),
            )
            predictor=self.steps[-1],
            .add(caller="fit", callee="fit")
            .add(caller="predict", callee="predict"),
        )

def test_assert_request_is_empty():
    requests = MetadataRequest(owner="test")
    assert_request_is_empty(requests)

    requests.fit.add_request(param="foo", alias=None)
    # this should still work, since None is the default value

    requests.fit.add_request(param="bar", alias="value")
        # now requests is no more empty
        assert_request_is_empty(requests)

    # but one can exclude a method
    assert_request_is_empty(requests, exclude="fit")

    requests.score.add_request(param="carrot", alias=True)
        # excluding `fit` is not enough
        assert_request_is_empty(requests, exclude="fit")

    # and excluding both fit and score would avoid an exception
    assert_request_is_empty(requests, exclude=["fit", "score"])

    # test if a router is empty
    assert_request_is_empty(
        MetadataRouter(owner="test")
        .add_self_request(WeightedMetaRegressor(estimator=None))
        .add(
            estimator=ConsumingRegressor(),
        )
    )

def test_estimator_puts_self_in_registry(estimator):
    """Check that an estimator puts itself in the registry upon fit."""
    assert estimator in estimator.registry

def test_request_type_is_alias(val, res):
    # Test request_is_alias
    assert request_is_alias(val) == res

def test_request_type_is_valid(val, res):
    # Test request_is_valid
    assert request_is_valid(val) == res

def test_default_requests():
    class OddEstimator(BaseEstimator):
        __metadata_request__fit = {
            # set a different default request
            "sample_weight": True
        }  # type: ignore[var-annotated]

    odd_request = get_routing_for_object(OddEstimator())
    assert odd_request.fit.requests == {"sample_weight": True}

    # check other test estimators
    assert not len(get_routing_for_object(NonConsumingClassifier()).fit.requests)
    assert_request_is_empty(NonConsumingClassifier().get_metadata_routing())

    trs_request = get_routing_for_object(ConsumingTransformer())
    assert trs_request.fit.requests == {
        "sample_weight": None,
        "metadata": None,
    }
    assert trs_request.transform.requests == {"metadata": None, "sample_weight": None}
    assert_request_is_empty(trs_request)

    est_request = get_routing_for_object(ConsumingClassifier())
    assert est_request.fit.requests == {
    }
    assert_request_is_empty(est_request)

def test_default_request_override():
    """Test that default requests are correctly overridden regardless of the ASCII order
    of the class names, hence testing small and capital letter class name starts.
    Non-regression test for https://github.com/scikit-learn/scikit-learn/issues/28430
    """

    class Base(BaseEstimator):
        __metadata_request__split = {"groups": True}

    class class_1(Base):
        __metadata_request__split = {"groups": "sample_domain"}

    class Class_1(Base):

    assert_request_equal(
        class_1()._get_metadata_request(), {"split": {"groups": "sample_domain"}}
    )
        Class_1()._get_metadata_request(), {"split": {"groups": "sample_domain"}}
    )

def test_process_routing_invalid_method():
    with pytest.raises(TypeError, match="Can only route and process input"):
        process_routing(ConsumingClassifier(), "invalid_method", groups=my_groups)

def test_process_routing_invalid_object():
    class InvalidObject:
        pass

    with pytest.raises(AttributeError, match="either implement the routing method"):
        process_routing(InvalidObject(), "fit", groups=my_groups)

def test_process_routing_empty_params_get_with_default(method, default):
    empty_params = {}
    routed_params = process_routing(ConsumingClassifier(), "fit", **empty_params)

    # Behaviour should be an empty dictionary returned for each method when retrieved.
    params_for_method = routed_params[method]
    assert isinstance(params_for_method, dict)
    assert set(params_for_method.keys()) == set(METHODS)

    # No default to `get` should be equivalent to the default
    default_params_for_method = routed_params.get(method, default=default)
    assert default_params_for_method == params_for_method

def test_simple_metadata_routing():
    # Tests that metadata is properly routed

    # The underlying estimator doesn't accept or request metadata
    clf = WeightedMetaClassifier(estimator=NonConsumingClassifier())

    # Meta-estimator consumes sample_weight, but doesn't forward it to the underlying
    # estimator
    clf.fit(X, y, sample_weight=my_weights)

    # If the estimator accepts the metadata but doesn't explicitly say it doesn't
    # need it, there's an error
    clf = WeightedMetaClassifier(estimator=ConsumingClassifier())
    err_message = (
        "[sample_weight] are passed but are not explicitly set as requested or"
        " not requested for ConsumingClassifier.fit"
    )
    with pytest.raises(ValueError, match=re.escape(err_message)):
        clf.fit(X, y, sample_weight=my_weights)

    # Explicitly saying the estimator doesn't need it, makes the error go away,
    # because in this case `WeightedMetaClassifier` consumes `sample_weight`. If
    # there was no consumer of sample_weight, passing it would result in an
    # error.
    clf = WeightedMetaClassifier(
        estimator=ConsumingClassifier().set_fit_request(sample_weight=False)
    )
    # this doesn't raise since WeightedMetaClassifier itself is a consumer,
    # and passing metadata to the consumer directly is fine regardless of its
    # metadata_request values.
    check_recorded_metadata(clf.estimator_, method="fit", parent="fit")

    # Requesting a metadata will make the meta-estimator forward it correctly
        estimator=ConsumingClassifier().set_fit_request(sample_weight=True)
    )
        clf.estimator_, method="fit", parent="fit", sample_weight=my_weights
    )

    # And requesting it with an alias
        estimator=ConsumingClassifier().set_fit_request(
            sample_weight="alternative_weight"
        )
    )
    clf.fit(X, y, alternative_weight=my_weights)
    )

def test_nested_routing():
    # check if metadata is routed in a nested routing situation.
    pipeline = SimplePipeline(
        [
            MetaTransformer(
                transformer=ConsumingTransformer()
                .set_fit_request(metadata=True, sample_weight=False)
                .set_transform_request(sample_weight=True, metadata=False)
            ),
            WeightedMetaRegressor(
                estimator=ConsumingRegressor()
                .set_fit_request(sample_weight="inner_weights", metadata=False)
                .set_predict_request(sample_weight=False)
            ).set_fit_request(sample_weight="outer_weights"),
        ]
    )
    w1, w2, w3 = [1], [2], [3]
    pipeline.fit(
        X, y, metadata=my_groups, sample_weight=w1, outer_weights=w2, inner_weights=w3
    )
        pipeline.steps_[0].transformer_,
        metadata=my_groups,
    )
        sample_weight=w1,
    )
        pipeline.steps_[1], method="fit", parent="fit", sample_weight=w2
    )
        pipeline.steps_[1].estimator_, method="fit", parent="fit", sample_weight=w3
    )

    pipeline.predict(X, sample_weight=w3)
        sample_weight=w3,
    )

def test_nested_routing_conflict():
    # check if an error is raised if there's a conflict between keys
        [
                .set_transform_request(sample_weight=True)
            ),
                estimator=ConsumingRegressor().set_fit_request(sample_weight=True)
        ]
    )
    w1, w2 = [1], [2]
        match=(
            re.escape(
                "In WeightedMetaRegressor, there is a conflict on sample_weight between"
                " what is requested for this estimator and what is requested by its"
                " children. You can resolve this conflict by using an alias for the"
                " child estimator(s) requested metadata."
            )
        ),
    ):
        pipeline.fit(X, y, metadata=my_groups, sample_weight=w1, outer_weights=w2)

def test_invalid_metadata():
    # check that passing wrong metadata raises an error
    trs = MetaTransformer(
        transformer=ConsumingTransformer().set_transform_request(sample_weight=True)
    )
        match=(re.escape("transform got unexpected argument(s) {'other_param'}")),
    ):
        trs.fit(X, y).transform(X, other_param=my_weights)

    # passing a metadata which is not requested by any estimator should also raise
        transformer=ConsumingTransformer().set_transform_request(sample_weight=False)
    )
        match=(re.escape("transform got unexpected argument(s) {'sample_weight'}")),
    ):
        trs.fit(X, y).transform(X, sample_weight=my_weights)

def test_get_metadata_routing():
    class TestDefaultsBadMethodName(_MetadataRequester):
            "sample_weight": None,
            "my_param": None,
        }
        __metadata_request__score = {
            "my_param": True,
            "my_other_param": None,
        }
        # this will raise an error since we don't understand "other_method" as a method
        __metadata_request__other_method = {"my_param": True}

    class TestDefaults(_MetadataRequester):
        }
        }
        __metadata_request__predict = {"my_param": True}

        AttributeError, match="'MetadataRequest' object has no attribute 'other_method'"
    ):
        TestDefaultsBadMethodName().get_metadata_routing()

    expected = {
        "score": {
        },
        "fit": {
        },
        "predict": {"my_param": True},
    }
    assert_request_equal(TestDefaults().get_metadata_routing(), expected)

    est = TestDefaults().set_score_request(my_param="other_param")
        "score": {
            "my_param": "other_param",
        },
        "fit": {
        },
    }
    assert_request_equal(est.get_metadata_routing(), expected)

    est = TestDefaults().set_fit_request(sample_weight=True)
        "score": {
        },
        "fit": {
            "sample_weight": True,
        },
    }

def test_setting_default_requests():
    # Test _get_default_requests method
    test_cases = dict()

    class ExplicitRequest(BaseEstimator):
        # `fit` doesn't accept `props` explicitly, but we want to request it
        __metadata_request__fit = {"prop": None}

        def fit(self, X, y, **kwargs):

    test_cases[ExplicitRequest] = {"prop": None}

    class ExplicitRequestOverwrite(BaseEstimator):
        # `fit` explicitly accepts `props`, but we want to change the default
        # request value from None to True
        __metadata_request__fit = {"prop": True}

        def fit(self, X, y, prop=None, **kwargs):

    test_cases[ExplicitRequestOverwrite] = {"prop": True}

    class ImplicitRequest(BaseEstimator):
        # `fit` requests `prop` and the default None should be used

    test_cases[ImplicitRequest] = {"prop": None}

    class ImplicitRequestRemoval(BaseEstimator):
        # `fit` (in this class or a parent) requests `prop`, but we don't want
        # it requested at all.
        __metadata_request__fit = {"prop": metadata_routing.UNUSED}

    test_cases[ImplicitRequestRemoval] = {}

    for Klass, requests in test_cases.items():
        assert get_routing_for_object(Klass()).fit.requests == requests
        assert_request_is_empty(Klass().get_metadata_routing(), exclude="fit")
        Klass().fit(None, None)  # for coverage

def test_removing_non_existing_param_raises():
    """Test that removing a metadata using UNUSED which doesn't exist raises."""

    class InvalidRequestRemoval(BaseEstimator):

    with pytest.raises(ValueError, match="Trying to remove parameter"):
        InvalidRequestRemoval().get_metadata_routing()

def test_method_metadata_request():
    mmr = MethodMetadataRequest(owner="test", method="fit")

    with pytest.raises(ValueError, match="The alias you're setting for"):
        mmr.add_request(param="foo", alias=1.4)

    mmr.add_request(param="foo", alias=None)
    assert mmr.requests == {"foo": None}
    mmr.add_request(param="foo", alias=False)
    assert mmr.requests == {"foo": False}
    mmr.add_request(param="foo", alias=True)
    assert mmr.requests == {"foo": True}
    mmr.add_request(param="foo", alias="foo")
    mmr.add_request(param="foo", alias="bar")
    assert mmr.requests == {"foo": "bar"}
    assert mmr._get_param_names(return_alias=False) == {"foo"}
    assert mmr._get_param_names(return_alias=True) == {"bar"}

def test_get_routing_for_object():
    class Consumer(BaseEstimator):

    assert_request_is_empty(get_routing_for_object(None))
    assert_request_is_empty(get_routing_for_object(object()))

    mr = MetadataRequest(owner="test")
    mr.fit.add_request(param="foo", alias="bar")
    mr_factory = get_routing_for_object(mr)
    assert_request_is_empty(mr_factory, exclude="fit")
    assert mr_factory.fit.requests == {"foo": "bar"}

    mr = get_routing_for_object(Consumer())
    assert_request_is_empty(mr, exclude="fit")
    assert mr.fit.requests == {"prop": None}

def test_metadata_request_consumes_method():
    """Test that MetadataRequest().consumes() method works as expected."""
    request = MetadataRouter(owner="test")
    assert request.consumes(method="fit", params={"foo"}) == set()

    request = MetadataRequest(owner="test")
    request.fit.add_request(param="foo", alias=True)
    assert request.consumes(method="fit", params={"foo"}) == {"foo"}

    request.fit.add_request(param="foo", alias="bar")
    assert request.consumes(method="fit", params={"bar", "foo"}) == {"bar"}

def test_metadata_router_consumes_method():
    """Test that MetadataRouter().consumes method works as expected."""
    # having it here instead of parametrizing the test since `set_fit_request`
    # is not available while collecting the tests.
    cases = [
        (
            ),
            {"sample_weight"},
        ),
        (
                estimator=ConsumingRegressor().set_fit_request(
                    sample_weight="my_weights"
                )
            ),
            {"my_weights", "sample_weight"},
            {"my_weights"},
        ),
    ]

    for obj, input, output in cases:
        assert obj.get_metadata_routing().consumes(method="fit", params=input) == output

def test_metaestimator_warnings():
    class WeightedMetaRegressorWarn(WeightedMetaRegressor):
        __metadata_request__fit = {"sample_weight": metadata_routing.WARN}

        UserWarning, match="Support for .* has recently been added to this class"
    ):
        WeightedMetaRegressorWarn(
            estimator=LinearRegression().set_fit_request(sample_weight=False)
        ).fit(X, y, sample_weight=my_weights)

def test_estimator_warnings():
    class ConsumingRegressorWarn(ConsumingRegressor):

    ):
        MetaRegressor(estimator=ConsumingRegressorWarn()).fit(
            X, y, sample_weight=my_weights
        )

def test_string_representations(obj, string):
    assert str(obj) == string

def test_validations(obj, method, inputs, err_cls, err_msg):
    with pytest.raises(err_cls, match=err_msg):
        getattr(obj, method)(**inputs)

def test_methodmapping():
    mm = (
        MethodMapping()
        .add(caller="fit", callee="transform")
        .add(caller="fit", callee="fit")
    )

    mm_list = list(mm)
    assert mm_list[0] == ("fit", "transform")
    assert mm_list[1] == ("fit", "fit")

    mm = MethodMapping()
    for method in METHODS:
        mm.add(caller=method, callee=method)
        assert MethodPair(method, method) in mm._routes
    assert len(mm._routes) == len(METHODS)

    mm = MethodMapping().add(caller="score", callee="score")
    assert repr(mm) == "[{'caller': 'score', 'callee': 'score'}]"

def test_metadatarouter_add_self_request():
    # adding a MetadataRequest as `self` adds a copy
    request = MetadataRequest(owner="nested")
    request.fit.add_request(param="param", alias=True)
    router = MetadataRouter(owner="test").add_self_request(request)
    assert str(router._self_request) == str(request)
    # should be a copy, not the same object
    assert router._self_request is not request

    # one can add an estimator as self
    est = ConsumingRegressor().set_fit_request(sample_weight="my_weights")
    router = MetadataRouter(owner="test").add_self_request(obj=est)
    assert str(router._self_request) == str(est.get_metadata_routing())
    assert router._self_request is not est.get_metadata_routing()

    # adding a consumer+router as self should only add the consumer part
    est = WeightedMetaRegressor(
        estimator=ConsumingRegressor().set_fit_request(sample_weight="nested_weights")
    )
    # _get_metadata_request() returns the consumer part of the requests
    assert str(router._self_request) == str(est._get_metadata_request())
    # get_metadata_routing() returns the complete request set, consumer and
    # router included.
    assert str(router._self_request) != str(est.get_metadata_routing())
    # it should be a copy, not the same object
    assert router._self_request is not est._get_metadata_request()

def test_metadata_routing_add():
    # adding one with a string `method_mapping`
        est=ConsumingRegressor().set_fit_request(sample_weight="weights"),
        method_mapping=MethodMapping().add(caller="fit", callee="fit"),
    )
    assert (
        str(router)
        == "{'est': {'mapping': [{'caller': 'fit', 'callee': 'fit'}], 'router': {'fit':"
        " {'sample_weight': 'weights', 'metadata': None}, 'partial_fit':"
        " {'sample_weight': None, 'metadata': None}, 'predict': {'sample_weight':"
        " None, 'metadata': None}, 'score': {'sample_weight': None, 'metadata':"
        " None}}}}"
    )

    # adding one with an instance of MethodMapping
        method_mapping=MethodMapping().add(caller="fit", callee="score"),
        est=ConsumingRegressor().set_score_request(sample_weight=True),
    )
    assert (
        == "{'est': {'mapping': [{'caller': 'fit', 'callee': 'score'}], 'router':"
        " {'fit': {'sample_weight': None, 'metadata': None}, 'partial_fit':"
        " None, 'metadata': None}, 'score': {'sample_weight': True, 'metadata':"
    )

def test_metadata_routing_get_param_names():
    router = (
        .add_self_request(
            WeightedMetaRegressor(estimator=ConsumingRegressor()).set_fit_request(
                sample_weight="self_weights"
            )
        )
        .add(
            trs=ConsumingTransformer().set_fit_request(
                sample_weight="transform_weights"
            ),
        )
    )

    assert (
        == "{'$self_request': {'fit': {'sample_weight': 'self_weights'}, 'score':"
        " {'sample_weight': None}}, 'trs': {'mapping': [{'caller': 'fit', 'callee':"
        " 'fit'}], 'router': {'fit': {'sample_weight': 'transform_weights',"
        " 'metadata': None}, 'transform': {'sample_weight': None, 'metadata': None},"
        " 'inverse_transform': {'sample_weight': None, 'metadata': None}}}}"
    )

    assert router._get_param_names(
        method="fit", return_alias=True, ignore_self_request=False
    ) == {"transform_weights", "metadata", "self_weights"}
    # return_alias=False will return original names for "self"
        method="fit", return_alias=False, ignore_self_request=False
    ) == {"sample_weight", "metadata", "transform_weights"}
    # ignoring self would remove "sample_weight"
        method="fit", return_alias=False, ignore_self_request=True
    ) == {"metadata", "transform_weights"}
    # return_alias is ignored when ignore_self_request=True
        method="fit", return_alias=True, ignore_self_request=True
    ) == router._get_param_names(
    )

def test_method_generation():
    # Test if all required request methods are generated.

    # TODO: these test classes can be moved to sklearn.utils._testing once we
    # have a better idea of what the commonly used classes are.
    class SimpleEstimator(BaseEstimator):
        # This class should have no set_{method}_request

        def fit_transform(self, X, y):

        def fit_predict(self, X, y):

        def partial_fit(self, X, y):

        def split(self, X, y=None):

        assert not hasattr(SimpleEstimator(), f"set_{method}_request")

        # This class should have every set_{method}_request

        def fit_transform(self, X, y, sample_weight=None):

        def fit_predict(self, X, y, sample_weight=None):

        def partial_fit(self, X, y, sample_weight=None):

        def predict(self, X, sample_weight=None):

        def predict_proba(self, X, sample_weight=None):

        def predict_log_proba(self, X, sample_weight=None):

        def decision_function(self, X, sample_weight=None):

        def score(self, X, y, sample_weight=None):

        def split(self, X, y=None, sample_weight=None):

        def transform(self, X, sample_weight=None):

        def inverse_transform(self, X, sample_weight=None):

    # composite methods shouldn't have a corresponding set method.
    for method in COMPOSITE_METHODS:

    # simple methods should have a corresponding set method.
    for method in SIMPLE_METHODS:
        assert hasattr(SimpleEstimator(), f"set_{method}_request")

def test_composite_methods():
    # Test the behavior and the values of methods (composite methods) whose
    # request values are a union of requests by other methods (simple methods).
    # fit_transform and fit_predict are the only composite methods we have in
    # scikit-learn.
        def fit(self, X, y, foo=None, bar=None):

        def predict(self, X, foo=None, bar=None):

        def transform(self, X, other_param=None):

    # Since no request is set for fit or predict or transform, the request for
    # fit_transform and fit_predict should also be empty.
    assert est.get_metadata_routing().fit_transform.requests == {
        "bar": None,
        "foo": None,
        "other_param": None,
    }
    assert est.get_metadata_routing().fit_predict.requests == {"bar": None, "foo": None}

    # setting the request on only one of them should raise an error
    est.set_fit_request(foo=True, bar="test")
    with pytest.raises(ValueError, match="Conflicting metadata requests for"):
        est.get_metadata_routing().fit_predict

    # setting the request on the other one should fail if not the same as the
    # first method
    est.set_predict_request(bar=True)

    # now the requests are consistent and getting the requests for fit_predict
    # shouldn't raise.
    est.set_predict_request(foo=True, bar="test")
    est.get_metadata_routing().fit_predict

    # setting the request for a none-overlapping parameter would merge them
    # together.
    est.set_transform_request(other_param=True)
        "bar": "test",
        "foo": True,
        "other_param": True,
    }

def test_no_feature_flag_raises_error():
    """Test that when feature flag disabled, set_{method}_requests raises."""
        with pytest.raises(RuntimeError, match="This method is only available"):
            ConsumingClassifier().set_fit_request(sample_weight=True)

def test_none_metadata_passed():
    """Test that passing None as metadata when not requested doesn't raise"""
    MetaRegressor(estimator=ConsumingRegressor()).fit(X, y, sample_weight=None)

def test_no_metadata_always_works():
    """Test that when no metadata is passed, having a meta-estimator which does
    not yet support metadata routing works.

    Non-regression test for https://github.com/scikit-learn/scikit-learn/issues/28246
    """

    class Estimator(_RoutingNotSupportedMixin, BaseEstimator):

    # This passes since no metadata is passed.
    MetaRegressor(estimator=Estimator()).fit(X, y)
    # This fails since metadata is passed but Estimator() does not support it.
        NotImplementedError, match="Estimator has not implemented metadata routing yet."
    ):
        MetaRegressor(estimator=Estimator()).fit(X, y, metadata=my_groups)

def test_unsetmetadatapassederror_correct():
    """Test that UnsetMetadataPassedError raises the correct error message when
    set_{method}_request is not set in nested cases."""
    weighted_meta = WeightedMetaClassifier(estimator=ConsumingClassifier())
    pipe = SimplePipeline([weighted_meta])
        "[metadata] are passed but are not explicitly set as requested or not requested"
        " for ConsumingClassifier.fit, which is used within WeightedMetaClassifier.fit."
        " Call `ConsumingClassifier.set_fit_request({metadata}=True/False)` for each"
        " metadata you want to request/ignore."
    )

    with pytest.raises(UnsetMetadataPassedError, match=msg):
        pipe.fit(X, y, metadata="blah")

def test_unsetmetadatapassederror_correct_for_composite_methods():
    composite metadata request methods are not set in nested cases."""
    consuming_transformer = ConsumingTransformer()
    pipe = Pipeline([("consuming_transformer", consuming_transformer)])

        " for ConsumingTransformer.fit_transform, which is used within"
        " Pipeline.fit_transform. Call"
        " `ConsumingTransformer.set_fit_request({metadata}=True/False)"
        ".set_transform_request({metadata}=True/False)`"
        " for each metadata you want to request/ignore."
    )
        pipe.fit_transform(X, y, metadata="blah")

def test_unbound_set_methods_work():
    """Tests that if the set_{method}_request is unbound, it still works.

    Also test that passing positional arguments to the set_{method}_request fails
    with the right TypeError message.

    Non-regression test for https://github.com/scikit-learn/scikit-learn/issues/28632
    """

    class A(BaseEstimator):

        "set_fit_request() takes 0 positional argument but 1 were given"
    )

    # Test positional arguments error before making the descriptor method unbound.
    with pytest.raises(TypeError, match=error_message):
        A().set_fit_request(True)

    # This somehow makes the descriptor method unbound, which results in the `instance`
    # argument being None, and instead `self` being passed as a positional argument
    # to the descriptor method.
    A.set_fit_request = A.set_fit_request

    # This should pass as usual
    A().set_fit_request(sample_weight=True)

    # Test positional arguments error after making the descriptor method unbound.

            )
            )

        )

            )
        )

        }
        }

        }
        }

def record_metadata(obj, record_default=True, **kwargs):
    """Utility function to store passed metadata to a method of obj.

    If record_default is False, kwargs whose values are "default" are skipped.
    This is so that checks on keyword arguments whose default was not changed
    are skipped.

    """
    stack = inspect.stack()
    callee = stack[1].function
    caller = stack[2].function
    if not hasattr(obj, "_records"):
        obj._records = defaultdict(lambda: defaultdict(list))
    if not record_default:
        kwargs = {
            key: val
            for key, val in kwargs.items()
            if not isinstance(val, str) or (val != "default")
        }
    obj._records[callee][caller].append(kwargs)

def check_recorded_metadata(obj, method, parent, split_params=tuple(), **kwargs):
    """Check whether the expected metadata is passed to the object's method.

    obj : estimator object
        sub-estimator to check routed params for
    method : str
        sub-estimator's method where metadata is routed to, or otherwise in
        the context of metadata routing referred to as 'callee'
    parent : str
        the parent method which should have called `method`, or otherwise in
        the context of metadata routing referred to as 'caller'
    split_params : tuple, default=empty
        specifies any parameters which are to be checked as being a subset
        of the original values
    **kwargs : dict
        passed metadata
    """
    all_records = (
        getattr(obj, "_records", dict()).get(method, dict()).get(parent, list())
    )
    for record in all_records:
        # first check that the names of the metadata passed are the same as
        # expected. The names are stored as keys in `record`.
        assert set(kwargs.keys()) == set(record.keys()), (
            f"Expected {kwargs.keys()} vs {record.keys()}"
        )
        for key, value in kwargs.items():
            recorded_value = record[key]
            # The following condition is used to check for any specified parameters
            # being a subset of the original values
            if key in split_params and recorded_value is not None:
                assert np.isin(recorded_value, value).all()
            else:
                if isinstance(recorded_value, np.ndarray):
                    assert_array_equal(recorded_value, value)
                else:
                    assert recorded_value is value, (
                        f"Expected {recorded_value} vs {value}. Method: {method}"
                    )

def assert_request_is_empty(metadata_request, exclude=None):
    """Check if a metadata request dict is empty.

    One can exclude a method or a list of methods from the check using the
    ``exclude`` parameter. If metadata_request is a MetadataRouter, then
    ``exclude`` can be of the form ``{"object" : [method, ...]}``.
    """
    if isinstance(metadata_request, MetadataRouter):
        for name, route_mapping in metadata_request:
            if exclude is not None and name in exclude:
                _exclude = exclude[name]
            else:
                _exclude = None
            assert_request_is_empty(route_mapping.router, exclude=_exclude)
        return

    exclude = [] if exclude is None else exclude
        if method in exclude:
            continue
        mmr = getattr(metadata_request, method)
        props = [
            prop
            for prop, alias in mmr.requests.items()
            if isinstance(alias, str) or alias is not None
        ]
        assert not props

def assert_request_equal(request, dictionary):
    for method, requests in dictionary.items():
        mmr = getattr(request, method)
        assert mmr.requests == requests

    empty_methods = [method for method in SIMPLE_METHODS if method not in dictionary]
    for method in empty_methods:
        assert not len(getattr(request, method).requests)

class _Registry(list):
    # This list is used to get a reference to the sub-estimators, which are not
    # necessarily stored on the metaestimator. We need to override __deepcopy__
    # because the sub-estimators are probably cloned, which would result in a
    # new copy of the list, but we need copy and deep copy both to return the
    # same instance.
    def __deepcopy__(self, memo):

    def __copy__(self):

class ConsumingRegressor(RegressorMixin, BaseEstimator):
    """A regressor consuming metadata.

    registry : list, default=None
        If a list, the estimator will append itself to the list in order to have
        a reference to the estimator later on. Since that reference is not
        required in all tests, registration can be skipped by leaving this value
        as None.
    """

    def __init__(self, registry=None):
        self.registry = registry

    def partial_fit(self, X, y, sample_weight="default", metadata="default"):
        if self.registry is not None:
            self.registry.append(self)

        record_metadata_not_default(
            self, sample_weight=sample_weight, metadata=metadata
        )

    def fit(self, X, y, sample_weight="default", metadata="default"):

        )

    def predict(self, X, y=None, sample_weight="default", metadata="default"):
        )
        return np.zeros(shape=(len(X),))

    def score(self, X, y, sample_weight="default", metadata="default"):
        )
        return 1

class NonConsumingClassifier(ClassifierMixin, BaseEstimator):
    """A classifier which accepts no metadata on any method."""

    def __init__(self, alpha=0.0):

        self.coef_ = np.ones_like(X)

    def partial_fit(self, X, y, classes=None):

        y_pred = np.empty(shape=(len(X),))
        y_pred[: len(X) // 2] = 0
        y_pred[len(X) // 2 :] = 1

        # dummy probabilities to support predict_proba
        y_proba = np.empty(shape=(len(X), len(self.classes_)), dtype=np.float32)
        # each row sums up to 1.0:
        y_proba[:] = np.random.dirichlet(alpha=np.ones(len(self.classes_)), size=len(X))
        return y_proba

        # dummy probabilities to support predict_log_proba
        return self.predict_proba(X)

class NonConsumingRegressor(RegressorMixin, BaseEstimator):

    def partial_fit(self, X, y):

        return np.ones(len(X))  # pragma: no cover

class ConsumingClassifier(ClassifierMixin, BaseEstimator):
    """A classifier consuming metadata.

        as None.

    alpha : float, default=0
        This parameter is only used to test the ``*SearchCV`` objects, and
        doesn't do anything.
    """

    def __init__(self, registry=None, alpha=0.0):

    def partial_fit(
        self, X, y, classes=None, sample_weight="default", metadata="default"
    ):

        )
        _check_partial_fit_first_call(self, classes)

        )

    def predict(self, X, sample_weight="default", metadata="default"):
        )
        y_score = np.empty(shape=(len(X),), dtype="int8")
        y_score[len(X) // 2 :] = 0
        y_score[: len(X) // 2] = 1
        return y_score

    def predict_proba(self, X, sample_weight="default", metadata="default"):
        )

    def predict_log_proba(self, X, sample_weight="default", metadata="default"):
        )

    def decision_function(self, X, sample_weight="default", metadata="default"):
        )
        y_score = np.empty(shape=(len(X),))

        )
        return 1

class ConsumingClassifierWithoutPredictProba(ConsumingClassifier):
    """ConsumingClassifier without a predict_proba method, but with predict_log_proba.

    Used to mimic dynamic method selection such as in the `_parallel_predict_proba()`
    function called by `BaggingClassifier`.
    """

    @property
    def predict_proba(self):
        raise AttributeError("This estimator does not support predict_proba")

class ConsumingClassifierWithoutPredictLogProba(ConsumingClassifier):
    """ConsumingClassifier without a predict_log_proba method, but with predict_proba.

    Used to mimic dynamic method selection such as in
    `BaggingClassifier.predict_log_proba()`.
    """

    @property
    def predict_log_proba(self):
        raise AttributeError("This estimator does not support predict_log_proba")

class ConsumingClassifierWithOnlyPredict(ConsumingClassifier):
    """ConsumingClassifier with only a predict method.

    """

    @property

    @property

class ConsumingTransformer(TransformerMixin, BaseEstimator):
    """A transformer which accepts metadata on fit and transform.

        as None.
    """

    def fit(self, X, y=None, sample_weight="default", metadata="default"):

        )

    def transform(self, X, sample_weight="default", metadata="default"):
        )
        return X + 1

    def fit_transform(self, X, y, sample_weight="default", metadata="default"):
        # implementing ``fit_transform`` is necessary since
        # ``TransformerMixin.fit_transform`` doesn't route any metadata to
        # ``transform``, while here we want ``transform`` to receive
        # ``sample_weight`` and ``metadata``.
        )
        return self.fit(X, y, sample_weight=sample_weight, metadata=metadata).transform(
            X, sample_weight=sample_weight, metadata=metadata
        )

    def inverse_transform(self, X, sample_weight=None, metadata=None):
        )
        return X - 1

class ConsumingNoFitTransformTransformer(BaseEstimator):
    """A metadata consuming transformer that doesn't inherit from
    TransformerMixin, and thus doesn't implement `fit_transform`. Note that
    TransformerMixin's `fit_transform` doesn't route metadata to `transform`."""

    def fit(self, X, y=None, sample_weight=None, metadata=None):

        record_metadata(self, sample_weight=sample_weight, metadata=metadata)

    def transform(self, X, sample_weight=None, metadata=None):
        return X

class ConsumingScorer(_Scorer):
            score_func=mean_squared_error, sign=1, kwargs={}, response_method="predict"
        )

    def _score(self, method_caller, clf, X, y, **kwargs):

        record_metadata_not_default(self, **kwargs)

        sample_weight = kwargs.get("sample_weight", None)
        return super()._score(method_caller, clf, X, y, sample_weight=sample_weight)

class ConsumingSplitter(GroupsConsumerMixin, BaseCrossValidator):

    def split(self, X, y=None, groups="default", metadata="default"):

        record_metadata_not_default(self, groups=groups, metadata=metadata)

        split_index = len(X) // 2
        train_indices = list(range(0, split_index))
        test_indices = list(range(split_index, len(X)))
        yield test_indices, train_indices
        yield train_indices, test_indices

    def get_n_splits(self, X=None, y=None, groups=None, metadata=None):
        return 2

    def _iter_test_indices(self, X=None, y=None, groups=None):
        yield test_indices
        yield train_indices

class MetaRegressor(MetaEstimatorMixin, RegressorMixin, BaseEstimator):
    """A meta-regressor which is only a router."""

        self.estimator_ = clone(self.estimator).fit(X, y, **params.estimator.fit)

        )

class WeightedMetaRegressor(MetaEstimatorMixin, RegressorMixin, BaseEstimator):
    """A meta-regressor which is also a consumer."""

    def __init__(self, estimator, registry=None):

        record_metadata(self, sample_weight=sample_weight)
        params = process_routing(self, "fit", sample_weight=sample_weight, **fit_params)

        return self.estimator_.predict(X, **params.estimator.predict)

        router = (
            .add(
                .add(caller="predict", callee="predict"),
            )
        )

class WeightedMetaClassifier(MetaEstimatorMixin, ClassifierMixin, BaseEstimator):
    """A meta-estimator which also consumes sample_weight itself in ``fit``."""

    def fit(self, X, y, sample_weight=None, **kwargs):

        params = process_routing(self, "fit", sample_weight=sample_weight, **kwargs)

        router = (
            .add(
            )
        )

class MetaTransformer(MetaEstimatorMixin, TransformerMixin, BaseEstimator):
    """A simple meta-transformer."""

    def __init__(self, transformer):
        self.transformer = transformer

        self.transformer_ = clone(self.transformer).fit(X, y, **params.transformer.fit)

    def transform(self, X, y=None, **transform_params):
        params = process_routing(self, "transform", **transform_params)
        return self.transformer_.transform(X, **params.transformer.transform)

            transformer=self.transformer,
            .add(caller="transform", callee="transform"),
        )

        )

        )

        )

        )
        return 1

    ):

        )

        )

        )

        )

        )

        )

        )

        )
        return X + 1

        )
        )

        )
        return X - 1

        return X

        )

        return 2

        )

        router = (
            .add(
            )
        )

        router = (
            .add(
            )
        )

        )

def test_class_docstring_consistency(case):
    """Check docstrings parameters consistency between related classes."""
    assert_docstring_consistency(**case)

def test_function_docstring_consistency(case):
    """Check docstrings parameters consistency between related functions."""

def get_init_args(metaestimator_info, sub_estimator_consumes):
    """Get the init args for a metaestimator

    This is a helper function to get the init args for a metaestimator from
    the METAESTIMATORS list. It returns an empty dict if no init args are
    required.

    metaestimator_info : dict
        The metaestimator info from METAESTIMATORS

    sub_estimator_consumes : bool
        Whether the sub-estimator consumes metadata or not.

    Returns
    -------
        The init args for the metaestimator.

    (estimator, estimator_registry) : (estimator, registry)
        The sub-estimator and the corresponding registry.

    (scorer, scorer_registry) : (scorer, registry)
        The scorer and the corresponding registry.

    (cv, cv_registry) : (CV splitter, registry)
        The CV splitter and the corresponding registry.
    """
    kwargs = metaestimator_info.get("init_args", {})
    estimator, estimator_registry = None, None
    scorer, scorer_registry = None, None
    cv, cv_registry = None, None
    if "estimator" in metaestimator_info:
        estimator_name = metaestimator_info["estimator_name"]
        estimator_registry = _Registry()
        sub_estimator_type = metaestimator_info["estimator"]
        if sub_estimator_consumes:
            if sub_estimator_type == "regressor":
                estimator = ConsumingRegressor(estimator_registry)
            elif sub_estimator_type == "classifier":
                estimator = ConsumingClassifier(estimator_registry)
            else:
                raise ValueError("Unpermitted `sub_estimator_type`.")  # pragma: nocover
        else:
                estimator = NonConsumingRegressor()
                estimator = NonConsumingClassifier()
            else:
        kwargs[estimator_name] = estimator
    if "scorer_name" in metaestimator_info:
        scorer_name = metaestimator_info["scorer_name"]
        scorer_registry = _Registry()
        scorer = ConsumingScorer(registry=scorer_registry)
        kwargs[scorer_name] = scorer
    if "cv_name" in metaestimator_info:
        cv_name = metaestimator_info["cv_name"]
        cv_registry = _Registry()
        cv = ConsumingSplitter(registry=cv_registry)
        kwargs[cv_name] = cv

    return (
        kwargs,
        (estimator, estimator_registry),
        (scorer, scorer_registry),
        (cv, cv_registry),
    )

def set_requests(obj, *, method_mapping, methods, metadata_name, value=True):
    """Call `set_{method}_request` on a list of methods from the sub-estimator.

    obj : BaseEstimator
        The object for which `set_{method}_request` methods are called.

    method_mapping : dict
        The method mapping in the form of `{caller: [callee, ...]}`.
        If a "caller" is not present in the method mapping, a one-to-one mapping is
        assumed.

    methods : list of str
        The list of methods as "caller"s for which the request for the child should
        be set.

    metadata_name : str
        The name of the metadata to be routed, usually either `"metadata"` or
        `"sample_weight"` in our tests.

    value : None, bool, or str
        The request value to be set, by default it's `True`
    """
    for caller in methods:
        for callee in method_mapping.get(caller, [caller]):
            set_request_for_method = getattr(obj, f"set_{callee}_request")
            set_request_for_method(**{metadata_name: value})
            if (
                isinstance(obj, BaseEstimator)
                and is_classifier(obj)
                and callee == "partial_fit"
            ):
                set_request_for_method(classes=True)

def test_unsupported_estimators_get_metadata_routing(estimator):
    """Test that get_metadata_routing is not implemented on meta-estimators for
    which we haven't implemented routing yet."""
        estimator.get_metadata_routing()

def test_unsupported_estimators_fit_with_metadata(estimator):
    """Test that fit raises NotImplementedError when metadata routing is
    enabled and a metadata is passed on meta-estimators for which we haven't
    implemented routing yet."""
        try:
            estimator.fit([[1]], [1], sample_weight=[1])
            # not all meta-estimators in the list support sample_weight,
            # and for those we skip this test.
            raise NotImplementedError

def test_registry_copy():
    # test that _Registry is not copied into a new instance.
    a = _Registry()
    b = _Registry()
    assert a is not b
    assert a is copy.copy(a)
    assert a is copy.deepcopy(a)

def test_default_request(metaestimator):
    # Check that by default request is empty and the right type
    metaestimator_class = metaestimator["metaestimator"]
    kwargs, *_ = get_init_args(metaestimator, sub_estimator_consumes=True)
    instance = metaestimator_class(**kwargs)
    if "cv_name" in metaestimator:
        # Our GroupCV splitters request groups by default, which we should
        # ignore in this test.
        exclude = {"splitter": ["split"]}
    else:
        exclude = None
    assert_request_is_empty(instance.get_metadata_routing(), exclude=exclude)
    assert isinstance(instance.get_metadata_routing(), MetadataRouter)

def test_error_on_missing_requests_for_sub_estimator(metaestimator):
    # Test that a UnsetMetadataPassedError is raised when the sub-estimator's
    # requests are not set
    if "estimator" not in metaestimator:
        # This test only makes sense for metaestimators which have a
        # sub-estimator, e.g. MyMetaEstimator(estimator=MySubEstimator())
        return

    X = metaestimator["X"]
    y = metaestimator["y"]
    routing_methods = metaestimator["estimator_routing_methods"]

    for method_name in routing_methods:
        for key in ["sample_weight", "metadata"]:
            kwargs, (estimator, _), (scorer, _), *_ = get_init_args(
                metaestimator, sub_estimator_consumes=True
            )
            if scorer:
                scorer.set_score_request(**{key: True})
            val = {"sample_weight": sample_weight, "metadata": metadata}[key]
            method_kwargs = {key: val}
            instance = metaestimator_class(**kwargs)
            msg = (
                f"[{key}] are passed but are not explicitly set as requested or not"
                f" requested for {estimator.__class__.__name__}.{method_name}"
            )
            with pytest.raises(UnsetMetadataPassedError, match=re.escape(msg)):
                method = getattr(instance, method_name)
                if "fit" not in method_name:
                    # set request on fit
                    set_requests(
                        estimator,
                        method_mapping=metaestimator.get("method_mapping", {}),
                        methods=["fit"],
                        metadata_name=key,
                    )
                    instance.fit(X, y, **method_kwargs)
                # making sure the requests are unset, in case they were set as a
                # side effect of setting them for fit. For instance, if method
                # mapping for fit is: `"fit": ["fit", "score"]`, that would mean
                # calling `.score` here would not raise, because we have already
                # set request value for child estimator's `score`.
                set_requests(
                    method_mapping=metaestimator.get("method_mapping", {}),
                    methods=["fit"],
                    metadata_name=key,
                    value=None,
                )
                try:
                    # `fit`, `partial_fit`, 'score' accept y, others don't.
                    method(X, y, **method_kwargs)
                except TypeError:
                    method(X, **method_kwargs)

def test_setting_request_on_sub_estimator_removes_error(metaestimator):
    # When the metadata is explicitly requested on the sub-estimator, there
    # should be no errors.
        return

    method_mapping = metaestimator.get("method_mapping", {})
    preserves_metadata = metaestimator.get("preserves_metadata", True)

            kwargs, (estimator, registry), (scorer, _), (cv, _) = get_init_args(
            )
            if scorer:
                    scorer, method_mapping={}, methods=["score"], metadata_name=key
                )
            if cv:
                cv.set_split_request(groups=True, metadata=True)

            # `set_{method}_request({metadata}==True)` on the underlying objects
            set_requests(
                method_mapping=method_mapping,
                methods=[method_name],
                metadata_name=key,
            )

            method = getattr(instance, method_name)
            extra_method_args = metaestimator.get("method_args", {}).get(
                method_name, {}
            )
            if "fit" not in method_name:
                # fit before calling method
                instance.fit(X, y)
            try:
                # `fit` and `partial_fit` accept y, others don't.
                method(X, y, **method_kwargs, **extra_method_args)
                method(X, **method_kwargs, **extra_method_args)

            # sanity check that registry is not empty, or else the test passes
            # trivially
            assert registry
            split_params = (
                method_kwargs.keys() if preserves_metadata == "subset" else ()
            )
            for estimator in registry:
                    parent=method_name,
                    split_params=split_params,
                    **method_kwargs,
                )

def test_non_consuming_estimator_works(metaestimator):
    # Test that when a non-consuming estimator is given, the meta-estimator
    # works w/o setting any requests.
    # Regression test for https://github.com/scikit-learn/scikit-learn/issues/28239
        return

    def set_request(estimator, method_name):
        # e.g. call set_fit_request on estimator
        if is_classifier(estimator) and method_name == "partial_fit":
            estimator.set_partial_fit_request(classes=True)

        kwargs, (estimator, _), (_, _), (_, _) = get_init_args(
            metaestimator, sub_estimator_consumes=False
        )
        instance = metaestimator_class(**kwargs)
        set_request(estimator, method_name)
        method = getattr(instance, method_name)
        extra_method_args = metaestimator.get("method_args", {}).get(method_name, {})
        if "fit" not in method_name:
            instance.fit(X, y, **extra_method_args)
        # The following should pass w/o raising a routing error.
        try:
            # `fit` and `partial_fit` accept y, others don't.
            method(X, y, **extra_method_args)
            method(X, **extra_method_args)

def test_metadata_is_routed_correctly_to_scorer(metaestimator):
    """Test that any requested metadata is correctly routed to the underlying
    scorers in CV estimators.
    """
    if "scorer_name" not in metaestimator:
        # This test only makes sense for CV estimators
        return

    routing_methods = metaestimator["scorer_routing_methods"]

        kwargs, (estimator, _), (scorer, registry), (cv, _) = get_init_args(
            metaestimator, sub_estimator_consumes=True
        )
        scorer.set_score_request(sample_weight=True)
        if cv:
            cv.set_split_request(groups=True, metadata=True)
                metadata_name="sample_weight",
            )
        method_kwargs = {"sample_weight": sample_weight}
            instance.fit(X, y)
        method(X, y, **method_kwargs)

        for _scorer in registry:
                obj=_scorer,
                method="score",
                parent=method_name,
                split_params=("sample_weight",),
                **method_kwargs,
            )

def test_metadata_is_routed_correctly_to_splitter(metaestimator):
    splitters in CV estimators.
    """
    if "cv_routing_methods" not in metaestimator:
        # This test is only for metaestimators accepting a CV splitter
        return

    routing_methods = metaestimator["cv_routing_methods"]
    X_ = metaestimator["X"]
    y_ = metaestimator["y"]

        kwargs, (estimator, _), (scorer, _), (cv, registry) = get_init_args(
        )
        if estimator:
            estimator.set_fit_request(sample_weight=False, metadata=False)
        if scorer:
            scorer.set_score_request(sample_weight=False, metadata=False)
        cv.set_split_request(groups=True, metadata=True)
        method_kwargs = {"groups": groups, "metadata": metadata}
        method(X_, y_, **method_kwargs)
        for _splitter in registry:
                obj=_splitter, method="split", parent=method_name, **method_kwargs
            )

def test_metadata_routed_to_group_splitter(metaestimator):
    """Test that groups are routed correctly if group splitter of CV estimator is used
    within cross_validate. Regression test for issue described in PR #29634 to test that
    `ValueError: The 'groups' parameter should not be None.` is not raised."""

        return

    # remove `ConsumingSplitter` from kwargs, so 'cv' param isn't passed twice:
    kwargs.pop("cv", None)
    instance = metaestimator_class(cv=GroupKFold(n_splits=2), **kwargs)
    cross_validate(
        instance,
        X_,
        y_,
        params={"groups": groups},
        cv=GroupKFold(n_splits=2),
        scoring=make_scorer(mean_squared_error, response_method="predict"),
    )

def test_config_context():
    assert get_config() == {
        "assume_finite": False,
        "working_memory": 1024,
        "print_changed_only": True,
        "display": "diagram",
        "array_api_dispatch": False,
        "pairwise_dist_chunk_size": 256,
        "enable_cython_pairwise_dist": True,
        "transform_output": "default",
        "enable_metadata_routing": False,
        "skip_parameter_validation": False,
    }

    # Not using as a context manager affects nothing
    config_context(assume_finite=True)
    assert get_config()["assume_finite"] is False

        assert get_config() == {
            "assume_finite": True,
            "working_memory": 1024,
            "print_changed_only": True,
            "display": "diagram",
            "array_api_dispatch": False,
            "pairwise_dist_chunk_size": 256,
            "enable_cython_pairwise_dist": True,
            "transform_output": "default",
            "enable_metadata_routing": False,
            "skip_parameter_validation": False,
        }

        with config_context(assume_finite=None):
            assert get_config()["assume_finite"] is True

        assert get_config()["assume_finite"] is True

        with config_context(assume_finite=False):
            assert get_config()["assume_finite"] is False

            with config_context(assume_finite=None):
                assert get_config()["assume_finite"] is False

                # global setting will not be retained outside of context that
                # did not modify this setting
                set_config(assume_finite=True)
                assert get_config()["assume_finite"] is True

    }

    # No positional arguments
        config_context(True)

    # No unknown arguments
        config_context(do_something_else=True).__enter__()

def test_config_context_exception():
    try:
            raise ValueError()
        pass

def test_set_config():
    set_config(assume_finite=None)
    set_config(assume_finite=True)
    assert get_config()["assume_finite"] is True
    set_config(assume_finite=False)

        set_config(do_something_else=True)

def set_assume_finite(assume_finite, sleep_duration):
    """Return the value of assume_finite after waiting `sleep_duration`."""
    with config_context(assume_finite=assume_finite):
        time.sleep(sleep_duration)
        return get_config()["assume_finite"]

def test_config_threadsafe_joblib(backend):
    """Test that the global config is threadsafe with all joblib backends.
    Two jobs are spawned and sets assume_finite to two different values.
    When the job with a duration 0.1s completes, the assume_finite value
    should be the same as the value passed to the function. In other words,
    it is not influenced by the other job setting assume_finite to True.
    """
    assume_finites = [False, True, False, True]
    sleep_durations = [0.1, 0.2, 0.1, 0.2]

    items = Parallel(backend=backend, n_jobs=2)(
        delayed(set_assume_finite)(assume_finite, sleep_dur)
        for assume_finite, sleep_dur in zip(assume_finites, sleep_durations)
    )

    assert items == [False, True, False, True]

def test_config_threadsafe():
    """Uses threads directly to test that the global config does not change
    between threads. Same test as `test_config_threadsafe_joblib` but with
    `ThreadPoolExecutor`."""

    with ThreadPoolExecutor(max_workers=2) as e:
        items = [
            output
            for output in e.map(set_assume_finite, assume_finites, sleep_durations)
        ]

def test_config_array_api_dispatch_error_scipy(monkeypatch):
    """Check error when SciPy is too old"""
    monkeypatch.setattr(sklearn.utils._array_api.scipy, "__version__", "1.13.0")

    with pytest.raises(ImportError, match="SciPy must be 1.14.0 or newer"):
            pass

        set_config(array_api_dispatch=True)

def test_import_skl():
    # Test either above import has failed for some reason
    # "import *" is discouraged outside of the module level, hence we
    # rely on setting up the variable above
    assert _top_import_error is None

def test_lda_predict():
    # Test LDA classification.
    # This checks that LDA implements fit and predict and returns correct
    # values for simple toy data.
    for test_case in solver_shrinkage:
        solver, shrinkage = test_case
        clf = LinearDiscriminantAnalysis(solver=solver, shrinkage=shrinkage)
        y_pred = clf.fit(X, y).predict(X)
        assert_array_equal(y_pred, y, "solver %s" % solver)

        # Assert that it works with 1D data
        y_pred1 = clf.fit(X1, y).predict(X1)
        assert_array_equal(y_pred1, y, "solver %s" % solver)

        # Test probability estimates
        y_proba_pred1 = clf.predict_proba(X1)
        assert_array_equal((y_proba_pred1[:, 1] > 0.5) + 1, y, "solver %s" % solver)
        y_log_proba_pred1 = clf.predict_log_proba(X1)
            np.exp(y_log_proba_pred1),
            y_proba_pred1,
            rtol=1e-6,
            err_msg="solver %s" % solver,
        )

        # Primarily test for commit 2f34950 -- "reuse" of priors
        y_pred3 = clf.fit(X, y3).predict(X)
        # LDA shouldn't be able to separate those
        assert np.any(y_pred3 != y3), "solver %s" % solver

    clf = LinearDiscriminantAnalysis(solver="svd", shrinkage="auto")

    clf = LinearDiscriminantAnalysis(
        solver="lsqr", shrinkage=0.1, covariance_estimator=ShrunkCovariance()
    )
        match=(
            "covariance_estimator and shrinkage "
            "parameters are not None. "
            "Only one of the two can be set."
        ),
    ):

    # test bad solver with covariance_estimator
    clf = LinearDiscriminantAnalysis(solver="svd", covariance_estimator=LedoitWolf())
        ValueError, match="covariance estimator is not supported with svd"
    ):

    # test bad covariance estimator
        solver="lsqr", covariance_estimator=KMeans(n_clusters=2, n_init="auto")
    )

def test_lda_predict_proba(solver, n_classes):
    def generate_dataset(n_samples, centers, covariances, random_state=None):
        """Generate a multivariate normal data given some centers and
        covariances"""
        X = np.vstack(
            [
                rng.multivariate_normal(mean, cov, size=n_samples // len(centers))
                for mean, cov in zip(centers, covariances)
            ]
        )
        y = np.hstack(
            [[clazz] * (n_samples // len(centers)) for clazz in range(len(centers))]
        )
        return X, y

    blob_centers = np.array([[0, 0], [-10, 40], [-30, 30]])[:n_classes]
    blob_stds = np.array([[[10, 10], [10, 100]]] * len(blob_centers))
    X, y = generate_dataset(
        n_samples=90000, centers=blob_centers, covariances=blob_stds, random_state=42
    )
    lda = LinearDiscriminantAnalysis(
        solver=solver, store_covariance=True, shrinkage=None
    # check that the empirical means and covariances are close enough to the
    # one used to generate the data
    assert_allclose(lda.means_, blob_centers, atol=1e-1)
    assert_allclose(lda.covariance_, blob_stds[0], atol=1)

    # implement the method to compute the probability given in The Elements
    # of Statistical Learning (cf. p.127, Sect. 4.4.5 "Logistic Regression
    # or LDA?")
    precision = linalg.inv(blob_stds[0])
    alpha_k = []
    alpha_k_0 = []
    for clazz in range(len(blob_centers) - 1):
        alpha_k.append(
            np.dot(precision, (blob_centers[clazz] - blob_centers[-1])[:, np.newaxis])
        )
        alpha_k_0.append(
            np.dot(
                -0.5 * (blob_centers[clazz] + blob_centers[-1])[np.newaxis, :],
                alpha_k[-1],
            )
        )

    sample = np.array([[-22, 22]])

    def discriminant_func(sample, coef, intercept, clazz):
        return np.exp(intercept[clazz] + np.dot(sample, coef[clazz])).item()

    prob = np.array(
        [
            float(
                discriminant_func(sample, alpha_k, alpha_k_0, clazz)
                / (
                    1
                    + sum(
                        [
                            discriminant_func(sample, alpha_k, alpha_k_0, clazz)
                            for clazz in range(n_classes - 1)
                        ]
                    )
                )
            )
            for clazz in range(n_classes - 1)
        ]
    )

    prob_ref = 1 - np.sum(prob)

    # check the consistency of the computed probability
    # all probabilities should sum to one
    prob_ref_2 = float(
        1
        / (
            1
            + sum(
                [
                    discriminant_func(sample, alpha_k, alpha_k_0, clazz)
                    for clazz in range(n_classes - 1)
                ]
            )
        )
    )

    assert prob_ref == pytest.approx(prob_ref_2)
    # check that the probability of LDA are close to the theoretical
    # probabilities
        lda.predict_proba(sample), np.hstack([prob, prob_ref])[np.newaxis], atol=1e-2
    )

def test_lda_priors():
    # Test priors (negative priors)
    priors = np.array([0.5, -0.5])
    clf = LinearDiscriminantAnalysis(priors=priors)
    msg = "priors must be non-negative"

    # Test that priors passed as a list are correctly handled (run to see if
    # failure)
    clf = LinearDiscriminantAnalysis(priors=[0.5, 0.5])

    # Test that priors always sum to 1
    priors = np.array([0.5, 0.6])
    prior_norm = np.array([0.45, 0.55])

    assert_array_almost_equal(clf.priors_, prior_norm, 2)

def test_lda_coefs():
    # Test if the coefficients of the solvers are approximately the same.
        n_samples=n_samples, n_features=n_features, centers=n_classes, random_state=11
    )

    clf_lda_svd = LinearDiscriminantAnalysis(solver="svd")
    clf_lda_lsqr = LinearDiscriminantAnalysis(solver="lsqr")
    clf_lda_eigen = LinearDiscriminantAnalysis(solver="eigen")

    clf_lda_svd.fit(X, y)
    clf_lda_lsqr.fit(X, y)
    clf_lda_eigen.fit(X, y)

    assert_array_almost_equal(clf_lda_svd.coef_, clf_lda_lsqr.coef_, 1)
    assert_array_almost_equal(clf_lda_svd.coef_, clf_lda_eigen.coef_, 1)
    assert_array_almost_equal(clf_lda_eigen.coef_, clf_lda_lsqr.coef_, 1)

    clf = LinearDiscriminantAnalysis(solver="svd", n_components=1)
    X_transformed = clf.fit(X, y).transform(X)
    assert X_transformed.shape[1] == 1
    clf = LinearDiscriminantAnalysis(solver="eigen", n_components=1)

    clf = LinearDiscriminantAnalysis(solver="lsqr", n_components=1)
    msg = "transform not implemented for 'lsqr'"

    with pytest.raises(NotImplementedError, match=msg):
        clf.transform(X)

def test_lda_explained_variance_ratio():
    # Test if the sum of the normalized eigen vectors values equals 1,
    # Also tests whether the explained_variance_ratio_ formed by the
    # eigen solver is the same as the explained_variance_ratio_ formed
    # by the svd solver

    state = np.random.RandomState(0)
    X = state.normal(loc=0, scale=100, size=(40, 20))
    y = state.randint(0, 3, size=(40,))

    assert_almost_equal(clf_lda_eigen.explained_variance_ratio_.sum(), 1.0, 3)
    assert clf_lda_eigen.explained_variance_ratio_.shape == (2,), (
        "Unexpected length for explained_variance_ratio_"
    )

    assert_almost_equal(clf_lda_svd.explained_variance_ratio_.sum(), 1.0, 3)
    assert clf_lda_svd.explained_variance_ratio_.shape == (2,), (
    )

        clf_lda_svd.explained_variance_ratio_, clf_lda_eigen.explained_variance_ratio_
    )

def test_lda_orthogonality():
    # arrange four classes with their means in a kite-shaped pattern
    # the longer distance should be transformed to the first component, and
    # the shorter distance to the second component.
    means = np.array([[0, 0, -1], [0, 2, 0], [0, -2, 0], [0, 0, 5]])

    # We construct perfectly symmetric distributions, so the LDA can estimate
    # precise means.
    scatter = np.array(
        [
            [0.1, 0, 0],
            [-0.1, 0, 0],
            [0, 0.1, 0],
            [0, -0.1, 0],
            [0, 0, 0.1],
            [0, 0, -0.1],
        ]
    )

    X = (means[:, np.newaxis, :] + scatter[np.newaxis, :, :]).reshape((-1, 3))
    y = np.repeat(np.arange(means.shape[0]), scatter.shape[0])

    # Fit LDA and transform the means
    clf = LinearDiscriminantAnalysis(solver="svd").fit(X, y)
    means_transformed = clf.transform(means)

    d1 = means_transformed[3] - means_transformed[0]
    d2 = means_transformed[2] - means_transformed[1]
    d1 /= np.sqrt(np.sum(d1**2))
    d2 /= np.sqrt(np.sum(d2**2))

    # the transformed within-class covariance should be the identity matrix
    assert_almost_equal(np.cov(clf.transform(scatter).T), np.eye(2))

    # the means of classes 0 and 3 should lie on the first component
    assert_almost_equal(np.abs(np.dot(d1[:2], [1, 0])), 1.0)

    # the means of classes 1 and 2 should lie on the second component
    assert_almost_equal(np.abs(np.dot(d2[:2], [0, 1])), 1.0)

def test_lda_scaling():
    # Test if classification works correctly with differently scaled features.
    n = 100
    # use uniform distribution of features to make sure there is absolutely no
    # overlap between classes.
    x1 = rng.uniform(-1, 1, (n, 3)) + [-10, 0, 0]
    x2 = rng.uniform(-1, 1, (n, 3)) + [10, 0, 0]
    x = np.vstack((x1, x2)) * [1, 100, 10000]
    y = [-1] * n + [1] * n

    for solver in ("svd", "lsqr", "eigen"):
        clf = LinearDiscriminantAnalysis(solver=solver)
        # should be able to separate the data perfectly
        assert clf.fit(x, y).score(x, y) == 1.0, "using covariance: %s" % solver

def test_lda_store_covariance():
    # Test for solver 'lsqr' and 'eigen'
    # 'store_covariance' has no effect on 'lsqr' and 'eigen' solvers
    for solver in ("lsqr", "eigen"):
        clf = LinearDiscriminantAnalysis(solver=solver).fit(X6, y6)
        assert hasattr(clf, "covariance_")

        # Test the actual attribute:
        clf = LinearDiscriminantAnalysis(solver=solver, store_covariance=True).fit(
            X6, y6
        )

            clf.covariance_, np.array([[0.422222, 0.088889], [0.088889, 0.533333]])
        )

    # Test for SVD solver, the default is to not set the covariances_ attribute
    clf = LinearDiscriminantAnalysis(solver="svd").fit(X6, y6)
    assert not hasattr(clf, "covariance_")

    # Test the actual attribute:
    clf = LinearDiscriminantAnalysis(solver=solver, store_covariance=True).fit(X6, y6)
    assert hasattr(clf, "covariance_")

        clf.covariance_, np.array([[0.422222, 0.088889], [0.088889, 0.533333]])
    )

def test_lda_shrinkage(seed):
    # Test that shrunk covariance estimator and shrinkage parameter behave the
    # same
    y = rng.randint(3, size=(100))
    c1 = LinearDiscriminantAnalysis(store_covariance=True, shrinkage=0.5, solver="lsqr")
    c2 = LinearDiscriminantAnalysis(
        store_covariance=True,
        covariance_estimator=ShrunkCovariance(shrinkage=0.5),
        solver="lsqr",
    )
    c1.fit(X, y)
    c2.fit(X, y)
    assert_allclose(c1.means_, c2.means_)
    assert_allclose(c1.covariance_, c2.covariance_)

def test_lda_ledoitwolf():
    # When shrinkage="auto" current implementation uses ledoitwolf estimation
    # of covariance after standardizing the data. This checks that it is indeed
    # the case
    class StandardizedLedoitWolf:
        def fit(self, X):
            sc = StandardScaler()  # standardize features
            X_sc = sc.fit_transform(X)
            s = ledoit_wolf(X_sc)[0]
            # rescale
            s = sc.scale_[:, np.newaxis] * s * sc.scale_[np.newaxis, :]
            self.covariance_ = s

    y = rng.randint(3, size=(100,))
    c1 = LinearDiscriminantAnalysis(
        store_covariance=True, shrinkage="auto", solver="lsqr"
    )
        covariance_estimator=StandardizedLedoitWolf(),
    )

def test_lda_dimension_warning(n_classes, n_features):
    # we create n_classes labels by repeating and truncating a
    # range(n_classes) until n_samples
    y = np.tile(range(n_classes), n_samples // n_classes + 1)[:n_samples]
    max_components = min(n_features, n_classes - 1)

    for n_components in [max_components - 1, None, max_components]:
        # if n_components <= min(n_classes - 1, n_features), no warning
        lda = LinearDiscriminantAnalysis(n_components=n_components)
        lda.fit(X, y)

    for n_components in [max_components + 1, max(n_features, n_classes - 1) + 1]:
        # if n_components > min(n_classes - 1, n_features), raise error.
        # We test one unit higher than max_components, and then something
        # larger than both n_features and n_classes - 1 to ensure the test
        # works for any value of n_component
        msg = "n_components cannot be larger than "
            lda.fit(X, y)

def test_lda_dtype_match(data_type, expected_type):
    for solver, shrinkage in solver_shrinkage:
        clf.fit(X.astype(data_type), y.astype(data_type))
        assert clf.coef_.dtype == expected_type

def test_lda_numeric_consistency_float32_float64():
        clf_32 = LinearDiscriminantAnalysis(solver=solver, shrinkage=shrinkage)
        clf_32.fit(X.astype(np.float32), y.astype(np.float32))
        clf_64 = LinearDiscriminantAnalysis(solver=solver, shrinkage=shrinkage)
        clf_64.fit(X.astype(np.float64), y.astype(np.float64))

        # Check value consistency between types
        rtol = 1e-6
        assert_allclose(clf_32.coef_, clf_64.coef_, rtol=rtol)

def test_qda():
    # QDA classification.
    # This checks that QDA implements fit and predict and returns
    # correct values for a simple toy dataset.
    clf = QuadraticDiscriminantAnalysis()
    y_pred = clf.fit(X6, y6).predict(X6)
    assert_array_equal(y_pred, y6)

    # Assure that it works with 1D data
    y_pred1 = clf.fit(X7, y6).predict(X7)
    assert_array_equal(y_pred1, y6)

    # Test probas estimates
    y_proba_pred1 = clf.predict_proba(X7)
    assert_array_equal((y_proba_pred1[:, 1] > 0.5) + 1, y6)
    y_log_proba_pred1 = clf.predict_log_proba(X7)
    assert_array_almost_equal(np.exp(y_log_proba_pred1), y_proba_pred1, 8)

    y_pred3 = clf.fit(X6, y7).predict(X6)
    # QDA shouldn't be able to separate those
    assert np.any(y_pred3 != y7)

    # Classes should have at least 2 elements
        clf.fit(X6, y4)

def test_qda_priors():
    n_pos = np.sum(y_pred == 2)

    neg = 1e-10
    clf = QuadraticDiscriminantAnalysis(priors=np.array([neg, 1 - neg]))
    n_pos2 = np.sum(y_pred == 2)

    assert n_pos2 > n_pos

def test_qda_prior_type(priors_type):
    """Check that priors accept array-like."""
    priors = [0.5, 0.5]
    clf = QuadraticDiscriminantAnalysis(
        priors=_convert_container([0.5, 0.5], priors_type)
    ).fit(X6, y6)
    assert isinstance(clf.priors_, np.ndarray)
    assert_array_equal(clf.priors_, priors)

def test_qda_prior_copy():
    """Check that altering `priors` without `fit` doesn't change `priors_`"""
    priors = np.array([0.5, 0.5])
    qda = QuadraticDiscriminantAnalysis(priors=priors).fit(X, y)

    # we expect the following
    assert_array_equal(qda.priors_, qda.priors)

    # altering `priors` without `fit` should not change `priors_`
    priors[0] = 0.2
    assert qda.priors_[0] != qda.priors[0]

def test_qda_store_covariance():
    # The default is to not set the covariances_ attribute
    clf = QuadraticDiscriminantAnalysis().fit(X6, y6)

    clf = QuadraticDiscriminantAnalysis(store_covariance=True).fit(X6, y6)

    assert_array_almost_equal(clf.covariance_[0], np.array([[0.7, 0.45], [0.45, 0.7]]))

        clf.covariance_[1],
        np.array([[0.33333333, -0.33333333], [-0.33333333, 0.66666667]]),
    )

def test_qda_regularization():
    # The default is reg_param=0. and will cause issues when there is a
    # constant variable.

    # Fitting on data with constant variable without regularization
    # triggers a LinAlgError.
    msg = r"The covariance matrix of class .+ is not full rank"
    with pytest.warns(linalg.LinAlgWarning, match=msg):
        y_pred = clf.fit(X2, y6)

    y_pred = clf.predict(X2)
    assert np.any(y_pred != y6)

    # Adding a little regularization fixes the fit time error.
    clf = QuadraticDiscriminantAnalysis(reg_param=0.01)
    clf.fit(X2, y6)

    # LinAlgWarning should also be there for the n_samples_in_a_class <
    # n_features case.
        clf.fit(X5, y5)

    # The error will persist even with regularization
    clf = QuadraticDiscriminantAnalysis(reg_param=0.3)

    x, y = make_blobs(n_samples=100, n_features=5, centers=1, random_state=42)

    # make features correlated
    x = np.dot(x, np.arange(x.shape[1] ** 2).reshape(x.shape[1], x.shape[1]))

    c_e = _cov(x, "empirical")
    assert_almost_equal(c_e, c_e.T)

    c_s = _cov(x, "auto")
    assert_almost_equal(c_s, c_s.T)

def test_raises_value_error_on_same_number_of_classes_and_samples(solver):
    """
    Tests that if the number of samples equals the number
    of classes, a ValueError is raised.
    """
    X = np.array([[0.5, 0.6], [0.6, 0.5]])
    y = np.array(["a", "b"])
    clf = LinearDiscriminantAnalysis(solver=solver)
    with pytest.raises(ValueError, match="The number of samples must be more"):

    """Check get_feature_names_out uses class name as prefix."""

    est = LinearDiscriminantAnalysis().fit(X, y)

    class_name_lower = "LinearDiscriminantAnalysis".lower()
        [
            f"{class_name_lower}{i}"
            for i in range(est.explained_variance_ratio_.shape[0])
        ],
    )

            [
            ]
        )
        )
        return X, y

            # rescale

            # rescale

def test_polynomial_count_sketch(gamma, degree, coef0, n_components):
    # test that PolynomialCountSketch approximates polynomial
    # kernel on random data

    # compute exact kernel
    kernel = polynomial_kernel(X, Y, gamma=gamma, degree=degree, coef0=coef0)

    # approximate kernel mapping
    ps_transform = PolynomialCountSketch(
        coef0=coef0,
        degree=degree,
    )
    X_trans = ps_transform.fit_transform(X)
    Y_trans = ps_transform.transform(Y)
    kernel_approx = np.dot(X_trans, Y_trans.T)

    error = kernel - kernel_approx
    assert np.abs(np.mean(error)) <= 0.05  # close to unbiased
    np.abs(error, out=error)
    assert np.max(error) <= 0.1  # nothing too far off
    assert np.mean(error) <= 0.05  # mean is fairly close

def test_polynomial_count_sketch_dense_sparse(gamma, degree, coef0, csr_container):
    """Check that PolynomialCountSketch results are the same for dense and sparse
    input.
    """
    ps_dense = PolynomialCountSketch(
        n_components=500, gamma=gamma, degree=degree, coef0=coef0, random_state=42
    )
    Xt_dense = ps_dense.fit_transform(X)
    Yt_dense = ps_dense.transform(Y)

    ps_sparse = PolynomialCountSketch(
    )
    Xt_sparse = ps_sparse.fit_transform(csr_container(X))
    Yt_sparse = ps_sparse.transform(csr_container(Y))

    assert_allclose(Xt_dense, Xt_sparse)
    assert_allclose(Yt_dense, Yt_sparse)

def test_additive_chi2_sampler(csr_container):
    # test that AdditiveChi2Sampler approximates kernel on random data

    # abbreviations for easier formula
    X_ = X[:, np.newaxis, :].copy()
    Y_ = Y[np.newaxis, :, :].copy()

    large_kernel = 2 * X_ * Y_ / (X_ + Y_)

    # reduce to n_samples_x x n_samples_y by summing over features
    kernel = large_kernel.sum(axis=2)

    transform = AdditiveChi2Sampler(sample_steps=3)
    X_trans = transform.fit_transform(X)
    Y_trans = transform.transform(Y)

    assert_array_almost_equal(kernel, kernel_approx, 1)

    X_sp_trans = transform.fit_transform(csr_container(X))
    Y_sp_trans = transform.transform(csr_container(Y))

    assert_array_equal(X_trans, X_sp_trans.toarray())
    assert_array_equal(Y_trans, Y_sp_trans.toarray())

    # test error is raised on negative input
    Y_neg = Y.copy()
    Y_neg[0, 0] = -1
        transform.fit(Y_neg)

def test_additive_chi2_sampler_sample_steps(method, sample_steps):
    """Check that the input sample step doesn't raise an error
    and that sample interval doesn't change after fit.
    """
    transformer = AdditiveChi2Sampler(sample_steps=sample_steps)
    getattr(transformer, method)(X)

    sample_interval = 0.5
    transformer = AdditiveChi2Sampler(
        sample_steps=sample_steps,
        sample_interval=sample_interval,
    )
    assert transformer.sample_interval == sample_interval

def test_additive_chi2_sampler_wrong_sample_steps(method):
    """Check that we raise a ValueError on invalid sample_steps"""
    transformer = AdditiveChi2Sampler(sample_steps=4)
        "If sample_steps is not in [1, 2, 3], you need to provide sample_interval"
    )
        getattr(transformer, method)(X)

def test_skewed_chi2_sampler():
    # test that RBFSampler approximates kernel on random data

    c = 0.03
    # set on negative component but greater than c to ensure that the kernel
    # approximation is valid on the group (-c; +\infty) endowed with the skewed
    # multiplication.
    Y_ = Y.copy()
    Y_[0, 0] = -c / 2.0

    X_c = (X + c)[:, np.newaxis, :]
    Y_c = (Y_ + c)[np.newaxis, :, :]

    # we do it in log-space in the hope that it's more stable
    # this array is n_samples_x x n_samples_y big x n_features
    log_kernel = (
        (np.log(X_c) / 2.0) + (np.log(Y_c) / 2.0) + np.log(2.0) - np.log(X_c + Y_c)
    )
    # reduce to n_samples_x x n_samples_y by summing over features in log-space
    kernel = np.exp(log_kernel.sum(axis=2))

    transform = SkewedChi2Sampler(skewedness=c, n_components=1000, random_state=42)
    Y_trans = transform.transform(Y_)

    assert np.isfinite(kernel).all(), "NaNs found in the Gram matrix"
    assert np.isfinite(kernel_approx).all(), "NaNs found in the approximate Gram matrix"

    # test error is raised on when inputs contains values smaller than -c
    Y_neg = Y_.copy()
    Y_neg[0, 0] = -c * 2.0
    msg = "X may not contain entries smaller than -skewedness"
        transform.transform(Y_neg)

def test_additive_chi2_sampler_exceptions():
    """Ensures correct error message"""
    transformer = AdditiveChi2Sampler()
    X_neg = X.copy()
    X_neg[0, 0] = -1
    with pytest.raises(ValueError, match="X in AdditiveChi2Sampler"):
        transformer.fit(X_neg)
        transformer.fit(X)
        transformer.transform(X_neg)

def test_rbf_sampler():
    gamma = 10.0
    kernel = rbf_kernel(X, Y, gamma=gamma)

    rbf_transform = RBFSampler(gamma=gamma, n_components=1000, random_state=42)
    X_trans = rbf_transform.fit_transform(X)
    Y_trans = rbf_transform.transform(Y)

    assert np.abs(np.mean(error)) <= 0.01  # close to unbiased

def test_rbf_sampler_fitted_attributes_dtype(global_dtype):
    rbf = RBFSampler()

    rbf.fit(X)

    assert rbf.random_offset_.dtype == global_dtype
    assert rbf.random_weights_.dtype == global_dtype

def test_rbf_sampler_dtype_equivalence():
    rbf32 = RBFSampler(random_state=42)
    X32 = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float32)
    rbf32.fit(X32)

    rbf64 = RBFSampler(random_state=42)
    X64 = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float64)
    rbf64.fit(X64)

    assert_allclose(rbf32.random_offset_, rbf64.random_offset_)
    assert_allclose(rbf32.random_weights_, rbf64.random_weights_)

def test_rbf_sampler_gamma_scale():
    """Check the inner value computed when `gamma='scale'`."""
    rbf = RBFSampler(gamma="scale")
    rbf.fit(X, y)
    assert rbf._gamma == pytest.approx(4)

def test_skewed_chi2_sampler_fitted_attributes_dtype(global_dtype):
    skewed_chi2_sampler = SkewedChi2Sampler()

    skewed_chi2_sampler.fit(X)

    assert skewed_chi2_sampler.random_offset_.dtype == global_dtype
    assert skewed_chi2_sampler.random_weights_.dtype == global_dtype

def test_skewed_chi2_sampler_dtype_equivalence():
    skewed_chi2_sampler_32 = SkewedChi2Sampler(random_state=42)
    skewed_chi2_sampler_32.fit(X_32)

    skewed_chi2_sampler_64 = SkewedChi2Sampler(random_state=42)
    skewed_chi2_sampler_64.fit(X_64)

        skewed_chi2_sampler_32.random_offset_, skewed_chi2_sampler_64.random_offset_
    )
        skewed_chi2_sampler_32.random_weights_, skewed_chi2_sampler_64.random_weights_
    )

def test_input_validation(csr_container):
    # Regression test: kernel approx. transformers should work on lists
    # No assertions; the old versions would simply crash
    AdditiveChi2Sampler().fit(X).transform(X)
    SkewedChi2Sampler().fit(X).transform(X)
    RBFSampler().fit(X).transform(X)

def test_nystroem_approximation():
    # some basic tests
    X = rnd.uniform(size=(10, 4))

    # With n_components = n_samples this is exact
    X_transformed = Nystroem(n_components=X.shape[0]).fit_transform(X)
    K = rbf_kernel(X)
    assert_array_almost_equal(np.dot(X_transformed, X_transformed.T), K)

    trans = Nystroem(n_components=2, random_state=rnd)
    X_transformed = trans.fit(X).transform(X)

    # test callable kernel
    trans = Nystroem(n_components=2, kernel=_linear_kernel, random_state=rnd)

    # test that available kernels fit and transform
        trans = Nystroem(n_components=2, kernel=kern, random_state=rnd)
        X_transformed = trans.fit(X).transform(X)
        assert X_transformed.shape == (X.shape[0], 2)

def test_nystroem_default_parameters():
    rnd = np.random.RandomState(42)

    # rbf kernel should behave as gamma=None by default
    # aka gamma = 1 / n_features
    nystroem = Nystroem(n_components=10)
    X_transformed = nystroem.fit_transform(X)
    K = rbf_kernel(X, gamma=None)
    K2 = np.dot(X_transformed, X_transformed.T)
    assert_array_almost_equal(K, K2)

    # chi2 kernel should behave as gamma=1 by default
    nystroem = Nystroem(kernel="chi2", n_components=10)
    K = chi2_kernel(X, gamma=1)

def test_nystroem_singular_kernel():
    # test that nystroem works with singular kernel matrix
    X = rng.rand(10, 20)
    X = np.vstack([X] * 2)  # duplicate samples

    gamma = 100
    N = Nystroem(gamma=gamma, n_components=X.shape[0]).fit(X)
    X_transformed = N.transform(X)

    K = rbf_kernel(X, gamma=gamma)

    assert_array_almost_equal(K, np.dot(X_transformed, X_transformed.T))
    assert np.all(np.isfinite(Y))

def test_nystroem_poly_kernel_params():
    # Non-regression: Nystroem should pass other parameters beside gamma.
    rnd = np.random.RandomState(37)

    K = polynomial_kernel(X, degree=3.1, coef0=0.1)
    nystroem = Nystroem(
        kernel="polynomial", n_components=X.shape[0], degree=3.1, coef0=0.1
    )

def test_nystroem_callable():
    # Test Nystroem on a callable.
    X = rnd.uniform(size=(n_samples, 4))

    def logging_histogram_kernel(x, y, log):
        """Histogram kernel that writes to a log."""
        log.append(1)

    kernel_log = []
    X = list(X)  # test input validation
    Nystroem(
        kernel=logging_histogram_kernel,
        n_components=(n_samples - 1),
        kernel_params={"log": kernel_log},
    ).fit(X)
    assert len(kernel_log) == n_samples * (n_samples - 1) / 2

    # if degree, gamma or coef0 is passed, we raise a ValueError
    msg = "Don't pass gamma, coef0 or degree to Nystroem"
    params = ({"gamma": 1}, {"coef0": 1}, {"degree": 2})
    for param in params:
        ny = Nystroem(kernel=_linear_kernel, n_components=(n_samples - 1), **param)
            ny.fit(X)

def test_nystroem_precomputed_kernel():
    # Non-regression: test Nystroem on precomputed kernel.
    # PR - 14706
    rnd = np.random.RandomState(12)

    K = polynomial_kernel(X, degree=2, coef0=0.1)
    nystroem = Nystroem(kernel="precomputed", n_components=X.shape[0])
    X_transformed = nystroem.fit_transform(K)

        ny = Nystroem(kernel="precomputed", n_components=X.shape[0], **param)
            ny.fit(K)

def test_nystroem_component_indices():
    """Check that `component_indices_` corresponds to the subset of
    training points used to construct the feature map.
    https://github.com/scikit-learn/scikit-learn/issues/20474
    """
    X, _ = make_classification(n_samples=100, n_features=20)
    feature_map_nystroem = Nystroem(
    )
    feature_map_nystroem.fit(X)
    assert feature_map_nystroem.component_indices_.shape == (10,)

def test_get_feature_names_out(Estimator):
    """Check get_feature_names_out"""
    est = Estimator().fit(X)
    X_trans = est.transform(X)

    class_name = Estimator.__name__.lower()
    expected_names = [f"{class_name}{i}" for i in range(X_trans.shape[1])]

def test_additivechi2sampler_get_feature_names_out():
    """Check get_feature_names_out for AdditiveChi2Sampler."""
    X = rng.random_sample(size=(300, 3))

    chi2_sampler = AdditiveChi2Sampler(sample_steps=3).fit(X)
    input_names = ["f0", "f1", "f2"]
    suffixes = [
        "f0_sqrt",
        "f1_sqrt",
        "f2_sqrt",
        "f0_cos1",
        "f1_cos1",
        "f2_cos1",
        "f0_sin1",
        "f1_sin1",
        "f2_sin1",
        "f0_cos2",
        "f1_cos2",
        "f2_cos2",
        "f0_sin2",
        "f1_sin2",
        "f2_sin2",
    ]

    names_out = chi2_sampler.get_feature_names_out(input_features=input_names)
    expected_names = [f"additivechi2sampler_{suffix}" for suffix in suffixes]

class MyEstimator(BaseEstimator):
    def __init__(self, l1=0, empty=None):
        self.l1 = l1
        self.empty = empty

class K(BaseEstimator):
    def __init__(self, c=None, d=None):
        self.c = c
        self.d = d

class T(BaseEstimator):
        self.a = a
        self.b = b

class NaNTag(BaseEstimator):

class NoNaNTag(BaseEstimator):
        tags.input_tags.allow_nan = False

class OverrideTag(NaNTag):

class ModifyInitParams(BaseEstimator):
    """Deprecated behavior.
    Equal parameters but with a type cast.
    Doesn't fulfill a is a
    """

    def __init__(self, a=np.array([0])):
        self.a = a.copy()

class Buggy(BaseEstimator):
    "A buggy estimator that does not set its parameters right."

    def __init__(self, a=None):
        self.a = 1

class NoEstimator:
        pass

class VargEstimator(BaseEstimator):
    """scikit-learn estimators shouldn't have vargs."""

    def __init__(self, *vargs):
        pass

def test_clone():
    # Tests that clone creates a correct deep copy.
    # We create an estimator, make a copy of its original state
    # (which, in this case, is the current state of the estimator),
    # and check that the obtained copy is a correct deep copy.

    from sklearn.feature_selection import SelectFpr, f_classif

    selector = SelectFpr(f_classif, alpha=0.1)
    new_selector = clone(selector)
    assert selector is not new_selector
    assert selector.get_params() == new_selector.get_params()

    selector = SelectFpr(f_classif, alpha=np.zeros((10, 2)))

def test_clone_2():
    # Tests that clone doesn't copy everything.
    # We first create an estimator, give it an own attribute, and
    # make a copy of its original state. Then we check that the copy doesn't
    # have the specific attribute we manually added to the initial estimator.

    selector.own_attribute = "test"
    assert not hasattr(new_selector, "own_attribute")

def test_clone_buggy():
    # Check that clone raises an error on buggy estimators.
    buggy = Buggy()
    buggy.a = 2
    with pytest.raises(RuntimeError):
        clone(buggy)

    no_estimator = NoEstimator()
        clone(no_estimator)

    varg_est = VargEstimator()
        clone(varg_est)

    est = ModifyInitParams()
        clone(est)

def test_clone_empty_array():
    # Regression test for cloning estimators with empty arrays
    clf = MyEstimator(empty=np.array([]))
    clf2 = clone(clf)
    assert_array_equal(clf.empty, clf2.empty)

    clf = MyEstimator(empty=sp.csr_matrix(np.array([[0]])))
    assert_array_equal(clf.empty.data, clf2.empty.data)

def test_clone_nan():
    # Regression test for cloning estimators with default parameter as np.nan
    clf = MyEstimator(empty=np.nan)

    assert clf.empty is clf2.empty

def test_clone_dict():
    # test that clone creates a clone of a dict
    orig = {"a": MyEstimator()}
    cloned = clone(orig)
    assert orig["a"] is not cloned["a"]

def test_clone_sparse_matrices():
    sparse_matrix_classes = [
        cls
        for name in dir(sp)
        if name.endswith("_matrix") and type(cls := getattr(sp, name)) is type
    ]

    for cls in sparse_matrix_classes:
        sparse_matrix = cls(np.eye(5))
        clf = MyEstimator(empty=sparse_matrix)
        clf_cloned = clone(clf)
        assert clf.empty.__class__ is clf_cloned.empty.__class__
        assert_array_equal(clf.empty.toarray(), clf_cloned.empty.toarray())

def test_clone_estimator_types():
    # Check that clone works for parameters that are types rather than
    # instances
    clf = MyEstimator(empty=MyEstimator)

def test_clone_class_rather_than_instance():
    # Check that clone raises expected error message when
    # cloning class rather than instance
    msg = "You should provide an instance of scikit-learn estimator"
        clone(MyEstimator)

def test_repr():
    # Smoke test the repr of the base estimator.
    my_estimator = MyEstimator()
    repr(my_estimator)
    test = T(K(), K())
    assert repr(test) == "T(a=K(), b=K())"

    some_est = T(a=["long_params"] * 1000)
    assert len(repr(some_est)) == 485

def test_str():
    # Smoke test the str of the base estimator
    str(my_estimator)

def test_get_params():
    test = T(K(), K)

    assert "a__d" in test.get_params(deep=True)
    assert "a__d" not in test.get_params(deep=False)

    test.set_params(a__d=2)
    assert test.a.d == 2

        test.set_params(a__a=2)

def test_is_estimator_type_class():
    with pytest.warns(FutureWarning, match="passing a class to.*is deprecated"):
        assert is_classifier(SVC)

        assert is_regressor(SVR)

        assert is_clusterer(KMeans)

        assert is_outlier_detector(IsolationForest)

def test_is_classifier(estimator, expected_result):
    assert is_classifier(estimator) == expected_result

def test_is_regressor(estimator, expected_result):
    assert is_regressor(estimator) == expected_result

def test_is_clusterer(estimator, expected_result):
    assert is_clusterer(estimator) == expected_result

def test_set_params():
    # test nested estimator parameter setting
    clf = Pipeline([("svc", SVC())])

    # non-existing parameter in svc
        clf.set_params(svc__stupid_param=True)

    # non-existing parameter of pipeline
        clf.set_params(svm__stupid_param=True)

def test_set_params_passes_all_parameters():
    # Make sure all parameters are passed together to set_params
    # of nested estimator. Regression test for #9944

    class TestDecisionTree(DecisionTreeClassifier):
        def set_params(self, **kwargs):
            super().set_params(**kwargs)
            # expected_kwargs is in test scope
            assert kwargs == expected_kwargs

    expected_kwargs = {"max_depth": 5, "min_samples_leaf": 2}
    for est in [
        Pipeline([("estimator", TestDecisionTree())]),
        GridSearchCV(TestDecisionTree(), {}),
    ]:
        est.set_params(estimator__max_depth=5, estimator__min_samples_leaf=2)

def test_set_params_updates_valid_params():
    # Check that set_params tries to set SVC().C, not
    # DecisionTreeClassifier().C
    gscv = GridSearchCV(DecisionTreeClassifier(), {})
    gscv.set_params(estimator=SVC(), estimator__C=42.0)
    assert gscv.estimator.C == 42.0

def test_score_sample_weight(tree, dataset):
    # check that the score with and without sample weights are different

    tree.fit(X, y)
    # generate random sample weights
    sample_weight = rng.randint(1, 10, size=len(y))
    score_unweighted = tree.score(X, y)
    score_weighted = tree.score(X, y, sample_weight=sample_weight)
    msg = "Unweighted and weighted scores are unexpectedly equal"
    assert score_unweighted != score_weighted, msg

def test_clone_pandas_dataframe():
    class DummyEstimator(TransformerMixin, BaseEstimator):
        """This is a dummy class for generating numerical features

        This feature extractor extracts numerical features from pandas data
        frame.

        df: pandas data frame
            The pandas data frame parameter.

        Notes
        -----
        """

        def __init__(self, df=None, scalar_param=1):
            self.df = df
            self.scalar_param = scalar_param

            pass

            pass

    # build and clone estimator
    d = np.arange(10)
    df = MockDataFrame(d)
    e = DummyEstimator(df, scalar_param=1)
    cloned_e = clone(e)

    # the test
    assert (e.df == cloned_e.df).values.all()
    assert e.scalar_param == cloned_e.scalar_param

def test_clone_protocol():
    """Checks that clone works with `__sklearn_clone__` protocol."""

    class FrozenEstimator(BaseEstimator):
        def __init__(self, fitted_estimator):
            self.fitted_estimator = fitted_estimator

        def __getattr__(self, name):
            return getattr(self.fitted_estimator, name)

        def __sklearn_clone__(self):

        def fit(self, *args, **kwargs):

        def fit_transform(self, *args, **kwargs):
            return self.fitted_estimator.transform(*args, **kwargs)

    X = np.array([[-1, -1], [-2, -1], [-3, -2]])
    components = pca.components_

    frozen_pca = FrozenEstimator(pca)
    assert_allclose(frozen_pca.components_, components)

    # Calling PCA methods such as `get_feature_names_out` still works
    assert_array_equal(frozen_pca.get_feature_names_out(), pca.get_feature_names_out())

    # Fitting on a new data does not alter `components_`
    X_new = np.asarray([[-1, 2], [3, 4], [1, 2]])
    frozen_pca.fit(X_new)

    # `fit_transform` does not alter state
    frozen_pca.fit_transform(X_new)

    # Cloning estimator is a no-op
    clone_frozen_pca = clone(frozen_pca)
    assert clone_frozen_pca is frozen_pca
    assert_allclose(clone_frozen_pca.components_, components)

def test_pickle_version_warning_is_not_raised_with_matching_version():
    tree = DecisionTreeClassifier().fit(iris.data, iris.target)
    tree_pickle = pickle.dumps(tree)
    assert b"_sklearn_version" in tree_pickle

        tree_restored = pickle.loads(tree_pickle)

    # test that we can predict with the restored decision tree classifier
    score_of_original = tree.score(iris.data, iris.target)
    score_of_restored = tree_restored.score(iris.data, iris.target)
    assert score_of_original == score_of_restored

class TreeBadVersion(DecisionTreeClassifier):
        return dict(self.__dict__.items(), _sklearn_version="something")

def test_pickle_version_warning_is_issued_upon_different_version():
    tree = TreeBadVersion().fit(iris.data, iris.target)
    tree_pickle_other = pickle.dumps(tree)
    message = pickle_error_message.format(
        estimator="TreeBadVersion",
        old_version="something",
        current_version=sklearn.__version__,
    )
    with pytest.warns(UserWarning, match=message) as warning_record:
        pickle.loads(tree_pickle_other)

    message = warning_record.list[0].message
    assert isinstance(message, InconsistentVersionWarning)
    assert message.estimator_name == "TreeBadVersion"
    assert message.original_sklearn_version == "something"
    assert message.current_sklearn_version == sklearn.__version__

class TreeNoVersion(DecisionTreeClassifier):
        return self.__dict__

def test_pickle_version_warning_is_issued_when_no_version_info_in_pickle():
    # TreeNoVersion has no getstate, like pre-0.18
    tree = TreeNoVersion().fit(iris.data, iris.target)

    tree_pickle_noversion = pickle.dumps(tree)
    assert b"_sklearn_version" not in tree_pickle_noversion
        estimator="TreeNoVersion",
        old_version="pre-0.18",
    )
    # check we got the warning about using pre-0.18 pickle
        pickle.loads(tree_pickle_noversion)

def test_pickle_version_no_warning_is_issued_with_non_sklearn_estimator():
    try:
        module_backup = TreeNoVersion.__module__
        TreeNoVersion.__module__ = "notsklearn"

            pickle.loads(tree_pickle_noversion)
    finally:
        TreeNoVersion.__module__ = module_backup

class DontPickleAttributeMixin:
        data = self.__dict__.copy()
        data["_attribute_not_pickled"] = None

        state["_restored"] = True
        self.__dict__.update(state)

class MultiInheritanceEstimator(DontPickleAttributeMixin, BaseEstimator):
    def __init__(self, attribute_pickled=5):
        self.attribute_pickled = attribute_pickled
        self._attribute_not_pickled = None

def test_pickling_when_getstate_is_overwritten_by_mixin():
    estimator = MultiInheritanceEstimator()
    estimator._attribute_not_pickled = "this attribute should not be pickled"

    serialized = pickle.dumps(estimator)
    estimator_restored = pickle.loads(serialized)
    assert estimator_restored.attribute_pickled == 5
    assert estimator_restored._attribute_not_pickled is None
    assert estimator_restored._restored

def test_pickling_when_getstate_is_overwritten_by_mixin_outside_of_sklearn():
    try:
        estimator = MultiInheritanceEstimator()
        text = "this attribute should not be pickled"
        estimator._attribute_not_pickled = text
        old_mod = type(estimator).__module__
        type(estimator).__module__ = "notsklearn"

        serialized = estimator.__getstate__()
        assert serialized == {"_attribute_not_pickled": None, "attribute_pickled": 5}

        serialized["attribute_pickled"] = 4
        estimator.__setstate__(serialized)
        assert estimator.attribute_pickled == 4
        assert estimator._restored
    finally:
        type(estimator).__module__ = old_mod

class SingleInheritanceEstimator(BaseEstimator):

        state["_attribute_not_pickled"] = None

def test_pickling_works_when_getstate_is_overwritten_in_the_child_class():
    estimator = SingleInheritanceEstimator()

def test_tag_inheritance():
    # test that changing tags by inheritance is not allowed

    nan_tag_est = NaNTag()
    no_nan_tag_est = NoNaNTag()
    assert nan_tag_est.__sklearn_tags__().input_tags.allow_nan
    assert not no_nan_tag_est.__sklearn_tags__().input_tags.allow_nan

    redefine_tags_est = OverrideTag()
    assert not redefine_tags_est.__sklearn_tags__().input_tags.allow_nan

    diamond_tag_est = DiamondOverwriteTag()
    assert diamond_tag_est.__sklearn_tags__().input_tags.allow_nan

    inherit_diamond_tag_est = InheritDiamondOverwriteTag()
    assert inherit_diamond_tag_est.__sklearn_tags__().input_tags.allow_nan

def test_raises_on_get_params_non_attribute():
    class MyEstimator(BaseEstimator):
        def __init__(self, param=5):
            pass

    est = MyEstimator()
    msg = "'MyEstimator' object has no attribute 'param'"

        est.get_params()

def test_repr_mimebundle_():
    # Checks the display configuration flag controls the json output
    tree = DecisionTreeClassifier()
    output = tree._repr_mimebundle_()
    assert "text/plain" in output
    assert "text/html" in output

    with config_context(display="text"):
        output = tree._repr_mimebundle_()
        assert "text/plain" in output
        assert "text/html" not in output

def test_repr_html_wraps():
    # Checks the display configuration flag controls the html output

    output = tree._repr_html_()
    assert "<style>" in output

        msg = "_repr_html_ is only defined when"
        with pytest.raises(AttributeError, match=msg):
            output = tree._repr_html_()

def test_n_features_in_validation():
    """Check that `_check_n_features` validates data when reset=False"""
    X_train = [[1, 2, 3], [4, 5, 6]]
    _check_n_features(est, X_train, reset=True)

    msg = "X does not contain any features, but MyEstimator is expecting 3 features"
        _check_n_features(est, "invalid X", reset=False)

def test_n_features_in_no_validation():
    """Check that `_check_n_features` does not validate data when
    n_features_in_ is not defined."""
    _check_n_features(est, "invalid X", reset=True)

    # does not raise
    _check_n_features(est, "invalid X", reset=False)

def test_feature_names_in():
    """Check that feature_name_in are recorded by `_validate_data`"""
    X_np = iris.data
    df = pd.DataFrame(X_np, columns=iris.feature_names)

    class NoOpTransformer(TransformerMixin, BaseEstimator):
            validate_data(self, X)

            validate_data(self, X, reset=False)
            return X

    # fit on dataframe saves the feature names
    trans = NoOpTransformer().fit(df)
    assert_array_equal(trans.feature_names_in_, df.columns)

    # fit again but on ndarray does not keep the previous feature names (see #21383)
    trans.fit(X_np)
    assert not hasattr(trans, "feature_names_in_")

    trans.fit(df)
    msg = "The feature names should match those that were passed"
    df_bad = pd.DataFrame(X_np, columns=iris.feature_names[::-1])
        trans.transform(df_bad)

    # warns when fitted on dataframe and transforming a ndarray
    msg = (
        "X does not have valid feature names, but NoOpTransformer was "
        "fitted with feature names"
    )
        trans.transform(X_np)

    # warns when fitted on a ndarray and transforming dataframe
    msg = "X has feature names, but NoOpTransformer was fitted without feature names"
    trans = NoOpTransformer().fit(X_np)
        trans.transform(df)

    # fit on dataframe with all integer feature names works without warning
    df_int_names = pd.DataFrame(X_np)
    trans = NoOpTransformer()
        trans.fit(df_int_names)

    # fit on dataframe with no feature names or all integer feature names
    # -> do not warn on transform
    Xs = [X_np, df_int_names]
    for X in Xs:
            trans.transform(X)

    # fit on dataframe with feature names that are mixed raises an error:
    df_mixed = pd.DataFrame(X_np, columns=["a", "b", 1, 2])
        "Feature names are only supported if all input features have string names, "
        "but your input has ['int', 'str'] as feature name / column name types. "
        "If you want feature names to be stored and validated, you must convert "
        "them all to strings, by using X.columns = X.columns.astype(str) for "
        "example. Otherwise you can remove feature / column names from your input "
        "data, or convert them all to a non-string data type."
    )
        trans.fit(df_mixed)

    # transform on feature names that are mixed also raises:
        trans.transform(df_mixed)

def test_validate_data_skip_check_array():
    """Check skip_check_array option of _validate_data."""

    df = pd.DataFrame(iris.data, columns=iris.feature_names)
    y = pd.Series(iris.target)

        pass

    no_op = NoOpTransformer()
    X_np_out = validate_data(no_op, df, skip_check_array=False)
    assert isinstance(X_np_out, np.ndarray)
    assert_allclose(X_np_out, df.to_numpy())

    X_df_out = validate_data(no_op, df, skip_check_array=True)
    assert X_df_out is df

    y_np_out = validate_data(no_op, y=y, skip_check_array=False)
    assert isinstance(y_np_out, np.ndarray)
    assert_allclose(y_np_out, y.to_numpy())

    y_series_out = validate_data(no_op, y=y, skip_check_array=True)
    assert y_series_out is y

    X_np_out, y_np_out = validate_data(no_op, df, y, skip_check_array=False)

    X_df_out, y_series_out = validate_data(no_op, df, y, skip_check_array=True)

    msg = "Validation should be done on X, y or both."
        validate_data(no_op)

def test_clone_keeps_output_config():
    """Check that clone keeps the set_output config."""

    ss = StandardScaler().set_output(transform="pandas")
    config = _get_output_config("transform", ss)

    ss_clone = clone(ss)
    config_clone = _get_output_config("transform", ss_clone)
    assert config == config_clone

def test_estimator_empty_instance_dict(estimator):
    """Check that ``__getstate__`` returns an empty ``dict`` with an empty
    instance.

    Python 3.11+ changed behaviour by returning ``None`` instead of raising an
    ``AttributeError``. Non-regression test for gh-25188.
    """
    state = estimator.__getstate__()
    expected = {"_sklearn_version": sklearn.__version__}
    assert state == expected

    # this should not raise
    pickle.loads(pickle.dumps(BaseEstimator()))

def test_estimator_getstate_using_slots_error_message():
    """Using a `BaseEstimator` with `__slots__` is not supported."""

    class WithSlots:
        __slots__ = ("x",)

    class Estimator(BaseEstimator, WithSlots):
        pass

    msg = (
        "You cannot use `__slots__` in objects inheriting from "
        "`sklearn.base.BaseEstimator`"
    )

        Estimator().__getstate__()

        pickle.dumps(Estimator())

def test_dataframe_protocol(constructor_name, minversion):
    """Uses the dataframe exchange protocol to get feature names."""
    data = [[1, 4, 2], [3, 3, 6]]
    columns = ["col_0", "col_1", "col_2"]
    df = _convert_container(
        data, constructor_name, columns_name=columns, minversion=minversion
    )

            return validate_data(self, X, reset=False)

    no_op.fit(df)
    assert_array_equal(no_op.feature_names_in_, columns)
    X_out = no_op.transform(df)

    if constructor_name != "pyarrow":
        # pyarrow does not work with `np.asarray`
        # https://github.com/apache/arrow/issues/34886
        assert_allclose(df, X_out)

    bad_names = ["a", "b", "c"]
    df_bad = _convert_container(data, constructor_name, columns_name=bad_names)
    with pytest.raises(ValueError, match="The feature names should match"):
        no_op.transform(df_bad)

def test_transformer_fit_transform_with_metadata_in_transform():
    """Test that having a transformer with metadata for transform raises a
    warning when calling fit_transform."""

    class CustomTransformer(BaseEstimator, TransformerMixin):
        def fit(self, X, y=None, prop=None):

        def transform(self, X, prop=None):
            return X

    # passing the metadata to `fit_transform` should raise a warning since it
    # could potentially be consumed by `transform`
    with pytest.warns(UserWarning, match="`transform` method which consumes metadata"):
        CustomTransformer().set_transform_request(prop=True).fit_transform(
            [[1]], [1], prop=1
        )

    # not passing a metadata which can potentially be consumed by `transform` should
    # not raise a warning
        CustomTransformer().set_transform_request(prop=True).fit_transform([[1]], [1])

def test_outlier_mixin_fit_predict_with_metadata_in_predict():
    """Test that having an OutlierMixin with metadata for predict raises a
    warning when calling fit_predict."""

    class CustomOutlierDetector(BaseEstimator, OutlierMixin):

        def predict(self, X, prop=None):
            return X

    # passing the metadata to `fit_predict` should raise a warning since it
    # could potentially be consumed by `predict`
    with pytest.warns(UserWarning, match="`predict` method which consumes metadata"):
        CustomOutlierDetector().set_predict_request(prop=True).fit_predict(
        )

    # not passing a metadata which can potentially be consumed by `predict` should
        CustomOutlierDetector().set_predict_request(prop=True).fit_predict([[1]], [1])

        self.c = c
        self.d = d

        frame.

        Notes
        -----
        """

            pass

            pass

            pass

            return X

        pass

            return X

            return X

            return X

def test_docstring_parameters():
    # Test module docstring formatting

    # Skip test if numpydoc is not found
    pytest.importorskip(
        "numpydoc", reason="numpydoc is required to test the docstrings"
    )

    # XXX unreached code as of v0.22
    from numpydoc import docscrape

    incorrect = []
    for name in PUBLIC_MODULES:
        if name.endswith(".conftest"):
            # pytest tooling, not part of the scikit-learn API
            continue
        if name == "sklearn.utils.fixes":
            # We cannot always control these docstrings
            continue
            module = importlib.import_module(name)
        classes = inspect.getmembers(module, inspect.isclass)
        # Exclude non-scikit-learn classes
        classes = [cls for cls in classes if cls[1].__module__.startswith("sklearn")]
        for cname, cls in classes:
            this_incorrect = []
            if cname in _DOCSTRING_IGNORES or cname.startswith("_"):
                continue
            if inspect.isabstract(cls):
                continue
            with warnings.catch_warnings(record=True) as w:
                cdoc = docscrape.ClassDoc(cls)
            if len(w):
                    "Error for __init__ of %s in %s:\n%s" % (cls, name, w[0])
                )

            # Skip checks on deprecated classes
            if _is_deprecated(cls.__new__):
                continue

            this_incorrect += check_docstring_parameters(cls.__init__, cdoc)

            for method_name in cdoc.methods:
                method = getattr(cls, method_name)
                if _is_deprecated(method):
                    continue
                param_ignore = None
                # Now skip docstring test for y when y is None
                # by default for API reason
                if method_name in _METHODS_IGNORE_NONE_Y:
                    sig = signature(method)
                    if "y" in sig.parameters and sig.parameters["y"].default is None:
                        param_ignore = ["y"]  # ignore y for fit and score
                result = check_docstring_parameters(method, ignore=param_ignore)
                this_incorrect += result

            incorrect += this_incorrect

        functions = inspect.getmembers(module, inspect.isfunction)
        # Exclude imported functions
        functions = [fn for fn in functions if fn[1].__module__ == name]
        for fname, func in functions:
            # Don't test private methods / functions
            if fname.startswith("_"):
                continue
            if fname == "configuration" and name.endswith("setup"):
                continue
            name_ = _get_func_name(func)
            if not any(d in name_ for d in _DOCSTRING_IGNORES) and not _is_deprecated(
                func
            ):
                incorrect += check_docstring_parameters(func)

    msg = "\n".join(incorrect)
    if len(incorrect) > 0:
        raise AssertionError("Docstring Error:\n" + msg)

def _construct_searchcv_instance(SearchCV):
    return SearchCV(LogisticRegression(), {"C": [0.1, 1]})

def _construct_compose_pipeline_instance(Estimator):
    # Minimal / degenerate instances: only useful to test the docstrings.
    if Estimator.__name__ == "ColumnTransformer":
        return Estimator(transformers=[("transformer", "passthrough", [0, 1])])
    elif Estimator.__name__ == "Pipeline":
        return Estimator(steps=[("clf", LogisticRegression())])
    elif Estimator.__name__ == "FeatureUnion":
        return Estimator(transformer_list=[("transformer", FunctionTransformer())])

def _construct_sparse_coder(Estimator):
    # XXX: hard-coded assumption that n_features=3
    dictionary = np.array(
        [[0, 1, 0], [-1, -1, 2], [1, 1, 1], [0, 1, 1], [0, 2, 1]],
    )
    return Estimator(dictionary=dictionary)

def test_fit_docstring_attributes(name, Estimator):
    pytest.importorskip("numpydoc")

    doc = docscrape.ClassDoc(Estimator)
    attributes = doc["Attributes"]

    if Estimator.__name__ in (
        "HalvingRandomSearchCV",
        "RandomizedSearchCV",
        "HalvingGridSearchCV",
        "GridSearchCV",
    ):
        est = _construct_searchcv_instance(Estimator)
    elif Estimator.__name__ in (
        "ColumnTransformer",
        "Pipeline",
        "FeatureUnion",
    ):
        est = _construct_compose_pipeline_instance(Estimator)
    elif Estimator.__name__ == "SparseCoder":
        est = _construct_sparse_coder(Estimator)
    elif Estimator.__name__ == "FrozenEstimator":
        X, y = make_classification(n_samples=20, n_features=5, random_state=0)
        est = Estimator(LogisticRegression().fit(X, y))
    else:
        # TODO(devtools): use _tested_estimators instead of all_estimators in the
        # decorator
        est = next(_construct_instances(Estimator))

    if Estimator.__name__ == "SelectKBest":
        est.set_params(k=2)
    elif Estimator.__name__ == "DummyClassifier":
        est.set_params(strategy="stratified")
    elif Estimator.__name__ == "CCA" or Estimator.__name__.startswith("PLS"):
        # default = 2 is invalid for single target
        est.set_params(n_components=1)
        "GaussianRandomProjection",
        "SparseRandomProjection",
    ):
        # default="auto" raises an error with the shape of `X`
        est.set_params(n_components=2)
    elif Estimator.__name__ == "TSNE":
        # default raises an error, perplexity must be less than n_samples
        est.set_params(perplexity=2)
    # TODO(1.9) remove
    elif Estimator.__name__ == "KBinsDiscretizer":
        # default raises an FutureWarning if quantile method is at default "warn"
        est.set_params(quantile_method="averaged_inverted_cdf")
    elif Estimator.__name__ == "MDS":
        # default raises a FutureWarning
        est.set_params(n_init=1)

    # Low max iter to speed up tests: we are only interested in checking the existence
    # of fitted attributes. This should be invariant to whether it has converged or not.
    if "max_iter" in est.get_params():
        est.set_params(max_iter=2)
        # min value for `TSNE` is 250
        if Estimator.__name__ == "TSNE":
            est.set_params(max_iter=250)

    if "random_state" in est.get_params():
        est.set_params(random_state=0)

    # In case we want to deprecate some attributes in the future
    skipped_attributes = {}

    if Estimator.__name__.endswith("Vectorizer"):
        # Vectorizer require some specific input data
        if Estimator.__name__ in (
            "CountVectorizer",
            "HashingVectorizer",
            "TfidfVectorizer",
        ):
            X = [
                "This is the first document.",
                "This document is the second document.",
                "And this is the third one.",
                "Is this the first document?",
            ]
        elif Estimator.__name__ == "DictVectorizer":
            X = [{"foo": 1, "bar": 2}, {"foo": 3, "baz": 1}]
        y = None
    else:
            n_samples=20,
            n_features=3,
            random_state=2,
        )

        y = _enforce_estimator_tags_y(est, y)
        X = _enforce_estimator_tags_X(est, X)

    if est.__sklearn_tags__().target_tags.one_d_labels:
        est.fit(y)
    elif est.__sklearn_tags__().target_tags.two_d_labels:
        est.fit(np.c_[y, y])
    elif est.__sklearn_tags__().input_tags.three_d_array:
        est.fit(X[np.newaxis, ...], y)
    else:

    for attr in attributes:
        if attr.name in skipped_attributes:
            continue
        desc = " ".join(attr.desc).lower()
        # As certain attributes are present "only" if a certain parameter is
        # provided, this checks if the word "only" is present in the attribute
        # description, and if not the attribute is required to be present.
        if "only " in desc:
            continue
        # ignore deprecation warnings
        with ignore_warnings(category=FutureWarning):
            assert hasattr(est, attr.name)

    fit_attr = _get_all_fitted_attributes(est)
    fit_attr_names = [attr.name for attr in attributes]
    undocumented_attrs = set(fit_attr).difference(fit_attr_names)
    undocumented_attrs = set(undocumented_attrs).difference(skipped_attributes)
    if undocumented_attrs:
            f"Undocumented attributes for {Estimator.__name__}: {undocumented_attrs}"
        )

def _get_all_fitted_attributes(estimator):
    "Get all the fitted attributes of an estimator including properties"
    # attributes
    fit_attr = list(estimator.__dict__.keys())

    # properties
        warnings.filterwarnings("error", category=FutureWarning)

        for name in dir(estimator.__class__):
            obj = getattr(estimator.__class__, name)
            if not isinstance(obj, property):
                continue

            # ignore properties that raises an AttributeError and deprecated
            # properties
            try:
                getattr(estimator, name)
            except (AttributeError, FutureWarning):
                continue
            fit_attr.append(name)

    return [k for k in fit_attr if k.endswith("_") and not k.startswith("_")]

def _check_predict_proba(clf, X, y):
    proba = clf.predict_proba(X)

    # We know that we can have division by zero
        warnings.filterwarnings("ignore", "divide by zero encountered in log")
        log_proba = clf.predict_log_proba(X)

    y = np.atleast_1d(y)
        y = np.reshape(y, (-1, 1))

    n_outputs = y.shape[1]

    if n_outputs == 1:
        proba = [proba]
        log_proba = [log_proba]

    for k in range(n_outputs):
        assert proba[k].shape[0] == n_samples
        assert proba[k].shape[1] == len(np.unique(y[:, k]))
        assert_array_almost_equal(proba[k].sum(axis=1), np.ones(len(X)))
        # We know that we can have division by zero
            warnings.filterwarnings("ignore", "divide by zero encountered in log")
            assert_array_almost_equal(np.log(proba[k]), log_proba[k])

def _check_behavior_2d(clf):
    # 1d case
    X = np.array([[0], [0], [0], [0]])  # ignored
    y = np.array([1, 2, 1, 1])
    est = clone(clf)
    y_pred = est.predict(X)
    assert y.shape == y_pred.shape

    # 2d case
    y = np.array([[1, 0], [2, 0], [1, 0], [1, 3]])

def _check_behavior_2d_for_constant(clf):
    # 2d case only
    y = np.array([[1, 0, 5, 4, 3], [2, 0, 1, 2, 5], [1, 0, 4, 5, 2], [1, 3, 3, 2, 0]])

def _check_equality_regressor(statistic, y_learn, y_pred_learn, y_test, y_pred_test):
    assert_array_almost_equal(np.tile(statistic, (y_learn.shape[0], 1)), y_pred_learn)
    assert_array_almost_equal(np.tile(statistic, (y_test.shape[0], 1)), y_pred_test)

def test_feature_names_in_and_n_features_in_(global_random_seed, n_samples=10):

    X = pd.DataFrame([[0]] * n_samples, columns=["feature_1"])
    y = random_state.rand(n_samples)

    est = DummyRegressor().fit(X, y)
    assert hasattr(est, "feature_names_in_")
    assert hasattr(est, "n_features_in_")

    est = DummyClassifier().fit(X, y)

def test_most_frequent_and_prior_strategy():
    X = [[0], [0], [0], [0]]  # ignored
    y = [1, 2, 1, 1]

    for strategy in ("most_frequent", "prior"):
        clf = DummyClassifier(strategy=strategy, random_state=0)
        assert_array_equal(clf.predict(X), np.ones(len(X)))
        _check_predict_proba(clf, X, y)

        if strategy == "prior":
                clf.predict_proba([X[0]]), clf.class_prior_.reshape((1, -1))
            )
        else:
                clf.predict_proba([X[0]]), clf.class_prior_.reshape((1, -1)) > 0.5
            )

def test_most_frequent_and_prior_strategy_with_2d_column_y():
    # non-regression test added in
    # https://github.com/scikit-learn/scikit-learn/pull/13545
    X = [[0], [0], [0], [0]]
    y_1d = [1, 2, 1, 1]
    y_2d = [[1], [2], [1], [1]]

        clf_1d = DummyClassifier(strategy=strategy, random_state=0)
        clf_2d = DummyClassifier(strategy=strategy, random_state=0)

        clf_1d.fit(X, y_1d)
        clf_2d.fit(X, y_2d)
        assert_array_equal(clf_1d.predict(X), clf_2d.predict(X))

def test_most_frequent_and_prior_strategy_multioutput():

    for strategy in ("prior", "most_frequent"):
            clf.predict(X),
            np.hstack([np.ones((n_samples, 1)), np.zeros((n_samples, 1))]),
        )
        _check_behavior_2d(clf)

def test_stratified_strategy(global_random_seed):
    X = [[0]] * 5  # ignored
    y = [1, 2, 1, 1, 2]
    clf = DummyClassifier(strategy="stratified", random_state=global_random_seed)

    X = [[0]] * 500
    p = np.bincount(y_pred) / float(len(X))
    assert_almost_equal(p[1], 3.0 / 5, decimal=1)
    assert_almost_equal(p[2], 2.0 / 5, decimal=1)
    _check_predict_proba(clf, X, y)

def test_stratified_strategy_multioutput(global_random_seed):
    y = np.array([[2, 1], [2, 2], [1, 1], [1, 2], [1, 1]])

    for k in range(y.shape[1]):
        p = np.bincount(y_pred[:, k]) / float(len(X))
        assert_almost_equal(p[1], 3.0 / 5, decimal=1)
        assert_almost_equal(p[2], 2.0 / 5, decimal=1)

    _check_behavior_2d(clf)

def test_uniform_strategy(global_random_seed):
    X = [[0]] * 4  # ignored
    clf = DummyClassifier(strategy="uniform", random_state=global_random_seed)

    assert_almost_equal(p[1], 0.5, decimal=1)
    assert_almost_equal(p[2], 0.5, decimal=1)

def test_uniform_strategy_multioutput(global_random_seed):
    y = np.array([[2, 1], [2, 2], [1, 2], [1, 1]])

        assert_almost_equal(p[1], 0.5, decimal=1)
        assert_almost_equal(p[2], 0.5, decimal=1)

def test_string_labels():
    X = [[0]] * 5
    y = ["paris", "paris", "tokyo", "amsterdam", "berlin"]
    clf = DummyClassifier(strategy="most_frequent")
    assert_array_equal(clf.predict(X), ["paris"] * 5)

def test_classifier_score_with_None(y, y_test):
    clf.fit(None, y)
    assert clf.score(None, y_test) == 0.5

def test_classifier_prediction_independent_of_X(strategy, global_random_seed):
    y = [0, 2, 1, 1]
    X1 = [[0]] * 4
    clf1 = DummyClassifier(
        strategy=strategy, random_state=global_random_seed, constant=0
    )
    clf1.fit(X1, y)
    predictions1 = clf1.predict(X1)

    X2 = [[1]] * 4
    clf2 = DummyClassifier(
    )
    clf2.fit(X2, y)
    predictions2 = clf2.predict(X2)

    assert_array_equal(predictions1, predictions2)

def test_mean_strategy_regressor(global_random_seed):

    y = random_state.randn(4)

    reg = DummyRegressor()
    assert_array_equal(reg.predict(X), [np.mean(y)] * len(X))

def test_mean_strategy_multioutput_regressor(global_random_seed):

    X_learn = random_state.randn(10, 10)
    y_learn = random_state.randn(10, 5)

    mean = np.mean(y_learn, axis=0).reshape((1, -1))

    X_test = random_state.randn(20, 10)
    y_test = random_state.randn(20, 5)

    # Correctness oracle
    est = DummyRegressor()
    est.fit(X_learn, y_learn)
    y_pred_learn = est.predict(X_learn)
    y_pred_test = est.predict(X_test)

    _check_equality_regressor(mean, y_learn, y_pred_learn, y_test, y_pred_test)
    _check_behavior_2d(est)

def test_regressor_exceptions():
        reg.predict([])

def test_median_strategy_regressor(global_random_seed):

    y = random_state.randn(5)

    reg = DummyRegressor(strategy="median")
    assert_array_equal(reg.predict(X), [np.median(y)] * len(X))

def test_median_strategy_multioutput_regressor(global_random_seed):

    median = np.median(y_learn, axis=0).reshape((1, -1))

    est = DummyRegressor(strategy="median")

    _check_equality_regressor(median, y_learn, y_pred_learn, y_test, y_pred_test)

def test_quantile_strategy_regressor(global_random_seed):

    reg = DummyRegressor(strategy="quantile", quantile=0.5)

    reg = DummyRegressor(strategy="quantile", quantile=0)
    assert_array_equal(reg.predict(X), [np.min(y)] * len(X))

    reg = DummyRegressor(strategy="quantile", quantile=1)
    assert_array_equal(reg.predict(X), [np.max(y)] * len(X))

    reg = DummyRegressor(strategy="quantile", quantile=0.3)
    assert_array_equal(reg.predict(X), [np.percentile(y, q=30)] * len(X))

def test_quantile_strategy_multioutput_regressor(global_random_seed):

    quantile_values = np.percentile(y_learn, axis=0, q=80).reshape((1, -1))

    est = DummyRegressor(strategy="quantile", quantile=0.5)

    est = DummyRegressor(strategy="quantile", quantile=0.8)

    _check_equality_regressor(
        quantile_values, y_learn, y_pred_learn, y_test, y_pred_test
    )

def test_quantile_invalid():
    y = [0] * 5  # ignored

    est = DummyRegressor(strategy="quantile", quantile=None)
    err_msg = (
        "When using `strategy='quantile', you have to specify the desired quantile"
    )

def test_quantile_strategy_empty_train():
    est = DummyRegressor(strategy="quantile", quantile=0.4)
        est.fit([], [])

def test_constant_strategy_regressor(global_random_seed):

    reg = DummyRegressor(strategy="constant", constant=[43])
    assert_array_equal(reg.predict(X), [43] * len(X))

    reg = DummyRegressor(strategy="constant", constant=43)

    # non-regression test for #22478
    assert not isinstance(reg.constant, np.ndarray)

def test_constant_strategy_multioutput_regressor(global_random_seed):

    # test with 2d array
    constants = random_state.randn(5)

    est = DummyRegressor(strategy="constant", constant=constants)

    _check_equality_regressor(constants, y_learn, y_pred_learn, y_test, y_pred_test)
    _check_behavior_2d_for_constant(est)

def test_y_mean_attribute_regressor():
    X = [[0]] * 5
    y = [1, 2, 4, 6, 8]
    # when strategy = 'mean'
    est = DummyRegressor(strategy="mean")

    assert est.constant_ == np.mean(y)

def test_constants_not_specified_regressor():
    X = [[0]] * 5

    est = DummyRegressor(strategy="constant")
    err_msg = "Constant target value has to be specified"

def test_constant_size_multioutput_regressor(global_random_seed):
    X = random_state.randn(10, 10)
    y = random_state.randn(10, 5)

    est = DummyRegressor(strategy="constant", constant=[1, 2, 3, 4])
    err_msg = r"Constant target value should have shape \(5, 1\)."

def test_constant_strategy():
    y = [2, 1, 2, 2]

    clf = DummyClassifier(strategy="constant", random_state=0, constant=1)
    assert_array_equal(clf.predict(X), np.ones(len(X)))

    y = ["two", "one", "two", "two"]
    clf = DummyClassifier(strategy="constant", random_state=0, constant="one")
    assert_array_equal(clf.predict(X), np.array(["one"] * 4))

def test_constant_strategy_multioutput():
    y = np.array([[2, 3], [1, 3], [2, 3], [2, 0]])

    clf = DummyClassifier(strategy="constant", random_state=0, constant=[1, 0])
        clf.predict(X), np.hstack([np.ones((n_samples, 1)), np.zeros((n_samples, 1))])
    )

def test_constant_strategy_exceptions(y, params, err_msg):

    clf = DummyClassifier(strategy="constant", **params)

def test_classification_sample_weight():
    X = [[0], [0], [1]]
    y = [0, 1, 0]
    sample_weight = [0.1, 1.0, 0.1]

    clf = DummyClassifier(strategy="stratified").fit(X, y, sample_weight)
    assert_array_almost_equal(clf.class_prior_, [0.2 / 1.2, 1.0 / 1.2])

def test_constant_strategy_sparse_target(csc_container):
    y = csc_container(np.array([[0, 1], [4, 0], [1, 1], [1, 4], [1, 1]]))

    assert sp.issparse(y_pred)
        y_pred.toarray(), np.hstack([np.ones((n_samples, 1)), np.zeros((n_samples, 1))])
    )

def test_uniform_strategy_sparse_target_warning(global_random_seed, csc_container):
    y = csc_container(np.array([[2, 1], [2, 2], [1, 4], [4, 2], [1, 1]]))

    with pytest.warns(UserWarning, match="the uniform strategy would not save memory"):

        assert_almost_equal(p[1], 1 / 3, decimal=1)
        assert_almost_equal(p[2], 1 / 3, decimal=1)
        assert_almost_equal(p[4], 1 / 3, decimal=1)

def test_stratified_strategy_sparse_target(global_random_seed, csc_container):
    y = csc_container(np.array([[4, 1], [0, 0], [1, 1], [1, 4], [1, 1]]))

    y_pred = y_pred.toarray()

        assert_almost_equal(p[0], 1.0 / 5, decimal=1)
        assert_almost_equal(p[4], 1.0 / 5, decimal=1)

def test_most_frequent_and_prior_strategy_sparse_target(csc_container):
    y = csc_container(np.array([[1, 0], [1, 3], [4, 0], [0, 1], [1, 0]]))

    y_expected = np.hstack([np.ones((n_samples, 1)), np.zeros((n_samples, 1))])

        y_pred = clf.predict(X)
        assert sp.issparse(y_pred)
        assert_array_equal(y_pred.toarray(), y_expected)

def test_dummy_regressor_sample_weight(global_random_seed, n_samples=10):

    X = [[0]] * n_samples
    sample_weight = random_state.rand(n_samples)

    est = DummyRegressor(strategy="mean").fit(X, y, sample_weight)
    assert est.constant_ == np.average(y, weights=sample_weight)

    est = DummyRegressor(strategy="median").fit(X, y, sample_weight)
    assert est.constant_ == _weighted_percentile(y, sample_weight, 50.0)

    est = DummyRegressor(strategy="quantile", quantile=0.95).fit(X, y, sample_weight)
    assert est.constant_ == _weighted_percentile(y, sample_weight, 95.0)

def test_dummy_regressor_on_3D_array():
    X = np.array([[["foo"]], [["bar"]], [["baz"]]])
    y = np.array([2, 2, 2])
    y_expected = np.array([2, 2, 2])
    cls = DummyRegressor()
    y_pred = cls.predict(X)
    assert_array_equal(y_pred, y_expected)

def test_dummy_classifier_on_3D_array():
    y = [2, 2, 2]
    y_expected = [2, 2, 2]
    y_proba_expected = [[1], [1], [1]]
    cls = DummyClassifier(strategy="stratified")
    y_pred_proba = cls.predict_proba(X)
    assert_array_equal(y_pred_proba, y_proba_expected)

def test_dummy_regressor_return_std():
    X = [[0]] * 3  # ignored
    y_std_expected = np.array([0, 0, 0])
    y_pred_list = cls.predict(X, return_std=True)
    # there should be two elements when return_std is True
    assert len(y_pred_list) == 2
    # the second element should be all zeros
    assert_array_equal(y_pred_list[1], y_std_expected)

def test_regressor_score_with_None(y, y_test):
    reg.fit(None, y)
    assert reg.score(None, y_test) == 1.0

def test_regressor_prediction_independent_of_X(strategy):
    reg1 = DummyRegressor(strategy=strategy, constant=0, quantile=0.7)
    reg1.fit(X1, y)
    predictions1 = reg1.predict(X1)

    reg2 = DummyRegressor(strategy=strategy, constant=0, quantile=0.7)
    reg2.fit(X2, y)
    predictions2 = reg2.predict(X2)

def test_dtype_of_classifier_probas(strategy):
    X = np.zeros(4)
    model = DummyClassifier(strategy=strategy, random_state=0, constant=0)
    probas = model.fit(X, y).predict_proba(X)

    assert probas.dtype == np.float64

def get_random_normal_x_binary_y(global_random_seed):
    # A bit more random tests
    X1 = rng.normal(size=(10, 3))
    y1 = (rng.normal(size=10) > 0).astype(int)
    return X1, y1

def get_random_integer_x_three_classes_y(global_random_seed):
    # Data is 6 random integer points in a 100 dimensional space classified to
    # three classes.
    X2 = rng.randint(5, size=(6, 100))
    y2 = np.array([1, 1, 2, 2, 3, 3])
    return X2, y2

def test_gnb():
    # Gaussian Naive Bayes classification.
    # This checks that GaussianNB implements fit and predict and returns

    clf = GaussianNB()
    y_pred = clf.fit(X, y).predict(X)
    assert_array_equal(y_pred, y)

    y_pred_proba = clf.predict_proba(X)
    y_pred_log_proba = clf.predict_log_proba(X)
    assert_array_almost_equal(np.log(y_pred_proba), y_pred_log_proba, 8)

    # Test whether label mismatch between target y and classes raises
    # an Error
    # FIXME Remove this test once the more general partial_fit tests are merged
        ValueError, match="The target label.* in y do not exist in the initial classes"
    ):
        GaussianNB().partial_fit(X, y, classes=[0, 1])

def test_gnb_prior(global_random_seed):
    # Test whether class priors are properly set.
    clf = GaussianNB().fit(X, y)
    assert_array_almost_equal(np.array([3, 3]) / 6.0, clf.class_prior_, 8)
    X1, y1 = get_random_normal_x_binary_y(global_random_seed)
    clf = GaussianNB().fit(X1, y1)
    # Check that the class priors sum to 1
    assert_array_almost_equal(clf.class_prior_.sum(), 1)

def test_gnb_sample_weight(global_random_seed):
    """Test whether sample weights are properly used in GNB."""
    # Sample weights all being 1 should not change results
    sw = np.ones(6)
    clf_sw = GaussianNB().fit(X, y, sw)

    assert_array_almost_equal(clf.theta_, clf_sw.theta_)
    assert_array_almost_equal(clf.var_, clf_sw.var_)

    # Fitting twice with half sample-weights should result
    # in same result as fitting once with full weights

    sw = rng.rand(y.shape[0])
    clf1 = GaussianNB().fit(X, y, sample_weight=sw)
    clf2 = GaussianNB().partial_fit(X, y, classes=[1, 2], sample_weight=sw / 2)
    clf2.partial_fit(X, y, sample_weight=sw / 2)

    assert_array_almost_equal(clf1.theta_, clf2.theta_)
    assert_array_almost_equal(clf1.var_, clf2.var_)

    # Check that duplicate entries and correspondingly increased sample
    # weights yield the same result
    ind = rng.randint(0, X.shape[0], 20)
    sample_weight = np.bincount(ind, minlength=X.shape[0])

    clf_dupl = GaussianNB().fit(X[ind], y[ind])
    clf_sw = GaussianNB().fit(X, y, sample_weight)

    assert_array_almost_equal(clf_dupl.theta_, clf_sw.theta_)
    assert_array_almost_equal(clf_dupl.var_, clf_sw.var_)

    # non-regression test for gh-24140 where a division by zero was
    # occurring when a single class was present
    sample_weight = (y == 1).astype(np.float64)
    clf = GaussianNB().fit(X, y, sample_weight=sample_weight)

def test_gnb_neg_priors():
    """Test whether an error is raised in case of negative priors"""
    clf = GaussianNB(priors=np.array([-1.0, 2.0]))

    msg = "Priors must be non-negative"

def test_gnb_priors():
    """Test whether the class prior override is properly used"""
    clf = GaussianNB(priors=np.array([0.3, 0.7])).fit(X, y)
        clf.predict_proba([[-0.1, -0.1]]),
        np.array([[0.825303662161683, 0.174696337838317]]),
        8,
    )
    assert_array_almost_equal(clf.class_prior_, np.array([0.3, 0.7]))

def test_gnb_priors_sum_isclose():
    # test whether the class prior sum is properly tested"""
        [
            [-1, -1],
            [-2, -1],
            [-3, -2],
            [-4, -5],
            [-5, -4],
            [1, 1],
            [2, 1],
            [3, 2],
            [4, 4],
            [5, 5],
        ]
    )
    priors = np.array([0.08, 0.14, 0.03, 0.16, 0.11, 0.16, 0.07, 0.14, 0.11, 0.0])
    Y = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    clf = GaussianNB(priors=priors)
    # smoke test for issue #9633

def test_gnb_wrong_nb_priors():
    """Test whether an error is raised if the number of prior is different
    from the number of class"""
    clf = GaussianNB(priors=np.array([0.25, 0.25, 0.25, 0.25]))

    msg = "Number of priors must match number of classes"

def test_gnb_prior_greater_one():
    """Test if an error is raised if the sum of prior greater than one"""
    clf = GaussianNB(priors=np.array([2.0, 1.0]))

    msg = "The sum of the priors should be 1"

def test_gnb_prior_large_bias():
    """Test if good prediction when class prior favor largely one class"""
    clf = GaussianNB(priors=np.array([0.01, 0.99]))
    assert clf.predict([[-0.1, -0.1]]) == np.array([2])

def test_gnb_check_update_with_no_data():
    """Test when the partial fit is called without any data"""
    # Create an empty array
    prev_points = 100
    mean = 0.0
    var = 1.0
    x_empty = np.empty((0, X.shape[1]))
    tmean, tvar = GaussianNB._update_mean_variance(prev_points, mean, var, x_empty)
    assert tmean == mean
    assert tvar == var

def test_gnb_partial_fit():
    clf_pf = GaussianNB().partial_fit(X, y, np.unique(y))
    assert_array_almost_equal(clf.theta_, clf_pf.theta_)
    assert_array_almost_equal(clf.var_, clf_pf.var_)
    assert_array_almost_equal(clf.class_prior_, clf_pf.class_prior_)

    clf_pf2 = GaussianNB().partial_fit(X[0::2, :], y[0::2], np.unique(y))
    clf_pf2.partial_fit(X[1::2], y[1::2])
    assert_array_almost_equal(clf.theta_, clf_pf2.theta_)
    assert_array_almost_equal(clf.var_, clf_pf2.var_)
    assert_array_almost_equal(clf.class_prior_, clf_pf2.class_prior_)

def test_gnb_naive_bayes_scale_invariance():
    # Scaling the data should not change the prediction results
    labels = [GaussianNB().fit(f * X, y).predict(f * X) for f in [1e-10, 1, 1e10]]
    assert_array_equal(labels[0], labels[1])
    assert_array_equal(labels[1], labels[2])

def test_discretenb_prior(DiscreteNaiveBayes, global_random_seed):
    X2, y2 = get_random_integer_x_three_classes_y(global_random_seed)
    clf = DiscreteNaiveBayes().fit(X2, y2)
        np.log(np.array([2, 2, 2]) / 6.0), clf.class_log_prior_, 8
    )

def test_discretenb_partial_fit(DiscreteNaiveBayes):
    clf1 = DiscreteNaiveBayes()
    clf1.fit([[0, 1], [1, 0], [1, 1]], [0, 1, 1])

    clf2 = DiscreteNaiveBayes()
    clf2.partial_fit([[0, 1], [1, 0], [1, 1]], [0, 1, 1], classes=[0, 1])
    assert_array_equal(clf1.class_count_, clf2.class_count_)
    if DiscreteNaiveBayes is CategoricalNB:
        for i in range(len(clf1.category_count_)):
            assert_array_equal(clf1.category_count_[i], clf2.category_count_[i])
    else:
        assert_array_equal(clf1.feature_count_, clf2.feature_count_)

    clf3 = DiscreteNaiveBayes()
    # all categories have to appear in the first partial fit
    clf3.partial_fit([[0, 1]], [0], classes=[0, 1])
    clf3.partial_fit([[1, 0]], [1])
    clf3.partial_fit([[1, 1]], [1])
    assert_array_equal(clf1.class_count_, clf3.class_count_)
        # the categories for each feature of CategoricalNB are mapped to an
        # index chronologically with each call of partial fit and therefore
        # the category_count matrices cannot be compared for equality
                clf1.category_count_[i].shape, clf3.category_count_[i].shape
            )
                np.sum(clf1.category_count_[i], axis=1),
                np.sum(clf3.category_count_[i], axis=1),
            )

        # assert category 0 occurs 1x in the first class and 0x in the 2nd
        # class
        assert_array_equal(clf1.category_count_[0][0], np.array([1, 0]))
        # assert category 1 occurs 0x in the first class and 2x in the 2nd
        # class
        assert_array_equal(clf1.category_count_[0][1], np.array([0, 2]))

        # assert category 0 occurs 0x in the first class and 1x in the 2nd
        # class
        assert_array_equal(clf1.category_count_[1][0], np.array([0, 1]))
        # assert category 1 occurs 1x in the first class and 1x in the 2nd
        # class
        assert_array_equal(clf1.category_count_[1][1], np.array([1, 1]))
    else:
        assert_array_equal(clf1.feature_count_, clf3.feature_count_)

def test_NB_partial_fit_no_first_classes(NaiveBayes, global_random_seed):
    # classes is required for first call to partial fit

        ValueError, match="classes must be passed on the first call to partial_fit."
    ):
        NaiveBayes().partial_fit(X2, y2)

    # check consistency of consecutive classes values
    clf = NaiveBayes()
    clf.partial_fit(X2, y2, classes=np.unique(y2))
        ValueError, match="is not the same as on last call to partial_fit"
    ):
        clf.partial_fit(X2, y2, classes=np.arange(42))

def test_discretenb_predict_proba():
    # Test discrete NB classes' probability scores

    # The 100s below distinguish Bernoulli from multinomial.
    # FIXME: write a test to show this.
    X_bernoulli = [[1, 100, 0], [0, 1, 0], [0, 100, 1]]
    X_multinomial = [[0, 1], [1, 3], [4, 0]]

    # test binary case (1-d output)
    y = [0, 0, 2]  # 2 is regression test for binary case, 02e673
    for DiscreteNaiveBayes, X in zip(
        [BernoulliNB, MultinomialNB], [X_bernoulli, X_multinomial]
    ):
        clf = DiscreteNaiveBayes().fit(X, y)
        assert clf.predict(X[-1:]) == 2
        assert clf.predict_proba([X[0]]).shape == (1, 2)
            clf.predict_proba(X[:2]).sum(axis=1), np.array([1.0, 1.0]), 6
        )

    # test multiclass case (2-d output, must sum to one)
    y = [0, 1, 2]
    ):
        assert clf.predict_proba(X[0:1]).shape == (1, 3)
        assert clf.predict_proba(X[:2]).shape == (2, 3)
        assert_almost_equal(np.sum(clf.predict_proba([X[1]])), 1)
        assert_almost_equal(np.sum(clf.predict_proba([X[-1]])), 1)
        assert_almost_equal(np.sum(np.exp(clf.class_log_prior_)), 1)

def test_discretenb_uniform_prior(DiscreteNaiveBayes):
    # Test whether discrete NB classes fit a uniform prior
    # when fit_prior=False and class_prior=None

    clf = DiscreteNaiveBayes()
    clf.set_params(fit_prior=False)
    clf.fit([[0], [0], [1]], [0, 0, 1])
    prior = np.exp(clf.class_log_prior_)
    assert_array_almost_equal(prior, np.array([0.5, 0.5]))

def test_discretenb_provide_prior(DiscreteNaiveBayes):
    # Test whether discrete NB classes use provided prior

    clf = DiscreteNaiveBayes(class_prior=[0.5, 0.5])

    # Inconsistent number of classes with prior
        clf.fit([[0], [1], [2]], [0, 1, 2])

    msg = "is not the same as on last call to partial_fit"
        clf.partial_fit([[0], [1]], [0, 1], classes=[0, 1, 1])

def test_discretenb_provide_prior_with_partial_fit(DiscreteNaiveBayes):
    # when using partial_fit

    iris_data1, iris_data2, iris_target1, iris_target2 = train_test_split(
        iris.data, iris.target, test_size=0.4, random_state=415
    )

    for prior in [None, [0.3, 0.3, 0.4]]:
        clf_full = DiscreteNaiveBayes(class_prior=prior)
        clf_full.fit(iris.data, iris.target)
        clf_partial = DiscreteNaiveBayes(class_prior=prior)
        clf_partial.partial_fit(iris_data1, iris_target1, classes=[0, 1, 2])
        clf_partial.partial_fit(iris_data2, iris_target2)
            clf_full.class_log_prior_, clf_partial.class_log_prior_
        )

def test_discretenb_sample_weight_multiclass(DiscreteNaiveBayes):
    # check shape consistency for number of samples at fit time
    X = [
        [0, 0, 1],
        [0, 1, 1],
        [0, 1, 1],
        [1, 0, 0],
    ]
    y = [0, 0, 1, 2]
    sample_weight = np.array([1, 1, 2, 2], dtype=np.float64)
    sample_weight /= sample_weight.sum()
    clf = DiscreteNaiveBayes().fit(X, y, sample_weight=sample_weight)
    assert_array_equal(clf.predict(X), [0, 1, 1, 2])

    # Check sample weight using the partial_fit method
    clf.partial_fit(X[:2], y[:2], classes=[0, 1, 2], sample_weight=sample_weight[:2])
    clf.partial_fit(X[2:3], y[2:3], sample_weight=sample_weight[2:3])
    clf.partial_fit(X[3:], y[3:], sample_weight=sample_weight[3:])

def test_discretenb_degenerate_one_class_case(
    DiscreteNaiveBayes,
    use_partial_fit,
    train_on_single_class_y,
):
    # Most array attributes of a discrete naive Bayes classifier should have a
    # first-axis length equal to the number of classes. Exceptions include:
    # ComplementNB.feature_all_, CategoricalNB.n_categories_.
    # Confirm that this is the case for binary problems and the degenerate
    # case of a single class in the training set, when fitting with `fit` or
    # `partial_fit`.
    # Non-regression test for handling degenerate one-class case:
    # https://github.com/scikit-learn/scikit-learn/issues/18974

    X = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
    y = [1, 1, 2]
    if train_on_single_class_y:
        X = X[:-1]
        y = y[:-1]
    classes = sorted(list(set(y)))
    num_classes = len(classes)

    if use_partial_fit:
        clf.partial_fit(X, y, classes=classes)
    else:
    assert clf.predict(X[:1]) == y[0]

    # Check that attributes have expected first-axis lengths
    attribute_names = [
        "classes_",
        "class_count_",
        "class_log_prior_",
        "feature_count_",
        "feature_log_prob_",
    ]
    for attribute_name in attribute_names:
        attribute = getattr(clf, attribute_name, None)
        if attribute is None:
            # CategoricalNB has no feature_count_ attribute
            continue
        if isinstance(attribute, np.ndarray):
            assert attribute.shape[0] == num_classes
        else:
            # CategoricalNB.feature_log_prob_ is a list of arrays
            for element in attribute:
                assert element.shape[0] == num_classes

def test_mnnb(kind, global_random_seed, csr_container):
    # Test Multinomial Naive Bayes classification.
    # This checks that MultinomialNB implements fit and predict and returns

    if kind == "dense":
        X = X2
    elif kind == "sparse":
        X = csr_container(X2)

    # Check the ability to predict the learning set.
    clf = MultinomialNB()

        clf.fit(-X, y2)
    y_pred = clf.fit(X, y2).predict(X)

    assert_array_equal(y_pred, y2)

    # Verify that np.log(clf.predict_proba(X)) gives the same results as
    # clf.predict_log_proba(X)

    # Check that incremental fitting yields the same results
    clf2 = MultinomialNB()
    clf2.partial_fit(X[:2], y2[:2], classes=np.unique(y2))
    clf2.partial_fit(X[2:5], y2[2:5])
    clf2.partial_fit(X[5:], y2[5:])

    y_pred2 = clf2.predict(X)
    assert_array_equal(y_pred2, y2)

    y_pred_proba2 = clf2.predict_proba(X)
    y_pred_log_proba2 = clf2.predict_log_proba(X)
    assert_array_almost_equal(np.log(y_pred_proba2), y_pred_log_proba2, 8)
    assert_array_almost_equal(y_pred_proba2, y_pred_proba)
    assert_array_almost_equal(y_pred_log_proba2, y_pred_log_proba)

    # Partial fit on the whole data at once should be the same as fit too
    clf3 = MultinomialNB()
    clf3.partial_fit(X, y2, classes=np.unique(y2))

    y_pred3 = clf3.predict(X)
    assert_array_equal(y_pred3, y2)
    y_pred_proba3 = clf3.predict_proba(X)
    y_pred_log_proba3 = clf3.predict_log_proba(X)
    assert_array_almost_equal(np.log(y_pred_proba3), y_pred_log_proba3, 8)
    assert_array_almost_equal(y_pred_proba3, y_pred_proba)
    assert_array_almost_equal(y_pred_log_proba3, y_pred_log_proba)

def test_mnb_prior_unobserved_targets():
    # test smoothing of prior for yet unobserved targets

    # Create toy training data
    X = np.array([[0, 1], [1, 0]])

        clf.partial_fit(X, y, classes=[0, 1, 2])

    assert clf.predict([[0, 1]]) == 0
    assert clf.predict([[1, 0]]) == 1
    assert clf.predict([[1, 1]]) == 0

    # add a training example with previously unobserved class

        clf.partial_fit([[1, 1]], [2])

    assert clf.predict([[1, 1]]) == 2

def test_bnb():
    # Tests that BernoulliNB when alpha=1.0 gives the same values as
    # those given for the toy example in Manning, Raghavan, and
    # Schuetze's "Introduction to Information Retrieval" book:
    # https://nlp.stanford.edu/IR-book/html/htmledition/the-bernoulli-model-1.html

    # Training data points are:
    # Chinese Beijing Chinese (class: China)
    # Chinese Chinese Shanghai (class: China)
    # Chinese Macao (class: China)
    # Tokyo Japan Chinese (class: Japan)

    # Features are Beijing, Chinese, Japan, Macao, Shanghai, and Tokyo
        [[1, 1, 0, 0, 0, 0], [0, 1, 0, 0, 1, 0], [0, 1, 0, 1, 0, 0], [0, 1, 1, 0, 0, 1]]
    )

    # Classes are China (0), Japan (1)
    Y = np.array([0, 0, 0, 1])

    # Fit BernoulliBN w/ alpha = 1.0
    clf = BernoulliNB(alpha=1.0)

    # Check the class prior is correct
    class_prior = np.array([0.75, 0.25])
    assert_array_almost_equal(np.exp(clf.class_log_prior_), class_prior)

    # Check the feature probabilities are correct
    feature_prob = np.array(
        [
            [0.4, 0.8, 0.2, 0.4, 0.4, 0.2],
            [1 / 3.0, 2 / 3.0, 2 / 3.0, 1 / 3.0, 1 / 3.0, 2 / 3.0],
        ]
    )
    assert_array_almost_equal(np.exp(clf.feature_log_prob_), feature_prob)

    # Testing data point is:
    # Chinese Chinese Chinese Tokyo Japan
    X_test = np.array([[0, 1, 1, 0, 0, 1]])

    # Check the predictive probabilities are correct
    unnorm_predict_proba = np.array([[0.005183999999999999, 0.02194787379972565]])
    predict_proba = unnorm_predict_proba / np.sum(unnorm_predict_proba)
    assert_array_almost_equal(clf.predict_proba(X_test), predict_proba)

def test_bnb_feature_log_prob():
    # Test for issue #4268.
    # Tests that the feature log prob value computed by BernoulliNB when
    # alpha=1.0 is equal to the expression given in Manning, Raghavan,
    # and Schuetze's "Introduction to Information Retrieval" book:
    # http://nlp.stanford.edu/IR-book/html/htmledition/the-bernoulli-model-1.html

    X = np.array([[0, 0, 0], [1, 1, 0], [0, 1, 0], [1, 0, 1], [0, 1, 0]])
    Y = np.array([0, 0, 1, 2, 2])

    # Fit Bernoulli NB w/ alpha = 1.0

    # Manually form the (log) numerator and denominator that
    # constitute P(feature presence | class)
    num = np.log(clf.feature_count_ + 1.0)
    denom = np.tile(np.log(clf.class_count_ + 2.0), (X.shape[1], 1)).T

    # Check manual estimate matches
    assert_array_almost_equal(clf.feature_log_prob_, (num - denom))

def test_cnb():
    # Tests ComplementNB when alpha=1.0 for the toy example in Manning,
    # Raghavan, and Schuetze's "Introduction to Information Retrieval" book:

    # Features are Beijing, Chinese, Japan, Macao, Shanghai, and Tokyo.
    )

    # Classes are China (0), Japan (1).

    # Check that weights are correct. See steps 4-6 in Table 4 of
    # Rennie et al. (2003).
    theta = np.array(
        [
            [
                (0 + 1) / (3 + 6),
                (1 + 1) / (3 + 6),
            ],
            [
                (1 + 1) / (6 + 6),
                (3 + 1) / (6 + 6),
                (0 + 1) / (6 + 6),
            ],
        ]
    )

    weights = np.zeros(theta.shape)
    normed_weights = np.zeros(theta.shape)
        weights[i] = -np.log(theta[i])
        normed_weights[i] = weights[i] / weights[i].sum()

    # Verify inputs are nonnegative.
    clf = ComplementNB(alpha=1.0)

    msg = re.escape("Negative values in data passed to ComplementNB (input X)")
        clf.fit(-X, Y)

    # Check that counts/weights are correct.
    feature_count = np.array([[1, 3, 0, 1, 1, 0], [0, 1, 1, 0, 0, 1]])
    assert_array_equal(clf.feature_count_, feature_count)
    class_count = np.array([3, 1])
    assert_array_equal(clf.class_count_, class_count)
    feature_all = np.array([1, 4, 1, 1, 1, 1])
    assert_array_equal(clf.feature_all_, feature_all)
    assert_array_almost_equal(clf.feature_log_prob_, weights)

    clf = ComplementNB(alpha=1.0, norm=True)
    assert_array_almost_equal(clf.feature_log_prob_, normed_weights)

def test_categoricalnb(global_random_seed):
    # Check the ability to predict the training set.
    clf = CategoricalNB()

    y_pred = clf.fit(X2, y2).predict(X2)

    X3 = np.array([[1, 4], [2, 5]])
    y3 = np.array([1, 2])
    clf = CategoricalNB(alpha=1, fit_prior=False)

    clf.fit(X3, y3)
    assert_array_equal(clf.n_categories_, np.array([3, 6]))

    # Check error is raised for X with negative entries
    X = np.array([[0, -1]])
    error_msg = re.escape("Negative values in data passed to CategoricalNB (input X)")

    # Test alpha
    X3_test = np.array([[2, 5]])
    # alpha=1 increases the count of all categories by one so the final
    # probability for each category is not 50/50 but 1/3 to 2/3
    bayes_numerator = np.array([[1 / 3 * 1 / 3, 2 / 3 * 2 / 3]])
    bayes_denominator = bayes_numerator.sum()
        clf.predict_proba(X3_test), bayes_numerator / bayes_denominator
    )

    # Assert category_count has counted all features
    assert len(clf.category_count_) == X3.shape[1]

    # Check sample_weight
    X = np.array([[0, 0], [0, 1], [0, 0], [1, 1]])
    assert_array_equal(clf.predict(np.array([[0, 0]])), np.array([1]))
    assert_array_equal(clf.n_categories_, np.array([2, 2]))

    for factor in [1.0, 0.3, 5, 0.0001]:
        X = np.array([[0, 0], [0, 1], [0, 0], [1, 1]])
        y = np.array([1, 1, 2, 2])
        sample_weight = np.array([1, 1, 10, 0.1]) * factor
        clf = CategoricalNB(alpha=1, fit_prior=False)
        assert_array_equal(clf.predict(np.array([[0, 0]])), np.array([2]))
        assert_array_equal(clf.n_categories_, np.array([2, 2]))

def test_categoricalnb_with_min_categories(
    min_categories, exp_X1_count, exp_X2_count, new_X, exp_n_categories_
):
    X_n_categories = np.array([[0, 0], [0, 1], [0, 0], [1, 1]])
    y_n_categories = np.array([1, 1, 2, 2])
    expected_prediction = np.array([1])

    clf = CategoricalNB(alpha=1, fit_prior=False, min_categories=min_categories)
    clf.fit(X_n_categories, y_n_categories)
    X1_count, X2_count = clf.category_count_
    assert_array_equal(X1_count, exp_X1_count)
    assert_array_equal(X2_count, exp_X2_count)
    predictions = clf.predict(new_X)
    assert_array_equal(predictions, expected_prediction)
    assert_array_equal(clf.n_categories_, exp_n_categories_)

def test_categoricalnb_min_categories_errors(min_categories, error_msg):

def test_alpha(csr_container):
    # Setting alpha=0 should not output nan results when p(x_i|y_j)=0 is a case
    X = np.array([[1, 0], [1, 1]])
    nb = BernoulliNB(alpha=0.0, force_alpha=False)
    msg = "alpha too small will result in numeric errors, setting alpha = 1.0e-10"
        nb.partial_fit(X, y, classes=[0, 1])
        nb.fit(X, y)
    prob = np.array([[1, 0], [0, 1]])
    assert_array_almost_equal(nb.predict_proba(X), prob)

    nb = MultinomialNB(alpha=0.0, force_alpha=False)
    prob = np.array([[2.0 / 3, 1.0 / 3], [0, 1]])

    nb = CategoricalNB(alpha=0.0, force_alpha=False)
    prob = np.array([[1.0, 0.0], [0.0, 1.0]])

    # Test sparse X

def test_alpha_vector():

    # Setting alpha=np.array with same length
    # as number of features should be fine
    alpha = np.array([1, 2])
    nb = MultinomialNB(alpha=alpha, force_alpha=False)
    nb.partial_fit(X, y, classes=[0, 1])

    # Test feature probabilities uses pseudo-counts (alpha)
    feature_prob = np.array([[1 / 2, 1 / 2], [2 / 5, 3 / 5]])
    assert_array_almost_equal(nb.feature_log_prob_, np.log(feature_prob))

    # Test predictions
    prob = np.array([[5 / 9, 4 / 9], [25 / 49, 24 / 49]])

    # Test alpha non-negative
    alpha = np.array([1.0, -0.1])
    m_nb = MultinomialNB(alpha=alpha, force_alpha=False)
    expected_msg = "All values in alpha must be greater than 0."
        m_nb.fit(X, y)

    # Test that too small pseudo-counts are replaced
    ALPHA_MIN = 1e-10
    alpha = np.array([ALPHA_MIN / 2, 0.5])
    m_nb.partial_fit(X, y, classes=[0, 1])
    assert_array_almost_equal(m_nb._check_alpha(), [ALPHA_MIN, 0.5], decimal=12)

    # Test correct dimensions
    alpha = np.array([1.0, 2.0, 3.0])
    expected_msg = "When alpha is an array, it should contains `n_features`"

def test_check_accuracy_on_digits():
    # Non regression test to make sure that any further refactoring / optim
    # of the NB models do not harm the performance on a slightly non-linearly
    # separable dataset
    binary_3v8 = np.logical_or(y == 3, y == 8)
    X_3v8, y_3v8 = X[binary_3v8], y[binary_3v8]

    # Multinomial NB
    scores = cross_val_score(MultinomialNB(alpha=10), X, y, cv=10)
    assert scores.mean() > 0.86

    scores = cross_val_score(MultinomialNB(alpha=10), X_3v8, y_3v8, cv=10)
    assert scores.mean() > 0.94

    # Bernoulli NB
    scores = cross_val_score(BernoulliNB(alpha=10), X > 4, y, cv=10)
    assert scores.mean() > 0.83

    scores = cross_val_score(BernoulliNB(alpha=10), X_3v8 > 4, y_3v8, cv=10)
    assert scores.mean() > 0.92

    # Gaussian NB
    scores = cross_val_score(GaussianNB(), X, y, cv=10)
    assert scores.mean() > 0.77

    scores = cross_val_score(GaussianNB(var_smoothing=0.1), X, y, cv=10)
    assert scores.mean() > 0.89

    scores = cross_val_score(GaussianNB(), X_3v8, y_3v8, cv=10)

def test_check_alpha():
    """The provided value for alpha must only be
    used if alpha < _ALPHA_MIN and force_alpha is True.

    https://github.com/scikit-learn/scikit-learn/issues/10772
    """
    _ALPHA_MIN = 1e-10
    b = BernoulliNB(alpha=0, force_alpha=True)
    assert b._check_alpha() == 0

    alphas = np.array([0.0, 1.0])

    b = BernoulliNB(alpha=alphas, force_alpha=True)
    # We manually set `n_features_in_` not to have `_check_alpha` err
    b.n_features_in_ = alphas.shape[0]
    assert_array_equal(b._check_alpha(), alphas)

    msg = (
        "alpha too small will result in numeric errors, setting alpha = %.1e"
        % _ALPHA_MIN
    )
    b = BernoulliNB(alpha=0, force_alpha=False)
        assert b._check_alpha() == _ALPHA_MIN

    b = BernoulliNB(alpha=alphas, force_alpha=False)
        assert_array_equal(b._check_alpha(), np.array([_ALPHA_MIN, 1.0]))

def test_predict_joint_proba(Estimator, global_random_seed):
    est = Estimator().fit(X2, y2)
    jll = est.predict_joint_log_proba(X2)
    log_prob_x = logsumexp(jll, axis=1)
    log_prob_x_y = jll - np.atleast_2d(log_prob_x).T
    assert_allclose(est.predict_log_proba(X2), log_prob_x_y)

def test_openmp_parallelism_enabled():
    # Check that sklearn is built with OpenMP-based parallelism enabled.
    # This test can be skipped by setting the environment variable
    # ``SKLEARN_SKIP_OPENMP_TEST``.
    if os.getenv("SKLEARN_SKIP_OPENMP_TEST"):
        pytest.skip("test explicitly skipped (SKLEARN_SKIP_OPENMP_TEST)")

    base_url = "dev" if __version__.endswith(".dev0") else "stable"
    err_msg = textwrap.dedent(
        """
        This test fails because scikit-learn has been built without OpenMP.
        This is not recommended since some estimators will run in sequential
        mode instead of leveraging thread-based parallelism.

        You can find instructions to build scikit-learn with OpenMP at this
        address:

            https://scikit-learn.org/{}/developers/advanced_installation.html

        You can skip this test by setting the environment variable
        SKLEARN_SKIP_OPENMP_TEST to any value.
        """
    ).format(base_url)

    assert _openmp_parallelism_enabled(), err_msg

def test_ovr_exceptions():
    ovr = OneVsRestClassifier(LinearSVC(random_state=0))

    # test predicting without fitting
        ovr.predict([])

    # Fail on multioutput data
    msg = "Multioutput target data is not supported with label binarization"
        X = np.array([[1, 0], [0, 1]])
        y = np.array([[1, 2], [3, 1]])
        OneVsRestClassifier(MultinomialNB()).fit(X, y)

        y = np.array([[1.5, 2.4], [3.1, 0.8]])

def test_check_classification_targets():
    # Test that check_classification_target return correct type. #5782
    y = np.array([0.0, 1.1, 2.0, 3.0])
    msg = type_of_target(y)

def test_ovr_fit_predict():
    # A classifier which implements decision_function.
    pred = ovr.fit(iris.data, iris.target).predict(iris.data)
    assert len(ovr.estimators_) == n_classes

    pred2 = clf.fit(iris.data, iris.target).predict(iris.data)
    assert np.mean(iris.target == pred) == np.mean(iris.target == pred2)

    # A classifier which implements predict_proba.
    ovr = OneVsRestClassifier(MultinomialNB())
    assert np.mean(iris.target == pred) > 0.65

def test_ovr_partial_fit():
    # Test if partial_fit is working as intended
    X, y = shuffle(iris.data, iris.target, random_state=0)
    ovr.partial_fit(X[:100], y[:100], np.unique(y))
    ovr.partial_fit(X[100:], y[100:])
    pred = ovr.predict(X)
    ovr2 = OneVsRestClassifier(MultinomialNB())
    pred2 = ovr2.fit(X, y).predict(X)

    assert_almost_equal(pred, pred2)
    assert len(ovr.estimators_) == len(np.unique(y))
    assert np.mean(y == pred) > 0.65

    # Test when mini batches doesn't have all classes
    # with SGDClassifier
    X = np.abs(np.random.randn(14, 2))
    y = [1, 1, 1, 1, 2, 3, 3, 0, 0, 2, 3, 1, 2, 3]

    ovr = OneVsRestClassifier(
        SGDClassifier(max_iter=1, tol=None, shuffle=False, random_state=0)
    )
    ovr.partial_fit(X[:7], y[:7], np.unique(y))
    ovr.partial_fit(X[7:], y[7:])
    ovr1 = OneVsRestClassifier(
    )
    pred1 = ovr1.fit(X, y).predict(X)
    assert np.mean(pred == y) == np.mean(pred1 == y)

    # test partial_fit only exists if estimator has it:
    ovr = OneVsRestClassifier(SVC())
    assert not hasattr(ovr, "partial_fit")

def test_ovr_partial_fit_exceptions():
    # If a new class that was not in the first call of partial fit is seen
    # it should raise ValueError
    y1 = [5] + y[7:-1]
    msg = r"Mini-batch contains \[.+\] while classes must be subset of \[.+\]"
        ovr.partial_fit(X=X[7:], y=y1)

def test_ovr_ovo_regressor():
    # test that ovr and ovo work on regressors which don't have a decision_
    # function
    ovr = OneVsRestClassifier(DecisionTreeRegressor())
    assert_array_equal(np.unique(pred), [0, 1, 2])
    # we are doing something sensible
    assert np.mean(pred == iris.target) > 0.9

    ovr = OneVsOneClassifier(DecisionTreeRegressor())
    assert len(ovr.estimators_) == n_classes * (n_classes - 1) / 2

def test_ovr_fit_predict_sparse(sparse_container):
    base_clf = MultinomialNB(alpha=1)

    X, Y = datasets.make_multilabel_classification(
        n_classes=5,
        n_labels=3,
        length=50,
    )

    X_train, Y_train = X[:80], Y[:80]
    X_test = X[80:]

    clf = OneVsRestClassifier(base_clf).fit(X_train, Y_train)
    Y_pred = clf.predict(X_test)

    clf_sprs = OneVsRestClassifier(base_clf).fit(X_train, sparse_container(Y_train))
    Y_pred_sprs = clf_sprs.predict(X_test)

    assert clf.multilabel_
    assert sp.issparse(Y_pred_sprs)
    assert_array_equal(Y_pred_sprs.toarray(), Y_pred)

    # Test predict_proba
    Y_proba = clf_sprs.predict_proba(X_test)

    # predict assigns a label if the probability that the
    # sample has the label is greater than 0.5.
    pred = Y_proba > 0.5
    assert_array_equal(pred, Y_pred_sprs.toarray())

    clf_sprs = OneVsRestClassifier(clf).fit(X_train, sparse_container(Y_train))
    dec_pred = (clf_sprs.decision_function(X_test) > 0).astype(int)
    assert_array_equal(dec_pred, clf_sprs.predict(X_test).toarray())

def test_ovr_always_present():
    # Test that ovr works with classes that are always present or absent.
    # Note: tests is the case where _ConstantPredictor is utilised
    X = np.ones((10, 2))
    X[:5, :] = 0

    # Build an indicator matrix where two features are always on.
    # As list of lists, it would be: [[int(i >= 5), 2, 3] for i in range(10)]
    y = np.zeros((10, 3))
    y[5:, 0] = 1
    y[:, 1] = 1
    y[:, 2] = 1

    msg = r"Label .+ is present in all training examples"
        ovr.fit(X, y)
    y_pred = ovr.predict(X)
    assert_array_equal(np.array(y_pred), np.array(y))
    y_pred = ovr.decision_function(X)
    assert np.unique(y_pred[:, -2:]) == 1
    y_pred = ovr.predict_proba(X)
    assert_array_equal(y_pred[:, -1], np.ones(X.shape[0]))

    # y has a constantly absent label
    y = np.zeros((10, 2))
    y[5:, 0] = 1  # variable label

    msg = r"Label not 1 is present in all training examples"
    assert_array_equal(y_pred[:, -1], np.zeros(X.shape[0]))

def test_ovr_multiclass():
    # Toy dataset where features correspond directly to labels.
    X = np.array([[0, 0, 5], [0, 5, 0], [3, 0, 0], [0, 0, 6], [6, 0, 0]])
    y = ["eggs", "spam", "ham", "eggs", "ham"]
    Y = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0], [0, 0, 1], [1, 0, 0]])

    classes = set("ham eggs spam".split())

    for base_clf in (
        MultinomialNB(),
        LinearSVC(random_state=0),
        Ridge(),
        ElasticNet(),
    ):
        clf = OneVsRestClassifier(base_clf).fit(X, y)
        assert set(clf.classes_) == classes
        y_pred = clf.predict(np.array([[0, 0, 4]]))[0]
        assert_array_equal(y_pred, ["eggs"])

        # test input as label indicator matrix
        clf = OneVsRestClassifier(base_clf).fit(X, Y)
        y_pred = clf.predict([[0, 0, 4]])[0]
        assert_array_equal(y_pred, [0, 0, 1])

def test_ovr_binary():
    y = ["eggs", "spam", "spam", "eggs", "spam"]
    Y = np.array([[0, 1, 1, 0, 1]]).T

    classes = set("eggs spam".split())

    def conduct_test(base_clf, test_predict_proba=False):
        if hasattr(base_clf, "decision_function"):
            dec = clf.decision_function(X)
            assert dec.shape == (5,)

        if test_predict_proba:
            X_test = np.array([[0, 0, 4]])
            probabilities = clf.predict_proba(X_test)
            assert 2 == len(probabilities[0])
            assert clf.classes_[np.argmax(probabilities, axis=1)] == clf.predict(X_test)

        y_pred = clf.predict([[3, 0, 0]])[0]
        assert y_pred == 1

        Ridge(),
    ):
        conduct_test(base_clf)

    for base_clf in (MultinomialNB(), SVC(probability=True), LogisticRegression()):
        conduct_test(base_clf, test_predict_proba=True)

def test_ovr_multilabel():
    X = np.array([[0, 4, 5], [0, 5, 0], [3, 3, 3], [4, 0, 6], [6, 0, 0]])
    y = np.array([[0, 1, 1], [0, 1, 0], [1, 1, 1], [1, 0, 1], [1, 0, 0]])

        Ridge(),
        Lasso(alpha=0.5),
    ):
        y_pred = clf.predict([[0, 4, 4]])[0]
        assert_array_equal(y_pred, [0, 1, 1])
        assert clf.multilabel_

def test_ovr_fit_predict_svc():
    ovr = OneVsRestClassifier(svm.SVC())
    ovr.fit(iris.data, iris.target)
    assert len(ovr.estimators_) == 3
    assert ovr.score(iris.data, iris.target) > 0.9

def test_ovr_multilabel_dataset():
    for au, prec, recall in zip((True, False), (0.51, 0.66), (0.51, 0.80)):
        X, Y = datasets.make_multilabel_classification(
            n_classes=5,
            n_labels=2,
            length=50,
            allow_unlabeled=au,
        )
        X_train, Y_train = X[:80], Y[:80]
        X_test, Y_test = X[80:], Y[80:]
        clf = OneVsRestClassifier(base_clf).fit(X_train, Y_train)
        Y_pred = clf.predict(X_test)

            precision_score(Y_test, Y_pred, average="micro"), prec, decimal=2
        )
            recall_score(Y_test, Y_pred, average="micro"), recall, decimal=2
        )

def test_ovr_multilabel_predict_proba():
    for au in (False, True):
            n_labels=3,
        )
        X_test = X[80:]

        # Decision function only estimator.
        decision_only = OneVsRestClassifier(svm.SVR()).fit(X_train, Y_train)
        assert not hasattr(decision_only, "predict_proba")

        # Estimator with predict_proba disabled, depending on parameters.
        decision_only = OneVsRestClassifier(svm.SVC(probability=False))
        decision_only.fit(X_train, Y_train)
        assert hasattr(decision_only, "decision_function")

        # Estimator which can get predict_proba enabled after fitting
        gs = GridSearchCV(
            svm.SVC(probability=False), param_grid={"probability": [True]}
        )
        proba_after_fit = OneVsRestClassifier(gs)
        assert not hasattr(proba_after_fit, "predict_proba")
        proba_after_fit.fit(X_train, Y_train)
        assert hasattr(proba_after_fit, "predict_proba")

        Y_proba = clf.predict_proba(X_test)

        # predict assigns a label if the probability that the
        # sample has the label is greater than 0.5.
        pred = Y_proba > 0.5
        assert_array_equal(pred, Y_pred)

def test_ovr_single_label_predict_proba():
    X, Y = iris.data, iris.target

    # Decision function only estimator.
    decision_only = OneVsRestClassifier(svm.SVR()).fit(X_train, Y_train)
    assert not hasattr(decision_only, "predict_proba")

    Y_proba = clf.predict_proba(X_test)

    assert_almost_equal(Y_proba.sum(axis=1), 1.0)
    # sample has the label with the greatest predictive probability.
    pred = Y_proba.argmax(axis=1)
    assert not (pred - Y_pred).any()

def test_ovr_multilabel_decision_function():
    )
    clf = OneVsRestClassifier(svm.SVC()).fit(X_train, Y_train)
        (clf.decision_function(X_test) > 0).astype(int), clf.predict(X_test)
    )

def test_ovr_single_label_decision_function():
    X, Y = datasets.make_classification(n_samples=100, n_features=20, random_state=0)
    assert_array_equal(clf.decision_function(X_test).ravel() > 0, clf.predict(X_test))

def test_ovr_gridsearch():
    Cs = [0.1, 0.5, 0.8]
    cv = GridSearchCV(ovr, {"estimator__C": Cs})
    cv.fit(iris.data, iris.target)
    best_C = cv.best_estimator_.estimators_[0].C
    assert best_C in Cs

def test_ovr_pipeline():
    # Test with pipeline of length one
    # This test is needed because the multiclass estimators may fail to detect
    # the presence of predict_proba or decision_function.
    clf = Pipeline([("tree", DecisionTreeClassifier())])
    ovr_pipe = OneVsRestClassifier(clf)
    ovr_pipe.fit(iris.data, iris.target)
    ovr = OneVsRestClassifier(DecisionTreeClassifier())
    assert_array_equal(ovr.predict(iris.data), ovr_pipe.predict(iris.data))

def test_ovo_exceptions():
    ovo = OneVsOneClassifier(LinearSVC(random_state=0))
        ovo.predict([])

def test_ovo_fit_on_list():
    # Test that OneVsOne fitting works with a list of targets and yields the
    # same output as predict from an array
    prediction_from_array = ovo.fit(iris.data, iris.target).predict(iris.data)
    iris_data_list = [list(a) for a in iris.data]
    prediction_from_list = ovo.fit(iris_data_list, list(iris.target)).predict(
        iris_data_list
    )
    assert_array_equal(prediction_from_array, prediction_from_list)

def test_ovo_fit_predict():
    ovo.fit(iris.data, iris.target).predict(iris.data)
    assert len(ovo.estimators_) == n_classes * (n_classes - 1) / 2

    ovo = OneVsOneClassifier(MultinomialNB())

def test_ovo_partial_fit_predict():
    temp = datasets.load_iris()
    X, y = temp.data, temp.target
    ovo1 = OneVsOneClassifier(MultinomialNB())
    ovo1.partial_fit(X[:100], y[:100], np.unique(y))
    ovo1.partial_fit(X[100:], y[100:])
    pred1 = ovo1.predict(X)

    ovo2 = OneVsOneClassifier(MultinomialNB())
    ovo2.fit(X, y)
    pred2 = ovo2.predict(X)
    assert len(ovo1.estimators_) == n_classes * (n_classes - 1) / 2
    assert np.mean(y == pred1) > 0.65

    # Test when mini-batches have binary target classes
    ovo1.partial_fit(X[:60], y[:60], np.unique(y))
    ovo1.partial_fit(X[60:], y[60:])
    pred2 = ovo2.fit(X, y).predict(X)

    assert len(ovo1.estimators_) == len(np.unique(y))

    X = np.random.rand(14, 2)
    y = [1, 1, 2, 3, 3, 0, 0, 4, 4, 4, 4, 4, 2, 2]
    ovo.partial_fit(X[:7], y[:7], [0, 1, 2, 3, 4])
    ovo.partial_fit(X[7:], y[7:])
    pred = ovo.predict(X)

    # raises error when mini-batch does not have classes from all_classes
    error_y = [0, 1, 2, 3, 4, 5, 2]
    message_re = escape(
        "Mini-batch contains {0} while it must be subset of {1}".format(
            np.unique(error_y), np.unique(y)
        )
    )
    with pytest.raises(ValueError, match=message_re):
        ovo.partial_fit(X[:7], error_y, np.unique(y))

    ovr = OneVsOneClassifier(SVC())

def test_ovo_decision_function():
    n_samples = iris.data.shape[0]

    ovo_clf = OneVsOneClassifier(LinearSVC(random_state=0))
    # first binary
    ovo_clf.fit(iris.data, iris.target == 0)
    decisions = ovo_clf.decision_function(iris.data)
    assert decisions.shape == (n_samples,)

    # then multi-class
    ovo_clf.fit(iris.data, iris.target)

    assert decisions.shape == (n_samples, n_classes)
    assert_array_equal(decisions.argmax(axis=1), ovo_clf.predict(iris.data))

    # Compute the votes
    votes = np.zeros((n_samples, n_classes))

    k = 0
    for i in range(n_classes):
        for j in range(i + 1, n_classes):
            pred = ovo_clf.estimators_[k].predict(iris.data)
            votes[pred == 0, i] += 1
            votes[pred == 1, j] += 1
            k += 1

    # Extract votes and verify
    assert_array_equal(votes, np.round(decisions))

    for class_idx in range(n_classes):
        # For each sample and each class, there only 3 possible vote levels
        # because they are only 3 distinct class pairs thus 3 distinct
        # binary classifiers.
        # Therefore, sorting predictions based on votes would yield
        # mostly tied predictions:
        assert set(votes[:, class_idx]).issubset(set([0.0, 1.0, 2.0]))

        # The OVO decision function on the other hand is able to resolve
        # most of the ties on this data as it combines both the vote counts
        # and the aggregated confidence levels of the binary classifiers
        # to compute the aggregate decision function. The iris dataset
        # has 150 samples with a couple of duplicates. The OvO decisions
        # can resolve most of the ties:
        assert len(np.unique(decisions[:, class_idx])) > 146

def test_ovo_gridsearch():
    cv = GridSearchCV(ovo, {"estimator__C": Cs})

def test_ovo_ties():
    # Test that ties are broken using the decision function,
    # not defaulting to the smallest label
    X = np.array([[1, 2], [2, 1], [-2, 1], [-2, -1]])
    y = np.array([2, 0, 1, 2])
    multi_clf = OneVsOneClassifier(Perceptron(shuffle=False, max_iter=4, tol=None))
    ovo_prediction = multi_clf.fit(X, y).predict(X)
    ovo_decision = multi_clf.decision_function(X)

    # Classifiers are in order 0-1, 0-2, 1-2
    # Use decision_function to compute the votes and the normalized
    # sum_of_confidences, which is used to disambiguate when there is a tie in
    # votes.
    votes = np.round(ovo_decision)
    normalized_confidences = ovo_decision - votes

    # For the first point, there is one vote per class
    assert_array_equal(votes[0, :], 1)
    # For the rest, there is no tie and the prediction is the argmax
    assert_array_equal(np.argmax(votes[1:], axis=1), ovo_prediction[1:])
    # For the tie, the prediction is the class with the highest score
    assert ovo_prediction[0] == normalized_confidences[0].argmax()

def test_ovo_ties2():
    # test that ties can not only be won by the first two labels
    y_ref = np.array([2, 0, 1, 2])

    # cycle through labels so that each label wins once
        y = (y_ref + i) % 3
        multi_clf = OneVsOneClassifier(Perceptron(shuffle=False, max_iter=4, tol=None))
        ovo_prediction = multi_clf.fit(X, y).predict(X)
        assert ovo_prediction[0] == i % 3

def test_ovo_string_y():
    # Test that the OvO doesn't mess up the encoding of string labels
    X = np.eye(4)
    y = np.array(["a", "b", "c", "d"])

    ovo = OneVsOneClassifier(LinearSVC())
    ovo.fit(X, y)
    assert_array_equal(y, ovo.predict(X))

def test_ovo_one_class():
    # Test error for OvO with one class
    y = np.array(["a"] * 4)

    msg = "when only one class"
        ovo.fit(X, y)

def test_ovo_float_y():
    # Test that the OvO errors on float targets
    y = iris.data[:, 0]

def test_ecoc_exceptions():
    ecoc = OutputCodeClassifier(LinearSVC(random_state=0))
        ecoc.predict([])

def test_ecoc_fit_predict():
    ecoc = OutputCodeClassifier(LinearSVC(random_state=0), code_size=2, random_state=0)
    ecoc.fit(iris.data, iris.target).predict(iris.data)
    assert len(ecoc.estimators_) == n_classes * 2

    ecoc = OutputCodeClassifier(MultinomialNB(), code_size=2, random_state=0)

def test_ecoc_gridsearch():
    ecoc = OutputCodeClassifier(LinearSVC(random_state=0), random_state=0)
    cv = GridSearchCV(ecoc, {"estimator__C": Cs})

def test_ecoc_float_y():
    # Test that the OCC errors on float targets

    ovo = OutputCodeClassifier(LinearSVC())

def test_ecoc_delegate_sparse_base_estimator(csc_container):
    # Non-regression test for
    # https://github.com/scikit-learn/scikit-learn/issues/17218
    X_sp = csc_container(X)

    # create an estimator that does not support sparse input
    base_estimator = CheckingClassifier(
        check_X=check_array,
        check_X_params={"ensure_2d": True, "accept_sparse": False},
    )
    ecoc = OutputCodeClassifier(base_estimator, random_state=0)

    with pytest.raises(TypeError, match="Sparse data was passed"):
        ecoc.fit(X_sp, y)

    ecoc.fit(X, y)
        ecoc.predict(X_sp)

    # smoke test to check when sparse input should be supported
    ecoc.fit(X_sp, y).predict(X_sp)
    assert len(ecoc.estimators_) == 4

def test_pairwise_indices():
    clf_precomputed = svm.SVC(kernel="precomputed")

    ovr_false = OneVsOneClassifier(clf_precomputed)
    linear_kernel = np.dot(X, X.T)
    ovr_false.fit(linear_kernel, y)

    n_estimators = len(ovr_false.estimators_)
    precomputed_indices = ovr_false.pairwise_indices_

    for idx in precomputed_indices:
        assert (
            idx.shape[0] * n_estimators / (n_estimators - 1) == linear_kernel.shape[0]
        )

def test_pairwise_n_features_in():
    """Check the n_features_in_ attributes of the meta and base estimators

    When the training data is a regular design matrix, everything is intuitive.
    However, when the training data is a precomputed kernel matrix, the
    multiclass strategy can resample the kernel matrix of the underlying base
    estimator both row-wise and column-wise and this has a non-trivial impact
    on the expected value for the n_features_in_ of both the meta and the base
    estimators.
    """

    # Remove the last sample to make the classes not exactly balanced and make
    # the test more interesting.
    assert y[-1] == 0
    X = X[:-1]
    y = y[:-1]

    # Fitting directly on the design matrix:
    assert X.shape == (149, 4)

    clf_notprecomputed = svm.SVC(kernel="linear").fit(X, y)
    assert clf_notprecomputed.n_features_in_ == 4

    ovr_notprecomputed = OneVsRestClassifier(clf_notprecomputed).fit(X, y)
    assert ovr_notprecomputed.n_features_in_ == 4
    for est in ovr_notprecomputed.estimators_:
        assert est.n_features_in_ == 4

    ovo_notprecomputed = OneVsOneClassifier(clf_notprecomputed).fit(X, y)
    assert ovo_notprecomputed.n_features_in_ == 4
    assert ovo_notprecomputed.n_classes_ == 3
    assert len(ovo_notprecomputed.estimators_) == 3
    for est in ovo_notprecomputed.estimators_:

    # When working with precomputed kernels we have one "feature" per training
    # sample:
    K = X @ X.T
    assert K.shape == (149, 149)

    clf_precomputed = svm.SVC(kernel="precomputed").fit(K, y)
    assert clf_precomputed.n_features_in_ == 149

    ovr_precomputed = OneVsRestClassifier(clf_precomputed).fit(K, y)
    assert ovr_precomputed.n_features_in_ == 149
    assert ovr_precomputed.n_classes_ == 3
    assert len(ovr_precomputed.estimators_) == 3
    for est in ovr_precomputed.estimators_:
        assert est.n_features_in_ == 149

    # This becomes really interesting with OvO and precomputed kernel together:
    # internally, OvO will drop the samples of the classes not part of the pair
    # of classes under consideration for a given binary classifier. Since we
    # use a precomputed kernel, it will also drop the matching columns of the
    # kernel matrix, and therefore we have fewer "features" as result.
    #
    # Since class 0 has 49 samples, and class 1 and 2 have 50 samples each, a
    # single OvO binary classifier works with a sub-kernel matrix of shape
    # either (99, 99) or (100, 100).
    ovo_precomputed = OneVsOneClassifier(clf_precomputed).fit(K, y)
    assert ovo_precomputed.n_features_in_ == 149
    assert ovo_precomputed.estimators_[0].n_features_in_ == 99  # class 0 vs class 1
    assert ovo_precomputed.estimators_[1].n_features_in_ == 99  # class 0 vs class 2
    assert ovo_precomputed.estimators_[2].n_features_in_ == 100  # class 1 vs class 2

def test_pairwise_tag(MultiClassClassifier):
    clf_notprecomputed = svm.SVC()

    ovr_false = MultiClassClassifier(clf_notprecomputed)
    assert not ovr_false.__sklearn_tags__().input_tags.pairwise

    ovr_true = MultiClassClassifier(clf_precomputed)
    assert ovr_true.__sklearn_tags__().input_tags.pairwise

def test_pairwise_cross_val_score(MultiClassClassifier):
    clf_notprecomputed = svm.SVC(kernel="linear")

    multiclass_clf_notprecomputed = MultiClassClassifier(clf_notprecomputed)
    multiclass_clf_precomputed = MultiClassClassifier(clf_precomputed)

    score_not_precomputed = cross_val_score(
        multiclass_clf_notprecomputed, X, y, error_score="raise"
    )
    score_precomputed = cross_val_score(
        multiclass_clf_precomputed, linear_kernel, y, error_score="raise"
    )
    assert_array_equal(score_precomputed, score_not_precomputed)

def test_support_missing_values(MultiClassClassifier):
    # smoke test to check that pipeline OvR and OvO classifiers are letting
    # the underlying pipeline or classifiers
    X = np.copy(X)  # Copy to avoid that the original data is modified
    mask = rng.choice([1, 0], X.shape, p=[0.1, 0.9]).astype(bool)
    lr = make_pipeline(SimpleImputer(), LogisticRegression(random_state=rng))

    MultiClassClassifier(lr).fit(X, y).score(X, y)

def test_constant_int_target(make_y):
    """Check that constant y target does not raise.

    Non-regression test for #21869
    """
    y = make_y((10, 1), dtype=np.int32)

    ovr.fit(X, y)
    expected = np.zeros((X.shape[0], 2))
    expected[:, 0] = 1

def test_ovo_consistent_binary_classification():
    """Check that ovo is consistent with binary classifier.

    Non-regression test for #13617.
    """

    clf = KNeighborsClassifier(n_neighbors=8, weights="distance")
    ovo = OneVsOneClassifier(clf)

    assert_array_equal(clf.predict(X), ovo.predict(X))

def test_multiclass_estimator_attribute_error():
    does not implement the `partial_fit` method, which is decorated with

    """

    # LogisticRegression does not implement 'partial_fit' and should raise an
    # AttributeError
    clf = OneVsRestClassifier(estimator=LogisticRegression(random_state=42))

    outer_msg = "This 'OneVsRestClassifier' has no attribute 'partial_fit'"
    inner_msg = "'LogisticRegression' object has no attribute 'partial_fit'"
        clf.partial_fit(iris.data, iris.target)

def get_all_methods():
    estimators = all_estimators()
    displays = all_displays()
    for name, Klass in estimators + displays:
        if name.startswith("_"):
            # skip private classes
            continue
        methods = []
        for name in dir(Klass):
            if name.startswith("_"):
                continue
            method_obj = getattr(Klass, name)
            if hasattr(method_obj, "__call__") or isinstance(method_obj, property):
                methods.append(name)
        methods.append(None)

        for method in sorted(methods, key=str):
            yield Klass, method

def get_all_functions_names():
    functions = all_functions()
    for _, func in functions:
        # exclude functions from utils.fixex since they come from external packages
        if "utils.fixes" not in func.__module__:
            yield f"{func.__module__}.{func.__name__}"

def filter_errors(errors, method, Klass=None):
    """
    Ignore some errors based on the method type.

    These rules are specific for scikit-learn."""
    for code, message in errors:
        # We ignore following error code,
        #  - RT02: The first line of the Returns section
        #    should contain only the type, ..
        #   (as we may need refer to the name of the returned
        #    object)
        #  - GL01: Docstring text (summary) should start in the line
        #    immediately after the opening quotes (not in the same line,
        #    or leaving a blank line in between)
        #  - GL02: If there's a blank line, it should be before the
        #    first line of the Returns section, not after (it allows to have
        #    short docstrings for properties).

        if code in ["RT02", "GL01", "GL02"]:
            continue

        # Ignore PR02: Unknown parameters for properties. We sometimes use
        # properties for ducktyping, i.e. SGDClassifier.predict_proba
        # Ignore GL08: Parsing of the method signature failed, possibly because this is
        # a property. Properties are sometimes used for deprecated attributes and the
        # attribute is already documented in the class docstring.
        #
        # All error codes:
        # https://numpydoc.readthedocs.io/en/latest/validation.html#built-in-validation-checks
        if code in ("PR02", "GL08") and Klass is not None and method is not None:
            method_obj = getattr(Klass, method)
            if isinstance(method_obj, property):
                continue

        # Following codes are only taken into account for the
        # top level class docstrings:
        #  - ES01: No extended summary found
        #  - SA01: See Also section not found
        #  - EX01: No examples section found

        if method is not None and code in ["EX01", "SA01", "ES01"]:
            continue
        yield code, message

def repr_errors(res, Klass=None, method: Optional[str] = None) -> str:
    """Pretty print original docstring and the obtained errors

    res : dict
        result of numpydoc.validate.validate
    Klass : {Estimator, Display, None}
        estimator object or None
        if estimator is not None, either the method name or None.

    Returns
    -------
    str
       String representation of the error.
    """
    if method is None:
        if hasattr(Klass, "__init__"):
            method = "__init__"
        elif Klass is None:
            raise ValueError("At least one of Klass, method should be provided")
        else:

    if Klass is not None:
        obj = getattr(Klass, method)
        try:
            obj_signature = str(signature(obj))
            # In particular we can't parse the signature of properties
            obj_signature = (
                "\nParsing of the method signature failed, "
                "possibly because this is a property."
            )

        obj_name = Klass.__name__ + "." + method
    else:
        obj_signature = ""
        obj_name = method

    msg = "\n\n" + "\n\n".join(
        [
            str(res["file"]),
            obj_name + obj_signature,
            res["docstring"],
            "# Errors",
            "\n".join(
                " - {}: {}".format(code, message) for code, message in res["errors"]
            ),
        ]
    )
    return msg

def test_function_docstring(function_name, request):
    """Check function docstrings using numpydoc."""
    res = numpydoc_validation.validate(function_name)

    res["errors"] = list(filter_errors(res["errors"], method="function"))

    if res["errors"]:
        msg = repr_errors(res, method=f"Tested function: {function_name}")

def test_docstring(Klass, method, request):
    base_import_path = Klass.__module__
    import_path = [base_import_path, Klass.__name__]
    if method is not None:
        import_path.append(method)

    import_path = ".".join(import_path)

    res = numpydoc_validation.validate(import_path)

    res["errors"] = list(filter_errors(res["errors"], method, Klass=Klass))

        msg = repr_errors(res, Klass, method)

def _get_func_info(func_module):
    module_name, func_name = func_module.rsplit(".", 1)
    module = import_module(module_name)
    func = getattr(module, func_name)

    func_sig = signature(func)
    func_params = [
        p.name
        for p in func_sig.parameters.values()
        if p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
    ]

    # The parameters `*args` and `**kwargs` are ignored since we cannot generate
    # constraints.
    required_params = [
        p.name
        if p.default is p.empty and p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
    ]

    return func, func_name, func_params, required_params

def _check_function_param_validation(
    func, func_name, func_params, required_params, parameter_constraints
):
    """Check that an informative error is raised when the value of a parameter does not
    have an appropriate type or value.
    """
    # generate valid values for the required parameters
    valid_required_params = {}
    for param_name in required_params:
        if parameter_constraints[param_name] == "no_validation":
            valid_required_params[param_name] = 1
        else:
            valid_required_params[param_name] = generate_valid_param(
                make_constraint(parameter_constraints[param_name][0])
            )

    # check that there is a constraint for each parameter
    if func_params:
        validation_params = parameter_constraints.keys()
        unexpected_params = set(validation_params) - set(func_params)
        missing_params = set(func_params) - set(validation_params)
        err_msg = (
            "Mismatch between _parameter_constraints and the parameters of"
            f" {func_name}.\nConsider the unexpected parameters {unexpected_params} and"
            f" expected but missing parameters {missing_params}\n"
        )
        assert set(validation_params) == set(func_params), err_msg

    # this object does not have a valid type for sure for all params
    param_with_bad_type = type("BadType", (), {})()

    for param_name in func_params:
        constraints = parameter_constraints[param_name]

        if constraints == "no_validation":
            # This parameter is not validated
            continue

        # Mixing an interval of reals and an interval of integers must be avoided.
        if any(
            isinstance(constraint, Interval) and constraint.type == Integral
            for constraint in constraints
        ) and any(
            isinstance(constraint, Interval) and constraint.type == Real
        ):
                f"The constraint for parameter {param_name} of {func_name} can't have a"
                " mix of intervals of Integral and Real types. Use the type"
                " RealNotInt instead of Real."
            )

        match = (
            rf"The '{param_name}' parameter of {func_name} must be .* Got .* instead."
        )

        err_msg = (
            f"{func_name} does not raise an informative error message when the "
            f"parameter {param_name} does not have a valid type. If any Python type "
            "is valid, the constraint should be 'no_validation'."
        )

        # First, check that the error is raised if param doesn't match any valid type.
        with pytest.raises(InvalidParameterError, match=match):
            func(**{**valid_required_params, param_name: param_with_bad_type})
            pytest.fail(err_msg)

        # Then, for constraints that are more than a type constraint, check that the
        # error is raised if param does match a valid type but does not match any valid
        # value for this type.
        constraints = [make_constraint(constraint) for constraint in constraints]

        for constraint in constraints:
            try:
                bad_value = generate_invalid_param_val(constraint)
            except NotImplementedError:
                continue

            err_msg = (
                f"{func_name} does not raise an informative error message when the "
                f"parameter {param_name} does not have a valid value.\n"
                "Constraints should be disjoint. For instance "
                "[StrOptions({'a_string'}), str] is not a acceptable set of "
                "constraint because generating an invalid string for the first "
                "constraint will always produce a valid string for the second "
                "constraint."
            )

            with pytest.raises(InvalidParameterError, match=match):
                func(**{**valid_required_params, param_name: bad_value})
                pytest.fail(err_msg)

def test_function_param_validation(func_module):
    """Check param validation for public functions that are not wrappers around
    """
    func, func_name, func_params, required_params = _get_func_info(func_module)

    parameter_constraints = getattr(func, "_skl_parameter_constraints")

    _check_function_param_validation(
        func, func_name, func_params, required_params, parameter_constraints
    )

def test_class_wrapper_param_validation(func_module, class_module):
    """Check param validation for public functions that are wrappers around
    """

    module_name, class_name = class_module.rsplit(".", 1)
    klass = getattr(module, class_name)

    parameter_constraints_func = getattr(func, "_skl_parameter_constraints")
    parameter_constraints_class = getattr(klass, "_parameter_constraints")
    parameter_constraints = {
        **parameter_constraints_class,
        **parameter_constraints_func,
    }
        k: v for k, v in parameter_constraints.items() if k in func_params
    }

    )

def test_kernel_ridge():
    pred = Ridge(alpha=1, fit_intercept=False).fit(X, y).predict(X)
    pred2 = KernelRidge(kernel="linear", alpha=1).fit(X, y).predict(X)
    assert_array_almost_equal(pred, pred2)

def test_kernel_ridge_sparse(sparse_container):
    pred = (
        Ridge(alpha=1, fit_intercept=False, solver="cholesky")
        .fit(X_sparse, y)
        .predict(X_sparse)
    )
    pred2 = KernelRidge(kernel="linear", alpha=1).fit(X_sparse, y).predict(X_sparse)

def test_kernel_ridge_singular_kernel():
    # alpha=0 causes a LinAlgError in computing the dual coefficients,
    # which causes a fallback to a lstsq solver. This is tested here.
    pred = Ridge(alpha=0, fit_intercept=False).fit(X, y).predict(X)
    kr = KernelRidge(kernel="linear", alpha=0)
    ignore_warnings(kr.fit)(X, y)
    pred2 = kr.predict(X)

def test_kernel_ridge_precomputed():
    for kernel in ["linear", "rbf", "poly", "cosine"]:
        K = pairwise_kernels(X, X, metric=kernel)
        pred = KernelRidge(kernel=kernel).fit(X, y).predict(X)
        pred2 = KernelRidge(kernel="precomputed").fit(K, y).predict(K)
        assert_array_almost_equal(pred, pred2)

def test_kernel_ridge_precomputed_kernel_unchanged():
    K = np.dot(X, X.T)
    K2 = K.copy()
    KernelRidge(kernel="precomputed").fit(K, y)

def test_kernel_ridge_sample_weights():
    K = np.dot(X, X.T)  # precomputed kernel
    sw = np.random.RandomState(0).rand(X.shape[0])

    pred = Ridge(alpha=1, fit_intercept=False).fit(X, y, sample_weight=sw).predict(X)
    pred2 = KernelRidge(kernel="linear", alpha=1).fit(X, y, sample_weight=sw).predict(X)
    pred3 = (
        KernelRidge(kernel="precomputed", alpha=1)
        .fit(K, y, sample_weight=sw)
        .predict(K)
    )
    assert_array_almost_equal(pred, pred3)

def test_kernel_ridge_multi_output():
    pred = Ridge(alpha=1, fit_intercept=False).fit(X, Y).predict(X)
    pred2 = KernelRidge(kernel="linear", alpha=1).fit(X, Y).predict(X)

    pred3 = KernelRidge(kernel="linear", alpha=1).fit(X, y).predict(X)
    pred3 = np.array([pred3, pred3]).T
    assert_array_almost_equal(pred2, pred3)

def test_min_dependencies_readme():
    # Test that the minimum dependencies in the README.rst file are
    # consistent with the minimum dependencies defined at the file:
    # sklearn/_min_dependencies.py

    pattern = re.compile(
        r"(\.\. \|)"
        r"(([A-Za-z]+\-?)+)"
        r"(MinVersion\| replace::)"
        r"( [0-9]+\.[0-9]+(\.[0-9]+)?)"
    )

    readme_path = Path(sklearn.__file__).parent.parent
    readme_file = readme_path / "README.rst"

    if not os.path.exists(readme_file):
        # Skip the test if the README.rst file is not available.
        # For instance, when installing scikit-learn from wheels
        pytest.skip("The README.rst file is not available.")

    with readme_file.open("r") as f:
        for line in f:
            matched = pattern.match(line)

            if not matched:
                continue

            package, version = matched.group(2), matched.group(5)
            package = package.lower()

            if package in dependent_packages:
                version = parse_version(version)
                min_version = parse_version(dependent_packages[package][0])

                assert version == min_version, f"{package} has a mismatched version"

def check_pyproject_section(
    pyproject_section, min_dependencies_tag, skip_version_check_for=None
):
    # tomllib is available in Python 3.11
    tomllib = pytest.importorskip("tomllib")

    if skip_version_check_for is None:
        skip_version_check_for = []

    expected_packages = min_depencies_tag_to_packages_without_version[
        min_dependencies_tag
    ]

    root_directory = Path(sklearn.__file__).parent.parent
    pyproject_toml_path = root_directory / "pyproject.toml"

    if not pyproject_toml_path.exists():
        # Skip the test if the pyproject.toml file is not available.
        pytest.skip("pyproject.toml is not available.")

    with pyproject_toml_path.open("rb") as f:
        pyproject_toml = tomllib.load(f)

    pyproject_section_keys = pyproject_section.split(".")
    info = pyproject_toml
    for key in pyproject_section_keys:
        info = info[key]

    pyproject_build_min_versions = {}
    for requirement in info:
        if ">=" in requirement:
            package, version = requirement.split(">=")
        elif "==" in requirement:
            package, version = requirement.split("==")
        else:
                f"{requirement} not supported yet in this test. "
                "Only >= and == are supported for version requirements"
            )

        pyproject_build_min_versions[package] = version

    assert sorted(pyproject_build_min_versions) == sorted(expected_packages)

    for package, version in pyproject_build_min_versions.items():
        version = parse_version(version)
        expected_min_version = parse_version(dependent_packages[package][0])
        if package in skip_version_check_for:
            continue

        assert version == expected_min_version, f"{package} has a mismatched version"

def test_min_dependencies_pyproject_toml(pyproject_section, min_dependencies_tag):
    """Check versions in pyproject.toml is consistent with _min_dependencies."""
    # NumPy is more complex because build-time (>=1.25) and run-time (>=1.19.5)
    # requirement currently don't match
    skip_version_check_for = ["numpy"] if min_dependencies_tag == "build" else None
    check_pyproject_section(
        pyproject_section,
        min_dependencies_tag,
        skip_version_check_for=skip_version_check_for,
    )

class DelegatorData:
        self,
        name,
        construct,
        skip_methods=(),
        fit_args=make_classification(random_state=0),
    ):
        self.name = name
        self.construct = construct
        self.fit_args = fit_args
        self.skip_methods = skip_methods

def test_metaestimator_delegation():
    # Ensures specified metaestimators have methods iff subestimator does
    def hides(method):
        @property
        def wrapper(obj):
            if obj.hidden_method == method.__name__:
                raise AttributeError("%r is hidden" % obj.hidden_method)
            return functools.partial(method, obj)

    class SubEstimator(BaseEstimator):
        def __init__(self, param=1, hidden_method=None):
            self.param = param
            self.hidden_method = hidden_method

        def fit(self, X, y=None, *args, **kwargs):
            self.coef_ = np.arange(X.shape[1])

        def _check_fit(self):

        @hides
        def inverse_transform(self, X, *args, **kwargs):
            self._check_fit()
            return X

        @hides
        def transform(self, X, *args, **kwargs):
            return X

        @hides
        def predict(self, X, *args, **kwargs):
            return np.ones(X.shape[0])

        @hides
        def predict_proba(self, X, *args, **kwargs):

        @hides
        def predict_log_proba(self, X, *args, **kwargs):

        @hides
        def decision_function(self, X, *args, **kwargs):

        @hides
        def score(self, X, y, *args, **kwargs):
            return 1.0

    methods = [
        k
        for k in SubEstimator.__dict__.keys()
        if not k.startswith("_") and not k.startswith("fit")
    ]
    methods.sort()

    for delegator_data in DELEGATING_METAESTIMATORS:
        delegate = SubEstimator()
        delegator = delegator_data.construct(delegate)
            if method in delegator_data.skip_methods:
                continue
            assert hasattr(delegate, method)
            assert hasattr(delegator, method), (
                "%s does not have method %r when its delegate does"
                % (
                    delegator_data.name,
                    method,
                )
            )
            # delegation before fit raises a NotFittedError
            if method == "score":
                with pytest.raises(NotFittedError):
                    getattr(delegator, method)(
                        delegator_data.fit_args[0], delegator_data.fit_args[1]
                    )
            else:
                    getattr(delegator, method)(delegator_data.fit_args[0])

        delegator.fit(*delegator_data.fit_args)
                continue
            # smoke test delegation
                getattr(delegator, method)(
                    delegator_data.fit_args[0], delegator_data.fit_args[1]
                )
            else:
                getattr(delegator, method)(delegator_data.fit_args[0])

                continue
            delegate = SubEstimator(hidden_method=method)
            delegator = delegator_data.construct(delegate)
            assert not hasattr(delegate, method)
            assert not hasattr(delegator, method), (
                "%s has method %r when its delegate does not"
                % (
                    method,
                )
            )

def _get_instance_with_pipeline(meta_estimator, init_params):
    """Given a single meta-estimator instance, generate an instance with a pipeline"""
    if {"estimator", "base_estimator", "regressor"} & init_params:
        if is_regressor(meta_estimator):
            estimator = make_pipeline(TfidfVectorizer(), Ridge())
            param_grid = {"ridge__alpha": [0.1, 1.0]}
        else:
            estimator = make_pipeline(TfidfVectorizer(), LogisticRegression())
            param_grid = {"logisticregression__C": [0.1, 1.0]}

        if init_params.intersection(
            {"param_grid", "param_distributions"}
        ):  # SearchCV estimators
            extra_params = {"n_iter": 2} if "n_iter" in init_params else {}
            return type(meta_estimator)(estimator, param_grid, **extra_params)
        else:
            return type(meta_estimator)(estimator)

    if "transformer_list" in init_params:
        # FeatureUnion
        transformer_list = [
            ("trans1", make_pipeline(TfidfVectorizer(), MaxAbsScaler())),
            (
                "trans2",
                make_pipeline(TfidfVectorizer(), StandardScaler(with_mean=False)),
            ),
        ]
        return type(meta_estimator)(transformer_list)

    if "estimators" in init_params:
        # stacking, voting
            estimator = [
                ("est1", make_pipeline(TfidfVectorizer(), Ridge(alpha=0.1))),
                ("est2", make_pipeline(TfidfVectorizer(), Ridge(alpha=1))),
            ]
        else:
                (
                    "est1",
                    make_pipeline(TfidfVectorizer(), LogisticRegression(C=0.1)),
                ),
                ("est2", make_pipeline(TfidfVectorizer(), LogisticRegression(C=1))),
            ]
        return type(meta_estimator)(estimator)

def _generate_meta_estimator_instances_with_pipeline():
    """Generate instances of meta-estimators fed with a pipeline

    Are considered meta-estimators all estimators accepting one of "estimator",
    "base_estimator" or "estimators".
    """
    print("estimators: ", len(all_estimators()))
    for _, Estimator in sorted(all_estimators()):
        sig = set(signature(Estimator).parameters)

        print("\n", Estimator.__name__, sig)
        if not sig.intersection(
            {
                "estimator",
                "base_estimator",
                "regressor",
                "transformer_list",
                "estimators",
            }
        ):
            continue

        with suppress(SkipTest):
            for meta_estimator in _construct_instances(Estimator):
                print(meta_estimator)
                yield _get_instance_with_pipeline(meta_estimator, sig)

def _get_meta_estimator_id(estimator):
    return estimator.__class__.__name__

def test_meta_estimators_delegate_data_validation(estimator):
    # Check that meta-estimators delegate data validation to the inner
    # estimator(s).

    X = rng.choice(np.array(["aa", "bb", "cc"], dtype=object), size=n_samples)

        y = rng.normal(size=n_samples)
    else:
        y = rng.randint(3, size=n_samples)

    # We convert to lists to make sure it works on array-like
    X = _enforce_estimator_tags_X(estimator, X).tolist()
    y = _enforce_estimator_tags_y(estimator, y).tolist()

    # Calling fit should not raise any data validation exception since X is a
    # valid input datastructure for the first step of the pipeline passed as
    # base estimator to the meta estimator.

    # n_features_in_ should not be defined since data is not tabular data.
    assert not hasattr(estimator, "n_features_in_")

        self,
        name,
    ):

        @property

        @hides
            return X

        @hides
            return X

        @hides

        @hides

        @hides

        @hides

        @hides
            return 1.0

            return X

            return X

            return 1.0

def test_permutation_invariance():
    # check that fit is permutation invariant.
    # regression test of missing sorting of sample-weights
    ir = IsotonicRegression()
    x = [1, 2, 3, 4, 5, 6, 7]
    y = [1, 41, 51, 1, 2, 5, 24]
    sample_weight = [1, 2, 3, 4, 5, 6, 7]
    x_s, y_s, sample_weight_s = shuffle(x, y, sample_weight, random_state=0)
    y_transformed = ir.fit_transform(x, y, sample_weight=sample_weight)
    y_transformed_s = ir.fit(x_s, y_s, sample_weight=sample_weight_s).transform(x)

    assert_array_equal(y_transformed, y_transformed_s)

def test_check_increasing_small_number_of_samples():
    x = [0, 1, 2]
    y = [1, 1.1, 1.05]

        is_increasing = check_increasing(x, y)

    assert is_increasing

def test_check_increasing_up():
    x = [0, 1, 2, 3, 4, 5]
    y = [0, 1.5, 2.77, 8.99, 8.99, 50]

    # Check that we got increasing=True and no warnings

def test_check_increasing_up_extreme():
    y = [0, 1, 2, 3, 4, 5]

def test_check_increasing_down():
    y = [0, -1.5, -2.77, -8.99, -8.99, -50]

    # Check that we got increasing=False and no warnings

    assert not is_increasing

def test_check_increasing_down_extreme():
    y = [0, -1, -2, -3, -4, -5]

def test_check_ci_warn():
    y = [0, -1, 2, -3, 4, -5]

    # Check that we got increasing=False and CI interval warning
    msg = "interval"

def test_isotonic_regression():
    y = np.array([3, 7, 5, 9, 8, 7, 10])
    y_ = np.array([3, 6, 6, 8, 8, 8, 10])
    assert_array_equal(y_, isotonic_regression(y))

    y = np.array([10, 0, 2])
    y_ = np.array([4, 4, 4])

    x = np.arange(len(y))
    ir = IsotonicRegression(y_min=0.0, y_max=1.0)
    ir.fit(x, y)
    assert_array_equal(ir.fit(x, y).transform(x), ir.fit_transform(x, y))
    assert_array_equal(ir.transform(x), ir.predict(x))

    # check that it is immune to permutation
    perm = np.random.permutation(len(y))
    assert_array_equal(ir.fit_transform(x[perm], y[perm]), ir.fit_transform(x, y)[perm])
    assert_array_equal(ir.transform(x[perm]), ir.transform(x)[perm])

    # check we don't crash when all x are equal:
    assert_array_equal(ir.fit_transform(np.ones(len(x)), y), np.mean(y))

def test_isotonic_regression_ties_min():
    # Setup examples with ties on minimum
    x = [1, 1, 2, 3, 4, 5]
    y = [1, 2, 3, 4, 5, 6]
    y_true = [1.5, 1.5, 3, 4, 5, 6]

    # Check that we get identical results for fit/transform and fit_transform
    assert_array_equal(y_true, ir.fit_transform(x, y))

def test_isotonic_regression_ties_max():
    # Setup examples with ties on maximum
    x = [1, 2, 3, 4, 5, 5]
    y_true = [1, 2, 3, 4, 5.5, 5.5]

def test_isotonic_regression_ties_secondary_():
    """
    Test isotonic regression fit, transform  and fit_transform
    against the "secondary" ties method and "pituitary" data from R
     "isotone" package, as detailed in: J. d. Leeuw, K. Hornik, P. Mair,
     Isotone Optimization in R: Pool-Adjacent-Violators Algorithm
    (PAVA) and Active Set Methods

    Set values based on pituitary example and
     the following R command detailed in the paper above:
    > library("isotone")
    > data("pituitary")
    > res1 <- gpava(pituitary$age, pituitary$size, ties="secondary")
    > res1$x

    `isotone` version: 1.0-2, 2014-09-07
    R version: R version 3.1.1 (2014-07-10)
    """
    x = [8, 8, 8, 10, 10, 10, 12, 12, 12, 14, 14]
    y = [21, 23.5, 23, 24, 21, 25, 21.5, 22, 19, 23.5, 25]
    y_true = [
        22.22222,
        22.22222,
        22.22222,
        22.22222,
        22.22222,
        22.22222,
        22.22222,
        22.22222,
        22.22222,
        24.25,
        24.25,
    ]

    # Check fit, transform and fit_transform
    assert_array_almost_equal(ir.transform(x), y_true, 4)
    assert_array_almost_equal(ir.fit_transform(x, y), y_true, 4)

def test_isotonic_regression_with_ties_in_differently_sized_groups():
    """
    Non-regression test to handle issue 9432:
    https://github.com/scikit-learn/scikit-learn/issues/9432

    Compare against output in R:
    > x <- c(0, 1, 1, 2, 3, 4)
    > y <- c(0, 0, 1, 0, 0, 1)
    > res1 <- gpava(x, y, ties="secondary")
    > res1$x

    `isotone` version: 1.1-0, 2015-07-24
    R version: R version 3.3.2 (2016-10-31)
    """
    x = np.array([0, 1, 1, 2, 3, 4])
    y = np.array([0, 0, 1, 0, 0, 1])
    y_true = np.array([0.0, 0.25, 0.25, 0.25, 0.25, 1.0])
    assert_array_almost_equal(ir.transform(x), y_true)
    assert_array_almost_equal(ir.fit_transform(x, y), y_true)

def test_isotonic_regression_reversed():
    y = np.array([10, 9, 10, 7, 6, 6.1, 5])
    y_result = np.array([10, 9.5, 9.5, 7, 6.05, 6.05, 5])

    y_iso = isotonic_regression(y, increasing=False)
    assert_allclose(y_iso, y_result)

    y_ = IsotonicRegression(increasing=False).fit_transform(np.arange(len(y)), y)
    assert_allclose(y_, y_result)
    assert_array_equal(np.ones(y_[:-1].shape), ((y_[:-1] - y_[1:]) >= 0))

def test_isotonic_regression_auto_decreasing():
    # Set y and x for decreasing

    # Create model and fit_transform
    ir = IsotonicRegression(increasing="auto")
    with warnings.catch_warnings(record=True) as w:
        y_ = ir.fit_transform(x, y)
        # work-around for pearson divide warnings in scipy <= 0.17.0
        assert all(["invalid value encountered in " in str(warn.message) for warn in w])

    # Check that relationship decreases
    is_increasing = y_[0] < y_[-1]

def test_isotonic_regression_auto_increasing():
    y = np.array([5, 6.1, 6, 7, 10, 9, 10])

    # Check that relationship increases

def test_assert_raises_exceptions():

        ir.fit([0, 1, 2], [5, 7, 3], [0.1, 0.6])

        ir.fit([0, 1, 2], [5, 7])

    msg = "X should be a 1d array"
        ir.fit(rng.randn(3, 10), [0, 1, 2])

    msg = "Isotonic regression input X should be a 1d array"
        ir.transform(rng.randn(3, 10))

def test_isotonic_sample_weight_parameter_default_value():
    # check if default value of sample_weight parameter is one
    # random test data
    n = 100
    x = np.arange(n)
    y = rng.randint(-50, 50, size=(n,)) + 50.0 * np.log(1 + np.arange(n))
    # check if value is correctly used
    weights = np.ones(n)
    y_set_value = ir.fit_transform(x, y, sample_weight=weights)
    y_default_value = ir.fit_transform(x, y)

    assert_array_equal(y_set_value, y_default_value)

def test_isotonic_min_max_boundaries():
    # check if min value is used correctly
    ir = IsotonicRegression(y_min=2, y_max=4)
    n = 6
    y = np.arange(n)
    y_test = [2, 2, 2, 3, 4, 4]
    y_result = np.round(ir.fit_transform(x, y))
    assert_array_equal(y_result, y_test)

def test_isotonic_sample_weight():
    expected_y = [1, 13.95, 13.95, 13.95, 13.95, 13.95, 24]
    received_y = ir.fit_transform(x, y, sample_weight=sample_weight)

    assert_array_equal(expected_y, received_y)

def test_isotonic_regression_oob_raise():
    # Set y and x

    # Create model and fit
    ir = IsotonicRegression(increasing="auto", out_of_bounds="raise")

    # Check that an exception is thrown
    msg = "in x_new is below the interpolation range"
        ir.predict([min(x) - 10, max(x) + 10])

def test_isotonic_regression_oob_clip():
    # Set y and x

    ir = IsotonicRegression(increasing="auto", out_of_bounds="clip")

    # Predict from  training and test x and check that min/max match.
    y1 = ir.predict([min(x) - 10, max(x) + 10])
    y2 = ir.predict(x)
    assert max(y1) == max(y2)
    assert min(y1) == min(y2)

def test_isotonic_regression_oob_nan():
    # Set y and x

    ir = IsotonicRegression(increasing="auto", out_of_bounds="nan")

    # Predict from  training and test x and check that we have two NaNs.
    assert sum(np.isnan(y1)) == 2

def test_isotonic_regression_pickle():

    ir_ser = pickle.dumps(ir, pickle.HIGHEST_PROTOCOL)
    ir2 = pickle.loads(ir_ser)
    np.testing.assert_array_equal(ir.predict(x), ir2.predict(x))

def test_isotonic_duplicate_min_entry():
    x = [0, 0, 1]
    y = [0, 0, 1]

    ir = IsotonicRegression(increasing=True, out_of_bounds="clip")
    all_predictions_finite = np.all(np.isfinite(ir.predict(x)))
    assert all_predictions_finite

def test_isotonic_ymin_ymax():
    # Test from @NelleV's issue:
    # https://github.com/scikit-learn/scikit-learn/issues/6921
        [
            1.263,
            1.318,
            -0.572,
            0.307,
            -0.707,
            -0.176,
            -1.599,
            1.059,
            1.396,
            1.906,
            0.210,
            0.028,
            -0.081,
            0.444,
            0.018,
            -0.377,
            -0.896,
            -0.377,
            -1.327,
            0.180,
        ]
    )
    y = isotonic_regression(x, y_min=0.0, y_max=0.1)

    assert np.all(y >= 0)
    assert np.all(y <= 0.1)

    # Also test decreasing case since the logic there is different
    y = isotonic_regression(x, y_min=0.0, y_max=0.1, increasing=False)

    # Finally, test with only one bound
    y = isotonic_regression(x, y_min=0.0, increasing=False)

def test_isotonic_zero_weight_loop():
    # Test from @ogrisel's issue:
    # https://github.com/scikit-learn/scikit-learn/issues/4297

    # Get deterministic RNG with seed

    # Create regression and samples
    regression = IsotonicRegression()
    x = np.linspace(-3, 3, n_samples)
    y = x + rng.uniform(size=n_samples)

    # Get some random weights and zero out
    w = rng.uniform(size=n_samples)
    w[5:8] = 0
    regression.fit(x, y, sample_weight=w)

    # This will hang in failure case.

def test_fast_predict():
    # test that the faster prediction change doesn't
    # affect out-of-sample predictions:
    # https://github.com/scikit-learn/scikit-learn/pull/6206
    rng = np.random.RandomState(123)
    n_samples = 10**3
    # X values over the -10,10 range
    X_train = 20.0 * rng.rand(n_samples) - 10
    y_train = (
        np.less(rng.rand(n_samples), expit(X_train)).astype("int64").astype("float64")
    )

    weights = rng.rand(n_samples)
    # we also want to test that everything still works when some weights are 0
    weights[rng.rand(n_samples) < 0.1] = 0

    slow_model = IsotonicRegression(y_min=0, y_max=1, out_of_bounds="clip")
    fast_model = IsotonicRegression(y_min=0, y_max=1, out_of_bounds="clip")

    # Build interpolation function with ALL input data, not just the
    # non-redundant subset. The following 2 lines are taken from the
    # .fit() method, without removing unnecessary points
    X_train_fit, y_train_fit = slow_model._build_y(
        X_train, y_train, sample_weight=weights, trim_duplicates=False
    )
    slow_model._build_f(X_train_fit, y_train_fit)

    # fit with just the necessary data
    fast_model.fit(X_train, y_train, sample_weight=weights)

    X_test = 20.0 * rng.rand(n_samples) - 10
    y_pred_slow = slow_model.predict(X_test)
    y_pred_fast = fast_model.predict(X_test)

    assert_array_equal(y_pred_slow, y_pred_fast)

def test_isotonic_copy_before_fit():
    # https://github.com/scikit-learn/scikit-learn/issues/6628
    copy.copy(ir)

def test_isotonic_dtype(dtype):
    y = [2, 1, 4, 3, 5]
    weights = np.array([0.9, 0.9, 0.9, 0.9, 0.9], dtype=np.float64)
    reg = IsotonicRegression()

    for sample_weight in (None, weights.astype(np.float32), weights):
        y_np = np.array(y, dtype=dtype)
        expected_dtype = check_array(
            y_np, dtype=[np.float64, np.float32], ensure_2d=False
        ).dtype

        res = isotonic_regression(y_np, sample_weight=sample_weight)
        assert res.dtype == expected_dtype

        X = np.arange(len(y)).astype(dtype)
        reg.fit(X, y_np, sample_weight=sample_weight)
        res = reg.predict(X)

def test_isotonic_mismatched_dtype(y_dtype):
    # regression test for #15004
    # check that data are converted when X and y dtype differ
    y = np.array([2, 1, 4, 3, 5], dtype=y_dtype)
    X = np.arange(len(y), dtype=np.float32)
    assert reg.predict(X).dtype == X.dtype

def test_make_unique_dtype():
    x_list = [2, 2, 2, 3, 5]
    for dtype in (np.float32, np.float64):
        x = np.array(x_list, dtype=dtype)
        y = x.copy()
        w = np.ones_like(x)
        x, y, w = _make_unique(x, y, w)
        assert_array_equal(x, [2, 3, 5])

def test_make_unique_tolerance(dtype):
    # Check that equality takes account of np.finfo tolerance
    x = np.array([0, 1e-16, 1, 1 + 1e-14], dtype=dtype)
    y = x.copy()
    w = np.ones_like(x)
    x, y, w = _make_unique(x, y, w)
    if dtype == np.float64:
        x_out = np.array([0, 1, 1 + 1e-14])
    else:
        x_out = np.array([0, 1])
    assert_array_equal(x, x_out)

def test_isotonic_make_unique_tolerance():
    # Check that averaging of targets for duplicate X is done correctly,
    # taking into account tolerance
    X = np.array([0, 1, 1 + 1e-16, 2], dtype=np.float64)
    y = np.array([0, 1, 2, 3], dtype=np.float64)
    ireg = IsotonicRegression().fit(X, y)
    y_pred = ireg.predict([0, 0.5, 1, 1.5, 2])

    assert_array_equal(y_pred, np.array([0, 0.75, 1.5, 2.25, 3]))
    assert_array_equal(ireg.X_thresholds_, np.array([0.0, 1.0, 2.0]))
    assert_array_equal(ireg.y_thresholds_, np.array([0.0, 1.5, 3.0]))

def test_isotonic_non_regression_inf_slope():
    # Non-regression test to ensure that inf values are not returned
    # see: https://github.com/scikit-learn/scikit-learn/issues/10903
    X = np.array([0.0, 4.1e-320, 4.4e-314, 1.0])
    y = np.array([0.42, 0.42, 0.44, 0.44])
    y_pred = ireg.predict(np.array([0, 2.1e-319, 5.4e-316, 1e-10]))
    assert np.all(np.isfinite(y_pred))

def test_isotonic_thresholds(increasing):
    X = rng.normal(size=n_samples)
    y = rng.normal(size=n_samples)
    ireg = IsotonicRegression(increasing=increasing).fit(X, y)
    X_thresholds, y_thresholds = ireg.X_thresholds_, ireg.y_thresholds_
    assert X_thresholds.shape == y_thresholds.shape

    # Input thresholds are a strict subset of the training set (unless
    # the data is already strictly monotonic which is not the case with
    # this random data)
    assert X_thresholds.shape[0] < X.shape[0]
    assert np.isin(X_thresholds, X).all()

    # Output thresholds lie in the range of the training set:
    assert y_thresholds.max() <= y.max()
    assert y_thresholds.min() >= y.min()

    assert all(np.diff(X_thresholds) > 0)
    if increasing:
        assert all(np.diff(y_thresholds) >= 0)
    else:
        assert all(np.diff(y_thresholds) <= 0)

def test_input_shape_validation():
    # Test from #15012
    # Check that IsotonicRegression can handle 2darray with only 1 feature
    X = np.arange(10)
    X_2d = X.reshape(-1, 1)
    y = np.arange(10)

    iso_reg = IsotonicRegression().fit(X, y)
    iso_reg_2d = IsotonicRegression().fit(X_2d, y)

    assert iso_reg.X_max_ == iso_reg_2d.X_max_
    assert iso_reg.X_min_ == iso_reg_2d.X_min_
    assert iso_reg.y_max == iso_reg_2d.y_max
    assert iso_reg.y_min == iso_reg_2d.y_min
    assert_array_equal(iso_reg.X_thresholds_, iso_reg_2d.X_thresholds_)
    assert_array_equal(iso_reg.y_thresholds_, iso_reg_2d.y_thresholds_)

    y_pred1 = iso_reg.predict(X)
    y_pred2 = iso_reg_2d.predict(X_2d)
    assert_allclose(y_pred1, y_pred2)

def test_isotonic_2darray_more_than_1_feature():
    # Ensure IsotonicRegression raises error if input has more than 1 feature
    X_2d = np.c_[X, X]

    msg = "should be a 1d array or 2d array with 1 feature"
        IsotonicRegression().fit(X_2d, y)

        iso_reg.predict(X_2d)

        iso_reg.transform(X_2d)

def test_isotonic_regression_sample_weight_not_overwritten():
    """Check that calling fitting function of isotonic regression will not
    overwrite `sample_weight`.
    https://github.com/scikit-learn/scikit-learn/issues/20508
    """
    X, y = make_regression(n_samples=10, n_features=1, random_state=41)
    sample_weight_original = np.ones_like(y)
    sample_weight_original[0] = 10
    sample_weight_fit = sample_weight_original.copy()

    isotonic_regression(y, sample_weight=sample_weight_fit)
    assert_allclose(sample_weight_fit, sample_weight_original)

    IsotonicRegression().fit(X, y, sample_weight=sample_weight_fit)

def test_get_feature_names_out(shape):
    """Check `get_feature_names_out` for `IsotonicRegression`."""
    if shape == "2d":
        X = X.reshape(-1, 1)

    iso = IsotonicRegression().fit(X, y)
    assert isinstance(names, np.ndarray)
    assert names.dtype == object
    assert_array_equal(["isotonicregression0"], names)

def test_isotonic_regression_output_predict():
    """Check that `predict` does return the expected output type.

    We need to check that `transform` will output a DataFrame and a NumPy array
    when we set `transform_output` to `pandas`.

    https://github.com/scikit-learn/scikit-learn/issues/25499
    """
    X, y = make_regression(n_samples=10, n_features=1, random_state=42)
    regressor = IsotonicRegression()
        regressor.fit(X, y)
        X_trans = regressor.transform(X)
        y_pred = regressor.predict(X)

    assert isinstance(y_pred, np.ndarray)

def make_sparse_random_data(
    coo_container,
    n_nonzeros,
    sparse_format="csr",
):
    """Make some random data with uniformly located non zero entries with
    Gaussian distributed values; `sparse_format` can be `"csr"` (default) or
    `None` (in which case a dense array is returned).
    """
    data_coo = coo_container(
        (
            rng.randn(n_nonzeros),
            (
                rng.randint(n_samples, size=n_nonzeros),
                rng.randint(n_features, size=n_nonzeros),
            ),
        ),
        shape=(n_samples, n_features),
    )
    if sparse_format is not None:
        return data_coo.asformat(sparse_format)
    else:
        return data_coo.toarray()

def densify(matrix):
    if not sp.issparse(matrix):
        return matrix
    else:
        return matrix.toarray()

def test_invalid_jl_domain(n_samples, eps):
        johnson_lindenstrauss_min_dim(n_samples, eps=eps)

def test_input_size_jl_min_dim():
        johnson_lindenstrauss_min_dim(3 * [100], eps=2 * [0.9])

    johnson_lindenstrauss_min_dim(
        np.random.randint(1, 10, size=(10, 10)), eps=np.full((10, 10), 0.5)
    )

def check_input_size_random_matrix(random_matrix):
    inputs = [(0, 0), (-1, 1), (1, -1), (1, 0), (-1, 0)]
    for n_components, n_features in inputs:
            random_matrix(n_components, n_features)

def check_size_generated(random_matrix):
    inputs = [(1, 5), (5, 1), (5, 5), (1, 1)]
        assert random_matrix(n_components, n_features).shape == (
        )

def check_zero_mean_and_unit_norm(random_matrix):
    # All random matrix should produce a transformation matrix
    # with zero mean and unit norm for each columns

    A = densify(random_matrix(10000, 1, random_state=0))

    assert_array_almost_equal(0, np.mean(A), 3)
    assert_array_almost_equal(1.0, np.linalg.norm(A), 1)

def check_input_with_sparse_random_matrix(random_matrix):
    n_components, n_features = 5, 10

    for density in [-1.0, 0.0, 1.1]:
            random_matrix(n_components, n_features, density=density)

def test_basic_property_of_random_matrix(random_matrix):
    # Check basic properties of random matrix generation
    check_input_size_random_matrix(random_matrix)
    check_size_generated(random_matrix)
    check_zero_mean_and_unit_norm(random_matrix)

def test_basic_property_of_sparse_random_matrix(random_matrix):
    check_input_with_sparse_random_matrix(random_matrix)

    random_matrix_dense = functools.partial(random_matrix, density=1.0)

    check_zero_mean_and_unit_norm(random_matrix_dense)

def test_gaussian_random_matrix():
    # Check some statical properties of Gaussian random matrix
    # Check that the random matrix follow the proper distribution.
    # Let's say that each element of a_{ij} of A is taken from
    #   a_ij ~ N(0.0, 1 / n_components).
    #
    n_features = 1000
    A = _gaussian_random_matrix(n_components, n_features, random_state=0)

    assert_array_almost_equal(0.0, np.mean(A), 2)
    assert_array_almost_equal(np.var(A, ddof=1), 1 / n_components, 1)

def test_sparse_random_matrix():
    # Check some statical properties of sparse random matrix
    n_features = 500

    for density in [0.3, 1.0]:
        s = 1 / density

        A = _sparse_random_matrix(
            n_components, n_features, density=density, random_state=0
        )
        A = densify(A)

        # Check possible values
        values = np.unique(A)
        assert np.sqrt(s) / np.sqrt(n_components) in values
        assert -np.sqrt(s) / np.sqrt(n_components) in values

        if density == 1.0:
            assert np.size(values) == 2
        else:
            assert 0.0 in values
            assert np.size(values) == 3

        # Check that the random matrix follow the proper distribution.
        # Let's say that each element of a_{ij} of A is taken from
        #
        # - -sqrt(s) / sqrt(n_components)   with probability 1 / 2s
        # -  0                              with probability 1 - 1 / s
        # - +sqrt(s) / sqrt(n_components)   with probability 1 / 2s
        #
        assert_almost_equal(np.mean(A == 0.0), 1 - 1 / s, decimal=2)
            np.mean(A == np.sqrt(s) / np.sqrt(n_components)), 1 / (2 * s), decimal=2
        )
            np.mean(A == -np.sqrt(s) / np.sqrt(n_components)), 1 / (2 * s), decimal=2
        )

        assert_almost_equal(np.var(A == 0.0, ddof=1), (1 - 1 / s) * 1 / s, decimal=2)
            np.var(A == np.sqrt(s) / np.sqrt(n_components), ddof=1),
            (1 - 1 / (2 * s)) * 1 / (2 * s),
            decimal=2,
        )
            np.var(A == -np.sqrt(s) / np.sqrt(n_components), ddof=1),
        )

def test_random_projection_transformer_invalid_input():
    n_components = "auto"
    fit_data = [[0, 1, 2]]
    for RandomProjection in all_RandomProjection:
            RandomProjection(n_components=n_components).fit(fit_data)

def test_try_to_transform_before_fit(coo_container, global_random_seed):
    data = make_sparse_random_data(
        coo_container,
        n_features,
        n_nonzeros,
        sparse_format=None,
    )
            RandomProjection(n_components="auto").transform(data)

def test_too_many_samples_to_find_a_safe_embedding(coo_container, global_random_seed):
        n_samples=1000,
        n_features=100,
        n_nonzeros=1000,
    )

        rp = RandomProjection(n_components="auto", eps=0.1)
        expected_msg = (
            "eps=0.100000 and n_samples=1000 lead to a target dimension"
            " of 5920 which is larger than the original space with"
            " n_features=100"
        )
            rp.fit(data)

def test_random_projection_embedding_quality(coo_container):
        n_samples=8,
        n_features=5000,
        n_nonzeros=15000,
    )
    eps = 0.2

    original_distances = euclidean_distances(data, squared=True)
    original_distances = original_distances.ravel()
    non_identical = original_distances != 0.0

    # remove 0 distances to avoid division by 0
    original_distances = original_distances[non_identical]

        rp = RandomProjection(n_components="auto", eps=eps, random_state=0)
        projected = rp.fit_transform(data)

        projected_distances = euclidean_distances(projected, squared=True)
        projected_distances = projected_distances.ravel()

        # remove 0 distances to avoid division by 0
        projected_distances = projected_distances[non_identical]

        distances_ratio = projected_distances / original_distances

        # check that the automatically tuned values for the density respect the
        # contract for eps: pairwise distances are preserved according to the
        # Johnson-Lindenstrauss lemma
        assert distances_ratio.max() < 1 + eps
        assert 1 - eps < distances_ratio.min()

def test_SparseRandomProj_output_representation(coo_container):
    dense_data = make_sparse_random_data(
    )
    sparse_data = make_sparse_random_data(
        sparse_format="csr",
    )
    for SparseRandomProj in all_SparseRandomProjection:
        # when using sparse input, the projected data can be forced to be a
        # dense numpy array
        rp = SparseRandomProj(n_components=10, dense_output=True, random_state=0)
        rp.fit(dense_data)
        assert isinstance(rp.transform(dense_data), np.ndarray)
        assert isinstance(rp.transform(sparse_data), np.ndarray)

        # the output can be left to a sparse matrix instead
        rp = SparseRandomProj(n_components=10, dense_output=False, random_state=0)
        rp = rp.fit(dense_data)
        # output for dense input will stay dense:

        # output for sparse output will be sparse:
        assert sp.issparse(rp.transform(sparse_data))

def test_correct_RandomProjection_dimensions_embedding(
    coo_container, global_random_seed
):
    )
        rp = RandomProjection(n_components="auto", random_state=0, eps=0.5).fit(data)

        # the number of components is adjusted from the shape of the training
        # set
        assert rp.n_components == "auto"
        assert rp.n_components_ == 110

        if RandomProjection in all_SparseRandomProjection:
            assert rp.density == "auto"
            assert_almost_equal(rp.density_, 0.03, 2)

        assert rp.components_.shape == (110, n_features)

        projected_1 = rp.transform(data)
        assert projected_1.shape == (n_samples, 110)

        # once the RP is 'fitted' the projection is always the same
        projected_2 = rp.transform(data)
        assert_array_equal(projected_1, projected_2)

        # fit transform with same random seed will lead to the same results
        rp2 = RandomProjection(random_state=0, eps=0.5)
        projected_3 = rp2.fit_transform(data)
        assert_array_equal(projected_1, projected_3)

        # Try to transform with an input X of size different from fitted.
            rp.transform(data[:, 1:5])

        # it is also possible to fix the number of components and the density
        # level
            rp = RandomProjection(n_components=100, density=0.001, random_state=0)
            projected = rp.fit_transform(data)
            assert projected.shape == (n_samples, 100)
            assert rp.components_.shape == (100, n_features)
            assert rp.components_.nnz < 115  # close to 1% density
            assert 85 < rp.components_.nnz  # close to 1% density

def test_warning_n_components_greater_than_n_features(
):
    n_nonzeros = int(n_features / 4)
    )

        with pytest.warns(DataDimensionalityWarning):
            RandomProjection(n_components=n_features + 1).fit(data)

def test_works_with_sparse_data(coo_container, global_random_seed):
    )
    )

        rp_dense = RandomProjection(n_components=3, random_state=1).fit(dense_data)
        rp_sparse = RandomProjection(n_components=3, random_state=1).fit(sparse_data)
            densify(rp_dense.components_), densify(rp_sparse.components_)
        )

def test_johnson_lindenstrauss_min_dim():
    """Test Johnson-Lindenstrauss for small eps.

    Regression test for #17111: before #19374, 32-bit systems would fail.
    """
    assert johnson_lindenstrauss_min_dim(100, eps=1e-5) == 368416070986

def test_random_projection_feature_names_out(
    coo_container, random_projection_cls, global_random_seed
):
    )
    random_projection = random_projection_cls(n_components=2)
    random_projection.fit(data)
    names_out = random_projection.get_feature_names_out()
    class_name_lower = random_projection_cls.__name__.lower()
        [f"{class_name_lower}{i}" for i in range(random_projection.n_components_)],
    )

    random_projection_cls,
    compute_inverse_components,
):

    random_projection = random_projection_cls(
        compute_inverse_components=compute_inverse_components,
    )

    X_dense = make_sparse_random_data(
        n_nonzeros=n_samples * n_features // 100 + 1,
    )
    X_csr = make_sparse_random_data(
    )

    for X in [X_dense, X_csr]:
            warnings.filterwarnings(
                "ignore",
                message=(
                    "The number of components is higher than the number of features"
                ),
                category=DataDimensionalityWarning,
            )
            projected = random_projection.fit_transform(X)

        if compute_inverse_components:
            assert hasattr(random_projection, "inverse_components_")
            inv_components = random_projection.inverse_components_
            assert inv_components.shape == (n_features, n_components)

        projected_back = random_projection.inverse_transform(projected)
        assert projected_back.shape == X.shape

        projected_again = random_projection.transform(projected_back)
        if hasattr(projected, "toarray"):
            projected = projected.toarray()
        assert_allclose(projected, projected_again, rtol=1e-7, atol=1e-10)

def test_random_projection_dtype_match(
    random_projection_cls, input_dtype, expected_dtype
):
    X = rng.rand(25, 3000)
    rp = random_projection_cls(random_state=0)
    transformed = rp.fit_transform(X.astype(input_dtype))

    assert rp.components_.dtype == expected_dtype
    assert transformed.dtype == expected_dtype

def test_random_projection_numerical_consistency(random_projection_cls):
    # Verify numerical consistency among np.float32 and np.float64
    atol = 1e-5
    rp_32 = random_projection_cls(random_state=0)
    rp_64 = random_projection_cls(random_state=0)

    projection_32 = rp_32.fit_transform(X.astype(np.float32))
    projection_64 = rp_64.fit_transform(X.astype(np.float64))

    assert_allclose(projection_64, projection_32, atol=atol)

    assert_allclose_dense_sparse(rp_32.components_, rp_64.components_)

    X, y = make_classification(n_samples=N_SAMPLES, n_features=6, random_state=42)
    return X, y

def test_calibration(data, method, csr_container, ensemble):
    # Test calibration objects with isotonic and sigmoid
    n_samples = N_SAMPLES // 2
    X, y = data
    sample_weight = np.random.RandomState(seed=42).uniform(size=y.size)

    X -= X.min()  # MultinomialNB only allows positive X

    # split train and test
    X_train, y_train, sw_train = X[:n_samples], y[:n_samples], sample_weight[:n_samples]
    X_test, y_test = X[n_samples:], y[n_samples:]

    # Naive-Bayes
    clf = MultinomialNB().fit(X_train, y_train, sample_weight=sw_train)
    prob_pos_clf = clf.predict_proba(X_test)[:, 1]

    cal_clf = CalibratedClassifierCV(clf, cv=y.size + 1, ensemble=ensemble)
        cal_clf.fit(X, y)

    # Naive Bayes with calibration
    for this_X_train, this_X_test in [
        (X_train, X_test),
        (csr_container(X_train), csr_container(X_test)),
    ]:
        cal_clf = CalibratedClassifierCV(clf, method=method, cv=5, ensemble=ensemble)
        # Note that this fit overwrites the fit on the entire training
        # set
        cal_clf.fit(this_X_train, y_train, sample_weight=sw_train)
        prob_pos_cal_clf = cal_clf.predict_proba(this_X_test)[:, 1]

        # Check that brier score has improved after calibration
        assert brier_score_loss(y_test, prob_pos_clf) > brier_score_loss(
            y_test, prob_pos_cal_clf
        )

        # Check invariance against relabeling [0, 1] -> [1, 2]
        cal_clf.fit(this_X_train, y_train + 1, sample_weight=sw_train)
        prob_pos_cal_clf_relabeled = cal_clf.predict_proba(this_X_test)[:, 1]
        assert_array_almost_equal(prob_pos_cal_clf, prob_pos_cal_clf_relabeled)

        # Check invariance against relabeling [0, 1] -> [-1, 1]
        cal_clf.fit(this_X_train, 2 * y_train - 1, sample_weight=sw_train)

        # Check invariance against relabeling [0, 1] -> [1, 0]
        cal_clf.fit(this_X_train, (y_train + 1) % 2, sample_weight=sw_train)
        if method == "sigmoid":
            assert_array_almost_equal(prob_pos_cal_clf, 1 - prob_pos_cal_clf_relabeled)
        else:
            # Isotonic calibration is not invariant against relabeling
            # but should improve in both cases
            assert brier_score_loss(y_test, prob_pos_clf) > brier_score_loss(
                (y_test + 1) % 2, prob_pos_cal_clf_relabeled
            )

def test_calibration_default_estimator(data):
    # Check estimator default is LinearSVC
    X, y = data
    calib_clf = CalibratedClassifierCV(cv=2)
    calib_clf.fit(X, y)

    base_est = calib_clf.calibrated_classifiers_[0].estimator
    assert isinstance(base_est, LinearSVC)

def test_calibration_cv_splitter(data, ensemble):
    # Check when `cv` is a CV splitter
    X, y = data

    splits = 5
    kfold = KFold(n_splits=splits)
    calib_clf = CalibratedClassifierCV(cv=kfold, ensemble=ensemble)
    assert isinstance(calib_clf.cv, KFold)
    assert calib_clf.cv.n_splits == splits

    expected_n_clf = splits if ensemble else 1
    assert len(calib_clf.calibrated_classifiers_) == expected_n_clf

def test_calibration_cv_nfold(data):
    # Check error raised when number of examples per class less than nfold
    X, y = data

    kfold = KFold(n_splits=101)
    calib_clf = CalibratedClassifierCV(cv=kfold, ensemble=True)
    with pytest.raises(ValueError, match="Requesting 101-fold cross-validation"):
        calib_clf.fit(X, y)

    calib_clf = CalibratedClassifierCV(cv=LeaveOneOut(), ensemble=True)
    with pytest.raises(ValueError, match="LeaveOneOut cross-validation does"):

def test_sample_weight(data, method, ensemble):
    X, y = data

    sample_weight = np.random.RandomState(seed=42).uniform(size=len(y))
    X_test = X[n_samples:]

    estimator = LinearSVC(random_state=42)
    calibrated_clf = CalibratedClassifierCV(estimator, method=method, ensemble=ensemble)
    calibrated_clf.fit(X_train, y_train, sample_weight=sw_train)
    probs_with_sw = calibrated_clf.predict_proba(X_test)

    # As the weights are used for the calibration, they should still yield
    # different predictions
    calibrated_clf.fit(X_train, y_train)
    probs_without_sw = calibrated_clf.predict_proba(X_test)

    diff = np.linalg.norm(probs_with_sw - probs_without_sw)
    assert diff > 0.1

def test_parallel_execution(data, method, ensemble):
    """Test parallel calibration"""
    X, y = data

    estimator = make_pipeline(StandardScaler(), LinearSVC(random_state=42))

    cal_clf_parallel = CalibratedClassifierCV(
        estimator, method=method, n_jobs=2, ensemble=ensemble
    )
    cal_clf_parallel.fit(X_train, y_train)
    probs_parallel = cal_clf_parallel.predict_proba(X_test)

    cal_clf_sequential = CalibratedClassifierCV(
        estimator, method=method, n_jobs=1, ensemble=ensemble
    )
    cal_clf_sequential.fit(X_train, y_train)
    probs_sequential = cal_clf_sequential.predict_proba(X_test)

    assert_allclose(probs_parallel, probs_sequential)

def test_calibration_multiclass(method, ensemble, seed):
    def multiclass_brier(y_true, proba_pred, n_classes):
        Y_onehot = np.eye(n_classes)[y_true]
        return np.sum((Y_onehot - proba_pred) ** 2) / Y_onehot.shape[0]

    # Test calibration for multiclass with classifier that implements
    # only decision function.
    clf = LinearSVC(random_state=7)
        n_samples=500, n_features=100, random_state=seed, centers=10, cluster_std=15.0
    )

    # Use an unbalanced dataset by collapsing 8 clusters into one class
    # to make the naive calibration based on a softmax more unlikely
    # to work.
    y[y > 2] = 2
    n_classes = np.unique(y).shape[0]
    X_train, y_train = X[::2], y[::2]
    X_test, y_test = X[1::2], y[1::2]

    cal_clf = CalibratedClassifierCV(clf, method=method, cv=5, ensemble=ensemble)
    cal_clf.fit(X_train, y_train)
    probas = cal_clf.predict_proba(X_test)
    # Check probabilities sum to 1
    assert_allclose(np.sum(probas, axis=1), np.ones(len(X_test)))

    # Check that the dataset is not too trivial, otherwise it's hard
    # to get interesting calibration data during the internal
    # cross-validation loop.
    assert 0.65 < clf.score(X_test, y_test) < 0.95

    # Check that the accuracy of the calibrated model is never degraded
    # too much compared to the original classifier.
    assert cal_clf.score(X_test, y_test) > 0.95 * clf.score(X_test, y_test)

    # Check that Brier loss of calibrated classifier is smaller than
    # loss obtained by naively turning OvR decision function to
    # probabilities via a softmax
    uncalibrated_brier = multiclass_brier(
        y_test, softmax(clf.decision_function(X_test)), n_classes=n_classes
    )
    calibrated_brier = multiclass_brier(y_test, probas, n_classes=n_classes)

    assert calibrated_brier < 1.1 * uncalibrated_brier

    # Test that calibration of a multiclass classifier decreases log-loss
    # for RandomForestClassifier
    clf = RandomForestClassifier(n_estimators=30, random_state=42)
    clf_probs = clf.predict_proba(X_test)
    uncalibrated_brier = multiclass_brier(y_test, clf_probs, n_classes=n_classes)

    cal_clf_probs = cal_clf.predict_proba(X_test)
    calibrated_brier = multiclass_brier(y_test, cal_clf_probs, n_classes=n_classes)

def test_calibration_zero_probability():
    # Test an edge case where _CalibratedClassifier avoids numerical errors
    # in the multiclass normalization step if all the calibrators output
    # are zero all at once for a given sample and instead fallback to uniform
    # probabilities.
    class ZeroCalibrator:
        # This function is called from _CalibratedClassifier.predict_proba.

        n_samples=50, n_features=10, random_state=7, centers=10, cluster_std=15.0
    )
    clf = DummyClassifier().fit(X, y)
    calibrator = ZeroCalibrator()
    cal_clf = _CalibratedClassifier(
        estimator=clf, calibrators=[calibrator], classes=clf.classes_
    )

    probas = cal_clf.predict_proba(X)

    # Check that all probabilities are uniformly 1. / clf.n_classes_
    assert_allclose(probas, 1.0 / clf.n_classes_)

def test_calibration_prefit(csr_container):
    """Test calibration for prefitted classifiers"""
    # TODO(1.8): Remove cv="prefit" options here and the @ignore_warnings of the test
    X, y = make_classification(n_samples=3 * n_samples, n_features=6, random_state=42)

    X_calib, y_calib, sw_calib = (
        X[n_samples : 2 * n_samples],
        y[n_samples : 2 * n_samples],
        sample_weight[n_samples : 2 * n_samples],
    )
    X_test, y_test = X[2 * n_samples :], y[2 * n_samples :]

    # Check error if clf not prefit
    unfit_clf = CalibratedClassifierCV(clf, cv="prefit")
        unfit_clf.fit(X_calib, y_calib)

    clf.fit(X_train, y_train, sw_train)

    for this_X_calib, this_X_test in [
        (X_calib, X_test),
        (csr_container(X_calib), csr_container(X_test)),
    ]:
        for method in ["isotonic", "sigmoid"]:
            cal_clf_prefit = CalibratedClassifierCV(clf, method=method, cv="prefit")
            cal_clf_frozen = CalibratedClassifierCV(FrozenEstimator(clf), method=method)

            for sw in [sw_calib, None]:
                cal_clf_prefit.fit(this_X_calib, y_calib, sample_weight=sw)
                cal_clf_frozen.fit(this_X_calib, y_calib, sample_weight=sw)

                y_prob_prefit = cal_clf_prefit.predict_proba(this_X_test)
                y_prob_frozen = cal_clf_frozen.predict_proba(this_X_test)
                y_pred_prefit = cal_clf_prefit.predict(this_X_test)
                y_pred_frozen = cal_clf_frozen.predict(this_X_test)
                prob_pos_cal_clf_prefit = y_prob_prefit[:, 1]
                prob_pos_cal_clf_frozen = y_prob_frozen[:, 1]
                assert_array_equal(y_pred_prefit, y_pred_frozen)
                assert_array_equal(
                    y_pred_prefit, np.array([0, 1])[np.argmax(y_prob_prefit, axis=1)]
                )
                assert brier_score_loss(y_test, prob_pos_clf) > brier_score_loss(
                    y_test, prob_pos_cal_clf_frozen
                )

def test_calibration_ensemble_false(data, method):
    # Test that `ensemble=False` is the same as using predictions from
    # `cross_val_predict` to train calibrator.
    X, y = data

    cal_clf = CalibratedClassifierCV(clf, method=method, cv=3, ensemble=False)
    cal_clf.fit(X, y)
    cal_probas = cal_clf.predict_proba(X)

    # Get probas manually
    unbiased_preds = cross_val_predict(clf, X, y, cv=3, method="decision_function")
    if method == "isotonic":
        calibrator = IsotonicRegression(out_of_bounds="clip")
    else:
        calibrator = _SigmoidCalibration()
    calibrator.fit(unbiased_preds, y)
    # Use `clf` fit on all data
    clf_df = clf.decision_function(X)
    manual_probas = calibrator.predict(clf_df)
    assert_allclose(cal_probas[:, 1], manual_probas)

def test_sigmoid_calibration():
    """Test calibration values with Platt sigmoid model"""
    exF = np.array([5, -4, 1.0])
    exY = np.array([1, -1, -1])
    # computed from my python port of the C++ code in LibSVM
    AB_lin_libsvm = np.array([-0.20261354391187855, 0.65236314980010512])
    assert_array_almost_equal(AB_lin_libsvm, _sigmoid_calibration(exF, exY), 3)
    lin_prob = 1.0 / (1.0 + np.exp(AB_lin_libsvm[0] * exF + AB_lin_libsvm[1]))
    sk_prob = _SigmoidCalibration().fit(exF, exY).predict(exF)
    assert_array_almost_equal(lin_prob, sk_prob, 6)

    # check that _SigmoidCalibration().fit only accepts 1d array or 2d column
    # arrays
        _SigmoidCalibration().fit(np.vstack((exF, exF)), exY)

def test_calibration_curve():
    """Check calibration_curve function"""
    y_true = np.array([0, 0, 0, 1, 1, 1])
    y_pred = np.array([0.0, 0.1, 0.2, 0.8, 0.9, 1.0])
    prob_true, prob_pred = calibration_curve(y_true, y_pred, n_bins=2)
    assert len(prob_true) == len(prob_pred)
    assert len(prob_true) == 2
    assert_almost_equal(prob_true, [0, 1])
    assert_almost_equal(prob_pred, [0.1, 0.9])

    # Probabilities outside [0, 1] should not be accepted at all.
        calibration_curve([1], [-0.1])

    # test that quantiles work as expected
    y_true2 = np.array([0, 0, 0, 0, 1, 1])
    y_pred2 = np.array([0.0, 0.1, 0.2, 0.5, 0.9, 1.0])
    prob_true_quantile, prob_pred_quantile = calibration_curve(
        y_true2, y_pred2, n_bins=2, strategy="quantile"
    )

    assert len(prob_true_quantile) == len(prob_pred_quantile)
    assert len(prob_true_quantile) == 2
    assert_almost_equal(prob_true_quantile, [0, 2 / 3])
    assert_almost_equal(prob_pred_quantile, [0.1, 0.8])

    # Check that error is raised when invalid strategy is selected
        calibration_curve(y_true2, y_pred2, strategy="percentile")

def test_calibration_nan_imputer(ensemble):
    """Test that calibration can accept nan"""
        n_samples=10, n_features=2, n_informative=2, n_redundant=0, random_state=42
    )
    clf = Pipeline(
        [("imputer", SimpleImputer()), ("rf", RandomForestClassifier(n_estimators=1))]
    )
    clf_c = CalibratedClassifierCV(clf, cv=2, method="isotonic", ensemble=ensemble)
    clf_c.fit(X, y)
    clf_c.predict(X)

def test_calibration_prob_sum(ensemble):
    # Test that sum of probabilities is (max) 1. A non-regression test for
    # issue #7796 - when test has fewer classes than train
    X, _ = make_classification(n_samples=10, n_features=5, n_classes=2)
    y = [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
    clf = LinearSVC(C=1.0, random_state=7)
    # In the first and last fold, test will have 1 class while train will have 2
    clf_prob = CalibratedClassifierCV(
        clf, method="sigmoid", cv=KFold(n_splits=3), ensemble=ensemble
    )
    clf_prob.fit(X, y)
    assert_allclose(clf_prob.predict_proba(X).sum(axis=1), 1.0)

def test_calibration_less_classes(ensemble):
    # Test to check calibration works fine when train set in a test-train
    # split does not contain all classes
    # In 1st split, train is missing class 0
    # In 3rd split, train is missing class 3
    X = np.random.randn(12, 5)
    y = [0, 0, 0, 1] + [1, 1, 2, 2] + [2, 3, 3, 3]
    clf = DecisionTreeClassifier(random_state=7)
    cal_clf = CalibratedClassifierCV(
        clf, method="sigmoid", cv=KFold(3), ensemble=ensemble
    )

    if ensemble:
        classes = np.arange(4)
        for calib_i, class_i in zip([0, 2], [0, 3]):
            proba = cal_clf.calibrated_classifiers_[calib_i].predict_proba(X)
            # Check that the unobserved class has proba=0
            assert_array_equal(proba[:, class_i], np.zeros(len(y)))
            # Check for all other classes proba>0
            assert np.all(proba[:, classes != class_i] > 0)

    # When `ensemble=False`, `cross_val_predict` is used to compute predictions
    # to fit only one `calibrated_classifiers_`
    else:
        proba = cal_clf.calibrated_classifiers_[0].predict_proba(X)
        assert_array_almost_equal(proba.sum(axis=1), np.ones(proba.shape[0]))

def test_calibration_accepts_ndarray(X):
    """Test that calibration accepts n-dimensional arrays as input"""
    y = [1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0]

    class MockTensorClassifier(ClassifierMixin, BaseEstimator):
        """A toy estimator that accepts tensor inputs"""

            # toy decision function that just needs to have the right shape:
            return X.reshape(X.shape[0], -1).sum(axis=1)

    calibrated_clf = CalibratedClassifierCV(MockTensorClassifier())
    # we should be able to fit this classifier with no error
    calibrated_clf.fit(X, y)

def dict_data():
    dict_data = [
        {"state": "NY", "age": "adult"},
        {"state": "TX", "age": "adult"},
        {"state": "VT", "age": "child"},
        {"state": "CT", "age": "adult"},
        {"state": "BR", "age": "child"},
    ]
    text_labels = [1, 0, 1, 1, 0]
    return dict_data, text_labels

def dict_data_pipeline(dict_data):
    X, y = dict_data
    pipeline_prefit = Pipeline(
        [("vectorizer", DictVectorizer()), ("clf", RandomForestClassifier())]
    )
    return pipeline_prefit.fit(X, y)

def test_calibration_dict_pipeline(dict_data, dict_data_pipeline):
    """Test that calibration works in prefit pipeline with transformer

    `X` is not array-like, sparse matrix or dataframe at the start.
    See https://github.com/scikit-learn/scikit-learn/issues/8710

    Also test it can predict without running into validation errors.
    See https://github.com/scikit-learn/scikit-learn/issues/19637
    """
    clf = dict_data_pipeline
    calib_clf = CalibratedClassifierCV(FrozenEstimator(clf), cv=2)
    # Check attributes are obtained from fitted estimator
    assert_array_equal(calib_clf.classes_, clf.classes_)

    # Neither the pipeline nor the calibration meta-estimator
    # expose the n_features_in_ check on this kind of data.
    assert not hasattr(clf, "n_features_in_")
    assert not hasattr(calib_clf, "n_features_in_")

    # Ensure that no error is thrown with predict and predict_proba
    calib_clf.predict(X)
    calib_clf.predict_proba(X)

def test_calibration_attributes(clf, cv):
    # Check that `n_features_in_` and `classes_` attributes created properly
    X, y = make_classification(n_samples=10, n_features=5, n_classes=2, random_state=7)
    if cv == "prefit":
        clf = clf.fit(X, y)
        calib_clf = CalibratedClassifierCV(clf, cv=cv)
            calib_clf.fit(X, y)
    else:

        assert_array_equal(calib_clf.classes_, clf.classes_)
        assert calib_clf.n_features_in_ == clf.n_features_in_
    else:
        classes = LabelEncoder().fit(y).classes_
        assert_array_equal(calib_clf.classes_, classes)
        assert calib_clf.n_features_in_ == X.shape[1]

def test_calibration_inconsistent_prefit_n_features_in():
    # Check that `n_features_in_` from prefit base estimator
    # is consistent with training set
    clf = LinearSVC(C=1).fit(X, y)
    calib_clf = CalibratedClassifierCV(FrozenEstimator(clf))

    msg = "X has 3 features, but LinearSVC is expecting 5 features as input."
        calib_clf.fit(X[:, :3], y)

def test_calibration_votingclassifier():
    # Check that `CalibratedClassifier` works with `VotingClassifier`.
    # The method `predict_proba` from `VotingClassifier` is dynamically
    # defined via a property that only works when voting="soft".
    vote = VotingClassifier(
        estimators=[("lr" + str(i), LogisticRegression()) for i in range(3)],
    )
    vote.fit(X, y)

    calib_clf = CalibratedClassifierCV(estimator=FrozenEstimator(vote))
    # smoke test: should not raise an error

def iris_data_binary(iris_data):
    X, y = iris_data

def test_calibration_display_compute(pyplot, iris_data_binary, n_bins, strategy):
    # Ensure `CalibrationDisplay.from_predictions` and `calibration_curve`
    # compute the same results. Also checks attributes of the
    # CalibrationDisplay object.
    X, y = iris_data_binary

    viz = CalibrationDisplay.from_estimator(
        lr, X, y, n_bins=n_bins, strategy=strategy, alpha=0.8
    )

    y_prob = lr.predict_proba(X)[:, 1]
    prob_true, prob_pred = calibration_curve(
        y, y_prob, n_bins=n_bins, strategy=strategy
    )

    assert_allclose(viz.prob_true, prob_true)
    assert_allclose(viz.prob_pred, prob_pred)
    assert_allclose(viz.y_prob, y_prob)

    assert viz.estimator_name == "LogisticRegression"

    assert isinstance(viz.line_, mpl.lines.Line2D)
    assert viz.line_.get_alpha() == 0.8
    assert isinstance(viz.ax_, mpl.axes.Axes)
    assert isinstance(viz.figure_, mpl.figure.Figure)

    assert viz.ax_.get_xlabel() == "Mean predicted probability (Positive class: 1)"
    assert viz.ax_.get_ylabel() == "Fraction of positives (Positive class: 1)"

    expected_legend_labels = ["LogisticRegression", "Perfectly calibrated"]
    legend_labels = viz.ax_.get_legend().get_texts()
    assert len(legend_labels) == len(expected_legend_labels)
    for labels in legend_labels:
        assert labels.get_text() in expected_legend_labels

def test_plot_calibration_curve_pipeline(pyplot, iris_data_binary):
    # Ensure pipelines are supported by CalibrationDisplay.from_estimator
    clf = make_pipeline(StandardScaler(), LogisticRegression())
    viz = CalibrationDisplay.from_estimator(clf, X, y)

    expected_legend_labels = [viz.estimator_name, "Perfectly calibrated"]

def test_calibration_display_default_labels(pyplot, name, expected_label):
    prob_true = np.array([0, 1, 1, 0])
    prob_pred = np.array([0.2, 0.8, 0.8, 0.4])
    y_prob = np.array([])

    viz = CalibrationDisplay(prob_true, prob_pred, y_prob, estimator_name=name)
    viz.plot()

    expected_legend_labels = [] if name is None else [name]
    expected_legend_labels.append("Perfectly calibrated")

def test_calibration_display_label_class_plot(pyplot):
    # Checks that when instantiating `CalibrationDisplay` class then calling
    # `plot`, `self.estimator_name` is the one given in `plot`

    name = "name one"
    assert viz.estimator_name == name
    name = "name two"
    viz.plot(name=name)

    expected_legend_labels = [name, "Perfectly calibrated"]

def test_calibration_display_name_multiple_calls(
    constructor_name, pyplot, iris_data_binary
):
    # Check that the `name` used when calling
    # `CalibrationDisplay.from_predictions` or
    # `CalibrationDisplay.from_estimator` is used when multiple
    # `CalibrationDisplay.viz.plot()` calls are made.
    y_prob = clf.predict_proba(X)[:, 1]

    constructor = getattr(CalibrationDisplay, constructor_name)
    params = (clf, X, y) if constructor_name == "from_estimator" else (y, y_prob)

    viz = constructor(*params, name=clf_name)
    assert viz.estimator_name == clf_name

    expected_legend_labels = [clf_name, "Perfectly calibrated"]

    viz.plot(name=clf_name)

def test_calibration_display_ref_line(pyplot, iris_data_binary):
    # Check that `ref_line` only appears once
    dt = DecisionTreeClassifier().fit(X, y)

    viz = CalibrationDisplay.from_estimator(lr, X, y)
    viz2 = CalibrationDisplay.from_estimator(dt, X, y, ax=viz.ax_)

    labels = viz2.ax_.get_legend_handles_labels()[1]
    assert labels.count("Perfectly calibrated") == 1

def test_calibration_curve_pos_label_error_str(dtype_y_str):
    """Check error message when a `pos_label` is not specified with `str` targets."""

    err_msg = (
        "pass pos_label explicitly"
    )
        calibration_curve(y1, y2)

def test_calibration_curve_pos_label(dtype_y_str):
    """Check the behaviour when passing explicitly `pos_label`."""
    y_true = np.array([0, 0, 0, 1, 1, 1, 1, 1, 1])
    classes = np.array(["spam", "egg"], dtype=dtype_y_str)
    y_true_str = classes[y_true]
    y_pred = np.array([0.1, 0.2, 0.3, 0.4, 0.65, 0.7, 0.8, 0.9, 1.0])

    # default case
    prob_true, _ = calibration_curve(y_true, y_pred, n_bins=4)
    assert_allclose(prob_true, [0, 0.5, 1, 1])
    # if `y_true` contains `str`, then `pos_label` is required
    prob_true, _ = calibration_curve(y_true_str, y_pred, n_bins=4, pos_label="egg")

    prob_true, _ = calibration_curve(y_true, 1 - y_pred, n_bins=4, pos_label=0)
    assert_allclose(prob_true, [0, 0, 0.5, 1])
    prob_true, _ = calibration_curve(y_true_str, 1 - y_pred, n_bins=4, pos_label="spam")

def test_calibration_display_kwargs(pyplot, iris_data_binary, kwargs):
    """Check that matplotlib aliases are handled."""

    viz = CalibrationDisplay.from_estimator(lr, X, y, **kwargs)

    assert viz.line_.get_color() == "red"
    assert viz.line_.get_linewidth() == 2
    assert viz.line_.get_linestyle() == "-."

def test_calibration_display_pos_label(
    pyplot, iris_data_binary, pos_label, expected_pos_label
):
    """Check the behaviour of `pos_label` in the `CalibrationDisplay`."""

    viz = CalibrationDisplay.from_estimator(lr, X, y, pos_label=pos_label)

    y_prob = lr.predict_proba(X)[:, expected_pos_label]
    prob_true, prob_pred = calibration_curve(y, y_prob, pos_label=pos_label)

    assert (
        viz.ax_.get_xlabel()
        == f"Mean predicted probability (Positive class: {expected_pos_label})"
    )
    assert (
        viz.ax_.get_ylabel()
        == f"Fraction of positives (Positive class: {expected_pos_label})"
    )

    expected_legend_labels = [lr.__class__.__name__, "Perfectly calibrated"]

def test_calibrated_classifier_cv_double_sample_weights_equivalence(method, ensemble):
    """Check that passing repeating twice the dataset `X` is equivalent to
    passing a `sample_weight` with a factor 2."""
    # Scale the data to avoid any convergence issue
    # Only use 2 classes
    X, y = X[:100], y[:100]
    sample_weight = np.ones_like(y) * 2

    # Interlace the data such that a 2-fold cross-validation will be equivalent
    # to using the original dataset with a sample weights of 2
    X_twice = np.zeros((X.shape[0] * 2, X.shape[1]), dtype=X.dtype)
    X_twice[::2, :] = X
    X_twice[1::2, :] = X
    y_twice = np.zeros(y.shape[0] * 2, dtype=y.dtype)
    y_twice[::2] = y
    y_twice[1::2] = y

    calibrated_clf_without_weights = CalibratedClassifierCV(
        ensemble=ensemble,
        cv=2,
    )
    calibrated_clf_with_weights = clone(calibrated_clf_without_weights)

    calibrated_clf_with_weights.fit(X, y, sample_weight=sample_weight)
    calibrated_clf_without_weights.fit(X_twice, y_twice)

    # Check that the underlying fitted estimators have the same coefficients
    for est_with_weights, est_without_weights in zip(
        calibrated_clf_with_weights.calibrated_classifiers_,
        calibrated_clf_without_weights.calibrated_classifiers_,
    ):
            est_with_weights.estimator.coef_,
            est_without_weights.estimator.coef_,
        )

    # Check that the predictions are the same
    y_pred_with_weights = calibrated_clf_with_weights.predict_proba(X)
    y_pred_without_weights = calibrated_clf_without_weights.predict_proba(X)

    assert_allclose(y_pred_with_weights, y_pred_without_weights)

def test_calibration_with_fit_params(fit_params_type, data):
    """Tests that fit_params are passed to the underlying base estimator.

    https://github.com/scikit-learn/scikit-learn/issues/12384
    """
    X, y = data
    fit_params = {
        "a": _convert_container(y, fit_params_type),
        "b": _convert_container(y, fit_params_type),
    }

    clf = CheckingClassifier(expected_fit_params=["a", "b"])
    pc_clf = CalibratedClassifierCV(clf)

    pc_clf.fit(X, y, **fit_params)

def test_calibration_with_sample_weight_estimator(sample_weight, data):
    """Tests that sample_weight is passed to the underlying base
    estimator.
    """
    X, y = data
    clf = CheckingClassifier(expected_sample_weight=True)

    pc_clf.fit(X, y, sample_weight=sample_weight)

def test_calibration_without_sample_weight_estimator(data):
    """Check that even if the estimator doesn't support
    sample_weight, fitting with sample_weight still works.

    There should be a warning, since the sample_weight is not passed
    on to the estimator.
    """
    X, y = data

    class ClfWithoutSampleWeight(CheckingClassifier):
            assert "sample_weight" not in fit_params
            return super().fit(X, y, **fit_params)

    clf = ClfWithoutSampleWeight()

        pc_clf.fit(X, y, sample_weight=sample_weight)

def test_calibration_with_non_sample_aligned_fit_param(data):
    """Check that CalibratedClassifierCV does not enforce sample alignment
    for fit parameters."""

    class TestClassifier(LogisticRegression):
        def fit(self, X, y, sample_weight=None, fit_param=None):
            assert fit_param is not None
            return super().fit(X, y, sample_weight=sample_weight)

    CalibratedClassifierCV(estimator=TestClassifier()).fit(
        *data, fit_param=np.ones(len(data[1]) + 1)
    )

def test_calibrated_classifier_cv_works_with_large_confidence_scores(
):
    """Test that :class:`CalibratedClassifierCV` works with large confidence
    scores when using the `sigmoid` method, particularly with the
    :class:`SGDClassifier`.

    Non-regression test for issue #26766.
    """
    prob = 0.67
    n = 1000
    random_noise = np.random.default_rng(global_random_seed).normal(size=n)

    y = np.array([1] * int(n * prob) + [0] * (n - int(n * prob)))
    X = 1e5 * y.reshape((-1, 1)) + random_noise

    # Check that the decision function of SGDClassifier produces predicted
    # values that are quite large, for the data under consideration.
    cv = check_cv(cv=None, y=y, classifier=True)
    indices = cv.split(X, y)
    for train, test in indices:
        X_train, y_train = X[train], y[train]
        X_test = X[test]
        sgd_clf = SGDClassifier(loss="squared_hinge", random_state=global_random_seed)
        sgd_clf.fit(X_train, y_train)
        predictions = sgd_clf.decision_function(X_test)
        assert (predictions > 1e4).any()

    # Compare the CalibratedClassifierCV using the sigmoid method with the
    # CalibratedClassifierCV using the isotonic method. The isotonic method
    # is used for comparison because it is numerically stable.
    clf_sigmoid = CalibratedClassifierCV(
        SGDClassifier(loss="squared_hinge", random_state=global_random_seed),
    )
    score_sigmoid = cross_val_score(clf_sigmoid, X, y, scoring="roc_auc")

    # The isotonic method is used for comparison because it is numerically
    # stable.
    clf_isotonic = CalibratedClassifierCV(
        method="isotonic",
    )
    score_isotonic = cross_val_score(clf_isotonic, X, y, scoring="roc_auc")

    # The AUC score should be the same because it is invariant under
    # strictly monotonic conditions
    assert_allclose(score_sigmoid, score_isotonic)

def test_sigmoid_calibration_max_abs_prediction_threshold(global_random_seed):
    n = 100
    y = random_state.randint(0, 2, size=n)

    # Check that for small enough predictions ranging from -2 to 2, the
    # threshold value has no impact on the outcome
    predictions_small = random_state.uniform(low=-2, high=2, size=100)

    # Using a threshold lower than the maximum absolute value of the
    # predictions enables internal re-scaling by max(abs(predictions_small)).
    threshold_1 = 0.1
    a1, b1 = _sigmoid_calibration(
        predictions=predictions_small,
        y=y,
        max_abs_prediction_threshold=threshold_1,
    )

    # Using a larger threshold disables rescaling.
    threshold_2 = 10
    a2, b2 = _sigmoid_calibration(
        y=y,
        max_abs_prediction_threshold=threshold_2,
    )

    # Using default threshold of 30 also disables the scaling.
    a3, b3 = _sigmoid_calibration(
        y=y,
    )

    # Depends on the tolerance of the underlying quasy-newton solver which is
    # not too strict by default.
    atol = 1e-6
    assert_allclose(a1, a2, atol=atol)
    assert_allclose(a2, a3, atol=atol)
    assert_allclose(b1, b2, atol=atol)
    assert_allclose(b2, b3, atol=atol)

def test_float32_predict_proba(data, use_sample_weight, method):
    """Check that CalibratedClassifierCV works with float32 predict proba.

    Non-regression test for gh-28245 and gh-28247.
    """
    if use_sample_weight:
        # Use dtype=np.float64 to check that this does not trigger an
        # unintentional upcasting: the dtype of the base estimator should
        # control the dtype of the final model. In particular, the
        # sigmoid calibrator relies on inputs (predictions and sample weights)
        # with consistent dtypes because it is partially written in Cython.
        # As this test forces the predictions to be `float32`, we want to check
        # that `CalibratedClassifierCV` internally converts `sample_weight` to
        # the same dtype to avoid crashing the Cython call.
        sample_weight = np.ones_like(data[1], dtype=np.float64)
    else:

    class DummyClassifer32(DummyClassifier):
            return super().predict_proba(X).astype(np.float32)

    model = DummyClassifer32()
    calibrator = CalibratedClassifierCV(model, method=method)
    # Does not raise an error.
    calibrator.fit(*data, sample_weight=sample_weight)

    # Check with frozen prefit model
    model = DummyClassifer32().fit(*data, sample_weight=sample_weight)
    calibrator = CalibratedClassifierCV(FrozenEstimator(model), method=method)

    # TODO(1.8): remove me once the deprecation period is over.
    # Check with prefit model using the deprecated cv="prefit" argument:
    calibrator = CalibratedClassifierCV(model, method=method, cv="prefit")
        calibrator.fit(*data, sample_weight=sample_weight)

def test_error_less_class_samples_than_folds():
    """Check that CalibratedClassifierCV works with string targets.

    non-regression test for issue #28841.
    """
    X = np.random.normal(size=(20, 3))
    y = ["a"] * 10 + ["b"] * 10

    CalibratedClassifierCV(cv=3).fit(X, y)

def _get_rescaled_operator(X, X_offset, sample_weight_sqrt):
    """Create LinearOperator for matrix products with implicit centering.

    Matrix product `LinearOperator @ coef` returns `(X - X_offset) @ coef`.
    """

    def matvec(b):
        return X.dot(b) - sample_weight_sqrt * b.dot(X_offset)

    def rmatvec(b):
        return X.T.dot(b) - X_offset * b.dot(sample_weight_sqrt)

    X1 = sparse.linalg.LinearOperator(shape=X.shape, matvec=matvec, rmatvec=rmatvec)
    return X1

def _solve_sparse_cg(
    X,
    y,
    alpha,
    max_iter=None,
    tol=1e-4,
    X_offset=None,
    X_scale=None,
    sample_weight_sqrt=None,
):
    if sample_weight_sqrt is None:
        sample_weight_sqrt = np.ones(X.shape[0], dtype=X.dtype)

    if X_offset is None or X_scale is None:
        X1 = sp_linalg.aslinearoperator(X)
    else:
        X_offset_scale = X_offset / X_scale
        X1 = _get_rescaled_operator(X, X_offset_scale, sample_weight_sqrt)

    coefs = np.empty((y.shape[1], n_features), dtype=X.dtype)

    if n_features > n_samples:

        def create_mv(curr_alpha):
            def _mv(x):
                return X1.matvec(X1.rmatvec(x)) + curr_alpha * x

            return _mv

    else:

                return X1.rmatvec(X1.matvec(x)) + curr_alpha * x

            return _mv

    for i in range(y.shape[1]):
        y_column = y[:, i]

        mv = create_mv(alpha[i])
        if n_features > n_samples:
            # kernel ridge
            # w = X.T * inv(X X^t + alpha*Id) y
            C = sp_linalg.LinearOperator(
                (n_samples, n_samples), matvec=mv, dtype=X.dtype
            )
            coef, info = _sparse_linalg_cg(C, y_column, rtol=tol)
            coefs[i] = X1.rmatvec(coef)
        else:
            # linear ridge
            # w = inv(X^t X + alpha*Id) * X.T y
            y_column = X1.rmatvec(y_column)
                (n_features, n_features), matvec=mv, dtype=X.dtype
            )
            coefs[i], info = _sparse_linalg_cg(C, y_column, maxiter=max_iter, rtol=tol)

        if info < 0:
            raise ValueError("Failed with error code %d" % info)

        if max_iter is None and info > 0 and verbose:
                "sparse_cg did not converge after %d iterations." % info,
            )

    return coefs

def _solve_lsqr(
    X,
    y,
    *,
    alpha,
    fit_intercept=True,
    tol=1e-4,
):
    """Solve Ridge regression via LSQR.

    We expect that y is always mean centered.
    If X is dense, we expect it to be mean centered such that we can solve
        ||y - Xw||_2^2 + alpha * ||w||_2^2

    If X is sparse, we expect X_offset to be given such that we can solve
        ||y - (X - X_offset)w||_2^2 + alpha * ||w||_2^2

    With sample weights S=diag(sample_weight), this becomes
        ||sqrt(S) (y - (X - X_offset) w)||_2^2 + alpha * ||w||_2^2
    and we expect y and X to already be rescaled, i.e. sqrt(S) @ y, sqrt(S) @ X. In
    this case, X_offset is the sample_weight weighted mean of X before scaling by
    sqrt(S). The objective then reads
       ||y - (X - sqrt(S) X_offset) w)||_2^2 + alpha * ||w||_2^2
    """

    if sparse.issparse(X) and fit_intercept:
    else:
        # No need to touch anything
        X1 = X

    n_iter = np.empty(y.shape[1], dtype=np.int32)

    # According to the lsqr documentation, alpha = damp^2.
    sqrt_alpha = np.sqrt(alpha)

        info = sp_linalg.lsqr(
            X1, y_column, damp=sqrt_alpha[i], atol=tol, btol=tol, iter_lim=max_iter
        )
        coefs[i] = info[0]
        n_iter[i] = info[2]

    return coefs, n_iter

def _solve_cholesky(X, y, alpha):
    # w = inv(X^t X + alpha*Id) * X.T y
    n_targets = y.shape[1]

    A = safe_sparse_dot(X.T, X, dense_output=True)
    Xy = safe_sparse_dot(X.T, y, dense_output=True)

    one_alpha = np.array_equal(alpha, len(alpha) * [alpha[0]])

    if one_alpha:
        A.flat[:: n_features + 1] += alpha[0]
        return linalg.solve(A, Xy, assume_a="pos", overwrite_a=True).T
    else:
        coefs = np.empty([n_targets, n_features], dtype=X.dtype)
        for coef, target, current_alpha in zip(coefs, Xy.T, alpha):
            A.flat[:: n_features + 1] += current_alpha
            coef[:] = linalg.solve(A, target, assume_a="pos", overwrite_a=False).ravel()
            A.flat[:: n_features + 1] -= current_alpha
        return coefs

def _solve_cholesky_kernel(K, y, alpha, sample_weight=None, copy=False):
    # dual_coef = inv(X X^t + alpha*Id) y
    n_samples = K.shape[0]

    if copy:
        K = K.copy()

    alpha = np.atleast_1d(alpha)
    one_alpha = (alpha == alpha[0]).all()
    has_sw = isinstance(sample_weight, np.ndarray) or sample_weight not in [1.0, None]

    if has_sw:
        # Unlike other solvers, we need to support sample_weight directly
        # because K might be a pre-computed kernel.
        sw = np.sqrt(np.atleast_1d(sample_weight))
        y = y * sw[:, np.newaxis]
        K *= np.outer(sw, sw)

        # Only one penalty, we can solve multi-target problems in one time.
        K.flat[:: n_samples + 1] += alpha[0]

        try:
            # Note: we must use overwrite_a=False in order to be able to
            #       use the fall-back solution below in case a LinAlgError
            #       is raised
            dual_coef = linalg.solve(K, y, assume_a="pos", overwrite_a=False)
                "Singular matrix in solving dual problem. Using "
                "least-squares solution instead."
            )
            dual_coef = linalg.lstsq(K, y)[0]

        # K is expensive to compute and store in memory so change it back in
        # case it was user-given.
        K.flat[:: n_samples + 1] -= alpha[0]

        if has_sw:
            dual_coef *= sw[:, np.newaxis]

        return dual_coef
    else:
        # One penalty per target. We need to solve each target separately.
        dual_coefs = np.empty([n_targets, n_samples], K.dtype)

        for dual_coef, target, current_alpha in zip(dual_coefs, y.T, alpha):
            K.flat[:: n_samples + 1] += current_alpha

            dual_coef[:] = linalg.solve(
                K, target, assume_a="pos", overwrite_a=False
            ).ravel()

            K.flat[:: n_samples + 1] -= current_alpha

        if has_sw:
            dual_coefs *= sw[np.newaxis, :]

        return dual_coefs.T

def _solve_svd(X, y, alpha, xp=None):
    xp, _ = get_namespace(X, xp=xp)
    U, s, Vt = xp.linalg.svd(X, full_matrices=False)
    idx = s > 1e-15  # same default value as scipy.linalg.pinv
    s_nnz = s[idx][:, None]
    UTy = U.T @ y
    d = xp.zeros((s.shape[0], alpha.shape[0]), dtype=X.dtype, device=device(X))
    d[idx] = s_nnz / (s_nnz**2 + alpha)
    d_UT_y = d * UTy
    return (Vt.T @ d_UT_y).T

def _solve_lbfgs(
    X,
    y,
    alpha,
    positive=True,
    tol=1e-4,
):
    """Solve ridge regression with LBFGS.

    The main purpose is fitting with forcing coefficients to be positive.
    For unconstrained ridge regression, there are faster dedicated solver methods.
    Note that with positive bounds on the coefficients, LBFGS seems faster
    than scipy.optimize.lsq_linear.
    """

    options = {}
    if max_iter is not None:
        options["maxiter"] = max_iter
    config = {
        "method": "L-BFGS-B",
        "tol": tol,
        "jac": True,
        "options": options,
    }
        config["bounds"] = [(0, np.inf)] * n_features

    if X_offset is not None and X_scale is not None:
    else:
        X_offset_scale = None

        x0 = np.zeros((n_features,))

        def func(w):
            residual = X.dot(w) - y_column
            if X_offset_scale is not None:
                residual -= sample_weight_sqrt * w.dot(X_offset_scale)
            f = 0.5 * residual.dot(residual) + 0.5 * alpha[i] * w.dot(w)
            grad = X.T @ residual + alpha[i] * w
                grad -= X_offset_scale * residual.dot(sample_weight_sqrt)

            return f, grad

        result = optimize.minimize(func, x0, **config)
        if not result["success"]:
                (
                    "The lbfgs solver did not converge. Try increasing max_iter "
                    f"or tol. Currently: max_iter={max_iter} and tol={tol}"
                ),
            )
        coefs[i] = result["x"]

def _get_valid_accept_sparse(is_X_sparse, solver):
    if is_X_sparse and solver in ["auto", "sag", "saga"]:
        return "csr"
    else:
        return ["csr", "csc", "coo"]

def ridge_regression(
    X,
    y,
    alpha,
    *,
    solver="auto",
    tol=1e-4,
    return_intercept=False,
):
    """Solve the ridge equation by the method of normal equations.

    Read more in the :ref:`User Guide <ridge_regression>`.

    X : {array-like, sparse matrix, LinearOperator} of shape \
        Training data.

    alpha : float or array-like of shape (n_targets,)
        Constant that multiplies the L2 term, controlling regularization
        strength. `alpha` must be a non-negative float i.e. in `[0, inf)`.

        When `alpha = 0`, the objective is equivalent to ordinary least
        squares, solved by the :class:`LinearRegression` object. For numerical
        reasons, using `alpha = 0` with the `Ridge` object is not advised.
        Instead, you should use the :class:`LinearRegression` object.

        If an array is passed, penalties are assumed to be specific to the
        targets. Hence they must correspond in number.

    sample_weight : float or array-like of shape (n_samples,), default=None
        Individual weights for each sample. If given a float, every sample
        will have the same weight. If sample_weight is not None and
        solver='auto', the solver will be set to 'cholesky'.

    solver : {'auto', 'svd', 'cholesky', 'lsqr', 'sparse_cg', \
            'sag', 'saga', 'lbfgs'}, default='auto'
        Solver to use in the computational routines:

        - 'auto' chooses the solver automatically based on the type of data.

        - 'svd' uses a Singular Value Decomposition of X to compute the Ridge
          coefficients. It is the most stable solver, in particular more stable
          for singular matrices than 'cholesky' at the cost of being slower.

        - 'cholesky' uses the standard scipy.linalg.solve function to
          obtain a closed-form solution via a Cholesky decomposition of
          dot(X.T, X)

        - 'sparse_cg' uses the conjugate gradient solver as found in
          scipy.sparse.linalg.cg. As an iterative algorithm, this solver is
          more appropriate than 'cholesky' for large-scale data
          (possibility to set `tol` and `max_iter`).

        - 'lsqr' uses the dedicated regularized least-squares routine
          scipy.sparse.linalg.lsqr. It is the fastest and uses an iterative
          procedure.

        - 'sag' uses a Stochastic Average Gradient descent, and 'saga' uses
          its improved, unbiased version named SAGA. Both methods also use an
          iterative procedure, and are often faster than other solvers when
          both n_samples and n_features are large. Note that 'sag' and
          'saga' fast convergence is only guaranteed on features with
          approximately the same scale. You can preprocess the data with a
          scaler from sklearn.preprocessing.

        - 'lbfgs' uses L-BFGS-B algorithm implemented in
          `scipy.optimize.minimize`. It can be used only when `positive`
          is True.

        All solvers except 'svd' support both dense and sparse data. However, only
        'lsqr', 'sag', 'sparse_cg', and 'lbfgs' support sparse input when
        `fit_intercept` is True.

           Stochastic Average Gradient descent solver.
           SAGA solver.

        Maximum number of iterations for conjugate gradient solver.
        For the 'sparse_cg' and 'lsqr' solvers, the default value is determined
        by scipy.sparse.linalg. For 'sag' and saga solver, the default value is
        1000. For 'lbfgs' solver, the default value is 15000.

        Precision of the solution. Note that `tol` has no effect for solvers 'svd' and
        'cholesky'.

           Default value changed from 1e-3 to 1e-4 for consistency with other linear
           models.

        Verbosity level. Setting verbose > 0 will display additional
        information depending on the solver used.

        When set to ``True``, forces the coefficients to be positive.
        Only 'lbfgs' solver is supported in this case.

        Used when ``solver`` == 'sag' or 'saga' to shuffle the data.

        If True, the method also returns `n_iter`, the actual number of
        iteration performed by the solver.

    return_intercept : bool, default=False
        If True and if X is sparse, the method also returns the intercept,
        and the solver is automatically changed to 'sag'. This is only a
        temporary fix for fitting the intercept with sparse data. For dense
        data, use sklearn.linear_model._preprocess_data before your regression.

        If False, the input arrays X and y will not be checked.

    Returns
    -------
    coef : ndarray of shape (n_features,) or (n_targets, n_features)

    n_iter : int, optional
        The actual number of iteration performed by the solver.
        Only returned if `return_n_iter` is True.

    intercept : float or ndarray of shape (n_targets,)
        The intercept of the model. Only returned if `return_intercept`
        is True and if X is a scipy sparse array.

    Notes
    -----
    This function won't compute the intercept.

    Regularization improves the conditioning of the problem and
    reduces the variance of the estimates. Larger values specify stronger
    regularization. Alpha corresponds to ``1 / (2C)`` in other linear
    models such as :class:`~sklearn.linear_model.LogisticRegression` or
    :class:`~sklearn.svm.LinearSVC`. If an array is passed, penalties are
    assumed to be specific to the targets. Hence they must correspond in
    number.

    Examples
    --------
    >>> from sklearn.linear_model import ridge_regression
    >>> X = rng.randn(100, 4)
    >>> y = 2.0 * X[:, 0] - 1.0 * X[:, 1] + 0.1 * rng.standard_normal(100)
    >>> coef, intercept = ridge_regression(X, y, alpha=1.0, return_intercept=True)
    >>> list(coef)
    [np.float64(1.9...), np.float64(-1.0...), np.float64(-0.0...), np.float64(-0.0...)]
    >>> intercept
    np.float64(-0.0...)
    """
    return _ridge_regression(
        X,
        y,
        alpha,
        tol=tol,
        return_n_iter=return_n_iter,
        return_intercept=return_intercept,
        X_scale=None,
        X_offset=None,
        check_input=check_input,
    )

def _ridge_regression(
    X,
    y,
    alpha,
    tol=1e-4,
    return_solver=False,
    fit_intercept=False,
):
    xp, is_array_api_compliant, device_ = get_namespace_and_device(
        X, y, sample_weight, X_scale, X_offset
    )
    is_numpy_namespace = _is_numpy_namespace(xp)
    X_is_sparse = sparse.issparse(X)

    has_sw = sample_weight is not None

    solver = resolve_solver(solver, positive, return_intercept, X_is_sparse, xp)

    if is_numpy_namespace and not X_is_sparse:

    if not is_numpy_namespace and solver != "svd":
            f"Array API dispatch to namespace {xp.__name__} only supports "
            f"solver 'svd'. Got '{solver}'."
        )

    if positive and solver != "lbfgs":
            "When positive=True, only 'lbfgs' solver can be used. "
            f"Please change solver {solver} to 'lbfgs' "
            "or set positive=False."
        )

    if solver == "lbfgs" and not positive:
            "'lbfgs' solver can be used only when positive=True. "
            "Please use another solver."
        )

    if return_intercept and solver != "sag":
            "In Ridge, only 'sag' solver can directly fit the "
            "intercept. Please change solver to 'sag' or set "
            "return_intercept=False."
        )

        _dtype = [xp.float64, xp.float32]
        _accept_sparse = _get_valid_accept_sparse(X_is_sparse, solver)
        X = check_array(X, accept_sparse=_accept_sparse, dtype=_dtype, order="C")
        y = check_array(y, dtype=X.dtype, ensure_2d=False, order=None)

    if y.ndim > 2:
        raise ValueError("Target y has the wrong shape %s" % str(y.shape))

        y = xp.reshape(y, (-1, 1))

    n_samples_, n_targets = y.shape

    if n_samples != n_samples_:
            "Number of samples in X and y does not correspond: %d != %d"
            % (n_samples, n_samples_)
        )

    if has_sw:

        if solver not in ["sag", "saga"]:
            # SAG supports sample_weight directly. For other solvers,
            # we implement sample_weight via a simple rescaling.
            X, y, sample_weight_sqrt = _rescale_data(X, y, sample_weight)

    # Some callers of this method might pass alpha as single
    # element array which already has been validated.
    if alpha is not None and not isinstance(alpha, type(xp.asarray([0.0]))):
        alpha = check_scalar(
            alpha,
            "alpha",
            min_val=0.0,
            include_boundaries="left",
        )

    # There should be either 1 or n_targets penalties
    alpha = _ravel(xp.asarray(alpha, device=device_, dtype=X.dtype), xp=xp)
    if alpha.shape[0] not in [1, n_targets]:
            "Number of targets and number of penalties do not correspond: %d != %d"
            % (alpha.shape[0], n_targets)
        )

    if alpha.shape[0] == 1 and n_targets > 1:
        alpha = xp.full(
            shape=(n_targets,), fill_value=alpha[0], dtype=alpha.dtype, device=device_
        )

    n_iter = None
    if solver == "sparse_cg":
        coef = _solve_sparse_cg(
            X,
            y,
            alpha,
            tol=tol,
            X_offset=X_offset,
            X_scale=X_scale,
            sample_weight_sqrt=sample_weight_sqrt if has_sw else None,
        )

    elif solver == "lsqr":
        coef, n_iter = _solve_lsqr(
            X,
            y,
            fit_intercept=fit_intercept,
            tol=tol,
        )

    elif solver == "cholesky":
            K = safe_sparse_dot(X, X.T, dense_output=True)
            try:
                dual_coef = _solve_cholesky_kernel(K, y, alpha)

                coef = safe_sparse_dot(X.T, dual_coef, dense_output=True).T
            except linalg.LinAlgError:
                # use SVD solver if matrix is singular
                solver = "svd"
        else:
            try:
                coef = _solve_cholesky(X, y, alpha)

    elif solver in ["sag", "saga"]:
        # precompute max_squared_sum for all targets
        max_squared_sum = row_norms(X, squared=True).max()

        coef = np.empty((y.shape[1], n_features), dtype=X.dtype)
        n_iter = np.empty(y.shape[1], dtype=np.int32)
        intercept = np.zeros((y.shape[1],), dtype=X.dtype)
        for i, (alpha_i, target) in enumerate(zip(alpha, y.T)):
            init = {
                "coef": np.zeros((n_features + int(return_intercept), 1), dtype=X.dtype)
            }
            coef_, n_iter_, _ = sag_solver(
                X,
                target.ravel(),
                "squared",
                alpha_i,
                0,
                max_iter,
                tol,
                verbose,
                False,
                max_squared_sum,
                init,
                is_saga=solver == "saga",
            )
            if return_intercept:
                coef[i] = coef_[:-1]
                intercept[i] = coef_[-1]
            else:
                coef[i] = coef_
            n_iter[i] = n_iter_

        if intercept.shape[0] == 1:
            intercept = intercept[0]

    elif solver == "lbfgs":
        coef = _solve_lbfgs(
            X,
            y,
            alpha,
            tol=tol,
        )

    if solver == "svd":
        if X_is_sparse:
            raise TypeError("SVD solver does not support sparse inputs currently")
        coef = _solve_svd(X, y, alpha, xp)

        coef = _ravel(coef)

    coef = xp.asarray(coef)

    if return_n_iter and return_intercept:
        res = coef, n_iter, intercept
    elif return_intercept:
        res = coef, intercept
    elif return_n_iter:
        res = coef, n_iter
    else:
        res = coef

    return (*res, solver) if return_solver else res

def resolve_solver(solver, positive, return_intercept, is_sparse, xp):
    if solver != "auto":
        return solver

    auto_solver_np = resolve_solver_for_numpy(positive, return_intercept, is_sparse)
    if is_numpy_namespace:
        return auto_solver_np

            "The solvers that support positive fitting do not support "
            f"Array API dispatch to namespace {xp.__name__}. Please "
            "either disable Array API dispatch, or use a numpy-like "
            "namespace, or set `positive=False`."
        )

    # At the moment, Array API dispatch only supports the "svd" solver.
    solver = "svd"
    if solver != auto_solver_np:
            f"Using Array API dispatch to namespace {xp.__name__} with "
            f"`solver='auto'` will result in using the solver '{solver}'. "
            "The results may differ from those when using a Numpy array, "
            f"because in that case the preferred solver would be {auto_solver_np}. "
            f"Set `solver='{solver}'` to suppress this warning."
        )

    return solver

def resolve_solver_for_numpy(positive, return_intercept, is_sparse):
        return "lbfgs"

    if return_intercept:
        # sag supports fitting intercept directly
        return "sag"

    if not is_sparse:
        return "cholesky"

    return "sparse_cg"

class _BaseRidge(LinearModel, metaclass=ABCMeta):
        "solver": [
                {"auto", "svd", "cholesky", "lsqr", "sparse_cg", "sag", "saga", "lbfgs"}
            )
        ],
        "positive": ["boolean"],
    }

        self,
        *,
        tol=1e-4,
        solver="auto",
        positive=False,
    ):
        self.positive = positive

        xp, is_array_api_compliant = get_namespace(X, y, sample_weight)

        if self.solver == "lbfgs" and not self.positive:
                "'lbfgs' solver can be used only when positive=True. "
                "Please use another solver."
            )

        if self.positive:
            if self.solver not in ["auto", "lbfgs"]:
                    f"solver='{self.solver}' does not support positive fitting. Please"
                    " set the solver to 'auto' or 'lbfgs', or set `positive=False`"
                )
            else:
                solver = self.solver
        elif sparse.issparse(X) and self.fit_intercept:
            if self.solver not in ["auto", "lbfgs", "lsqr", "sag", "sparse_cg"]:
                    "solver='{}' does not support fitting the intercept "
                    "on sparse data. Please set the solver to 'auto' or "
                    "'lsqr', 'sparse_cg', 'sag', 'lbfgs' "
                    "or set `fit_intercept=False`".format(self.solver)
                )
            if self.solver in ["lsqr", "lbfgs"]:
            elif self.solver == "sag" and self.max_iter is None and self.tol > 1e-4:
                    '"sag" solver requires many iterations to fit '
                    "an intercept with sparse inputs. Either set the "
                    'solver to "auto" or "sparse_cg", or set a low '
                    '"tol" and a high "max_iter" (especially if inputs are '
                    "not standardized)."
                )
                solver = "sag"
            else:
                solver = "sparse_cg"
        else:
            solver = self.solver

            sample_weight = _check_sample_weight(sample_weight, X, dtype=X.dtype)

        # when X is sparse we only remove offset from y
        X, y, X_offset, y_offset, X_scale = _preprocess_data(
            X,
            y,
            fit_intercept=self.fit_intercept,
            copy=self.copy_X,
        )

        if solver == "sag" and sparse.issparse(X) and self.fit_intercept:
            self.coef_, self.n_iter_, self.intercept_, self.solver_ = _ridge_regression(
                X,
                y,
                alpha=self.alpha,
                solver="sag",
                positive=self.positive,
                return_n_iter=True,
                return_intercept=True,
                return_solver=True,
                check_input=False,
            )
            # add the offset which was subtracted by _preprocess_data
            self.intercept_ += y_offset

        else:
            if sparse.issparse(X) and self.fit_intercept:
                # required to fit intercept with sparse_cg and lbfgs solver
                params = {"X_offset": X_offset, "X_scale": X_scale}
            else:
                # for dense matrices or when intercept is set to 0
                params = {}

            self.coef_, self.n_iter_, self.solver_ = _ridge_regression(
                X,
                y,
                return_intercept=False,
                fit_intercept=self.fit_intercept,
                **params,
            )
            self._set_intercept(X_offset, y_offset, X_scale)

class Ridge(MultiOutputMixin, RegressorMixin, _BaseRidge):
    """Linear least squares with l2 regularization.

    Minimizes the objective function::

    ||y - Xw||^2_2 + alpha * ||w||^2_2

    This model solves a regression model where the loss function is
    the linear least squares function and regularization is given by
    the l2-norm. Also known as Ridge Regression or Tikhonov regularization.
    (i.e., when y is a 2d-array of shape (n_samples, n_targets)).

    alpha : {float, ndarray of shape (n_targets,)}, default=1.0

        Whether to fit the intercept for this model. If set
        to false, no intercept will be used in calculations
        (i.e. ``X`` and ``y`` are expected to be centered).

        If True, X will be copied; else, it may be overwritten.

        For 'sparse_cg' and 'lsqr' solvers, the default value is determined
        by scipy.sparse.linalg. For 'sag' solver, the default value is 1000.
        For 'lbfgs' solver, the default value is 15000.

        The precision of the solution (`coef_`) is determined by `tol` which
        specifies a different convergence criterion for each solver:

        - 'svd': `tol` has no impact.

        - 'cholesky': `tol` has no impact.

        - 'sparse_cg': norm of residuals smaller than `tol`.

        - 'lsqr': `tol` is set as atol and btol of scipy.sparse.linalg.lsqr,
          which control the norm of the residual vector in terms of the norms of
          matrix and coefficients.

        - 'sag' and 'saga': relative change of coef smaller than `tol`.

        - 'lbfgs': maximum of the absolute (projected) gradient=max|residuals|
          smaller than `tol`.

           models.

          obtain a closed-form solution.

          is True.

           `random_state` to support Stochastic Average Gradient.

    coef_ : ndarray of shape (n_features,) or (n_targets, n_features)

    intercept_ : float or ndarray of shape (n_targets,)
        Independent term in decision function. Set to 0.0 if
        ``fit_intercept = False``.

    n_iter_ : None or ndarray of shape (n_targets,)
        Actual number of iterations for each target. Available only for
        sag and lsqr solvers. Other solvers will return None.

    solver_ : str
        The solver that was used at fit time by the computational
        routines.

    See Also
    --------
    RidgeClassifier : Ridge classifier.
    RidgeCV : Ridge regression with built-in cross validation.
    :class:`~sklearn.kernel_ridge.KernelRidge` : Kernel ridge regression
        combines ridge regression with the kernel trick.

    Notes
    -----
    :class:`~sklearn.svm.LinearSVC`.

    Examples
    --------
    >>> clf = Ridge(alpha=1.0)
    Ridge()
    """

        self,
        *,
        tol=1e-4,
    ):
            copy_X=copy_X,
            tol=tol,
        )

        """Fit Ridge regression model.

        y : ndarray of shape (n_samples,) or (n_samples, n_targets)

        sample_weight : float or ndarray of shape (n_samples,), default=None
            Individual weights for each sample. If given a float, every sample
            will have the same weight.

        Returns
        -------
        """
        _accept_sparse = _get_valid_accept_sparse(sparse.issparse(X), self.solver)
        xp, _ = get_namespace(X, y, sample_weight)
            self,
            X,
            y,
            accept_sparse=_accept_sparse,
        )

        tags.input_tags.sparse = (self.solver != "svd") and (
            self.solver != "cholesky" or not self.fit_intercept
        )

class _RidgeClassifierMixin(LinearClassifierMixin):
    def _prepare_data(self, X, y, sample_weight, solver):
        """Validate `X` and `y` and binarize `y`.

        solver : str
            The solver used in `Ridge` to know which sparse format to support.

        Returns
        -------
            Validated training data.

            Validated target values.

            Validated sample weights.

            The binarized version of `y`.
        """
        accept_sparse = _get_valid_accept_sparse(sparse.issparse(X), solver)
            self,
            X,
            y,
            y_numeric=False,
        )

        self._label_binarizer = LabelBinarizer(pos_label=1, neg_label=-1)
        Y = self._label_binarizer.fit_transform(y)
        if not self._label_binarizer.y_type_.startswith("multilabel"):

        if self.class_weight:
            sample_weight = sample_weight * compute_sample_weight(self.class_weight, y)
        return X, y, sample_weight, Y

        """Predict class labels for samples in `X`.

        X : {array-like, spare matrix} of shape (n_samples, n_features)
            The data matrix for which we want to predict the targets.

        Returns
        -------
            Vector or matrix containing the predictions. In binary and
            multiclass problems, this is a vector containing `n_samples`. In
            a multilabel problem, it returns a matrix of shape
            `(n_samples, n_outputs)`.
        """
        check_is_fitted(self, attributes=["_label_binarizer"])
        if self._label_binarizer.y_type_.startswith("multilabel"):
            # Threshold such that the negative label is -1 and positive label
            # is 1 to use the inverse transform of the label binarizer fitted
            # during fit.
            scores = 2 * (self.decision_function(X) > 0) - 1
            return self._label_binarizer.inverse_transform(scores)

    @property
        """Classes labels."""
        return self._label_binarizer.classes_

class RidgeClassifier(_RidgeClassifierMixin, _BaseRidge):
    """Classifier using Ridge regression.

    This classifier first converts the target values into ``{-1, 1}`` and
    then treats the problem as a regression task (multi-output regression in
    the multiclass case).

        :class:`~sklearn.svm.LinearSVC`.

        Whether to calculate the intercept for this model. If set to false, no
        intercept will be used in calculations (e.g. data is expected to be
        already centered).

        The default value is determined by scipy.sparse.linalg.

           models.

          its unbiased and more flexible version named SAGA. Both methods
          use an iterative procedure, and are often faster than other solvers
          when both n_samples and n_features are large. Note that 'sag' and

          .. versionadded:: 0.17
             Stochastic Average Gradient descent solver.
          .. versionadded:: 0.19
             SAGA solver.

          is True.

    coef_ : ndarray of shape (1, n_features) or (n_classes, n_features)
        Coefficient of the features in the decision function.

        ``coef_`` is of shape (1, n_features) when the given problem is binary.

        routines.

    See Also
    --------
    Ridge : Ridge regression.
    RidgeClassifierCV :  Ridge classifier with built-in cross validation.

    Notes
    -----
    For multi-class classification, n_class classifiers are trained in
    a one-versus-all approach. Concretely, this is implemented by taking
    advantage of the multi-variate response support in Ridge.

    Examples
    --------
    >>> from sklearn.linear_model import RidgeClassifier
    >>> clf = RidgeClassifier().fit(X, y)
    0.9595...
    """

        **_BaseRidge._parameter_constraints,
    }

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        """Fit Ridge classifier model.

               *sample_weight* support to RidgeClassifier.

        Returns
        -------
            Instance of the estimator.
        """
        X, y, sample_weight, Y = self._prepare_data(X, y, sample_weight, self.solver)

        super().fit(X, Y, sample_weight=sample_weight)

        )

def _check_gcv_mode(X, gcv_mode):
    if gcv_mode in ["eigen", "svd"]:
        return gcv_mode
    # if X has more rows than columns, use decomposition of X^T.X,
    # otherwise X.X^T
    if X.shape[0] > X.shape[1]:
        return "svd"
    return "eigen"

def _find_smallest_angle(query, vectors):
    """Find the column of vectors that is most aligned with the query.

    Both query and the columns of vectors must have their l2 norm equal to 1.

    query : ndarray of shape (n_samples,)
        Normalized query vector.

    vectors : ndarray of shape (n_samples, n_features)
        Vectors to which we compare query, as columns. Must be normalized.
    """
    abs_cosine = np.abs(query.dot(vectors))
    index = np.argmax(abs_cosine)
    return index

class _X_CenterStackOp(sparse.linalg.LinearOperator):
    """Behaves as centered and scaled X with an added intercept column.

    This operator behaves as
    np.hstack([X - sqrt_sw[:, None] * X_mean, sqrt_sw[:, None]])
    """

    def __init__(self, X, X_mean, sqrt_sw):
        super().__init__(X.dtype, (n_samples, n_features + 1))
        self.X = X
        self.X_mean = X_mean
        self.sqrt_sw = sqrt_sw

    def _matvec(self, v):
        v = v.ravel()
        return (
            safe_sparse_dot(self.X, v[:-1], dense_output=True)
            - self.sqrt_sw * self.X_mean.dot(v[:-1])
            + v[-1] * self.sqrt_sw
        )

    def _matmat(self, v):
        return (
            - self.sqrt_sw[:, None] * self.X_mean.dot(v[:-1])
            + v[-1] * self.sqrt_sw[:, None]
        )

    def _transpose(self):
        return _XT_CenterStackOp(self.X, self.X_mean, self.sqrt_sw)

class _XT_CenterStackOp(sparse.linalg.LinearOperator):
    """Behaves as transposed centered and scaled X with an intercept column.

    np.hstack([X - sqrt_sw[:, None] * X_mean, sqrt_sw[:, None]]).T
    """

        super().__init__(X.dtype, (n_features + 1, n_samples))
        self.X = X

        n_features = self.shape[0]
        res = np.empty(n_features, dtype=self.X.dtype)
        res[:-1] = safe_sparse_dot(self.X.T, v, dense_output=True) - (
            self.X_mean * self.sqrt_sw.dot(v)
        )
        res[-1] = np.dot(v, self.sqrt_sw)
        return res

        res = np.empty((n_features, v.shape[1]), dtype=self.X.dtype)
        res[:-1] = safe_sparse_dot(self.X.T, v, dense_output=True) - self.X_mean[
            :, None
        ] * self.sqrt_sw.dot(v)
        res[-1] = np.dot(self.sqrt_sw, v)
        return res

class _IdentityRegressor(RegressorMixin, BaseEstimator):
    """Fake regressor which will directly output the prediction."""

    def decision_function(self, y_predict):
        return y_predict

    def predict(self, y_predict):

class _IdentityClassifier(LinearClassifierMixin, BaseEstimator):
    """Fake classifier which will directly output the prediction.

    We inherit from LinearClassifierMixin to get the proper shape for the
    output `y`.
    """

    def __init__(self, classes):

class _RidgeGCV(LinearModel):
    """Ridge regression with built-in Leave-one-out Cross-Validation.

    This class is not intended to be used directly. Use RidgeCV instead.

    `_RidgeGCV` uses a Generalized Cross-Validation for model selection. It's an
    efficient approximation of leave-one-out cross-validation (LOO-CV), where instead of
    computing multiple models by excluding one data point at a time, it uses an
    algebraic shortcut to approximate the LOO-CV error, making it faster and
    computationally more efficient.

    Using a naive grid-search approach with a leave-one-out cross-validation in contrast
    requires to fit `n_samples` models to compute the prediction error for each sample
    and then to repeat this process for each alpha in the grid.

    Here, the prediction error for each sample is computed by solving a **single**
    linear system (in other words a single model) via a matrix factorization (i.e.
    eigendecomposition or SVD) solving the problem stated in the Notes section. Finally,
    we need to repeat this process for each alpha in the grid. The detailed complexity
    is further discussed in Sect. 4 in [1].

    This algebraic approach is only applicable for regularized least squares
    problems. It could potentially be extended to kernel ridge regression.

    See the Notes section and references for more details regarding the formulation
    and the linear system that is solved.

    Notes
    -----

    We want to solve (K + alpha*Id)c = y,
    where K = X X^T is the kernel matrix.

    Let G = (K + alpha*Id).

    Dual solution: c = G^-1y
    Primal solution: w = X^T c

    Compute eigendecomposition K = Q V Q^T.
    Then G^-1 = Q (V + alpha*Id)^-1 Q^T,
    where (V + alpha*Id) is diagonal.
    It is thus inexpensive to inverse for many alphas.

    Let loov be the vector of prediction values for each example
    when the model was fitted with all examples but this example.

    loov = (KG^-1Y - diag(KG^-1)Y) / diag(I-KG^-1)

    Let looe be the vector of prediction errors for each example

    looe = y - loov = c / diag(G^-1)

    The best score (negative mean squared error or user-provided scoring) is
    stored in the `best_score_` attribute, and the selected hyperparameter in
    `alpha_`.

    [1] http://cbcl.mit.edu/publications/ps/MIT-CSAIL-TR-2007-025.pdf
    [2] https://www.mit.edu/~9.520/spring07/Classes/rlsslides.pdf
    """

        self,
        alphas=(0.1, 1.0, 10.0),
        *,
        scoring=None,
        gcv_mode=None,
        store_cv_results=False,
        is_clf=False,
        alpha_per_target=False,
    ):
        self.gcv_mode = gcv_mode
        self.store_cv_results = store_cv_results
        self.is_clf = is_clf
        self.alpha_per_target = alpha_per_target

    def _decomp_diag(v_prime, Q):
        # compute diagonal of the matrix: dot(Q, dot(diag(v_prime), Q^T))
        return (v_prime * Q**2).sum(axis=-1)

    def _diag_dot(D, B):
        # compute dot(diag(D), B)
        if len(B.shape) > 1:
            # handle case where B is > 1-d
            D = D[(slice(None),) + (np.newaxis,) * (len(B.shape) - 1)]
        return D * B

    def _compute_gram(self, X, sqrt_sw):
        """Computes the Gram matrix XX^T with possible centering.

            The preprocessed design matrix.

        sqrt_sw : ndarray of shape (n_samples,)
            square roots of sample weights

        Returns
        -------
        gram : ndarray of shape (n_samples, n_samples)
            The Gram matrix.
        X_mean : ndarray of shape (n_feature,)
            The weighted mean of ``X`` for each feature.

        Notes
        -----
        When X is dense the centering has been done in preprocessing
        so the mean is 0 and we just compute XX^T.

        When X is sparse it has not been centered in preprocessing, but it has
        been scaled by sqrt(sample weights).

        When self.fit_intercept is False no centering is done.

        The centered X is never actually computed because centering would break
        the sparsity of X.
        """
        center = self.fit_intercept and sparse.issparse(X)
        if not center:
            # in this case centering has been done in preprocessing
            # or we are not fitting an intercept.
            X_mean = np.zeros(X.shape[1], dtype=X.dtype)
            return safe_sparse_dot(X, X.T, dense_output=True), X_mean
        # X is sparse
        sample_weight_matrix = sparse.dia_matrix(
            (sqrt_sw, 0), shape=(n_samples, n_samples)
        )
        X_weighted = sample_weight_matrix.dot(X)
        X_mean, _ = mean_variance_axis(X_weighted, axis=0)
        X_mean *= n_samples / sqrt_sw.dot(sqrt_sw)
        X_mX = sqrt_sw[:, None] * safe_sparse_dot(X_mean, X.T, dense_output=True)
        X_mX_m = np.outer(sqrt_sw, sqrt_sw) * np.dot(X_mean, X_mean)
        return (
            safe_sparse_dot(X, X.T, dense_output=True) + X_mX_m - X_mX - X_mX.T,
            X_mean,
        )

    def _compute_covariance(self, X, sqrt_sw):
        """Computes covariance matrix X^TX with possible centering.

        Returns
        -------
        covariance : ndarray of shape (n_features, n_features)
            The covariance matrix.

        Notes
        -----
        Since X is sparse it has not been centered in preprocessing, but it has

        """
        if not self.fit_intercept:
            return safe_sparse_dot(X.T, X, dense_output=True), X_mean
        # this function only gets called for sparse X
        )
        X_mean = X_mean * n_samples / sqrt_sw.dot(sqrt_sw)
        weight_sum = sqrt_sw.dot(sqrt_sw)
        return (
            safe_sparse_dot(X.T, X, dense_output=True)
            - weight_sum * np.outer(X_mean, X_mean),
            X_mean,
        )

    def _sparse_multidot_diag(self, X, A, X_mean, sqrt_sw):
        """Compute the diagonal of (X - X_mean).dot(A).dot((X - X_mean).T)
        without explicitly centering X nor computing X.dot(A)
        when X is sparse.

        A : ndarray of shape (n_features, n_features)

        X_mean : ndarray of shape (n_features,)

        sqrt_sw : ndarray of shape (n_features,)

        Returns
        -------
        diag : np.ndarray, shape (n_samples,)
            The computed diagonal.
        """
        intercept_col = scale = sqrt_sw
        batch_size = X.shape[1]
        diag = np.empty(X.shape[0], dtype=X.dtype)
        for start in range(0, X.shape[0], batch_size):
            batch = slice(start, min(X.shape[0], start + batch_size), 1)
            X_batch = np.empty(
                (X[batch].shape[0], X.shape[1] + self.fit_intercept), dtype=X.dtype
            )
                X_batch[:, :-1] = X[batch].toarray() - X_mean * scale[batch][:, None]
                X_batch[:, -1] = intercept_col[batch]
            else:
                X_batch = X[batch].toarray()
            diag[batch] = (X_batch.dot(A) * X_batch).sum(axis=1)
        return diag

    def _eigen_decompose_gram(self, X, y, sqrt_sw):
        """Eigendecomposition of X.X^T, used when n_samples <= n_features."""
        # if X is dense it has already been centered in preprocessing
        K, X_mean = self._compute_gram(X, sqrt_sw)
        if self.fit_intercept:
            # to emulate centering X with sample weights,
            # ie removing the weighted average, we add a column
            # containing the square roots of the sample weights.
            # by centering, it is orthogonal to the other columns
            K += np.outer(sqrt_sw, sqrt_sw)
        eigvals, Q = linalg.eigh(K)
        QT_y = np.dot(Q.T, y)
        return X_mean, eigvals, Q, QT_y

    def _solve_eigen_gram(self, alpha, y, sqrt_sw, X_mean, eigvals, Q, QT_y):
        """Compute dual coefficients and diagonal of G^-1.

        Used when we have a decomposition of X.X^T (n_samples <= n_features).
        """
        w = 1.0 / (eigvals + alpha)
            # the vector containing the square roots of the sample weights (1
            # when no sample weights) is the eigenvector of XX^T which
            # corresponds to the intercept; we cancel the regularization on
            # this dimension. the corresponding eigenvalue is
            # sum(sample_weight).
            normalized_sw = sqrt_sw / np.linalg.norm(sqrt_sw)
            intercept_dim = _find_smallest_angle(normalized_sw, Q)
            w[intercept_dim] = 0  # cancel regularization for the intercept

        c = np.dot(Q, self._diag_dot(w, QT_y))
        G_inverse_diag = self._decomp_diag(w, Q)
        # handle case where y is 2-d
        if len(y.shape) != 1:
            G_inverse_diag = G_inverse_diag[:, np.newaxis]
        return G_inverse_diag, c

    def _eigen_decompose_covariance(self, X, y, sqrt_sw):
        """Eigendecomposition of X^T.X, used when n_samples > n_features
        and X is sparse.
        """
        cov = np.empty((n_features + 1, n_features + 1), dtype=X.dtype)
        cov[:-1, :-1], X_mean = self._compute_covariance(X, sqrt_sw)
            cov = cov[:-1, :-1]
        # to emulate centering X with sample weights,
        # ie removing the weighted average, we add a column
        # containing the square roots of the sample weights.
        # by centering, it is orthogonal to the other columns
        # when all samples have the same weight we add a column of 1
        else:
            cov[-1] = 0
            cov[:, -1] = 0
            cov[-1, -1] = sqrt_sw.dot(sqrt_sw)
        nullspace_dim = max(0, n_features - n_samples)
        eigvals, V = linalg.eigh(cov)
        # remove eigenvalues and vectors in the null space of X^T.X
        eigvals = eigvals[nullspace_dim:]
        V = V[:, nullspace_dim:]
        return X_mean, eigvals, V, X

    def _solve_eigen_covariance_no_intercept(
        self, alpha, y, sqrt_sw, X_mean, eigvals, V, X
    ):

        Used when we have a decomposition of X^T.X
        (n_samples > n_features and X is sparse), and not fitting an intercept.
        """
        w = 1 / (eigvals + alpha)
        A = (V * w).dot(V.T)
        AXy = A.dot(safe_sparse_dot(X.T, y, dense_output=True))
        y_hat = safe_sparse_dot(X, AXy, dense_output=True)
        hat_diag = self._sparse_multidot_diag(X, A, X_mean, sqrt_sw)
            # handle case where y is 2-d
            hat_diag = hat_diag[:, np.newaxis]
        return (1 - hat_diag) / alpha, (y - y_hat) / alpha

    def _solve_eigen_covariance_intercept(
    ):

        (n_samples > n_features and X is sparse),
        and we are fitting an intercept.
        """
        # the vector [0, 0, ..., 0, 1]
        # is the eigenvector of X^TX which
        # corresponds to the intercept; we cancel the regularization on
        # this dimension. the corresponding eigenvalue is
        # sum(sample_weight), e.g. n when uniform sample weights.
        intercept_sv = np.zeros(V.shape[0])
        intercept_sv[-1] = 1
        intercept_dim = _find_smallest_angle(intercept_sv, V)
        w[intercept_dim] = 1 / eigvals[intercept_dim]
        # add a column to X containing the square roots of sample weights
        X_op = _X_CenterStackOp(X, X_mean, sqrt_sw)
        AXy = A.dot(X_op.T.dot(y))
        y_hat = X_op.dot(AXy)
        # return (1 - hat_diag), (y - y_hat)

    def _solve_eigen_covariance(self, alpha, y, sqrt_sw, X_mean, eigvals, V, X):

        (n_samples > n_features and X is sparse).
        """
            return self._solve_eigen_covariance_intercept(
                alpha, y, sqrt_sw, X_mean, eigvals, V, X
            )
        return self._solve_eigen_covariance_no_intercept(
            alpha, y, sqrt_sw, X_mean, eigvals, V, X
        )

    def _svd_decompose_design_matrix(self, X, y, sqrt_sw):
        # X already centered
        X_mean = np.zeros(X.shape[1], dtype=X.dtype)
            # to emulate fit_intercept=True situation, add a column
            # containing the square roots of the sample weights
            # by centering, the other columns are orthogonal to that one
            intercept_column = sqrt_sw[:, None]
            X = np.hstack((X, intercept_column))
        U, singvals, _ = linalg.svd(X, full_matrices=0)
        singvals_sq = singvals**2
        UT_y = np.dot(U.T, y)
        return X_mean, singvals_sq, U, UT_y

    def _solve_svd_design_matrix(self, alpha, y, sqrt_sw, X_mean, singvals_sq, U, UT_y):

        Used when we have an SVD decomposition of X
        (n_samples > n_features and X is dense).
        """
        w = ((singvals_sq + alpha) ** -1) - (alpha**-1)
            # detect intercept column
            intercept_dim = _find_smallest_angle(normalized_sw, U)
            # cancel the regularization for the intercept
            w[intercept_dim] = -(alpha**-1)
        c = np.dot(U, self._diag_dot(w, UT_y)) + (alpha**-1) * y
        G_inverse_diag = self._decomp_diag(w, U) + (alpha**-1)

    def fit(self, X, y, sample_weight=None, score_params=None):
        """Fit Ridge regression model with gcv.

            Training data. Will be cast to float64 if necessary.

            Target values. Will be cast to float64 if necessary.

            will have the same weight. Note that the scale of `sample_weight`
            has an impact on the loss; i.e. multiplying all weights by `k`
            is equivalent to setting `alpha / k`.

        score_params : dict, default=None
            Parameters to be passed to the underlying scorer.

        Returns
        -------
        """
            self,
            X,
            y,
            dtype=[np.float64],
        )

        # alpha_per_target cannot be used in classifier mode. All subclasses
        # of _RidgeGCV that are classifiers keep alpha_per_target at its
        # default value: False, so the condition below should never happen.
        assert not (self.is_clf and self.alpha_per_target)

        self.alphas = np.asarray(self.alphas)

        unscaled_y = y
            X,
            y,
        )

        gcv_mode = _check_gcv_mode(X, self.gcv_mode)

        if gcv_mode == "eigen":
            decompose = self._eigen_decompose_gram
            solve = self._solve_eigen_gram
        elif gcv_mode == "svd":
                decompose = self._eigen_decompose_covariance
                solve = self._solve_eigen_covariance
            else:
                decompose = self._svd_decompose_design_matrix
                solve = self._solve_svd_design_matrix

            X, y, sqrt_sw = _rescale_data(X, y, sample_weight)
        else:
            sqrt_sw = np.ones(n_samples, dtype=X.dtype)

        X_mean, *decomposition = decompose(X, y, sqrt_sw)

        n_y = 1 if len(y.shape) == 1 else y.shape[1]
        n_alphas = 1 if np.ndim(self.alphas) == 0 else len(self.alphas)

        if self.store_cv_results:
            self.cv_results_ = np.empty((n_samples * n_y, n_alphas), dtype=X.dtype)

        best_coef, best_score, best_alpha = None, None, None

        for i, alpha in enumerate(np.atleast_1d(self.alphas)):
            G_inverse_diag, c = solve(float(alpha), y, sqrt_sw, X_mean, *decomposition)
            if self.scoring is None:
                squared_errors = (c / G_inverse_diag) ** 2
                alpha_score = self._score_without_scorer(squared_errors=squared_errors)
                if self.store_cv_results:
                    self.cv_results_[:, i] = squared_errors.ravel()
            else:
                predictions = y - (c / G_inverse_diag)
                # Rescale predictions back to original scale
                if sample_weight is not None:  # avoid the unnecessary division by ones
                    if predictions.ndim > 1:
                        predictions /= sqrt_sw[:, None]
                    else:
                        predictions /= sqrt_sw
                predictions += y_offset

                    self.cv_results_[:, i] = predictions.ravel()

                score_params = score_params or {}
                alpha_score = self._score(
                    predictions=predictions,
                    y=unscaled_y,
                    n_y=n_y,
                    scorer=self.scoring,
                    score_params=score_params,
                )

            # Keep track of the best model
            if best_score is None:
                # initialize
                if self.alpha_per_target and n_y > 1:
                    best_coef = c
                    best_score = np.atleast_1d(alpha_score)
                    best_alpha = np.full(n_y, alpha)
                else:
                    best_score = alpha_score
                    best_alpha = alpha
            else:
                # update
                    to_update = alpha_score > best_score
                    best_coef[:, to_update] = c[:, to_update]
                    best_score[to_update] = alpha_score[to_update]
                    best_alpha[to_update] = alpha
                elif alpha_score > best_score:
                    best_coef, best_score, best_alpha = c, alpha_score, alpha

        self.best_score_ = best_score
        self.dual_coef_ = best_coef
        self.coef_ = safe_sparse_dot(self.dual_coef_.T, X)
        if y.ndim == 1 or y.shape[1] == 1:
            self.coef_ = self.coef_.ravel()

            X_offset = X_mean * X_scale
        else:
            X_offset += X_mean * X_scale
        self._set_intercept(X_offset, y_offset, X_scale)

            if len(y.shape) == 1:
                cv_results_shape = n_samples, n_alphas
            else:
                cv_results_shape = n_samples, n_y, n_alphas
            self.cv_results_ = self.cv_results_.reshape(cv_results_shape)

    def _score_without_scorer(self, squared_errors):
        """Performs scoring using squared errors when the scorer is None."""
        if self.alpha_per_target:
            _score = -squared_errors.mean(axis=0)
        else:
            _score = -squared_errors.mean()

        return _score

    def _score(self, *, predictions, y, n_y, scorer, score_params):
        """Performs scoring with the specified scorer using the
        predictions and the true y values.
        """
        if self.is_clf:
            identity_estimator = _IdentityClassifier(classes=np.arange(n_y))
            _score = scorer(
                identity_estimator,
                y.argmax(axis=1),
                **score_params,
            )
        else:
            identity_estimator = _IdentityRegressor()
            if self.alpha_per_target:
                _score = np.array(
                    [
                        scorer(
                            identity_estimator,
                            predictions[:, j],
                            y[:, j],
                            **score_params,
                        )
                        for j in range(n_y)
                    ]
                )
            else:
                _score = scorer(identity_estimator, predictions, y, **score_params)

        # Required since this is neither a RegressorMixin nor a ClassifierMixin

class _BaseRidgeCV(LinearModel):
        "alphas": ["array-like", Interval(Real, 0, None, closed="neither")],
        "scoring": [StrOptions(set(get_scorer_names())), callable, None],
        "gcv_mode": [StrOptions({"auto", "svd", "eigen"}), None],
        "store_cv_results": ["boolean"],
        "alpha_per_target": ["boolean"],
    }

        self,
        *,
        cv=None,
    ):

    def fit(self, X, y, sample_weight=None, **params):
        """Fit Ridge regression model with cv.

            Training data. If using GCV, will be cast to float64
            if necessary.

            Target values. Will be cast to X's dtype if necessary.

            Extra parameters for the underlying scorer.

        Returns
        -------

        Notes
        -----
        When sample_weight is provided, the selected hyperparameter may depend
        on whether we use leave-one-out cross-validation (cv=None)
        or another form of cross-validation, because only leave-one-out
        cross-validation takes the sample weights into account when computing
        the validation score.
        """
        cv = self.cv
        scorer = self._get_scorer()

        # `_RidgeGCV` does not work for alpha = 0
        if cv is None:
            check_scalar_alpha = partial(
                check_scalar,
                target_type=numbers.Real,
                min_val=0.0,
                include_boundaries="neither",
            )
        else:
                include_boundaries="left",
            )

        if isinstance(self.alphas, (np.ndarray, list, tuple)):
            n_alphas = 1 if np.ndim(self.alphas) == 0 else len(self.alphas)
            if n_alphas != 1:
                for index, alpha in enumerate(self.alphas):
                    alpha = check_scalar_alpha(alpha, f"alphas[{index}]")
            else:
                self.alphas[0] = check_scalar_alpha(self.alphas[0], "alphas")
        alphas = np.asarray(self.alphas)

            params["sample_weight"] = sample_weight

                    self,
                    "fit",
                    **params,
                )
            else:
                routed_params = Bunch(scorer=Bunch(score={}))
                    routed_params.scorer.score["sample_weight"] = sample_weight

            # reset `scorer` variable to original user-intend if no scoring is passed
                scorer = None

            estimator = _RidgeGCV(
                alphas,
                scoring=scorer,
                gcv_mode=self.gcv_mode,
                store_cv_results=self.store_cv_results,
                is_clf=is_classifier(self),
                alpha_per_target=self.alpha_per_target,
            )
            estimator.fit(
                X,
                y,
                score_params=routed_params.scorer.score,
            )
            self.alpha_ = estimator.alpha_
            self.best_score_ = estimator.best_score_
            if self.store_cv_results:
                self.cv_results_ = estimator.cv_results_
        else:
                raise ValueError("cv!=None and store_cv_results=True are incompatible")
                raise ValueError("cv!=None and alpha_per_target=True are incompatible")

            parameters = {"alpha": alphas}
            solver = "sparse_cg" if sparse.issparse(X) else "auto"
            model = RidgeClassifier if is_classifier(self) else Ridge
            estimator = model(
            )

            grid_search = GridSearchCV(
                parameters,
                cv=cv,
            )

            grid_search.fit(X, y, **params)
            estimator = grid_search.best_estimator_
            self.alpha_ = grid_search.best_estimator_.alpha
            self.best_score_ = grid_search.best_score_

        self.coef_ = estimator.coef_
        self.intercept_ = estimator.intercept_
        self.n_features_in_ = estimator.n_features_in_
        if hasattr(estimator, "feature_names_in_"):
            self.feature_names_in_ = estimator.feature_names_in_

        Returns
        -------
        """
        router = (
            .add(
                scorer=self.scoring,
                method_mapping=MethodMapping().add(caller="fit", callee="score"),
            )
            .add(
            )
        )

    def _get_scorer(self):
        scorer = check_scoring(estimator=self, scoring=self.scoring, allow_none=True)
        if _routing_enabled() and self.scoring is None:
            # This estimator passes an array of 1s as sample_weight even if
            # sample_weight is not provided by the user. Therefore we need to
            # always request it. But we don't set it if it's passed explicitly
            # by the user.
            scorer.set_score_request(sample_weight=True)
        return scorer

class RidgeCV(MultiOutputMixin, RegressorMixin, _BaseRidgeCV):
    """Ridge regression with built-in cross-validation.

    By default, it performs efficient Leave-One-Out Cross-Validation.

    alphas : array-like of shape (n_alphas,), default=(0.1, 1.0, 10.0)
        Array of alpha values to try.
        If using Leave-One-Out cross-validation, alphas must be strictly positive.

        Whether to calculate the intercept for this model. If set
        (i.e. data is expected to be centered).

    scoring : str, callable, default=None
        The scoring method to use for cross-validation. Options:

        - `None`: negative :ref:`mean squared error <mean_squared_error>` if cv is
          None (i.e. when using leave-one-out cross-validation), or
          :ref:`coefficient of determination <r2_score>` (:math:`R^2`) otherwise.

        - None, to use the efficient Leave-One-Out cross-validation

        :class:`~sklearn.model_selection.StratifiedKFold` is used, else,
        :class:`~sklearn.model_selection.KFold` is used.

    gcv_mode : {'auto', 'svd', 'eigen'}, default='auto'
        Flag indicating which strategy to use when performing
        Leave-One-Out Cross-Validation. Options are::

            'auto' : use 'svd' if n_samples > n_features, otherwise use 'eigen'
            'svd' : force use of singular value decomposition of X when X is
                dense, eigenvalue decomposition of X^T.X when X is sparse.
            'eigen' : force computation via eigendecomposition of X.X^T

        The 'auto' mode is the default and is intended to pick the cheaper
        option of the two depending on the shape of the training data.

    store_cv_results : bool, default=False
        Flag indicating if the cross-validation values corresponding to
        each alpha should be stored in the ``cv_results_`` attribute (see
        below). This flag is only compatible with ``cv=None`` (i.e. using
        Leave-One-Out Cross-Validation).

            Parameter name changed from `store_cv_values` to `store_cv_results`.

    alpha_per_target : bool, default=False
        Flag indicating whether to optimize the alpha value (picked from the
        `alphas` parameter list) for each target separately (for multi-output
        settings: multiple prediction targets). When set to `True`, after
        fitting, the `alpha_` attribute will contain a value for each target.
        When set to `False`, a single alpha is used for all targets.

    cv_results_ : ndarray of shape (n_samples, n_alphas) or \
            shape (n_samples, n_targets, n_alphas), optional
        Cross-validation values for each alpha (only available if
        ``store_cv_results=True`` and ``cv=None``). After ``fit()`` has been
        called, this attribute will contain the mean squared errors if
        `scoring is None` otherwise it will contain standardized per point
        prediction values.

            `cv_values_` changed to `cv_results_`.

    coef_ : ndarray of shape (n_features) or (n_targets, n_features)

    alpha_ : float or ndarray of shape (n_targets,)
        Estimated regularization parameter, or, if ``alpha_per_target=True``,
        the estimated regularization parameter for each target.

    best_score_ : float or ndarray of shape (n_targets,)
        Score of base estimator with best alpha, or, if
        ``alpha_per_target=True``, a score for each target.

    See Also
    --------
    RidgeClassifier : Classifier based on ridge regression on {-1, 1} labels.
    RidgeClassifierCV : Ridge classifier with built-in cross validation.

    Examples
    --------
    >>> clf = RidgeCV(alphas=[1e-3, 1e-2, 1e-1, 1]).fit(X, y)
    0.5166...
    """

        Returns
        -------

        Notes
        -----
        """
        super().fit(X, y, sample_weight=sample_weight, **params)

class RidgeClassifierCV(_RidgeClassifierMixin, _BaseRidgeCV):
    """Ridge classifier with built-in cross-validation.

    By default, it performs Leave-One-Out Cross-Validation. Currently,
    only the n_features > n_samples case is handled efficiently.

          :ref:`accuracy <accuracy_score>` otherwise.

        Flag indicating if the cross-validation results corresponding to

    cv_results_ : ndarray of shape (n_samples, n_targets, n_alphas), optional
        Cross-validation results for each alpha (only if ``store_cv_results=True`` and
        ``cv=None``). After ``fit()`` has been called, this attribute will
        contain the mean squared errors if `scoring is None` otherwise it
        will contain standardized per point prediction values.

    coef_ : ndarray of shape (1, n_features) or (n_targets, n_features)

        Estimated regularization parameter.

    best_score_ : float
        Score of base estimator with best alpha.

    See Also
    --------

    Notes
    -----

    Examples
    --------
    >>> clf = RidgeClassifierCV(alphas=[1e-3, 1e-2, 1e-1, 1]).fit(X, y)
    0.9630...
    """

        **_BaseRidgeCV._parameter_constraints,
    }
    for param in ("gcv_mode", "alpha_per_target"):

        self,
        *,
        cv=None,
    ):
            alphas=alphas,
            cv=cv,
            store_cv_results=store_cv_results,
        )

        """Fit Ridge classifier with cv.

            and `n_features` is the number of features. When using GCV,
            will be cast to float64 if necessary.

        Returns
        -------
        """
        # `RidgeClassifier` does not accept "sag" or "saga" solver and thus support
        # csr, csc, and coo sparse matrices. By using solver="eigen" we force to accept
        # all sparse format.
        X, y, sample_weight, Y = self._prepare_data(X, y, sample_weight, solver="eigen")

        # If cv is None, gcv mode will be used and we used the binarized Y
        # since y will not be binarized in _RidgeGCV estimator.
        # If cv is not None, a GridSearchCV with some RidgeClassifier
        # estimators are used where y will be binarized. Thus, we pass y
        # instead of the binarized Y.
        target = Y if self.cv is None else y
        super().fit(X, target, sample_weight=sample_weight, **params)

        self,
        *,
        tol=1e-4,
    ):

            )

                )
            else:
                )
                )
            else:
        else:

            X,
            y,
        )

                X,
                y,
            )

        else:
            else:
                params = {}

                X,
                y,
                **params,
            )

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        Returns
        -------
        """
            self,
            X,
            y,
        )

        )

        Returns
        -------

        """
            self,
            X,
            y,
        )

        Returns
        -------
        """

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        Returns
        -------
        """

        )

        self.X = X

        return (
        )

        return (
        )

        self.X = X

        )
        return res

            :, None
        return res

        self,
        *,
    ):

        return D * B

        Returns
        -------

        Notes
        -----

        """
        )
        return (
            X_mean,
        )

        Returns
        -------

        Notes
        -----

        """
        )
        return (
            X_mean,
        )

        Returns
        -------
        """
            )
            else:

        """

        """
        else:
            cov[-1] = 0

    ):

        """

    ):

        """

        """
            )
        )

        """

        Returns
        -------
        """
            self,
            X,
            y,
        )

            X,
            y,
        )

            else:

        else:

            else:
                    else:

                    n_y=n_y,
                )

                else:
            else:
                # update

        else:

            else:

        else:

        """
            )
        else:
                    [
                        scorer(
                            y[:, j],
                        )
                    ]
                )
            else:

        self,
        *,
        cv=None,
    ):

        Returns
        -------

        Notes
        -----
        """

            )
        else:
            )

            else:

                    self,
                    "fit",
                    **params,
                )
            else:

                alphas,
            )
                X,
                y,
            )
        else:

            )

                cv=cv,
            )

        Returns
        -------
        """
        router = (
            .add(
            )
            .add(
            )
        )

        Returns
        -------

        Notes
        -----
        """

        self,
        *,
        cv=None,
    ):
            cv=cv,
        )

        Returns
        -------
        """

            return _mv

            return _mv

def _set_order(X, y, order="C"):
    """Change the order of X and y if necessary.

    order : {None, 'C', 'F'}
        If 'C', dense arrays are returned as C-ordered, sparse matrices in csr
        format. If 'F', dense arrays are return as F-ordered, sparse matrices
        in csc format.

    Returns
    -------
        Training data with guaranteed order.

        Target values with guaranteed order.
    """
    if order not in [None, "C", "F"]:
            "Unknown value for order. Got {} instead of None, 'C' or 'F'.".format(order)
        )
    sparse_X = sparse.issparse(X)
    sparse_y = sparse.issparse(y)
    if order is not None:
        sparse_format = "csc" if order == "F" else "csr"
        if sparse_X:
            X = X.asformat(sparse_format, copy=False)
        else:
            X = np.asarray(X, order=order)
        if sparse_y:
            y = y.asformat(sparse_format)
        else:
            y = np.asarray(y, order=order)
    return X, y

def _alpha_grid(
    X,
    y,
    Xy=None,
    l1_ratio=1.0,
    eps=1e-3,
    n_alphas=100,
    copy_X=True,
):
    """Compute the grid of alpha values for elastic net parameter search

        Training data. Pass directly as Fortran-contiguous data to avoid
        unnecessary memory duplication

    y : ndarray of shape (n_samples,) or (n_samples, n_outputs)
        Target values

    Xy : array-like of shape (n_features,) or (n_features, n_outputs),\
         default=None
        Xy = np.dot(X.T, y) that can be precomputed.

    l1_ratio : float, default=1.0
        The elastic net mixing parameter, with ``0 < l1_ratio <= 1``.
        For ``l1_ratio = 0`` the penalty is an L2 penalty. (currently not
        supported) ``For l1_ratio = 1`` it is an L1 penalty. For
        ``0 < l1_ratio <1``, the penalty is a combination of L1 and L2.

    eps : float, default=1e-3
        Length of the path. ``eps=1e-3`` means that
        ``alpha_min / alpha_max = 1e-3``

    n_alphas : int, default=100
        Number of alphas along the regularization path

        Whether to fit an intercept or not

        If ``True``, X will be copied; else, it may be overwritten.

    sample_weight : ndarray of shape (n_samples,), default=None
    """
    if l1_ratio == 0:
            "Automatic alpha grid generation is not supported for"
            " l1_ratio=0. Please supply a grid by providing "
            "your estimator with the appropriate `alphas=` "
            "argument."
        )
    if Xy is not None:
        Xyw = Xy
    else:
        X, y, X_offset, _, _ = _preprocess_data(
            X,
            y,
            copy=copy_X,
        )
            if y.ndim > 1:
                yw = y * sample_weight.reshape(-1, 1)
            else:
                yw = y * sample_weight
        else:
            yw = y
            Xyw = safe_sparse_dot(X.T, yw, dense_output=True) - np.sum(yw) * X_offset
        else:
            Xyw = np.dot(X.T, yw)

    if Xyw.ndim == 1:
        Xyw = Xyw[:, np.newaxis]
        n_samples = sample_weight.sum()
    else:
    alpha_max = np.sqrt(np.sum(Xyw**2, axis=1)).max() / (n_samples * l1_ratio)

    if alpha_max <= np.finfo(np.float64).resolution:
        return np.full(n_alphas, np.finfo(np.float64).resolution)

    return np.geomspace(alpha_max, alpha_max * eps, num=n_alphas)

def lasso_path(
    X,
    y,
    *,
    eps=1e-3,
    alphas=None,
    precompute="auto",
    Xy=None,
    coef_init=None,
    **params,
):
    """Compute Lasso path with coordinate descent.

    The Lasso optimization function varies for mono and multi-outputs.

    For mono-output tasks it is::

        (1 / (2 * n_samples)) * ||y - Xw||^2_2 + alpha * ||w||_1

    For multi-output tasks it is::

        (1 / (2 * n_samples)) * ||Y - XW||^2_Fro + alpha * ||W||_21

    Where::

        ||W||_21 = \\sum_i \\sqrt{\\sum_j w_{ij}^2}

    i.e. the sum of norm of each row.

    Read more in the :ref:`User Guide <lasso>`.

        unnecessary memory duplication. If ``y`` is mono-output then ``X``
        can be sparse.

    y : {array-like, sparse matrix} of shape (n_samples,) or \
        (n_samples, n_targets)

        ``alpha_min / alpha_max = 1e-3``.

        Number of alphas along the regularization path.

    alphas : array-like, default=None
        List of alphas where to compute the models.
        If ``None`` alphas are set automatically.

    precompute : 'auto', bool or array-like of shape \
            (n_features, n_features), default='auto'
        Whether to use a precomputed Gram matrix to speed up
        calculations. If set to ``'auto'`` let us decide. The Gram
        matrix can also be passed as argument.

    Xy : array-like of shape (n_features,) or (n_features, n_targets),\
        Xy = np.dot(X.T, y) that can be precomputed. It is useful
        only when the Gram matrix is precomputed.

    coef_init : array-like of shape (n_features, ), default=None
        The initial values of the coefficients.

        Amount of verbosity.

        Whether to return the number of iterations or not.

        If set to True, forces coefficients to be positive.
        (Only allowed when ``y.ndim == 1``).

    **params : kwargs
        Keyword arguments passed to the coordinate descent solver.

    Returns
    -------
    alphas : ndarray of shape (n_alphas,)
        The alphas along the path where models are computed.

    coefs : ndarray of shape (n_features, n_alphas) or \
            (n_targets, n_features, n_alphas)
        Coefficients along the path.

    dual_gaps : ndarray of shape (n_alphas,)
        The dual gaps at the end of the optimization for each alpha.

    n_iters : list of int
        The number of iterations taken by the coordinate descent optimizer to
        reach the specified tolerance for each alpha.

    See Also
    --------
    lars_path : Compute Least Angle Regression or Lasso path using LARS
        algorithm.
    Lasso : The Lasso is a linear model that estimates sparse coefficients.
    LassoLars : Lasso model fit with Least Angle Regression a.k.a. Lars.
    LassoCV : Lasso linear model with iterative fitting along a regularization
        path.
    LassoLarsCV : Cross-validated Lasso using the LARS algorithm.
    sklearn.decomposition.sparse_encode : Estimator that can be used to
        transform signals into sparse linear combination of atoms from a fixed.

    Notes
    -----
    :ref:`examples/linear_model/plot_lasso_lasso_lars_elasticnet_path.py
    <sphx_glr_auto_examples_linear_model_plot_lasso_lasso_lars_elasticnet_path.py>`.

    To avoid unnecessary memory duplication the X argument of the fit method
    should be directly passed as a Fortran-contiguous numpy array.

    Note that in certain cases, the Lars solver may be significantly
    faster to implement this functionality. In particular, linear
    interpolation can be used to retrieve model coefficients between the
    values output by lars_path

    Examples
    --------

    Comparing lasso_path and lars_path with interpolation:

    >>> from sklearn.linear_model import lasso_path
    >>> X = np.array([[1, 2, 3.1], [2.3, 5.4, 4.3]]).T
    >>> y = np.array([1, 2, 3.1])
    >>> # Use lasso_path to compute a coefficient path
    >>> _, coef_path, _ = lasso_path(X, y, alphas=[5., 1., .5])
    >>> print(coef_path)
    [[0.         0.         0.46874778]
     [0.2159048  0.4425765  0.23689075]]

    >>> # Now use lars_path and 1D linear interpolation to compute the
    >>> # same path
    >>> from sklearn.linear_model import lars_path
    >>> alphas, active, coef_path_lars = lars_path(X, y, method='lasso')
    >>> from scipy import interpolate
    >>> coef_path_continuous = interpolate.interp1d(alphas[::-1],
    ...                                             coef_path_lars[:, ::-1])
    >>> print(coef_path_continuous([5., 1., .5]))
    [[0.         0.         0.46915237]
     [0.2159048  0.4425765  0.23668876]]
    """
    return enet_path(
        X,
        y,
        l1_ratio=1.0,
        eps=eps,
        alphas=alphas,
        precompute=precompute,
        Xy=Xy,
        copy_X=copy_X,
        coef_init=coef_init,
        **params,
    )

def enet_path(
    X,
    y,
    *,
    l1_ratio=0.5,
    eps=1e-3,
    Xy=None,
    **params,
):
    """Compute elastic net path with coordinate descent.

    The elastic net optimization function varies for mono and multi-outputs.

        1 / (2 * n_samples) * ||y - Xw||^2_2
        + alpha * l1_ratio * ||w||_1
        + 0.5 * alpha * (1 - l1_ratio) * ||w||^2_2

        (1 / (2 * n_samples)) * ||Y - XW||_Fro^2
        + alpha * l1_ratio * ||W||_21
        + 0.5 * alpha * (1 - l1_ratio) * ||W||_Fro^2

    Where::

    Read more in the :ref:`User Guide <elastic_net>`.

    l1_ratio : float, default=0.5
        Number between 0 and 1 passed to elastic net (scaling between
        l1 and l2 penalties). ``l1_ratio=1`` corresponds to the Lasso.

        If None alphas are set automatically.

        If set to False, the input validation checks are skipped (including the
        Gram matrix when provided). It is assumed that they are handled
        by the caller.

    Returns
    -------

        (Is returned when ``return_n_iter`` is set to True).

    See Also
    --------
    MultiTaskElasticNet : Multi-task ElasticNet model trained with L1/L2 mixed-norm \
    as regularizer.
    MultiTaskElasticNetCV : Multi-task L1/L2 ElasticNet with built-in cross-validation.
    ElasticNet : Linear regression with combined L1 and L2 priors as regularizer.
    ElasticNetCV : Elastic Net model with iterative fitting along a regularization path.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.linear_model import enet_path
    >>> X, y, true_coef = make_regression(
    ...    n_samples=100, n_features=5, n_informative=2, coef=True, random_state=0
    ... )
    >>> true_coef
    array([ 0.        ,  0.        ,  0.        , 97.9..., 45.7...])
    >>> alphas, estimated_coef, _ = enet_path(X, y, n_alphas=3)
    >>> alphas.shape
    (3,)
    >>> estimated_coef
     array([[ 0.        ,  0.78...,  0.56...],
            [ 0.        ,  1.12...,  0.61...],
            [-0.        , -2.12..., -1.12...],
            [ 0.        , 23.04..., 88.93...],
            [ 0.        , 10.63..., 41.56...]])
    """
    X_offset_param = params.pop("X_offset", None)
    X_scale_param = params.pop("X_scale", None)
    sample_weight = params.pop("sample_weight", None)
    tol = params.pop("tol", 1e-4)
    max_iter = params.pop("max_iter", 1000)
    random_state = params.pop("random_state", None)
    selection = params.pop("selection", "cyclic")

    if len(params) > 0:
        raise ValueError("Unexpected parameters in params", params.keys())

    # We expect X and y to be already Fortran ordered when bypassing
    # checks
            X,
        )
            y,
            dtype=X.dtype.type,
        )
        if Xy is not None:
            # Xy should be a 1d contiguous array or a 2D C ordered array
            Xy = check_array(
                Xy, dtype=X.dtype.type, order="C", copy=False, ensure_2d=False
            )

    if y.ndim != 1:
        n_targets = y.shape[1]

    if multi_output and positive:
        raise ValueError("positive=True is not allowed for multi-output (y.ndim != 1)")

    # MultiTaskElasticNet does not support sparse matrices
    if not multi_output and sparse.issparse(X):
        if X_offset_param is not None:
            # As sparse matrices are not actually centered we need this to be passed to
            # the CD solver.
            X_sparse_scaling = X_offset_param / X_scale_param
            X_sparse_scaling = np.asarray(X_sparse_scaling, dtype=X.dtype)
        else:
            X_sparse_scaling = np.zeros(n_features, dtype=X.dtype)

    # X should have been passed through _pre_fit already if function is called
    # from ElasticNet.fit
        X, y, _, _, _, precompute, Xy = _pre_fit(
            X,
            y,
            Xy,
            precompute,
        )
    if alphas is None:
        # No need to normalize of fit_intercept: it has been done
        # above
        alphas = _alpha_grid(
            X,
            y,
            Xy=Xy,
            eps=eps,
            n_alphas=n_alphas,
            copy_X=False,
        )
    elif len(alphas) > 1:
        alphas = np.sort(alphas)[::-1]  # make sure alphas are properly ordered

    n_alphas = len(alphas)
    dual_gaps = np.empty(n_alphas)
    n_iters = []

    if selection not in ["random", "cyclic"]:
        raise ValueError("selection should be either random or cyclic.")
    random = selection == "random"

    if not multi_output:
        coefs = np.empty((n_features, n_alphas), dtype=X.dtype)
    else:
        coefs = np.empty((n_targets, n_features, n_alphas), dtype=X.dtype)

    if coef_init is None:
        coef_ = np.zeros(coefs.shape[:-1], dtype=X.dtype, order="F")
    else:
        coef_ = np.asfortranarray(coef_init, dtype=X.dtype)

    for i, alpha in enumerate(alphas):
        # account for n_samples scaling in objectives between here and cd_fast
        l1_reg = alpha * l1_ratio * n_samples
        l2_reg = alpha * (1.0 - l1_ratio) * n_samples
        if not multi_output and sparse.issparse(X):
            model = cd_fast.sparse_enet_coordinate_descent(
                w=coef_,
                alpha=l1_reg,
                beta=l2_reg,
                X_data=X.data,
                X_indices=X.indices,
                X_indptr=X.indptr,
                y=y,
                X_mean=X_sparse_scaling,
                tol=tol,
                rng=rng,
                random=random,
            )
        elif multi_output:
            model = cd_fast.enet_coordinate_descent_multi_task(
                coef_, l1_reg, l2_reg, X, y, max_iter, tol, rng, random
            )
        elif isinstance(precompute, np.ndarray):
            # We expect precompute to be already Fortran ordered when bypassing
            # checks
            if check_input:
                precompute = check_array(precompute, dtype=X.dtype.type, order="C")
            model = cd_fast.enet_coordinate_descent_gram(
                coef_,
                l1_reg,
                l2_reg,
                precompute,
                Xy,
                y,
                max_iter,
                tol,
                rng,
                random,
                positive,
            )
        elif precompute is False:
            model = cd_fast.enet_coordinate_descent(
                coef_, l1_reg, l2_reg, X, y, max_iter, tol, rng, random, positive
            )
        else:
                "Precompute should be one of True, False, 'auto' or array-like. Got %r"
                % precompute
            )
        coef_, dual_gap_, eps_, n_iter_ = model
        coefs[..., i] = coef_
        # we correct the scale of the returned dual gap, as the objective
        # in cd_fast is n_samples * the objective in this docstring.
        dual_gaps[i] = dual_gap_ / n_samples
        n_iters.append(n_iter_)

            if verbose > 2:
                print(model)
            elif verbose > 1:
                print("Path: %03i out of %03i" % (i, n_alphas))
            else:
                sys.stderr.write(".")

        return alphas, coefs, dual_gaps, n_iters
    return alphas, coefs, dual_gaps

class ElasticNet(MultiOutputMixin, RegressorMixin, LinearModel):
    """Linear regression with combined L1 and L2 priors as regularizer.

            1 / (2 * n_samples) * ||y - Xw||^2_2
            + alpha * l1_ratio * ||w||_1
            + 0.5 * alpha * (1 - l1_ratio) * ||w||^2_2

    If you are interested in controlling the L1 and L2 penalty
    separately, keep in mind that this is equivalent to::

            a * ||w||_1 + 0.5 * b * ||w||_2^2

    where::

            alpha = a + b and l1_ratio = a / (a + b)

    The parameter l1_ratio corresponds to alpha in the glmnet R package while
    alpha corresponds to the lambda parameter in glmnet. Specifically, l1_ratio
    = 1 is the lasso penalty. Currently, l1_ratio <= 0.01 is not reliable,
    unless you supply your own sequence of alpha.

        Constant that multiplies the penalty terms. Defaults to 1.0.
        See the notes for the exact mathematical meaning of this
        parameter. ``alpha = 0`` is equivalent to an ordinary least square,
        solved by the :class:`LinearRegression` object. For numerical
        reasons, using ``alpha = 0`` with the ``Lasso`` object is not advised.
        Given this, you should use the :class:`LinearRegression` object.

        The ElasticNet mixing parameter, with ``0 <= l1_ratio <= 1``. For
        ``l1_ratio = 0`` the penalty is an L2 penalty. ``For l1_ratio = 1`` it
        is an L1 penalty.  For ``0 < l1_ratio < 1``, the penalty is a
        combination of L1 and L2.

        Whether the intercept should be estimated or not. If ``False``, the
        data is assumed to be already centered.

    precompute : bool or array-like of shape (n_features, n_features),\
                 default=False
        calculations. The Gram matrix can also be passed as argument.
        For sparse input this option is always ``False`` to preserve sparsity.
        Check :ref:`an example on how to use a precomputed Gram Matrix in ElasticNet
        <sphx_glr_auto_examples_linear_model_plot_elastic_net_precomputed_gram_matrix_with_weighted_samples.py>`

        The tolerance for the optimization: if the updates are
        smaller than ``tol``, the optimization code checks the
        dual gap for optimality and continues until it is smaller
        than ``tol``, see Notes below.

        When set to ``True``, reuse the solution of the previous call to fit as
        initialization, otherwise, just erase the previous solution.

        The seed of the pseudo random number generator that selects a random
        feature to update. Used when ``selection`` == 'random'.

    selection : {'cyclic', 'random'}, default='cyclic'
        If set to 'random', a random coefficient is updated every iteration
        rather than looping over features sequentially by default. This
        (setting to 'random') often leads to significantly faster convergence
        especially when tol is higher than 1e-4.

        Parameter vector (w in the cost function formula).

    sparse_coef_ : sparse matrix of shape (n_features,) or \
            (n_targets, n_features)
        Sparse representation of the `coef_`.

        Independent term in decision function.

    n_iter_ : list of int
        Number of iterations run by the coordinate descent solver to reach
        the specified tolerance.

    dual_gap_ : float or ndarray of shape (n_targets,)
        Given param alpha, the dual gaps at the end of the optimization,
        same shape as each observation of y.

    See Also
    --------
    ElasticNetCV : Elastic net model with best model selection by
    SGDRegressor : Implements elastic net regression with incremental training.
    SGDClassifier : Implements logistic regression with elastic net penalty
        (``SGDClassifier(loss="log_loss", penalty="elasticnet")``).

    Notes
    -----

    The precise stopping criteria based on `tol` are the following: First, check that
    that maximum coordinate update, i.e. :math:`\\max_j |w_j^{new} - w_j^{old}|`
    is smaller than `tol` times the maximum absolute coefficient, :math:`\\max_j |w_j|`.
    If so, then additionally check whether the dual gap is smaller than `tol` times
    :math:`||y||_2^2 / n_{\text{samples}}`.

    Examples
    --------
    >>> from sklearn.linear_model import ElasticNet

    >>> X, y = make_regression(n_features=2, random_state=0)
    >>> regr = ElasticNet(random_state=0)
    ElasticNet(random_state=0)
    >>> print(regr.coef_)
    [18.83816048 64.55968825]
    >>> print(regr.intercept_)
    1.451...
    >>> print(regr.predict([[0, 0]]))
    [1.451...]
    """

        "precompute": ["boolean", "array-like"],
        "selection": [StrOptions({"cyclic", "random"})],
    }

    path = staticmethod(enet_path)

        self,
        *,
        l1_ratio=0.5,
        precompute=False,
        tol=1e-4,
        selection="cyclic",
    ):
        self.precompute = precompute
        self.selection = selection

        """Fit model with coordinate descent.

        X : {ndarray, sparse matrix, sparse array} of (n_samples, n_features)
            Data.

            Note that large sparse matrices and arrays requiring `int64`
            indices are not accepted.

            Target. Will be cast to X's dtype if necessary.

            Sample weights. Internally, the `sample_weight` vector will be
            rescaled to sum to `n_samples`.

            Don't use this parameter unless you know what you do.

        Returns
        -------

        Notes
        -----
        Coordinate descent is an algorithm that considers each column of
        data at a time hence it will automatically convert the X input
        as a Fortran-contiguous numpy array if necessary.

        To avoid memory re-allocation it is advised to allocate the
        initial data in memory directly using that format.
        """
        if self.alpha == 0:
                (
                    "With alpha=0, this algorithm does not converge "
                    "well. You are advised to use the LinearRegression "
                    "estimator"
                ),
            )

        # Remember if X is copied
        X_copied = False
        # We expect X and y to be float64 or float32 Fortran ordered arrays
        # when bypassing checks
            X_copied = self.copy_X and self.fit_intercept
                self,
                X,
                y,
                copy=X_copied,
                multi_output=True,
                y_numeric=True,
            )
                y, order="F", copy=False, dtype=X.dtype.type, ensure_2d=False
            )

        alpha = self.alpha

        if isinstance(sample_weight, numbers.Number):
            sample_weight = None
                sample_weight = _check_sample_weight(sample_weight, X, dtype=X.dtype)
            # TLDR: Rescale sw to sum up to n_samples.
            # Long: The objective function of Enet
            #
            #    1/2 * np.average(squared error, weights=sw)
            #    + alpha * penalty                                             (1)
            #
            # is invariant under rescaling of sw.
            # But enet_path coordinate descent minimizes
            #
            #     1/2 * sum(squared error) + alpha' * penalty                  (2)
            #
            # and therefore sets
            #
            #     alpha' = n_samples * alpha                                   (3)
            #
            # inside its function body, which results in objective (2) being
            # equivalent to (1) in case of no sw.
            # With sw, however, enet_path should set
            #
            #     alpha' = sum(sw) * alpha                                     (4)
            #
            # Therefore, we use the freedom of Eq. (1) to rescale sw before
            # calling enet_path, i.e.
            #
            #     sw *= n_samples / sum(sw)
            #
            # such that sum(sw) = n_samples. This way, (3) and (4) are the same.
            sample_weight = sample_weight * (n_samples / np.sum(sample_weight))
            # Note: Alternatively, we could also have rescaled alpha instead
            # of sample_weight:
            #
            #     alpha *= np.sum(sample_weight) / n_samples

        # Ensure copying happens only once, don't do it again if done above.
        # X and y will be rescaled if sample_weight is not None, order='F'
        # ensures that the returned X and y are still F-contiguous.
        should_copy = self.copy_X and not X_copied
        X, y, X_offset, y_offset, X_scale, precompute, Xy = _pre_fit(
            X,
            y,
            None,
            self.precompute,
            copy=should_copy,
        )
        # coordinate descent needs F-ordered arrays and _pre_fit might have
        # called _rescale_data
        if check_input or sample_weight is not None:
            X, y = _set_order(X, y, order="F")
            y = y[:, np.newaxis]
        if Xy is not None and Xy.ndim == 1:
            Xy = Xy[:, np.newaxis]

        if not self.warm_start or not hasattr(self, "coef_"):
            coef_ = np.zeros((n_targets, n_features), dtype=X.dtype, order="F")
        else:
            coef_ = self.coef_
            if coef_.ndim == 1:
                coef_ = coef_[np.newaxis, :]

        dual_gaps_ = np.zeros(n_targets, dtype=X.dtype)

        for k in range(n_targets):
            if Xy is not None:
                this_Xy = Xy[:, k]
            else:
                this_Xy = None
            _, this_coef, this_dual_gap, this_iter = self.path(
                X,
                y[:, k],
                l1_ratio=self.l1_ratio,
                eps=None,
                n_alphas=None,
                alphas=[alpha],
                precompute=precompute,
                Xy=this_Xy,
                copy_X=True,
                coef_init=coef_[k],
                verbose=False,
                # from here on **params
                X_offset=X_offset,
                X_scale=X_scale,
                selection=self.selection,
            )
            coef_[k] = this_coef[:, 0]
            dual_gaps_[k] = this_dual_gap[0]
            self.n_iter_.append(this_iter[0])

        if n_targets == 1:
            self.n_iter_ = self.n_iter_[0]
            self.coef_ = coef_[0]
            self.dual_gap_ = dual_gaps_[0]
        else:
            self.coef_ = coef_
            self.dual_gap_ = dual_gaps_

        # check for finiteness of coefficients
        if not all(np.isfinite(w).all() for w in [self.coef_, self.intercept_]):
                "Coordinate descent iterations resulted in non-finite parameter"
                " values. The input data may contain large values and need to"
                " be preprocessed."
            )

        # return self for chaining fit and predict calls

    @property
    def sparse_coef_(self):
        """Sparse representation of the fitted `coef_`."""
        return sparse.csr_matrix(self.coef_)

        """Decision function of the linear model.

        X : numpy array or scipy.sparse matrix of shape (n_samples, n_features)

        Returns
        -------
        T : ndarray of shape (n_samples,)
            The predicted decision function.
        """
            return safe_sparse_dot(X, self.coef_.T, dense_output=True) + self.intercept_
        else:
            return super()._decision_function(X)

class Lasso(ElasticNet):
    """Linear Model trained with L1 prior as regularizer (aka the Lasso).

    The optimization objective for Lasso is::

    Technically the Lasso model is optimizing the same objective function as
    the Elastic Net with ``l1_ratio=1.0`` (no L2 penalty).

        Constant that multiplies the L1 term, controlling regularization

        reasons, using `alpha = 0` with the `Lasso` object is not advised.

        to False, no intercept will be used in calculations

        When set to True, reuse the solution of the previous call to fit as

    sparse_coef_ : sparse matrix of shape (n_features, 1) or \
        Readonly property derived from ``coef_``.

    n_iter_ : int or list of int

    See Also
    --------
    lars_path : Regularization path using LARS.
    lasso_path : Regularization path using Lasso.
    LassoLars : Lasso Path along the regularization parameter using LARS algorithm.
    LassoCV : Lasso alpha parameter by cross-validation.
    LassoLarsCV : Lasso least angle parameter algorithm by cross-validation.
    sklearn.decomposition.sparse_encode : Sparse coding array estimator.

    Notes
    -----
    The algorithm used to fit the model is coordinate descent.

    regularization. Alpha corresponds to `1 / (2C)` in other linear

    :math:`||y||_2^2 / n_{\\text{samples}}`.

    The target can be a 2-dimensional array, resulting in the optimization of the
    following objective::

        (1 / (2 * n_samples)) * ||Y - XW||^2_F + alpha * ||W||_11

    where :math:`||W||_{1,1}` is the sum of the magnitude of the matrix coefficients.
    It should not be confused with :class:`~sklearn.linear_model.MultiTaskLasso` which
    instead penalizes the :math:`L_{2,1}` norm of the coefficients, yielding row-wise
    sparsity in the coefficients.

    Examples
    --------
    >>> from sklearn import linear_model
    >>> clf = linear_model.Lasso(alpha=0.1)
    >>> clf.fit([[0,0], [1, 1], [2, 2]], [0, 1, 2])
    Lasso(alpha=0.1)
    >>> print(clf.coef_)
    [0.85 0.  ]
    >>> print(clf.intercept_)
    0.15...
    """

        **ElasticNet._parameter_constraints,
    }
    _parameter_constraints.pop("l1_ratio")

        self,
        *,
        tol=1e-4,
    ):
            l1_ratio=1.0,
            precompute=precompute,
            tol=tol,
            selection=selection,
        )

def _path_residuals(
    X,
    y,
    train,
    test,
    path,
    path_params,
    l1_ratio=1,
    X_order=None,
):
    """Returns the MSE for the models computed by 'path'.

    sample_weight : None or array-like of shape (n_samples,)

    train : list of indices
        The indices of the train set.

    test : list of indices
        The indices of the test set.

    path : callable
        Function returning a list of models on the path. See
        enet_path for an example of signature.

    path_params : dictionary
        Parameters passed to the path function.

        Array of float that is used for cross-validation. If not
        provided, computed using 'path'.

    l1_ratio : float, default=1
        float between 0 and 1 passed to ElasticNet (scaling between
        l1 and l2 penalties). For ``l1_ratio = 0`` the penalty is an
        L2 penalty. For ``l1_ratio = 1`` it is an L1 penalty. For ``0
        < l1_ratio < 1``, the penalty is a combination of L1 and L2.

    X_order : {'F', 'C'}, default=None
        The order of the arrays expected by the path function to
        avoid memory copies.

    dtype : a numpy dtype, default=None
        The dtype of the arrays expected by the path function to
    """
    X_test = X[test]
    y_test = y[test]
        sw_train, sw_test = None, None
    else:
        sw_train = sample_weight[train]
        sw_test = sample_weight[test]
        # TLDR: Rescale sw_train to sum up to n_samples on the training set.
        # See TLDR and long comment inside ElasticNet.fit.
        sw_train *= n_samples / np.sum(sw_train)
        # Note: Alternatively, we could also have rescaled alpha instead
        # of sample_weight:
        #
        #     alpha *= np.sum(sample_weight) / n_samples

    if not sparse.issparse(X):
        for array, array_input in (
            (X_train, X),
            (y_train, y),
            (X_test, X),
            (y_test, y),
        ):
            if array.base is not array_input and not array.flags["WRITEABLE"]:
                # fancy indexing should create a writable copy but it doesn't
                # for read-only memmaps (cf. numpy#14132).
                array.setflags(write=True)

        precompute = path_params["precompute"]
    else:
        # No Gram variant of multi-task exists right now.
        # Fall back to default enet_multitask
        precompute = False

    X_train, y_train, X_offset, y_offset, X_scale, precompute, Xy = _pre_fit(
        X_train,
        y_train,
        None,
        precompute,
        sample_weight=sw_train,
    )

    path_params = path_params.copy()
    path_params["Xy"] = Xy
    path_params["X_offset"] = X_offset
    path_params["X_scale"] = X_scale
    path_params["precompute"] = precompute
    path_params["copy_X"] = False
    path_params["alphas"] = alphas
    # needed for sparse cd solver
    path_params["sample_weight"] = sw_train

    if "l1_ratio" in path_params:
        path_params["l1_ratio"] = l1_ratio

    # Do the ordering and type casting here, as if it is done in the path,
    # X is copied and a reference is kept here
    X_train = check_array(X_train, accept_sparse="csc", dtype=dtype, order=X_order)
    alphas, coefs, _ = path(X_train, y_train, **path_params)
    del X_train, y_train

        # Doing this so that it becomes coherent with multioutput.
        coefs = coefs[np.newaxis, :, :]
        y_offset = np.atleast_1d(y_offset)
        y_test = y_test[:, np.newaxis]

    intercepts = y_offset[:, np.newaxis] - np.dot(X_offset, coefs)
    X_test_coefs = safe_sparse_dot(X_test, coefs)
    residues = X_test_coefs - y_test[:, :, np.newaxis]
    residues += intercepts
        this_mse = (residues**2).mean(axis=0)
    else:
        this_mse = np.average(residues**2, weights=sw_test, axis=0)

    return this_mse.mean(axis=0)

class LinearModelCV(MultiOutputMixin, LinearModel, ABC):
    """Base class for iterative model fitting along a regularization path."""

        "n_alphas": [
        ],
        # TODO(1.9): remove "warn" and None options.
        "alphas": [
            None,
            Hidden(StrOptions({"warn"})),
        ],
        "precompute": [StrOptions({"auto"}), "array-like", "boolean"],
    }

        self,
        eps=1e-3,
        n_alphas="deprecated",
        alphas="warn",
        precompute="auto",
        tol=1e-4,
        cv=None,
    ):
        self.n_alphas = n_alphas

        """Model to be fitted after the best alpha has been determined."""

    def _is_multitask(self):
        """Bool indicating if class is meant for multidimensional target."""

    def path(X, y, **kwargs):
        """Compute path with coordinate descent."""

        """Fit linear model with coordinate descent.

        Fit is on grid of alphas and best alpha estimated by cross-validation.

            Training data. Pass directly as Fortran-contiguous data
            to avoid unnecessary memory duplication. If y is mono-output,
            X can be sparse. Note that large sparse matrices and arrays
            requiring `int64` indices are not accepted.

        sample_weight : float or array-like of shape (n_samples,), \
            Sample weights used for fitting and evaluation of the weighted
            mean squared error of each cv-fold. Note that the cross validated
            MSE that is finally used to find the best model is the unweighted
            mean over the (weighted) MSEs of each test fold.

            Parameters to be passed to the CV splitter.

        Returns
        -------
            Returns an instance of fitted model.
        """

        # TODO(1.9): remove n_alphas and alphas={"warn", None}; set alphas=100 by
        # default. Remove these deprecations messages and use self.alphas directly
        # instead of self._alphas.
        if self.n_alphas == "deprecated":
            self._alphas = 100
        else:
                "'n_alphas' was deprecated in 1.7 and will be removed in 1.9. "
                "'alphas' now accepts an integer value which removes the need to pass "
                "'n_alphas'. The default value of 'alphas' will change from None to "
                "100 in 1.9. Pass an explicit value to 'alphas' and leave 'n_alphas' "
                "to its default value to silence this warning.",
            )
            self._alphas = self.n_alphas

        if isinstance(self.alphas, str) and self.alphas == "warn":
            # - If self.n_alphas == "deprecated", both are left to their default values
            #   so we don't warn since the future default behavior will be the same as
            #   the current default behavior.
            # - If self.n_alphas != "deprecated", then we already warned about it
            #   and the warning message mentions the future self.alphas default, so
            #   no need to warn a second time.
            pass
        elif self.alphas is None:
                "'alphas=None' is deprecated and will be removed in 1.9, at which "
                "point the default value will be set to 100. Set 'alphas=100' "
                "to silence this warning.",
            )
        else:
            self._alphas = self.alphas

        # This makes sure that there is no duplication in memory.
        # Dealing right with copy_X is important in the following:
        # Multiple functions touch X and subsamples of X and can induce a
        # lot of duplication of memory
        copy_X = self.copy_X and self.fit_intercept

        check_y_params = dict(
            copy=False, dtype=[np.float64, np.float32], ensure_2d=False
        )
        if isinstance(X, np.ndarray) or sparse.issparse(X):
            # Keep a reference to X
            reference_to_old_X = X
            # Let us not impose fortran ordering so far: it is
            # not useful for the cross-validation loop and will be done
            # by the model fitting itself

            # csr. We also want to allow y to be 64 or 32 but check_X_y only
            # allows to convert for 64.
            )
            )
                if hasattr(reference_to_old_X, "data") and not np.may_share_memory(
                    reference_to_old_X.data, X.data
                ):
                    # X is a sparse matrix and has been copied
                    copy_X = False
            elif not np.may_share_memory(reference_to_old_X, X):
                # X has been copied
                copy_X = False
            del reference_to_old_X
        else:
                copy=copy_X,
            )
            )
            copy_X = False

        if not self._is_multitask():
            if y.ndim > 1 and y.shape[1] > 1:
                    "For multi-task outputs, use MultiTask%s" % self.__class__.__name__
                )
        else:
                raise TypeError("X should be dense but a sparse matrix waspassed")
            elif y.ndim == 1:
                    "For mono-task outputs, use %sCV" % self.__class__.__name__[9:]
                )

        model = self._get_estimator()

        # All LinearModelCV parameters except 'cv' are acceptable
        path_params = self.get_params()

        # Pop `intercept` that is not parameter of the path function
        path_params.pop("fit_intercept", None)

        if "l1_ratio" in path_params:
            l1_ratios = np.atleast_1d(path_params["l1_ratio"])
            # For the first path, we need to set l1_ratio
            path_params["l1_ratio"] = l1_ratios[0]
        else:
            l1_ratios = [
                1,
            ]
        path_params.pop("cv", None)
        path_params.pop("n_jobs", None)

        n_l1_ratio = len(l1_ratios)

        check_scalar_alpha = partial(
            check_scalar,
            target_type=Real,
        )

        if isinstance(self._alphas, Integral):
            alphas = [
                _alpha_grid(
                    X,
                    y,
                    l1_ratio=l1_ratio,
                    fit_intercept=self.fit_intercept,
                    eps=self.eps,
                    n_alphas=self._alphas,
                    copy_X=self.copy_X,
                )
                for l1_ratio in l1_ratios
            ]
        else:
            # Making sure alphas entries are scalars.
            for index, alpha in enumerate(self._alphas):
                check_scalar_alpha(alpha, f"alphas[{index}]")
            # Making sure alphas is properly ordered.
            alphas = np.tile(np.sort(self._alphas)[::-1], (n_l1_ratio, 1))

        # We want n_alphas to be the number of alphas used for each l1_ratio.
        n_alphas = len(alphas[0])
        path_params.update({"n_alphas": n_alphas})

        path_params["copy_X"] = copy_X
        # We are not computing in parallel, we can modify X
        # inplace in the folds
        if effective_n_jobs(self.n_jobs) > 1:
            path_params["copy_X"] = False

        # init cross-validation generator
        cv = check_cv(self.cv)

            splitter_supports_sample_weight = get_routing_for_object(cv).consumes(
                method="split", params=["sample_weight"]
            )
            if (
                sample_weight is not None
                and not splitter_supports_sample_weight
                and not has_fit_parameter(self, "sample_weight")
            ):
                    "The CV splitter and underlying estimator do not support"
                    " sample weights."
                )

            if splitter_supports_sample_weight:
                params["sample_weight"] = sample_weight

                self, "sample_weight"
            ):
                # MultiTaskElasticNetCV does not (yet) support sample_weight
                sample_weight = None
        else:
            routed_params.splitter = Bunch(split=Bunch())

        # Compute path for all folds and compute MSE to get the best alpha
        folds = list(cv.split(X, y, **routed_params.splitter.split))
        best_mse = np.inf

        # We do a double for loop folded in one, in order to be able to
        # iterate in parallel on l1_ratio and folds
        jobs = (
            delayed(_path_residuals)(
                X,
                y,
                train,
                test,
                self.fit_intercept,
                self.path,
                path_params,
                alphas=this_alphas,
                l1_ratio=this_l1_ratio,
                X_order="F",
                dtype=X.dtype.type,
            )
            for this_l1_ratio, this_alphas in zip(l1_ratios, alphas)
            for train, test in folds
        )
        mse_paths = Parallel(
        )(jobs)
        mse_paths = np.reshape(mse_paths, (n_l1_ratio, len(folds), -1))
        # The mean is computed over folds.
        mean_mse = np.mean(mse_paths, axis=1)
        self.mse_path_ = np.squeeze(np.moveaxis(mse_paths, 2, 1))
        for l1_ratio, l1_alphas, mse_alphas in zip(l1_ratios, alphas, mean_mse):
            i_best_alpha = np.argmin(mse_alphas)
            this_best_mse = mse_alphas[i_best_alpha]
            if this_best_mse < best_mse:
                best_alpha = l1_alphas[i_best_alpha]
                best_l1_ratio = l1_ratio
                best_mse = this_best_mse

        self.l1_ratio_ = best_l1_ratio
            self.alphas_ = np.asarray(alphas)
            if n_l1_ratio == 1:
                self.alphas_ = self.alphas_[0]
        # Remove duplicate alphas in case alphas is provided.
        else:
            self.alphas_ = np.asarray(alphas[0])

        # Refit the model with the parameters selected
        common_params = {
            name: value
            for name, value in self.get_params().items()
            if name in model.get_params()
        }
        model.set_params(**common_params)
        model.alpha = best_alpha
        model.l1_ratio = best_l1_ratio
        model.copy_X = copy_X
        precompute = getattr(self, "precompute", None)
        if isinstance(precompute, str) and precompute == "auto":
            model.precompute = False

            # MultiTaskElasticNetCV does not (yet) support sample_weight, even
            # not sample_weight=None.
            model.fit(X, y)
        else:
            model.fit(X, y, sample_weight=sample_weight)
        if not hasattr(self, "l1_ratio"):
            del self.l1_ratio_
        self.coef_ = model.coef_
        self.intercept_ = model.intercept_
        self.dual_gap_ = model.dual_gap_
        self.n_iter_ = model.n_iter_

        Returns
        -------
        """
        router = (
            .add(
                splitter=check_cv(self.cv),
            )
        )

        multitask = self._is_multitask()
        tags.input_tags.sparse = not multitask
        tags.target_tags.multi_output = multitask

class LassoCV(RegressorMixin, LinearModelCV):
    """Lasso linear model with iterative fitting along a regularization path.

    The best model is selected by cross-validation.

            `n_alphas` was deprecated in 1.7 and will be removed in 1.9. Use `alphas`
            instead.

    alphas : array-like or int, default=None
        Values of alphas to test along the regularization path.
        If int, `alphas` values are generated automatically.
        If array-like, list of alpha values to use.

            `alphas` accepts an integer value which removes the need to pass
            `n_alphas`.

            `alphas=None` was deprecated in 1.7 and will be removed in 1.9, at which
            point the default value will be set to 100.

        than ``tol``.

        - int, to specify the number of folds.

        For int/None inputs, :class:`~sklearn.model_selection.KFold` is used.

        Number of CPUs to use during the cross validation.

        If positive, restrict regression coefficients to be positive.

        The amount of penalization chosen by cross validation.

    mse_path_ : ndarray of shape (n_alphas, n_folds)
        Mean square error for the test set on each fold, varying alpha.

    alphas_ : ndarray of shape (n_alphas,)
        The grid of alphas used for fitting.

        The dual gap at the end of the optimization for the optimal alpha
        (``alpha_``).

        the specified tolerance for the optimal alpha.

    See Also
    --------
    lasso_path : Compute Lasso path with coordinate descent.
        path.

    Notes
    -----

    To avoid unnecessary memory duplication the `X` argument of the `fit`
    method should be directly passed as a Fortran-contiguous numpy array.

    For an example, see :ref:`examples/linear_model/plot_lasso_model_selection.py
    <sphx_glr_auto_examples_linear_model_plot_lasso_model_selection.py>`.

    :class:`LassoCV` leads to different results than a hyperparameter
    search using :class:`~sklearn.model_selection.GridSearchCV` with a
    :class:`Lasso` model. In :class:`LassoCV`, a model for a given
    penalty `alpha` is warm started using the coefficients of the
    closest model (trained at the previous iteration) on the
    regularization path. It tends to speed up the hyperparameter
    search.

    Examples
    --------
    >>> from sklearn.linear_model import LassoCV
    >>> X, y = make_regression(noise=4, random_state=0)
    >>> reg = LassoCV(cv=5, random_state=0).fit(X, y)
    >>> reg.score(X, y)
    0.9993...
    >>> reg.predict(X[:1,])
    array([-78.4951...])
    """

    path = staticmethod(lasso_path)

        self,
        *,
        eps=1e-3,
        tol=1e-4,
        cv=None,
    ):
            eps=eps,
            tol=tol,
            cv=cv,
        )

        return Lasso()

        """Fit Lasso model with coordinate descent.

        Returns
        -------
        """
        return super().fit(X, y, sample_weight=sample_weight, **params)

class ElasticNetCV(RegressorMixin, LinearModelCV):
    """Elastic Net model with iterative fitting along a regularization path.

    l1_ratio : float or list of float, default=0.5
        Float between 0 and 1 passed to ElasticNet (scaling between
        l1 and l2 penalties). For ``l1_ratio = 0``
        the penalty is an L2 penalty. For ``l1_ratio = 1`` it is an L1 penalty.
        For ``0 < l1_ratio < 1``, the penalty is a combination of L1 and L2
        This parameter can be a list, in which case the different
        values are tested by cross-validation and the one giving the best
        prediction score is used. Note that a good choice of list of
        values for l1_ratio is often to put more values close to 1
        (i.e. Lasso) and less close to 0 (i.e. Ridge), as in ``[.1, .5, .7,
        .9, .95, .99, 1]``.

        Number of alphas along the regularization path, used for each l1_ratio.

            instead.

        Values of alphas to test along the regularization path, used for each l1_ratio.

    verbose : bool or int, default=0

    l1_ratio_ : float
        The compromise between l1 and l2 penalization chosen by

    intercept_ : float or ndarray of shape (n_targets, n_features)
        Independent term in the decision function.

    mse_path_ : ndarray of shape (n_l1_ratio, n_alpha, n_folds)
        Mean square error for the test set on each fold, varying l1_ratio and
        alpha.

    alphas_ : ndarray of shape (n_alphas,) or (n_l1_ratio, n_alphas)
        The grid of alphas used for fitting, for each l1_ratio.

    dual_gap_ : float
        The dual gaps at the end of the optimization for the optimal alpha.

    See Also
    --------
    enet_path : Compute elastic net path with coordinate descent.

    Notes
    -----
    In `fit`, once the best parameters `l1_ratio` and `alpha` are found through

    The parameter `l1_ratio` corresponds to alpha in the glmnet R package
    while alpha corresponds to the lambda parameter in glmnet.
    More specifically, the optimization objective is::

        a * L1 + b * L2

    for::

        alpha = a + b and l1_ratio = a / (a + b).

    :ref:`examples/linear_model/plot_lasso_model_selection.py

    Examples
    --------
    >>> from sklearn.linear_model import ElasticNetCV

    >>> regr = ElasticNetCV(cv=5, random_state=0)
    ElasticNetCV(cv=5, random_state=0)
    >>> print(regr.alpha_)
    0.199...
    0.398...
    [0.398...]
    """

        **LinearModelCV._parameter_constraints,
        "l1_ratio": [Interval(Real, 0, 1, closed="both"), "array-like"],
    }

        self,
        *,
        eps=1e-3,
        tol=1e-4,
        cv=None,
    ):

        return ElasticNet()

        """Fit ElasticNet model with coordinate descent.

        Returns
        -------
        """

class MultiTaskElasticNet(Lasso):
    """Multi-task ElasticNet model trained with L1/L2 mixed-norm as regularizer.

    The optimization objective for MultiTaskElasticNet is::

    Where::

        ||W||_21 = sum_i sqrt(sum_j W_ij ^ 2)

    i.e. the sum of norms of each row.

    Read more in the :ref:`User Guide <multi_task_elastic_net>`.

        Constant that multiplies the L1/L2 term. Defaults to 1.0.

        The ElasticNet mixing parameter, with 0 < l1_ratio <= 1.
        For l1_ratio = 1 the penalty is an L1/L2 penalty. For l1_ratio = 0 it
        is an L2 penalty.
        For ``0 < l1_ratio < 1``, the penalty is a combination of L1/L2 and L2.

        Parameter vector (W in the cost function formula). If a 1D y is
        passed in at fit (non multi-task usage), ``coef_`` is then a 1D array.
        Note that ``coef_`` stores the transpose of ``W``, ``W.T``.

        The dual gaps at the end of the optimization.

    eps_ : float
        The tolerance scaled scaled by the variance of the target `y`.

    See Also
    --------
    MultiTaskElasticNetCV : Multi-task L1/L2 ElasticNet with built-in
    MultiTaskLasso : Multi-task Lasso model trained with L1/L2
        mixed-norm as regularizer.

    Notes
    -----

    To avoid unnecessary memory duplication the X and y arguments of the fit
    method should be directly passed as Fortran-contiguous numpy arrays.

    Examples
    --------
    >>> clf = linear_model.MultiTaskElasticNet(alpha=0.1)
    >>> clf.fit([[0,0], [1, 1], [2, 2]], [[0, 0], [1, 1], [2, 2]])
    MultiTaskElasticNet(alpha=0.1)
    [[0.45663524 0.45612256]
     [0.45663524 0.45612256]]
    [0.0872422 0.0872422]
    """

    }
    for param in ("precompute", "positive"):

        self,
        *,
        tol=1e-4,
    ):

        """Fit MultiTaskElasticNet model with coordinate descent.

            Data.
        y : ndarray of shape (n_samples, n_targets)

        Returns
        -------

        Notes
        -----

        """
        # Need to validate separately here.
        # We can't pass multi_output=True because that would allow y to be csr.
        check_X_params = dict(
            copy=self.copy_X and self.fit_intercept,
        )
        check_y_params = dict(ensure_2d=False, order="F")
            self, X, y, validate_separately=(check_X_params, check_y_params)
        )
        y = y.astype(X.dtype)

        if hasattr(self, "l1_ratio"):
            model_str = "ElasticNet"
        else:
            model_str = "Lasso"
            raise ValueError("For mono-task outputs, use %s" % model_str)

            X, y, fit_intercept=self.fit_intercept, copy=False
        )

            self.coef_ = np.zeros(
                (n_targets, n_features), dtype=X.dtype.type, order="F"
            )

        l1_reg = self.alpha * self.l1_ratio * n_samples
        l2_reg = self.alpha * (1.0 - self.l1_ratio) * n_samples

        self.coef_ = np.asfortranarray(self.coef_)  # coef contiguous in memory

        random = self.selection == "random"

        (
            self.coef_,
            self.dual_gap_,
            self.eps_,
        ) = cd_fast.enet_coordinate_descent_multi_task(
            l1_reg,
            l2_reg,
            X,
            y,
            self.tol,
            check_random_state(self.random_state),
            random,
        )

        # account for different objective scaling here and in cd_fast
        self.dual_gap_ /= n_samples

class MultiTaskLasso(MultiTaskElasticNet):
    """Multi-task Lasso model trained with L1/L2 mixed-norm as regularizer.

    Where::

    Read more in the :ref:`User Guide <multi_task_lasso>`.

        Parameter vector (W in the cost function formula).

    dual_gap_ : ndarray of shape (n_alphas,)

    See Also
    --------
    Lasso: Linear Model trained with L1 prior as regularizer (aka the Lasso).
    MultiTaskLassoCV: Multi-task L1 regularized linear model with built-in
    MultiTaskElasticNetCV: Multi-task L1/L2 ElasticNet with built-in cross-validation.

    Notes
    -----

    Examples
    --------
    >>> clf = linear_model.MultiTaskLasso(alpha=0.1)
    >>> clf.fit([[0, 1], [1, 2], [2, 4]], [[0, 0], [1, 1], [2, 3]])
    MultiTaskLasso(alpha=0.1)
    [[0.         0.60809415]
    [0.         0.94592424]]
    [-0.41888636 -0.87382323]
    """

        **MultiTaskElasticNet._parameter_constraints,
    }

        self,
        *,
        tol=1e-4,
    ):
        self.l1_ratio = 1.0

class MultiTaskElasticNetCV(RegressorMixin, LinearModelCV):
    """Multi-task L1/L2 ElasticNet with built-in cross-validation.

        (1 / (2 * n_samples)) * ||Y - XW||^Fro_2

    Where::

            instead.

        Number of CPUs to use during the cross validation. Note that this is
        used only if multiple values for l1_ratio are given.

    mse_path_ : ndarray of shape (n_alphas, n_folds) or \
                (n_l1_ratio, n_alphas, n_folds)

        Best l1_ratio obtained by cross-validation.

        The dual gap at the end of the optimization for the optimal alpha.

    See Also
    --------
    MultiTaskElasticNet : Multi-task L1/L2 ElasticNet with built-in cross-validation.
    MultiTaskLassoCV : Multi-task Lasso model trained with L1 norm
        as regularizer and built-in cross-validation.

    Notes
    -----

    To avoid unnecessary memory duplication the `X` and `y` arguments of the
    `fit` method should be directly passed as Fortran-contiguous numpy arrays.

    Examples
    --------
    >>> clf = linear_model.MultiTaskElasticNetCV(cv=3)
    >>> clf.fit([[0,0], [1, 1], [2, 2]],
    ...         [[0, 0], [1, 1], [2, 2]])
    MultiTaskElasticNetCV(cv=3)
    [[0.52875032 0.46958558]
     [0.52875032 0.46958558]]
    [0.00166409 0.00166409]
    """

    }
    _parameter_constraints.pop("precompute")
    _parameter_constraints.pop("positive")

        self,
        *,
        eps=1e-3,
        tol=1e-4,
        cv=None,
    ):

        return MultiTaskElasticNet()

    # This is necessary as LinearModelCV now supports sample_weight while
    # MultiTaskElasticNetCV does not (yet).

            Training target variable. Will be cast to X's dtype if necessary.

        Returns
        -------
            Returns MultiTaskElasticNet instance.
        """
        return super().fit(X, y, **params)

class MultiTaskLassoCV(RegressorMixin, LinearModelCV):

    The optimization objective for MultiTaskLasso is::

        (1 / (2 * n_samples)) * ||Y - XW||^Fro_2 + alpha * ||W||_21

    Where::

            instead.

    See Also
    --------
    MultiTaskElasticNet : Multi-task ElasticNet model trained with L1/L2

    Notes
    -----

    Examples
    --------
    >>> from sklearn.linear_model import MultiTaskLassoCV
    >>> X, y = make_regression(n_targets=2, noise=4, random_state=0)
    >>> reg = MultiTaskLassoCV(cv=5, random_state=0).fit(X, y)
    >>> r2_score(y, reg.predict(X))
    0.9994...
    >>> reg.alpha_
    np.float64(0.5713...)
    array([[153.7971...,  94.9015...]])
    """

    }

        self,
        *,
        eps=1e-3,
        tol=1e-4,
        cv=None,
    ):
            eps=eps,
            tol=tol,
            cv=cv,
        )

        return MultiTaskLasso()

    # MultiTaskLassoCV does not (yet).
        """Fit MultiTaskLasso model with coordinate descent.

            Data.

        Returns
        -------
        """

        self,
        *,
        tol=1e-4,
    ):

            Data.

        Returns
        -------

        Notes
        -----

        """
                (
                ),
            )

                self,
                X,
                y,
            )
            )

            #
            #
            #
            #
            #
            #
            #
            #
            #
            #
            #

            X,
            y,
            None,
        )

        else:

            else:
                X,
                y[:, k],
                eps=None,
            )

        else:

            )

        Returns
        -------
        """
        else:

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        self,
        eps=1e-3,
        tol=1e-4,
        cv=None,
    ):

        Returns
        -------
        """

        else:
            )

            pass
            )
        else:

        )

            )
            )
                ):
        else:
            )
            )

                )
        else:
                )

        else:
                1,
            ]

        )

            alphas = [
                    X,
                    y,
                )
            ]
        else:

            )
            if (
            ):
                )

            ):
        else:

        jobs = (
                X,
                y,
                train,
                test,
            )
        )
        )(jobs)

        else:

        }

        else:

        Returns
        -------
        """
        router = (
            .add(
            )
        )

        self,
        *,
        eps=1e-3,
        tol=1e-4,
        cv=None,
    ):
            eps=eps,
            tol=tol,
            cv=cv,
        )

        Returns
        -------
        """

        self,
        *,
        eps=1e-3,
        tol=1e-4,
        cv=None,
    ):

        Returns
        -------
        """

        self,
        *,
        tol=1e-4,
    ):

            Data.

        Returns
        -------

        Notes
        -----

        """
        )
        )

        else:

        )

            )

        (
            l1_reg,
            l2_reg,
            X,
            y,
            self.tol,
            random,
        )

        self,
        *,
        tol=1e-4,
    ):

        self,
        *,
        eps=1e-3,
        tol=1e-4,
        cv=None,
    ):

        Returns
        -------
        """

        self,
        *,
        eps=1e-3,
        tol=1e-4,
        cv=None,
    ):
            eps=eps,
            tol=tol,
            cv=cv,
        )

            Data.

        Returns
        -------
        """

class BayesianRidge(RegressorMixin, LinearModel):
    """Bayesian ridge regression.

    Fit a Bayesian ridge model. See the Notes section for details on this
    implementation and the optimization of the regularization parameters
    lambda (precision of the weights) and alpha (precision of the noise).

    Read more in the :ref:`User Guide <bayesian_regression>`.
    For an intuitive visualization of how the sinusoid is approximated by
    a polynomial using different pairs of initial values, see
    :ref:`sphx_glr_auto_examples_linear_model_plot_bayesian_ridge_curvefit.py`.

        stopping independently of any early stopping criterion.

        Stop the algorithm if w has converged.

    alpha_1 : float, default=1e-6
        Hyper-parameter : shape parameter for the Gamma distribution prior
        over the alpha parameter.

    alpha_2 : float, default=1e-6
        Hyper-parameter : inverse scale parameter (rate parameter) for the
        Gamma distribution prior over the alpha parameter.

    lambda_1 : float, default=1e-6
        over the lambda parameter.

    lambda_2 : float, default=1e-6
        Gamma distribution prior over the lambda parameter.

    alpha_init : float, default=None
        Initial value for alpha (precision of the noise).
        If not set, alpha_init is 1/Var(y).

    lambda_init : float, default=None
        Initial value for lambda (precision of the weights).
        If not set, lambda_init is 1.

    compute_score : bool, default=False
        If True, compute the log marginal likelihood at each iteration of the

        Whether to calculate the intercept for this model.
        The intercept is not treated as a probabilistic parameter
        and thus has no associated variance. If set

        Verbose mode when fitting the model.

    coef_ : array-like of shape (n_features,)
        Coefficients of the regression model (mean of distribution)

        `fit_intercept = False`.

       Estimated precision of the noise.

    lambda_ : float
       Estimated precision of the weights.

    sigma_ : array-like of shape (n_features, n_features)
        Estimated variance-covariance matrix of the weights

    scores_ : array-like of shape (n_iter_+1,)
        If computed_score is True, value of the log marginal likelihood (to be
        maximized) at each iteration of the optimization. The array starts
        with the value of the log marginal likelihood obtained for the initial
        values of alpha and lambda and ends with the value obtained for the
        estimated alpha and lambda.

        The actual number of iterations to reach the stopping criterion.

    X_offset_ : ndarray of shape (n_features,)
        If `fit_intercept=True`, offset subtracted for centering data to a
        zero mean. Set to np.zeros(n_features) otherwise.

    X_scale_ : ndarray of shape (n_features,)
        Set to np.ones(n_features).

    See Also
    --------
    ARDRegression : Bayesian ARD regression.

    Notes
    -----
    There exist several strategies to perform Bayesian ridge regression. This
    implementation is based on the algorithm described in Appendix A of
    (Tipping, 2001) where updates of the regularization parameters are done as
    suggested in (MacKay, 1992). Note that according to A New
    View of Automatic Relevance Determination (Wipf and Nagarajan, 2008) these
    update rules do not guarantee that the marginal likelihood is increasing
    between two consecutive iterations of the optimization.

    D. J. C. MacKay, Bayesian Interpolation, Computation and Neural Systems,
    Vol. 4, No. 3, 1992.

    M. E. Tipping, Sparse Bayesian Learning and the Relevance Vector Machine,
    Journal of Machine Learning Research, Vol. 1, 2001.

    Examples
    --------
    >>> clf = linear_model.BayesianRidge()
    BayesianRidge()
    >>> clf.predict([[1, 1]])
    array([1.])
    """

        "tol": [Interval(Real, 0, None, closed="neither")],
        "alpha_1": [Interval(Real, 0, None, closed="left")],
        "alpha_2": [Interval(Real, 0, None, closed="left")],
        "lambda_1": [Interval(Real, 0, None, closed="left")],
        "lambda_2": [Interval(Real, 0, None, closed="left")],
        "alpha_init": [None, Interval(Real, 0, None, closed="left")],
        "lambda_init": [None, Interval(Real, 0, None, closed="left")],
        "compute_score": ["boolean"],
    }

        self,
        *,
        tol=1.0e-3,
        alpha_1=1.0e-6,
        alpha_2=1.0e-6,
        lambda_1=1.0e-6,
        lambda_2=1.0e-6,
        alpha_init=None,
        lambda_init=None,
        compute_score=False,
    ):
        self.alpha_1 = alpha_1
        self.alpha_2 = alpha_2
        self.lambda_1 = lambda_1
        self.lambda_2 = lambda_2
        self.alpha_init = alpha_init
        self.lambda_init = lambda_init
        self.compute_score = compute_score

        sample_weight : ndarray of shape (n_samples,), default=None
            Individual weights for each sample.

               parameter *sample_weight* support to BayesianRidge.

        Returns
        -------
        """
            self,
            X,
            y,
        )
        dtype = X.dtype

        sw_sum = n_samples
        y_var = y.var()
            sample_weight = _check_sample_weight(sample_weight, X, dtype=dtype)
            y_mean = np.average(y, weights=sample_weight)
            y_var = np.average((y - y_mean) ** 2, weights=sample_weight)

        X, y, X_offset_, y_offset_, X_scale_ = _preprocess_data(
            X,
            y,
        )

            # Sample weight can be implemented via a simple rescaling.
            X, y, _ = _rescale_data(X, y, sample_weight)

        self.X_offset_ = X_offset_
        self.X_scale_ = X_scale_

        # Initialization of the values of the parameters
        eps = np.finfo(np.float64).eps
        # Add `eps` in the denominator to omit division by zero
        alpha_ = self.alpha_init
        lambda_ = self.lambda_init
        if alpha_ is None:
            alpha_ = 1.0 / (y_var + eps)
        if lambda_ is None:
            lambda_ = 1.0

        # Avoid unintended type promotion to float64 with numpy 2
        alpha_ = np.asarray(alpha_, dtype=dtype)
        lambda_ = np.asarray(lambda_, dtype=dtype)

        lambda_1 = self.lambda_1
        lambda_2 = self.lambda_2
        alpha_1 = self.alpha_1
        alpha_2 = self.alpha_2

        self.scores_ = list()
        coef_old_ = None

        XT_y = np.dot(X.T, y)
        # Let M, N = n_samples, n_features and K = min(M, N).
        # The posterior covariance matrix needs Vh_full: (N, N).
        # The full SVD is only required when n_samples < n_features.
        # When n_samples < n_features, K=M and full_matrices=True
        # U: (M, M), S: M, Vh_full: (N, N), Vh: (M, N)
        # When n_samples > n_features, K=N and full_matrices=False
        # U: (M, N), S: N, Vh_full: (N, N), Vh: (N, N)
        U, S, Vh_full = linalg.svd(X, full_matrices=(n_samples < n_features))
        K = len(S)
        eigen_vals_ = S**2
        eigen_vals_full = np.zeros(n_features, dtype=dtype)
        eigen_vals_full[0:K] = eigen_vals_
        Vh = Vh_full[0:K, :]

        # Convergence loop of the bayesian ridge regression
        for iter_ in range(self.max_iter):
            # update posterior mean coef_ based on alpha_ and lambda_ and
            # compute corresponding sse (sum of squared errors)
            coef_, sse_ = self._update_coef_(
                X, y, n_samples, n_features, XT_y, U, Vh, eigen_vals_, alpha_, lambda_
            )
            if self.compute_score:
                # compute the log marginal likelihood
                s = self._log_marginal_likelihood(
                    n_samples,
                    n_features,
                    sw_sum,
                    eigen_vals_,
                    alpha_,
                    lambda_,
                    coef_,
                    sse_,
                )
                self.scores_.append(s)

            # Update alpha and lambda according to (MacKay, 1992)
            gamma_ = np.sum((alpha_ * eigen_vals_) / (lambda_ + alpha_ * eigen_vals_))
            lambda_ = (gamma_ + 2 * lambda_1) / (np.sum(coef_**2) + 2 * lambda_2)
            alpha_ = (sw_sum - gamma_ + 2 * alpha_1) / (sse_ + 2 * alpha_2)

            # Check for convergence
            if iter_ != 0 and np.sum(np.abs(coef_old_ - coef_)) < self.tol:
                    print("Convergence after ", str(iter_), " iterations")
                break
            coef_old_ = np.copy(coef_)

        self.n_iter_ = iter_ + 1

        # return regularization parameters and corresponding posterior mean,
        # log marginal likelihood and posterior covariance
        self.alpha_ = alpha_
        self.lambda_ = lambda_
        self.coef_, sse_ = self._update_coef_(
            X, y, n_samples, n_features, XT_y, U, Vh, eigen_vals_, alpha_, lambda_
        )
        if self.compute_score:
            # compute the log marginal likelihood
            s = self._log_marginal_likelihood(
                n_features,
                sw_sum,
                eigen_vals_,
                alpha_,
                lambda_,
                coef_,
                sse_,
            )
            self.scores_.append(s)
            self.scores_ = np.array(self.scores_)

        # posterior covariance
        self.sigma_ = np.dot(
            Vh_full.T, Vh_full / (alpha_ * eigen_vals_full + lambda_)[:, np.newaxis]
        )

        self._set_intercept(X_offset_, y_offset_, X_scale_)

        """Predict using the linear model.

        In addition to the mean of the predictive distribution, also its
        standard deviation can be returned.

            Samples.

        Returns
        -------
        y_mean : array-like of shape (n_samples,)
            Mean of predictive distribution of query points.

        y_std : array-like of shape (n_samples,)
        """
        y_mean = self._decision_function(X)
        if not return_std:
            return y_mean
        else:
            sigmas_squared_data = (np.dot(X, self.sigma_) * X).sum(axis=1)
            y_std = np.sqrt(sigmas_squared_data + (1.0 / self.alpha_))
            return y_mean, y_std

    def _update_coef_(
        self, X, y, n_samples, n_features, XT_y, U, Vh, eigen_vals_, alpha_, lambda_
    ):
        """Update posterior mean and compute corresponding sse (sum of squared errors).

        Posterior mean is given by coef_ = scaled_sigma_ * X.T * y where
        scaled_sigma_ = (lambda_/alpha_ * np.eye(n_features)
                         + np.dot(X.T, X))^-1
        """

        if n_samples > n_features:
            coef_ = np.linalg.multi_dot(
                [Vh.T, Vh / (eigen_vals_ + lambda_ / alpha_)[:, np.newaxis], XT_y]
            )
        else:
                [X.T, U / (eigen_vals_ + lambda_ / alpha_)[None, :], U.T, y]
            )

        # Note: we do not need to explicitly use the weights in this sum because
        # y and X were preprocessed by _rescale_data to handle the weights.
        sse_ = np.sum((y - np.dot(X, coef_)) ** 2)

        return coef_, sse_

    def _log_marginal_likelihood(
        self, n_samples, n_features, sw_sum, eigen_vals, alpha_, lambda_, coef, sse
    ):
        """Log marginal likelihood."""

        # compute the log of the determinant of the posterior covariance.
        # posterior covariance is given by
        # sigma = (lambda_ * np.eye(n_features) + alpha_ * np.dot(X.T, X))^-1
            logdet_sigma = -np.sum(np.log(lambda_ + alpha_ * eigen_vals))
        else:
            logdet_sigma = np.full(n_features, lambda_, dtype=np.array(lambda_).dtype)
            logdet_sigma[:n_samples] += alpha_ * eigen_vals
            logdet_sigma = -np.sum(np.log(logdet_sigma))

        score = lambda_1 * log(lambda_) - lambda_2 * lambda_
        score += alpha_1 * log(alpha_) - alpha_2 * alpha_
        score += 0.5 * (
            n_features * log(lambda_)
            + sw_sum * log(alpha_)
            - alpha_ * sse
            - lambda_ * np.sum(coef**2)
            + logdet_sigma
            - sw_sum * log(2 * np.pi)
        )

class ARDRegression(RegressorMixin, LinearModel):
    """Bayesian ARD regression.

    Fit the weights of a regression model, using an ARD prior. The weights of
    the regression model are assumed to be in Gaussian distributions.
    Also estimate the parameters lambda (precisions of the distributions of the
    weights) and alpha (precision of the distribution of the noise).
    The estimation is done by an iterative procedures (Evidence Maximization)

        If True, compute the objective function at each step of the model.

    threshold_lambda : float, default=10 000
        Threshold for removing (pruning) weights with high precision from
        the computation.

       estimated precision of the noise.

    lambda_ : array-like of shape (n_features,)
       estimated precisions of the weights.

        estimated variance-covariance matrix of the weights

    scores_ : float
        if computed, value of the objective function (to be maximized)

    X_offset_ : float

    X_scale_ : float

    See Also
    --------
    BayesianRidge : Bayesian ridge regression.

    Notes
    -----
    For an example, see :ref:`examples/linear_model/plot_ard.py
    <sphx_glr_auto_examples_linear_model_plot_ard.py>`.

    D. J. C. MacKay, Bayesian nonlinear modeling for the prediction
    competition, ASHRAE Transactions, 1994.

    R. Salakhutdinov, Lecture notes on Statistical Machine Learning,
    http://www.utstat.toronto.edu/~rsalakhu/sta4273/notes/Lecture2.pdf#page=15
    Their beta is our ``self.alpha_``
    Their alpha is our ``self.lambda_``
    ARD is a little different than the slide: only dimensions/features for
    which ``self.lambda_ < self.threshold_lambda`` are kept and the rest are
    discarded.

    Examples
    --------
    >>> clf = linear_model.ARDRegression()
    ARDRegression()
    """

        "threshold_lambda": [Interval(Real, 0, None, closed="left")],
    }

        self,
        *,
        threshold_lambda=1.0e4,
    ):
        self.threshold_lambda = threshold_lambda

        Iterative procedure to maximize the evidence

            Target values (integers). Will be cast to X's dtype if necessary.

        Returns
        -------
        """
            self,
            X,
            y,
        )

        coef_ = np.zeros(n_features, dtype=dtype)

            X, y, fit_intercept=self.fit_intercept, copy=self.copy_X
        )

        # Launch the convergence loop
        keep_lambda = np.ones(n_features, dtype=bool)

        # Add `eps` in the denominator to omit division by zero if `np.var(y)`
        # is zero.
        # Explicitly set dtype to avoid unintended type promotion with numpy 2.
        alpha_ = np.asarray(1.0 / (np.var(y) + eps), dtype=dtype)
        lambda_ = np.ones(n_features, dtype=dtype)

        def update_coeff(X, y, coef_, alpha_, keep_lambda, sigma_):
            coef_[keep_lambda] = alpha_ * np.linalg.multi_dot(
                [sigma_, X[:, keep_lambda].T, y]
            )
            return coef_

        update_sigma = (
            self._update_sigma
            if n_samples >= n_features
            else self._update_sigma_woodbury
        )
        # Iterative procedure of ARDRegression
            sigma_ = update_sigma(X, alpha_, lambda_, keep_lambda)
            coef_ = update_coeff(X, y, coef_, alpha_, keep_lambda, sigma_)

            # Update alpha and lambda
            sse_ = np.sum((y - np.dot(X, coef_)) ** 2)
            gamma_ = 1.0 - lambda_[keep_lambda] * np.diag(sigma_)
            lambda_[keep_lambda] = (gamma_ + 2.0 * lambda_1) / (
                (coef_[keep_lambda]) ** 2 + 2.0 * lambda_2
            )
            alpha_ = (n_samples - gamma_.sum() + 2.0 * alpha_1) / (sse_ + 2.0 * alpha_2)

            # Prune the weights with a precision over a threshold
            keep_lambda = lambda_ < self.threshold_lambda
            coef_[~keep_lambda] = 0

            # Compute the objective function
                s = (lambda_1 * np.log(lambda_) - lambda_2 * lambda_).sum()
                s += alpha_1 * log(alpha_) - alpha_2 * alpha_
                s += 0.5 * (
                    fast_logdet(sigma_)
                    + n_samples * log(alpha_)
                    + np.sum(np.log(lambda_))
                )
                s -= 0.5 * (alpha_ * sse_ + (lambda_ * coef_**2).sum())

            if iter_ > 0 and np.sum(np.abs(coef_old_ - coef_)) < self.tol:
                    print("Converged after %s iterations" % iter_)
                break

            if not keep_lambda.any():
                break

        if keep_lambda.any():
            # update sigma and mu using updated params from the last iteration
        else:
            sigma_ = np.array([]).reshape(0, 0)

        self.coef_ = coef_
        self.sigma_ = sigma_

    def _update_sigma_woodbury(self, X, alpha_, lambda_, keep_lambda):
        # See slides as referenced in the docstring note
        # this function is used when n_samples < n_features and will invert
        # a matrix of shape (n_samples, n_samples) making use of the
        # woodbury formula:
        # https://en.wikipedia.org/wiki/Woodbury_matrix_identity
        X_keep = X[:, keep_lambda]
        inv_lambda = 1 / lambda_[keep_lambda].reshape(1, -1)
        sigma_ = pinvh(
            np.eye(n_samples, dtype=X.dtype) / alpha_
            + np.dot(X_keep * inv_lambda, X_keep.T)
        )
        sigma_ = np.dot(sigma_, X_keep * inv_lambda)
        sigma_ = -np.dot(inv_lambda.reshape(-1, 1) * X_keep.T, sigma_)
        sigma_[np.diag_indices(sigma_.shape[1])] += 1.0 / lambda_[keep_lambda]
        return sigma_

    def _update_sigma(self, X, alpha_, lambda_, keep_lambda):
        # this function is used when n_samples >= n_features and will
        # invert a matrix of shape (n_features, n_features)
        gram = np.dot(X_keep.T, X_keep)
        eye = np.eye(gram.shape[0], dtype=X.dtype)
        sigma_inv = lambda_[keep_lambda] * eye + alpha_ * gram
        sigma_ = pinvh(sigma_inv)

            Samples.

        Returns
        -------

        """
        if return_std is False:
        else:
            col_index = self.lambda_ < self.threshold_lambda
            X = _safe_indexing(X, indices=col_index, axis=1)

        self,
        *,
    ):

        Returns
        -------
        """
            self,
            X,
            y,
        )

            X,
            y,
        )

        K = len(S)

            )
                    sw_sum,
                    alpha_,
                    lambda_,
                    coef_,
                    sse_,
                )

                break

        )
                sw_sum,
                alpha_,
                lambda_,
                coef_,
                sse_,
            )

        )

            Samples.

        Returns
        -------

        """
        else:

    ):

        """

            )
        else:
            )

    ):

        else:

        )

        self,
        *,
    ):

        Returns
        -------
        """
            self,
            X,
            y,
        )

        )

        # is zero.

            )

        )

            )

                s += 0.5 * (
                )

                break

                break

        else:

        )

            Samples.

        Returns
        -------

        """
        else:

            )

def make_dataset(X, y, sample_weight, random_state=None):
    """Create ``Dataset`` abstraction for sparse and dense inputs.

    This also returns the ``intercept_decay`` which is different
    for sparse datasets.

        Training data

    y : array-like, shape (n_samples, )

    sample_weight : numpy array of shape (n_samples,)
        The weight of each sample

    random_state : int, RandomState instance or None (default)
        Determines random number generation for dataset random sampling. It is not
        used for dataset shuffling.

    Returns
    -------
    dataset
        The ``Dataset`` abstraction
    intercept_decay
        The intercept decay
    """

    # seed should never be 0 in SequentialDataset64
    seed = rng.randint(1, np.iinfo(np.int32).max)

        CSRData = CSRDataset32
        ArrayData = ArrayDataset32
    else:
        CSRData = CSRDataset64
        ArrayData = ArrayDataset64

        dataset = CSRData(X.data, X.indptr, X.indices, y, sample_weight, seed=seed)
        intercept_decay = SPARSE_INTERCEPT_DECAY
    else:
        X = np.ascontiguousarray(X)
        dataset = ArrayData(X, y, sample_weight, seed=seed)
        intercept_decay = 1.0

    return dataset, intercept_decay

def _preprocess_data(
    X,
    y,
    *,
    copy_y=True,
):
    """Common data preprocessing for fitting linear models.

    This helper is in charge of the following steps:

    - Ensure that `sample_weight` is an array or `None`.
    - If `check_input=True`, perform standard input validation of `X`, `y`.
    - Perform copies if requested to avoid side-effects in case of inplace
      modifications of the input.

    Then, if `fit_intercept=True` this preprocessing centers both `X` and `y` as
    follows:
        - if `X` is dense, center the data and
        store the mean vector in `X_offset`.
        - if `X` is sparse, store the mean in `X_offset`
        without centering `X`. The centering is expected to be handled by the
        linear solver where appropriate.
        - in either case, always center `y` and store the mean in `y_offset`.
        - both `X_offset` and `y_offset` are always weighted by `sample_weight`
          if not set to `None`.

    If `fit_intercept=False`, no centering is performed and `X_offset`, `y_offset`
    are set to zero.

    Returns
    -------
    X_out : {ndarray, sparse matrix} of shape (n_samples, n_features)
        If copy=True a copy of the input X is triggered, otherwise operations are
        inplace.
        If input X is dense, then X_out is centered.
    y_out : {ndarray, sparse matrix} of shape (n_samples,) or (n_samples, n_targets)
        Centered version of y. Possibly performed inplace on input y depending
        on the copy_y parameter.
    X_offset : ndarray of shape (n_features,)
        The mean per column of input X.
    y_offset : float or ndarray of shape (n_features,)
    X_scale : ndarray of shape (n_features,)
        Always an array of ones. TODO: refactor the code base to make it
        possible to remove this unused variable.
    """
    xp, _, device_ = get_namespace_and_device(X, y, sample_weight)
    X_is_sparse = sp.issparse(X)

    if isinstance(sample_weight, numbers.Number):
        sample_weight = xp.asarray(sample_weight)

            X, copy=copy, accept_sparse=["csr", "csc"], dtype=supported_float_dtypes(xp)
        )
        y = check_array(y, dtype=X.dtype, copy=copy_y, ensure_2d=False)
    else:
        y = xp.astype(y, X.dtype, copy=copy_y)
        if copy:
            if X_is_sparse:
                X = X.copy()
            else:
                X = _asarray_with_order(X, order="K", copy=True, xp=xp)

    dtype_ = X.dtype

            X_offset, X_var = mean_variance_axis(X, axis=0, weights=sample_weight)
        else:
            X_offset = _average(X, axis=0, weights=sample_weight, xp=xp)

            X_offset = xp.astype(X_offset, X.dtype, copy=False)
            X -= X_offset

        y_offset = _average(y, axis=0, weights=sample_weight, xp=xp)
        y -= y_offset
    else:
        X_offset = xp.zeros(n_features, dtype=X.dtype, device=device_)
            y_offset = xp.asarray(0.0, dtype=dtype_, device=device_)
        else:
            y_offset = xp.zeros(y.shape[1], dtype=dtype_, device=device_)

    # XXX: X_scale is no longer needed. It is an historic artifact from the
    # time where linear model exposed the normalize parameter.
    X_scale = xp.ones(n_features, dtype=X.dtype, device=device_)
    return X, y, X_offset, y_offset, X_scale

def _rescale_data(X, y, sample_weight, inplace=False):
    """Rescale data sample-wise by square root of sample_weight.

    For many linear models, this enables easy support for sample_weight because

        (y - X w)' S (y - X w)

    with S = diag(sample_weight) becomes

        ||y_rescaled - X_rescaled w||_2^2

    when setting

        y_rescaled = sqrt(S) y
        X_rescaled = sqrt(S) X

    Returns
    -------
    X_rescaled : {array-like, sparse matrix}

    y_rescaled : {array-like, sparse matrix}
    """
    # Assume that _validate_data and _check_sample_weight have been called by
    # the caller.
    xp, _ = get_namespace(X, y, sample_weight)
    sample_weight_sqrt = xp.sqrt(sample_weight)

    if sp.issparse(X) or sp.issparse(y):
        sw_matrix = sparse.dia_matrix(
            (sample_weight_sqrt, 0), shape=(n_samples, n_samples)
        )

        X = safe_sparse_dot(sw_matrix, X)
    else:
        if inplace:
            X *= sample_weight_sqrt[:, None]
        else:
            X = X * sample_weight_sqrt[:, None]

    if sp.issparse(y):
        y = safe_sparse_dot(sw_matrix, y)
    else:
                y *= sample_weight_sqrt
            else:
                y *= sample_weight_sqrt[:, None]
        else:
                y = y * sample_weight_sqrt
            else:
                y = y * sample_weight_sqrt[:, None]
    return X, y, sample_weight_sqrt

class LinearModel(BaseEstimator, metaclass=ABCMeta):
    """Base class for Linear Models"""

        """Fit model."""

        X = validate_data(self, X, accept_sparse=["csr", "csc", "coo"], reset=False)
        coef_ = self.coef_
        if coef_.ndim == 1:
            return X @ coef_ + self.intercept_
        else:
            return X @ coef_.T + self.intercept_

        """
        Predict using the linear model.

        X : array-like or sparse matrix, shape (n_samples, n_features)
            Samples.

        Returns
        -------
        C : array, shape (n_samples,)
        """
        return self._decision_function(X)

    def _set_intercept(self, X_offset, y_offset, X_scale):
        """Set the intercept_"""

        xp, _ = get_namespace(X_offset, y_offset, X_scale)

            # We always want coef_.dtype=X.dtype. For instance, X.dtype can differ from
            # coef_.dtype if warm_start=True.
            coef_ = xp.astype(self.coef_, X_scale.dtype, copy=False)
            coef_ = self.coef_ = xp.divide(coef_, X_scale)

                intercept_ = y_offset - X_offset @ coef_
            else:
                intercept_ = y_offset - X_offset @ coef_.T

            self.intercept_ = intercept_

        else:
            self.intercept_ = 0.0

class LinearClassifierMixin(ClassifierMixin):
    """Mixin for linear classifiers.

    Handles prediction for sparse and dense X.
    """

        """
        Predict confidence scores for samples.

        The confidence score for a sample is proportional to the signed
        distance of that sample to the hyperplane.

            The data matrix for which we want to get the confidence scores.

        Returns
        -------
        scores : ndarray of shape (n_samples,) or (n_samples, n_classes)
            Confidence scores per `(n_samples, n_classes)` combination. In the
            binary case, confidence score for `self.classes_[1]` where >0 means
            this class would be predicted.
        """

        scores = safe_sparse_dot(X, self.coef_.T, dense_output=True) + self.intercept_
        return (
            xp.reshape(scores, (-1,))
            if (scores.ndim > 1 and scores.shape[1] == 1)
            else scores
        )

        """
        Predict class labels for samples in X.

            The data matrix for which we want to get the predictions.

        Returns
        -------
            Vector containing the class labels for each sample.
        """
        scores = self.decision_function(X)
        if len(scores.shape) == 1:
            indices = xp.astype(scores > 0, indexing_dtype(xp))
        else:
            indices = xp.argmax(scores, axis=1)

        return xp.take(self.classes_, indices, axis=0)

    def _predict_proba_lr(self, X):
        """Probability estimation for OvR logistic regression.

        Positive class probabilities are computed as
        1. / (1. + np.exp(-self.decision_function(X)));
        multiclass is handled by normalizing that over all classes.
        """
        prob = self.decision_function(X)
        expit(prob, out=prob)
        if prob.ndim == 1:
            return np.vstack([1 - prob, prob]).T
        else:
            # OvR normalization, like LibLinear's predict_probability
            prob /= prob.sum(axis=1).reshape((prob.shape[0], -1))
            return prob

class SparseCoefMixin:
    """Mixin for converting coef_ to and from CSR format.

    L1-regularizing estimators should inherit this.
    """

    def densify(self):
        """
        Convert coefficient matrix to dense array format.

        Converts the ``coef_`` member (back) to a numpy.ndarray. This is the
        default format of ``coef_`` and is required for fitting, so calling
        this method is only required on models that have previously been
        sparsified; otherwise, it is a no-op.

        Returns
        -------
        self
        """
        msg = "Estimator, %(name)s, must be fitted before densifying."
        check_is_fitted(self, msg=msg)
        if sp.issparse(self.coef_):
            self.coef_ = self.coef_.toarray()

    def sparsify(self):
        """
        Convert coefficient matrix to sparse format.

        Converts the ``coef_`` member to a scipy.sparse matrix, which for
        L1-regularized models can be much more memory- and storage-efficient
        than the usual numpy.ndarray representation.

        The ``intercept_`` member is not converted.

        Returns
        -------
        self

        Notes
        -----
        For non-sparse models, i.e. when there are not many zeros in ``coef_``,
        this may actually *increase* memory usage, so use this method with
        care. A rule of thumb is that the number of zero elements, which can
        be computed with ``(coef_ == 0).sum()``, must be more than 50% for this
        to provide significant benefits.

        After calling this method, further fitting with the partial_fit
        method (if any) will not work until you call densify.
        """
        msg = "Estimator, %(name)s, must be fitted before sparsifying."
        self.coef_ = sp.csr_matrix(self.coef_)

class LinearRegression(MultiOutputMixin, RegressorMixin, LinearModel):
    """
    Ordinary least squares Linear Regression.

    LinearRegression fits a linear model with coefficients w = (w1, ..., wp)
    to minimize the residual sum of squares between the observed targets in
    the dataset, and the targets predicted by the linear approximation.

        specifies a different convergence criterion for the `lsqr` solver.
        `tol` is set as `atol` and `btol` of `scipy.sparse.linalg.lsqr` when
        fitting on sparse training data. This parameter has no effect when fitting
        on dense data.

        The number of jobs to use for the computation. This will only provide
        speedup in case of sufficiently large problems, that is if firstly
        `n_targets > 1` and secondly `X` is sparse or if `positive` is set
        to `True`. ``None`` means 1 unless in a

        When set to ``True``, forces the coefficients to be positive. This
        option is only supported for dense arrays.

    coef_ : array of shape (n_features, ) or (n_targets, n_features)
        Estimated coefficients for the linear regression problem.
        If multiple targets are passed during the fit (y 2D), this
        is a 2D array of shape (n_targets, n_features), while if only
        one target is passed, this is a 1D array of length n_features.

    rank_ : int
        Rank of matrix `X`. Only available when `X` is dense.

    singular_ : array of shape (min(X, y),)
        Singular values of `X`. Only available when `X` is dense.

    intercept_ : float or array of shape (n_targets,)
        Independent term in the linear model. Set to 0.0 if

    See Also
    --------
    Ridge : Ridge regression addresses some of the
        problems of Ordinary Least Squares by imposing a penalty on the
        size of the coefficients with l2 regularization.
    Lasso : The Lasso is a linear model that estimates
        sparse coefficients with l1 regularization.
    ElasticNet : Elastic-Net is a linear regression
        model trained with both l1 and l2 -norm regularization of the
        coefficients.

    Notes
    -----
    From the implementation point of view, this is just plain Ordinary
    Least Squares (scipy.linalg.lstsq) or Non Negative Least Squares
    (scipy.optimize.nnls) wrapped as a predictor object.

    Examples
    --------
    >>> X = np.array([[1, 1], [1, 2], [2, 2], [2, 3]])
    >>> # y = 1 * x_0 + 2 * x_1 + 3
    >>> y = np.dot(X, np.array([1, 2])) + 3
    >>> reg = LinearRegression().fit(X, y)
    1.0
    >>> reg.coef_
    array([1., 2.])
    >>> reg.intercept_
    np.float64(3.0...)
    >>> reg.predict(np.array([[3, 5]]))
    array([16.])
    """

    }

        self,
        *,
        tol=1e-6,
    ):

        """
        Fit linear model.

               parameter *sample_weight* support to LinearRegression.

        Returns
        -------
            Fitted Estimator.
        """
        n_jobs_ = self.n_jobs

        accept_sparse = False if self.positive else ["csr", "csc", "coo"]

            self,
            X,
            y,
        )

        has_sw = sample_weight is not None
        if has_sw:
                sample_weight, X, dtype=X.dtype, ensure_non_negative=True
            )

        # Note that neither _rescale_data nor the rest of the fit method of
        # LinearRegression can benefit from in-place operations when X is a
        # sparse matrix. Therefore, let's not copy X when it is sparse.
        copy_X_in_preprocess_data = self.copy_X and not sp.issparse(X)

            X,
            y,
            copy=copy_X_in_preprocess_data,
        )

        if has_sw:
            # Sample weight can be implemented via a simple rescaling. Note
            # that we safely do inplace rescaling when _preprocess_data has
            # already made a copy if requested.
            X, y, sample_weight_sqrt = _rescale_data(
                X, y, sample_weight, inplace=copy_X_in_preprocess_data
            )

            if y.ndim < 2:
                self.coef_ = optimize.nnls(X, y)[0]
            else:
                # scipy.optimize.nnls cannot handle y with shape (M, K)
                outs = Parallel(n_jobs=n_jobs_)(
                    delayed(optimize.nnls)(X, y[:, j]) for j in range(y.shape[1])
                )
                self.coef_ = np.vstack([out[0] for out in outs])
            X_offset_scale = X_offset / X_scale

            if has_sw:

                def matvec(b):
                    return X.dot(b) - sample_weight_sqrt * b.dot(X_offset_scale)

                def rmatvec(b):
                    return X.T.dot(b) - X_offset_scale * b.dot(sample_weight_sqrt)

            else:

                    return X.dot(b) - b.dot(X_offset_scale)

                    return X.T.dot(b) - X_offset_scale * b.sum()

            X_centered = sparse.linalg.LinearOperator(
                shape=X.shape, matvec=matvec, rmatvec=rmatvec
            )

                self.coef_ = lsqr(X_centered, y, atol=self.tol, btol=self.tol)[0]
            else:
                # sparse_lstsq cannot handle y with shape (M, K)
                    delayed(lsqr)(
                        X_centered, y[:, j].ravel(), atol=self.tol, btol=self.tol
                    )
                    for j in range(y.shape[1])
                )
        else:
            # cut-off ratio for small singular values
            cond = max(X.shape) * np.finfo(X.dtype).eps
            self.coef_, _, self.rank_, self.singular_ = linalg.lstsq(X, y, cond=cond)
            self.coef_ = self.coef_.T

            self.coef_ = np.ravel(self.coef_)

        tags.input_tags.sparse = not self.positive

def _check_precomputed_gram_matrix(
    X, precompute, X_offset, X_scale, rtol=None, atol=1e-5
):
    """Computes a single element of the gram matrix and compares it to
    the corresponding element of the user supplied gram matrix.

    If the values do not match a ValueError will be thrown.

        Data array.

    precompute : array-like of shape (n_features, n_features)
        User-supplied gram matrix.

        Array of feature means used to center design matrix.

        Array of feature scale factors used to normalize design matrix.

    rtol : float, default=None
        Relative tolerance; see numpy.allclose
        If None, it is set to 1e-4 for arrays of dtype numpy.float32 and 1e-7
        otherwise.

    atol : float, default=1e-5
        absolute tolerance; see :func`numpy.allclose`. Note that the default
        here is more tolerant than the default for
        :func:`numpy.testing.assert_allclose`, where `atol=0`.

    Raises
    ------
        Raised when the provided Gram matrix is not consistent.
    """

    f1 = n_features // 2
    f2 = min(f1 + 1, n_features - 1)

    v1 = (X[:, f1] - X_offset[f1]) * X_scale[f1]
    v2 = (X[:, f2] - X_offset[f2]) * X_scale[f2]

    expected = np.dot(v1, v2)
    actual = precompute[f1, f2]

    dtypes = [precompute.dtype, expected.dtype]
        rtols = [1e-4 if dtype == np.float32 else 1e-7 for dtype in dtypes]
        rtol = max(rtols)

    if not np.isclose(expected, actual, rtol=rtol, atol=atol):
            "Gram matrix passed in via 'precompute' parameter "
            "did not pass validation when a single element was "
            "checked - please check that it was computed "
            f"properly. For element ({f1},{f2}) we computed "
            f"{expected} but the user-supplied value was "
            f"{actual}."
        )

def _pre_fit(
    X,
    y,
    Xy,
    precompute,
    copy,
):
    """Function used at beginning of fit in linear models with L1 or L0 penalty.

    This function applies _preprocess_data and additionally computes the gram matrix
    `precompute` as needed as well as `Xy`.
    """

    if sparse.issparse(X):
        # copy is not needed here as X is not modified inplace when X is sparse
            X,
            y,
        )
    else:
        # copy was done in fit if necessary
            X,
            y,
        )
        # Rescale only in dense case. Sparse cd solver directly deals with
        # sample_weight.
            # This triggers copies anyway.
            X, y, _ = _rescale_data(X, y, sample_weight=sample_weight)

    if hasattr(precompute, "__array__"):
        if fit_intercept and not np.allclose(X_offset, np.zeros(n_features)):
                (
                    "Gram matrix was provided but X was centered to fit "
                    "intercept: recomputing Gram matrix."
                ),
            )
            # TODO: instead of warning and recomputing, we could just center
            # the user provided Gram matrix a-posteriori (after making a copy
            # when `copy=True`).
            # recompute Gram
            precompute = "auto"
            Xy = None
        elif check_input:
            # If we're going to use the user's precomputed gram matrix, we
            # do a quick check to make sure its not totally bogus.
            _check_precomputed_gram_matrix(X, precompute, X_offset, X_scale)

    # precompute if n_samples > n_features
    if isinstance(precompute, str) and precompute == "auto":
        precompute = n_samples > n_features

    if precompute is True:
        # make sure that the 'precompute' array is contiguous.
        precompute = np.empty(shape=(n_features, n_features), dtype=X.dtype, order="C")
        np.dot(X.T, X, out=precompute)

    if not hasattr(precompute, "__array__"):
        Xy = None  # cannot use Xy if precompute is not Gram

    if hasattr(precompute, "__array__") and Xy is None:
        common_dtype = np.result_type(X.dtype, y.dtype)
            # Xy is 1d, make sure it is contiguous.
            Xy = np.empty(shape=n_features, dtype=common_dtype, order="C")
            np.dot(X.T, y, out=Xy)
        else:
            # Make sure that Xy is always F contiguous even if X or y are not
            # contiguous: the goal is to make it fast to extract the data for a
            # specific target.
            n_targets = y.shape[1]
            Xy = np.empty(shape=(n_features, n_targets), dtype=common_dtype, order="F")
            np.dot(y.T, X, out=Xy.T)

    return X, y, X_offset, y_offset, X_scale, precompute, Xy

        else:

        """

            Samples.

        Returns
        -------
        """

            else:

        else:

        """

        Returns
        -------
        """

        return (
        )

        """

        Returns
        -------
        """
        else:

        """
        else:

        """

        Returns
        -------
        self
        """

        """

        Returns
        -------
        self

        Notes
        -----

        """

        self,
        *,
        tol=1e-6,
    ):

        """

        Returns
        -------
        """

            self,
            X,
            y,
        )

        if has_sw:
            )

            X,
            y,
        )

        if has_sw:
            )

            else:
                )

            if has_sw:

            else:

            )

            else:
                    )
                )
        else:

def get_auto_step_size(
    max_squared_sum, alpha_scaled, loss, fit_intercept, n_samples=None, is_saga=False
):
    """Compute automatic step size for SAG solver.

    The step size is set to 1 / (alpha_scaled + L + fit_intercept) where L is
    the max sum of squares for over all samples.

    max_squared_sum : float
        Maximum squared sum of X over samples.

    alpha_scaled : float
        Constant that multiplies the regularization term, scaled by
        1. / n_samples, the number of samples.

    loss : {'log', 'squared', 'multinomial'}
        The loss function used in SAG solver.

        Specifies if a constant (a.k.a. bias or intercept) will be
        added to the decision function.

        Number of rows in X. Useful if is_saga=True.

    is_saga : bool, default=False
        Whether to return step size for the SAGA algorithm or the SAG

    Returns
    -------
    step_size : float
        Step size used in SAG solver.

    Schmidt, M., Roux, N. L., & Bach, F. (2013).
    Minimizing finite sums with the stochastic average gradient
    https://hal.inria.fr/hal-00860051/document

    :arxiv:`Defazio, A., Bach F. & Lacoste-Julien S. (2014).
    "SAGA: A Fast Incremental Gradient Method With Support
    for Non-Strongly Convex Composite Objectives" <1407.0202>`
    """
    if loss in ("log", "multinomial"):
        L = 0.25 * (max_squared_sum + int(fit_intercept)) + alpha_scaled
    elif loss == "squared":
        # inverse Lipschitz constant for squared loss
        L = max_squared_sum + int(fit_intercept) + alpha_scaled
    else:
            "Unknown loss function for SAG solver, got %s instead of 'log' or 'squared'"
            % loss
        )
    if is_saga:
        # SAGA theoretical step size is 1/3L or 1 / (2 * (L + mu n))
        # See Defazio et al. 2014
        mun = min(2 * n_samples * alpha_scaled, L)
        step = 1.0 / (2 * L + mun)
    else:
        # SAG theoretical step size is 1/16L but it is recommended to use 1 / L
        # see http://www.birs.ca//workshops//2014/14w5003/files/schmidt.pdf,
        # slide 65
        step = 1.0 / L
    return step

def sag_solver(
    X,
    y,
    loss="log",
    beta=0.0,
    tol=0.001,
    max_squared_sum=None,
    warm_start_mem=None,
    is_saga=False,
):
    """SAG solver for Ridge and LogisticRegression.

    SAG stands for Stochastic Average Gradient: the gradient of the loss is
    estimated each sample at a time and the model is updated along the way with
    a constant learning rate.

    IMPORTANT NOTE: 'sag' solver converges faster on columns that are on the
    same scale. You can normalize the data by using
    sklearn.preprocessing.StandardScaler on your data before passing it to the
    fit method.

    sparse scipy arrays of floating point values for the features. It will
    fit the data according to squared loss or log loss.

    The regularizer is a penalty added to the loss function that shrinks model
    parameters towards the zero vector using the squared euclidean norm L2.

        Target values. With loss='multinomial', y must be label encoded
        (see preprocessing.LabelEncoder). For loss='log' it must be in [0, 1].

        Weights applied to individual samples (1. for unweighted).

    loss : {'log', 'squared', 'multinomial'}, default='log'
        Loss function that will be optimized:
        -'log' is the binary logistic loss, as used in LogisticRegression.
        -'squared' is the squared loss, as used in Ridge.
        -'multinomial' is the multinomial logistic loss, as used in
         LogisticRegression.

           *loss='multinomial'*

    alpha : float, default=1.
        L2 regularization term in the objective function
        ``(0.5 * alpha * || W ||_F^2)``.

    beta : float, default=0.
        L1 regularization term in the objective function
        ``(beta * || W ||_1)``. Only applied if ``is_saga`` is set to True.

        The max number of passes over the training data if the stopping
        criteria is not reached.

    tol : float, default=0.001
        The stopping criteria for the weights. The iterations will stop when
        max(change in weights) / max(weights) < tol.

        Used when shuffling the data. Pass an int for reproducible output

    max_squared_sum : float, default=None
        Maximum squared sum of X over samples. If None, it will be computed,
        going through all the samples. The value should be precomputed
        to speed up cross validation.

    warm_start_mem : dict, default=None
        The initialization parameters used for warm starting. Warm starting is
        currently used in LogisticRegression but not in Ridge.
        It contains:
            - 'coef': the weight vector, with the intercept in last line
                if the intercept is fitted.
            - 'gradient_memory': the scalar gradient for all seen samples.
            - 'sum_gradient': the sum of gradient over all seen samples,
                for each feature.
            - 'intercept_sum_gradient': the sum of gradient over all seen
                samples, for the intercept.
            - 'seen': array of boolean describing the seen samples.
            - 'num_seen': the number of seen samples.

        Whether to use the SAGA algorithm or the SAG algorithm. SAGA behaves
        better in the first epochs, and allow for l1 regularisation.

    Returns
    -------
    coef_ : ndarray of shape (n_features,)
        Weight vector.

        The number of full pass on all samples.

    warm_start_mem : dict
        Contains a 'coef' key with the fitted result, and possibly the
        fitted intercept at the end of the array. Contains also other keys
        used for warm starting.

    Examples
    --------
    >>> clf = linear_model.Ridge(solver='sag')
    Ridge(solver='sag')

    >>> clf = linear_model.LogisticRegression(solver='sag')
    LogisticRegression(solver='sag')

    See Also
    --------
    Ridge, SGDRegressor, ElasticNet, Lasso, SVR,
    LogisticRegression, SGDClassifier, LinearSVC, Perceptron
    """
    if warm_start_mem is None:
        warm_start_mem = {}
    # Ridge default max_iter is None
    if max_iter is None:
        max_iter = 1000

        _dtype = [np.float64, np.float32]
        X = check_array(X, dtype=_dtype, accept_sparse="csr", order="C")
        y = check_array(y, dtype=_dtype, ensure_2d=False, order="C")

    n_samples, n_features = X.shape[0], X.shape[1]
    # As in SGD, the alpha is scaled by n_samples.
    alpha_scaled = float(alpha) / n_samples
    beta_scaled = float(beta) / n_samples

    # if loss == 'multinomial', y should be label encoded.
    n_classes = int(y.max()) + 1 if loss == "multinomial" else 1

    if "coef" in warm_start_mem.keys():
        coef_init = warm_start_mem["coef"]
    else:
        # assume fit_intercept is False
        coef_init = np.zeros((n_features, n_classes), dtype=X.dtype, order="C")

    # coef_init contains possibly the intercept_init at the end.
    # Note that Ridge centers the data before fitting, so fit_intercept=False.
    fit_intercept = coef_init.shape[0] == (n_features + 1)
        intercept_init = coef_init[-1, :]
        coef_init = coef_init[:-1, :]
    else:
        intercept_init = np.zeros(n_classes, dtype=X.dtype)

    if "intercept_sum_gradient" in warm_start_mem.keys():
        intercept_sum_gradient = warm_start_mem["intercept_sum_gradient"]
    else:
        intercept_sum_gradient = np.zeros(n_classes, dtype=X.dtype)

    if "gradient_memory" in warm_start_mem.keys():
        gradient_memory_init = warm_start_mem["gradient_memory"]
    else:
        gradient_memory_init = np.zeros(
            (n_samples, n_classes), dtype=X.dtype, order="C"
        )
    if "sum_gradient" in warm_start_mem.keys():
        sum_gradient_init = warm_start_mem["sum_gradient"]
    else:
        sum_gradient_init = np.zeros((n_features, n_classes), dtype=X.dtype, order="C")

    if "seen" in warm_start_mem.keys():
        seen_init = warm_start_mem["seen"]
    else:
        seen_init = np.zeros(n_samples, dtype=np.int32, order="C")

    if "num_seen" in warm_start_mem.keys():
        num_seen_init = warm_start_mem["num_seen"]
    else:
        num_seen_init = 0

    dataset, intercept_decay = make_dataset(X, y, sample_weight, random_state)

    if max_squared_sum is None:
    step_size = get_auto_step_size(
        max_squared_sum,
        alpha_scaled,
        loss,
        fit_intercept,
        is_saga=is_saga,
    )
    if step_size * alpha_scaled == 1:
        raise ZeroDivisionError(
            "Current sag implementation does not handle "
            "the case step_size * alpha_scaled == 1"
        )

    sag = sag64 if X.dtype == np.float64 else sag32
    num_seen, n_iter_ = sag(
        dataset,
        coef_init,
        intercept_init,
        n_classes,
        tol,
        max_iter,
        loss,
        step_size,
        beta_scaled,
        sum_gradient_init,
        gradient_memory_init,
        seen_init,
        num_seen_init,
        intercept_sum_gradient,
        intercept_decay,
        is_saga,
        verbose,
    )

    if n_iter_ == max_iter:
            "The max_iter was reached which means the coef_ did not converge",
        )

        coef_init = np.vstack((coef_init, intercept_init))

    warm_start_mem = {
        "coef": coef_init,
        "sum_gradient": sum_gradient_init,
        "intercept_sum_gradient": intercept_sum_gradient,
        "gradient_memory": gradient_memory_init,
        "seen": seen_init,
        "num_seen": num_seen,
    }

    if loss == "multinomial":
        coef_ = coef_init.T
    else:
        coef_ = coef_init[:, 0]

    return coef_, n_iter_, warm_start_mem

def _dynamic_max_trials(n_inliers, n_samples, min_samples, probability):
    """Determine number trials such that at least one outlier-free subset is
    sampled for the given inlier/outlier ratio.

    n_inliers : int
        Number of inliers in the data.

        Total number of samples in the data.

        Minimum number of samples chosen randomly from original data.

    probability : float
        Probability (confidence) that one outlier-free sample is generated.

    Returns
    -------
    trials : int
        Number of trials.

    """
    inlier_ratio = n_inliers / float(n_samples)
    nom = max(_EPSILON, 1 - probability)
    denom = max(_EPSILON, 1 - inlier_ratio**min_samples)
    if nom == 1:
        return 0
    if denom == 1:
        return float("inf")
    return abs(float(np.ceil(np.log(nom) / np.log(denom))))

class RANSACRegressor(
):
    """RANSAC (RANdom SAmple Consensus) algorithm.

    RANSAC is an iterative algorithm for the robust estimation of parameters
    from a subset of inliers from the complete data set.

    Read more in the :ref:`User Guide <ransac_regression>`.

        Base estimator object which implements the following methods:

        * `fit(X, y)`: Fit model to given training data and target values.
        * `score(X, y)`: Returns the mean accuracy on the given test data,
          which is used for the stop criterion defined by `stop_score`.
          Additionally, the score is used to decide which of two equally
          large consensus sets is chosen as the better one.
        * `predict(X)`: Returns predicted values using the linear model,
          which is used to compute residual error using loss function.

        If `estimator` is None, then
        :class:`~sklearn.linear_model.LinearRegression` is used for
        target values of dtype float.

        Note that the current implementation only supports regression
        estimators.

    min_samples : int (>= 1) or float ([0, 1]), default=None
        Minimum number of samples chosen randomly from original data. Treated
        as an absolute number of samples for `min_samples >= 1`, treated as a
        relative number `ceil(min_samples * X.shape[0])` for
        `min_samples < 1`. This is typically chosen as the minimal number of
        samples necessary to estimate the given `estimator`. By default a
        :class:`~sklearn.linear_model.LinearRegression` estimator is assumed and
        `min_samples` is chosen as ``X.shape[1] + 1``. This parameter is highly
        dependent upon the model, so if a `estimator` other than
        :class:`~sklearn.linear_model.LinearRegression` is used, the user must
        provide a value.

    residual_threshold : float, default=None
        Maximum residual for a data sample to be classified as an inlier.
        By default the threshold is chosen as the MAD (median absolute
        deviation) of the target values `y`. Points whose residuals are
        strictly equal to the threshold are considered as inliers.

    is_data_valid : callable, default=None
        This function is called with the randomly selected data before the
        model is fitted to it: `is_data_valid(X, y)`. If its return value is
        False the current randomly chosen sub-sample is skipped.

    is_model_valid : callable, default=None
        This function is called with the estimated model and the randomly
        selected data: `is_model_valid(model, X, y)`. If its return value is
        Rejecting samples with this function is computationally costlier than
        with `is_data_valid`. `is_model_valid` should therefore only be used if
        the estimated model is needed for making the rejection decision.

    max_trials : int, default=100
        Maximum number of iterations for random sample selection.

    max_skips : int, default=np.inf
        Maximum number of iterations that can be skipped due to finding zero
        inliers or invalid data defined by ``is_data_valid`` or invalid models
        defined by ``is_model_valid``.

    stop_n_inliers : int, default=np.inf
        Stop iteration if at least this number of inliers are found.

    stop_score : float, default=np.inf
        Stop iteration if score is greater equal than this threshold.

    stop_probability : float in range [0, 1], default=0.99
        RANSAC iteration stops if at least one outlier-free set of the training
        data is sampled in RANSAC. This requires to generate at least N
        samples (iterations)::

            N >= log(1 - probability) / log(1 - e**m)

        where the probability (confidence) is typically set to high value such
        as 0.99 (the default) and e is the current fraction of inliers w.r.t.
        the total number of samples.

    loss : str, callable, default='absolute_error'
        String inputs, 'absolute_error' and 'squared_error' are supported which
        find the absolute error and squared error per sample respectively.

        If ``loss`` is a callable, then it should be a function that takes
        two arrays as inputs, the true and predicted value and returns a 1-D
        array with the i-th value of the array corresponding to the loss
        on ``X[i]``.

        If the loss on a sample is greater than the ``residual_threshold``,
        then this sample is classified as an outlier.

    estimator_ : object
        Final model fitted on the inliers predicted by the "best" model found
        during RANSAC sampling (copy of the `estimator` object).

    n_trials_ : int
        Number of random selection trials until one of the stop criteria is
        met. It is always ``<= max_trials``.

    inlier_mask_ : bool array of shape [n_samples]
        Boolean mask of inliers classified as ``True``.

    n_skips_no_inliers_ : int
        Number of iterations skipped due to finding zero inliers.

    n_skips_invalid_data_ : int
        Number of iterations skipped due to invalid data defined by
        ``is_data_valid``.

    n_skips_invalid_model_ : int
        Number of iterations skipped due to an invalid model defined by
        ``is_model_valid``.

    See Also
    --------
    HuberRegressor : Linear regression model that is robust to outliers.
    TheilSenRegressor : Theil-Sen Estimator robust multivariate regression model.
    SGDRegressor : Fitted by minimizing a regularized empirical loss with SGD.

    .. [1] https://en.wikipedia.org/wiki/RANSAC
    .. [2] https://www.sri.com/wp-content/uploads/2021/12/ransac-publication.pdf
    .. [3] https://bmva-archive.org.uk/bmvc/2009/Papers/Paper355/Paper355.pdf

    Examples
    --------
    >>> from sklearn.linear_model import RANSACRegressor
    >>> X, y = make_regression(
    ...     n_samples=200, n_features=2, noise=4.0, random_state=0)
    >>> reg = RANSACRegressor(random_state=0).fit(X, y)
    0.9885...
    array([-31.9417...])

    :ref:`sphx_glr_auto_examples_linear_model_plot_ransac.py`
    """

        "estimator": [HasMethods(["fit", "score", "predict"]), None],
            None,
        ],
        "residual_threshold": [Interval(Real, 0, None, closed="left"), None],
        "is_data_valid": [callable, None],
        "is_model_valid": [callable, None],
        "max_trials": [
        ],
        "max_skips": [
        ],
        "stop_n_inliers": [
        ],
        "stop_score": [Interval(Real, None, None, closed="both")],
        "stop_probability": [Interval(Real, 0, 1, closed="both")],
        "loss": [StrOptions({"absolute_error", "squared_error"}), callable],
    }

        self,
        *,
        residual_threshold=None,
        is_data_valid=None,
        is_model_valid=None,
        max_trials=100,
        max_skips=np.inf,
        stop_n_inliers=np.inf,
        stop_score=np.inf,
        stop_probability=0.99,
    ):
        self.residual_threshold = residual_threshold
        self.is_data_valid = is_data_valid
        self.is_model_valid = is_model_valid
        self.max_trials = max_trials
        self.max_skips = max_skips
        self.stop_n_inliers = stop_n_inliers
        self.stop_score = stop_score
        self.stop_probability = stop_probability

        # RansacRegressor.estimator is not validated yet
    )
        """Fit estimator using RANSAC algorithm.

            Individual weights for each sample
            raises error if sample_weight is passed and estimator
            fit method does not support it.

                details.

        Returns
        -------
            Fitted `RANSACRegressor` estimator.

        Raises
        ------
        ValueError
            If no valid consensus set could be found. This occurs if
            `is_data_valid` and `is_model_valid` return False for all
            `max_trials` randomly chosen sub-samples.
        """
        # Need to validate separately here. We can't pass multi_output=True
        # because that would allow y to be csr. Delay expensive finiteness
        # check to the estimator's own input validation.
        check_X_params = dict(accept_sparse="csr", ensure_all_finite=False)
        check_y_params = dict(ensure_2d=False)
        )

            estimator = clone(self.estimator)
        else:
            estimator = LinearRegression()

        if self.min_samples is None:
            if not isinstance(estimator, LinearRegression):
                    "`min_samples` needs to be explicitly set when estimator "
                    "is not a LinearRegression."
                )
            min_samples = X.shape[1] + 1
        elif 0 < self.min_samples < 1:
            min_samples = np.ceil(self.min_samples * X.shape[0])
        elif self.min_samples >= 1:
            min_samples = self.min_samples
        if min_samples > X.shape[0]:
                "`min_samples` may not be larger than number "
                "of samples: n_samples = %d." % (X.shape[0])
            )

        if self.residual_threshold is None:
            # MAD (median absolute deviation)
            residual_threshold = np.median(np.abs(y - np.median(y)))
        else:
            residual_threshold = self.residual_threshold

        if self.loss == "absolute_error":
                loss_function = lambda y_true, y_pred: np.abs(y_true - y_pred)
            else:
                loss_function = lambda y_true, y_pred: np.sum(
                    np.abs(y_true - y_pred), axis=1
                )
        elif self.loss == "squared_error":
                loss_function = lambda y_true, y_pred: (y_true - y_pred) ** 2
            else:
                    (y_true - y_pred) ** 2, axis=1
                )

        elif callable(self.loss):
            loss_function = self.loss

        try:  # Not all estimator accept a random_state
            estimator.set_params(random_state=random_state)
            pass

        estimator_fit_has_sample_weight = has_fit_parameter(estimator, "sample_weight")
        estimator_name = type(estimator).__name__
        if sample_weight is not None and not estimator_fit_has_sample_weight:
                "%s does not support sample_weight. Sample"
                " weights are only used for the calibration"
                " itself." % estimator_name
            )

        else:
            routed_params.estimator = Bunch(fit={}, predict={}, score={})
                sample_weight = _check_sample_weight(sample_weight, X)
                routed_params.estimator.fit = {"sample_weight": sample_weight}

        n_inliers_best = 1
        score_best = -np.inf
        inlier_mask_best = None
        X_inlier_best = None
        y_inlier_best = None
        inlier_best_idxs_subset = None
        self.n_skips_no_inliers_ = 0
        self.n_skips_invalid_data_ = 0
        self.n_skips_invalid_model_ = 0

        # number of data samples
        sample_idxs = np.arange(n_samples)

        self.n_trials_ = 0
        max_trials = self.max_trials
        while self.n_trials_ < max_trials:
            self.n_trials_ += 1

            if (
                self.n_skips_no_inliers_
                + self.n_skips_invalid_data_
                + self.n_skips_invalid_model_
            ) > self.max_skips:
                break

            # choose random sample set
            subset_idxs = sample_without_replacement(
                n_samples, min_samples, random_state=random_state
            )
            X_subset = X[subset_idxs]
            y_subset = y[subset_idxs]

            # check if random sample set is valid
            if self.is_data_valid is not None and not self.is_data_valid(
                X_subset, y_subset
            ):
                self.n_skips_invalid_data_ += 1
                continue

            # cut `fit_params` down to `subset_idxs`
            fit_params_subset = _check_method_params(
                X, params=routed_params.estimator.fit, indices=subset_idxs
            )

            # fit model for current random sample set
            estimator.fit(X_subset, y_subset, **fit_params_subset)

            # check if estimated model is valid
            if self.is_model_valid is not None and not self.is_model_valid(
                estimator, X_subset, y_subset
            ):
                self.n_skips_invalid_model_ += 1
                continue

            # residuals of all data for current random sample model
            y_pred = estimator.predict(X)
            residuals_subset = loss_function(y, y_pred)

            # classify data into inliers and outliers
            inlier_mask_subset = residuals_subset <= residual_threshold
            n_inliers_subset = np.sum(inlier_mask_subset)

            # less inliers -> skip current random sample
            if n_inliers_subset < n_inliers_best:
                self.n_skips_no_inliers_ += 1
                continue

            # extract inlier data set
            inlier_idxs_subset = sample_idxs[inlier_mask_subset]
            X_inlier_subset = X[inlier_idxs_subset]
            y_inlier_subset = y[inlier_idxs_subset]

            # cut `fit_params` down to `inlier_idxs_subset`
            score_params_inlier_subset = _check_method_params(
                X, params=routed_params.estimator.score, indices=inlier_idxs_subset
            )

            # score of inlier data set
            score_subset = estimator.score(
                X_inlier_subset,
                y_inlier_subset,
                **score_params_inlier_subset,
            )

            # same number of inliers but worse score -> skip current random
            # sample
            if n_inliers_subset == n_inliers_best and score_subset < score_best:
                continue

            # save current random sample as best sample
            n_inliers_best = n_inliers_subset
            score_best = score_subset
            inlier_mask_best = inlier_mask_subset
            X_inlier_best = X_inlier_subset
            y_inlier_best = y_inlier_subset
            inlier_best_idxs_subset = inlier_idxs_subset

            max_trials = min(
                max_trials,
                _dynamic_max_trials(
                    n_inliers_best, n_samples, min_samples, self.stop_probability
                ),
            )

            # break if sufficient number of inliers or score is reached
            if n_inliers_best >= self.stop_n_inliers or score_best >= self.stop_score:
                break

        # if none of the iterations met the required criteria
        if inlier_mask_best is None:
            if (
                    "RANSAC skipped more iterations than `max_skips` without"
                    " finding a valid consensus set. Iterations were skipped"
                    " because each randomly chosen sub-sample failed the"
                    " passing criteria. See estimator attributes for"
                    " diagnostics (n_skips*)."
                )
            else:
                    "RANSAC could not find a valid consensus set. All"
                    " `max_trials` iterations were skipped because each"
                    " randomly chosen sub-sample failed the passing criteria."
                    " See estimator attributes for diagnostics (n_skips*)."
                )
        else:
            if (
                    (
                        "RANSAC found a valid consensus set but exited"
                        " early due to skipping more iterations than"
                        " `max_skips`. See estimator attributes for"
                        " diagnostics (n_skips*)."
                    ),
                )

        # estimate final model using all inliers
        fit_params_best_idxs_subset = _check_method_params(
            X, params=routed_params.estimator.fit, indices=inlier_best_idxs_subset
        )

        estimator.fit(X_inlier_best, y_inlier_best, **fit_params_best_idxs_subset)

        self.estimator_ = estimator
        self.inlier_mask_ = inlier_mask_best

        """Predict using the estimated model.

        This is a wrapper for `estimator_.predict(X)`.

        X : {array-like or sparse matrix} of shape (n_samples, n_features)

            Parameters routed to the `predict` method of the sub-estimator via
            the metadata routing API.

                details.

        Returns
        -------
        y : array, shape = [n_samples] or [n_samples, n_targets]
        """
            self,
            X,
        )

            predict_params = process_routing(self, "predict", **params).estimator[
                "predict"
            ]
        else:
            predict_params = {}

        return self.estimator_.predict(X, **predict_params)

        """Return the score of the prediction.

        This is a wrapper for `estimator_.score(X, y)`.

        X : (array-like or sparse matrix} of shape (n_samples, n_features)

            Parameters routed to the `score` method of the sub-estimator via

                details.

        Returns
        -------
        z : float
            Score of the prediction.
        """
            self,
            X,
        )

            score_params = process_routing(self, "score", **params).estimator["score"]
        else:
            score_params = {}

        return self.estimator_.score(X, y, **score_params)

        Returns
        -------
        """
            .add(caller="fit", callee="score")
        )

            tags.input_tags.sparse = True  # default estimator is LinearRegression
        else:

        self,
        *,
    ):

                details.

        Returns
        -------

        Raises
        ------
        """
        )

        else:

                )
            )

        else:

            else:
                )
            else:
                )

            pass

            )

        else:

            if (
                break

            )

            ):
                continue

            )

            ):
                continue

                continue

            )

            )

            # sample
                continue

                ),
            )

                break

            if (
                )
            else:
                )
        else:
            if (
                    (
                    ),
                )

        )

                details.

        Returns
        -------
        """
            self,
            X,
        )

                "predict"
            ]
        else:

                details.

        Returns
        -------
        z : float
        """
            self,
            X,
        )

        else:

        Returns
        -------
        """
        )

        else:

class Perceptron(BaseSGDClassifier):
    """Linear perceptron classifier.

    The implementation is a wrapper around :class:`~sklearn.linear_model.SGDClassifier`
    by fixing the `loss` and `learning_rate` parameters as::

        SGDClassifier(loss="perceptron", learning_rate="constant")

    Other available parameters are described below and are forwarded to
    :class:`~sklearn.linear_model.SGDClassifier`.

    Read more in the :ref:`User Guide <perceptron>`.

    penalty : {'l2','l1','elasticnet'}, default=None
        The penalty (aka regularization term) to be used.

        Constant that multiplies the regularization term if regularization is
        used.

    l1_ratio : float, default=0.15
        The Elastic Net mixing parameter, with `0 <= l1_ratio <= 1`.
        `l1_ratio=0` corresponds to L2 penalty, `l1_ratio=1` to L1.
        Only used if `penalty='elasticnet'`.

        Whether the intercept should be estimated or not. If False, the

        It only impacts the behavior in the ``fit`` method, and not the

    tol : float or None, default=1e-3
        The stopping criterion. If it is not None, the iterations will stop
        when (loss > previous_loss - tol).

        Whether or not the training data should be shuffled after each epoch.

    eta0 : float, default=1
        Constant by which the updates are multiplied.

        The number of CPUs to use to do the OVA (One Versus All, for
        multi-class problems) computation.

        Used to shuffle the training data, when ``shuffle`` is set to
        ``True``. Pass an int for reproducible output across multiple

        score is not improving. If set to True, it will automatically set aside
        a stratified fraction of training data as validation and terminate
        training when validation score is not improving by at least `tol` for
        `n_iter_no_change` consecutive epochs.

    n_iter_no_change : int, default=5
        Number of iterations with no improvement to wait before early stopping.

    class_weight : dict, {class_label: weight} or "balanced", default=None
        Preset for the class_weight fit parameter.

        Weights associated with classes. If not given, all classes
        are supposed to have weight one.

        initialization, otherwise, just erase the previous solution. See
        :term:`the Glossary <warm_start>`.

    coef_ : ndarray of shape (1, n_features) if n_classes == 2 else \
            (n_classes, n_features)
        Weights assigned to the features.

        For multiclass fits, it is the maximum over every binary fit.

    t_ : int
        Number of weight updates performed during training.
        Same as ``(n_iter_ * n_samples + 1)``.

    See Also
    --------
    sklearn.linear_model.SGDClassifier : Linear classifiers
        (SVM, logistic regression, etc.) with SGD training.

    Notes
    -----
    ``Perceptron`` is a classification algorithm which shares the same
    underlying implementation with ``SGDClassifier``. In fact,
    ``Perceptron()`` is equivalent to `SGDClassifier(loss="perceptron",
    eta0=1, learning_rate="constant", penalty=None)`.

    https://en.wikipedia.org/wiki/Perceptron and references therein.

    Examples
    --------
    >>> from sklearn.linear_model import Perceptron
    >>> clf = Perceptron(tol=1e-3, random_state=0)
    Perceptron()
    0.939...
    """

    _parameter_constraints: dict = {**BaseSGDClassifier._parameter_constraints}
    _parameter_constraints.pop("loss")
    _parameter_constraints.pop("average")
        {
            "penalty": [StrOptions({"l2", "l1", "elasticnet"}), None],
            "alpha": [Interval(Real, 0, None, closed="left")],
            "l1_ratio": [Interval(Real, 0, 1, closed="both")],
            "eta0": [Interval(Real, 0, None, closed="left")],
        }
    )

        self,
        *,
        penalty=None,
        l1_ratio=0.15,
        tol=1e-3,
        eta0=1.0,
    ):
            loss="perceptron",
            penalty=penalty,
            tol=tol,
            learning_rate="constant",
            eta0=eta0,
            power_t=0.5,
        )

        self,
        *,
        tol=1e-3,
        eta0=1.0,
    ):
            tol=tol,
        )

def _cholesky_omp(X, y, n_nonzero_coefs, tol=None, copy_X=True, return_path=False):
    """Orthogonal Matching Pursuit step using the Cholesky decomposition.

        Input dictionary. Columns are assumed to have unit norm.

        Input targets.

        Targeted number of non-zero elements.

    tol : float, default=None
        Targeted squared error, if not None overrides n_nonzero_coefs.

        Whether the design matrix X must be copied by the algorithm. A false
        value is only helpful if X is already Fortran-ordered, otherwise a
        copy is made anyway.

    return_path : bool, default=False
        Whether to return every value of the nonzero coefficients along the
        forward path. Useful for cross-validation.

    Returns
    -------
    gamma : ndarray of shape (n_nonzero_coefs,)
        Non-zero elements of the solution.

    idx : ndarray of shape (n_nonzero_coefs,)
        Indices of the positions of the elements in gamma within the solution
        vector.

    coef : ndarray of shape (n_features, n_nonzero_coefs)
        The first k values of column k correspond to the coefficient value
        for the active features at that step. The lower left triangle contains
        garbage. Only returned if ``return_path=True``.

    n_active : int
        Number of active features at convergence.
    """
    if copy_X:
        X = X.copy("F")
    else:  # even if we are allowed to overwrite, still copy it if bad order

    min_float = np.finfo(X.dtype).eps
    nrm2, swap = linalg.get_blas_funcs(("nrm2", "swap"), (X,))
    (potrs,) = get_lapack_funcs(("potrs",), (X,))

    alpha = np.dot(X.T, y)
    residual = y
    gamma = np.empty(0)
    n_active = 0
    indices = np.arange(X.shape[1])  # keeping track of swapping

    max_features = X.shape[1] if tol is not None else n_nonzero_coefs

    L = np.empty((max_features, max_features), dtype=X.dtype)

    if return_path:
        coefs = np.empty_like(L)

        lam = np.argmax(np.abs(np.dot(X.T, residual)))
        if lam < n_active or alpha[lam] ** 2 < min_float:
            # atom already selected or inner product too small
            warnings.warn(premature, RuntimeWarning, stacklevel=2)
            break

        if n_active > 0:
            # Updates the Cholesky decomposition of X' X
            L[n_active, :n_active] = np.dot(X[:, :n_active].T, X[:, lam])
            linalg.solve_triangular(
                L[:n_active, :n_active],
                L[n_active, :n_active],
                trans=0,
                lower=1,
                overwrite_b=True,
                check_finite=False,
            )
            v = nrm2(L[n_active, :n_active]) ** 2
            Lkk = linalg.norm(X[:, lam]) ** 2 - v
            if Lkk <= min_float:  # selected atoms are dependent
                warnings.warn(premature, RuntimeWarning, stacklevel=2)
                break
            L[n_active, n_active] = sqrt(Lkk)
        else:
            L[0, 0] = linalg.norm(X[:, lam])

        X.T[n_active], X.T[lam] = swap(X.T[n_active], X.T[lam])
        alpha[n_active], alpha[lam] = alpha[lam], alpha[n_active]
        indices[n_active], indices[lam] = indices[lam], indices[n_active]
        n_active += 1

        # solves LL'x = X'y as a composition of two triangular systems
        gamma, _ = potrs(
            L[:n_active, :n_active], alpha[:n_active], lower=True, overwrite_b=False
        )

        if return_path:
            coefs[:n_active, n_active - 1] = gamma
        residual = y - np.dot(X[:, :n_active], gamma)
        if tol is not None and nrm2(residual) ** 2 <= tol:
            break
        elif n_active == max_features:
            break

        return gamma, indices[:n_active], coefs[:, :n_active], n_active
    else:
        return gamma, indices[:n_active], n_active

def _gram_omp(
    Gram,
    Xy,
    tol_0=None,
    tol=None,
    copy_Gram=True,
    copy_Xy=True,
    return_path=False,
):
    """Orthogonal Matching Pursuit step on a precomputed Gram matrix.

    This function uses the Cholesky decomposition method.

    Gram : ndarray of shape (n_features, n_features)
        Gram matrix of the input data matrix.

    Xy : ndarray of shape (n_features,)

    tol_0 : float, default=None
        Squared norm of y, required if tol is not None.

    copy_Gram : bool, default=True
        Whether the gram matrix must be copied by the algorithm. A false
        value is only helpful if it is already Fortran-ordered, otherwise a

    copy_Xy : bool, default=True
        Whether the covariance vector Xy must be copied by the algorithm.
        If False, it may be overwritten.

    Returns
    -------

        vector.

    coefs : ndarray of shape (n_features, n_nonzero_coefs)

    """
    Gram = Gram.copy("F") if copy_Gram else np.asfortranarray(Gram)

    if copy_Xy or not Xy.flags.writeable:
        Xy = Xy.copy()

    min_float = np.finfo(Gram.dtype).eps
    nrm2, swap = linalg.get_blas_funcs(("nrm2", "swap"), (Gram,))
    (potrs,) = get_lapack_funcs(("potrs",), (Gram,))

    indices = np.arange(len(Gram))  # keeping track of swapping
    alpha = Xy
    tol_curr = tol_0
    delta = 0

    max_features = len(Gram) if tol is not None else n_nonzero_coefs

    L = np.empty((max_features, max_features), dtype=Gram.dtype)

    L[0, 0] = 1.0

        lam = np.argmax(np.abs(alpha))
            # selected same atom twice, or inner product too small
            warnings.warn(premature, RuntimeWarning, stacklevel=3)
            break
            L[n_active, :n_active] = Gram[lam, :n_active]
                trans=0,
                lower=1,
            )
            Lkk = Gram[lam, lam] - v
                warnings.warn(premature, RuntimeWarning, stacklevel=3)
                break
        else:
            L[0, 0] = sqrt(Gram[lam, lam])

        Gram[n_active], Gram[lam] = swap(Gram[n_active], Gram[lam])
        Gram.T[n_active], Gram.T[lam] = swap(Gram.T[n_active], Gram.T[lam])
        Xy[n_active], Xy[lam] = Xy[lam], Xy[n_active]
            L[:n_active, :n_active], Xy[:n_active], lower=True, overwrite_b=False
        )
        beta = np.dot(Gram[:, :n_active], gamma)
        alpha = Xy - beta
        if tol is not None:
            tol_curr += delta
            delta = np.inner(gamma, beta[:n_active])
            tol_curr -= delta
            if abs(tol_curr) <= tol:
                break
            break

    else:

def orthogonal_mp(
    X,
    y,
    *,
    tol=None,
    precompute=False,
):
    r"""Orthogonal Matching Pursuit (OMP).

    Solves n_targets Orthogonal Matching Pursuit problems.
    An instance of the problem has the form:

    When parametrized by the number of non-zero coefficients using
    `n_nonzero_coefs`:
    argmin ||y - X\gamma||^2 subject to ||\gamma||_0 <= n_{nonzero coefs}

    When parametrized by error using the parameter `tol`:
    argmin ||\gamma||_0 subject to ||y - X\gamma||^2 <= tol

    Read more in the :ref:`User Guide <omp>`.

        Input data. Columns are assumed to have unit norm.

        Desired number of non-zero entries in the solution. If None (by
        default) this value is set to 10% of n_features.

        Maximum squared norm of the residual. If not None, overrides n_nonzero_coefs.

    precompute : 'auto' or bool, default=False
        Whether to perform precomputations. Improves performance when n_targets
        or n_samples is very large.

    Returns
    -------
        Coefficients of the OMP solution. If `return_path=True`, this contains
        the whole coefficient path. In this case its shape is
        (n_features, n_features) or (n_features, n_targets, n_features) and
        iterating over the last axis generates coefficients in increasing order
        of active features.

    n_iters : array-like or int
        Number of active features across every target. Returned only if
        `return_n_iter` is set to True.

    See Also
    --------
    OrthogonalMatchingPursuit : Orthogonal Matching Pursuit model.
    orthogonal_mp_gram : Solve OMP problems using Gram matrix and the product X.T * y.
    lars_path : Compute Least Angle Regression or Lasso path using LARS algorithm.
    sklearn.decomposition.sparse_encode : Sparse coding.

    Notes
    -----
    Orthogonal matching pursuit was introduced in S. Mallat, Z. Zhang,
    Matching pursuits with time-frequency dictionaries, IEEE Transactions on
    Signal Processing, Vol. 41, No. 12. (December 1993), pp. 3397-3415.
    (https://www.di.ens.fr/~mallat/papiers/MallatPursuit93.pdf)

    This implementation is based on Rubinstein, R., Zibulevsky, M. and Elad,
    M., Efficient Implementation of the K-SVD Algorithm using Batch Orthogonal
    Matching Pursuit Technical Report - CS Technion, April 2008.
    https://www.cs.technion.ac.il/~ronrubin/Publications/KSVD-OMP-v2.pdf

    Examples
    --------
    >>> from sklearn.linear_model import orthogonal_mp
    >>> coef = orthogonal_mp(X, y)
    >>> coef.shape
    (100,)
    >>> X[:1,] @ coef
    array([-78.68...])
    """
    X = check_array(X, order="F", copy=copy_X)
    copy_X = False
        y = y.reshape(-1, 1)
    y = check_array(y)
    if y.shape[1] > 1:  # subsequent targets will be affected
        copy_X = True
    if n_nonzero_coefs is None and tol is None:
        # default for n_nonzero_coefs is 0.1 * n_features
        # but at least one.
        n_nonzero_coefs = max(int(0.1 * X.shape[1]), 1)
    if tol is None and n_nonzero_coefs > X.shape[1]:
            "The number of atoms cannot be more than the number of features"
        )
    if precompute == "auto":
        precompute = X.shape[0] > X.shape[1]
    if precompute:
        G = np.dot(X.T, X)
        G = np.asfortranarray(G)
        Xy = np.dot(X.T, y)
            norms_squared = np.sum((y**2), axis=0)
        else:
            norms_squared = None
        return orthogonal_mp_gram(
            G,
            Xy,
            n_nonzero_coefs=n_nonzero_coefs,
            tol=tol,
            norms_squared=norms_squared,
            copy_Gram=copy_X,
            copy_Xy=False,
            return_path=return_path,
        )

        coef = np.zeros((X.shape[1], y.shape[1], X.shape[1]))
    else:
        coef = np.zeros((X.shape[1], y.shape[1]))

        out = _cholesky_omp(
            X, y[:, k], n_nonzero_coefs, tol, copy_X=copy_X, return_path=return_path
        )
            _, idx, coefs, n_iter = out
            coef = coef[:, :, : len(idx)]
            for n_active, x in enumerate(coefs.T):
                coef[idx[: n_active + 1], k, n_active] = x[: n_active + 1]
        else:
            x, idx, n_iter = out
            coef[idx, k] = x
        n_iters.append(n_iter)

        n_iters = n_iters[0]

        return np.squeeze(coef), n_iters
    else:
        return np.squeeze(coef)

def orthogonal_mp_gram(
    Gram,
    Xy,
    *,
    tol=None,
    norms_squared=None,
):
    """Gram Orthogonal Matching Pursuit (OMP).

    Solves n_targets Orthogonal Matching Pursuit problems using only
    the Gram matrix X.T * X and the product X.T * y.

    Gram : array-like of shape (n_features, n_features)
        Gram matrix of the input data: `X.T * X`.

    Xy : array-like of shape (n_features,) or (n_features, n_targets)
        Input targets multiplied by `X`: `X.T * y`.

        Desired number of non-zero entries in the solution. If `None` (by

        Maximum squared norm of the residual. If not `None`,
        overrides `n_nonzero_coefs`.

    norms_squared : array-like of shape (n_targets,), default=None
        Squared L2 norms of the lines of `y`. Required if `tol` is not None.

        Whether the gram matrix must be copied by the algorithm. A `False`

        Whether the covariance vector `Xy` must be copied by the algorithm.
        If `False`, it may be overwritten.

    Returns
    -------
        `(n_features, n_features)` or `(n_features, n_targets, n_features)` and
        iterating over the last axis yields coefficients in increasing order

    n_iters : list or int

    See Also
    --------
    OrthogonalMatchingPursuit : Orthogonal Matching Pursuit model (OMP).
    orthogonal_mp : Solves n_targets Orthogonal Matching Pursuit problems.
    lars_path : Compute Least Angle Regression or Lasso path using
        LARS algorithm.
    sklearn.decomposition.sparse_encode : Generic sparse coding.
        Each column of the result is the solution to a Lasso problem.

    Notes
    -----
    Orthogonal matching pursuit was introduced in G. Mallat, Z. Zhang,

    Examples
    --------
    >>> from sklearn.linear_model import orthogonal_mp_gram
    >>> coef = orthogonal_mp_gram(X.T @ X, X.T @ y)
    (100,)
    """
    Gram = check_array(Gram, order="F", copy=copy_Gram)
    Xy = np.asarray(Xy)
    if Xy.ndim > 1 and Xy.shape[1] > 1:
        # or subsequent target will be affected
        copy_Gram = True
    if Xy.ndim == 1:
        Xy = Xy[:, np.newaxis]
            norms_squared = [norms_squared]
        # Make the copy once instead of many times in _gram_omp itself.

        n_nonzero_coefs = int(0.1 * len(Gram))
    if tol is not None and norms_squared is None:
            "Gram OMP needs the precomputed norms in order "
            "to evaluate the error sum of squares."
        )
    if tol is not None and tol < 0:
        raise ValueError("Epsilon cannot be negative")
    if tol is None and n_nonzero_coefs <= 0:
        raise ValueError("The number of atoms must be positive")
    if tol is None and n_nonzero_coefs > len(Gram):
        )

        coef = np.zeros((len(Gram), Xy.shape[1], len(Gram)), dtype=Gram.dtype)
    else:
        coef = np.zeros((len(Gram), Xy.shape[1]), dtype=Gram.dtype)

    for k in range(Xy.shape[1]):
        out = _gram_omp(
            Gram,
            Xy[:, k],
            n_nonzero_coefs,
            norms_squared[k] if tol is not None else None,
            tol,
            copy_Gram=copy_Gram,
        )
        else:

    if Xy.shape[1] == 1:

    else:

class OrthogonalMatchingPursuit(MultiOutputMixin, RegressorMixin, LinearModel):
    """Orthogonal Matching Pursuit model (OMP).

        Desired number of non-zero entries in the solution. Ignored if `tol` is set.
        When `None` and `tol` is also `None`, this value is either set to 10% of
        `n_features` or 1, whichever is greater.

    precompute : 'auto' or bool, default='auto'
        Whether to use a precomputed Gram and Xy matrix to speed up
        calculations. Improves performance when :term:`n_targets` or
        :term:`n_samples` is very large. Note that if you already have such
        matrices, you can pass them directly to the fit method.

        Parameter vector (w in the formula).

    n_iter_ : int or array-like
        Number of active features across every target.

    n_nonzero_coefs_ : int or None
        The number of non-zero coefficients in the solution or `None` when `tol` is
        set. If `n_nonzero_coefs` is None and `tol` is None this value is either set
        to 10% of `n_features` or 1, whichever is greater.

    See Also
    --------
    orthogonal_mp_gram :  Solves n_targets Orthogonal Matching Pursuit
        problems using only the Gram matrix X.T * X and the product X.T * y.
    Lars : Least Angle Regression model a.k.a. LAR.
    OrthogonalMatchingPursuitCV : Cross-validated
        Orthogonal Matching Pursuit model (OMP).

    Notes
    -----

    Examples
    --------
    >>> from sklearn.linear_model import OrthogonalMatchingPursuit
    >>> reg = OrthogonalMatchingPursuit().fit(X, y)
    0.9991...
    array([-78.3854...])
    """

        "n_nonzero_coefs": [Interval(Integral, 1, None, closed="left"), None],
        "tol": [Interval(Real, 0, None, closed="left"), None],
        "precompute": [StrOptions({"auto"}), "boolean"],
    }

        self,
        *,
        n_nonzero_coefs=None,
        tol=None,
    ):
        self.n_nonzero_coefs = n_nonzero_coefs

        Returns
        -------
        """
        X, y = validate_data(self, X, y, multi_output=True, y_numeric=True)

        X, y, X_offset, y_offset, X_scale, Gram, Xy = _pre_fit(
            X, y, None, self.precompute, self.fit_intercept, copy=True
        )

        if self.n_nonzero_coefs is None and self.tol is None:
            # default for n_nonzero_coefs is 0.1 * n_features
            # but at least one.
            self.n_nonzero_coefs_ = max(int(0.1 * n_features), 1)
        elif self.tol is not None:
            self.n_nonzero_coefs_ = None
        else:
            self.n_nonzero_coefs_ = self.n_nonzero_coefs

        if Gram is False:
            coef_, self.n_iter_ = orthogonal_mp(
                X,
                y,
                n_nonzero_coefs=self.n_nonzero_coefs_,
                precompute=False,
            )
        else:
            norms_sq = np.sum(y**2, axis=0) if self.tol is not None else None

            coef_, self.n_iter_ = orthogonal_mp_gram(
                Gram,
                Xy=Xy,
                norms_squared=norms_sq,
                copy_Gram=True,
                copy_Xy=True,
            )
        self.coef_ = coef_.T

def _omp_path_residues(
    X_train,
    y_train,
    X_test,
    y_test,
):
    """Compute the residues on left-out data for a full LARS path.

    X_train : ndarray of shape (n_samples, n_features)
        The data to fit the LARS on.

    y_train : ndarray of shape (n_samples)
        The target variable to fit LARS on.

        The data to compute the residues on.

    y_test : ndarray of shape (n_samples)
        The target variable to compute the residues on.

        Whether X_train, X_test, y_train and y_test should be copied.  If
        False, they may be overwritten.

        Maximum numbers of iterations to perform, therefore maximum features
        to include. 100 by default.

    Returns
    -------
    residues : ndarray of shape (n_samples, max_features)
        Residues of the prediction on the test data.
    """

    if copy:
        X_train = X_train.copy()
        y_train = y_train.copy()
        X_test = X_test.copy()
        y_test = y_test.copy()

        X_mean = X_train.mean(axis=0)
        X_train -= X_mean
        X_test -= X_mean
        y_mean = y_train.mean(axis=0)
        y_train = as_float_array(y_train, copy=False)
        y_train -= y_mean
        y_test = as_float_array(y_test, copy=False)
        y_test -= y_mean

    coefs = orthogonal_mp(
        X_train,
        y_train,
        n_nonzero_coefs=max_iter,
        tol=None,
        copy_X=False,
        return_path=True,
    )
    if coefs.ndim == 1:
        coefs = coefs[:, np.newaxis]

    return np.dot(coefs.T, X_test.T) - y_test

class OrthogonalMatchingPursuitCV(RegressorMixin, LinearModel):
    """Cross-validated Orthogonal Matching Pursuit model (OMP).

        to include. 10% of ``n_features`` but at least 5 if available.

        For integer/None inputs, :class:`~sklearn.model_selection.KFold` is used.

        Sets the verbosity amount.

        Parameter vector (w in the problem formulation).

    n_nonzero_coefs_ : int
        Estimated number of non-zero coefficients giving the best mean squared
        error over the cross-validation folds.

        Number of active features across every target for the model refit with
        the best hyperparameters got by cross-validating across all folds.

    See Also
    --------
    orthogonal_mp_gram : Solves n_targets Orthogonal Matching Pursuit
    LarsCV : Cross-validated Least Angle Regression model.
    LassoLarsCV : Cross-validated Lasso model fit with Least Angle Regression.

    Notes
    -----
    In `fit`, once the optimal number of non-zero coefficients is found through

    Examples
    --------
    >>> from sklearn.linear_model import OrthogonalMatchingPursuitCV
    >>> X, y = make_regression(n_features=100, n_informative=10,
    ...                        noise=4, random_state=0)
    >>> reg = OrthogonalMatchingPursuitCV(cv=5).fit(X, y)
    0.9991...
    >>> reg.n_nonzero_coefs_
    np.int64(10)
    """

    }

        self,
        *,
        cv=None,
    ):

            Parameters to pass to the underlying splitter.

        Returns
        -------
        """

        X, y = validate_data(self, X, y, y_numeric=True, ensure_min_features=2)
        X = as_float_array(X, copy=False, ensure_all_finite=False)
        cv = check_cv(self.cv, classifier=False)
        else:
            routed_params.splitter = Bunch(split={})
        max_iter = (
            min(max(int(0.1 * X.shape[1]), 5), X.shape[1])
            if not self.max_iter
            else self.max_iter
        )
        cv_paths = Parallel(n_jobs=self.n_jobs, verbose=self.verbose)(
            delayed(_omp_path_residues)(
                X[train],
                y[train],
                X[test],
                y[test],
                self.copy,
                max_iter,
            )
            for train, test in cv.split(X, **routed_params.splitter.split)
        )

        min_early_stop = min(fold.shape[0] for fold in cv_paths)
        mse_folds = np.array(
            [(fold[:min_early_stop] ** 2).mean(axis=1) for fold in cv_paths]
        )
        best_n_nonzero_coefs = np.argmin(mse_folds.mean(axis=0)) + 1
        self.n_nonzero_coefs_ = best_n_nonzero_coefs
        omp = OrthogonalMatchingPursuit(
            n_nonzero_coefs=best_n_nonzero_coefs,

        self.coef_ = omp.coef_
        self.intercept_ = omp.intercept_
        self.n_iter_ = omp.n_iter_

        Returns
        -------
        """

            splitter=self.cv,
            method_mapping=MethodMapping().add(caller="fit", callee="split"),
        )

        self,
        *,
        tol=None,
    ):

        Returns
        -------
        """

        )

        else:

                X,
                y,
            )
        else:

                Gram,
                Xy=Xy,
            )

        self,
        *,
        cv=None,
    ):

        Returns
        -------
        """

        else:
        )
                X[train],
                y[train],
                X[test],
                y[test],
                max_iter,
            )
        )

        )

        Returns
        -------
        """

        )

def _modified_weiszfeld_step(X, x_old):
    """Modified Weiszfeld step.

    This function defines one iteration step in order to approximate the
    spatial median (L1 median). It is a form of an iteratively re-weighted
    least squares method.

    x_old : ndarray of shape = (n_features,)
        Current start vector.

    Returns
    -------
    x_new : ndarray of shape (n_features,)
        New iteration step.

    - On Computation of Spatial Median for Robust Data Mining, 2005
      T. Kärkkäinen and S. Äyrämö
      http://users.jyu.fi/~samiayr/pdf/ayramo_eurogen05.pdf
    """
    diff = X - x_old
    diff_norm = np.sqrt(np.sum(diff**2, axis=1))
    mask = diff_norm >= _EPSILON
    # x_old equals one of our samples
    is_x_old_in_X = int(mask.sum() < X.shape[0])

    diff = diff[mask]
    diff_norm = diff_norm[mask][:, np.newaxis]
    quotient_norm = linalg.norm(np.sum(diff / diff_norm, axis=0))

    if quotient_norm > _EPSILON:  # to avoid division by zero
        new_direction = np.sum(X[mask, :] / diff_norm, axis=0) / np.sum(
            1 / diff_norm, axis=0
        )
    else:
        new_direction = 1.0
        quotient_norm = 1.0

    return (
        max(0.0, 1.0 - is_x_old_in_X / quotient_norm) * new_direction
        + min(1.0, is_x_old_in_X / quotient_norm) * x_old
    )

def _spatial_median(X, max_iter=300, tol=1.0e-3):
    """Spatial median (L1 median).

    The spatial median is member of a class of so-called M-estimators which
    are defined by an optimization problem. Given a number of p points in an
    n-dimensional space, the point x minimizing the sum of all distances to the
    p other points is called spatial median.

    tol : float, default=1.e-3
        Stop the algorithm if spatial_median has converged.

    Returns
    -------
    spatial_median : ndarray of shape = (n_features,)
        Spatial median.

        Number of iterations needed.

    """
    if X.shape[1] == 1:
        return 1, np.median(X.ravel(), keepdims=True)

    tol **= 2  # We are computing the tol on the squared norm
    spatial_median_old = np.mean(X, axis=0)

    for n_iter in range(max_iter):
        spatial_median = _modified_weiszfeld_step(X, spatial_median_old)
        if np.sum((spatial_median_old - spatial_median) ** 2) < tol:
            break
        else:
            spatial_median_old = spatial_median
    else:
            "Maximum number of iterations {max_iter} reached in "
            "spatial median for TheilSen regressor."
            "".format(max_iter=max_iter),
        )
    return n_iter, spatial_median

def _breakdown_point(n_samples, n_subsamples):
    """Approximation of the breakdown point.

    n_subsamples : int
        Number of subsamples to consider.

    Returns
    -------
    breakdown_point : float
        Approximation of breakdown point.
    """
    return (
        1
        - (
            0.5 ** (1 / n_subsamples) * (n_samples - n_subsamples + 1)
            + n_subsamples
            - 1
        )
        / n_samples
    )

def _lstsq(X, y, indices, fit_intercept):
    """Least Squares Estimator for TheilSenRegressor class.

    This function calculates the least squares method on a subset of rows of X
    and y defined by the indices array. Optionally, an intercept column is
    added if intercept is set to true.

        Design matrix, where `n_samples` is the number of samples and

        Target vector, where `n_samples` is the number of samples.

    indices : ndarray of shape (n_subpopulation, n_subsamples)
        Indices of all subsamples with respect to the chosen subpopulation.

        Fit intercept or not.

    Returns
    -------
    weights : ndarray of shape (n_subpopulation, n_features + intercept)
        Solution matrix of n_subpopulation solved least square problems.
    """
    fit_intercept = int(fit_intercept)
    n_features = X.shape[1] + fit_intercept
    n_subsamples = indices.shape[1]
    weights = np.empty((indices.shape[0], n_features))
    X_subpopulation = np.ones((n_subsamples, n_features))
    # gelss need to pad y_subpopulation to be of the max dim of X_subpopulation
    y_subpopulation = np.zeros((max(n_subsamples, n_features)))
    (lstsq,) = get_lapack_funcs(("gelss",), (X_subpopulation, y_subpopulation))

    for index, subset in enumerate(indices):
        X_subpopulation[:, fit_intercept:] = X[subset, :]
        y_subpopulation[:n_subsamples] = y[subset]
        weights[index] = lstsq(X_subpopulation, y_subpopulation)[1][:n_features]

    return weights

class TheilSenRegressor(RegressorMixin, LinearModel):
    """Theil-Sen Estimator: robust multivariate regression model.

    The algorithm calculates least square solutions on subsets with size
    n_subsamples of the samples in X. Any value of n_subsamples between the
    number of features and samples leads to an estimator with a compromise
    between robustness and efficiency. Since the number of least square
    solutions is "n_samples choose n_subsamples", it can be extremely large
    and can therefore be limited with max_subpopulation. If this limit is
    reached, the subsets are chosen randomly. In a final step, the spatial
    median (or L1 median) is calculated of all least square solutions.

    Read more in the :ref:`User Guide <theil_sen_regression>`.

        to false, no intercept will be used in calculations.

            `copy_X` was deprecated in 1.6 and will be removed in 1.8.
            It has no effect as a copy is always made.

    max_subpopulation : int, default=1e4
        Instead of computing with a set of cardinality 'n choose k', where n is
        the number of samples and k is the number of subsamples (at least
        number of features), consider only a stochastic subpopulation of a
        given maximal size if 'n choose k' is larger than max_subpopulation.
        For other than small problem sizes this parameter will determine
        memory usage and runtime if n_subsamples is not changed. Note that the
        data type should be int but floats such as 1e4 can be accepted too.

    n_subsamples : int, default=None
        Number of samples to calculate the parameters. This is at least the
        number of features (plus 1 if fit_intercept=True) and the number of
        samples as a maximum. A lower number leads to a higher breakdown
        point and a low efficiency while a high number leads to a low
        breakdown point and a high efficiency. If None, take the
        minimum number of subsamples leading to maximal robustness.
        If n_subsamples is set to n_samples, Theil-Sen is identical to least
        squares.

        Maximum number of iterations for the calculation of spatial median.

        Tolerance when calculating spatial median.

        A random number generator instance to define the state of the random
        permutations generator. Pass an int for reproducible output across

        Coefficients of the regression model (median of distribution).

        Estimated intercept of regression model.

    breakdown_ : float
        Approximated breakdown point.

        Number of iterations needed for the spatial median.

    n_subpopulation_ : int
        Number of combinations taken into account from 'n choose k', where n is
        the number of samples and k is the number of subsamples.

    See Also
    --------
    RANSACRegressor : RANSAC (RANdom SAmple Consensus) algorithm.

    - Theil-Sen Estimators in a Multiple Linear Regression Model, 2009
      Xin Dang, Hanxiang Peng, Xueqin Wang and Heping Zhang
      http://home.olemiss.edu/~xdang/papers/MTSE.pdf

    Examples
    --------
    >>> from sklearn.linear_model import TheilSenRegressor
    >>> reg = TheilSenRegressor(random_state=0).fit(X, y)
    0.9884...
    array([-31.5871...])
    """

        "copy_X": ["boolean", Hidden(StrOptions({"deprecated"}))],
        # target_type should be Integral but can accept Real for backward compatibility
        "max_subpopulation": [Interval(Real, 1, None, closed="left")],
        "n_subsamples": [None, Integral],
    }

        self,
        *,
        copy_X="deprecated",
        max_subpopulation=1e4,
        n_subsamples=None,
    ):
        self.max_subpopulation = max_subpopulation
        self.n_subsamples = n_subsamples

    def _check_subparams(self, n_samples, n_features):
        n_subsamples = self.n_subsamples

            n_dim = n_features + 1
        else:
            n_dim = n_features

        if n_subsamples is not None:
            if n_subsamples > n_samples:
                    "Invalid parameter since n_subsamples > "
                    "n_samples ({0} > {1}).".format(n_subsamples, n_samples)
                )
            if n_samples >= n_features:
                if n_dim > n_subsamples:
                    plus_1 = "+1" if self.fit_intercept else ""
                        "Invalid parameter since n_features{0} "
                        "> n_subsamples ({1} > {2})."
                        "".format(plus_1, n_dim, n_subsamples)
                    )
            else:  # if n_samples < n_features
                if n_subsamples != n_samples:
                        "Invalid parameter since n_subsamples != "
                        "n_samples ({0} != {1}) while n_samples "
                        "< n_features.".format(n_subsamples, n_samples)
                    )
        else:
            n_subsamples = min(n_dim, n_samples)

        all_combinations = max(1, np.rint(binom(n_samples, n_subsamples)))
        n_subpopulation = int(min(self.max_subpopulation, all_combinations))

        return n_subsamples, n_subpopulation

        """Fit linear model.

        Returns
        -------
            Fitted `TheilSenRegressor` estimator.
        """
        if self.copy_X != "deprecated":
                "`copy_X` was deprecated in 1.6 and will be removed in 1.8 since it "
            )

        X, y = validate_data(self, X, y, y_numeric=True)
        n_subsamples, self.n_subpopulation_ = self._check_subparams(
            n_samples, n_features
        )
        self.breakdown_ = _breakdown_point(n_samples, n_subsamples)

            print("Breakdown point: {0}".format(self.breakdown_))
            print("Number of samples: {0}".format(n_samples))
            tol_outliers = int(self.breakdown_ * n_samples)
            print("Tolerable outliers: {0}".format(tol_outliers))
            print("Number of subpopulations: {0}".format(self.n_subpopulation_))

        # Determine indices of subpopulation
        if np.rint(binom(n_samples, n_subsamples)) <= self.max_subpopulation:
            indices = list(combinations(range(n_samples), n_subsamples))
        else:
            indices = [
                random_state.choice(n_samples, size=n_subsamples, replace=False)
                for _ in range(self.n_subpopulation_)
            ]

        index_list = np.array_split(indices, n_jobs)
        weights = Parallel(n_jobs=n_jobs, verbose=self.verbose)(
            delayed(_lstsq)(X, y, index_list[job], self.fit_intercept)
            for job in range(n_jobs)
        )
        weights = np.vstack(weights)
        self.n_iter_, coefs = _spatial_median(
            weights, max_iter=self.max_iter, tol=self.tol
        )

            self.intercept_ = coefs[0]
            self.coef_ = coefs[1:]
        else:
            self.coef_ = coefs

        self,
        *,
    ):

        else:

                )
                    )
                    )
        else:

        Returns
        -------
        """
            )

        )

        else:
            indices = [
            ]

        )
        )

        else:

def _huber_loss_and_gradient(w, X, y, epsilon, alpha, sample_weight=None):
    """Returns the Huber loss and the gradient.

    w : ndarray, shape (n_features + 1,) or (n_features + 2,)
        Feature vector.
        w[:n_features] gives the coefficients
        w[-1] gives the scale factor and if the intercept is fit w[-2]
        gives the intercept factor.

        Target vector.

    epsilon : float
        Robustness of the Huber estimator.

        Regularization parameter.

        Weight assigned to each sample.

    Returns
    -------
        Huber loss.

    gradient : ndarray, shape (len(w))
        Returns the derivative of the Huber loss with respect to each
        coefficient, intercept and the scale as a vector.
    """
    fit_intercept = n_features + 2 == w.shape[0]
        intercept = w[-2]
    sigma = w[-1]
    w = w[:n_features]
    n_samples = np.sum(sample_weight)

    # Calculate the values where |y - X'w -c / sigma| > epsilon
    # The values above this threshold are outliers.
    linear_loss = y - safe_sparse_dot(X, w)
        linear_loss -= intercept
    abs_linear_loss = np.abs(linear_loss)
    outliers_mask = abs_linear_loss > epsilon * sigma

    # Calculate the linear loss due to the outliers.
    # This is equal to (2 * M * |y - X'w -c / sigma| - M**2) * sigma
    outliers = abs_linear_loss[outliers_mask]
    num_outliers = np.count_nonzero(outliers_mask)
    n_non_outliers = X.shape[0] - num_outliers

    # n_sq_outliers includes the weight give to the outliers while
    # num_outliers is just the number of outliers.
    outliers_sw = sample_weight[outliers_mask]
    n_sw_outliers = np.sum(outliers_sw)
    outlier_loss = (
        2.0 * epsilon * np.sum(outliers_sw * outliers)
        - sigma * n_sw_outliers * epsilon**2
    )

    # Calculate the quadratic loss due to the non-outliers.-
    # This is equal to |(y - X'w - c)**2 / sigma**2| * sigma
    non_outliers = linear_loss[~outliers_mask]
    weighted_non_outliers = sample_weight[~outliers_mask] * non_outliers
    weighted_loss = np.dot(weighted_non_outliers.T, non_outliers)
    squared_loss = weighted_loss / sigma

        grad = np.zeros(n_features + 2)
    else:
        grad = np.zeros(n_features + 1)

    # Gradient due to the squared loss.
    X_non_outliers = -axis0_safe_slice(X, ~outliers_mask, n_non_outliers)
    grad[:n_features] = (
        2.0 / sigma * safe_sparse_dot(weighted_non_outliers, X_non_outliers)
    )

    # Gradient due to the linear loss.
    signed_outliers = np.ones_like(outliers)
    signed_outliers_mask = linear_loss[outliers_mask] < 0
    signed_outliers[signed_outliers_mask] = -1.0
    X_outliers = axis0_safe_slice(X, outliers_mask, num_outliers)
    sw_outliers = sample_weight[outliers_mask] * signed_outliers
    grad[:n_features] -= 2.0 * epsilon * (safe_sparse_dot(sw_outliers, X_outliers))

    # Gradient due to the penalty.
    grad[:n_features] += alpha * 2.0 * w

    # Gradient due to sigma.
    grad[-1] = n_samples
    grad[-1] -= n_sw_outliers * epsilon**2
    grad[-1] -= squared_loss / sigma

    # Gradient due to the intercept.
        grad[-2] = -2.0 * np.sum(weighted_non_outliers) / sigma
        grad[-2] -= 2.0 * epsilon * np.sum(sw_outliers)

    loss = n_samples * sigma + squared_loss + outlier_loss
    loss += alpha * np.dot(w, w)
    return loss, grad

class HuberRegressor(LinearModel, RegressorMixin, BaseEstimator):
    """L2-regularized linear regression model that is robust to outliers.

    The Huber Regressor optimizes the squared loss for the samples where
    ``|(y - Xw - c) / sigma| < epsilon`` and the absolute loss for the samples
    where ``|(y - Xw - c) / sigma| > epsilon``, where the model coefficients
    ``w``, the intercept ``c`` and the scale ``sigma`` are parameters
    to be optimized. The parameter `sigma` makes sure that if `y` is scaled up
    or down by a certain factor, one does not need to rescale `epsilon` to
    achieve the same robustness. Note that this does not take into account
    the fact that the different features of `X` may be of different scales.

    The Huber loss function has the advantage of not being heavily influenced
    by the outliers while not completely ignoring their effect.

    Read more in the :ref:`User Guide <huber_regression>`

    epsilon : float, default=1.35
        The parameter epsilon controls the number of samples that should be
        classified as outliers. The smaller the epsilon, the more robust it is
        to outliers. Epsilon must be in the range `[1, inf)`.

        Maximum number of iterations that
        ``scipy.optimize.minimize(method="L-BFGS-B")`` should run for.

        Strength of the squared L2 regularization. Note that the penalty is
        equal to ``alpha * ||w||^2``.
        Must be in the range `[0, inf)`.

        This is useful if the stored attributes of a previously used model
        has to be reused. If set to False, then the coefficients will
        be rewritten for every call to fit.

        Whether or not to fit the intercept. This can be set to False
        if the data is already centered around the origin.

    tol : float, default=1e-05
        The iteration will stop when
        ``max{|proj g_i | i = 1, ..., n}`` <= ``tol``
        where pg_i is the i-th component of the projected gradient.

    coef_ : array, shape (n_features,)
        Features got by optimizing the L2-regularized Huber loss.

        Bias.

    scale_ : float
        The value by which ``|y - Xw - c|`` is scaled down.

        Number of iterations that
        ``scipy.optimize.minimize(method="L-BFGS-B")`` has run for.

            In SciPy <= 1.0.0 the number of lbfgs iterations may exceed
            ``max_iter``. ``n_iter_`` will now report at most ``max_iter``.

    outliers_ : array, shape (n_samples,)
        A boolean mask which is set to True where the samples are identified
        as outliers.

    See Also
    --------

    .. [1] Peter J. Huber, Elvezio M. Ronchetti, Robust Statistics
           Concomitant scale estimates, p. 172
    .. [2] Art B. Owen (2006), `A robust hybrid of lasso and ridge regression.
           <https://artowen.su.domains/reports/hhu.pdf>`_

    Examples
    --------
    >>> from sklearn.linear_model import HuberRegressor, LinearRegression
    >>> X, y, coef = make_regression(
    ...     n_samples=200, n_features=2, noise=4.0, coef=True, random_state=0)
    >>> X[:4] = rng.uniform(10, 20, (4, 2))
    >>> y[:4] = rng.uniform(10, 20, 4)
    >>> huber = HuberRegressor().fit(X, y)
    >>> huber.score(X, y)
    -7.284...
    >>> huber.predict(X[:1,])
    array([806.7200...])
    >>> linear = LinearRegression().fit(X, y)
    >>> print("True coefficients:", coef)
    True coefficients: [20.4923...  34.1698...]
    >>> print("Huber coefficients:", huber.coef_)
    Huber coefficients: [17.7906... 31.0106...]
    >>> print("Linear Regression coefficients:", linear.coef_)
    Linear Regression coefficients: [-1.9221...  7.0226...]
    """

        "epsilon": [Interval(Real, 1.0, None, closed="left")],
    }

        self,
        *,
        epsilon=1.35,
        tol=1e-05,
    ):

        y : array-like, shape (n_samples,)

        sample_weight : array-like, shape (n_samples,)
            Weight given to each sample.

        Returns
        -------
            Fitted `HuberRegressor` estimator.
        """
            self,
            X,
            y,
            accept_sparse=["csr"],
        )

        if self.warm_start and hasattr(self, "coef_"):
            parameters = np.concatenate((self.coef_, [self.intercept_, self.scale_]))
        else:
                parameters = np.zeros(X.shape[1] + 2)
            else:
                parameters = np.zeros(X.shape[1] + 1)
            # Make sure to initialize the scale parameter to a strictly
            # positive value:
            parameters[-1] = 1

        # Sigma or the scale factor should be non-negative.
        # Setting it to be zero might cause undefined bounds hence we set it
        # to a value close to zero.
        bounds = np.tile([-np.inf, np.inf], (parameters.shape[0], 1))
        bounds[-1][0] = np.finfo(np.float64).eps * 10

        opt_res = optimize.minimize(
            _huber_loss_and_gradient,
            parameters,
            jac=True,
            args=(X, y, self.epsilon, self.alpha, sample_weight),
            options={"maxiter": self.max_iter, "gtol": self.tol, "iprint": -1},
            bounds=bounds,
        )

        parameters = opt_res.x

        if opt_res.status == 2:
                "HuberRegressor convergence failed: l-BFGS-b solver terminated with %s"
                % opt_res.message
            )
        self.scale_ = parameters[-1]
            self.intercept_ = parameters[-2]
        else:
        self.coef_ = parameters[: X.shape[1]]

        residual = np.abs(y - safe_sparse_dot(X, self.coef_) - self.intercept_)
        self.outliers_ = residual > self.scale_ * self.epsilon

        self,
        *,
    ):

        Returns
        -------
        """
            self,
            X,
            y,
        )

        else:
            else:

            jac=True,
        )

            )
        else:

class PassiveAggressiveClassifier(BaseSGDClassifier):
    """Passive Aggressive Classifier.

    Read more in the :ref:`User Guide <passive_aggressive>`.

        Maximum step size (regularization). Defaults to 1.0.

        :meth:`~sklearn.linear_model.PassiveAggressiveClassifier.partial_fit` method.

    loss : str, default="hinge"
        The loss function to be used:
        hinge: equivalent to PA-I in the reference paper.
        squared_hinge: equivalent to PA-II in the reference paper.

        Repeatedly calling fit or partial_fit when warm_start is True can
        result in a different solution than when calling fit a single time
        because of the way the data is shuffled.

    class_weight : dict, {class_label: weight} or "balanced" or None, \

           parameter *class_weight* to automatically weight samples.

    average : bool or int, default=False
        When set to True, computes the averaged SGD weights and stores the
        result in the ``coef_`` attribute. If set to an int greater than 1,
        averaging will begin once the total number of samples seen reaches
        average. So average=10 will begin averaging after seeing 10 samples.

           parameter *average* to use weights averaging in SGD.

    t_ : int

    See Also
    --------
    SGDClassifier : Incrementally trained logistic regression.
    Perceptron : Linear perceptron classifier.

    Online Passive-Aggressive Algorithms
    <http://jmlr.csail.mit.edu/papers/volume7/crammer06a/crammer06a.pdf>
    K. Crammer, O. Dekel, J. Keshat, S. Shalev-Shwartz, Y. Singer - JMLR (2006)

    Examples
    --------
    >>> from sklearn.linear_model import PassiveAggressiveClassifier
    >>> clf = PassiveAggressiveClassifier(max_iter=1000, random_state=0,
    ... tol=1e-3)
    PassiveAggressiveClassifier(random_state=0)
    [[0.26642044 0.45070924 0.67251877 0.64185414]]
    [1.84127814]
    [1]
    """

        **BaseSGDClassifier._parameter_constraints,
        "C": [Interval(Real, 0, None, closed="right")],
    }

        self,
        *,
        C=1.0,
        tol=1e-3,
        loss="hinge",
        average=False,
    ):
            penalty=None,
            tol=tol,
            eta0=1.0,
        )

        self.C = C

        """Fit linear model with Passive Aggressive algorithm.

            Subset of the training data.

            Subset of the target values.

        classes : ndarray of shape (n_classes,)
            Can be obtained by via `np.unique(y_all)`, where y_all is the

        Returns
        -------
        """
        if not hasattr(self, "classes_"):
            self._more_validate_params(for_partial_fit=True)

            if self.class_weight == "balanced":
                    "class_weight 'balanced' is not supported for "
                    "partial_fit. For 'balanced' weights, use "
                    "`sklearn.utils.compute_class_weight` with "
                    "`class_weight='balanced'`. In place of y you "
                    "can use a large enough subset of the full "
                    "training set target to properly estimate the "
                    "class frequency distributions. Pass the "
                    "resulting weights as the class_weight "
                    "parameter."
                )

        lr = "pa1" if self.loss == "hinge" else "pa2"
            X,
            y,
            alpha=1.0,
            C=self.C,
            loss="hinge",
            learning_rate=lr,
            classes=classes,
            coef_init=None,
            intercept_init=None,
        )

    def fit(self, X, y, coef_init=None, intercept_init=None):

        coef_init : ndarray of shape (n_classes, n_features)
            The initial coefficients to warm-start the optimization.

        intercept_init : ndarray of shape (n_classes,)
            The initial intercept to warm-start the optimization.

        Returns
        -------
        """
        self._more_validate_params()

            X,
            y,
            C=self.C,
            coef_init=coef_init,
            intercept_init=intercept_init,
        )

class PassiveAggressiveRegressor(BaseSGDRegressor):
    """Passive Aggressive Regressor.

        data is assumed to be already centered. Defaults to True.

        :meth:`~sklearn.linear_model.PassiveAggressiveRegressor.partial_fit` method.

        Whether to use early stopping to terminate training when validation.
        a fraction of training data as validation and terminate
        training when validation score is not improving by at least tol for
        n_iter_no_change consecutive epochs.

    loss : str, default="epsilon_insensitive"
        epsilon_insensitive: equivalent to PA-I in the reference paper.
        squared_epsilon_insensitive: equivalent to PA-II in the reference
        paper.

        If the difference between the current prediction and the correct label
        is below this threshold, the model is not updated.

    coef_ : array, shape = [1, n_features] if n_classes == 2 else [n_classes,\
            n_features]

    intercept_ : array, shape = [1] if n_classes == 2 else [n_classes]

    t_ : int

    See Also
    --------
    SGDRegressor : Linear model fitted by minimizing a regularized
        empirical loss with SGD.

    K. Crammer, O. Dekel, J. Keshat, S. Shalev-Shwartz, Y. Singer - JMLR (2006).

    Examples
    --------
    >>> from sklearn.linear_model import PassiveAggressiveRegressor

    >>> regr = PassiveAggressiveRegressor(max_iter=100, random_state=0,
    PassiveAggressiveRegressor(max_iter=100, random_state=0)
    [20.48736655 34.18818427 67.59122734 87.94731329]
    [-0.02306214]
    """

        **BaseSGDRegressor._parameter_constraints,
        "epsilon": [Interval(Real, 0, None, closed="left")],
    }

        self,
        *,
        C=1.0,
        tol=1e-3,
        epsilon=DEFAULT_EPSILON,
    ):
            l1_ratio=0,
            eta0=1.0,
            tol=tol,
        )
        self.C = C

            Subset of training data.

            Subset of target values.

        Returns
        -------
        """
        if not hasattr(self, "coef_"):

        lr = "pa1" if self.loss == "epsilon_insensitive" else "pa2"
            X,
            y,
            C=self.C,
            loss="epsilon_insensitive",
        )

        coef_init : array, shape = [n_features]

        intercept_init : array, shape = [1]

        Returns
        -------
        """

            X,
            y,
            C=self.C,
        )

        self,
        *,
        C=1.0,
        tol=1e-3,
    ):
            tol=tol,
            eta0=1.0,
        )

        self.C = C

        Returns
        -------
        """

                )

            X,
            y,
            C=self.C,
        )

        Returns
        -------
        """

            X,
            y,
            C=self.C,
        )

        self,
        *,
        C=1.0,
        tol=1e-3,
    ):
            eta0=1.0,
            tol=tol,
        )
        self.C = C

        Returns
        -------
        """

            X,
            y,
            C=self.C,
        )

        Returns
        -------
        """

            X,
            y,
            C=self.C,
        )

class _ValidationScoreCallback:
    """Callback for early stopping based on validation score"""

    def __init__(self, estimator, X_val, y_val, sample_weight_val, classes=None):
        self.estimator = clone(estimator)
        self.estimator.t_ = 1  # to pass check_is_fitted
            self.estimator.classes_ = classes
        self.X_val = X_val
        self.y_val = y_val
        self.sample_weight_val = sample_weight_val

    def __call__(self, coef, intercept):
        est = self.estimator
        est.coef_ = coef.reshape(1, -1)
        est.intercept_ = np.atleast_1d(intercept)
        return est.score(self.X_val, self.y_val, self.sample_weight_val)

class BaseSGD(SparseCoefMixin, BaseEstimator, metaclass=ABCMeta):
    """Base class for SGD classification and regression."""

        "average": [Interval(Integral, 0, None, closed="neither"), "boolean"],
    }

        self,
        loss,
        *,
        C=1.0,
        tol=1e-3,
        learning_rate="optimal",
        eta0=0.0,
    ):
        self.C = C
        self.eta0 = eta0
        self.average = average

    def _more_validate_params(self, for_partial_fit=False):
        """Validate input params."""
        if self.early_stopping and for_partial_fit:
            raise ValueError("early_stopping should be False with partial_fit")
        if (
            self.learning_rate in ("constant", "invscaling", "adaptive")
            and self.eta0 <= 0.0
        ):
            raise ValueError("eta0 must be > 0")
        if self.learning_rate == "optimal" and self.alpha == 0:
                "alpha must be > 0 since "
                "learning_rate is 'optimal'. alpha is used "
                "to compute the optimal learning rate."
            )
        if self.penalty == "elasticnet" and self.l1_ratio is None:
            raise ValueError("l1_ratio must be set when penalty is 'elasticnet'")

        # raises ValueError if not registered
        self._get_penalty_type(self.penalty)
        self._get_learning_rate_type(self.learning_rate)

    def _get_l1_ratio(self):
        if self.l1_ratio is None:
            # plain_sgd expects a float. Any value is fine since at this point
            # penalty can't be "elsaticnet" so l1_ratio is not used.
            return 0.0
        return self.l1_ratio

    def _get_loss_function(self, loss):
        """Get concrete ``LossFunction`` object for str ``loss``."""
        loss_ = self.loss_functions[loss]
        loss_class, args = loss_[0], loss_[1:]
        if loss in ("huber", "epsilon_insensitive", "squared_epsilon_insensitive"):
            args = (self.epsilon,)
        return loss_class(*args)

    def _get_learning_rate_type(self, learning_rate):
        return LEARNING_RATE_TYPES[learning_rate]

    def _get_penalty_type(self, penalty):
        penalty = str(penalty).lower()
        return PENALTY_TYPES[penalty]

    def _allocate_parameter_mem(
        self,
        input_dtype,
        coef_init=None,
        intercept_init=None,
        one_class=0,
    ):
        """Allocate mem for parameters; initialize if provided."""
        if n_classes > 2:
            # allocate coef_ for multi-class
            if coef_init is not None:
                coef_init = np.asarray(coef_init, dtype=input_dtype, order="C")
                if coef_init.shape != (n_classes, n_features):
                    raise ValueError("Provided ``coef_`` does not match dataset. ")
                self.coef_ = coef_init
            else:
                self.coef_ = np.zeros(
                    (n_classes, n_features), dtype=input_dtype, order="C"
                )

            # allocate intercept_ for multi-class
            if intercept_init is not None:
                intercept_init = np.asarray(
                    intercept_init, order="C", dtype=input_dtype
                )
                if intercept_init.shape != (n_classes,):
                    raise ValueError("Provided intercept_init does not match dataset.")
                self.intercept_ = intercept_init
            else:
                self.intercept_ = np.zeros(n_classes, dtype=input_dtype, order="C")
        else:
            # allocate coef_
                coef_init = coef_init.ravel()
                if coef_init.shape != (n_features,):
                    raise ValueError("Provided coef_init does not match dataset.")
            else:
                self.coef_ = np.zeros(n_features, dtype=input_dtype, order="C")

            # allocate intercept_
                intercept_init = np.asarray(intercept_init, dtype=input_dtype)
                if intercept_init.shape != (1,) and intercept_init.shape != ():
                if one_class:
                    self.offset_ = intercept_init.reshape(
                        1,
                    )
                else:
                    self.intercept_ = intercept_init.reshape(
                        1,
                    )
            else:
                    self.offset_ = np.zeros(1, dtype=input_dtype, order="C")
                else:
                    self.intercept_ = np.zeros(1, dtype=input_dtype, order="C")

        # initialize average parameters
        if self.average > 0:
            self._standard_coef = self.coef_
            self._average_coef = np.zeros(
                self.coef_.shape, dtype=input_dtype, order="C"
            )
            if one_class:
                self._standard_intercept = 1 - self.offset_
            else:
                self._standard_intercept = self.intercept_

            self._average_intercept = np.zeros(
                self._standard_intercept.shape, dtype=input_dtype, order="C"
            )

    def _make_validation_split(self, y, sample_mask):
        """Split the dataset between training set and validation set.

        y : ndarray of shape (n_samples, )

        sample_mask : ndarray of shape (n_samples, )
            A boolean array indicating whether each sample should be included
            for validation set.

        Returns
        -------
        validation_mask : ndarray of shape (n_samples, )
            Equal to True on the validation set, False on the training set.
        """
        validation_mask = np.zeros(n_samples, dtype=np.bool_)
        if not self.early_stopping:
            # use the full set for training, with an empty validation set
            return validation_mask

            splitter_type = StratifiedShuffleSplit
        else:
            splitter_type = ShuffleSplit
        cv = splitter_type(
            test_size=self.validation_fraction, random_state=self.random_state
        )
        idx_train, idx_val = next(cv.split(np.zeros(shape=(y.shape[0], 1)), y))

        if not np.any(sample_mask[idx_val]):
                "The sample weights for validation set are all zero, consider using a"
                " different random state."
            )

        if idx_train.shape[0] == 0 or idx_val.shape[0] == 0:
                "Splitting %d samples into a train set and a validation set "
                "with validation_fraction=%r led to an empty set (%d and %d "
                "samples). Please either change validation_fraction, increase "
                "number of samples, or disable early_stopping."
                % (
                    self.validation_fraction,
                    idx_train.shape[0],
                    idx_val.shape[0],
                )
            )

        validation_mask[idx_val] = True
        return validation_mask

    def _make_validation_score_cb(
        self, validation_mask, X, y, sample_weight, classes=None
    ):

        return _ValidationScoreCallback(
            self,
            X[validation_mask],
            y[validation_mask],
            sample_weight[validation_mask],
        )

def _prepare_fit_binary(est, y, i, input_dtype, label_encode=True):
    """Initialization for fit_binary.

    Returns y, coef, intercept, average_coef, average_intercept.
    """
    y_i = np.ones(y.shape, dtype=input_dtype, order="C")
    if label_encode:
        # y in {0, 1}
        y_i[y != est.classes_[i]] = 0.0
    else:
        # y in {-1, +1}
        y_i[y != est.classes_[i]] = -1.0
    average_intercept = 0
    average_coef = None

    if len(est.classes_) == 2:
        if not est.average:
            coef = est.coef_.ravel()
            intercept = est.intercept_[0]
        else:
            coef = est._standard_coef.ravel()
            intercept = est._standard_intercept[0]
            average_coef = est._average_coef.ravel()
            average_intercept = est._average_intercept[0]
    else:
            coef = est.coef_[i]
            intercept = est.intercept_[i]
        else:
            coef = est._standard_coef[i]
            intercept = est._standard_intercept[i]
            average_coef = est._average_coef[i]
            average_intercept = est._average_intercept[i]

    return y_i, coef, intercept, average_coef, average_intercept

def fit_binary(
    est,
    i,
    X,
    y,
    alpha,
    C,
    learning_rate,
    max_iter,
    pos_weight,
    neg_weight,
    validation_mask=None,
):
    """Fit a single binary classifier.

    The i'th class is considered the "positive" class.

    est : Estimator object
        The estimator to fit

    i : int
        Index of the positive class

    X : numpy array or sparse matrix of shape [n_samples,n_features]

    y : numpy array of shape [n_samples, ]

        The regularization parameter

    C : float
        Maximum step size for passive aggressive

    learning_rate : str
        The learning rate. Accepted values are 'constant', 'optimal',
        'invscaling', 'pa1' and 'pa2'.

        The maximum number of iterations (epochs)

    pos_weight : float
        The weight of the positive class

    neg_weight : float
        The weight of the negative class

    sample_weight : numpy array of shape [n_samples, ]

    validation_mask : numpy array of shape [n_samples, ], default=None
        Precomputed validation mask in case _fit_binary is called in the
        context of a one-vs-rest reduction.

        If int, random_state is the seed used by the random number generator;
        If RandomState instance, random_state is the random number generator;
        If None, the random number generator is the RandomState instance used
        by `np.random`.
    """
    # if average is not true, average_coef, and average_intercept will be
    # unused
    label_encode = isinstance(est._loss_function_, CyHalfBinomialLoss)
    y_i, coef, intercept, average_coef, average_intercept = _prepare_fit_binary(
        est, y, i, input_dtype=X.dtype, label_encode=label_encode
    )
    assert y_i.shape[0] == y.shape[0] == sample_weight.shape[0]

    dataset, intercept_decay = make_dataset(
        X, y_i, sample_weight, random_state=random_state
    )

    penalty_type = est._get_penalty_type(est.penalty)
    learning_rate_type = est._get_learning_rate_type(learning_rate)

    if validation_mask is None:
        validation_mask = est._make_validation_split(y_i, sample_mask=sample_weight > 0)
    classes = np.array([-1, 1], dtype=y_i.dtype)
    validation_score_cb = est._make_validation_score_cb(
        validation_mask, X, y_i, sample_weight, classes=classes
    )

    # numpy mtrand expects a C long which is a signed 32 bit integer under
    # Windows
    seed = random_state.randint(MAX_INT)

    tol = est.tol if est.tol is not None else -np.inf

    _plain_sgd = _get_plain_sgd_function(input_dtype=coef.dtype)
    coef, intercept, average_coef, average_intercept, n_iter_ = _plain_sgd(
        coef,
        intercept,
        average_coef,
        average_intercept,
        est._loss_function_,
        penalty_type,
        alpha,
        C,
        est._get_l1_ratio(),
        dataset,
        validation_mask,
        est.early_stopping,
        validation_score_cb,
        int(est.n_iter_no_change),
        max_iter,
        tol,
        int(est.fit_intercept),
        int(est.verbose),
        int(est.shuffle),
        seed,
        pos_weight,
        neg_weight,
        learning_rate_type,
        est.eta0,
        est.power_t,
        0,
        est.t_,
        est.average,
    )

    if est.average:
        if len(est.classes_) == 2:
            est._average_intercept[0] = average_intercept
        else:
            est._average_intercept[i] = average_intercept

    return coef, intercept, n_iter_

def _get_plain_sgd_function(input_dtype):
    return _plain_sgd32 if input_dtype == np.float32 else _plain_sgd64

class BaseSGDClassifier(LinearClassifierMixin, BaseSGD, metaclass=ABCMeta):
    loss_functions = {
        "hinge": (Hinge, 1.0),
        "squared_hinge": (SquaredHinge, 1.0),
        "perceptron": (Hinge, 0.0),
        "log_loss": (CyHalfBinomialLoss,),
        "modified_huber": (ModifiedHuber,),
        "squared_error": (CyHalfSquaredError,),
        "huber": (CyHuberLoss, DEFAULT_EPSILON),
        "epsilon_insensitive": (EpsilonInsensitive, DEFAULT_EPSILON),
        "squared_epsilon_insensitive": (SquaredEpsilonInsensitive, DEFAULT_EPSILON),
    }

        **BaseSGD._parameter_constraints,
        "loss": [StrOptions(set(loss_functions))],
        "validation_fraction": [Interval(Real, 0, 1, closed="neither")],
    }

        self,
        *,
        tol=1e-3,
        eta0=0.0,
    ):
            tol=tol,
        )

    def _partial_fit(
        self,
        X,
        y,
        alpha,
        C,
        loss,
        max_iter,
        classes,
    ):
            self,
            X,
            y,
        )

        # Allocate datastructures from input arguments
        self._expanded_class_weight = compute_class_weight(
            self.class_weight, classes=self.classes_, y=y
        )

        if getattr(self, "coef_", None) is None or coef_init is not None:
            self._allocate_parameter_mem(
                input_dtype=X.dtype,
                coef_init=coef_init,
                intercept_init=intercept_init,
            )
        elif n_features != self.coef_.shape[-1]:
                "Number of features %d does not match previous data %d."
                % (n_features, self.coef_.shape[-1])
            )

        self._loss_function_ = self._get_loss_function(loss)
        if not hasattr(self, "t_"):
            self.t_ = 1.0

        # delegate to concrete training procedure
            self._fit_multiclass(
                X,
                y,
                C=C,
                learning_rate=learning_rate,
            )
        elif n_classes == 2:
            self._fit_binary(
                X,
                y,
                C=C,
            )
        else:
                % n_classes
            )

    def _fit(
        self,
        X,
        y,
        alpha,
        C,
        loss,
    ):
            # delete the attribute otherwise _partial_fit thinks it's not the first call
            delattr(self, "classes_")

        # labels can be encoded as float, int, or string literals
        # np.unique sorts in asc order; largest class id is positive class

            if coef_init is None:
                coef_init = self.coef_
            if intercept_init is None:
                intercept_init = self.intercept_
        else:
            self.coef_ = None
            self.intercept_ = None

            self._standard_intercept = self.intercept_
            self._average_coef = None
            self._average_intercept = None

        # Clear iteration count for multiple call to fit.
        self.t_ = 1.0

        self._partial_fit(
            X,
            y,
            alpha,
            C,
            loss,
            learning_rate,
            classes,
            coef_init,
            intercept_init,
        )

        if (
            self.tol is not None
            and self.tol > -np.inf
            and self.n_iter_ == self.max_iter
        ):
                (
                    "Maximum number of iteration reached before "
                    "convergence. Consider increasing max_iter to "
                    "improve the fit."
                ),
            )

    def _fit_binary(self, X, y, alpha, C, sample_weight, learning_rate, max_iter):
        """Fit a binary classifier on X and y."""
        coef, intercept, n_iter_ = fit_binary(
            self,
            1,
            X,
            y,
            alpha,
            C,
            max_iter,
            self._expanded_class_weight[1],
            self._expanded_class_weight[0],
        )

        self.t_ += n_iter_ * X.shape[0]
        self.n_iter_ = n_iter_

        # need to be 2d
            if self.average <= self.t_ - 1:
                self.coef_ = self._average_coef.reshape(1, -1)
                self.intercept_ = self._average_intercept
            else:
                self.coef_ = self._standard_coef.reshape(1, -1)
                self._standard_intercept = np.atleast_1d(intercept)
                self.intercept_ = self._standard_intercept
        else:
            self.coef_ = coef.reshape(1, -1)
            # intercept is a float, need to convert it to an array of length 1
            self.intercept_ = np.atleast_1d(intercept)

    def _fit_multiclass(self, X, y, alpha, C, learning_rate, sample_weight, max_iter):
        """Fit a multi-class classifier by combining binary classifiers

        Each binary classifier predicts one class versus all others. This
        strategy is called OvA (One versus All) or OvR (One versus Rest).
        """
        # Precompute the validation split using the multiclass labels
        # to ensure proper balancing of the classes.
        validation_mask = self._make_validation_split(y, sample_mask=sample_weight > 0)

        # Use joblib to fit OvA in parallel.
        # Pick the random seed for each job outside of fit_binary to avoid
        # sharing the estimator random state between threads which could lead
        # to non-deterministic behavior
        seeds = random_state.randint(MAX_INT, size=len(self.classes_))
        result = Parallel(
            n_jobs=self.n_jobs, verbose=self.verbose, require="sharedmem"
        )(
            delayed(fit_binary)(
                self,
                i,
                X,
                y,
                alpha,
                C,
                learning_rate,
                max_iter,
                self._expanded_class_weight[i],
                1.0,
                validation_mask=validation_mask,
            )
            for i, seed in enumerate(seeds)
        )

        # take the maximum of n_iter_ over every binary fit
        n_iter_ = 0.0
        for i, (_, intercept, n_iter_i) in enumerate(result):
            self.intercept_[i] = intercept
            n_iter_ = max(n_iter_, n_iter_i)

            if self.average <= self.t_ - 1.0:
                self.coef_ = self._average_coef
            else:
                self.coef_ = self._standard_coef
                self._standard_intercept = np.atleast_1d(self.intercept_)

        """Perform one epoch of stochastic gradient descent on given samples.

        Internally, this method uses ``max_iter = 1``. Therefore, it is not
        guaranteed that a minimum of the cost function is reached after calling
        it once. Matters such as objective convergence, early stopping, and
        learning rate adjustments should be handled by the user.

        classes : ndarray of shape (n_classes,), default=None

        sample_weight : array-like, shape (n_samples,), default=None
            Weights applied to individual samples.
            If not provided, uniform weights are assumed.

        Returns
        -------
        """

                    "class_weight '{0}' is not supported for "
                    "partial_fit. In order to use 'balanced' weights,"
                    " use compute_class_weight('{0}', "
                    "classes=classes, y=y). "
                    "In place of y you can use a large enough sample "
                    "of the full training set target to properly "
                    "estimate the class frequency distributions. "
                    "Pass the resulting weights as the class_weight "
                    "parameter.".format(self.class_weight)
                )

            X,
            y,
            C=1.0,
            learning_rate=self.learning_rate,
        )

    def fit(self, X, y, coef_init=None, intercept_init=None, sample_weight=None):
        """Fit linear model with Stochastic Gradient Descent.

        coef_init : ndarray of shape (n_classes, n_features), default=None

        intercept_init : ndarray of shape (n_classes,), default=None

            If not provided, uniform weights are assumed. These weights will
            be multiplied with class_weight (passed through the
            constructor) if class_weight is specified.

        Returns
        -------
        """

            X,
            y,
            C=1.0,
        )

class SGDClassifier(BaseSGDClassifier):
    """Linear classifiers (SVM, logistic regression, etc.) with SGD training.

    This estimator implements regularized linear models with stochastic
    gradient descent (SGD) learning: the gradient of the loss is estimated
    each sample at a time and the model is updated along the way with a
    decreasing strength schedule (aka learning rate). SGD allows minibatch
    (online/out-of-core) learning via the `partial_fit` method.
    For best results using the default learning rate schedule, the data should
    have zero mean and unit variance.

    This implementation works with data represented as dense or sparse arrays
    of floating point values for the features. The model it fits can be
    controlled with the loss parameter; by default, it fits a linear support
    vector machine (SVM).

    parameters towards the zero vector using either the squared euclidean norm
    L2 or the absolute norm L1 or a combination of both (Elastic Net). If the
    parameter update crosses the 0.0 value because of the regularizer, the
    update is truncated to 0.0 to allow for learning sparse models and achieve
    online feature selection.

    Read more in the :ref:`User Guide <sgd>`.

    loss : {'hinge', 'log_loss', 'modified_huber', 'squared_hinge',\
        'perceptron', 'squared_error', 'huber', 'epsilon_insensitive',\
        'squared_epsilon_insensitive'}, default='hinge'
        The loss function to be used.

        - 'hinge' gives a linear SVM.
        - 'log_loss' gives logistic regression, a probabilistic classifier.
        - 'modified_huber' is another smooth loss that brings tolerance to
          outliers as well as probability estimates.
        - 'squared_hinge' is like hinge but is quadratically penalized.
        - 'perceptron' is the linear loss used by the perceptron algorithm.
        - The other losses, 'squared_error', 'huber', 'epsilon_insensitive' and
          'squared_epsilon_insensitive' are designed for regression but can be useful
          in classification as well; see
          :class:`~sklearn.linear_model.SGDRegressor` for a description.

        More details about the losses formulas can be found in the :ref:`User Guide
        <sgd_mathematical_formulation>` and you can find a visualisation of the loss
        functions in
        :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_loss_functions.py`.

    penalty : {'l2', 'l1', 'elasticnet', None}, default='l2'
        The penalty (aka regularization term) to be used. Defaults to 'l2'
        which is the standard regularizer for linear SVM models. 'l1' and
        'elasticnet' might bring sparsity to the model (feature selection)
        not achievable with 'l2'. No penalty is added when set to `None`.

        You can see a visualisation of the penalties in
        :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_penalties.py`.

        Constant that multiplies the regularization term. The higher the
        value, the stronger the regularization. Also used to compute the
        learning rate when `learning_rate` is set to 'optimal'.

        The Elastic Net mixing parameter, with 0 <= l1_ratio <= 1.
        l1_ratio=0 corresponds to L2 penalty, l1_ratio=1 to L1.
        Only used if `penalty` is 'elasticnet'.
        Values must be in the range `[0.0, 1.0]` or can be `None` if
        `penalty` is not `elasticnet`.

            `l1_ratio` can be `None` when `penalty` is not "elasticnet".

        The stopping criterion. If it is not None, training will stop
        when (loss > best_loss - tol) for ``n_iter_no_change`` consecutive
        epochs.
        Convergence is checked against the training loss or the
        validation loss depending on the `early_stopping` parameter.

        Epsilon in the epsilon-insensitive loss functions; only if `loss` is
        'huber', 'epsilon_insensitive', or 'squared_epsilon_insensitive'.
        For 'huber', determines the threshold at which it becomes less
        important to get the prediction exactly right.
        For epsilon-insensitive, any differences between the current prediction
        and the correct label are ignored if they are less than this threshold.

        Used for shuffling the data, when ``shuffle`` is set to ``True``.
        Integer values must be in the range `[0, 2**32 - 1]`.

    learning_rate : str, default='optimal'
        The learning rate schedule:

        - 'constant': `eta = eta0`
        - 'optimal': `eta = 1.0 / (alpha * (t + t0))`
          where `t0` is chosen by a heuristic proposed by Leon Bottou.
        - 'invscaling': `eta = eta0 / pow(t, power_t)`
        - 'adaptive': `eta = eta0`, as long as the training keeps decreasing.
          Each time n_iter_no_change consecutive epochs fail to decrease the
          training loss by tol or fail to increase validation score by tol if
          `early_stopping` is `True`, the current learning rate is divided by 5.

            Added 'adaptive' option.

    eta0 : float, default=0.0
        The initial learning rate for the 'constant', 'invscaling' or
        'adaptive' schedules. The default value is 0.0 as eta0 is not used by
        the default schedule 'optimal'.

        Values must be in the range `(-inf, inf)`.

        score is not improving. If set to `True`, it will automatically set aside
        training when validation score returned by the `score` method is not
        improving by at least tol for n_iter_no_change consecutive epochs.

        See :ref:`sphx_glr_auto_examples_linear_model_plot_sgd_early_stopping.py` for an
        example of the effects of early stopping.

            Added 'early_stopping' option

        Only used if `early_stopping` is True.

            Added 'validation_fraction' option

        Number of iterations with no improvement to wait before stopping
        fitting.
        Integer values must be in the range `[1, max_iter)`.

            Added 'n_iter_no_change' option

        If a dynamic learning rate is used, the learning rate is adapted
        depending on the number of samples already seen. Calling ``fit`` resets
        this counter, while ``partial_fit`` will result in increasing the
        existing counter.

        When set to `True`, computes the averaged SGD weights across all
        updates and stores the result in the ``coef_`` attribute. If set to
        an int greater than 1, averaging will begin once the total number of
        samples seen reaches `average`. So ``average=10`` will begin
        averaging after seeing 10 samples.
        Integer values must be in the range `[1, n_samples]`.

        The actual number of iterations before reaching the stopping criterion.

    t_ : int

    See Also
    --------
    sklearn.svm.LinearSVC : Linear support vector classification.
    LogisticRegression : Logistic regression.
    Perceptron : Inherits from SGDClassifier. ``Perceptron()`` is equivalent to
        ``SGDClassifier(loss="perceptron", eta0=1, learning_rate="constant",
        penalty=None)``.

    Examples
    --------
    >>> Y = np.array([1, 1, 2, 2])
    >>> # Always scale the input. The most convenient way is to use a pipeline.
    ...                     SGDClassifier(max_iter=1000, tol=1e-3))
                    ('sgdclassifier', SGDClassifier())])
    [1]
    """

        "penalty": [StrOptions({"l2", "l1", "elasticnet"}), None],
        "l1_ratio": [Interval(Real, 0, 1, closed="both"), None],
        "power_t": [Interval(Real, None, None, closed="neither")],
            StrOptions({"constant", "optimal", "invscaling", "adaptive"}),
            Hidden(StrOptions({"pa1", "pa2"})),
        ],
        "eta0": [Interval(Real, 0, None, closed="left")],
    }

        self,
        *,
        tol=1e-3,
        eta0=0.0,
    ):
            tol=tol,
        )

        if self.loss not in ("log_loss", "modified_huber"):
                "probability estimates are not available for loss=%r" % self.loss
            )

        This method is only available for log loss and modified Huber loss.

        Multiclass probability estimates are derived from binary (one-vs.-rest)
        estimates by simple normalization, as recommended by Zadrozny and
        Elkan.

        Binary probability estimates for loss="modified_huber" are given by
        (clip(decision_function(X), -1, 1) + 1) / 2. For other loss functions
        it is necessary to perform proper probability calibration by wrapping
        the classifier with
        :class:`~sklearn.calibration.CalibratedClassifierCV` instead.

            Input data for prediction.

        Returns
        -------
        ndarray of shape (n_samples, n_classes)

        Zadrozny and Elkan, "Transforming classifier scores into multiclass
        probability estimates", SIGKDD'02,
        https://dl.acm.org/doi/pdf/10.1145/775047.775151

        The justification for the formula in the loss="modified_huber"
        case is in the appendix B in:
        http://jmlr.csail.mit.edu/papers/volume2/zhang02c/zhang02c.pdf
        """

            return self._predict_proba_lr(X)

        elif self.loss == "modified_huber":
            binary = len(self.classes_) == 2
            scores = self.decision_function(X)

            if binary:
                prob2 = np.ones((scores.shape[0], 2))
                prob = prob2[:, 1]
            else:
                prob = scores

            np.clip(scores, -1, 1, prob)
            prob += 1.0
            prob /= 2.0

            if binary:
                prob2[:, 0] -= prob
                prob = prob2
            else:
                # the above might assign zero to all classes, which doesn't
                # normalize neatly; work around this to produce uniform
                # probabilities
                prob_sum = prob.sum(axis=1)
                all_zero = prob_sum == 0
                if np.any(all_zero):
                    prob[all_zero, :] = 1
                    prob_sum[all_zero] = len(self.classes_)

                # normalize
                prob /= prob_sum.reshape((prob.shape[0], -1))

        else:
                "predict_(log_)proba only supported when"
                " loss='log_loss' or loss='modified_huber' "
                "(%r given)" % self.loss
            )

        """Log of probability estimates.

        When loss="modified_huber", probability estimates may be hard zeros
        and ones, so taking the logarithm is not possible.

        See ``predict_proba`` for details.

        Returns
        -------
        T : array-like, shape (n_samples, n_classes)
            Returns the log-probability of the sample for each class in the
            model, where classes are ordered as they are in
            `self.classes_`.
        """

class BaseSGDRegressor(RegressorMixin, BaseSGD):
    }

    }

        self,
        *,
        tol=1e-3,
        learning_rate="invscaling",
        eta0=0.01,
        power_t=0.25,
    ):
            tol=tol,
        )

        self,
        X,
        y,
        alpha,
        C,
        loss,
        max_iter,
    ):
        first_call = getattr(self, "coef_", None) is None
            self,
            X,
            y,
        )
        y = y.astype(X.dtype, copy=False)

                n_classes=1,
            )
        if self.average > 0 and getattr(self, "_average_coef", None) is None:
            self._average_coef = np.zeros(n_features, dtype=X.dtype, order="C")
            self._average_intercept = np.zeros(1, dtype=X.dtype, order="C")

        self._fit_regressor(
            X, y, alpha, C, loss, learning_rate, sample_weight, max_iter
        )

        it once. Matters such as objective convergence and early stopping
        should be handled by the user.

        y : numpy array of shape (n_samples,)

        Returns
        -------
        """

            X,
            y,
            self.alpha,
            C=1.0,
        )

    def _fit(
        self,
        X,
        y,
        alpha,
        C,
        loss,
    ):
        if self.warm_start and getattr(self, "coef_", None) is not None:
        else:

            X,
            y,
            alpha,
            C,
            loss,
        )

        if (
        ):
                (
                ),
            )

        coef_init : ndarray of shape (n_features,), default=None

        intercept_init : ndarray of shape (1,), default=None

        Returns
        -------
            Fitted `SGDRegressor` estimator.
        """

            X,
            y,
            C=1.0,
        )

        """Predict using the linear model

        Returns
        -------
        ndarray of shape (n_samples,)
           Predicted target values per element in X.
        """

        return scores.ravel()

        Returns
        -------
        """

    def _fit_regressor(
        self, X, y, alpha, C, loss, learning_rate, sample_weight, max_iter
    ):
        loss_function = self._get_loss_function(loss)
        penalty_type = self._get_penalty_type(self.penalty)
        learning_rate_type = self._get_learning_rate_type(learning_rate)

        validation_score_cb = self._make_validation_score_cb(
            validation_mask, X, y, sample_weight
        )

        # numpy mtrand expects a C long which is a signed 32 bit integer under
        # Windows
        seed = random_state.randint(0, MAX_INT)

        dataset, intercept_decay = make_dataset(
            X, y, sample_weight, random_state=random_state
        )

        tol = self.tol if self.tol is not None else -np.inf

        if self.average:
            coef = self._standard_coef
            intercept = self._standard_intercept
            average_coef = self._average_coef
            average_intercept = self._average_intercept
        else:
            coef = self.coef_
            intercept = self.intercept_
            average_coef = None  # Not used
            average_intercept = [0]  # Not used

        _plain_sgd = _get_plain_sgd_function(input_dtype=coef.dtype)
        coef, intercept, average_coef, average_intercept, self.n_iter_ = _plain_sgd(
            coef,
            intercept[0],
            average_coef,
            average_intercept[0],
            loss_function,
            penalty_type,
            alpha,
            C,
            self._get_l1_ratio(),
            dataset,
            validation_mask,
            self.early_stopping,
            validation_score_cb,
            int(self.n_iter_no_change),
            max_iter,
            tol,
            int(self.fit_intercept),
            int(self.verbose),
            int(self.shuffle),
            seed,
            1.0,
            1.0,
            learning_rate_type,
            self.eta0,
            self.power_t,
            0,
            self.t_,
            intercept_decay,
            self.average,
        )

        self.t_ += self.n_iter_ * X.shape[0]

            self._average_intercept = np.atleast_1d(average_intercept)
            self._standard_intercept = np.atleast_1d(intercept)

                # made enough updates for averaging to be taken into account
                self.coef_ = average_coef
                self.intercept_ = np.atleast_1d(average_intercept)
            else:
                self.coef_ = coef
                self.intercept_ = np.atleast_1d(intercept)

        else:

class SGDRegressor(BaseSGDRegressor):
    """Linear model fitted by minimizing a regularized empirical loss with SGD.

    SGD stands for Stochastic Gradient Descent: the gradient of the loss is
    a decreasing strength schedule (aka learning rate).

    This implementation works with data represented as dense numpy arrays of
    floating point values for the features.

    loss : str, default='squared_error'
        The loss function to be used. The possible values are 'squared_error',
        'huber', 'epsilon_insensitive', or 'squared_epsilon_insensitive'

        The 'squared_error' refers to the ordinary least squares fit.
        'huber' modifies 'squared_error' to focus less on getting outliers
        correct by switching from squared to linear loss past a distance of
        epsilon. 'epsilon_insensitive' ignores errors less than epsilon and is
        linear past that; this is the loss function used in SVR.
        'squared_epsilon_insensitive' is the same but becomes squared loss past
        a tolerance of epsilon.

        More details about the losses formulas can be found in the
        :ref:`User Guide <sgd_mathematical_formulation>`.

        epochs.

    learning_rate : str, default='invscaling'

          where t0 is chosen by a heuristic proposed by Leon Bottou.
        - 'adaptive': eta = eta0, as long as the training keeps decreasing.
          early_stopping is True, the current learning rate is divided by 5.

    eta0 : float, default=0.01
        'adaptive' schedules. The default value is 0.01.

    power_t : float, default=0.25

        improving by at least `tol` for `n_iter_no_change` consecutive
        epochs.

        fitting.

        this counter, while ``partial_fit``  will result in increasing the

        When set to True, computes the averaged SGD weights across all

        The intercept term.

    t_ : int

    See Also
    --------
    Lars : Least Angle Regression model.
    Lasso : Linear Model trained with L1 prior as regularizer.
    Ridge : Linear least squares with l2 regularization.
    sklearn.svm.SVR : Epsilon-Support Vector Regression.

    Examples
    --------
    >>> from sklearn.linear_model import SGDRegressor
    >>> reg = make_pipeline(StandardScaler(),
    ...                     SGDRegressor(max_iter=1000, tol=1e-3))
    >>> reg.fit(X, y)
                    ('sgdregressor', SGDRegressor())])
    """

        ],
    }

        self,
        *,
        tol=1e-3,
    ):
            tol=tol,
        )

class SGDOneClassSVM(OutlierMixin, BaseSGD):
    """Solves linear One-Class SVM using Stochastic Gradient Descent.

    This implementation is meant to be used with a kernel approximation
    technique (e.g. `sklearn.kernel_approximation.Nystroem`) to obtain results
    similar to `sklearn.svm.OneClassSVM` which uses a Gaussian kernel by
    default.

    Read more in the :ref:`User Guide <sgd_online_one_class_svm>`.

        The nu parameter of the One Class SVM: an upper bound on the
        fraction of training errors and a lower bound of the fraction of
        support vectors. Should be in the interval (0, 1]. By default 0.5

        Whether the intercept should be estimated or not. Defaults to True.

        `partial_fit`. Defaults to 1000.

        when (loss > previous_loss - tol). Defaults to 1e-3.

        Defaults to True.

        The seed of the pseudo random number generator to use when shuffling
        the data.  If int, random_state is the seed used by the random number
        generator; If RandomState instance, random_state is the random number
        generator; If None, the random number generator is the RandomState
        instance used by `np.random`.

    learning_rate : {'constant', 'optimal', 'invscaling', 'adaptive'}, default='optimal'
        The learning rate schedule to use with `fit`. (If using `partial_fit`,
        learning rate must be controlled directly).

        average. So ``average=10`` will begin averaging after seeing 10
        samples.

    offset_ : ndarray of shape (1,)
        We have the relation: decision_function = score_samples - offset.

    t_ : int

    See Also
    --------

    Notes
    -----
    This estimator has a linear complexity in the number of training samples
    and is thus better suited than the `sklearn.svm.OneClassSVM`
    implementation for datasets with a large number of training samples (say
    > 10,000).

    Examples
    --------
    >>> clf = linear_model.SGDOneClassSVM(random_state=42)
    >>> clf.fit(X)
    SGDOneClassSVM(random_state=42)

    >>> print(clf.predict([[4, 4]]))
    [1]
    """

    loss_functions = {"hinge": (Hinge, 1.0)}

        ],
    }

        self,
        nu=0.5,
        tol=1e-3,
        eta0=0.0,
    ):
            penalty="l2",
            C=1.0,
            tol=tol,
            epsilon=DEFAULT_EPSILON,
            early_stopping=False,
            validation_fraction=0.1,
            n_iter_no_change=5,
        )

    def _fit_one_class(self, X, alpha, C, sample_weight, learning_rate, max_iter):
        """Uses SGD implementation with X and y=np.ones(n_samples)."""

        # The One-Class SVM uses the SGD implementation with
        # y=np.ones(n_samples).
        y = np.ones(n_samples, dtype=X.dtype, order="C")

        dataset, offset_decay = make_dataset(X, y, sample_weight)

        # early stopping is set to False for the One-Class SVM. thus
        # validation_mask and validation_score_cb will be set to values
        # associated to early_stopping=False in _make_validation_split and
        # _make_validation_score_cb respectively.
        )

        # Windows
        seed = random_state.randint(0, np.iinfo(np.int32).max)

        one_class = 1
        # There are no class weights for the One-Class SVM and they are
        # therefore set to 1.
        pos_weight = 1
        neg_weight = 1

        else:
            intercept = 1 - self.offset_

            coef,
            self._loss_function_,
            alpha,
            C,
            self.l1_ratio,
            dataset,
            max_iter,
            tol,
            seed,
            neg_weight,
            pos_weight,
            one_class,
            self.t_,
            offset_decay,
        )

        self.t_ += self.n_iter_ * n_samples

                self.offset_ = 1 - np.atleast_1d(average_intercept)
            else:
                self.offset_ = 1 - np.atleast_1d(intercept)

        else:
            self.offset_ = 1 - np.atleast_1d(intercept)

        self,
        X,
        alpha,
        C,
        loss,
        max_iter,
        offset_init,
    ):
            self,
            X,
            None,
        )

        # We use intercept = 1 - offset where intercept is the intercept of
        # the SGD implementation and offset is the offset of the One-Class SVM
        # optimization problem.
                intercept_init=offset_init,
                one_class=1,
            )
            )

        if self.average and getattr(self, "_average_coef", None) is None:

        self._fit_one_class(
            X,
            C=C,
        )

        """Fit linear One-Class SVM with Stochastic Gradient Descent.

        y : Ignored

        sample_weight : array-like, shape (n_samples,), optional

        Returns
        -------
        """

        alpha = self.nu / 2
            X,
            alpha,
            C=1.0,
            offset_init=None,
        )

    def _fit(
        self,
        X,
        alpha,
        C,
        loss,
        offset_init=None,
    ):
            if offset_init is None:
                offset_init = self.offset_
        else:
            self.offset_ = None

            X,
            alpha,
            C,
            loss,
            offset_init,
        )

        if (
        ):
                (
                ),
            )

    def fit(self, X, y=None, coef_init=None, offset_init=None, sample_weight=None):

        This solves an equivalent optimization problem of the
        One-Class SVM primal optimization problem and returns a weight vector
        w and an offset rho such that the decision function is given by
        <w, x> - rho.

        y : Ignored

        coef_init : array, shape (n_classes, n_features)

        offset_init : array, shape (n_classes,)
            The initial offset to warm-start the optimization.

        Returns
        -------
        """

        self._fit(
            X,
            C=1.0,
            offset_init=offset_init,
        )

        Signed distance is positive for an inlier and negative for an
        outlier.

            Testing data.

        Returns
        -------
        dec : array-like, shape (n_samples,)
            Decision function values of the samples.
        """

        check_is_fitted(self, "coef_")

        decisions = safe_sparse_dot(X, self.coef_.T, dense_output=True) - self.offset_

        return decisions.ravel()

        Returns
        -------
        score_samples : array-like, shape (n_samples,)
            Unshiffted scoring function values of the samples.
        """
        score_samples = self.decision_function(X) + self.offset_
        return score_samples

        """Return labels (1 inlier, -1 outlier) of the samples.

        Returns
        -------
        y : array, shape (n_samples,)
            Labels of the samples.
        """
        y = (self.decision_function(X) >= 0).astype(np.int32)
        y[y == 0] = -1  # for consistency with outlier detectors
        return y

        self,
        loss,
        *,
        C=1.0,
        tol=1e-3,
        eta0=0.0,
    ):
        self.C = C

        if (
        ):
            )

            return 0.0

        self,
    ):
            else:
                )

                )
            else:
        else:
            else:

                        1,
                    )
                else:
                        1,
                    )
            else:
                else:

            )
            else:

            )

        Returns
        -------
        """

        else:
        )

            )

                % (
                )
            )

    ):

            self,
        )

        self,
        *,
        tol=1e-3,
        eta0=0.0,
    ):
            tol=tol,
        )

        self,
        X,
        y,
        alpha,
        C,
        loss,
        max_iter,
        classes,
    ):
            self,
            X,
            y,
        )

        )

            )
            )

                X,
                y,
                C=C,
            )
                X,
                y,
                C=C,
            )
        else:
            )

    def _fit(
        self,
        X,
        y,
        alpha,
        C,
        loss,
    ):

        else:

            X,
            y,
            alpha,
            C,
            loss,
            classes,
        )

        if (
        ):
                (
                ),
            )

            self,
            1,
            X,
            y,
            alpha,
            C,
            max_iter,
        )

            else:
        else:

        """

        )(
                self,
                i,
                X,
                y,
                alpha,
                C,
                max_iter,
                1.0,
            )
        )

            else:

        Returns
        -------
        """

                )

            X,
            y,
            C=1.0,
        )

        Returns
        -------
        """

            X,
            y,
            C=1.0,
        )

        self,
        *,
        tol=1e-3,
        eta0=0.0,
    ):
            tol=tol,
        )

            )

        Elkan.

        Returns
        -------

        """

            if binary:
            else:

            prob += 1.0
            prob /= 2.0

            if binary:
            else:

        else:
            )

        Returns
        -------
        """

        self,
        *,
        tol=1e-3,
    ):
            tol=tol,
        )

        self,
        X,
        y,
        alpha,
        C,
        loss,
        max_iter,
    ):
            self,
            X,
            y,
        )

            )

        )

        Returns
        -------
        """

            X,
            y,
            C=1.0,
        )

    def _fit(
        self,
        X,
        y,
        alpha,
        C,
        loss,
    ):
        else:

            X,
            y,
            alpha,
            C,
            loss,
        )

        if (
        ):
                (
                ),
            )

        Returns
        -------
        """

            X,
            y,
            C=1.0,
        )

        Returns
        -------
        """

        Returns
        -------
        """

    ):

        )

        # Windows

        )

        else:

            coef,
            alpha,
            C,
            dataset,
            max_iter,
            tol,
            seed,
            1.0,
            1.0,
            0,
            self.t_,
        )

            else:

        else:

        self,
        nu=0.5,
        tol=1e-3,
        eta0=0.0,
    ):
            C=1.0,
            tol=tol,
        )

        )

        # Windows

        else:

            coef,
            alpha,
            C,
            dataset,
            max_iter,
            tol,
            seed,
            self.t_,
        )

            else:

        else:

        self,
        X,
        alpha,
        C,
        loss,
        max_iter,
    ):
            self,
            X,
            None,
        )

            )
            )

            X,
            C=C,
        )

        y : Ignored

        Returns
        -------
        """

            X,
            alpha,
            C=1.0,
        )

    def _fit(
        self,
        X,
        alpha,
        C,
        loss,
    ):
        else:

            X,
            alpha,
            C,
            loss,
        )

        if (
        ):
                (
                ),
            )

        y : Ignored

        Returns
        -------
        """

            X,
            C=1.0,
        )

        outlier.

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """
        return y

def sandwich_dot(X, W):
    """Compute the sandwich product X.T @ diag(W) @ X."""
    # TODO: This "sandwich product" is the main computational bottleneck for solvers
    # that use the full hessian matrix. Here, thread parallelism would pay-off the
    # most.
    # While a dedicated Cython routine could exploit the symmetry, it is very hard to
    # beat BLAS GEMM, even thought the latter cannot exploit the symmetry, unless one
    # pays the price of taking square roots and implements
    #    sqrtWX = sqrt(W)[: None] * X
    #    return sqrtWX.T @ sqrtWX
    # which (might) detect the symmetry and use BLAS SYRK under the hood.
        return (
            X.T @ sparse.dia_matrix((W, 0), shape=(n_samples, n_samples)) @ X
    else:
        # np.einsum may use less memory but the following, using BLAS matrix
        # multiplication (gemm), is by far faster.
        WX = W[:, None] * X
        return X.T @ WX

class LinearModelLoss:
    """General class for loss functions with raw_prediction = X @ coef + intercept.

    Note that raw_prediction is also known as linear predictor.

    The loss is the average of per sample losses and includes a term for L2
    regularization::

        loss = 1 / s_sum * sum_i s_i loss(y_i, X_i @ coef + intercept)
               + 1/2 * l2_reg_strength * ||coef||_2^2

    with sample weights s_i=1 if sample_weight=None and s_sum=sum_i s_i.

    Gradient and hessian, for simplicity without intercept, are::

        gradient = 1 / s_sum * X.T @ loss.gradient + l2_reg_strength * coef
        hessian = 1 / s_sum * X.T @ diag(loss.hessian) @ X
                  + l2_reg_strength * identity

        if fit_intercept:
            n_dof =  n_features + 1
        else:
            n_dof = n_features

        if base_loss.is_multiclass:
            coef.shape = (n_classes, n_dof) or ravelled (n_classes * n_dof,)
        else:
            coef.shape = (n_dof,)

        The intercept term is at the end of the coef array:
            if coef.shape (n_classes, n_dof):
                intercept = coef[:, -1]
            if coef.shape (n_classes * n_dof,)
                intercept = coef[n_features::n_dof] = coef[(n_dof-1)::n_dof]
            intercept.shape = (n_classes,)
        else:
            intercept = coef[-1]

        Shape of gradient follows shape of coef.
        gradient.shape = coef.shape

        But hessian (to make our lives simpler) are always 2-d:
            hessian.shape = (n_classes * n_dof, n_classes * n_dof)
        else:
            hessian.shape = (n_dof, n_dof)

    Note: If coef has shape (n_classes * n_dof,), the 2d-array can be reconstructed as

        coef.reshape((n_classes, -1), order="F")

    The option order="F" makes coef[:, i] contiguous. This, in turn, makes the
    coefficients without intercept, coef[:, :-1], contiguous and speeds up
    matrix-vector computations.

    Note: If the average loss per sample is wanted instead of the sum of the loss per
    sample, one can simply use a rescaled sample_weight such that
    sum(sample_weight) = 1.

    base_loss : instance of class BaseLoss from sklearn._loss.
    """

    def __init__(self, base_loss, fit_intercept):
        self.base_loss = base_loss

    def init_zero_coef(self, X, dtype=None):
        """Allocate coef of correct shape with zeros.

        -----------
        dtype : data-type, default=None
            Overrides the data type of coef. With dtype=None, coef will have the same
            dtype as X.

        Returns
        -------
        coef : ndarray of shape (n_dof,) or (n_classes, n_dof)
            Coefficients of a linear model.
        """
        n_classes = self.base_loss.n_classes
            n_dof = n_features + 1
        else:
        if self.base_loss.is_multiclass:
            coef = np.zeros_like(X, shape=(n_classes, n_dof), dtype=dtype, order="F")
        else:
            coef = np.zeros_like(X, shape=n_dof, dtype=dtype)

    def weight_intercept(self, coef):
        """Helper function to get coefficients and intercept.

        coef : ndarray of shape (n_dof,), (n_classes, n_dof) or (n_classes * n_dof,)
            If shape (n_classes * n_dof,), the classes of one feature are contiguous,
            i.e. one reconstructs the 2d-array via
            coef.reshape((n_classes, -1), order="F").

        Returns
        -------
        weights : ndarray of shape (n_features,) or (n_classes, n_features)
            Coefficients without intercept term.
        intercept : float or ndarray of shape (n_classes,)
            Intercept terms.
        """
        if not self.base_loss.is_multiclass:
                intercept = coef[-1]
                weights = coef[:-1]
            else:
                intercept = 0.0
                weights = coef
        else:
            # reshape to (n_classes, n_dof)
            if coef.ndim == 1:
                weights = coef.reshape((self.base_loss.n_classes, -1), order="F")
            else:
                intercept = weights[:, -1]
                weights = weights[:, :-1]
            else:

        return weights, intercept

    def weight_intercept_raw(self, coef, X):
        """Helper function to get coefficients, intercept and raw_prediction.

        Returns
        -------
        raw_prediction : ndarray of shape (n_samples,) or \
        """
        weights, intercept = self.weight_intercept(coef)

            raw_prediction = X @ weights + intercept
        else:
            # weights has shape (n_classes, n_dof)
            raw_prediction = X @ weights.T + intercept  # ndarray, likely C-contiguous

        return weights, intercept, raw_prediction

    def l2_penalty(self, weights, l2_reg_strength):
        """Compute L2 penalty term l2_reg_strength/2 *||w||_2^2."""
        norm2_w = weights @ weights if weights.ndim == 1 else squared_norm(weights)
        return 0.5 * l2_reg_strength * norm2_w

    def loss(
        self,
        coef,
        X,
        y,
        l2_reg_strength=0.0,
        raw_prediction=None,
    ):
        """Compute the loss as weighted average over point-wise losses.

        y : contiguous array of shape (n_samples,)
        sample_weight : None or contiguous array of shape (n_samples,), default=None
        l2_reg_strength : float, default=0.0
            L2 regularization strength
            Raw prediction values (in link space). If provided, these are used. If
            None, then raw_prediction = X @ coef + intercept is calculated.

        Returns
        -------
            Weighted average of losses per sample, plus penalty.
        """
        if raw_prediction is None:
            weights, intercept, raw_prediction = self.weight_intercept_raw(coef, X)
        else:
            weights, intercept = self.weight_intercept(coef)

        loss = self.base_loss.loss(
            y_true=y,
        )
        loss = np.average(loss, weights=sample_weight)

        return loss + self.l2_penalty(weights, l2_reg_strength)

        self,
        coef,
        X,
        y,
    ):
        """Computes the sum of loss and gradient w.r.t. coef.

        Returns
        -------

        gradient : ndarray of shape coef.shape
             The gradient of the loss.
        """
        (n_samples, n_features), n_classes = X.shape, self.base_loss.n_classes
        n_dof = n_features + int(self.fit_intercept)

        else:

        loss, grad_pointwise = self.base_loss.loss_gradient(
            y_true=y,
        )
        sw_sum = n_samples if sample_weight is None else np.sum(sample_weight)
        loss = loss.sum() / sw_sum
        loss += self.l2_penalty(weights, l2_reg_strength)

        grad_pointwise /= sw_sum

            grad = np.empty_like(coef, dtype=weights.dtype)
            grad[:n_features] = X.T @ grad_pointwise + l2_reg_strength * weights
                grad[-1] = grad_pointwise.sum()
        else:
            grad = np.empty((n_classes, n_dof), dtype=weights.dtype, order="F")
            # grad_pointwise.shape = (n_samples, n_classes)
            grad[:, :n_features] = grad_pointwise.T @ X + l2_reg_strength * weights
                grad[:, -1] = grad_pointwise.sum(axis=0)
                grad = grad.ravel(order="F")

        self,
        coef,
        X,
        y,
    ):
        """Computes the gradient w.r.t. coef.

        Returns
        -------
        """

        else:

        grad_pointwise = self.base_loss.gradient(
            y_true=y,
        )

            return grad
        else:
            # gradient.shape = (n_samples, n_classes)
                return grad.ravel(order="F")
            else:
                return grad

        self,
        coef,
        X,
        y,
    ):
        """Computes gradient and hessian w.r.t. coef.

        gradient_out : None or ndarray of shape coef.shape
        hessian_out : None or ndarray of shape (n_dof, n_dof) or \
            (n_classes * n_dof, n_classes * n_dof)

        Returns
        -------

        hessian : ndarray of shape (n_dof, n_dof) or \
            (n_classes, n_dof, n_dof, n_classes)
            Hessian matrix.

        hessian_warning : bool
            True if pointwise hessian has more than 25% of its elements non-positive.
        """
        else:

        # Allocate gradient.
            grad = np.empty_like(coef, dtype=weights.dtype, order="F")
        elif gradient_out.shape != coef.shape:
                f"gradient_out is required to have shape coef.shape = {coef.shape}; "
                f"got {gradient_out.shape}."
            )
        elif self.base_loss.is_multiclass and not gradient_out.flags.f_contiguous:
            raise ValueError("gradient_out must be F-contiguous.")
        else:
            grad = gradient_out
        # Allocate hessian.
        n = coef.size  # for multinomial this equals n_dof * n_classes
        if hessian_out is None:
            hess = np.empty((n, n), dtype=weights.dtype)
        elif hessian_out.shape != (n, n):
                f"hessian_out is required to have shape ({n, n}); got "
                f"{hessian_out.shape=}."
            )
        elif self.base_loss.is_multiclass and (
            not hessian_out.flags.c_contiguous and not hessian_out.flags.f_contiguous
        ):
            raise ValueError("hessian_out must be contiguous.")
        else:
            hess = hessian_out

            grad_pointwise, hess_pointwise = self.base_loss.gradient_hessian(
                y_true=y,
            )
            grad_pointwise /= sw_sum
            hess_pointwise /= sw_sum

            # For non-canonical link functions and far away from the optimum, the
            # pointwise hessian can be negative. We take care that 75% of the hessian
            # entries are positive.
            hessian_warning = (
                np.average(hess_pointwise <= 0, weights=sample_weight) > 0.25
            )
            hess_pointwise = np.abs(hess_pointwise)

            if hessian_warning:
                # Exit early without computing the hessian.
                return grad, hess, hessian_warning

            hess[:n_features, :n_features] = sandwich_dot(X, hess_pointwise)

            if l2_reg_strength > 0:
                # The L2 penalty enters the Hessian on the diagonal only. To add those
                # terms, we use a flattened view of the array.
                order = "C" if hess.flags.c_contiguous else "F"
                hess.reshape(-1, order=order)[: (n_features * n_dof) : (n_dof + 1)] += (
                    l2_reg_strength
                )

                # With intercept included as added column to X, the hessian becomes
                # hess = (X, 1)' @ diag(h) @ (X, 1)
                #      = (X' @ diag(h) @ X, X' @ h)
                #        (           h @ X, sum(h))
                # The left upper part has already been filled, it remains to compute
                # the last row and the last column.
                Xh = X.T @ hess_pointwise
                hess[:-1, -1] = Xh
                hess[-1, :-1] = Xh
                hess[-1, -1] = hess_pointwise.sum()
        else:
            # Here we may safely assume HalfMultinomialLoss aka categorical
            # cross-entropy.
            # HalfMultinomialLoss computes only the diagonal part of the hessian, i.e.
            # diagonal in the classes. Here, we want the full hessian. Therefore, we
            # call gradient_proba.
            grad_pointwise, proba = self.base_loss.gradient_proba(
                y_true=y,
            )
            grad = grad.reshape((n_classes, n_dof), order="F")

            # The full hessian matrix, i.e. not only the diagonal part, dropping most
            # indices, is given by:
            #
            #   hess = X' @ h @ X
            #
            # Here, h is a priori a 4-dimensional matrix of shape
            # (n_samples, n_samples, n_classes, n_classes). It is diagonal its first
            # two dimensions (the ones with n_samples), i.e. it is
            # effectively a 3-dimensional matrix (n_samples, n_classes, n_classes).
            #
            #   h = diag(p) - p' p
            #
            # or with indices k and l for classes
            #
            #   h_kl = p_k * delta_kl - p_k * p_l
            #
            # with p_k the (predicted) probability for class k. Only the dimension in
            # n_samples multiplies with X.
            # For 3 classes and n_samples = 1, this looks like ("@" is a bit misused
            # here):
            #
            #   hess = X' @ (h00 h10 h20) @ X
            #               (h10 h11 h12)
            #               (h20 h12 h22)
            #        = (X' @ diag(h00) @ X, X' @ diag(h10), X' @ diag(h20))
            #          (X' @ diag(h10) @ X, X' @ diag(h11), X' @ diag(h12))
            #          (X' @ diag(h20) @ X, X' @ diag(h12), X' @ diag(h22))
            #
            # Now coef of shape (n_classes * n_dof) is contiguous in n_classes.
            # Therefore, we want the hessian to follow this convention, too, i.e.
            #     hess[:n_classes, :n_classes] = (x0' @ h00 @ x0, x0' @ h10 @ x0, ..)
            #                                    (x0' @ h10 @ x0, x0' @ h11 @ x0, ..)
            #                                    (x0' @ h20 @ x0, x0' @ h12 @ x0, ..)
            # is the first feature, x0, for all classes. In our implementation, we
            # still want to take advantage of BLAS "X.T @ X". Therefore, we have some
            # index/slicing battle to fight.
                sw = sample_weight / sw_sum
            else:
                sw = 1.0 / sw_sum

            for k in range(n_classes):
                # Diagonal terms (in classes) hess_kk.
                # Note that this also writes to some of the lower triangular part.
                h = proba[:, k] * (1 - proba[:, k]) * sw
                hess[
                    k : n_classes * n_features : n_classes,
                ] = sandwich_dot(X, h)
                if self.fit_intercept:
                    # See above in the non multiclass case.
                    Xh = X.T @ h
                    hess[
                        k : n_classes * n_features : n_classes,
                        n_classes * n_features + k,
                    ] = Xh
                    hess[
                    ] = Xh
                    hess[n_classes * n_features + k, n_classes * n_features + k] = (
                        h.sum()
                    )
                # Off diagonal terms (in classes) hess_kl.
                for l in range(k + 1, n_classes):
                    # Upper triangle (in classes).
                    h = -proba[:, k] * proba[:, l] * sw
                    hess[
                        l : n_classes * n_features : n_classes,
                    ] = sandwich_dot(X, h)
                    if self.fit_intercept:
                        Xh = X.T @ h
                        hess[
                            k : n_classes * n_features : n_classes,
                            n_classes * n_features + l,
                        ] = Xh
                        hess[
                            n_classes * n_features + k,
                            l : n_classes * n_features : n_classes,
                        ] = Xh
                        hess[n_classes * n_features + k, n_classes * n_features + l] = (
                            h.sum()
                        )
                    # Fill lower triangle (in classes).
                    hess[l::n_classes, k::n_classes] = hess[k::n_classes, l::n_classes]

                # See above in the non multiclass case.
                hess.reshape(-1, order=order)[
                    : (n_classes**2 * n_features * n_dof) : (n_classes * n_dof + 1)
                ] += l2_reg_strength

            # The pointwise hessian is always non-negative for the multinomial loss.
            hessian_warning = False

        return grad, hess, hessian_warning

    def gradient_hessian_product(
        self, coef, X, y, sample_weight=None, l2_reg_strength=0.0, n_threads=1
    ):
        """Computes gradient and hessp (hessian product function) w.r.t. coef.

        Returns
        -------

        hessp : callable
            Function that takes in a vector input of shape of gradient and
            and returns matrix-vector product with hessian.
        """
        weights, intercept, raw_prediction = self.weight_intercept_raw(coef, X)

                y_true=y,
            )

            # Precompute as much as possible: hX, hX_sum and hessian_sum
            hessian_sum = hess_pointwise.sum()
                hX = (
                    sparse.dia_matrix((hess_pointwise, 0), shape=(n_samples, n_samples))
                    @ X
                )
            else:
                hX = hess_pointwise[:, np.newaxis] * X

                # Calculate the double derivative with respect to intercept.
                # Note: In case hX is sparse, hX.sum is a matrix object.
                hX_sum = np.squeeze(np.asarray(hX.sum(axis=0)))
                # prevent squeezing to zero-dim array if n_features == 1
                hX_sum = np.atleast_1d(hX_sum)

            # With intercept included and l2_reg_strength = 0, hessp returns
            # res = (X, 1)' @ diag(h) @ (X, 1) @ s
            #     = (X, 1)' @ (hX @ s[:n_features], sum(h) * s[-1])
            # res[:n_features] = X' @ hX @ s[:n_features] + sum(h) * s[-1]
            # res[-1] = 1' @ hX @ s[:n_features] + sum(h) * s[-1]
            def hessp(s):
                ret = np.empty_like(s)
                if sparse.issparse(X):
                    ret[:n_features] = X.T @ (hX @ s[:n_features])
                else:
                    ret[:n_features] = np.linalg.multi_dot([X.T, hX, s[:n_features]])
                ret[:n_features] += l2_reg_strength * s[:n_features]

                    ret[:n_features] += s[-1] * hX_sum
                    ret[-1] = hX_sum @ s[:n_features] + hessian_sum * s[-1]
                return ret

        else:
            # diagonal in the classes. Here, we want the matrix-vector product of the
            # full hessian. Therefore, we call gradient_proba.
                y_true=y,
            )

            # Full hessian-vector product, i.e. not only the diagonal part of the
            # hessian. Derivation with some index battle for input vector s:
            #   - sample index i
            #   - feature indices j, m
            #   - class indices k, l
            #   - 1_{k=l} is one if k=l else 0
            #   - p_i_k is the (predicted) probability that sample i belongs to class k
            #     for all i: sum_k p_i_k = 1
            #   - s_l_m is input vector for class l and feature m
            #   - X' = X transposed
            #
            # Note: Hessian with dropping most indices is just:
            #       X' @ p_k (1(k=l) - p_l) @ X
            #
            # result_{k j} = sum_{i, l, m} Hessian_{i, k j, m l} * s_l_m
            #   = sum_{i, l, m} (X')_{ji} * p_i_k * (1_{k=l} - p_i_l)
            #                   * X_{im} s_l_m
            #   = sum_{i, m} (X')_{ji} * p_i_k
            #                * (X_{im} * s_k_m - sum_l p_i_l * X_{im} * s_l_m)
            #
            # See also https://github.com/scikit-learn/scikit-learn/pull/3646#discussion_r17461411
                s = s.reshape((n_classes, -1), order="F")  # shape = (n_classes, n_dof)
                    s_intercept = s[:, -1]
                    s = s[:, :-1]  # shape = (n_classes, n_features)
                else:
                    s_intercept = 0
                tmp = X @ s.T + s_intercept  # X_{im} * s_k_m
                tmp += (-proba * tmp).sum(axis=1)[:, np.newaxis]  # - sum_l ..
                tmp *= proba  # * p_i_k
                    tmp *= sample_weight[:, np.newaxis]
                # hess_prod = empty_like(grad), but we ravel grad below and this
                # function is run after that.
                hess_prod = np.empty((n_classes, n_dof), dtype=weights.dtype, order="F")
                hess_prod[:, :n_features] = (tmp.T @ X) / sw_sum + l2_reg_strength * s
                    hess_prod[:, -1] = tmp.sum(axis=0) / sw_sum
                if coef.ndim == 1:
                    return hess_prod.ravel(order="F")
                else:
                    return hess_prod

                return grad.ravel(order="F"), hessp

        return grad, hessp

            dtype as X.

        Returns
        -------
        """
        else:
        else:

        Returns
        -------
        """
            else:
        else:
            else:
            else:

        Returns
        -------
        """

        else:

    def loss(
        self,
        coef,
        X,
        y,
    ):

        Returns
        -------
        """
        else:

            y_true=y,
        )

        self,
        coef,
        X,
        y,
    ):

        Returns
        -------

        """

        else:

            y_true=y,
        )

        else:

        self,
        coef,
        X,
        y,
    ):

        Returns
        -------
        """

        else:

            y_true=y,
        )

        else:
            else:

        self,
        coef,
        X,
        y,
    ):

        Returns
        -------

        """
        else:

            )
        else:
            )
        ):
        else:

                y_true=y,
            )

            )

                )

        else:
                y_true=y,
            )

            #
            #
            #
            #
            #
            #
            # here):
            #
            #
            else:

                hess[
                    Xh = X.T @ h
                    hess[
                    ] = Xh
                    hess[
                    ] = Xh
                        h.sum()
                    )
                    hess[
                        Xh = X.T @ h
                        hess[
                        ] = Xh
                        hess[
                        ] = Xh
                            h.sum()
                        )

    ):

        Returns
        -------

        """

                y_true=y,
            )

                hX = (
                    @ X
                )
            else:

                else:

                return ret

        else:
                y_true=y,
            )

            #
            #
            #
                else:
                else:

                else:

                return ret

                else:
                else:

class QuantileRegressor(LinearModel, RegressorMixin, BaseEstimator):
    """Linear regression model that predicts conditional quantiles.

    The linear :class:`QuantileRegressor` optimizes the pinball loss for a
    desired `quantile` and is robust to outliers.

    This model uses an L1 regularization like
    :class:`~sklearn.linear_model.Lasso`.

    Read more in the :ref:`User Guide <quantile_regression>`.

    quantile : float, default=0.5
        The quantile that the model tries to predict. It must be strictly
        between 0 and 1. If 0.5 (default), the model predicts the 50%
        quantile, i.e. the median.

        Regularization constant that multiplies the L1 penalty term.

        Whether or not to fit the intercept.

    solver : {'highs-ds', 'highs-ipm', 'highs', 'interior-point', \
            'revised simplex'}, default='highs'
        Method used by :func:`scipy.optimize.linprog` to solve the linear
        programming formulation.

        It is recommended to use the highs methods because
        they are the fastest ones. Solvers "highs-ds", "highs-ipm" and "highs"
        support sparse input data and, in fact, always convert to sparse csc.

        From `scipy>=1.11.0`, "interior-point" is not available anymore.

           The default of `solver` changed to `"highs"` in version 1.4.

    solver_options : dict, default=None
        Additional parameters passed to :func:`scipy.optimize.linprog` as
        options. If `None` and if `solver='interior-point'`, then
        `{"lstsq": True}` is passed to :func:`scipy.optimize.linprog` for the
        sake of stability.

    coef_ : array of shape (n_features,)
        Estimated coefficients for the features.

        The intercept of the model, aka bias term.

        The actual number of iterations performed by the solver.

    See Also
    --------
    Lasso : The Lasso is a linear model that estimates sparse coefficients
        with l1 regularization.

    Examples
    --------
    >>> from sklearn.linear_model import QuantileRegressor
    >>> n_samples, n_features = 10, 2
    >>> # the two following lines are optional in practice
    >>> from sklearn.utils.fixes import sp_version, parse_version
    >>> reg = QuantileRegressor(quantile=0.8).fit(X, y)
    >>> np.mean(y <= reg.predict(X))
    np.float64(0.8)
    """

        "quantile": [Interval(Real, 0, 1, closed="neither")],
                {
                    "highs-ds",
                    "highs-ipm",
                    "highs",
                    "interior-point",
                    "revised simplex",
                }
            ),
        ],
        "solver_options": [dict, None],
    }

        self,
        *,
        quantile=0.5,
        solver="highs",
        solver_options=None,
    ):
        self.solver_options = solver_options

        Returns
        -------
        """
            self,
            X,
            y,
            accept_sparse=["csc", "csr", "coo"],
            multi_output=False,
        )

        n_params = n_features

            n_params += 1
            # Note that centering y and X with _preprocess_data does not work
            # for quantile regression.

        # The objective is defined as 1/n * sum(pinball loss) + alpha * L1.
        # So we rescale the penalty term, which is equivalent.
        alpha = np.sum(sample_weight) * self.alpha

        if self.solver == "interior-point" and sp_version >= parse_version("1.11.0"):
                f"Solver {self.solver} is not anymore available in SciPy >= 1.11.0."
            )

        if sparse.issparse(X) and self.solver not in ["highs", "highs-ds", "highs-ipm"]:
                f"Solver {self.solver} does not support sparse X. "
                "Use solver 'highs' for example."
            )
        # make default solver more stable
        if self.solver_options is None and self.solver == "interior-point":
            solver_options = {"lstsq": True}
        else:
            solver_options = self.solver_options

        # After rescaling alpha, the minimization problem is
        #     min sum(pinball loss) + alpha * L1
        # Use linear programming formulation of quantile regression
        #     min_x c x
        #           A_eq x = b_eq
        #                0 <= x
        # x = (s0, s, t0, t, u, v) = slack variables >= 0
        # intercept = s0 - t0
        # coef = s - t
        # c = (0, alpha * 1_p, 0, alpha * 1_p, quantile * 1_n, (1-quantile) * 1_n)
        # residual = y - X@coef - intercept = u - v
        # A_eq = (1_n, X, -1_n, -X, diag(1_n), -diag(1_n))
        # b_eq = y
        # p = n_features
        # n = n_samples
        # 1_n = vector of length n with entries equal one
        # see https://stats.stackexchange.com/questions/384909/
        #
        # Filtering out zero sample weights from the beginning makes life
        # easier for the linprog solver.
        indices = np.nonzero(sample_weight)[0]
        n_indices = len(indices)  # use n_mask instead of n_samples
        if n_indices < len(sample_weight):
            sample_weight = sample_weight[indices]
            X = _safe_indexing(X, indices)
            y = _safe_indexing(y, indices)
        c = np.concatenate(
            [
                np.full(2 * n_params, fill_value=alpha),
                sample_weight * self.quantile,
                sample_weight * (1 - self.quantile),
            ]
        )
            # do not penalize the intercept
            c[0] = 0
            c[n_params] = 0

        if self.solver in ["highs", "highs-ds", "highs-ipm"]:
            # Note that highs methods always use a sparse CSC memory layout internally,
            # even for optimization problems parametrized using dense numpy arrays.
            # Therefore, we work with CSC matrices as early as possible to limit
            # unnecessary repeated memory copies.
            eye = sparse.eye(n_indices, dtype=X.dtype, format="csc")
                ones = sparse.csc_matrix(np.ones(shape=(n_indices, 1), dtype=X.dtype))
                A_eq = sparse.hstack([ones, X, -ones, -X, eye, -eye], format="csc")
            else:
                A_eq = sparse.hstack([X, -X, eye, -eye], format="csc")
        else:
            eye = np.eye(n_indices)
                ones = np.ones((n_indices, 1))
                A_eq = np.concatenate([ones, X, -ones, -X, eye, -eye], axis=1)
            else:
                A_eq = np.concatenate([X, -X, eye, -eye], axis=1)

        b_eq = y

        result = linprog(
            c=c,
            A_eq=A_eq,
            b_eq=b_eq,
            method=self.solver,
            options=solver_options,
        )
        solution = result.x
        if not result.success:
            failure = {
                1: "Iteration limit reached.",
                2: "Problem appears to be infeasible.",
                3: "Problem appears to be unbounded.",
                4: "Numerical difficulties encountered.",
            }
                "Linear programming for QuantileRegressor did not succeed.\n"
                f"Status is {result.status}: "
                + failure.setdefault(result.status, "unknown reason")
                + "\n"
                + "Result message of linprog:\n"
                + result.message,
            )

        # positive slack - negative slack
        # solution is an array with (params_pos, params_neg, u, v)
        params = solution[:n_params] - solution[n_params : 2 * n_params]

        self.n_iter_ = result.nit

            self.coef_ = params[1:]
            self.intercept_ = params[0]
        else:
            self.coef_ = params

        self,
        *,
    ):

        Returns
        -------
        """
            self,
            X,
            y,
        )

            )

            )
        else:

        #     min_x c x
        #                0 <= x
        # coef = s - t
        # b_eq = y
        #
            [
            ]
        )
            c[0] = 0

            else:
        else:
            else:

        b_eq = y

            c=c,
        )
            failure = {
            }
                + "\n"
            )

        else:

def _check_solver(solver, penalty, dual):
    if solver not in ["liblinear", "saga"] and penalty not in ("l2", None):
            f"Solver {solver} supports only 'l2' or None penalties, got {penalty} "
            "penalty."
        )
    if solver != "liblinear" and dual:
        raise ValueError(f"Solver {solver} supports only dual=False, got dual={dual}")

    if penalty == "elasticnet" and solver != "saga":
            f"Only 'saga' solver supports elasticnet penalty, got solver={solver}."
        )

    if solver == "liblinear" and penalty is None:
        raise ValueError("penalty=None is not supported for the liblinear solver")

def _check_multi_class(multi_class, solver, n_classes):
    """Computes the multi class type, either "multinomial" or "ovr".

    For `n_classes` > 2 and a solver that supports it, returns "multinomial".
    For all other cases, in particular binary classification, return "ovr".
    """
    if multi_class == "auto":
        if solver in ("liblinear",):
            multi_class = "ovr"
        elif n_classes > 2:
            multi_class = "multinomial"
        else:
    if multi_class == "multinomial" and solver in ("liblinear",):
        raise ValueError("Solver %s does not support a multinomial backend." % solver)
    return multi_class

def _logistic_regression_path(
    X,
    y,
    pos_class=None,
    Cs=10,
    tol=1e-4,
    solver="lbfgs",
    coef=None,
    dual=False,
    penalty="l2",
    intercept_scaling=1.0,
    multi_class="auto",
    l1_ratio=None,
):
    """Compute a Logistic Regression model for a list of regularization

    This is an implementation that uses the result of the previous model
    to speed up computations along the set of solutions, making it faster
    than sequentially calling LogisticRegression for the different parameters.
    Note that there will be no speedup with liblinear solver, since it does
    not handle warm-starting.

    Read more in the :ref:`User Guide <logistic_regression>`.

        Input data, target values.

    pos_class : int, default=None
        The class with respect to which we perform a one-vs-all fit.
        If None, then it is assumed that the given problem is binary.

    Cs : int or array-like of shape (n_cs,), default=10
        List of values for the regularization parameter or integer specifying
        the number of regularization parameters that should be used. In this
        case, the parameters will be chosen in a logarithmic scale between
        1e-4 and 1e4.

        Whether to fit an intercept for the model. In this case the shape of
        the returned array is (n_cs, n_features + 1).

        Maximum number of iterations for the solver.

        Stopping criterion. For the newton-cg and lbfgs solvers, the iteration
        will stop when ``max{|g_i | i = 1, ..., n} <= tol``
        where ``g_i`` is the i-th component of the gradient.

        For the liblinear and lbfgs solvers set verbose to any positive
        number for verbosity.

    solver : {'lbfgs', 'liblinear', 'newton-cg', 'newton-cholesky', 'sag', 'saga'}, \
            default='lbfgs'
        Numerical solver to use.

    coef : array-like of shape (n_features,), default=None
        Initialization value for coefficients of logistic regression.
        Useless for liblinear solver.

    dual : bool, default=False
        Dual or primal formulation. Dual formulation is only implemented for
        l2 penalty with liblinear solver. Prefer dual=False when
        n_samples > n_features.

    penalty : {'l1', 'l2', 'elasticnet'}, default='l2'
        Used to specify the norm used in the penalization. The 'newton-cg',
        'sag' and 'lbfgs' solvers support only l2 penalties. 'elasticnet' is
        only supported by the 'saga' solver.

    intercept_scaling : float, default=1.
        Useful only when the solver 'liblinear' is used
        and self.fit_intercept is set to True. In this case, x becomes
        [x, self.intercept_scaling],
        i.e. a "synthetic" feature with constant value equal to
        The intercept becomes ``intercept_scaling * synthetic_feature_weight``.

        Note! the synthetic feature weight is subject to l1/l2 regularization
        as all other features.
        To lessen the effect of regularization on synthetic feature weight
        (and therefore on the intercept) intercept_scaling has to be increased.

    multi_class : {'ovr', 'multinomial', 'auto'}, default='auto'
        If the option chosen is 'ovr', then a binary problem is fit for each
        label. For 'multinomial' the loss minimised is the multinomial loss fit
        across the entire probability distribution, *even when the data is
        binary*. 'multinomial' is unavailable when solver='liblinear'.
        'auto' selects 'ovr' if the data is binary, or if solver='liblinear',
        and otherwise selects 'multinomial'.

           Stochastic Average Gradient descent solver for 'multinomial' case.
            Default changed from 'ovr' to 'auto' in 0.22.

        Used when ``solver`` == 'sag', 'saga' or 'liblinear' to shuffle the
        data. See :term:`Glossary <random_state>` for details.

        Maximum squared sum of X over samples. Used only in SAG solver.
        If None, it will be computed, going through all the samples.
        The value should be precomputed to speed up cross validation.

    sample_weight : array-like of shape(n_samples,), default=None
        Array of weights that are assigned to individual samples.
        If not provided, then each sample is given unit weight.

    l1_ratio : float, default=None
        The Elastic-Net mixing parameter, with ``0 <= l1_ratio <= 1``. Only
        used if ``penalty='elasticnet'``. Setting ``l1_ratio=0`` is equivalent
        to using ``penalty='l2'``, while setting ``l1_ratio=1`` is equivalent
        to using ``penalty='l1'``. For ``0 < l1_ratio <1``, the penalty is a

       Number of OpenMP threads to use.

    Returns
    -------
    coefs : ndarray of shape (n_cs, n_features) or (n_cs, n_features + 1)
        List of coefficients for the Logistic Regression model. If
        fit_intercept is set to True then the second dimension will be
        n_features + 1, where the last item represents the intercept. For
        ``multiclass='multinomial'``, the shape is (n_classes, n_cs,
        n_features) or (n_classes, n_cs, n_features + 1).

    Cs : ndarray
        Grid of Cs used for cross-validation.

    n_iter : array of shape (n_cs,)
        Actual number of iteration for each Cs.

    Notes
    -----
    You might get slightly different results with the solver liblinear than
    with the others since this uses LIBLINEAR which penalizes the intercept.

        The "copy" parameter was removed.
    """
    if isinstance(Cs, numbers.Integral):
        Cs = np.logspace(-4, 4, Cs)

    solver = _check_solver(solver, penalty, dual)

    # Preprocessing.
            X,
            accept_large_sparse=solver not in ["liblinear", "sag", "saga"],
        )
        y = check_array(y, ensure_2d=False, dtype=None)

    multi_class = _check_multi_class(multi_class, solver, len(classes))
    if pos_class is None and multi_class != "multinomial":
        if classes.size > 2:
            raise ValueError("To fit OvR, use the pos_class argument")
        # np.unique(y) gives labels in sorted order.
        pos_class = classes[1]

    if sample_weight is not None or class_weight is not None:
        sample_weight = _check_sample_weight(sample_weight, X, dtype=X.dtype, copy=True)

    # If class_weights is a dict (provided by the user), the weights
    # are assigned to the original labels. If it is "balanced", then
    # the class_weights are assigned after masking the labels with a OvR.
    if isinstance(class_weight, dict) or (
        multi_class == "multinomial" and class_weight is not None
    ):
            class_weight, classes=classes, y=y, sample_weight=sample_weight
        )
        sample_weight *= class_weight_[le.fit_transform(y)]

    # For doing a ovr, we need to mask the labels first. For the
    # multinomial case this is not necessary.
        w0 = np.zeros(n_features + int(fit_intercept), dtype=X.dtype)
        mask = y == pos_class
        y_bin = np.ones(y.shape, dtype=X.dtype)
        if solver == "liblinear":
            mask_classes = np.array([-1, 1])
            y_bin[~mask] = -1.0
        else:
            # HalfBinomialLoss, used for those solvers, represents y in [0, 1] instead
            # of in [-1, 1].
            mask_classes = np.array([0, 1])
            y_bin[~mask] = 0.0

        # for compute_class_weight
        if class_weight == "balanced":
            class_weight_ = compute_class_weight(
                class_weight,
                classes=mask_classes,
                y=y_bin,
            )
            sample_weight *= class_weight_[le.fit_transform(y_bin)]

    else:
        if solver in ["sag", "saga", "lbfgs", "newton-cg", "newton-cholesky"]:
            # SAG, lbfgs, newton-cg and newton-cg multinomial solvers need
            # LabelEncoder, not LabelBinarizer, i.e. y as a 1d-array of integers.
            # LabelEncoder also saves memory compared to LabelBinarizer, especially
            # when n_classes is large.
            le = LabelEncoder()
            Y_multi = le.fit_transform(y).astype(X.dtype, copy=False)
        else:
            # For liblinear solver, apply LabelBinarizer, i.e. y is one-hot encoded.
            lbin = LabelBinarizer()
            Y_multi = lbin.fit_transform(y)
            if Y_multi.shape[1] == 1:
                Y_multi = np.hstack([1 - Y_multi, Y_multi])

        w0 = np.zeros(
            (classes.size, n_features + int(fit_intercept)), order="F", dtype=X.dtype
        )

    # IMPORTANT NOTE:
    # All solvers relying on LinearModelLoss need to scale the penalty with n_samples
    # or the sum of sample weights because the implemented logistic regression
    # objective here is (unfortunately)
    #     C * sum(pointwise_loss) + penalty
    # instead of (as LinearModelLoss does)
    #     mean(pointwise_loss) + 1/C * penalty
    if solver in ["lbfgs", "newton-cg", "newton-cholesky"]:
        # This needs to be calculated after sample_weight is multiplied by
        # class_weight. It is even tested that passing class_weight is equivalent to
        # passing sample_weights according to class_weight.

    if coef is not None:
        # it must work both giving the bias term and not
        if multi_class == "ovr":
            if coef.size not in (n_features, w0.size):
                    "Initialization coef is of shape %d, expected shape %d or %d"
                    % (coef.size, n_features, w0.size)
                )
            w0[: coef.size] = coef
        else:
            # For binary problems coef.shape[0] should be 1, otherwise it
            # should be classes.size.
            n_classes = classes.size
                n_classes = 1

            if coef.shape[0] != n_classes or coef.shape[1] not in (
                n_features + 1,
            ):
                    "Initialization coef is of shape (%d, %d), expected "
                    "shape (%d, %d) or (%d, %d)"
                    % (
                        coef.shape[0],
                        coef.shape[1],
                        classes.size,
                        n_features,
                        n_features + 1,
                    )
                )

            if n_classes == 1:
                w0[0, : coef.shape[1]] = -coef
                w0[1, : coef.shape[1]] = coef
            else:
                w0[:, : coef.shape[1]] = coef

    if multi_class == "multinomial":
        if solver in ["lbfgs", "newton-cg", "newton-cholesky"]:
            # scipy.optimize.minimize and newton-cg accept only ravelled parameters,
            # i.e. 1d-arrays. LinearModelLoss expects classes to be contiguous and
            # reconstructs the 2d-array via w0.reshape((n_classes, -1), order="F").
            # As w0 is F-contiguous, ravel(order="F") also avoids a copy.
            w0 = w0.ravel(order="F")
        loss = LinearModelLoss(
            base_loss=HalfMultinomialLoss(n_classes=classes.size),
        )
        target = Y_multi
        if solver == "lbfgs":
            func = loss.loss_gradient
        elif solver == "newton-cg":
            func = loss.loss
            grad = loss.gradient
            hess = loss.gradient_hessian_product  # hess = [gradient, hessp]
        warm_start_sag = {"coef": w0.T}
    else:
        target = y_bin
            loss = LinearModelLoss(
                base_loss=HalfBinomialLoss(), fit_intercept=fit_intercept
            )
            )
        elif solver == "newton-cholesky":
            )
        warm_start_sag = {"coef": np.expand_dims(w0, axis=1)}

    coefs = list()
    n_iter = np.zeros(len(Cs), dtype=np.int32)
    for i, C in enumerate(Cs):
            l2_reg_strength = 1.0 / (C * sw_sum)
            iprint = [-1, 50, 1, 100, 101][
                np.searchsorted(np.array([0, 1, 2, 3]), verbose)
            ]
            opt_res = optimize.minimize(
                func,
                w0,
                jac=True,
                args=(X, target, sample_weight, l2_reg_strength, n_threads),
                options={
                    "maxiter": max_iter,
                    "maxls": 50,  # default is 20
                    "iprint": iprint,
                    "gtol": tol,
                    "ftol": 64 * np.finfo(float).eps,
                },
            )
            n_iter_i = _check_optimize_result(
                solver,
                opt_res,
                max_iter,
                extra_warning_msg=_LOGISTIC_SOLVER_CONVERGENCE_MSG,
            )
            w0, loss = opt_res.x, opt_res.fun
            args = (X, target, sample_weight, l2_reg_strength, n_threads)
            w0, n_iter_i = _newton_cg(
                grad_hess=hess,
                func=func,
                grad=grad,
                x0=w0,
                args=args,
                maxiter=max_iter,
                tol=tol,
            )
            sol = NewtonCholeskySolver(
                coef=w0,
                linear_loss=loss,
                l2_reg_strength=l2_reg_strength,
                tol=tol,
            )
            w0 = sol.solve(X=X, y=target, sample_weight=sample_weight)
            n_iter_i = sol.iteration
        elif solver == "liblinear":
            (
                coef_,
                intercept_,
                n_iter_i,
            ) = _fit_liblinear(
                X,
                target,
                C,
                fit_intercept,
                intercept_scaling,
                None,
                penalty,
                dual,
                verbose,
                max_iter,
                tol,
            )
            if fit_intercept:
                w0 = np.concatenate([coef_.ravel(), intercept_])
            else:
                w0 = coef_.ravel()
            # n_iter_i is an array for each class. However, `target` is always encoded
            # in {-1, 1}, so we only take the first element of n_iter_i.
            n_iter_i = n_iter_i.item()

        elif solver in ["sag", "saga"]:
            if multi_class == "multinomial":
                target = target.astype(X.dtype, copy=False)
                loss = "multinomial"
            else:
                loss = "log"
            # alpha is for L2-norm, beta is for L1-norm
            if penalty == "l1":
                alpha = 0.0
                beta = 1.0 / C
            elif penalty == "l2":
                alpha = 1.0 / C
                beta = 0.0
            else:  # Elastic-Net penalty
                alpha = (1.0 / C) * (1 - l1_ratio)
                beta = (1.0 / C) * l1_ratio

            w0, n_iter_i, warm_start_sag = sag_solver(
                X,
                target,
                loss,
                alpha,
                beta,
                max_iter,
                tol,
                verbose,
                False,
                warm_start_sag,
                is_saga=(solver == "saga"),
            )

        else:
                "solver must be one of {'liblinear', 'lbfgs', "
                "'newton-cg', 'sag'}, got '%s' instead" % solver
            )

        if multi_class == "multinomial":
            n_classes = max(2, classes.size)
            if solver in ["lbfgs", "newton-cg", "newton-cholesky"]:
                multi_w0 = np.reshape(w0, (n_classes, -1), order="F")
            else:
                multi_w0 = w0
                multi_w0 = multi_w0[1][np.newaxis, :]
            coefs.append(multi_w0.copy())
        else:
            coefs.append(w0.copy())

        n_iter[i] = n_iter_i

    return np.array(coefs), np.array(Cs), n_iter

def _log_reg_scoring_path(
    X,
    y,
    train,
    test,
    *,
    pos_class,
    Cs,
    scoring,
    max_iter,
    tol,
    verbose,
    solver,
    penalty,
    dual,
    multi_class,
    max_squared_sum,
    l1_ratio,
    score_params,
):
    """Computes scores across logistic_regression_path

        Target labels.

    pos_class : int

    Cs : int or list of floats
        Each of the values in Cs describes the inverse of
        regularization strength. If Cs is as an int, then a grid of Cs
        values are chosen in a logarithmic scale between 1e-4 and 1e4.

        If False, then the bias term is set to zero. Else the last
        term of each coef_ gives us the intercept.

    tol : float

    class_weight : dict or 'balanced'

    solver : {'lbfgs', 'liblinear', 'newton-cg', 'newton-cholesky', 'sag', 'saga'}
        Decides which solver to use.

    penalty : {'l1', 'l2', 'elasticnet'}

    dual : bool

        The intercept becomes intercept_scaling * synthetic feature weight

    multi_class : {'auto', 'ovr', 'multinomial'}

    random_state : int, RandomState instance

    sample_weight : array-like of shape(n_samples,)

    l1_ratio : float

    score_params : dict
        Parameters to pass to the `score` method of the underlying scorer.

    Returns
    -------
        n_features + 1, where the last item represents the intercept.

    scores : ndarray of shape (n_cs,)
        Scores obtained for each Cs.

    n_iter : ndarray of shape(n_cs,)
    """

    sw_train, sw_test = None, None

    coefs, Cs, n_iter = _logistic_regression_path(
        X_train,
        y_train,
        Cs=Cs,
        class_weight=class_weight,
        pos_class=pos_class,
        multi_class=multi_class,
        tol=tol,
        dual=dual,
        penalty=penalty,
        check_input=False,
        max_squared_sum=max_squared_sum,
    )

    log_reg = LogisticRegression(solver=solver, multi_class=multi_class)

    # The score method of Logistic Regression has a classes_ attribute.
        log_reg.classes_ = np.array([-1, 1])
    elif multi_class == "multinomial":
        log_reg.classes_ = np.unique(y_train)
    else:
            "multi_class should be either multinomial or ovr, got %d" % multi_class
        )

    if pos_class is not None:
        mask = y_test == pos_class
        y_test = np.ones(y_test.shape, dtype=np.float64)
        y_test[~mask] = -1.0

    scores = list()

    scoring = get_scorer(scoring)
    for w in coefs:
            w = w[np.newaxis, :]
            log_reg.coef_ = w[:, :-1]
            log_reg.intercept_ = w[:, -1]
        else:
            log_reg.coef_ = w
            log_reg.intercept_ = 0.0

        if scoring is None:
            scores.append(log_reg.score(X_test, y_test, sample_weight=sw_test))
        else:
            score_params = score_params or {}
            score_params = _check_method_params(X=X, params=score_params, indices=test)
            scores.append(scoring(log_reg, X_test, y_test, **score_params))
    return coefs, Cs, np.array(scores), n_iter

class LogisticRegression(LinearClassifierMixin, SparseCoefMixin, BaseEstimator):
    """
    Logistic Regression (aka logit, MaxEnt) classifier.

    This class implements regularized logistic regression using the
    'liblinear' library, 'newton-cg', 'sag', 'saga' and 'lbfgs' solvers. **Note
    that regularization is applied by default**. It can handle both dense
    and sparse input. Use C-ordered arrays or CSR matrices containing 64-bit
    floats for optimal performance; any other input format will be converted
    (and copied).

    The 'newton-cg', 'sag', and 'lbfgs' solvers support only L2 regularization
    with primal formulation, or no regularization. The 'liblinear' solver
    supports both L1 and L2 regularization, with a dual formulation only for
    the L2 penalty. The Elastic-Net regularization is only supported by the
    'saga' solver.

    For :term:`multiclass` problems, only 'newton-cg', 'sag', 'saga' and 'lbfgs'
    handle multinomial loss. 'liblinear' and 'newton-cholesky' only handle binary
    classification but can be extended to handle multiclass by using
    :class:`~sklearn.multiclass.OneVsRestClassifier`.

    penalty : {'l1', 'l2', 'elasticnet', None}, default='l2'
        Specify the norm of the penalty:

        - `None`: no penalty is added;
        - `'l2'`: add a L2 penalty term and it is the default choice;
        - `'l1'`: add a L1 penalty term;
        - `'elasticnet'`: both L1 and L2 penalty terms are added.

           Some penalties may not work with some solvers. See the parameter
           `solver` below, to know the compatibility between the penalty and
           solver.

           l1 penalty with SAGA solver (allowing 'multinomial' + L1)

        Dual (constrained) or primal (regularized, see also
        :ref:`this equation <regularized-logistic-loss>`) formulation. Dual formulation
        is only implemented for l2 penalty with liblinear solver. Prefer dual=False when

        Inverse of regularization strength; must be a positive float.
        Like in support vector machines, smaller values specify stronger
        regularization.

        Specifies if a constant (a.k.a. bias or intercept) should be

    intercept_scaling : float, default=1

           *class_weight='balanced'*

        Algorithm to use in the optimization problem. Default is 'lbfgs'.
        To choose a solver, you might want to consider the following aspects:

        - For small datasets, 'liblinear' is a good choice, whereas 'sag'
          and 'saga' are faster for large ones;
        - For :term:`multiclass` problems, all solvers except 'liblinear' minimize the
          full multinomial loss;
        - 'liblinear' can only handle binary classification by default. To apply a
          one-versus-rest scheme for the multiclass setting one can wrap it with the
          :class:`~sklearn.multiclass.OneVsRestClassifier`.
        - 'newton-cholesky' is a good choice for
          `n_samples` >> `n_features * n_classes`, especially with one-hot encoded
          categorical features with rare categories. Be aware that the memory usage
          of this solver has a quadratic dependency on `n_features * n_classes`
          because it explicitly computes the full Hessian matrix.

           The choice of the algorithm depends on the penalty chosen and on
           (multinomial) multiclass support:

           ================= ============================== ======================
           solver            penalty                        multinomial multiclass
           'lbfgs'           'l2', None                     yes
           'liblinear'       'l1', 'l2'                     no
           'newton-cg'       'l2', None                     yes
           'newton-cholesky' 'l2', None                     no
           'sag'             'l2', None                     yes
           'saga'            'elasticnet', 'l1', 'l2', None yes

        .. note::
           'sag' and 'saga' fast convergence is only guaranteed on features
           with approximately the same scale. You can preprocess the data with
           a scaler from :mod:`sklearn.preprocessing`.

        .. seealso::
           Refer to the :ref:`User Guide <Logistic_regression>` for more
           information regarding :class:`LogisticRegression` and more specifically the
           :ref:`Table <logistic_regression_solvers>`
           summarizing solver/penalty supports.

            The default solver changed from 'liblinear' to 'lbfgs' in 0.22.
           newton-cholesky solver.

        Maximum number of iterations taken for the solvers to converge.

    multi_class : {'auto', 'ovr', 'multinomial'}, default='auto'

        .. deprecated:: 1.5
           ``multi_class`` was deprecated in version 1.5 and will be removed in 1.7.
           From then on, the recommended 'multinomial' will always be used for
           `n_classes >= 3`.
           Solvers that do not support 'multinomial' will raise an error.
           Use `sklearn.multiclass.OneVsRestClassifier(LogisticRegression())` if you
           still want to use OvR.

        Useless for liblinear solver. See :term:`the Glossary <warm_start>`.

           *warm_start* to support *lbfgs*, *newton-cg*, *sag*, *saga* solvers.

        Number of CPU cores used when parallelizing over classes if
        multi_class='ovr'". This parameter is ignored when the ``solver`` is
        set to 'liblinear' regardless of whether 'multi_class' is specified or
        not. ``None`` means 1 unless in a :obj:`joblib.parallel_backend`
        context. ``-1`` means using all processors.

    classes_ : ndarray of shape (n_classes, )
        A list of class labels known to the classifier.

        `coef_` is of shape (1, n_features) when the given problem is binary.
        In particular, when `multi_class='multinomial'`, `coef_` corresponds
        to outcome 1 (True) and `-coef_` corresponds to outcome 0 (False).

    intercept_ : ndarray of shape (1,) or (n_classes,)
        Intercept (a.k.a. bias) added to the decision function.

        If `fit_intercept` is set to False, the intercept is set to zero.
        `intercept_` is of shape (1,) when the given problem is binary.
        In particular, when `multi_class='multinomial'`, `intercept_`
        corresponds to outcome 1 (True) and `-intercept_` corresponds to
        outcome 0 (False).

    n_iter_ : ndarray of shape (n_classes,) or (1, )
        Actual number of iterations for all classes. If binary or multinomial,
        it returns only 1 element. For liblinear solver, only the maximum
        number of iteration across all classes is given.

    See Also
    --------
    SGDClassifier : Incrementally trained logistic regression (when given
        the parameter ``loss="log_loss"``).
    LogisticRegressionCV : Logistic regression with built-in cross validation.

    Notes
    -----
    select features when fitting the model. It is thus not uncommon,
    that happens, try with a smaller tol parameter.

    L-BFGS-B -- Software for Large-scale Bound-constrained Optimization
        Ciyou Zhu, Richard Byrd, Jorge Nocedal and Jose Luis Morales.
        http://users.iems.northwestern.edu/~nocedal/lbfgsb.html

    LIBLINEAR -- A Library for Large Linear Classification
        https://www.csie.ntu.edu.tw/~cjlin/liblinear/

    SAG -- Mark Schmidt, Nicolas Le Roux, and Francis Bach
        Minimizing Finite Sums with the Stochastic Average Gradient
        https://hal.inria.fr/hal-00860051/document

    SAGA -- Defazio, A., Bach F. & Lacoste-Julien S. (2014).
            :arxiv:`"SAGA: A Fast Incremental Gradient Method With Support
            for Non-Strongly Convex Composite Objectives" <1407.0202>`

    Hsiang-Fu Yu, Fang-Lan Huang, Chih-Jen Lin (2011). Dual coordinate descent
        methods for logistic regression and maximum entropy models.
        Machine Learning 85(1-2):41-75.
        https://www.csie.ntu.edu.tw/~cjlin/papers/maxent_dual.pdf

    Examples
    --------
    >>> clf.predict(X[:2, :])
    array([0, 0])
    >>> clf.predict_proba(X[:2, :])
    array([[9.8...e-01, 1.8...e-02, 1.4...e-08],
           [9.7...e-01, 2.8...e-02, ...e-08]])
    0.97...

    For a comparison of the LogisticRegression with other classifiers see:
    """

        "penalty": [StrOptions({"l1", "l2", "elasticnet"}), None],
        "dual": ["boolean"],
                {"lbfgs", "liblinear", "newton-cg", "newton-cholesky", "sag", "saga"}
            )
        ],
        "multi_class": [
            StrOptions({"auto", "ovr", "multinomial"}),
        ],
    }

        self,
        *,
        tol=1e-4,
        C=1.0,
        multi_class="deprecated",
        l1_ratio=None,
    ):
        self.C = C

        """
        Fit the model according to the given training data.

            Array of weights that are assigned to individual samples.
            If not provided, then each sample is given unit weight.

               *sample_weight* support to LogisticRegression.

        Returns
        -------
        self

        Notes
        -----
        The SAGA solver supports both float64 and float32 bit arrays.
        """
        solver = _check_solver(self.solver, self.penalty, self.dual)

        if self.penalty != "elasticnet" and self.l1_ratio is not None:
                "l1_ratio parameter is only used when penalty is "
                "'elasticnet'. Got "
                "(penalty={})".format(self.penalty)
            )

            raise ValueError("l1_ratio must be specified when penalty is elasticnet.")

        if self.penalty is None:
            if self.C != 1.0:  # default values
                    "Setting penalty=None will ignore the C and l1_ratio parameters"
                )
                # Note that check for l1_ratio is done right above
            C_ = np.inf
            penalty = "l2"
        else:
            C_ = self.C
            penalty = self.penalty

            _dtype = np.float64
        else:
            _dtype = [np.float64, np.float32]

            self,
            X,
            y,
            dtype=_dtype,
        )

        # TODO(1.7) remove multi_class
        multi_class = self.multi_class
        if self.multi_class == "multinomial" and len(self.classes_) == 2:
                (
                    "'multi_class' was deprecated in version 1.5 and will be removed in"
                    " 1.7. From then on, binary problems will be fit as proper binary "
                    " logistic regression models (as if multi_class='ovr' were set)."
                    " Leave it to its default value to avoid this warning."
                ),
            )
        elif self.multi_class in ("multinomial", "auto"):
                (
                    " 1.7. From then on, it will always use 'multinomial'."
                ),
            )
        elif self.multi_class == "ovr":
                (
                    " 1.7. Use OneVsRestClassifier(LogisticRegression(..)) instead."
                ),
            )
        else:
            # Set to old default value.
            multi_class = "auto"
        multi_class = _check_multi_class(multi_class, solver, len(self.classes_))

            if effective_n_jobs(self.n_jobs) != 1:
                    "'n_jobs' > 1 does not have any effect when"
                    " 'solver' is set to 'liblinear'. Got 'n_jobs'"
                    " = {}.".format(effective_n_jobs(self.n_jobs))
                )
            self.coef_, self.intercept_, self.n_iter_ = _fit_liblinear(
                X,
                y,
                self.C,
                self.intercept_scaling,
                self.class_weight,
                self.penalty,
                self.dual,
                self.tol,
                self.random_state,
            )

        if solver in ["sag", "saga"]:
            max_squared_sum = row_norms(X, squared=True).max()
        else:
            max_squared_sum = None

            )

            n_classes = 1
            classes_ = classes_[1:]

        if self.warm_start:
            warm_start_coef = getattr(self, "coef_", None)
        else:
            warm_start_coef = None
        if warm_start_coef is not None and self.fit_intercept:
            warm_start_coef = np.append(
                warm_start_coef, self.intercept_[:, np.newaxis], axis=1
            )

        # Hack so that we iterate only once for the multinomial case.
            classes_ = [None]
            warm_start_coef = [warm_start_coef]
        if warm_start_coef is None:
            warm_start_coef = [None] * n_classes

        path_func = delayed(_logistic_regression_path)

        # The SAG solver releases the GIL so it's more efficient to use
        # threads for this solver.
            prefer = "threads"
        else:
            prefer = "processes"

        # TODO: Refactor this to avoid joblib parallelism entirely when doing binary
        # and multinomial multiclass classification and use joblib only for the
        # one-vs-rest multiclass case.
        if (
            solver in ["lbfgs", "newton-cg", "newton-cholesky"]
            and len(classes_) == 1
            and effective_n_jobs(self.n_jobs) == 1
        ):
            # In the future, we would like n_threads = _openmp_effective_n_threads()
            # For the time being, we just do
            n_threads = 1
        else:

        fold_coefs_ = Parallel(n_jobs=self.n_jobs, verbose=self.verbose, prefer=prefer)(
            path_func(
                X,
                y,
                pos_class=class_,
                Cs=[C_],
                multi_class=multi_class,
                class_weight=self.class_weight,
                coef=warm_start_coef_,
                penalty=penalty,
                max_squared_sum=max_squared_sum,
            )
            for class_, warm_start_coef_ in zip(classes_, warm_start_coef)
        )

        fold_coefs_, _, n_iter_ = zip(*fold_coefs_)
        self.n_iter_ = np.asarray(n_iter_, dtype=np.int32)[:, 0]

            self.coef_ = fold_coefs_[0][0]
        else:
            self.coef_ = np.asarray(fold_coefs_)
            self.coef_ = self.coef_.reshape(
                n_classes, n_features + int(self.fit_intercept)
            )

            self.intercept_ = self.coef_[:, -1]
            self.coef_ = self.coef_[:, :-1]
        else:
            self.intercept_ = np.zeros(n_classes)

        """
        Probability estimates.

        The returned estimates for all classes are ordered by the
        label of classes.

        For a multi_class problem, if multi_class is set to be "multinomial"
        the softmax function is used to find the predicted probability of
        Else use a one-vs-rest approach, i.e. calculate the probability
        of each class assuming it to be positive using the logistic function
        and normalize these values across all the classes.

        Returns
        -------
            where classes are ordered as they are in ``self.classes_``.
        """

        ovr = self.multi_class in ["ovr", "warn"] or (
            self.multi_class in ["auto", "deprecated"]
            and (self.classes_.size <= 2 or self.solver == "liblinear")
        )
        if ovr:
            return super()._predict_proba_lr(X)
        else:
            decision = self.decision_function(X)
            if decision.ndim == 1:
                # Workaround for multi_class="multinomial" and binary outcomes
                # which requires softmax prediction with only a 1D decision.
                decision_2d = np.c_[-decision, decision]
            else:
                decision_2d = decision
            return softmax(decision_2d, copy=False)

        """
        Predict logarithm of probability estimates.

        Returns
        -------
            model, where classes are ordered as they are in ``self.classes_``.
        """

class LogisticRegressionCV(LogisticRegression, LinearClassifierMixin, BaseEstimator):
    """Logistic Regression CV (aka logit, MaxEnt) classifier.

    This class implements logistic regression using liblinear, newton-cg, sag
    or lbfgs optimizer. The newton-cg, sag and lbfgs solvers support only L2
    regularization with primal formulation. The liblinear solver supports both
    L1 and L2 regularization, with a dual formulation only for the L2 penalty.
    Elastic-Net penalty is only supported by the saga solver.

    For the grid of `Cs` values and `l1_ratios` values, the best hyperparameter
    is selected by the cross-validator
    :class:`~sklearn.model_selection.StratifiedKFold`, but it can be changed
    using the :term:`cv` parameter. The 'newton-cg', 'sag', 'saga' and 'lbfgs'
    solvers can warm-start the coefficients (see :term:`Glossary<warm_start>`).

    Cs : int or list of floats, default=10
        Each of the values in Cs describes the inverse of regularization
        strength. If Cs is as an int, then a grid of Cs values are chosen
        in a logarithmic scale between 1e-4 and 1e4.

    cv : int or cross-validation generator, default=None
        The default cross-validation generator used is Stratified K-Folds.
        If an integer is provided, then it is the number of folds used.
        See the module :mod:`sklearn.model_selection` module for the
        list of possible cross-validation objects.

        - `'l2'`: add a L2 penalty term (used by default);

           solver.

    scoring : str or callable, default=None

        - For multiclass problems, all solvers except 'liblinear' minimize the full
          multinomial loss;
        - 'liblinear' might be slower in :class:`LogisticRegressionCV`
          because it does not handle warm-starting.

           'lbfgs'           'l2'                           yes
           'newton-cg'       'l2'                           yes
           'newton-cholesky' 'l2',                          no
           'sag'             'l2',                          yes
           'saga'            'elasticnet', 'l1', 'l2'       yes

        .. note::

        Maximum number of iterations of the optimization algorithm.

           class_weight == 'balanced'

        Number of CPU cores used during the cross-validation loop.

        For the 'liblinear', 'sag' and 'lbfgs' solvers set verbose to any
        positive number for verbosity.

    refit : bool, default=True
        If set to True, the scores are averaged across all folds, and the
        coefs and the C that corresponds to the best score is taken, and a
        final refit is done using these parameters.
        Otherwise the coefs, intercepts and C that correspond to the
        best scores across folds are averaged.

    multi_class : {'auto, 'ovr', 'multinomial'}, default='auto'

           Use `sklearn.multiclass.OneVsRestClassifier(LogisticRegressionCV())` if you

        Used when `solver='sag'`, 'saga' or 'liblinear' to shuffle the data.
        Note that this only applies to the solver and not the cross-validation
        generator. See :term:`Glossary <random_state>` for details.

    l1_ratios : list of float, default=None
        The list of Elastic-Net mixing parameter, with ``0 <= l1_ratio <= 1``.
        Only used if ``penalty='elasticnet'``. A value of 0 is equivalent to
        using ``penalty='l2'``, while 1 is equivalent to using
        ``penalty='l1'``. For ``0 < l1_ratio <1``, the penalty is a combination
        of L1 and L2.

        `coef_` is of shape (1, n_features) when the given problem
        is binary.

        `intercept_` is of shape(1,) when the problem is binary.

    Cs_ : ndarray of shape (n_cs)
        Array of C i.e. inverse of regularization parameter values used
        for cross-validation.

    l1_ratios_ : ndarray of shape (n_l1_ratios)
        Array of l1_ratios used for cross-validation. If no l1_ratio is used
        (i.e. penalty is not 'elasticnet'), this is set to ``[None]``

    coefs_paths_ : ndarray of shape (n_folds, n_cs, n_features) or \
                   (n_folds, n_cs, n_features + 1)
        dict with classes as the keys, and the path of coefficients obtained
        during cross-validating across each fold and then across each Cs
        after doing an OvR for the corresponding class as values.
        If the 'multi_class' option is set to 'multinomial', then
        the coefs_paths are the coefficients corresponding to each class.
        Each dict value has shape ``(n_folds, n_cs, n_features)`` or
        ``(n_folds, n_cs, n_features + 1)`` depending on whether the
        intercept is fit or not. If ``penalty='elasticnet'``, the shape is
        ``(n_folds, n_cs, n_l1_ratios_, n_features)`` or
        ``(n_folds, n_cs, n_l1_ratios_, n_features + 1)``.

    scores_ : dict
        dict with classes as the keys, and the values as the
        grid of scores obtained during cross-validating each fold, after doing
        an OvR for the corresponding class. If the 'multi_class' option
        given is 'multinomial' then the same scores are repeated across
        all classes, since this is the multinomial class. Each dict value
        has shape ``(n_folds, n_cs)`` or ``(n_folds, n_cs, n_l1_ratios)`` if
        ``penalty='elasticnet'``.

    C_ : ndarray of shape (n_classes,) or (n_classes - 1,)
        Array of C that maps to the best scores across every class. If refit is
        set to False, then for each class, the best C is the average of the
        C's that correspond to the best scores for each fold.
        `C_` is of shape(n_classes,) when the problem is binary.

    l1_ratio_ : ndarray of shape (n_classes,) or (n_classes - 1,)
        Array of l1_ratio that maps to the best scores across every class. If
        refit is set to False, then for each class, the best l1_ratio is the
        average of the l1_ratio's that correspond to the best scores for each
        fold.  `l1_ratio_` is of shape(n_classes,) when the problem is binary.

    n_iter_ : ndarray of shape (n_classes, n_folds, n_cs) or (1, n_folds, n_cs)
        Actual number of iterations for all classes, folds and Cs.
        In the binary or multinomial cases, the first dimension is equal to 1.
        If ``penalty='elasticnet'``, the shape is ``(n_classes, n_folds,
        n_cs, n_l1_ratios)`` or ``(1, n_folds, n_cs, n_l1_ratios)``.

    See Also
    --------
    LogisticRegression : Logistic regression without tuning the
        hyperparameter `C`.

    Examples
    --------
    >>> from sklearn.linear_model import LogisticRegressionCV
    >>> clf = LogisticRegressionCV(cv=5, random_state=0).fit(X, y)
    >>> clf.predict_proba(X[:2, :]).shape
    (2, 3)
    0.98...
    """

    _parameter_constraints: dict = {**LogisticRegression._parameter_constraints}

    for param in ["C", "warm_start", "l1_ratio"]:

        {
            "Cs": [Interval(Integral, 1, None, closed="left"), "array-like"],
            "cv": ["cv_object"],
            "scoring": [StrOptions(set(get_scorer_names())), callable, None],
            "l1_ratios": ["array-like", None],
            "refit": ["boolean"],
            "penalty": [StrOptions({"l1", "l2", "elasticnet"})],
        }
    )

        self,
        *,
        Cs=10,
        cv=None,
        tol=1e-4,
        refit=True,
        l1_ratios=None,
    ):
        self.Cs = Cs
        self.refit = refit
        self.l1_ratios = l1_ratios

            Parameters to pass to the underlying splitter and scorer.

        Returns
        -------
            Fitted LogisticRegressionCV estimator.
        """

        if self.penalty == "elasticnet":
            if (
                self.l1_ratios is None
                or len(self.l1_ratios) == 0
                or any(
                    (
                        not isinstance(l1_ratio, numbers.Number)
                        or l1_ratio < 0
                        or l1_ratio > 1
                    )
                    for l1_ratio in self.l1_ratios
                )
            ):
                    "l1_ratios must be a list of numbers between "
                    "0 and 1; got (l1_ratios=%r)" % self.l1_ratios
                )
            l1_ratios_ = self.l1_ratios
        else:
            if self.l1_ratios is not None:
                    "l1_ratios parameter is only used when penalty "
                    "is 'elasticnet'. Got (penalty={})".format(self.penalty)
                )

            l1_ratios_ = [None]

            self,
            X,
            y,
        )

        class_weight = self.class_weight

        # Encode for string labels
        label_encoder = LabelEncoder().fit(y)
        y = label_encoder.transform(y)
        if isinstance(class_weight, dict):
            class_weight = {
                label_encoder.transform([cls])[0]: v for cls, v in class_weight.items()
            }

        # The original class labels
        classes = self.classes_ = label_encoder.classes_
        encoded_labels = label_encoder.transform(label_encoder.classes_)

                (
                ),
            )
                (
                ),
            )
                (
                    " 1.7. Use OneVsRestClassifier(LogisticRegressionCV(..)) instead."
                ),
            )
        else:
        multi_class = _check_multi_class(multi_class, solver, len(classes))

        else:

                self,
                "fit",
                **params,
            )
        else:
            routed_params.scorer = Bunch(score=params)
                routed_params.scorer.score["sample_weight"] = sample_weight

        cv = check_cv(self.cv, y, classifier=True)

        # Use the label encoded classes
        n_classes = len(encoded_labels)

                " class: %r" % classes[0]
            )

            # OvR in case of binary problems is as good as fitting
            # the higher label
            encoded_labels = encoded_labels[1:]
            classes = classes[1:]

        # We need this hack to iterate only once over labels, in the case of
        # multi_class = multinomial, without changing the value of the labels.
            iter_encoded_labels = iter_classes = [None]
        else:
            iter_encoded_labels = encoded_labels
            iter_classes = classes

        # compute the class weights for the entire dataset y
            class_weight = compute_class_weight(
                classes=np.arange(len(self.classes_)),
                y=y,
            )
            class_weight = dict(enumerate(class_weight))

        path_func = delayed(_log_reg_scoring_path)

        if self.solver in ["sag", "saga"]:
        else:

                X,
                y,
                train,
                test,
                pos_class=label,
                Cs=self.Cs,
                penalty=self.penalty,
                dual=self.dual,
                class_weight=class_weight,
                scoring=self.scoring,
                intercept_scaling=self.intercept_scaling,
            )
            for label in iter_encoded_labels
            for l1_ratio in l1_ratios_
        )

        # _log_reg_scoring_path will output different shapes depending on the
        # multi_class param, so we need to reshape the outputs accordingly.
        # Cs is of shape (n_classes . n_folds . n_l1_ratios, n_Cs) and all the
        # rows are equal, so we just take the first one.
        # After reshaping,
        # - scores is of shape (n_classes, n_folds, n_Cs . n_l1_ratios)
        # - coefs_paths is of shape
        #  (n_classes, n_folds, n_Cs . n_l1_ratios, n_features)
        # - n_iter is of shape
        #  (n_classes, n_folds, n_Cs . n_l1_ratios) or
        #  (1, n_folds, n_Cs . n_l1_ratios)
        coefs_paths, Cs, scores, n_iter_ = zip(*fold_coefs_)
        self.Cs_ = Cs[0]
            coefs_paths = np.reshape(
                coefs_paths,
                (len(folds), len(l1_ratios_) * len(self.Cs_), n_classes, -1),
            )
            # equiv to coefs_paths = np.moveaxis(coefs_paths, (0, 1, 2, 3),
            #                                                 (1, 2, 0, 3))
            coefs_paths = np.swapaxes(coefs_paths, 0, 1)
            coefs_paths = np.swapaxes(coefs_paths, 0, 2)
            self.n_iter_ = np.reshape(
                n_iter_, (1, len(folds), len(self.Cs_) * len(l1_ratios_))
            )
            # repeat same scores across all classes
            scores = np.tile(scores, (n_classes, 1, 1))
        else:
                (n_classes, len(folds), len(self.Cs_) * len(l1_ratios_), -1),
            )
                n_iter_, (n_classes, len(folds), len(self.Cs_) * len(l1_ratios_))
            )
        scores = np.reshape(scores, (n_classes, len(folds), -1))
        self.scores_ = dict(zip(classes, scores))
        self.coefs_paths_ = dict(zip(classes, coefs_paths))

        self.C_ = list()
        self.l1_ratio_ = list()
        self.coef_ = np.empty((n_classes, X.shape[1]))
        self.intercept_ = np.zeros(n_classes)
        for index, (cls, encoded_label) in enumerate(
            zip(iter_classes, iter_encoded_labels)
        ):
            if multi_class == "ovr":
                scores = self.scores_[cls]
                coefs_paths = self.coefs_paths_[cls]
            else:
                # For multinomial, all scores are the same across classes
                scores = scores[0]
                # coefs_paths will keep its original shape because
                # logistic_regression_path expects it this way

            if self.refit:
                # best_index is between 0 and (n_Cs . n_l1_ratios - 1)
                # for example, with n_cs=2 and n_l1_ratios=3
                # the layout of scores is
                # [c1, c2, c1, c2, c1, c2]
                #   l1_1 ,  l1_2 ,  l1_3
                best_index = scores.sum(axis=0).argmax()

                best_index_C = best_index % len(self.Cs_)
                C_ = self.Cs_[best_index_C]
                self.C_.append(C_)

                best_index_l1 = best_index // len(self.Cs_)
                l1_ratio_ = l1_ratios_[best_index_l1]
                self.l1_ratio_.append(l1_ratio_)

                if multi_class == "multinomial":
                    coef_init = np.mean(coefs_paths[:, :, best_index, :], axis=1)
                else:
                    coef_init = np.mean(coefs_paths[:, best_index, :], axis=0)

                # Note that y is label encoded and hence pos_class must be
                # the encoded label / None (for 'multinomial')
                w, _, _ = _logistic_regression_path(
                    X,
                    y,
                    pos_class=encoded_label,
                    Cs=[C_],
                    solver=solver,
                    coef=coef_init,
                    max_iter=self.max_iter,
                    tol=self.tol,
                    penalty=self.penalty,
                    class_weight=class_weight,
                    multi_class=multi_class,
                    verbose=max(0, self.verbose - 1),
                    random_state=self.random_state,
                    check_input=False,
                    max_squared_sum=max_squared_sum,
                    l1_ratio=l1_ratio_,
                )
                w = w[0]

            else:
                # Take the best scores across every fold and the average of
                # all coefficients corresponding to the best scores.
                best_indices = np.argmax(scores, axis=1)
                if multi_class == "ovr":
                    w = np.mean(
                        [coefs_paths[i, best_indices[i], :] for i in range(len(folds))],
                        axis=0,
                    )
                else:
                        [
                            coefs_paths[:, i, best_indices[i], :]
                            for i in range(len(folds))
                        ],
                        axis=0,
                    )

                best_indices_C = best_indices % len(self.Cs_)
                self.C_.append(np.mean(self.Cs_[best_indices_C]))

                if self.penalty == "elasticnet":
                    best_indices_l1 = best_indices // len(self.Cs_)
                    self.l1_ratio_.append(np.mean(l1_ratios_[best_indices_l1]))
                else:
                    self.l1_ratio_.append(None)

                self.C_ = np.tile(self.C_, n_classes)
                self.l1_ratio_ = np.tile(self.l1_ratio_, n_classes)
                self.coef_ = w[:, : X.shape[1]]
                    self.intercept_ = w[:, -1]
            else:
                self.coef_[index] = w[: X.shape[1]]
                    self.intercept_[index] = w[-1]

        self.C_ = np.asarray(self.C_)
        self.l1_ratio_ = np.asarray(self.l1_ratio_)
        self.l1_ratios_ = np.asarray(l1_ratios_)
        # if elasticnet was used, add the l1_ratios dimension to some
        # attributes
        if self.l1_ratios is not None:
            # with n_cs=2 and n_l1_ratios=3
            # the layout of scores is
            # [c1, c2, c1, c2, c1, c2]
            #   l1_1 ,  l1_2 ,  l1_3
            # To get a 2d array with the following layout
            #      l1_1, l1_2, l1_3
            # c1 [[ .  ,  .  ,  .  ],
            # c2  [ .  ,  .  ,  .  ]]
            # We need to first reshape and then transpose.
            # The same goes for the other arrays
            for cls, coefs_path in self.coefs_paths_.items():
                self.coefs_paths_[cls] = coefs_path.reshape(
                    (len(folds), self.l1_ratios_.size, self.Cs_.size, -1)
                )
                self.coefs_paths_[cls] = np.transpose(
                    self.coefs_paths_[cls], (0, 2, 1, 3)
                )
            for cls, score in self.scores_.items():
                self.scores_[cls] = score.reshape(
                    (len(folds), self.l1_ratios_.size, self.Cs_.size)
                )
                self.scores_[cls] = np.transpose(self.scores_[cls], (0, 2, 1))

            self.n_iter_ = self.n_iter_.reshape(
                (-1, len(folds), self.l1_ratios_.size, self.Cs_.size)
            )
            self.n_iter_ = np.transpose(self.n_iter_, (0, 1, 3, 2))

    def score(self, X, y, sample_weight=None, **score_params):
        """Score using the `scoring` option on the given test data and labels.

        **score_params : dict
            Parameters to pass to the `score` method of the underlying scorer.

        Returns
        -------
            Score of self.predict(X) w.r.t. y.
        """
        _raise_for_params(score_params, self, "score")

        scoring = self._get_scorer()
                self,
                "score",
            )
        else:
            routed_params.scorer = Bunch(score={})

        return scoring(
            self,
            X,
            y,
            **routed_params.scorer.score,
        )

        Returns
        -------
        """

        router = (
            .add(
            )
            .add(
                scorer=self._get_scorer(),
                .add(caller="score", callee="score")
                .add(caller="fit", callee="score"),
            )
        )

        """Get the scorer based on the scoring method specified.
        The default scoring method is `accuracy`.
        """
        scoring = self.scoring or "accuracy"

        self,
        *,
        tol=1e-4,
        C=1.0,
    ):
        self.C = C

        """

        Returns
        -------
        self

        Notes
        -----
        """

            )

                )
            C_ = np.inf
        else:
            C_ = self.C

        else:

            self,
            X,
            y,
        )

                (
                ),
            )
                (
                ),
            )
                (
                ),
            )
        else:

                )
                X,
                y,
                self.C,
                self.tol,
            )

        else:

            )

        else:
            )

        else:

        if (
        ):
        else:

                X,
                y,
                Cs=[C_],
            )
        )

        else:
            )

        else:

        """

        Returns
        -------
        """

        )
        if ovr:
        else:
            else:

        """

        Returns
        -------
        """

        self,
        *,
        Cs=10,
        cv=None,
        tol=1e-4,
    ):

        Returns
        -------
        """

            if (
                or any(
                    (
                    )
                )
            ):
                )
        else:
                )

            self,
            X,
            y,
        )

            }

                (
                ),
            )
                (
                ),
            )
                (
                ),
            )
        else:

        else:

                self,
                "fit",
                **params,
            )
        else:

            )

        else:

                y=y,
            )

        else:

                X,
                y,
                train,
                test,
            )
        )

            )
            )
        else:
            )
            )

        ):
            else:

                else:

                    X,
                    y,
                    Cs=[C_],
                )
                w = w[0]

            else:
                        axis=0,
                    )
                else:
                        [
                        ],
                        axis=0,
                    )

                else:

            else:

                )
                )
                )

            )

        Returns
        -------
        """

                self,
                "score",
            )
        else:

            self,
            X,
            y,
        )

        Returns
        -------
        """

        router = (
            .add(
            )
            .add(
            )
        )

        """

def lars_path(
    X,
    y,
    Xy=None,
    *,
    Gram=None,
    max_iter=500,
    alpha_min=0,
    method="lar",
    eps=np.finfo(float).eps,
    return_path=True,
):
    """Compute Least Angle Regression or Lasso path using the LARS algorithm.

    The optimization objective for the case method='lasso' is::

    (1 / (2 * n_samples)) * ||y - Xw||^2_2 + alpha * ||w||_1

    in the case of method='lar', the objective function is only known in
    the form of an implicit equation (see discussion in [1]_).

    Read more in the :ref:`User Guide <least_angle_regression>`.

    X : None or ndarray of shape (n_samples, n_features)
        Input data. If X is `None`, Gram must also be `None`.
        If only the Gram matrix is available, use `lars_path_gram` instead.

    y : None or ndarray of shape (n_samples,)

    Xy : array-like of shape (n_features,), default=None
        `Xy = X.T @ y` that can be precomputed. It is useful

    Gram : None, 'auto', bool, ndarray of shape (n_features, n_features), \
        Precomputed Gram matrix `X.T @ X`, if `'auto'`, the Gram
        matrix is precomputed from the given X, if there are more samples
        than features.

        Maximum number of iterations to perform, set to infinity for no limit.

    alpha_min : float, default=0
        Minimum correlation along the path. It corresponds to the
        regularization parameter `alpha` in the Lasso.

    method : {'lar', 'lasso'}, default='lar'
        Specifies the returned model. Select `'lar'` for Least Angle
        Regression, `'lasso'` for the Lasso.

        If `False`, `X` is overwritten.

    eps : float, default=np.finfo(float).eps
        systems. Unlike the `tol` parameter in some iterative
        optimization-based algorithms, this parameter does not control
        the tolerance of the optimization.

        If `False`, `Gram` is overwritten.

        Controls output verbosity.

    return_path : bool, default=True
        If `True`, returns the entire path, else returns only the
        last point of the path.

        Whether to return the number of iterations.

        Restrict coefficients to be >= 0.
        This option is only allowed with method 'lasso'. Note that the model
        coefficients will not converge to the ordinary-least-squares solution
        for small values of alpha. Only coefficients up to the smallest alpha
        value (`alphas_[alphas_ > 0.].min()` when fit_path=True) reached by
        the stepwise Lars-Lasso algorithm are typically in congruence with the
        solution of the coordinate descent `lasso_path` function.

    Returns
    -------
    alphas : ndarray of shape (n_alphas + 1,)
        Maximum of covariances (in absolute value) at each iteration.
        `n_alphas` is either `max_iter`, `n_features`, or the
        number of nodes in the path with `alpha >= alpha_min`, whichever
        is smaller.

    active : ndarray of shape (n_alphas,)
        Indices of active variables at the end of the path.

    coefs : ndarray of shape (n_features, n_alphas + 1)

        Number of iterations run. Returned only if `return_n_iter` is set
        to True.

    See Also
    --------
    lars_path_gram : Compute LARS path in the sufficient stats mode.
    LassoLarsCV : Cross-validated Lasso, using the LARS algorithm.

    .. [1] "Least Angle Regression", Efron et al.
           http://statweb.stanford.edu/~tibs/ftp/lars.pdf

    .. [2] `Wikipedia entry on the Least-angle regression
           <https://en.wikipedia.org/wiki/Least-angle_regression>`_

    .. [3] `Wikipedia entry on the Lasso
           <https://en.wikipedia.org/wiki/Lasso_(statistics)>`_

    Examples
    --------
    ... )
    >>> alphas, _, estimated_coef = lars_path(X, y)
    (3,)
    array([[ 0.     ,  0.     ,  0.     ],
           [ 0.     ,  0.     ,  0.     ],
           [ 0.     , 46.96..., 97.99...],
           [ 0.     ,  0.     , 45.70...]])
    """
    if X is None and Gram is not None:
            "X cannot be None if Gram is not None"
            "Use lars_path_gram to avoid passing X and y."
        )
    return _lars_path_solver(
        X=X,
        y=y,
        Xy=Xy,
        Gram=Gram,
        n_samples=None,
        alpha_min=alpha_min,
        eps=eps,
        copy_Gram=copy_Gram,
        return_path=return_path,
    )

def lars_path_gram(
    Xy,
    Gram,
    *,
):
    """The lars_path in the sufficient stats mode.

        `Xy = X.T @ y`.

        `Gram = X.T @ X`.

        Equivalent size of sample.

        regularization parameter alpha parameter in the Lasso.

        Regression, ``'lasso'`` for the Lasso.

        If `return_path==True` returns the entire path, else returns only the

        value (`alphas_[alphas_ > 0.].min()` when `fit_path=True`) reached by
        solution of the coordinate descent lasso_path function.

    Returns
    -------
        `n_alphas` is either `max_iter`, `n_features` or the

        to True.

    See Also
    --------
    lars_path_gram : Compute LARS path.

    Examples
    --------
    >>> from sklearn.linear_model import lars_path_gram
    ... )
    >>> alphas, _, estimated_coef = lars_path_gram(X.T @ y, X.T @ X, n_samples=100)
    (3,)
    """
        X=None,
        y=None,
        Xy=Xy,
        eps=eps,
    )

def _lars_path_solver(
    X,
    y,
    Xy=None,
    n_samples=None,
):
    """Compute Least Angle Regression or Lasso path using LARS algorithm [1]

    the form of an implicit equation (see discussion in [1])

        Input data. Note that if X is None then Gram must be specified,
        i.e., cannot be None or False.

        `Xy = np.dot(X.T, y)` that can be precomputed. It is useful

    Gram : None, 'auto' or array-like of shape (n_features, n_features), \
        Precomputed Gram matrix `(X' * X)`, if ``'auto'``, the Gram

    n_samples : int or float, default=None
        Equivalent size of sample. If `None`, it will be `n_samples`.

        Specifies the returned model. Select ``'lar'`` for Least Angle

        If ``False``, ``X`` is overwritten.

        systems. Unlike the ``tol`` parameter in some iterative

        If ``False``, ``Gram`` is overwritten.

        If ``return_path==True`` returns the entire path, else returns only the

        value (``alphas_[alphas_ > 0.].min()`` when fit_path=True) reached by

    Returns
    -------
    alphas : array-like of shape (n_alphas + 1,)
        ``n_alphas`` is either ``max_iter``, ``n_features`` or the
        number of nodes in the path with ``alpha >= alpha_min``, whichever

    active : array-like of shape (n_alphas,)

    coefs : array-like of shape (n_features, n_alphas + 1)
        Coefficients along the path

        Number of iterations run. Returned only if return_n_iter is set
        to True.

    See Also
    --------
    lasso_path
    LassoLars
    Lars
    LassoLarsCV
    LarsCV
    sklearn.decomposition.sparse_encode

    """
    if method == "lar" and positive:
        raise ValueError("Positive constraint not supported for 'lar' coding method.")

    n_samples = n_samples if n_samples is not None else y.size

    if Xy is None:
        Cov = np.dot(X.T, y)
    else:
        Cov = Xy.copy()

    if Gram is None or Gram is False:
        Gram = None
            raise ValueError("X and Gram cannot both be unspecified.")
    elif (isinstance(Gram, str) and Gram == "auto") or Gram is True:
        if Gram is True or X.shape[0] > X.shape[1]:
            Gram = np.dot(X.T, X)
        else:
            Gram = None
    elif copy_Gram:
        Gram = Gram.copy()

    if Gram is None:
    else:
        n_features = Cov.shape[0]
        if Gram.shape != (n_features, n_features):
            raise ValueError("The shapes of the inputs Gram and Xy do not match.")

    if copy_X and X is not None and Gram is None:
        # force copy. setting the array to be fortran-ordered
        # speeds up the calculation of the (partial) Gram matrix
        # and allows to easily swap columns

    max_features = min(max_iter, n_features)

    dtypes = set(a.dtype for a in (X, y, Xy, Gram) if a is not None)
    if len(dtypes) == 1:
        # use the precision level of input data if it is consistent
        return_dtype = next(iter(dtypes))
    else:
        # fallback to double precision otherwise
        return_dtype = np.float64

        coefs = np.zeros((max_features + 1, n_features), dtype=return_dtype)
        alphas = np.zeros(max_features + 1, dtype=return_dtype)
    else:
        coef, prev_coef = (
            np.zeros(n_features, dtype=return_dtype),
        )
        alpha, prev_alpha = (
            np.array([0.0], dtype=return_dtype),
        )
        # above better ideas?

    n_iter, n_active = 0, 0
    active, indices = list(), np.arange(n_features)
    # holds the sign of covariance
    sign_active = np.empty(max_features, dtype=np.int8)
    drop = False

    # will hold the cholesky factorization. Only lower part is
    # referenced.
        L = np.empty((max_features, max_features), dtype=X.dtype)
        swap, nrm2 = linalg.get_blas_funcs(("swap", "nrm2"), (X,))
    else:
        L = np.empty((max_features, max_features), dtype=Gram.dtype)
        swap, nrm2 = linalg.get_blas_funcs(("swap", "nrm2"), (Cov,))
    (solve_cholesky,) = get_lapack_funcs(("potrs",), (L,))

            print("Step\t\tAdded\t\tDropped\t\tActive set size\t\tC")
        else:

    tiny32 = np.finfo(np.float32).tiny  # to avoid division by 0 warning
    cov_precision = np.finfo(Cov.dtype).precision
    equality_tolerance = np.finfo(np.float32).eps

    if Gram is not None:
        Gram_copy = Gram.copy()
        Cov_copy = Cov.copy()

        if Cov.size:
            if positive:
                C_idx = np.argmax(Cov)
            else:
                C_idx = np.argmax(np.abs(Cov))

            C_ = Cov[C_idx]

                C = C_
            else:
                C = np.fabs(C_)
        else:
            C = 0.0

            alpha = alphas[n_iter, np.newaxis]
            coef = coefs[n_iter]
            prev_alpha = alphas[n_iter - 1, np.newaxis]
            prev_coef = coefs[n_iter - 1]

        alpha[0] = C / n_samples
        if alpha[0] <= alpha_min + equality_tolerance:  # early stopping
            if abs(alpha[0] - alpha_min) > equality_tolerance:
                # interpolation factor 0 <= ss < 1
                if n_iter > 0:
                    # In the first iteration, all alphas are zero, the formula
                    # below would make ss a NaN
                    ss = (prev_alpha[0] - alpha_min) / (prev_alpha[0] - alpha[0])
                    coef[:] = prev_coef + ss * (coef - prev_coef)
                alpha[0] = alpha_min
            if return_path:
                coefs[n_iter] = coef
            break

        if n_iter >= max_iter or n_active >= n_features:
            break
        if not drop:
            ##########################################################
            # Append x_j to the Cholesky factorization of (Xa * Xa') #
            #                                                        #
            #            ( L   0 )                                   #
            #     L  ->  (       )  , where L * w = Xa' x_j          #
            #            ( w   z )    and z = ||x_j||                #
            #                                                        #

                sign_active[n_active] = np.ones_like(C_)
            else:
                sign_active[n_active] = np.sign(C_)
            m, n = n_active, C_idx + n_active

            Cov[C_idx], Cov[0] = swap(Cov[C_idx], Cov[0])
            indices[n], indices[m] = indices[m], indices[n]
            Cov_not_shortened = Cov
            Cov = Cov[1:]  # remove Cov[0]

            if Gram is None:
                X.T[n], X.T[m] = swap(X.T[n], X.T[m])
                c = nrm2(X.T[n_active]) ** 2
                L[n_active, :n_active] = np.dot(X.T[n_active], X.T[:n_active].T)
            else:
                # swap does only work inplace if matrix is fortran
                # contiguous ...
                Gram[m], Gram[n] = swap(Gram[m], Gram[n])
                Gram[:, m], Gram[:, n] = swap(Gram[:, m], Gram[:, n])
                c = Gram[n_active, n_active]
                L[n_active, :n_active] = Gram[n_active, :n_active]

            # Update the cholesky decomposition for the Gram matrix
            if n_active:
                linalg.solve_triangular(
                    L[:n_active, :n_active],
                    L[n_active, :n_active],
                    trans=0,
                    lower=1,
                    overwrite_b=True,
                    **SOLVE_TRIANGULAR_ARGS,
                )

            v = np.dot(L[n_active, :n_active], L[n_active, :n_active])
            diag = max(np.sqrt(np.abs(c - v)), eps)
            L[n_active, n_active] = diag

            if diag < 1e-7:
                # The system is becoming too ill-conditioned.
                # We have degenerate vectors in our active set.
                # We'll 'drop for good' the last regressor added.
                    "Regressors in active set degenerate. "
                    "Dropping a regressor, after %i iterations, "
                    "i.e. alpha=%.3e, "
                    "with an active set of %i regressors, and "
                    "the smallest cholesky pivot element being %.3e."
                    " Reduce max_iter or increase eps parameters."
                    % (n_iter, alpha.item(), n_active, diag),
                )

                # XXX: need to figure a 'drop for good' way
                Cov = Cov_not_shortened
                Cov[0] = 0
                Cov[C_idx], Cov[0] = swap(Cov[C_idx], Cov[0])
                continue

            active.append(indices[n_active])
            n_active += 1

            if verbose > 1:
                print(
                    "%s\t\t%s\t\t%s\t\t%s\t\t%s" % (n_iter, active[-1], "", n_active, C)
                )

        if method == "lasso" and n_iter > 0 and prev_alpha[0] < alpha[0]:
            # alpha is increasing. This is because the updates of Cov are
            # bringing in too much numerical error that is greater than
            # than the remaining correlation with the
            # regressors. Time to bail out
                "Early stopping the lars path, as the residues "
                "are small and the current value of alpha is no "
                "longer well controlled. %i iterations, alpha=%.3e, "
                "previous alpha=%.3e, with an active set of %i "
                "regressors." % (n_iter, alpha.item(), prev_alpha.item(), n_active),
            )
            break

        # least squares solution
        least_squares, _ = solve_cholesky(
            L[:n_active, :n_active], sign_active[:n_active], lower=True
        )

        if least_squares.size == 1 and least_squares == 0:
            # This happens because sign_active[:n_active] = 0
            least_squares[...] = 1
            AA = 1.0
        else:
            # is this really needed ?
            AA = 1.0 / np.sqrt(np.sum(least_squares * sign_active[:n_active]))

            if not np.isfinite(AA):
                # L is too ill-conditioned
                i = 0
                L_ = L[:n_active, :n_active].copy()
                while not np.isfinite(AA):
                    L_.flat[:: n_active + 1] += (2**i) * eps
                    least_squares, _ = solve_cholesky(
                        L_, sign_active[:n_active], lower=True
                    )
                    tmp = max(np.sum(least_squares * sign_active[:n_active]), eps)
                    AA = 1.0 / np.sqrt(tmp)
                    i += 1
            least_squares *= AA

        if Gram is None:
            # equiangular direction of variables in the active set
            eq_dir = np.dot(X.T[:n_active].T, least_squares)
            # correlation between each unactive variables and
            # eqiangular vector
            corr_eq_dir = np.dot(X.T[n_active:], eq_dir)
        else:
            # if huge number of features, this takes 50% of time, I
            # think could be avoided if we just update it using an
            # orthogonal (QR) decomposition of X
            corr_eq_dir = np.dot(Gram[:n_active, n_active:].T, least_squares)

        # Explicit rounding can be necessary to avoid `np.argmax(Cov)` yielding
        # unstable results because of rounding errors.
        np.around(corr_eq_dir, decimals=cov_precision, out=corr_eq_dir)

        g1 = arrayfuncs.min_pos((C - Cov) / (AA - corr_eq_dir + tiny32))
            gamma_ = min(g1, C / AA)
        else:
            g2 = arrayfuncs.min_pos((C + Cov) / (AA + corr_eq_dir + tiny32))
            gamma_ = min(g1, g2, C / AA)

        # TODO: better names for these variables: z
        drop = False
        z = -coef[active] / (least_squares + tiny32)
        z_pos = arrayfuncs.min_pos(z)
        if z_pos < gamma_:
            # some coefficients have changed sign
            idx = np.where(z == z_pos)[0][::-1]

            # update the sign, important for LAR
            sign_active[idx] = -sign_active[idx]

            if method == "lasso":
                gamma_ = z_pos
            drop = True

        n_iter += 1

            if n_iter >= coefs.shape[0]:
                del coef, alpha, prev_alpha, prev_coef
                # resize the coefs and alphas array
                add_features = 2 * max(1, (max_features - n_active))
                coefs = np.resize(coefs, (n_iter + add_features, n_features))
                coefs[-add_features:] = 0
                alphas = np.resize(alphas, n_iter + add_features)
                alphas[-add_features:] = 0
        else:
            # mimic the effect of incrementing n_iter on the array references
            prev_coef = coef
            prev_alpha[0] = alpha[0]
            coef = np.zeros_like(coef)

        coef[active] = prev_coef[active] + gamma_ * least_squares

        # update correlations
        Cov -= gamma_ * corr_eq_dir

        # See if any coefficient has changed sign
        if drop and method == "lasso":
            # handle the case when idx is not length of 1
            for ii in idx:
                arrayfuncs.cholesky_delete(L[:n_active, :n_active], ii)

            n_active -= 1
            drop_idx = [active.pop(ii) for ii in idx]

                # propagate dropped variable
                for ii in idx:
                    for i in range(ii, n_active):
                        X.T[i], X.T[i + 1] = swap(X.T[i], X.T[i + 1])
                        # yeah this is stupid
                        indices[i], indices[i + 1] = indices[i + 1], indices[i]

                # TODO: this could be updated
                residual = y - np.dot(X[:, :n_active], coef[active])
                temp = np.dot(X.T[n_active], residual)

                Cov = np.r_[temp, Cov]
            else:
                        Gram[i], Gram[i + 1] = swap(Gram[i], Gram[i + 1])
                        Gram[:, i], Gram[:, i + 1] = swap(Gram[:, i], Gram[:, i + 1])

                # Cov_n = Cov_j + x_j * X + increment(betas) TODO:
                # will this still work with multiple drops ?

                # recompute covariance. Probably could be done better
                # wrong as Xy is not swapped with the rest of variables

                temp = Cov_copy[drop_idx] - np.dot(Gram_copy[drop_idx], coef)

            sign_active = np.delete(sign_active, idx)
            sign_active = np.append(sign_active, 0.0)  # just to maintain size
                print(
                    "%s\t\t%s\t\t%s\t\t%s\t\t%s"
                    % (n_iter, "", drop_idx, n_active, abs(temp))
                )

        # resize coefs in case of early stop
        alphas = alphas[: n_iter + 1]
        coefs = coefs[: n_iter + 1]

        if return_n_iter:
            return alphas, active, coefs.T, n_iter
        else:
            return alphas, active, coefs.T
    else:
            return alpha, active, coef, n_iter
        else:
            return alpha, active, coef

class Lars(MultiOutputMixin, RegressorMixin, LinearModel):
    """Least Angle Regression model a.k.a. LAR.

    precompute : bool, 'auto' or array-like , default='auto'

    n_nonzero_coefs : int, default=500
        Target number of non-zero coefficients. Use ``np.inf`` for no limit.

    fit_path : bool, default=True
        If True the full path is stored in the ``coef_path_`` attribute.
        If you compute the solution for a large problem or many targets,
        setting ``fit_path`` to ``False`` will lead to a speedup, especially
        with a small alpha.

    jitter : float, default=None
        Upper bound on a uniform noise parameter to be added to the
        `y` values, to satisfy the model's assumption of
        one-at-a-time computations. Might help with stability.

        Determines random number generation for jittering. Pass an int
        See :term:`Glossary <random_state>`. Ignored if `jitter` is None.

    alphas_ : array-like of shape (n_alphas + 1,) or list of such arrays
        is smaller. If this is a list of array-like, the length of the outer
        list is `n_targets`.

    active_ : list of shape (n_alphas,) or list of such lists
        If this is a list of list, the length of the outer list is `n_targets`.

    coef_path_ : array-like of shape (n_features, n_alphas + 1) or list \
            of such arrays
        The varying values of the coefficients along the path. It is not
        present if the ``fit_path`` parameter is ``False``. If this is a list
        of array-like, the length of the outer list is `n_targets`.

    coef_ : array-like of shape (n_features,) or (n_targets, n_features)
        Parameter vector (w in the formulation formula).

    intercept_ : float or array-like of shape (n_targets,)

    n_iter_ : array-like or int
        The number of iterations taken by lars_path to find the
        grid of alphas for each target.

    See Also
    --------
    lars_path: Compute Least Angle Regression or Lasso

    Examples
    --------
    >>> reg = linear_model.Lars(n_nonzero_coefs=1)
    >>> reg.fit([[-1, 1], [0, 0], [1, 1]], [-1.1111, 0, -1.1111])
    Lars(n_nonzero_coefs=1)
    >>> print(reg.coef_)
    [ 0. -1.11...]
    """

        "precompute": ["boolean", StrOptions({"auto"}), np.ndarray, Hidden(None)],
        "n_nonzero_coefs": [Interval(Integral, 1, None, closed="left")],
        "eps": [Interval(Real, 0, None, closed="left")],
        "fit_path": ["boolean"],
        "jitter": [Interval(Real, 0, None, closed="left"), None],
    }

    method = "lar"
    positive = False

        self,
        *,
        n_nonzero_coefs=500,
        eps=np.finfo(float).eps,
        fit_path=True,
        jitter=None,
    ):
        self.fit_path = fit_path
        self.jitter = jitter

    def _get_gram(precompute, X, y):
        if (not hasattr(precompute, "__array__")) and (
            (precompute is True)
            or (precompute == "auto" and X.shape[0] > X.shape[1])
            or (precompute == "auto" and y.shape[1] > 1)
        ):
            precompute = np.dot(X.T, X)

        return precompute

    def _fit(self, X, y, max_iter, alpha, fit_path, Xy=None):
        """Auxiliary method to fit the model using X, y as training data"""

        )

        Gram = self._get_gram(self.precompute, X, y)

        self.alphas_ = []
        self.coef_ = np.empty((n_targets, n_features), dtype=X.dtype)

        if fit_path:
            self.active_ = []
            self.coef_path_ = []
            for k in range(n_targets):
                this_Xy = None if Xy is None else Xy[:, k]
                alphas, active, coef_path, n_iter_ = lars_path(
                    X,
                    y[:, k],
                    Gram=Gram,
                    Xy=this_Xy,
                    copy_Gram=True,
                    alpha_min=alpha,
                    method=self.method,
                    max_iter=max_iter,
                    return_path=True,
                    return_n_iter=True,
                    positive=self.positive,
                )
                self.alphas_.append(alphas)
                self.active_.append(active)
                self.coef_path_.append(coef_path)
                self.coef_[k] = coef_path[:, -1]

            if n_targets == 1:
                self.alphas_, self.active_, self.coef_path_, self.coef_ = [
                    a[0]
                    for a in (self.alphas_, self.active_, self.coef_path_, self.coef_)
                ]
                self.n_iter_ = self.n_iter_[0]
        else:
                alphas, _, self.coef_[k], n_iter_ = lars_path(
                    X,
                    y[:, k],
                    return_path=False,
                )

    def fit(self, X, y, Xy=None):

        Xy : array-like of shape (n_features,) or (n_features, n_targets), \
            Xy = np.dot(X.T, y) that can be precomputed. It is useful
            only when the Gram matrix is precomputed.

        Returns
        -------
        """
            self, X, y, force_writeable=True, y_numeric=True, multi_output=True
        )

        alpha = getattr(self, "alpha", 0.0)
        if hasattr(self, "n_nonzero_coefs"):
            alpha = 0.0  # n_nonzero_coefs parametrization takes priority
            max_iter = self.n_nonzero_coefs
        else:
            max_iter = self.max_iter

        if self.jitter is not None:
            rng = check_random_state(self.random_state)

            noise = rng.uniform(high=self.jitter, size=len(y))
            y = y + noise

            X,
            y,
            fit_path=self.fit_path,
            Xy=Xy,
        )

class LassoLars(Lars):
    """Lasso model fit with Least Angle Regression a.k.a. Lars.

    It is a Linear Model trained with an L1 prior as regularizer.

        Constant that multiplies the penalty term. Defaults to 1.0.
        ``alpha = 0`` is equivalent to an ordinary least square, solved
        by :class:`LinearRegression`. For numerical reasons, using
        ``alpha = 0`` with the LassoLars object is not advised and you
        should prefer the LinearRegression object.

    precompute : bool, 'auto' or array-like, default='auto'

        If ``True`` the full path is stored in the ``coef_path_`` attribute.

        Restrict coefficients to be >= 0. Be aware that you might want to
        remove fit_intercept which is set True by default.
        Under the positive restriction the model coefficients will not converge
        to the ordinary-least-squares solution for small values of alpha.
        Only coefficients up to the smallest alpha value (``alphas_[alphas_ >
        0.].min()`` when fit_path=True) reached by the stepwise Lars-Lasso
        algorithm are typically in congruence with the solution of the
        coordinate descent Lasso estimator.

    active_ : list of length n_alphas or list of such lists

        If a list is passed it's expected to be one of n_targets such arrays.

    See Also
    --------
    lars_path : Compute Least Angle Regression or Lasso
    Lasso : Linear Model trained with L1 prior as
        regularizer (aka the Lasso).
    LassoCV : Lasso linear model with iterative fitting
        along a regularization path.
    LassoLarsCV: Cross-validated Lasso, using the LARS algorithm.
    LassoLarsIC : Lasso model fit with Lars using BIC
        or AIC for model selection.

    Examples
    --------
    >>> reg = linear_model.LassoLars(alpha=0.01)
    >>> reg.fit([[-1, 1], [0, 0], [1, 1]], [-1, 0, -1])
    LassoLars(alpha=0.01)
    [ 0.         -0.955...]
    """

        **Lars._parameter_constraints,
    }
    _parameter_constraints.pop("n_nonzero_coefs")

    method = "lasso"

        self,
        *,
    ):

def _check_copy_and_writeable(array, copy=False):
    if copy or not array.flags.writeable:
        return array.copy()
    return array

def _lars_path_residues(
    X_train,
    y_train,
    X_test,
    y_test,
):
    """Compute the residues on left-out data for a full LARS path

    X_train : array-like of shape (n_samples, n_features)
        The data to fit the LARS on

    y_train : array-like of shape (n_samples,)
        The target variable to fit LARS on

    X_test : array-like of shape (n_samples, n_features)
        The data to compute the residues on

    y_test : array-like of shape (n_samples,)
        The target variable to compute the residues on

        Precomputed Gram matrix (X' * X), if ``'auto'``, the Gram
        than features

        Whether X_train, X_test, y_train and y_test should be copied;
        if False, they may be overwritten.

    method : {'lar' , 'lasso'}, default='lar'

        Sets the amount of verbosity

        whether to calculate the intercept for this model. If set

        See reservations for using this option in combination with method
        'lasso' for expected small values of alpha in the doc of LassoLarsCV
        and LassoLarsIC.

    Returns
    --------
        ``n_alphas`` is either ``max_iter`` or ``n_features``, whichever

    active : list

    coefs : array-like of shape (n_features, n_alphas)

    residues : array-like of shape (n_alphas, n_samples)
        Residues of the prediction on the test data
    """
    X_train = _check_copy_and_writeable(X_train, copy)
    y_train = _check_copy_and_writeable(y_train, copy)
    X_test = _check_copy_and_writeable(X_test, copy)
    y_test = _check_copy_and_writeable(y_test, copy)

    alphas, active, coefs = lars_path(
        X_train,
        y_train,
        copy_Gram=False,
        verbose=max(0, verbose - 1),
        eps=eps,
    )
    residues = np.dot(X_test, coefs) - y_test[:, np.newaxis]
    return alphas, active, coefs, residues.T

class LarsCV(Lars):
    """Cross-validated Least Angle Regression model.

        calculations. If set to ``'auto'`` let us decide. The Gram matrix
        cannot be passed as argument since we will use only subsets of X.

    max_n_alphas : int, default=1000
        The maximum number of points on the path used to compute the
        residuals in the cross-validation.

        If this is a list of lists, the outer list length is `n_targets`.

        parameter vector (w in the formulation formula)

        independent term in decision function

    coef_path_ : array-like of shape (n_features, n_alphas)
        the varying values of the coefficients along the path

        the estimated regularization parameter alpha

    alphas_ : array-like of shape (n_alphas,)
        the different values of alpha along the path

    cv_alphas_ : array-like of shape (n_cv_alphas,)
        all the values of alpha along the path for the different folds

    mse_path_ : array-like of shape (n_folds, n_cv_alphas)
        the mean square error on left-out for each fold along the path
        (alpha values given by ``cv_alphas``)

        the number of iterations run by Lars with the optimal alpha.

    See Also
    --------

    Notes
    -----

    Examples
    --------
    >>> from sklearn.linear_model import LarsCV
    >>> X, y = make_regression(n_samples=200, noise=4.0, random_state=0)
    >>> reg = LarsCV(cv=5).fit(X, y)
    0.9996...
    np.float64(0.2961...)
    array([154.3996...])
    """

        "max_n_alphas": [Interval(Integral, 1, None, closed="left")],
    }

    for parameter in ["n_nonzero_coefs", "jitter", "fit_path", "random_state"]:
        _parameter_constraints.pop(parameter)

        self,
        *,
        cv=None,
        max_n_alphas=1000,
    ):
        self.max_n_alphas = max_n_alphas
            n_nonzero_coefs=500,
            eps=eps,
            fit_path=True,
        )

        tags.target_tags.multi_output = False

        Returns
        -------
        """

        X, y = validate_data(self, X, y, force_writeable=True, y_numeric=True)
        X = as_float_array(X, copy=self.copy_X)
        y = as_float_array(y, copy=self.copy_X)

        else:

        # As we use cross-validation, the Gram matrix is not precomputed here
        Gram = self.precompute
        if hasattr(Gram, "__array__"):
                'Parameter "precompute" cannot be an array in '
                '%s. Automatically switch to "auto" instead.' % self.__class__.__name__
            )
            Gram = "auto"

            delayed(_lars_path_residues)(
                X[train],
                y[train],
                X[test],
                y[test],
                Gram=Gram,
                method=self.method,
                verbose=max(0, self.verbose - 1),
                eps=self.eps,
            )
            for train, test in cv.split(X, y, **routed_params.splitter.split)
        )
        all_alphas = np.concatenate(next(zip(*cv_paths)))
        # Unique also sorts
        all_alphas = np.unique(all_alphas)
        # Take at most max_n_alphas values
        stride = int(max(1, int(len(all_alphas) / float(self.max_n_alphas))))
        all_alphas = all_alphas[::stride]

        mse_path = np.empty((len(all_alphas), len(cv_paths)))
        for index, (alphas, _, _, residues) in enumerate(cv_paths):
            alphas = alphas[::-1]
            residues = residues[::-1]
            if alphas[0] != 0:
                alphas = np.r_[0, alphas]
                residues = np.r_[residues[0, np.newaxis], residues]
            if alphas[-1] != all_alphas[-1]:
                alphas = np.r_[alphas, all_alphas[-1]]
                residues = np.r_[residues, residues[-1, np.newaxis]]
            this_residues = interpolate.interp1d(alphas, residues, axis=0)(all_alphas)
            this_residues **= 2
            mse_path[:, index] = np.mean(this_residues, axis=-1)

        mask = np.all(np.isfinite(mse_path), axis=-1)
        all_alphas = all_alphas[mask]
        mse_path = mse_path[mask]
        # Select the alpha that minimizes left-out error
        i_best_alpha = np.argmin(mse_path.mean(axis=-1))
        best_alpha = all_alphas[i_best_alpha]

        # Store our parameters
        self.cv_alphas_ = all_alphas
        self.mse_path_ = mse_path

        # Now compute the full model using best_alpha
        # it will call a lasso internally when self if LassoLarsCV
        # as self.method == 'lasso'
            X,
            y,
            Xy=None,
        )

        Returns
        -------
        """
        )

class LassoLarsCV(LarsCV):
    """Cross-validated Lasso, using the LARS algorithm.

    precompute : bool or 'auto' , default='auto'

        Under the positive restriction the model coefficients do not converge
        As a consequence using LassoLarsCV only makes sense for problems where
        a sparse solution is expected and/or reached.

        independent term in decision function.

    active_ : list of int

    See Also
    --------

    Notes
    -----
    The object solves the same problem as the
    :class:`~sklearn.linear_model.LassoCV` object. However, unlike the
    :class:`~sklearn.linear_model.LassoCV`, it find the relevant alphas values
    by itself. In general, because of this property, it will be more stable.
    However, it is more fragile to heavily multicollinear datasets.

    It is more efficient than the :class:`~sklearn.linear_model.LassoCV` if
    only a small number of features are selected compared to the total number,
    for instance if there are very few samples compared to the number of
    features.

    Examples
    --------
    >>> from sklearn.linear_model import LassoLarsCV
    >>> X, y = make_regression(noise=4.0, random_state=0)
    >>> reg = LassoLarsCV(cv=5).fit(X, y)
    0.9993...
    np.float64(0.3972...)
    array([-78.4831...])
    """

        **LarsCV._parameter_constraints,
    }

        self,
        *,
        cv=None,
    ):

class LassoLarsIC(LassoLars):
    """Lasso model fit with Lars using BIC or AIC for model selection.

    AIC is the Akaike information criterion [2]_ and BIC is the Bayes
    Information criterion [3]_. Such criteria are useful to select the value
    of the regularization parameter by making a trade-off between the
    goodness of fit and the complexity of the model. A good model should
    explain well the data while being simple.

    Read more in the :ref:`User Guide <lasso_lars_ic>`.

    criterion : {'aic', 'bic'}, default='aic'
        The type of criterion to use.

        Maximum number of iterations to perform. Can be used for
        early stopping.

        As a consequence using LassoLarsIC only makes sense for problems where

    noise_variance : float, default=None
        The estimated noise variance of the data. If `None`, an unbiased
        estimate is computed by an OLS model. However, it is only possible
        in the case where `n_samples > n_features + fit_intercept`.

        the alpha parameter chosen by the information criterion

        is smaller. If a list, it will be of length `n_targets`.

        number of iterations run by lars_path to find the grid of
        alphas.

    criterion_ : array-like of shape (n_alphas,)
        The value of the information criteria ('aic', 'bic') across all
        alphas. The alpha which has the smallest information criterion is
        chosen, as specified in [1]_.

        The estimated noise variance from the data used to compute the
        criterion.

    See Also
    --------

    Notes
    -----
    The number of degrees of freedom is computed as in [1]_.

    To have more details regarding the mathematical formulation of the
    AIC and BIC criteria, please refer to :ref:`User Guide <lasso_lars_ic>`.

    .. [1] :arxiv:`Zou, Hui, Trevor Hastie, and Robert Tibshirani.
            "On the degrees of freedom of the lasso."
            The Annals of Statistics 35.5 (2007): 2173-2192.
            <0712.0881>`

    .. [2] `Wikipedia entry on the Akaike information criterion
            <https://en.wikipedia.org/wiki/Akaike_information_criterion>`_

    .. [3] `Wikipedia entry on the Bayesian information criterion
            <https://en.wikipedia.org/wiki/Bayesian_information_criterion>`_

    Examples
    --------
    >>> reg = linear_model.LassoLarsIC(criterion='bic')
    >>> X = [[-2, 2], [-1, 1], [0, 0], [1, 1], [2, 2]]
    >>> y = [-2.2222, -1.1111, 0, -1.1111, -2.2222]
    LassoLarsIC(criterion='bic')
    [ 0.  -1.11...]
    """

        **LassoLars._parameter_constraints,
        "criterion": [StrOptions({"aic", "bic"})],
        "noise_variance": [Interval(Real, 0, None, closed="left"), None],
    }

    for parameter in ["jitter", "fit_path", "alpha", "random_state"]:

        self,
        criterion="aic",
        *,
        noise_variance=None,
    ):
        self.fit_path = True
        self.noise_variance = noise_variance

    def fit(self, X, y, copy_X=None):

        copy_X : bool, default=None
            If provided, this parameter will override the choice
            of copy_X made at instance creation.
            If ``True``, X will be copied; else, it may be overwritten.

        Returns
        -------
        """
        if copy_X is None:
            copy_X = self.copy_X

        X, y, Xmean, ymean, Xstd = _preprocess_data(
            X, y, fit_intercept=self.fit_intercept, copy=copy_X
        )

        alphas_, _, coef_path_, self.n_iter_ = lars_path(
            X,
            y,
            Gram=Gram,
            copy_Gram=True,
            alpha_min=0.0,
            method="lasso",
            positive=self.positive,
        )

        if self.criterion == "aic":
            criterion_factor = 2
        elif self.criterion == "bic":
            criterion_factor = log(n_samples)
        else:
                f"criterion should be either bic or aic, got {self.criterion!r}"
            )

        residuals = y[:, np.newaxis] - np.dot(X, coef_path_)
        residuals_sum_squares = np.sum(residuals**2, axis=0)
        degrees_of_freedom = np.zeros(coef_path_.shape[1], dtype=int)
        for k, coef in enumerate(coef_path_.T):
            mask = np.abs(coef) > np.finfo(coef.dtype).eps
            if not np.any(mask):
                continue
            # get the number of degrees of freedom equal to:
            # Xc = X[:, mask]
            # Trace(Xc * inv(Xc.T, Xc) * Xc.T) ie the number of non-zero coefs
            degrees_of_freedom[k] = np.sum(mask)

        self.alphas_ = alphas_

        if self.noise_variance is None:
            self.noise_variance_ = self._estimate_noise_variance(
                X, y, positive=self.positive
            )
        else:
            self.noise_variance_ = self.noise_variance

        self.criterion_ = (
            n_samples * np.log(2 * np.pi * self.noise_variance_)
            + residuals_sum_squares / self.noise_variance_
            + criterion_factor * degrees_of_freedom
        )
        n_best = np.argmin(self.criterion_)

        self.alpha_ = alphas_[n_best]
        self.coef_ = coef_path_[:, n_best]
        self._set_intercept(Xmean, ymean, Xstd)

    def _estimate_noise_variance(self, X, y, positive):
        """Compute an estimate of the variance with an OLS model.

            Data to be fitted by the OLS model. We expect the data to be
            centered.

            Associated target.

        positive : bool, default=False
            Restrict coefficients to be >= 0. This should be inline with
            the `positive` parameter from `LassoLarsIC`.

        Returns
        -------
        noise_variance : float
            An estimator of the noise variance of an OLS model.
        """
        if X.shape[0] <= X.shape[1] + self.fit_intercept:
                f"You are using {self.__class__.__name__} in the case where the number "
                "of samples is smaller than the number of features. In this setting, "
                "getting a good estimate for the variance of the noise is not "
                "possible. Provide an estimate of the noise variance in the "
                "constructor."
            )
        # X and y are already centered and we don't need to fit with an intercept
        ols_model = LinearRegression(positive=positive, fit_intercept=False)
        y_pred = ols_model.fit(X, y).predict(X)
        return np.sum((y - y_pred) ** 2) / (
            X.shape[0] - X.shape[1] - self.fit_intercept
        )

        self,
        *,
    ):

        ):

        )

                    X,
                    y[:, k],
                )

                    a[0]
                ]
        else:
                    X,
                    y[:, k],
                )

        Returns
        -------
        """
        )

        else:

            y = y + noise

            X,
            y,
            Xy=Xy,
        )

        self,
        *,
    ):

        self,
        *,
        cv=None,
    ):
            eps=eps,
        )

        Returns
        -------
        """

        else:

            )

                X[train],
                y[train],
                X[test],
                y[test],
            )
        )

            X,
            y,
            Xy=None,
        )

        Returns
        -------
        """
        )

        self,
        *,
        cv=None,
    ):

        self,
        *,
    ):

        Returns
        -------
        """

        )

            X,
            y,
        )

        else:
            )

                continue

            )
        else:

        )

            centered.

        Returns
        -------
        """
            )
        )

class _GeneralizedLinearRegressor(RegressorMixin, BaseEstimator):
    """Regression via a penalized Generalized Linear Model (GLM).

    GLMs based on a reproductive Exponential Dispersion Model (EDM) aim at fitting and
    predicting the mean of the target y as y_pred=h(X*w) with coefficients w.
    Therefore, the fit minimizes the following objective function with L2 priors as
    regularizer::

        1/(2*sum(s_i)) * sum(s_i * deviance(y_i, h(x_i*w)) + 1/2 * alpha * ||w||_2^2

    with inverse link function h, s=sample_weight and per observation (unit) deviance
    deviance(y_i, h(x_i*w)). Note that for an EDM, 1/2 * deviance is the negative
    log-likelihood up to a constant (in w) term.
    The parameter ``alpha`` corresponds to the lambda parameter in glmnet.

    Instead of implementing the EDM family and a link function separately, we directly
    use the loss functions `from sklearn._loss` which have the link functions included
    in them for performance reasons. We pick the loss functions that implement
    (1/2 times) EDM deviances.

    Read more in the :ref:`User Guide <Generalized_linear_models>`.

    .. versionadded:: 0.23

        Constant that multiplies the penalty term and thus determines the
        regularization strength. ``alpha = 0`` is equivalent to unpenalized
        GLMs. In this case, the design matrix `X` must have full column rank
        (no collinearities).

        added to the linear predictor (X @ coef + intercept).

    solver : {'lbfgs', 'newton-cholesky'}, default='lbfgs'
        Algorithm to use in the optimization problem:

        'lbfgs'
            Calls scipy's L-BFGS-B optimizer.

        'newton-cholesky'
            Uses Newton-Raphson steps (in arbitrary precision arithmetic equivalent to
            iterated reweighted least squares) with an inner Cholesky based solver.
            This solver is a good choice for `n_samples` >> `n_features`, especially
            with one-hot encoded categorical features with rare categories. Be aware
            that the memory usage of this solver has a quadratic dependency on
            `n_features` because it explicitly computes the Hessian matrix.

        The maximal number of iterations for the solver.

        Stopping criterion. For the lbfgs solver,
        the iteration will stop when ``max{|g_j|, j = 1, ..., d} <= tol``
        where ``g_j`` is the j-th component of the gradient (derivative) of
        the objective function.

        If set to ``True``, reuse the solution of the previous call to ``fit``
        as initialization for ``coef_`` and ``intercept_``.

        For the lbfgs solver set verbose to any positive number for verbosity.

        Estimated coefficients for the linear predictor (`X @ coef_ +
        intercept_`) in the GLM.

        Intercept (a.k.a. bias) added to linear predictor.

        Actual number of iterations used in the solver.

    _base_loss : BaseLoss, default=HalfSquaredError()
        This is set during fit via `self._get_loss()`.
        A `_base_loss` contains a specific loss function as well as the link
        function. The loss to be minimized specifies the distributional assumption of
        the GLM, i.e. the distribution from the EDM. Here are some examples:

        =======================  ========  ==========================
        _base_loss               Link      Target Domain
        HalfSquaredError         identity  y any real number
        HalfPoissonLoss          log       0 <= y
        HalfGammaLoss            log       0 < y
        HalfTweedieLoss          log       dependent on tweedie power
        HalfTweedieLossIdentity  identity  dependent on tweedie power

        The link function of the GLM, i.e. mapping from linear predictor
        `X @ coeff + intercept` to prediction `y_pred`. For instance, with a log link,
        we have `y_pred = exp(X @ coeff + intercept)`.
    """

    # We allow for NewtonSolver classes for the "solver" parameter but do not
    # make them public in the docstrings. This facilitates testing and
    # benchmarking.
            StrOptions({"lbfgs", "newton-cholesky"}),
            Hidden(type),
        ],
    }

        self,
        *,
        tol=1e-4,
    ):

        """Fit a Generalized Linear Model.

        Returns
        -------
        """
            self,
            X,
            y,
            accept_sparse=["csc", "csr"],
        )

        # required by losses
        if self.solver == "lbfgs":
            # lbfgs will force coef and therefore raw_prediction to be float64. The
            # base_loss needs y, X @ coef and sample_weight all of same dtype
            # (and contiguous).
            loss_dtype = np.float64
        else:
            loss_dtype = min(max(y.dtype, X.dtype), np.float64)
        y = check_array(y, dtype=loss_dtype, order="C", ensure_2d=False)

            # Note that _check_sample_weight calls check_array(order="C") required by
            # losses.
            sample_weight = _check_sample_weight(sample_weight, X, dtype=loss_dtype)

        self._base_loss = self._get_loss()

        linear_loss = LinearModelLoss(
            base_loss=self._base_loss,
        )

        if not linear_loss.base_loss.in_y_true_range(y):
                "Some value(s) of y are out of the valid range of the loss"
                f" {self._base_loss.__class__.__name__!r}."
            )

        # TODO: if alpha=0 check that X is not rank deficient

        # NOTE: Rescaling of sample_weight:
        # We want to minimize
        #     obj = 1/(2 * sum(sample_weight)) * sum(sample_weight * deviance)
        #         + 1/2 * alpha * L2,
        # with
        #     deviance = 2 * loss.
        # The objective is invariant to multiplying sample_weight by a constant. We
        # could choose this constant such that sum(sample_weight) = 1 in order to end
        # up with
        #     obj = sum(sample_weight * loss) + 1/2 * alpha * L2.
        # But LinearModelLoss.loss() already computes
        #     average(loss, weights=sample_weight)
        # Thus, without rescaling, we have
        #     obj = LinearModelLoss.loss(...)

                # LinearModelLoss needs intercept at the end of coefficient array.
                coef = np.concatenate((self.coef_, np.array([self.intercept_])))
            else:
                coef = self.coef_
            coef = coef.astype(loss_dtype, copy=False)
        else:
            coef = linear_loss.init_zero_coef(X, dtype=loss_dtype)
                coef[-1] = linear_loss.base_loss.link.link(
                    np.average(y, weights=sample_weight)
                )

        l2_reg_strength = self.alpha

        # Algorithms for optimization:
        # Note again that our losses implement 1/2 * deviance.
            func = linear_loss.loss_gradient

                func,
                coef,
                jac=True,
                options={
                    "maxiter": self.max_iter,
                    "iprint": self.verbose - 1,
                    "gtol": self.tol,
                    # The constant 64 was found empirically to pass the test suite.
                    # The point is that ftol is very small, but a bit larger than
                    # machine precision for float64, which is the dtype used by lbfgs.
                },
                args=(X, y, sample_weight, l2_reg_strength, n_threads),
            )
            self.n_iter_ = _check_optimize_result("lbfgs", opt_res)
            coef = opt_res.x
        elif self.solver == "newton-cholesky":
                coef=coef,
                linear_loss=linear_loss,
            )
            coef = sol.solve(X, y, sample_weight)
            self.n_iter_ = sol.iteration
        elif issubclass(self.solver, NewtonSolver):
            sol = self.solver(
            )
        else:
            raise ValueError(f"Invalid solver={self.solver}.")

            self.intercept_ = coef[-1]
            self.coef_ = coef[:-1]
        else:
            # set intercept to zero as the other linear models do
            self.coef_ = coef

    def _linear_predictor(self, X):
        """Compute the linear_predictor = `X @ coef_ + intercept_`.

        Note that we often use the term raw_prediction instead of linear predictor.

            Samples.

        Returns
        -------
        y_pred : array of shape (n_samples,)
            Returns predicted values of linear predictor.
        """
            self,
            X,
            allow_nd=False,
        )
        return X @ self.coef_ + self.intercept_

        """Predict using GLM with feature matrix X.

            Samples.

        Returns
        -------
        """
        # check_array is done in _linear_predictor
        raw_prediction = self._linear_predictor(X)
        y_pred = self._base_loss.link.inverse(raw_prediction)

        """Compute D^2, the percentage of deviance explained.

        D^2 is a generalization of the coefficient of determination R^2.
        R^2 uses squared error and D^2 uses the deviance of this GLM, see the
        :ref:`User Guide <regression_metrics>`.

        D^2 is defined as
        :math:`D^2 = 1-\\frac{D(y_{true},y_{pred})}{D_{null}}`,
        :math:`D_{null}` is the null deviance, i.e. the deviance of a model
        with intercept alone, which corresponds to :math:`y_{pred} = \\bar{y}`.
        The mean :math:`\\bar{y}` is averaged by sample_weight.
        Best possible score is 1.0 and it can be negative (because the model
        can be arbitrarily worse).

            True values of target.

        Returns
        -------
            D^2 of self.predict(X) w.r.t. y.
        """
        # TODO: Adapt link to User Guide in the docstring, once
        # https://github.com/scikit-learn/scikit-learn/pull/22118 is merged.
        #
        # Note, default score defined in RegressorMixin is R^2 score.
        # TODO: make D^2 a score function in module metrics (and thereby get
        #       input validation and so on)
        raw_prediction = self._linear_predictor(X)  # validates X
        y = check_array(y, dtype=raw_prediction.dtype, order="C", ensure_2d=False)

            # losses.
            sample_weight = _check_sample_weight(sample_weight, X, dtype=y.dtype)

        base_loss = self._base_loss

        if not base_loss.in_y_true_range(y):
                f" {base_loss.__name__}."
            )

        constant = np.average(
            base_loss.constant_to_optimal_zero(y_true=y, sample_weight=None),
        )

        # Missing factor of 2 in deviance cancels out.
        deviance = base_loss(
            y_true=y,
            n_threads=1,
        )
        y_mean = base_loss.link.link(np.average(y, weights=sample_weight))
        deviance_null = base_loss(
            y_true=y,
            raw_prediction=np.tile(y_mean, y.shape[0]),
        )
        return 1 - (deviance + constant) / (deviance_null + constant)

        try:
            # Create instance of BaseLoss if fit wasn't called yet. This is necessary as
            # TweedieRegressor might set the used loss during fit different from
            # self._base_loss.
            base_loss = self._get_loss()
            tags.target_tags.positive_only = not base_loss.in_y_true_range(-1.0)
            # This happens when the link or power parameter of TweedieRegressor is
            # invalid. We fallback on the default tags in that case.

    def _get_loss(self):
        """This is only necessary because of the link and power arguments of the
        TweedieRegressor.

        Note that we do not need to pass sample_weight to the loss class as this is
        only needed to set loss.constant_hessian on which GLMs do not rely.
        """
        return HalfSquaredError()

class PoissonRegressor(_GeneralizedLinearRegressor):
    """Generalized Linear Model with a Poisson distribution.

    This regressor uses the 'log' link function.

        Constant that multiplies the L2 penalty term and determines the
        Values of `alpha` must be in the range `[0.0, inf)`.

        added to the linear predictor (`X @ coef + intercept`).

        'lbfgs'

        as initialization for ``coef_`` and ``intercept_`` .

    See Also
    --------
    TweedieRegressor : Generalized Linear Model with a Tweedie distribution.

    Examples
    --------
    >>> clf = linear_model.PoissonRegressor()
    >>> X = [[1, 2], [2, 3], [3, 4], [4, 3]]
    >>> y = [12, 17, 22, 21]
    PoissonRegressor()
    np.float64(0.990...)
    >>> clf.coef_
    array([0.121..., 0.158...])
    >>> clf.intercept_
    np.float64(2.088...)
    >>> clf.predict([[1, 1], [3, 4]])
    array([10.676..., 21.875...])
    """

        **_GeneralizedLinearRegressor._parameter_constraints
    }

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        return HalfPoissonLoss()

class GammaRegressor(_GeneralizedLinearRegressor):
    """Generalized Linear Model with a Gamma distribution.

        added to the linear predictor `X @ coef_ + intercept_`.

        'lbfgs'

        as initialization for `coef_` and `intercept_`.

    See Also
    --------
    PoissonRegressor : Generalized Linear Model with a Poisson distribution.

    Examples
    --------
    >>> clf = linear_model.GammaRegressor()
    >>> y = [19, 26, 33, 30]
    GammaRegressor()
    np.float64(0.773...)
    array([0.072..., 0.066...])
    np.float64(2.896...)
    >>> clf.predict([[1, 0], [2, 8]])
    array([19.483..., 35.795...])
    """

    }

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        return HalfGammaLoss()

class TweedieRegressor(_GeneralizedLinearRegressor):
    """Generalized Linear Model with a Tweedie distribution.

    This estimator can be used to model different GLMs depending on the
    ``power`` parameter, which determines the underlying distribution.

            The power determines the underlying target distribution according
            to the following table:

            +-------+------------------------+
            | Power | Distribution           |
            +=======+========================+
            | 0     | Normal                 |
            | 1     | Poisson                |
            | (1,2) | Compound Poisson Gamma |
            | 2     | Gamma                  |
            | 3     | Inverse Gaussian       |

            For ``0 < power < 1``, no distribution exists.

    link : {'auto', 'identity', 'log'}, default='auto'
        `X @ coeff + intercept` to prediction `y_pred`. Option 'auto' sets
        the link depending on the chosen `power` parameter as follows:

        - 'identity' for ``power <= 0``, e.g. for the Normal distribution
        - 'log' for ``power > 0``, e.g. for Poisson, Gamma and Inverse Gaussian
          distributions

        'lbfgs'

    See Also
    --------
    GammaRegressor : Generalized Linear Model with a Gamma distribution.

    Examples
    --------
    >>> clf = linear_model.TweedieRegressor()
    >>> y = [2, 3.5, 5, 5.5]
    TweedieRegressor()
    np.float64(0.839...)
    array([0.599..., 0.299...])
    np.float64(1.600...)
    array([2.500..., 4.599...])
    """

        **_GeneralizedLinearRegressor._parameter_constraints,
        "power": [Interval(Real, None, None, closed="neither")],
        "link": [StrOptions({"auto", "identity", "log"})],
    }

        self,
        *,
        power=0.0,
        link="auto",
        tol=1e-4,
    ):
            tol=tol,
        )
        self.power = power

        if self.link == "auto":
            if self.power <= 0:
                # identity link
                return HalfTweedieLossIdentity(power=self.power)
            else:
                # log link
                return HalfTweedieLoss(power=self.power)

        if self.link == "log":
            return HalfTweedieLoss(power=self.power)

        if self.link == "identity":
            return HalfTweedieLossIdentity(power=self.power)

        self,
        *,
        tol=1e-4,
    ):

        Returns
        -------
        """
            self,
            X,
            y,
        )

        else:

            # losses.

        )

            )

        # with
        # up with

            else:
        else:
                )

                func,
                coef,
                jac=True,
                options={
                },
            )
            )
            )
        else:

        else:

            Samples.

        Returns
        -------
        """
            self,
            X,
        )

            Samples.

        Returns
        -------
        """

        Returns
        -------
        """
        #

            # losses.

            )

        )

            y_true=y,
        )
            y_true=y,
        )

        try:

        """

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

        self,
        *,
        tol=1e-4,
    ):
            tol=tol,
        )

            else:
                # log link

class NewtonSolver(ABC):
    """Newton solver for GLMs.

    This class implements Newton/2nd-order optimization routines for GLMs. Each Newton
    iteration aims at finding the Newton step which is done by the inner solver. With
    Hessian H, gradient g and coefficients coef, one step solves:

        H @ coef_newton = -g

    For our GLM / LinearModelLoss, we have gradient g and Hessian H:

        g = X.T @ loss.gradient + l2_reg_strength * coef
        H = X.T @ diag(loss.hessian) @ X + l2_reg_strength * identity

    Backtracking line search updates coef = coef_old + t * coef_newton for some t in
    (0, 1].

    This is a base class, actual implementations (child classes) may deviate from the
    above pattern and use structure specific tricks.

    Usage pattern:
        - initialize solver: sol = NewtonSolver(...)
        - solve the problem: sol.solve(X, y, sample_weight)

    - Jorge Nocedal, Stephen J. Wright. (2006) "Numerical Optimization"
      2nd edition
      https://doi.org/10.1007/978-0-387-40065-5

    - Stephen P. Boyd, Lieven Vandenberghe. (2004) "Convex Optimization."
      Cambridge University Press, 2004.
      https://web.stanford.edu/~boyd/cvxbook/bv_cvxbook.pdf

    coef : ndarray of shape (n_dof,), (n_classes, n_dof) or (n_classes * n_dof,)
        Initial coefficients of a linear model.
        If shape (n_classes * n_dof,), the classes of one feature are contiguous,
        i.e. one reconstructs the 2d-array via
        coef.reshape((n_classes, -1), order="F").

    linear_loss : LinearModelLoss
        The loss to be minimized.

    l2_reg_strength : float, default=0.0
        L2 regularization strength.

        The optimization problem is solved when each of the following condition is
        fulfilled:
        1. maximum |gradient| <= tol
        2. Newton decrement d: 1/2 * d^2 <= tol

        Maximum number of Newton steps allowed.

        Number of OpenMP threads to use for the computation of the Hessian and gradient
        of the loss function.

    coef_old : ndarray of shape coef.shape
        Coefficient of previous iteration.

    coef_newton : ndarray of shape coef.shape
        Newton step.

    gradient : ndarray of shape coef.shape
        Gradient of the loss w.r.t. the coefficients.

    gradient_old : ndarray of shape coef.shape
        Gradient of previous iteration.

    loss_value : float
        Value of objective function = loss + penalty.

    loss_value_old : float
        Value of objective function of previous itertion.

    raw_prediction : ndarray of shape (n_samples,) or (n_samples, n_classes)

    converged : bool
        Indicator for convergence of the solver.

    iteration : int
        Number of Newton steps, i.e. calls to inner_solve

    use_fallback_lbfgs_solve : bool
        If set to True, the solver will resort to call LBFGS to finish the optimisation
        procedure in case of convergence issues.

    gradient_times_newton : float
        gradient @ coef_newton, set in inner_solve and used by line_search. If the
        Newton step is a descent direction, this is negative.
    """

        self,
        *,
        coef,
        linear_loss=LinearModelLoss(base_loss=HalfSquaredError(), fit_intercept=True),
        tol=1e-4,
    ):
        self.coef = coef
        self.linear_loss = linear_loss
        self.l2_reg_strength = l2_reg_strength

    def setup(self, X, y, sample_weight):
        """Precomputations

        If None, initializes:
            - self.coef
        Sets:
            - self.raw_prediction
            - self.loss_value
        """
        _, _, self.raw_prediction = self.linear_loss.weight_intercept_raw(self.coef, X)
        self.loss_value = self.linear_loss.loss(
            coef=self.coef,
            X=X,
            y=y,
            l2_reg_strength=self.l2_reg_strength,
            n_threads=self.n_threads,
            raw_prediction=self.raw_prediction,
        )

    def update_gradient_hessian(self, X, y, sample_weight):
        """Update gradient and Hessian."""

    def inner_solve(self, X, y, sample_weight):
        """Compute Newton step.

        Sets:
            - self.coef_newton
            - self.gradient_times_newton
        """

    def fallback_lbfgs_solve(self, X, y, sample_weight):
        """Fallback solver in case of emergency.

        If a solver detects convergence problems, it may fall back to this methods in
        the hope to exit with success instead of raising an error.

        Sets:
            - self.converged
        """
            self.linear_loss.loss_gradient,
            self.coef,
            jac=True,
            options={
                "maxiter": self.max_iter - self.iteration,
                "maxls": 50,  # default is 20
                "iprint": self.verbose - 1,
                "ftol": 64 * np.finfo(np.float64).eps,
            },
            args=(X, y, sample_weight, self.l2_reg_strength, self.n_threads),
        )
        self.iteration += _check_optimize_result("lbfgs", opt_res)
        self.coef = opt_res.x
        self.converged = opt_res.status == 0

    def line_search(self, X, y, sample_weight):
        """Backtracking line search.

        Sets:
            - self.coef_old
            - self.loss_value_old
            - self.gradient_old
            - self.gradient
        """
        # line search parameters
        beta, sigma = 0.5, 0.00048828125  # 1/2, 1/2**11
        eps = 16 * np.finfo(self.loss_value.dtype).eps
        t = 1  # step size

        # gradient_times_newton = self.gradient @ self.coef_newton
        # was computed in inner_solve.
        armijo_term = sigma * self.gradient_times_newton
        _, _, raw_prediction_newton = self.linear_loss.weight_intercept_raw(
            self.coef_newton, X
        )

        self.coef_old = self.coef
        self.loss_value_old = self.loss_value
        self.gradient_old = self.gradient

        # np.sum(np.abs(self.gradient_old))
        sum_abs_grad_old = -1

        is_verbose = self.verbose >= 2
        if is_verbose:
            print("  Backtracking Line Search")
            print(f"    eps=16 * finfo.eps={eps}")

        for i in range(21):  # until and including t = beta**20 ~ 1e-6
            self.coef = self.coef_old + t * self.coef_newton
            raw = self.raw_prediction + t * raw_prediction_newton
            self.loss_value, self.gradient = self.linear_loss.loss_gradient(
                coef=self.coef,
                X=X,
                y=y,
                l2_reg_strength=self.l2_reg_strength,
                n_threads=self.n_threads,
                raw_prediction=raw,
            )
            # Note: If coef_newton is too large, loss_gradient may produce inf values,
            # potentially accompanied by a RuntimeWarning.
            # This case will be captured by the Armijo condition.

            # 1. Check Armijo / sufficient decrease condition.
            # The smaller (more negative) the better.
            loss_improvement = self.loss_value - self.loss_value_old
            check = loss_improvement <= t * armijo_term
            if is_verbose:
                print(
                    f"    line search iteration={i + 1}, step size={t}\n"
                    f"      check loss improvement <= armijo term: {loss_improvement} "
                    f"<= {t * armijo_term} {check}"
                )
            if check:
                break
            # 2. Deal with relative loss differences around machine precision.
            tiny_loss = np.abs(self.loss_value_old * eps)
            check = np.abs(loss_improvement) <= tiny_loss
                print(
                    "      check loss |improvement| <= eps * |loss_old|:"
                    f" {np.abs(loss_improvement)} <= {tiny_loss} {check}"
                )
            if check:
                if sum_abs_grad_old < 0:
                    sum_abs_grad_old = scipy.linalg.norm(self.gradient_old, ord=1)
                # 2.1 Check sum of absolute gradients as alternative condition.
                sum_abs_grad = scipy.linalg.norm(self.gradient, ord=1)
                check = sum_abs_grad < sum_abs_grad_old
                if is_verbose:
                    print(
                        "      check sum(|gradient|) < sum(|gradient_old|): "
                        f"{sum_abs_grad} < {sum_abs_grad_old} {check}"
                    )
                if check:
                    break

            t *= beta
        else:
                (
                    f"Line search of Newton solver {self.__class__.__name__} at"
                    f" iteration #{self.iteration} did no converge after 21 line search"
                    " refinement iterations. It will now resort to lbfgs instead."
                ),
            )
                print("  Line search did not converge and resorts to lbfgs instead.")
            self.use_fallback_lbfgs_solve = True
            return

        self.raw_prediction = raw
            print(
                f"    line search successful after {i + 1} iterations with "
                f"loss={self.loss_value}."
            )

    def check_convergence(self, X, y, sample_weight):
        """Check for convergence.

        Sets self.converged.
        """
            print("  Check Convergence")
        # Note: Checking maximum relative change of coefficient <= tol is a bad
        # convergence criterion because even a large step could have brought us close
        # to the true minimum.
        # coef_step = self.coef - self.coef_old
        # change = np.max(np.abs(coef_step) / np.maximum(1, np.abs(self.coef_old)))
        # check = change <= tol

        # 1. Criterion: maximum |gradient| <= tol
        #    The gradient was already updated in line_search()
        g_max_abs = np.max(np.abs(self.gradient))
        check = g_max_abs <= self.tol
            print(f"    1. max |gradient| {g_max_abs} <= {self.tol} {check}")
        if not check:
            return

        # 2. Criterion: For Newton decrement d, check 1/2 * d^2 <= tol
        #       d = sqrt(grad @ hessian^-1 @ grad)
        #         = sqrt(coef_newton @ hessian @ coef_newton)
        #    See Boyd, Vanderberghe (2009) "Convex Optimization" Chapter 9.5.1.
        d2 = self.coef_newton @ self.hessian @ self.coef_newton
        check = 0.5 * d2 <= self.tol
            print(f"    2. Newton decrement {0.5 * d2} <= {self.tol} {check}")
            return

            loss_value = self.linear_loss.loss(
                X=X,
                y=y,
            )
            print(f"  Solver did converge at loss = {loss_value}.")
        self.converged = True

    def finalize(self, X, y, sample_weight):
        """Finalize the solvers results.

        Some solvers may need this, others not.
        """
        pass

    def solve(self, X, y, sample_weight):
        """Solve the optimization problem.

        This is the main routine.

        Order of calls:
            self.setup()
            while iteration:
                self.update_gradient_hessian()
                self.inner_solve()
                self.line_search()
                self.check_convergence()
            self.finalize()

        Returns
        -------
            Solution of the optimization problem.
        """
        # setup usually:
        #   - initializes self.coef if needed
        #   - initializes and calculates self.raw_predictions, self.loss_value
        self.setup(X=X, y=y, sample_weight=sample_weight)

        self.iteration = 1
        self.converged = False
        self.use_fallback_lbfgs_solve = False

        while self.iteration <= self.max_iter and not self.converged:
                print(f"Newton iter={self.iteration}")

            self.use_fallback_lbfgs_solve = False  # Fallback solver.

            # 1. Update Hessian and gradient
            self.update_gradient_hessian(X=X, y=y, sample_weight=sample_weight)

            # TODO:
            # if iteration == 1:
            # We might stop early, e.g. we already are close to the optimum,
            # usually detected by zero gradients at this stage.

            # 2. Inner solver
            #    Calculate Newton step/direction
            #    This usually sets self.coef_newton and self.gradient_times_newton.
            self.inner_solve(X=X, y=y, sample_weight=sample_weight)
            if self.use_fallback_lbfgs_solve:
                break

            # 3. Backtracking line search
            #    This usually sets self.coef_old, self.coef, self.loss_value_old
            #    self.loss_value, self.gradient_old, self.gradient,
            #    self.raw_prediction.
            self.line_search(X=X, y=y, sample_weight=sample_weight)
                break

            # 4. Check convergence
            #    Sets self.converged.
            self.check_convergence(X=X, y=y, sample_weight=sample_weight)

            # 5. Next iteration
            self.iteration += 1

        if not self.converged:
                # Note: The fallback solver circumvents check_convergence and relies on
                # the convergence checks of lbfgs instead. Enough warnings have been
                # raised on the way.
                self.fallback_lbfgs_solve(X=X, y=y, sample_weight=sample_weight)
            else:
                    (
                        f"Newton solver did not converge after {self.iteration - 1} "
                        "iterations."
                    ),
                )

        self.iteration -= 1
        self.finalize(X=X, y=y, sample_weight=sample_weight)
        return self.coef

class NewtonCholeskySolver(NewtonSolver):
    """Cholesky based Newton solver.

    Inner solver for finding the Newton step H w_newton = -g uses Cholesky based linear
    solver.
    """

        super().setup(X=X, y=y, sample_weight=sample_weight)
        if self.linear_loss.base_loss.is_multiclass:
            # Easier with ravelled arrays, e.g., for scipy.linalg.solve.
            # As with LinearModelLoss, we always are contiguous in n_classes.
            self.coef = self.coef.ravel(order="F")
        # Note that the computation of gradient in LinearModelLoss follows the shape of
        # coef.
        self.gradient = np.empty_like(self.coef)
        # But the hessian is always 2d.
        n = self.coef.size
        self.hessian = np.empty_like(self.coef, shape=(n, n))
        # To help case distinctions.
        self.is_multinomial_with_intercept = (
            self.linear_loss.base_loss.is_multiclass and self.linear_loss.fit_intercept
        )
        self.is_multinomial_no_penalty = (
            self.linear_loss.base_loss.is_multiclass and self.l2_reg_strength == 0
        )

        _, _, self.hessian_warning = self.linear_loss.gradient_hessian(
            X=X,
            y=y,
            gradient_out=self.gradient,
            hessian_out=self.hessian,
            raw_prediction=self.raw_prediction,  # this was updated in line_search
        )

        if self.hessian_warning:
                (
                    f"The inner solver of {self.__class__.__name__} detected a "
                    "pointwise hessian with many negative values at iteration "
                    f"#{self.iteration}. It will now resort to lbfgs instead."
                ),
            )
                print(
                    "  The inner solver detected a pointwise Hessian with many "
                    "negative values and resorts to lbfgs instead."
                )
            return

        # Note: The following case distinction could also be shifted to the
        # implementation of HalfMultinomialLoss instead of here within the solver.
        if self.is_multinomial_no_penalty:
            # The multinomial loss is overparametrized for each unpenalized feature, so
            # at least the intercepts. This can be seen by noting that predicted
            # probabilities are invariant under shifting all coefficients of a single
            # feature j for all classes by the same amount c:
            #   coef[k, :] -> coef[k, :] + c    =>    proba stays the same
            # where we have assumed coef.shape = (n_classes, n_features).
            # Therefore, also the loss (-log-likelihood), gradient and hessian stay the
            # same, see
            # Noah Simon and Jerome Friedman and Trevor Hastie. (2013) "A Blockwise
            # Descent Algorithm for Group-penalized Multiresponse and Multinomial
            # Regression". https://doi.org/10.48550/arXiv.1311.6529
            #
            # We choose the standard approach and set all the coefficients of the last
            # class to zero, for all features including the intercept.
            n_classes = self.linear_loss.base_loss.n_classes
            n_dof = self.coef.size // n_classes  # degree of freedom per class
            n = self.coef.size - n_dof  # effective size
            self.coef[n_classes - 1 :: n_classes] = 0
            self.gradient[n_classes - 1 :: n_classes] = 0
            self.hessian[n_classes - 1 :: n_classes, :] = 0
            self.hessian[:, n_classes - 1 :: n_classes] = 0
            # We also need the reduced variants of gradient and hessian where the
            # entries set to zero are removed. For 2 features and 3 classes with
            # arbitrary values, "x" means removed:
            #   gradient = [0, 1, x, 3, 4, x]
            #
            #   hessian = [0,  1, x,  3,  4, x]
            #             [1,  7, x,  9, 10, x]
            #             [x,  x, x,  x,  x, x]
            #             [3,  9, x, 21, 22, x]
            #             [4, 10, x, 22, 28, x]
            #             [x,  x, x,  x, x,  x]
            # The following slicing triggers copies of gradient and hessian.
            gradient = self.gradient.reshape(-1, n_classes)[:, :-1].flatten()
            hessian = self.hessian.reshape(n_dof, n_classes, n_dof, n_classes)[
                :, :-1, :, :-1
            ].reshape(n, n)
        elif self.is_multinomial_with_intercept:
            # Here, only intercepts are unpenalized. We again choose the last class and
            # set its intercept to zero.
            self.coef[-1] = 0
            self.gradient[-1] = 0
            self.hessian[-1, :] = 0
            self.hessian[:, -1] = 0
            gradient, hessian = self.gradient[:-1], self.hessian[:-1, :-1]
        else:
            gradient, hessian = self.gradient, self.hessian

        try:
                warnings.simplefilter("error", scipy.linalg.LinAlgWarning)
                self.coef_newton = scipy.linalg.solve(
                    hessian, -gradient, check_finite=False, assume_a="sym"
                )
                if self.is_multinomial_no_penalty:
                    self.coef_newton = np.c_[
                        self.coef_newton.reshape(n_dof, n_classes - 1), np.zeros(n_dof)
                    ].reshape(-1)
                    assert self.coef_newton.flags.f_contiguous
                elif self.is_multinomial_with_intercept:
                    self.coef_newton = np.r_[self.coef_newton, 0]
                self.gradient_times_newton = self.gradient @ self.coef_newton
                if self.gradient_times_newton > 0:
                        print(
                            "  The inner solver found a Newton step that is not a "
                            "descent direction and resorts to LBFGS steps instead."
                        )
                    self.use_fallback_lbfgs_solve = True
                    return
        except (np.linalg.LinAlgError, scipy.linalg.LinAlgWarning) as e:
                f"The inner solver of {self.__class__.__name__} stumbled upon a "
                "singular or very ill-conditioned Hessian matrix at iteration "
                f"{self.iteration}. It will now resort to lbfgs instead.\n"
                "Further options are to use another solver or to avoid such situation "
                "in the first place. Possible remedies are removing collinear features"
                " of X or increasing the penalization strengths.\n"
                "The original Linear Algebra message was:\n" + str(e),
                scipy.linalg.LinAlgWarning,
            )
            # Possible causes:
            # 1. hess_pointwise is negative. But this is already taken care in
            #    LinearModelLoss.gradient_hessian.
            # 2. X is singular or ill-conditioned
            #    This might be the most probable cause.
            #
            # There are many possible ways to deal with this situation. Most of them
            # add, explicitly or implicitly, a matrix to the hessian to make it
            # positive definite, confer to Chapter 3.4 of Nocedal & Wright 2nd ed.
            # Instead, we resort to lbfgs.
                print(
                    "  The inner solver stumbled upon an singular or ill-conditioned "
                    "Hessian matrix and resorts to LBFGS instead."
                )
            return

            # Our convention is usually the symmetric parametrization where
            # sum(coef[classes, features], axis=0) = 0.
            # We convert now to this convention. Note that it does not change
            # the predicted probabilities.
            self.coef = self.coef.reshape(n_classes, -1, order="F")
            self.coef -= np.mean(self.coef, axis=0)
            # Only the intercept needs an update to the symmetric parametrization.
            self.coef[-n_classes:] -= np.mean(self.coef[-n_classes:])

        self,
        *,
        coef,
        tol=1e-4,
    ):

        Sets:
        """
            X=X,
            y=y,
        )

        Sets:
        """

        Sets:
        """
            jac=True,
            options={
            },
        )

        Sets:
        """

        )

                X=X,
                y=y,
            )

                print(
                )
            if check:
                break
                print(
                )
            if check:
                    print(
                    )
                if check:
                    break

            t *= beta
        else:
                (
                ),
            )
            return

            print(
            )

        """

            return

            return

                X=X,
                y=y,
            )

        """
        pass

        Returns
        -------
        """

            # TODO:

                break

                break

            else:
                    (
                    ),
                )

        # coef.
        )
        )

            X=X,
            y=y,
        )

                (
                ),
            )
                print(
                )
            return

            # same, see
            #
            #
        else:

        try:
                )
                        print(
                        )
                    return
            )
            #
                print(
                )
            return

class BinomialRegressor(_GeneralizedLinearRegressor):
        return HalfBinomialLoss()

def _special_minimize(fun, grad, x, tol_NM, tol):
    # Find good starting point by Nelder-Mead
    res_NM = minimize(
        fun, x, method="Nelder-Mead", options={"xatol": tol_NM, "fatol": tol_NM}
    )
    # Now refine via root finding on the gradient of the function, which is
    # more precise than minimizing the function itself.
    res = root(
        grad,
        res_NM.x,
        method="lm",
        options={"ftol": tol, "xtol": tol, "gtol": tol},
    )
    return res.x

def regression_data():
        n_samples=107, n_features=10, n_informative=80, noise=0.5, random_state=2
    )
    return X, y

def glm_dataset(global_random_seed, request):
    """Dataset with GLM solutions, well conditioned X.

    This is inspired by ols_ridge_dataset in test_ridge.py.

    The construction is based on the SVD decomposition of X = U S V'.

    type : {"long", "wide"}
        If "long", then n_samples > n_features.
        If "wide", then n_features > n_samples.
    model : a GLM model

    For "wide", we return the minimum norm solution:

        min ||w||_2 subject to w = argmin deviance(X, y, w)

    Note that the deviance is always minimized if y = inverse_link(X w) is possible to
    achieve, which it is in the wide data case. Therefore, we can construct the
    solution with minimum norm like (wide) OLS:

        min ||w||_2 subject to link(y) = raw_prediction = X w

    Returns
    -------
    model : GLM model
    X : ndarray
        Last column of 1, i.e. intercept.
    y : ndarray
    coef_unpenalized : ndarray
        Minimum norm solutions, i.e. min sum(loss(w)) (with minimum ||w||_2 in
        case of ambiguity)
        Last coefficient is intercept.
    coef_penalized : ndarray
        GLM solution with alpha=l2_reg_strength=1, i.e.
        min 1/n * sum(loss) + ||w[:-1]||_2^2.
    l2_reg_strength : float
        Always equal 1.
    """
    data_type, model = request.param
    # Make larger dim more than double as big as the smaller one.
    # This helps when constructing singular matrices like (X, X).
    if data_type == "long":
        n_samples, n_features = 12, 4
    else:
        n_samples, n_features = 4, 12
    k = min(n_samples, n_features)
        effective_rank=k,
        tail_strength=0.1,
    )
    X[:, -1] = 1  # last columns acts as intercept
    U, s, Vt = linalg.svd(X, full_matrices=False)
    assert np.all(s > 1e-3)  # to be sure
    assert np.max(s) / np.min(s) < 100  # condition number of X

        coef_unpenalized = rng.uniform(low=1, high=3, size=n_features)
        coef_unpenalized *= rng.choice([-1, 1], size=n_features)
        raw_prediction = X @ coef_unpenalized
    else:
        raw_prediction = rng.uniform(low=-3, high=3, size=n_samples)
        # minimum norm solution min ||w||_2 such that raw_prediction = X w:
        # w = X'(XX')^-1 raw_prediction = V s^-1 U' raw_prediction
        coef_unpenalized = Vt.T @ np.diag(1 / s) @ U.T @ raw_prediction

    linear_loss = LinearModelLoss(base_loss=model._get_loss(), fit_intercept=True)
    sw = np.full(shape=n_samples, fill_value=1 / n_samples)
    y = linear_loss.base_loss.link.inverse(raw_prediction)

    # Add penalty l2_reg_strength * ||coef||_2^2 for l2_reg_strength=1 and solve with
    # optimizer. Note that the problem is well conditioned such that we get accurate
    # results.
    l2_reg_strength = 1
    fun = partial(
        linear_loss.loss,
        X=X[:, :-1],
        y=y,
        sample_weight=sw,
        l2_reg_strength=l2_reg_strength,
    )
    grad = partial(
        linear_loss.gradient,
        y=y,
    )
    coef_penalized_with_intercept = _special_minimize(
        fun, grad, coef_unpenalized, tol_NM=1e-6, tol=1e-14
    )

    linear_loss = LinearModelLoss(base_loss=model._get_loss(), fit_intercept=False)
        y=y,
    )
        y=y,
    )
    coef_penalized_without_intercept = _special_minimize(
        fun, grad, coef_unpenalized[:-1], tol_NM=1e-6, tol=1e-14
    )

    # To be sure
    assert np.linalg.norm(coef_penalized_with_intercept) < np.linalg.norm(
        coef_unpenalized
    )

    return (
        model,
        X,
        y,
        coef_unpenalized,
        coef_penalized_with_intercept,
        coef_penalized_without_intercept,
        l2_reg_strength,
    )

def test_glm_regression(solver, fit_intercept, glm_dataset):
    """Test that GLM converges for all solvers to correct solution.

    We work with a simple constructed data set with known solution.
    """
    model, X, y, _, coef_with_intercept, coef_without_intercept, alpha = glm_dataset
        tol=1e-12,
    )

    model = clone(model).set_params(**params)
    X = X[:, :-1]  # remove intercept
        coef = coef_with_intercept
        intercept = coef[-1]
        coef = coef[:-1]
    else:
        coef = coef_without_intercept
        intercept = 0

    rtol = 5e-5 if solver == "lbfgs" else 1e-9
    assert model.intercept_ == pytest.approx(intercept, rel=rtol)
    assert_allclose(model.coef_, coef, rtol=rtol)

    # Same with sample_weight.
    model = (
        clone(model).set_params(**params).fit(X, y, sample_weight=np.ones(X.shape[0]))
    )

def test_glm_regression_hstacked_X(solver, fit_intercept, glm_dataset):
    """Test that GLM converges for all solvers to correct solution on hstacked data.

    Fit on [X] with alpha is the same as fit on [X, X]/2 with alpha/2.
    For long X, [X, X] is still a long but singular matrix.
    """
        alpha=alpha / 2,
    )

    X = 0.5 * np.concatenate((X, X), axis=1)
    assert np.linalg.matrix_rank(X) <= min(n_samples, n_features - 1)
    else:

        # XXX: Investigate if the ConvergenceWarning that can appear in some
        # cases should be considered a bug or not. In the mean time we don't
        # fail when the assertions below pass irrespective of the presence of
        # the warning.

    rtol = 2e-4 if solver == "lbfgs" else 5e-9
    assert_allclose(model.coef_, np.r_[coef, coef], rtol=rtol)

def test_glm_regression_vstacked_X(solver, fit_intercept, glm_dataset):
    """Test that GLM converges for all solvers to correct solution on vstacked data.

    Fit on [X] with alpha is the same as fit on [X], [y]
                                                [X], [y] with 1 * alpha.
    It is the same alpha as the average loss stays the same.
    For wide X, [X', X'] is a singular matrix.
    """
    )

    X = np.concatenate((X, X), axis=0)
    assert np.linalg.matrix_rank(X) <= min(n_samples, n_features)
    y = np.r_[y, y]
    else:

    rtol = 3e-5 if solver == "lbfgs" else 5e-9

def test_glm_regression_unpenalized(solver, fit_intercept, glm_dataset):
    """Test that unpenalized GLM converges for all solvers to correct solution.

    Note: This checks the minimum norm solution for wide X, i.e.
    n_samples < n_features:
    """
    model, X, y, coef, _, _, _ = glm_dataset
    alpha = 0  # unpenalized
    )

        X = X[:, :-1]  # remove intercept
    else:

        if solver.startswith("newton") and n_samples < n_features:
            # The newton solvers should warn and automatically fallback to LBFGS
            # in this case. The model should still converge.
            warnings.filterwarnings("ignore", category=scipy.linalg.LinAlgWarning)
        warnings.filterwarnings("ignore", category=ConvergenceWarning)

    # FIXME: `assert_allclose(model.coef_, coef)` should work for all cases but fails
    # for the wide/fat case with n_features > n_samples. Most current GLM solvers do
    # NOT return the minimum norm solution with fit_intercept=True.
    if n_samples > n_features:
        rtol = 5e-5 if solver == "lbfgs" else 1e-7
        assert model.intercept_ == pytest.approx(intercept)
        assert_allclose(model.coef_, coef, rtol=rtol)
    else:
        # As it is an underdetermined problem, prediction = y. The following shows that
        # we get a solution, i.e. a (non-unique) minimum of the objective function ...
        rtol = 5e-5
        if solver == "newton-cholesky":
            rtol = 5e-4
        assert_allclose(model.predict(X), y, rtol=rtol)

        norm_solution = np.linalg.norm(np.r_[intercept, coef])
        norm_model = np.linalg.norm(np.r_[model.intercept_, model.coef_])
            # XXX: This solver shows random behaviour. Sometimes it finds solutions
            # with norm_model <= norm_solution! So we check conditionally.
            if norm_model < (1 + 1e-12) * norm_solution:
                assert model.intercept_ == pytest.approx(intercept)
                assert_allclose(model.coef_, coef, rtol=rtol)
        elif solver == "lbfgs" and fit_intercept:
            # But it is not the minimum norm solution. Otherwise the norms would be
            # equal.
            assert norm_model > (1 + 1e-12) * norm_solution

            # See https://github.com/scikit-learn/scikit-learn/issues/23670.
            # Note: Even adding a tiny penalty does not give the minimal norm solution.
            # XXX: We could have naively expected LBFGS to find the minimal norm
            # solution by adding a very small penalty. Even that fails for a reason we
            # do not properly understand at this point.
        else:
            # When `fit_intercept=False`, LBFGS naturally converges to the minimum norm
            # solution on this problem.
            # XXX: Do we have any theoretical guarantees why this should be the case?
            assert model.intercept_ == pytest.approx(intercept, rel=rtol)
            assert_allclose(model.coef_, coef, rtol=rtol)

def test_glm_regression_unpenalized_hstacked_X(solver, fit_intercept, glm_dataset):

    GLM fit on [X] is the same as fit on [X, X]/2.
    For long X, [X, X] is a singular matrix and we check against the minimum norm
    solution:
    """
    )

            X = X[:, :-1]  # remove intercept
            X = 0.5 * np.concatenate((X, X), axis=1)
        else:
            # To know the minimum norm solution, we keep one intercept column and do
            # not divide by 2. Later on, we must take special care.
            X = np.c_[X[:, :-1], X[:, :-1], X[:, -1]]
    else:
        X = 0.5 * np.concatenate((X, X), axis=1)

        if solver.startswith("newton"):

    if fit_intercept and n_samples < n_features:
        # Here we take special care.
        model_intercept = 2 * model.intercept_
        model_coef = 2 * model.coef_[:-1]  # exclude the other intercept term.
        # For minimum norm solution, we would have
        # assert model.intercept_ == pytest.approx(model.coef_[-1])
    else:
        model_intercept = model.intercept_
        model_coef = model.coef_

        assert model_intercept == pytest.approx(intercept)
        rtol = 1e-4
        assert_allclose(model_coef, np.r_[coef, coef], rtol=rtol)
    else:
        rtol = 1e-6 if solver == "lbfgs" else 5e-6
        if (solver == "lbfgs" and fit_intercept) or solver == "newton-cholesky":
            # Same as in test_glm_regression_unpenalized.
            # equal.
            norm_solution = np.linalg.norm(
                0.5 * np.r_[intercept, intercept, coef, coef]
            )
            norm_model = np.linalg.norm(np.r_[model.intercept_, model.coef_])
            # For minimum norm solution, we would have
            # assert model.intercept_ == pytest.approx(model.coef_[-1])
        else:
            assert model_intercept == pytest.approx(intercept, rel=5e-6)
            assert_allclose(model_coef, np.r_[coef, coef], rtol=1e-4)

def test_glm_regression_unpenalized_vstacked_X(solver, fit_intercept, glm_dataset):

    GLM fit on [X] is the same as fit on [X], [y]
                                         [X], [y].
    For wide X, [X', X'] is a singular matrix and we check against the minimum norm
    solution:
    """
    )

    else:

        rtol = 5e-5 if solver == "lbfgs" else 1e-6
    else:

            if not (norm_model > (1 + 1e-12) * norm_solution):
                assert_allclose(model.coef_, coef, rtol=1e-4)
            # equal.
        else:
            rtol = 1e-5 if solver == "newton-cholesky" else 1e-4

def test_sample_weights_validation():
    """Test the raised errors in the validation of sample_weight."""
    # scalar value but not positive
    X = [[1]]
    y = [1]
    weights = 0
    glm = _GeneralizedLinearRegressor()

    # Positive weights are accepted
    glm.fit(X, y, sample_weight=1)

    # 2d array
    weights = [[0]]
    with pytest.raises(ValueError, match="must be 1D array or scalar"):
        glm.fit(X, y, weights)

    # 1d but wrong length
    weights = [1, 0]
    msg = r"sample_weight.shape == \(2,\), expected \(1,\)!"

def test_glm_wrong_y_range(glm):
    """
    Test that fitting a GLM model raises a ValueError when `y` contains
    values outside the valid range for the given distribution.

    Generalized Linear Models (GLMs) with certain distributions, such as
    Poisson, Gamma, and Tweedie (with power > 1), require `y` to be
    non-negative. This test ensures that passing a `y` array containing
    negative values triggers the expected ValueError with the correct message.
    """
    y = np.array([-1, 2])
    X = np.array([[1], [1]])
    msg = r"Some value\(s\) of y are out of the valid range of the loss"
        glm.fit(X, y)

def test_glm_identity_regression(fit_intercept):
    """Test GLM regression with identity link on a simple dataset."""
    coef = [1.0, 2.0]
    X = np.array([[1, 1, 1, 1, 1], [0, 1, 2, 3, 4]]).T
    y = np.dot(X, coef)
    glm = _GeneralizedLinearRegressor(
        alpha=0,
    )
        glm.fit(X[:, 1:], y)
        assert_allclose(glm.coef_, coef[1:], rtol=1e-10)
        assert_allclose(glm.intercept_, coef[0], rtol=1e-10)
    else:
        assert_allclose(glm.coef_, coef, rtol=1e-12)

def test_glm_sample_weight_consistency(fit_intercept, alpha, GLMEstimator):
    """Test that the impact of sample_weight is consistent"""

    glm_params = dict(alpha=alpha, fit_intercept=fit_intercept)

    glm = GLMEstimator(**glm_params).fit(X, y)
    coef = glm.coef_.copy()

    # sample_weight=np.ones(..) should be equivalent to sample_weight=None
    sample_weight = np.ones(y.shape)
    glm.fit(X, y, sample_weight=sample_weight)
    assert_allclose(glm.coef_, coef, rtol=1e-12)

    # sample_weight are normalized to 1 so, scaling them has no effect
    sample_weight = 2 * np.ones(y.shape)

    # setting one element of sample_weight to 0 is equivalent to removing
    # the corresponding sample
    coef1 = glm.coef_.copy()
    glm.fit(X[:-1], y[:-1])
    assert_allclose(glm.coef_, coef1, rtol=1e-12)

    # check that multiplying sample_weight by 2 is equivalent
    # to repeating corresponding samples twice
    X2 = np.concatenate([X, X[: n_samples // 2]], axis=0)
    y2 = np.concatenate([y, y[: n_samples // 2]])
    sample_weight_1 = np.ones(len(y))
    sample_weight_1[: n_samples // 2] = 2

    glm1 = GLMEstimator(**glm_params).fit(X, y, sample_weight=sample_weight_1)

    glm2 = GLMEstimator(**glm_params).fit(X2, y2, sample_weight=None)
    assert_allclose(glm1.coef_, glm2.coef_)

def test_glm_log_regression(solver, fit_intercept, estimator):
    """Test GLM regression with log link on a simple dataset."""
    coef = [0.2, -0.1]
    X = np.array([[0, 1, 2, 3, 4], [1, 1, 1, 1, 1]]).T
    y = np.exp(np.dot(X, coef))
    glm = clone(estimator).set_params(
        alpha=0,
        tol=1e-8,
    )
        res = glm.fit(X[:, :-1], y)
        assert_allclose(res.coef_, coef[:-1], rtol=1e-6)
        assert_allclose(res.intercept_, coef[-1], rtol=1e-6)
    else:
        res = glm.fit(X, y)
        assert_allclose(res.coef_, coef, rtol=2e-6)

def test_warm_start(solver, fit_intercept, global_random_seed):
    """
    Test that `warm_start=True` enables incremental fitting in PoissonRegressor.

    This test verifies that when using `warm_start=True`, the model continues
    optimizing from previous coefficients instead of restarting from scratch.
    It ensures that after an initial fit with `max_iter=1`, the model has a
    higher objective function value (indicating incomplete optimization).
    The test then checks whether allowing additional iterations enables
    convergence to a solution comparable to a fresh training run (`warm_start=False`).
    """
    n_samples, n_features = 100, 10
        n_informative=n_features - 2,
        bias=fit_intercept * 1.0,
    )
    y = np.abs(y)  # Poisson requires non-negative targets.
    alpha = 1
    params = {
        "solver": solver,
        "fit_intercept": fit_intercept,
        "tol": 1e-10,
    }

    glm1 = PoissonRegressor(warm_start=False, max_iter=1000, alpha=alpha, **params)
    glm1.fit(X, y)

    glm2 = PoissonRegressor(warm_start=True, max_iter=1, alpha=alpha, **params)
    # As we intentionally set max_iter=1 such that the solver should raise a
    # ConvergenceWarning.
        glm2.fit(X, y)

    linear_loss = LinearModelLoss(
        base_loss=glm1._get_loss(),
    )
    sw = np.full_like(y, fill_value=1 / n_samples)

    objective_glm1 = linear_loss.loss(
        coef=np.r_[glm1.coef_, glm1.intercept_] if fit_intercept else glm1.coef_,
        X=X,
        y=y,
        l2_reg_strength=alpha,
    )
    objective_glm2 = linear_loss.loss(
        coef=np.r_[glm2.coef_, glm2.intercept_] if fit_intercept else glm2.coef_,
        X=X,
        y=y,
    )
    assert objective_glm1 < objective_glm2

    glm2.set_params(max_iter=1000)
    glm2.fit(X, y)
    # The two models are not exactly identical since the lbfgs solver
    # computes the approximate hessian from previous iterations, which
    # will not be strictly identical in the case of a warm start.
    rtol = 2e-4 if solver == "lbfgs" else 1e-9
    assert_allclose(glm1.coef_, glm2.coef_, rtol=rtol)
    assert_allclose(glm1.score(X, y), glm2.score(X, y), rtol=1e-5)

def test_normal_ridge_comparison(
    n_samples, n_features, fit_intercept, sample_weight, request
):
    """Compare with Ridge regression for Normal distributions."""
    test_size = 10
        n_samples=n_samples + test_size,
        noise=0.5,
    )

        ridge_params = {"solver": "svd"}
    else:
        ridge_params = {"solver": "saga", "max_iter": 1000000, "tol": 1e-7}

    (
        X_train,
        X_test,
        y_train,
        y_test,
    ) = train_test_split(X, y, test_size=test_size, random_state=0)

    alpha = 1.0
        sw_train = None
        alpha_ridge = alpha * n_samples
    else:
        sw_train = np.random.RandomState(0).rand(len(y_train))
        alpha_ridge = alpha * sw_train.sum()

    # GLM has 1/(2*n) * Loss + 1/2*L2, Ridge has Loss + L2
    ridge = Ridge(
        alpha=alpha_ridge,
        **ridge_params,
    )
    ridge.fit(X_train, y_train, sample_weight=sw_train)

        tol=1e-5,
    )
    glm.fit(X_train, y_train, sample_weight=sw_train)
    assert glm.coef_.shape == (X.shape[1],)
    assert_allclose(glm.coef_, ridge.coef_, atol=5e-5)
    assert_allclose(glm.intercept_, ridge.intercept_, rtol=1e-5)
    assert_allclose(glm.predict(X_train), ridge.predict(X_train), rtol=2e-4)
    assert_allclose(glm.predict(X_test), ridge.predict(X_test), rtol=2e-4)

def test_poisson_glmnet(solver):
    """Compare Poisson regression with L2 regularization and LogLink to glmnet"""
    # library("glmnet")
    # options(digits=10)
    # df <- data.frame(a=c(-2,-1,1,2), b=c(0,0,1,1), y=c(0,1,1,2))
    # x <- data.matrix(df[,c("a", "b")])
    # y <- df$y
    # fit <- glmnet(x=x, y=y, alpha=0, intercept=T, family="poisson",
    #               standardize=F, thresh=1e-10, nlambda=10000)
    # coef(fit, s=1)
    # (Intercept) -0.12889386979
    # a            0.29019207995
    # b            0.03741173122
    X = np.array([[-2, -1, 1, 2], [0, 0, 1, 1]]).T
    y = np.array([0, 1, 1, 2])
    glm = PoissonRegressor(
        alpha=1,
        tol=1e-7,
    )
    glm.fit(X, y)
    assert_allclose(glm.intercept_, -0.12889386979, rtol=1e-5)
    assert_allclose(glm.coef_, [0.29019207995, 0.03741173122], rtol=1e-5)

def test_convergence_warning(regression_data):
    X, y = regression_data

    est = _GeneralizedLinearRegressor(max_iter=1, tol=1e-20)

def test_tweedie_link_argument(name, link_class):
    """Test GLM link argument set as string."""
    y = np.array([0.1, 0.5])  # in range of all distributions
    glm = TweedieRegressor(power=1, link=name).fit(X, y)
    assert isinstance(glm._base_loss.link, link_class)

def test_tweedie_link_auto(power, expected_link_class):
    """Test that link='auto' delivers the expected link function"""
    glm = TweedieRegressor(link="auto", power=power).fit(X, y)
    assert isinstance(glm._base_loss.link, expected_link_class)

def test_tweedie_score(regression_data, power, link):
    """Test that GLM score equals d2_tweedie_score for Tweedie losses."""
    # make y positive
    y = np.abs(y) + 1.0
    glm = TweedieRegressor(power=power, link=link).fit(X, y)
    assert glm.score(X, y) == pytest.approx(
        d2_tweedie_score(y, glm.predict(X), power=power)
    )

def test_tags(estimator, value):
    """Test that `positive_only` tag is correctly set for different estimators."""
    assert estimator.__sklearn_tags__().target_tags.positive_only is value

def test_linalg_warning_with_newton_solver(global_random_seed):
    """
    Test that the Newton solver raises a warning and falls back to LBFGS when
    encountering a singular or ill-conditioned Hessian matrix.

    This test assess the behavior of `PoissonRegressor` with the "newton-cholesky"
    solver.
    It verifies the following:-
    - The model significantly improves upon the constant baseline deviance.
    - LBFGS remains robust on collinear data.
    - The Newton solver raises a `LinAlgWarning` on collinear data and falls
      back to LBFGS.
    """
    newton_solver = "newton-cholesky"
    # Use at least 20 samples to reduce the likelihood of getting a degenerate
    # dataset for any global_random_seed.
    X_orig = rng.normal(size=(20, 3))
    y = rng.poisson(
        np.exp(X_orig @ np.ones(X_orig.shape[1])), size=X_orig.shape[0]
    ).astype(np.float64)

    # Collinear variation of the same input features.
    X_collinear = np.hstack([X_orig] * 10)

    # Let's consider the deviance of a constant baseline on this problem.
    baseline_pred = np.full_like(y, y.mean())
    constant_model_deviance = mean_poisson_deviance(y, baseline_pred)
    assert constant_model_deviance > 1.0

    # No warning raised on well-conditioned design, even without regularization.
    tol = 1e-10
        reg = PoissonRegressor(solver=newton_solver, alpha=0.0, tol=tol).fit(X_orig, y)
    original_newton_deviance = mean_poisson_deviance(y, reg.predict(X_orig))

    # On this dataset, we should have enough data points to not make it
    # possible to get a near zero deviance (for the any of the admissible
    # random seeds). This will make it easier to interpret meaning of rtol in
    # the subsequent assertions:
    assert original_newton_deviance > 0.2

    # We check that the model could successfully fit information in X_orig to
    # improve upon the constant baseline by a large margin (when evaluated on
    # the traing set).
    assert constant_model_deviance - original_newton_deviance > 0.1

    # LBFGS is robust to a collinear design because its approximation of the
    # Hessian is Symmeric Positive Definite by construction. Let's record its
    # solution
        reg = PoissonRegressor(solver="lbfgs", alpha=0.0, tol=tol).fit(X_collinear, y)
    collinear_lbfgs_deviance = mean_poisson_deviance(y, reg.predict(X_collinear))

    # The LBFGS solution on the collinear is expected to reach a comparable
    # solution to the Newton solution on the original data.
    rtol = 1e-6
    assert collinear_lbfgs_deviance == pytest.approx(original_newton_deviance, rel=rtol)

    # Fitting a Newton solver on the collinear version of the training data
    # without regularization should raise an informative warning and fallback
    # to the LBFGS solver.
    msg = (
        "The inner solver of .*Newton.*Solver stumbled upon a singular or very "
        "ill-conditioned Hessian matrix"
    )
    with pytest.warns(scipy.linalg.LinAlgWarning, match=msg):
        reg = PoissonRegressor(solver=newton_solver, alpha=0.0, tol=tol).fit(
            X_collinear, y
        )
    # As a result we should still automatically converge to a good solution.
    collinear_newton_deviance = mean_poisson_deviance(y, reg.predict(X_collinear))
    assert collinear_newton_deviance == pytest.approx(
        original_newton_deviance, rel=rtol
    )

    # Increasing the regularization slightly should make the problem go away:
        warnings.simplefilter("error", scipy.linalg.LinAlgWarning)
        reg = PoissonRegressor(solver=newton_solver, alpha=1e-10).fit(X_collinear, y)

    # The slightly penalized model on the collinear data should be close enough
    # to the unpenalized model on the original data.
    penalized_collinear_newton_deviance = mean_poisson_deviance(
        y, reg.predict(X_collinear)
    )
    assert penalized_collinear_newton_deviance == pytest.approx(
    )

def test_newton_solver_verbosity(capsys, verbose):
    """Test the std output of verbose newton solvers."""
    y = np.array([1, 2], dtype=float)
    X = np.array([[1.0, 0], [0, 1]], dtype=float)
    linear_loss = LinearModelLoss(base_loss=HalfPoissonLoss(), fit_intercept=False)
    sol = NewtonCholeskySolver(
        coef=linear_loss.init_zero_coef(X),
        linear_loss=linear_loss,
        l2_reg_strength=0,
    )
    sol.solve(X, y, None)  # returns array([0., 0.69314758])

    if verbose == 0:
        assert captured.out == ""
    else:
        msg = [
            "Newton iter=1",
            "Check Convergence",
            "1. max |gradient|",
            "2. Newton decrement",
            "Solver did converge at loss = ",
        ]
        for m in msg:
            assert m in captured.out

        msg = ["Backtracking Line Search", "line search iteration="]

    # Set the Newton solver to a state with a completely wrong Newton step.
    )
    sol.setup(X=X, y=y, sample_weight=None)
    sol.iteration = 1
    sol.update_gradient_hessian(X=X, y=y, sample_weight=None)
    sol.coef_newton = np.array([1.0, 0])
    sol.gradient_times_newton = sol.gradient @ sol.coef_newton
        sol.line_search(X=X, y=y, sample_weight=None)
        captured = capsys.readouterr()
    if verbose >= 1:
        assert (
            "Line search did not converge and resorts to lbfgs instead." in captured.out
        )

    # Set the Newton solver to a state with bad Newton step such that the loss
    # improvement in line search is tiny.
        coef=np.array([1e-12, 0.69314758]),
    )
    sol.coef_newton = np.array([1e-6, 0])
        msg = [
            "line search iteration=",
            "check loss improvement <= armijo term:",
            "check loss |improvement| <= eps * |loss_old|:",
            "check sum(|gradient|) < sum(|gradient_old|):",
        ]

    # Test for a case with negative hessian. We badly initialize coef for a Tweedie
    # loss with non-canonical link, e.g. Inverse Gaussian deviance with a log link.
        base_loss=HalfTweedieLoss(power=3), fit_intercept=False
    )
        coef=linear_loss.init_zero_coef(X) + 1,
    )
        sol.solve(X, y, None)
        assert (
            "The inner solver detected a pointwise Hessian with many negative values"
            " and resorts to lbfgs instead." in captured.out
        )

def test_balance_property(model, with_sample_weight, global_random_seed):
    # Test that sum(y_predicted) == sum(y_observed) on the training set.
    # This must hold for all linear models with deviance of an exponential disperson
    # family as loss and the corresponding canonical link if fit_intercept=True.
    # Examples:
    #     - squared error and identity link (most linear models)
    #     - Poisson deviance with log link
    #     - log loss with logit link
    # This is known as balance property or unconditional calibration/unbiasedness.
    # For reference, see Corollary 3.18, 3.20 and Chapter 5.1.5 of
    # M.V. Wuthrich and M. Merz, "Statistical Foundations of Actuarial Learning and its
    # Applications" (June 3, 2022). http://doi.org/10.2139/ssrn.3822407

    if (
        with_sample_weight
        and "sample_weight" not in inspect.signature(model.fit).parameters.keys()
    ):
        pytest.skip("Estimator does not support sample_weight.")

    rel = 2e-4  # test precision
    if isinstance(model, SGDRegressor):
        rel = 1e-1
    elif hasattr(model, "solver") and model.solver == "saga":
        rel = 1e-2

    n_train, n_features, n_targets = 100, 10, None
    if isinstance(
        model,
        (MultiTaskElasticNet, MultiTaskElasticNetCV, MultiTaskLasso, MultiTaskLassoCV),
    ):
        n_targets = 3
    X = make_low_rank_matrix(n_samples=n_train, n_features=n_features, random_state=rng)
    if n_targets:
        coef = (
            rng.uniform(low=-2, high=2, size=(n_features, n_targets))
            / np.max(X, axis=0)[:, None]
        )
    else:
        coef = rng.uniform(low=-2, high=2, size=n_features) / np.max(X, axis=0)

    expectation = np.exp(X @ coef + 0.5)
    y = rng.poisson(lam=expectation) + 1  # strict positive, i.e. y > 0
        y = (y > expectation + 1).astype(np.float64)

        sw = rng.uniform(low=1, high=10, size=y.shape[0])
    else:
        sw = None

    model.set_params(fit_intercept=True)  # to be sure
        model.fit(X, y, sample_weight=sw)
    else:
    # Assert balance property.
        assert np.average(model.predict_proba(X)[:, 1], weights=sw) == pytest.approx(
            np.average(y, weights=sw), rel=rel
        )
    else:
        assert np.average(model.predict(X), weights=sw, axis=0) == pytest.approx(
            np.average(y, weights=sw, axis=0), rel=rel
        )

def test_linear_model_regressor_coef_shape(Regressor, ndim):
    """Check the consistency of linear models `coef` shape."""
    if Regressor is LinearRegression:
        pytest.xfail("LinearRegression does not follow `coef_` shape contract!")

    X, y = make_regression(random_state=0, n_samples=200, n_features=20)
    y = MinMaxScaler().fit_transform(y.reshape(-1, 1))[:, 0] + 1
    y = y[:, np.newaxis] if ndim == 2 else y

    regressor = Regressor()
    set_random_state(regressor)
    regressor.fit(X, y)
    assert regressor.coef_.shape == (X.shape[1],)

def test_linear_model_classifier_coef_shape(Classifier, n_classes):
    if Classifier in (RidgeClassifier, RidgeClassifierCV):
        pytest.xfail(f"{Classifier} does not follow `coef_` shape contract!")

    X, y = make_classification(n_informative=10, n_classes=n_classes, random_state=0)

    classifier = Classifier()
    set_random_state(classifier)
    expected_shape = (1, n_features) if n_classes == 2 else (n_classes, n_features)
    assert classifier.coef_.shape == expected_shape

def check_predictions(clf, X, y):
    """Check that the model is able to fit the classification data"""
    n_samples = len(y)

    predicted = clf.fit(X, y).predict(X)
    assert_array_equal(clf.classes_, classes)

    assert predicted.shape == (n_samples,)
    assert_array_equal(predicted, y)

    probabilities = clf.predict_proba(X)
    assert probabilities.shape == (n_samples, n_classes)
    assert_array_almost_equal(probabilities.sum(axis=1), np.ones(n_samples))
    assert_array_equal(probabilities.argmax(axis=1), y)

def test_predict_2_classes(csr_container):
    # Simple sanity check on a 2 classes dataset
    # Make sure it predicts the correct result on simple datasets.
    check_predictions(LogisticRegression(random_state=0), X, Y1)
    check_predictions(LogisticRegression(random_state=0), csr_container(X), Y1)

    check_predictions(LogisticRegression(C=100, random_state=0), X, Y1)
    check_predictions(LogisticRegression(C=100, random_state=0), csr_container(X), Y1)

    check_predictions(LogisticRegression(fit_intercept=False, random_state=0), X, Y1)
    check_predictions(
        LogisticRegression(fit_intercept=False, random_state=0), csr_container(X), Y1
    )

def test_logistic_cv_mock_scorer():
    class MockScorer:
            self.calls = 0
            self.scores = [0.1, 0.4, 0.8, 0.5]

        def __call__(self, model, X, y, sample_weight=None):
            score = self.scores[self.calls % len(self.scores)]
            self.calls += 1

    mock_scorer = MockScorer()
    Cs = [1, 2, 3, 4]
    cv = 2

    lr = LogisticRegressionCV(Cs=Cs, scoring=mock_scorer, cv=cv)

    # Cs[2] has the highest score (0.8) from MockScorer
    assert lr.C_[0] == Cs[2]

    # scorer called 8 times (cv*len(Cs))
    assert mock_scorer.calls == cv * len(Cs)

    # reset mock_scorer
    mock_scorer.calls = 0
    custom_score = lr.score(X, lr.predict(X))

    assert custom_score == mock_scorer.scores[0]
    assert mock_scorer.calls == 1

def test_lr_liblinear_warning():
    n_samples, n_features = iris.data.shape

    lr = LogisticRegression(solver="liblinear", n_jobs=2)
        "'n_jobs' > 1 does not have any effect when"
        " 'solver' is set to 'liblinear'. Got 'n_jobs'"
        " = 2."
    )
    with pytest.warns(UserWarning, match=warning_message):
        lr.fit(iris.data, target)

def test_predict_3_classes(csr_container):
    check_predictions(LogisticRegression(C=10), X, Y2)
    check_predictions(LogisticRegression(C=10), csr_container(X), Y2)

def test_predict_iris(clf):
    """Test logistic regression with the iris dataset.

    Test that both multinomial and OvR solvers handle multiclass data correctly and
    give good accuracy score (>0.95) for the training data.
    """

    if clf.solver == "lbfgs":
        # lbfgs has convergence issues on the iris data with its default max_iter=100
            clf.fit(iris.data, target)
    else:
        clf.fit(iris.data, target)
    assert_array_equal(np.unique(target), clf.classes_)

    pred = clf.predict(iris.data)
    assert np.mean(pred == target) > 0.95

    probabilities = clf.predict_proba(iris.data)
    assert_allclose(probabilities.sum(axis=1), np.ones(n_samples))

    pred = iris.target_names[probabilities.argmax(axis=1)]

def test_check_solver_option(LR):

    # only 'liblinear' solver
    for solver in ["liblinear"]:
        msg = f"Solver {solver} does not support a multinomial backend."
        lr = LR(solver=solver, multi_class="multinomial")
            lr.fit(X, y)

    # all solvers except 'liblinear' and 'saga'
    for solver in ["lbfgs", "newton-cg", "newton-cholesky", "sag"]:
        msg = "Solver %s supports only 'l2' or None penalties," % solver
        lr = LR(solver=solver, penalty="l1", multi_class="ovr")
    for solver in ["lbfgs", "newton-cg", "newton-cholesky", "sag", "saga"]:
        msg = "Solver %s supports only dual=False, got dual=True" % solver
        lr = LR(solver=solver, dual=True, multi_class="ovr")

    # only saga supports elasticnet. We only test for liblinear because the
    # error is raised before for the other solvers (solver %s supports only l2
    # penalties)
        msg = f"Only 'saga' solver supports elasticnet penalty, got solver={solver}."
        lr = LR(solver=solver, penalty="elasticnet")

    # liblinear does not support penalty='none'
    # (LogisticRegressionCV does not supports penalty='none' at all)
    if LR is LogisticRegression:
        msg = "penalty=None is not supported for the liblinear solver"
        lr = LR(penalty=None, solver="liblinear")

def test_elasticnet_l1_ratio_err_helpful(LR):
    # Check that an informative error message is raised when penalty="elasticnet"
    # but l1_ratio is not specified.
    model = LR(penalty="elasticnet", solver="saga")
    with pytest.raises(ValueError, match=r".*l1_ratio.*"):
        model.fit(np.array([[1, 2], [3, 4]]), np.array([0, 1]))

def test_multinomial_binary(solver):
    # Test multinomial LR on a binary problem.
    target = (iris.target > 0).astype(np.intp)
    target = np.array(["setosa", "not-setosa"])[target]

    clf = LogisticRegression(
        solver=solver, multi_class="multinomial", random_state=42, max_iter=2000
    )
    clf.fit(iris.data, target)

    assert clf.coef_.shape == (1, iris.data.shape[1])
    assert clf.intercept_.shape == (1,)
    assert_array_equal(clf.predict(iris.data), target)

    mlr = LogisticRegression(
        solver=solver, multi_class="multinomial", random_state=42, fit_intercept=False
    )
    mlr.fit(iris.data, target)
    pred = clf.classes_[np.argmax(clf.predict_log_proba(iris.data), axis=1)]
    assert np.mean(pred == target) > 0.9

def test_multinomial_binary_probabilities(global_random_seed):
    # Test multinomial LR gives expected probabilities based on the
    # decision function, for a binary problem.
        multi_class="multinomial",
        solver="saga",
        tol=1e-3,
    )

    decision = clf.decision_function(X)

    expected_proba_class_1 = np.exp(decision) / (np.exp(decision) + np.exp(-decision))
    expected_proba = np.c_[1 - expected_proba_class_1, expected_proba_class_1]

    assert_almost_equal(proba, expected_proba)

def test_sparsify(coo_container):
    # Test sparsify and densify members.
    X = scale(iris.data)
    clf = LogisticRegression(random_state=0).fit(X, target)

    pred_d_d = clf.decision_function(X)

    assert sparse.issparse(clf.coef_)
    pred_s_d = clf.decision_function(X)

    sp_data = coo_container(X)
    pred_s_s = clf.decision_function(sp_data)

    clf.densify()
    pred_d_s = clf.decision_function(sp_data)

    assert_array_almost_equal(pred_d_d, pred_s_d)
    assert_array_almost_equal(pred_d_d, pred_s_s)
    assert_array_almost_equal(pred_d_d, pred_d_s)

def test_inconsistent_input():
    # Test that an exception is raised on inconsistent input
    X_ = rng.random_sample((5, 10))
    y_ = np.ones(X_.shape[0])
    y_[0] = 0

    # Wrong dimensions for training data
    y_wrong = y_[:-1]

        clf.fit(X, y_wrong)

    # Wrong dimensions for test data
        clf.fit(X_, y_).predict(rng.random_sample((3, 12)))

def test_write_parameters():
    # Test that we can write to coef_ and intercept_
    clf.fit(X, Y1)
    clf.coef_[:] = 0
    clf.intercept_[:] = 0
    assert_array_almost_equal(clf.decision_function(X), 0)

def test_nan():
    # Test proper NaN handling.
    # Regression test for Issue #252: fit used to go into an infinite loop.
    Xnan = np.array(X, dtype=np.float64)
    Xnan[0, 1] = np.nan
    logistic = LogisticRegression(random_state=0)

        logistic.fit(Xnan, Y1)

def test_consistency_path():
    # Test that the path algorithm is consistent
    X = np.concatenate((rng.randn(100, 2) + [1, 1], rng.randn(100, 2)))
    y = [1] * 100 + [-1] * 100
    Cs = np.logspace(0, 4, 10)

    # can't test with fit_intercept=True since LIBLINEAR
    # penalizes the intercept
    for solver in ["sag", "saga"]:
        coefs, Cs, _ = f(_logistic_regression_path)(
            X,
            y,
            Cs=Cs,
            tol=1e-5,
        )
        for i, C in enumerate(Cs):
            lr = LogisticRegression(
                C=C,
                tol=1e-5,
                max_iter=1000,
            )
            lr_coef = lr.coef_.ravel()
                lr_coef, coefs[i], decimal=4, err_msg="with solver = %s" % solver
            )

    # test for fit_intercept=True
    for solver in ("lbfgs", "newton-cg", "newton-cholesky", "liblinear", "sag", "saga"):
        Cs = [1e3]
            X,
            y,
            Cs=Cs,
            tol=1e-6,
            intercept_scaling=10000.0,
        )
        lr = LogisticRegression(
            C=Cs[0],
            tol=1e-6,
        )
        lr.fit(X, y)
        lr_coef = np.concatenate([lr.coef_.ravel(), lr.intercept_])
            lr_coef, coefs[0], decimal=4, err_msg="with solver = %s" % solver
        )

def test_logistic_regression_path_convergence_fail():
    Cs = [1e3]

    # Check that the convergence message points to both a model agnostic
    # advice (scaling the data) and to the logistic regression specific
    # documentation that includes hints on the solver configuration.
    with pytest.warns(ConvergenceWarning) as record:
        _logistic_regression_path(
            X, y, Cs=Cs, tol=0.0, max_iter=1, random_state=0, verbose=0
        )

    assert len(record) == 1
    warn_msg = record[0].message.args[0]
    assert "lbfgs failed to converge" in warn_msg
    assert "Increase the number of iterations" in warn_msg
    assert "scale the data" in warn_msg
    assert "linear_model.html#logistic-regression" in warn_msg

def test_liblinear_dual_random_state():
    # random_state is relevant for liblinear solver only if dual=True
    X, y = make_classification(n_samples=20, random_state=0)
    lr1 = LogisticRegression(
        dual=True,
        tol=1e-3,
        solver="liblinear",
    )
    lr1.fit(X, y)
    lr2 = LogisticRegression(
        tol=1e-3,
    )
    lr2.fit(X, y)
    lr3 = LogisticRegression(
        random_state=8,
        tol=1e-3,
    )
    lr3.fit(X, y)

    # same result for same random state
    assert_array_almost_equal(lr1.coef_, lr2.coef_)
    # different results for different random states
    msg = "Arrays are not almost equal to 6 decimals"
        assert_array_almost_equal(lr1.coef_, lr3.coef_)

def test_logistic_cv():
    # test for LogisticRegressionCV object
    X_ref = rng.randn(n_samples, n_features)
    y = np.sign(X_ref.dot(5 * rng.randn(n_features)))
    X_ref -= X_ref.mean()
    X_ref /= X_ref.std()
    lr_cv = LogisticRegressionCV(
        Cs=[1.0], fit_intercept=False, solver="liblinear", cv=3
    )
    lr_cv.fit(X_ref, y)
    lr = LogisticRegression(C=1.0, fit_intercept=False, solver="liblinear")
    lr.fit(X_ref, y)
    assert_array_almost_equal(lr.coef_, lr_cv.coef_)

    assert_array_equal(lr_cv.coef_.shape, (1, n_features))
    assert_array_equal(lr_cv.classes_, [-1, 1])
    assert len(lr_cv.classes_) == 2

    coefs_paths = np.asarray(list(lr_cv.coefs_paths_.values()))
    assert_array_equal(coefs_paths.shape, (1, 3, 1, n_features))
    assert_array_equal(lr_cv.Cs_.shape, (1,))
    scores = np.asarray(list(lr_cv.scores_.values()))
    assert_array_equal(scores.shape, (1, 3, 1))

def test_logistic_cv_multinomial_score(scoring, multiclass_agg_list):
    # test that LogisticRegressionCV uses the right score to compute its
    # cross-validation scores when using a multinomial scoring
    # see https://github.com/scikit-learn/scikit-learn/issues/8720
        n_samples=100, random_state=0, n_classes=3, n_informative=6
    )
    train, test = np.arange(80), np.arange(80, 100)
    lr = LogisticRegression(C=1.0)
    # we use lbfgs to support multinomial
    params = lr.get_params()
    # we store the params to set them further in _log_reg_scoring_path
    for key in ["C", "n_jobs", "warm_start"]:
        del params[key]
    lr.fit(X[train], y[train])
    for averaging in multiclass_agg_list:
        scorer = get_scorer(scoring + averaging)
            _log_reg_scoring_path(
                X,
                y,
                train,
                test,
                Cs=[1.0],
                pos_class=None,
                max_squared_sum=None,
                score_params=None,
                **(params | {"multi_class": "multinomial"}),
            )[2][0],
            scorer(lr, X[test], y[test]),
        )

def test_multinomial_logistic_regression_string_inputs():
    # Test with string labels for LogisticRegression(CV)
    n_samples, n_features, n_classes = 50, 5, 3
    X_ref, y = make_classification(
    )
    y_str = LabelEncoder().fit(["bar", "baz", "foo"]).inverse_transform(y)
    # For numerical labels, let y values be taken from set (-1, 0, 1)
    y = np.array(y) - 1
    # Test for string labels
    lr_cv = LogisticRegressionCV(Cs=3)
    lr_str = LogisticRegression()
    lr_cv_str = LogisticRegressionCV(Cs=3)

    lr_str.fit(X_ref, y_str)
    lr_cv_str.fit(X_ref, y_str)

    assert_array_almost_equal(lr.coef_, lr_str.coef_)
    assert sorted(lr_str.classes_) == ["bar", "baz", "foo"]
    assert_array_almost_equal(lr_cv.coef_, lr_cv_str.coef_)
    assert sorted(lr_cv_str.classes_) == ["bar", "baz", "foo"]

    # The predictions should be in original labels
    assert sorted(np.unique(lr_str.predict(X_ref))) == ["bar", "baz", "foo"]
    assert sorted(np.unique(lr_cv_str.predict(X_ref))) == ["bar", "baz", "foo"]

    # Make sure class weights can be given with string labels
    lr_cv_str = LogisticRegression(class_weight={"bar": 1, "baz": 2, "foo": 0}).fit(
        X_ref, y_str
    )
    assert sorted(np.unique(lr_cv_str.predict(X_ref))) == ["bar", "baz"]

def test_logistic_cv_sparse(csr_container):
    X, y = make_classification(n_samples=50, n_features=5, random_state=0)
    X[X < 1.0] = 0.0

    clf = LogisticRegressionCV()
    clfs = LogisticRegressionCV()
    clfs.fit(csr, y)
    assert_array_almost_equal(clfs.coef_, clf.coef_)
    assert_array_almost_equal(clfs.intercept_, clf.intercept_)
    assert clfs.C_ == clf.C_

def test_ovr_multinomial_iris():
    # Test that OvR and multinomial are correct using the iris dataset.
    train, target = iris.data, iris.target
    n_samples, n_features = train.shape

    # The cv indices from stratified kfold (where stratification is done based
    # on the fine-grained iris classes, i.e, before the classes 0 and 1 are
    # conflated) is used for both clf and clf1
    n_cv = 2
    cv = StratifiedKFold(n_cv)
    precomputed_folds = list(cv.split(train, target))

    # Train clf on the original dataset where classes 0 and 1 are separated
    clf = LogisticRegressionCV(cv=precomputed_folds, multi_class="ovr")
    clf.fit(train, target)

    # Conflate classes 0 and 1 and train clf1 on this modified dataset
    clf1 = LogisticRegressionCV(cv=precomputed_folds, multi_class="ovr")
    target_copy = target.copy()
    target_copy[target_copy == 0] = 1
    clf1.fit(train, target_copy)

    # Ensure that what OvR learns for class2 is same regardless of whether
    # classes 0 and 1 are separated or not
    assert_allclose(clf.scores_[2], clf1.scores_[2])
    assert_allclose(clf.intercept_[2:], clf1.intercept_)
    assert_allclose(clf.coef_[2][np.newaxis, :], clf1.coef_)

    # Test the shape of various attributes.
    assert clf.coef_.shape == (3, n_features)
    assert_array_equal(clf.classes_, [0, 1, 2])
    coefs_paths = np.asarray(list(clf.coefs_paths_.values()))
    assert coefs_paths.shape == (3, n_cv, 10, n_features + 1)
    assert clf.Cs_.shape == (10,)
    scores = np.asarray(list(clf.scores_.values()))
    assert scores.shape == (3, n_cv, 10)

    # Test that for the iris data multinomial gives a better accuracy than OvR
    for solver in ["lbfgs", "newton-cg", "sag", "saga"]:
        max_iter = 500 if solver in ["sag", "saga"] else 30
        clf_multi = LogisticRegressionCV(
            tol=1e-3 if solver in ["sag", "saga"] else 1e-2,
            cv=2,
        )
            # lbfgs requires scaling to avoid convergence warnings
            train = scale(train)

        clf_multi.fit(train, target)
        multi_score = clf_multi.score(train, target)
        ovr_score = clf.score(train, target)
        assert multi_score > ovr_score

        # Test attributes of LogisticRegressionCV
        assert clf.coef_.shape == clf_multi.coef_.shape
        assert_array_equal(clf_multi.classes_, [0, 1, 2])
        coefs_paths = np.asarray(list(clf_multi.coefs_paths_.values()))
        assert coefs_paths.shape == (3, n_cv, 10, n_features + 1)
        assert clf_multi.Cs_.shape == (10,)
        scores = np.asarray(list(clf_multi.scores_.values()))
        assert scores.shape == (3, n_cv, 10)

def test_logistic_regression_solvers():
    """Test solvers converge to the same result."""
    X, y = make_classification(n_features=10, n_informative=5, random_state=0)

    params = dict(fit_intercept=False, random_state=42)

    regressors = {
        solver: LogisticRegression(solver=solver, **params).fit(X, y)
        for solver in SOLVERS
    }

    for solver_1, solver_2 in itertools.combinations(regressors, r=2):
            regressors[solver_1].coef_, regressors[solver_2].coef_, decimal=3
        )

def test_logistic_regression_solvers_multiclass(fit_intercept):
    """Test solvers converge to the same result for multiclass problems."""
        n_samples=20, n_features=20, n_informative=10, n_classes=3, random_state=0
    )
    tol = 1e-8
    params = dict(fit_intercept=fit_intercept, tol=tol, random_state=42)

    # Override max iteration count for specific solvers to allow for
    # proper convergence.
    solver_max_iter = {"lbfgs": 200, "sag": 10_000, "saga": 10_000}

        solver: LogisticRegression(
            solver=solver, max_iter=solver_max_iter.get(solver, 100), **params
        for solver in set(SOLVERS) - set(["liblinear"])
    }

            regressors[solver_1].coef_,
            regressors[solver_2].coef_,
            rtol=5e-3 if (solver_1 == "saga" or solver_2 == "saga") else 1e-3,
            err_msg=f"{solver_1} vs {solver_2}",
        )
                regressors[solver_1].intercept_,
                regressors[solver_2].intercept_,
                rtol=5e-3 if (solver_1 == "saga" or solver_2 == "saga") else 1e-3,
                err_msg=f"{solver_1} vs {solver_2}",
            )

def test_logistic_regression_solvers_multiclass_unpenalized(
    fit_intercept, global_random_seed
):
    """Test and compare solver results for unpenalized multinomial multiclass."""
    # We want to avoid perfect separation.
    n_samples, n_features, n_classes = 100, 4, 3
        n_features=n_features + fit_intercept,
        effective_rank=n_features + fit_intercept,
    )
        X[:, -1] = 1
    U, s, Vt = svd(X)
    assert np.all(s > 1e-3)  # to be sure that X is not singular
        X = X[:, :-1]
    coef = rng.uniform(low=1, high=3, size=n_features * n_classes)
    coef = coef.reshape(n_classes, n_features)
    intercept = rng.uniform(low=-1, high=1, size=n_classes) * fit_intercept
    raw_prediction = X @ coef.T + intercept

    proba = loss.link.inverse(raw_prediction)
    # Only newer numpy version (1.22) support more dimensions on pvals.
    y = np.zeros(n_samples)
        y[i] = np.argwhere(rng.multinomial(n=1, pvals=proba[i, :]))[0, 0]

    tol = 1e-9
    params = dict(fit_intercept=fit_intercept, random_state=42)
    solver_tol = {"sag": 1e-8, "saga": 1e-8}
            C=np.inf,
            tol=solver_tol.get(solver, tol),
            max_iter=solver_max_iter.get(solver, 100),
            **params,
    }
    for solver in regressors.keys():
        # See the docstring of test_multinomial_identifiability_on_iris for reference.
            regressors[solver].coef_.sum(axis=0), 0, atol=1e-10, err_msg=solver
        )

            rtol=5e-3 if (solver_1 == "saga" or solver_2 == "saga") else 2e-3,
        )
            )

def test_logistic_regressioncv_class_weights(weight, class_weight, global_random_seed):
    """Test class_weight for LogisticRegressionCV."""
    n_classes = len(weight)
    if class_weight == "weight":
        class_weight = weight

        n_features=3,
    )
        Cs=1,
        fit_intercept=False,
        tol=1e-8,
    )
    clf_lbfgs = LogisticRegressionCV(solver="lbfgs", **params)

    # XXX: lbfgs' line search can fail and cause a ConvergenceWarning for some
    # 10% of the random seeds, but only on specific platforms (in particular
    # when using Atlas BLAS/LAPACK implementation). Doubling the maxls internal
    # parameter of the solver does not help. However this lack of proper
    # convergence does not seem to prevent the assertion to pass, so we ignore
    # the warning for now.
    # See: https://github.com/scikit-learn/scikit-learn/pull/27649
        clf_lbfgs.fit(X, y)

    for solver in set(SOLVERS) - set(["lbfgs", "liblinear", "newton-cholesky"]):
        clf = LogisticRegressionCV(solver=solver, **params)
        if solver in ("sag", "saga"):
            clf.set_params(
                tol=1e-18, max_iter=10000, random_state=global_random_seed + 1
            )

            clf.coef_, clf_lbfgs.coef_, rtol=1e-3, err_msg=f"{solver} vs lbfgs"
        )

def test_logistic_regression_sample_weights(problem, solver, global_random_seed):
    n_samples_per_cv_group = 200
    n_cv_groups = 3

        n_samples=n_samples_per_cv_group * n_cv_groups,
    )
    sw = np.ones(y.shape[0])

    kw_weighted = {
        "fit_intercept": False,
        "max_iter": 100_000 if solver.startswith("sag") else 1_000,
        "tol": 1e-8,
    }
    kw_repeated = kw_weighted.copy()
    sw[:n_samples_per_cv_group] = rng.randint(0, 5, size=n_samples_per_cv_group)
    X_repeated = np.repeat(X, sw.astype(int), axis=0)
    y_repeated = np.repeat(y, sw.astype(int), axis=0)

    if problem == "single":
        LR = LogisticRegression
    elif problem == "cv":
        LR = LogisticRegressionCV
        # We weight the first fold 2 times more.
        groups_weighted = np.concatenate(
            [
                np.full(n_samples_per_cv_group, 0),
                np.full(n_samples_per_cv_group, 1),
                np.full(n_samples_per_cv_group, 2),
            ]
        )
        splits_weighted = list(LeaveOneGroupOut().split(X, groups=groups_weighted))
        kw_weighted.update({"Cs": 100, "cv": splits_weighted})

        groups_repeated = np.repeat(groups_weighted, sw.astype(int), axis=0)
        splits_repeated = list(
            LeaveOneGroupOut().split(X_repeated, groups=groups_repeated)
        )
        kw_repeated.update({"Cs": 100, "cv": splits_repeated})

    clf_sw_weighted = LR(solver=solver, **kw_weighted)
    clf_sw_repeated = LR(solver=solver, **kw_repeated)

    if solver == "lbfgs":
        # lbfgs has convergence issues on the data but this should not impact
        # the quality of the results.
            clf_sw_weighted.fit(X, y, sample_weight=sw)
            clf_sw_repeated.fit(X_repeated, y_repeated)

    else:
        clf_sw_weighted.fit(X, y, sample_weight=sw)
        clf_sw_repeated.fit(X_repeated, y_repeated)

    if problem == "cv":
        assert_allclose(clf_sw_weighted.scores_[1], clf_sw_repeated.scores_[1])
    assert_allclose(clf_sw_weighted.coef_, clf_sw_repeated.coef_, atol=1e-5)

def test_logistic_regression_solver_class_weights(solver, global_random_seed):
    # Test that passing class_weight as [1, 2] is the same as
    # passing class weight = [1,1] but adjusting sample weights
    # to be 2 for all instances of class 1.

        n_samples=300,
    )

    sample_weight = y + 1

        "max_iter": 100_000,
    }
    clf_cw_12 = LogisticRegression(
        solver=solver, class_weight={0: 1, 1: 2}, **kw_weighted
    )
    clf_cw_12.fit(X, y)
    clf_sw_12 = LogisticRegression(solver=solver, **kw_weighted)
    clf_sw_12.fit(X, y, sample_weight=sample_weight)
    assert_allclose(clf_cw_12.coef_, clf_sw_12.coef_, atol=1e-6)

def test_sample_and_class_weight_equivalence_liblinear(global_random_seed):
    # Test the above for l1 penalty and l2 penalty with dual=True.
    # since the patched liblinear code is different.

    )

    clf_cw = LogisticRegression(
        class_weight={0: 1, 1: 2},
        max_iter=10_000,
    )
    clf_cw.fit(X, y)
    clf_sw = LogisticRegression(
    )
    clf_sw.fit(X, y, sample_weight)
    assert_allclose(clf_cw.coef_, clf_sw.coef_, atol=1e-10)

    )
    )

def _compute_class_weight_dictionary(y):
    # helper for returning a dictionary instead of an array
    class_weight = compute_class_weight("balanced", classes=classes, y=y)
    class_weight_dict = dict(zip(classes, class_weight))
    return class_weight_dict

def test_logistic_regression_class_weights(csr_container):
    # Scale data to avoid convergence warnings with the lbfgs solver
    X_iris = scale(iris.data)
    # Multinomial case: remove 90% of class 0
    X = X_iris[45:, :]
    y = iris.target[45:]
    class_weight_dict = _compute_class_weight_dictionary(y)

    for solver in set(SOLVERS) - set(["liblinear", "newton-cholesky"]):
        params = dict(solver=solver, max_iter=1000)
        clf1 = LogisticRegression(class_weight="balanced", **params)
        clf2 = LogisticRegression(class_weight=class_weight_dict, **params)
        clf1.fit(X, y)
        clf2.fit(X, y)
        assert len(clf1.classes_) == 3
        assert_allclose(clf1.coef_, clf2.coef_, rtol=1e-4)
        # Same as appropriate sample_weight.
        sw = np.ones(X.shape[0])
        for c in clf1.classes_:
            sw[y == c] *= class_weight_dict[c]
        clf3 = LogisticRegression(**params).fit(X, y, sample_weight=sw)
        assert_allclose(clf3.coef_, clf2.coef_, rtol=1e-4)

    # Binary case: remove 90% of class 0 and 100% of class 2
    X = X_iris[45:100, :]
    y = iris.target[45:100]

    for solver in SOLVERS:
        assert_array_almost_equal(clf1.coef_, clf2.coef_, decimal=6)

def test_logistic_regression_multinomial():
    # Tests for the multinomial option in logistic regression

    # Some basic attributes of Logistic Regression
    n_samples, n_features, n_classes = 50, 20, 3
        n_informative=10,
    )

    X = StandardScaler(with_mean=False).fit_transform(X)

    # 'lbfgs' is used as a referenced
    solver = "lbfgs"
    ref_i = LogisticRegression(solver=solver, tol=1e-6)
    ref_w = LogisticRegression(solver=solver, fit_intercept=False, tol=1e-6)
    ref_i.fit(X, y)
    ref_w.fit(X, y)
    assert ref_i.coef_.shape == (n_classes, n_features)
    assert ref_w.coef_.shape == (n_classes, n_features)
    for solver in ["sag", "saga", "newton-cg"]:
        clf_i = LogisticRegression(
            max_iter=2000,
            tol=1e-7,
        )
        clf_w = LogisticRegression(
            tol=1e-7,
        )
        clf_i.fit(X, y)
        clf_w.fit(X, y)
        assert clf_i.coef_.shape == (n_classes, n_features)
        assert clf_w.coef_.shape == (n_classes, n_features)

        # Compare solutions between lbfgs and the other solvers
        assert_allclose(ref_i.coef_, clf_i.coef_, rtol=1e-3)
        assert_allclose(ref_w.coef_, clf_w.coef_, rtol=1e-2)
        assert_allclose(ref_i.intercept_, clf_i.intercept_, rtol=1e-3)

    # Test that the path give almost the same results. However since in this
    # case we take the average of the coefs after fitting across all the
    # folds, it need not be exactly the same.
        clf_path = LogisticRegressionCV(
            solver=solver, max_iter=2000, tol=1e-6, Cs=[1.0]
        )
        clf_path.fit(X, y)
        assert_allclose(clf_path.coef_, ref_i.coef_, rtol=1e-2)
        assert_allclose(clf_path.intercept_, ref_i.intercept_, rtol=1e-2)

def test_liblinear_decision_function_zero():
    # Test negative prediction when decision_function values are zero.
    # Liblinear predicts the positive class when decision_function values
    # are zero. This is a test to verify that we do not do the same.
    # See Issue: https://github.com/scikit-learn/scikit-learn/issues/3600
    # and the PR https://github.com/scikit-learn/scikit-learn/pull/3623
    clf = LogisticRegression(fit_intercept=False, solver="liblinear")

    # Dummy data such that the decision function becomes zero.
    X = np.zeros((5, 5))
    assert_array_equal(clf.predict(X), np.zeros(5))

def test_liblinear_logregcv_sparse(csr_container):
    # Test LogRegCV with solver='liblinear' works for sparse matrices

    X, y = make_classification(n_samples=10, n_features=5, random_state=0)
    clf = LogisticRegressionCV(solver="liblinear")
    clf.fit(csr_container(X), y)

def test_saga_sparse(csr_container):

    clf = LogisticRegressionCV(solver="saga", tol=1e-2)

def test_logreg_intercept_scaling_zero():

    clf = LogisticRegression(fit_intercept=False)
    assert clf.intercept_ == 0.0

def test_logreg_l1():
    # Because liblinear penalizes the intercept and saga does not, we do not
    # fit the intercept to make it possible to compare the coefficients of
    # the two models at convergence.
    X, y = make_classification(n_samples=n_samples, n_features=20, random_state=0)
    X_noise = rng.normal(size=(n_samples, 3))
    X_constant = np.ones(shape=(n_samples, 2))
    X = np.concatenate((X, X_noise, X_constant), axis=1)
    lr_liblinear = LogisticRegression(
        C=1.0,
        tol=1e-10,
    )
    lr_liblinear.fit(X, y)

    lr_saga = LogisticRegression(
        C=1.0,
    )
    lr_saga.fit(X, y)
    assert_array_almost_equal(lr_saga.coef_, lr_liblinear.coef_)

    # Noise and constant features should be regularized to zero by the l1
    # penalty
    assert_array_almost_equal(lr_liblinear.coef_[0, -5:], np.zeros(5))
    assert_array_almost_equal(lr_saga.coef_[0, -5:], np.zeros(5))

def test_logreg_l1_sparse_data(csr_container):
    X_noise = rng.normal(scale=0.1, size=(n_samples, 3))
    X_constant = np.zeros(shape=(n_samples, 2))
    X[X < 1] = 0

        C=1.0,
    )

        C=1.0,
    )
    # penalty

    # Check that solving on the sparse and dense data yield the same results
    lr_saga_dense = LogisticRegression(
        C=1.0,
    )
    lr_saga_dense.fit(X.toarray(), y)
    assert_array_almost_equal(lr_saga.coef_, lr_saga_dense.coef_)

def test_logistic_regression_cv_refit(random_seed, penalty):
    # Test that when refit=True, logistic regression cv with the saga solver
    # converges to the same solution as logistic regression with a fixed
    # regularization parameter.
    # Internally the LogisticRegressionCV model uses a warm start to refit on
    # the full data model with the optimal C found by CV. As the penalized
    # logistic regression loss is convex, we should still recover exactly
    # the same solution as long as the stopping criterion is strict enough (and
    # that there are no exactly duplicated features when penalty='l1').
    X, y = make_classification(n_samples=100, n_features=20, random_state=random_seed)
        random_state=random_seed,
    )
    lr_cv = LogisticRegressionCV(Cs=[1.0], refit=True, **common_params)
    lr_cv.fit(X, y)
    lr = LogisticRegression(C=1.0, **common_params)
    assert_array_almost_equal(lr_cv.coef_, lr.coef_)

def test_logreg_predict_proba_multinomial():
        n_samples=10, n_features=20, random_state=0, n_classes=3, n_informative=10
    )

    # Predicted probabilities using the true-entropy loss should give a
    # smaller loss than those using the ovr method.
    clf_multi = LogisticRegression(solver="lbfgs")
    clf_multi.fit(X, y)
    clf_multi_loss = log_loss(y, clf_multi.predict_proba(X))
    clf_ovr = OneVsRestClassifier(LogisticRegression(solver="lbfgs"))
    clf_ovr.fit(X, y)
    clf_ovr_loss = log_loss(y, clf_ovr.predict_proba(X))
    assert clf_ovr_loss > clf_multi_loss

    # Predicted probabilities using the soft-max function should give a
    # smaller loss than those using the logistic function.
    clf_wrong_loss = log_loss(y, clf_multi._predict_proba_lr(X))
    assert clf_wrong_loss > clf_multi_loss

def test_max_iter(max_iter, multi_class, solver, message):
    # Test that the maximum number of iteration is reached
    X, y_bin = iris.data, iris.target.copy()
    y_bin[y_bin == 2] = 0

    if solver in ("liblinear",) and multi_class == "multinomial":
        pytest.skip("'multinomial' is not supported by liblinear")
    if solver == "newton-cholesky" and max_iter > 1:
        pytest.skip("solver newton-cholesky might converge very fast")

    lr = LogisticRegression(
        tol=1e-15,
    )
    with pytest.warns(ConvergenceWarning, match=message):
        lr.fit(X, y_bin)

    assert lr.n_iter_[0] == max_iter

def test_n_iter(solver):
    # Test that self.n_iter_ has the correct format.
        # lbfgs requires scaling to avoid convergence warnings
        X = scale(X)

    assert n_classes == 3

    # Also generate a binary classification sub-problem.
    y_bin = y.copy()

    n_Cs = 4
    n_cv_fold = 2

    # Binary classification case
    clf = LogisticRegression(tol=1e-2, C=1.0, solver=solver, random_state=42)
    clf.fit(X, y_bin)
    assert clf.n_iter_.shape == (1,)

    clf_cv = LogisticRegressionCV(
        tol=1e-2, solver=solver, Cs=n_Cs, cv=n_cv_fold, random_state=42
    )
    clf_cv.fit(X, y_bin)
    assert clf_cv.n_iter_.shape == (1, n_cv_fold, n_Cs)

    # OvR case
    clf.set_params(multi_class="ovr").fit(X, y)
    assert clf.n_iter_.shape == (n_classes,)

    clf_cv.set_params(multi_class="ovr").fit(X, y)
    assert clf_cv.n_iter_.shape == (n_classes, n_cv_fold, n_Cs)

    # multinomial case
    if solver in ("liblinear",):
        # This solver only supports one-vs-rest multiclass classification.
        return

    # When using the multinomial objective function, there is a single
    # optimization problem to solve for all classes at once:
    clf.set_params(multi_class="multinomial").fit(X, y)

    clf_cv.set_params(multi_class="multinomial").fit(X, y)

def test_warm_start(solver, warm_start, fit_intercept):
    # Warm starting does not work with liblinear solver.

        tol=1e-4,
        warm_start=warm_start,
    )
        coef_1 = clf.coef_

        clf.max_iter = 1
    cum_diff = np.sum(np.abs(coef_1 - clf.coef_))
    msg = (
        f"Warm starting issue with solver {solver}"
        f"with {fit_intercept=} and {warm_start=}"
    )
    if warm_start:
        assert 2.0 > cum_diff, msg
    else:
        assert cum_diff > 2.0, msg

def test_saga_vs_liblinear(csr_container):
    X = np.concatenate([X] * 3)
    y = np.concatenate([y] * 3)

    X_bin = X[y <= 1]
    y_bin = y[y <= 1] * 2 - 1

    X_sparse, y_sparse = make_classification(
        n_samples=50, n_features=20, random_state=0
    )
    X_sparse = csr_container(X_sparse)

    for X, y in ((X_bin, y_bin), (X_sparse, y_sparse)):
        for penalty in ["l1", "l2"]:
            # alpha=1e-3 is time consuming
            for alpha in np.logspace(-1, 1, 3):
                saga = LogisticRegression(
                    C=1.0 / (n_samples * alpha),
                    solver="saga",
                    max_iter=200,
                    fit_intercept=False,
                    penalty=penalty,
                    random_state=0,
                    tol=1e-6,
                )

                liblinear = LogisticRegression(
                    solver="liblinear",
                    tol=1e-6,
                )

                saga.fit(X, y)
                liblinear.fit(X, y)
                # Convergence for alpha=1e-3 is very slow
                assert_array_almost_equal(saga.coef_, liblinear.coef_, 3)

def test_dtype_match(solver, multi_class, fit_intercept, csr_container):
    # Test that np.float32 input data is not cast to np.float64 when possible
    # and that the output is approximately the same no matter the input format.

    if solver == "liblinear" and multi_class == "multinomial":
        pytest.skip(f"Solver={solver} does not support multinomial logistic.")

    out32_type = np.float64 if solver == "liblinear" else np.float32

    X_32 = np.array(X).astype(np.float32)
    y_32 = np.array(Y1).astype(np.float32)
    X_64 = np.array(X).astype(np.float64)
    y_64 = np.array(Y1).astype(np.float64)
    X_sparse_32 = csr_container(X, dtype=np.float32)
    X_sparse_64 = csr_container(X, dtype=np.float64)
    solver_tol = 5e-4

    lr_templ = LogisticRegression(
        tol=solver_tol,
    )

    # Check 32-bit type consistency
    lr_32 = clone(lr_templ)
    lr_32.fit(X_32, y_32)
    assert lr_32.coef_.dtype == out32_type

    # Check 32-bit type consistency with sparsity
    lr_32_sparse = clone(lr_templ)
    lr_32_sparse.fit(X_sparse_32, y_32)
    assert lr_32_sparse.coef_.dtype == out32_type

    # Check 64-bit type consistency
    lr_64 = clone(lr_templ)
    lr_64.fit(X_64, y_64)
    assert lr_64.coef_.dtype == np.float64

    # Check 64-bit type consistency with sparsity
    lr_64_sparse = clone(lr_templ)
    lr_64_sparse.fit(X_sparse_64, y_64)
    assert lr_64_sparse.coef_.dtype == np.float64

    # solver_tol bounds the norm of the loss gradient
    # dw ~= inv(H)*grad ==> |dw| ~= |inv(H)| * solver_tol, where H - hessian
    #
    # See https://github.com/scikit-learn/scikit-learn/pull/13645
    #
    # with  Z = np.hstack((np.ones((3,1)), np.array(X)))
    # In [8]: np.linalg.norm(np.diag([0,2,2]) + np.linalg.inv((Z.T @ Z)/4))
    # Out[8]: 1.7193336918135917

    # factor of 2 to get the ball diameter
    atol = 2 * 1.72 * solver_tol
    if os.name == "nt" and _IS_32BIT:
        # FIXME
        atol = 1e-2

    # Check accuracy consistency
    assert_allclose(lr_32.coef_, lr_64.coef_.astype(np.float32), atol=atol)

    if solver == "saga" and fit_intercept:
        # FIXME: SAGA on sparse data fits the intercept inaccurately with the
        # default tol and max_iter parameters.
        atol = 1e-1

    assert_allclose(lr_32.coef_, lr_32_sparse.coef_, atol=atol)
    assert_allclose(lr_64.coef_, lr_64_sparse.coef_, atol=atol)

def test_warm_start_converge_LR():
    # Test to see that the logistic regression converges on warm start,
    # with multi_class='multinomial'. Non-regressive test for #10836

    y = np.array([1] * 100 + [-1] * 100)
    lr_no_ws = LogisticRegression(solver="sag", warm_start=False, random_state=0)
    lr_ws = LogisticRegression(solver="sag", warm_start=True, random_state=0)

    lr_no_ws_loss = log_loss(y, lr_no_ws.fit(X, y).predict_proba(X))
        lr_ws.fit(X, y)
    lr_ws_loss = log_loss(y, lr_ws.predict_proba(X))
    assert_allclose(lr_no_ws_loss, lr_ws_loss, rtol=1e-5)

def test_elastic_net_coeffs():
    # make sure elasticnet penalty gives different coefficients from l1 and l2
    # with saga solver (l1_ratio different from 0 or 1)

    C = 2.0
    coeffs = list()
    for penalty, ratio in (("elasticnet", l1_ratio), ("l1", None), ("l2", None)):
            C=C,
            solver="saga",
            l1_ratio=ratio,
            tol=1e-3,
        )
        coeffs.append(lr.coef_)

    elastic_net_coeffs, l1_coeffs, l2_coeffs = coeffs
    # make sure coeffs differ by at least .1
    assert not np.allclose(elastic_net_coeffs, l1_coeffs, rtol=0, atol=0.1)
    assert not np.allclose(elastic_net_coeffs, l2_coeffs, rtol=0, atol=0.1)
    assert not np.allclose(l2_coeffs, l1_coeffs, rtol=0, atol=0.1)

def test_elastic_net_l1_l2_equivalence(C, penalty, l1_ratio):
    # Make sure elasticnet is equivalent to l1 when l1_ratio=1 and to l2 when
    # l1_ratio=0.

    lr_enet = LogisticRegression(
        penalty="elasticnet",
        C=C,
        tol=1e-2,
    )
    lr_expected = LogisticRegression(
        penalty=penalty, C=C, solver="saga", random_state=0, tol=1e-2
    )
    lr_enet.fit(X, y)
    lr_expected.fit(X, y)

    assert_array_almost_equal(lr_enet.coef_, lr_expected.coef_)

def test_elastic_net_vs_l1_l2(C):
    # Make sure that elasticnet with grid search on l1_ratio gives same or
    # better results than just l1 or just l2.

    X, y = make_classification(500, random_state=0)

    param_grid = {"l1_ratio": np.linspace(0, 1, 5)}

    enet_clf = LogisticRegression(
        penalty="elasticnet", C=C, solver="saga", random_state=0, tol=1e-2
    )
    gs = GridSearchCV(enet_clf, param_grid, refit=True)

    l1_clf = LogisticRegression(
        penalty="l1", C=C, solver="saga", random_state=0, tol=1e-2
    )
    l2_clf = LogisticRegression(
        penalty="l2", C=C, solver="saga", random_state=0, tol=1e-2
    )

    for clf in (gs, l1_clf, l2_clf):

    assert gs.score(X_test, y_test) >= l1_clf.score(X_test, y_test)
    assert gs.score(X_test, y_test) >= l2_clf.score(X_test, y_test)

def test_LogisticRegression_elastic_net_objective(C, l1_ratio):
    # Check that training with a penalty matching the objective leads
    # to a lower objective.
    # Here we train a logistic regression with l2 (a) and elasticnet (b)
    # penalties, and compute the elasticnet objective. That of a should be
    # greater than that of b (both objectives are convex).
    )
    X = scale(X)

        C=C,
    )
    lr_l2 = LogisticRegression(
        penalty="l2", solver="saga", random_state=0, C=C, fit_intercept=False
    )
    lr_l2.fit(X, y)

    def enet_objective(lr):
        coef = lr.coef_.ravel()
        obj = C * log_loss(y, lr.predict_proba(X))
        obj += l1_ratio * np.sum(np.abs(coef))
        obj += (1.0 - l1_ratio) * 0.5 * np.dot(coef, coef)
        return obj

    assert enet_objective(lr_enet) < enet_objective(lr_l2)

def test_LogisticRegressionCV_GridSearchCV_elastic_net(n_classes):
    # make sure LogisticRegressionCV gives same best params (l1 and C) as
    # GridSearchCV when penalty is elasticnet

        n_samples=100, n_classes=n_classes, n_informative=3, random_state=0
    )

    cv = StratifiedKFold(5)

    l1_ratios = np.linspace(0, 1, 3)
    Cs = np.logspace(-4, 4, 3)

    lrcv = LogisticRegressionCV(
        Cs=Cs,
        cv=cv,
        l1_ratios=l1_ratios,
        tol=1e-2,
    )
    lrcv.fit(X, y)

    param_grid = {"C": Cs, "l1_ratio": l1_ratios}
        tol=1e-2,
    )
    gs = GridSearchCV(lr, param_grid, cv=cv)
    gs.fit(X, y)

    assert gs.best_params_["l1_ratio"] == lrcv.l1_ratio_[0]
    assert gs.best_params_["C"] == lrcv.C_[0]

def test_LogisticRegressionCV_GridSearchCV_elastic_net_ovr():
    # GridSearchCV when penalty is elasticnet and multiclass is ovr. We can't
    # compare best_params like in the previous test because
    # LogisticRegressionCV with multi_class='ovr' will have one C and one
    # l1_param for each class, while LogisticRegression will share the
    # parameters over the *n_classes* classifiers.

        n_samples=100, n_classes=3, n_informative=3, random_state=0
    )

        Cs=Cs,
        cv=cv,
        tol=1e-2,
    )
    lrcv.fit(X_train, y_train)

        tol=1e-2,
    )
    gs.fit(X_train, y_train)

    # Check that predictions are 80% the same
    assert (lrcv.predict(X_train) == gs.predict(X_train)).mean() >= 0.8
    assert (lrcv.predict(X_test) == gs.predict(X_test)).mean() >= 0.8

def test_LogisticRegressionCV_no_refit(penalty, multi_class):
    # Test LogisticRegressionCV attribute shapes when refit is False

        n_informative=n_classes,
    )

    if penalty == "elasticnet":
        l1_ratios = np.linspace(0, 1, 2)
    else:
        l1_ratios = None

        Cs=Cs,
        tol=1e-2,
        refit=False,
    )
    assert lrcv.C_.shape == (n_classes,)
    assert lrcv.l1_ratio_.shape == (n_classes,)
    assert lrcv.coef_.shape == (n_classes, n_features)

def test_LogisticRegressionCV_elasticnet_attribute_shapes():
    # Make sure the shapes of scores_ and coefs_paths_ attributes are correct
    # when using elasticnet (added one dimension for l1_ratios)

    )

    l1_ratios = np.linspace(0, 1, 2)

    n_folds = 2
        Cs=Cs,
        cv=n_folds,
        tol=1e-2,
    )
    coefs_paths = np.asarray(list(lrcv.coefs_paths_.values()))
    assert coefs_paths.shape == (
        n_folds,
        Cs.size,
        l1_ratios.size,
        n_features + 1,
    )
    scores = np.asarray(list(lrcv.scores_.values()))
    assert scores.shape == (n_classes, n_folds, Cs.size, l1_ratios.size)

    assert lrcv.n_iter_.shape == (n_classes, n_folds, Cs.size, l1_ratios.size)

def test_l1_ratio_non_elasticnet():
    msg = (
        r"l1_ratio parameter is only used when penalty is"
        r" 'elasticnet'\. Got \(penalty=l1\)"
    )
        LogisticRegression(penalty="l1", solver="saga", l1_ratio=0.5).fit(X, Y1)

def test_elastic_net_versus_sgd(C, l1_ratio):
    # Compare elasticnet penalty in LogisticRegression() and SGD(loss='log')
    )

    sgd = SGDClassifier(
        tol=None,
        max_iter=2000,
        alpha=1.0 / C / n_samples,
    )
    log = LogisticRegression(
        tol=1e-5,
        C=C,
    )

    sgd.fit(X, y)
    log.fit(X, y)
    assert_array_almost_equal(sgd.coef_, log.coef_, decimal=1)

def test_logistic_regression_path_coefs_multinomial():
    # Make sure that the returned coefs by logistic_regression_path when
    # multi_class='multinomial' don't override each other (used to be a
    # bug).
    )
    Cs = [0.00001, 1, 10000]
    coefs, _, _ = _logistic_regression_path(
        X,
        y,
        Cs=Cs,
    )

        assert_array_almost_equal(coefs[0], coefs[1], decimal=1)
        assert_array_almost_equal(coefs[0], coefs[2], decimal=1)
        assert_array_almost_equal(coefs[1], coefs[2], decimal=1)

def test_logistic_regression_multi_class_auto(est, solver):
    # check multi_class='auto' => multi_class='ovr'
    # iff binary y or liblinear

    def fit(X, y, **kw):
        return clone(est).set_params(**kw).fit(X, y)

    scaled_data = scale(iris.data)
    X = scaled_data[::10]
    X2 = scaled_data[1::10]
    y_multi = iris.target[::10]
    y_bin = y_multi == 0
    est_auto_bin = fit(X, y_bin, multi_class="auto", solver=solver)
    est_ovr_bin = fit(X, y_bin, multi_class="ovr", solver=solver)
    assert_allclose(est_auto_bin.coef_, est_ovr_bin.coef_)
    assert_allclose(est_auto_bin.predict_proba(X2), est_ovr_bin.predict_proba(X2))

    est_auto_multi = fit(X, y_multi, multi_class="auto", solver=solver)
    if solver == "liblinear":
        est_ovr_multi = fit(X, y_multi, multi_class="ovr", solver=solver)
        assert_allclose(est_auto_multi.coef_, est_ovr_multi.coef_)
            est_auto_multi.predict_proba(X2), est_ovr_multi.predict_proba(X2)
        )
    else:
        est_multi_multi = fit(X, y_multi, multi_class="multinomial", solver=solver)
        assert_allclose(est_auto_multi.coef_, est_multi_multi.coef_)
            est_auto_multi.predict_proba(X2), est_multi_multi.predict_proba(X2)
        )

        # Make sure multi_class='ovr' is distinct from ='multinomial'
            est_auto_bin.coef_,
            fit(X, y_bin, multi_class="multinomial", solver=solver).coef_,
        )
            fit(X, y_multi, multi_class="multinomial", solver=solver).coef_,
        )

def test_penalty_none(solver):
    # - Make sure warning is raised if penalty=None and C is set to a
    #   non-default value.
    # - Make sure setting penalty=None is equivalent to setting C=np.inf with
    #   l2 penalty.
    X, y = make_classification(n_samples=1000, n_redundant=0, random_state=0)

    msg = "Setting penalty=None will ignore the C"
    lr = LogisticRegression(penalty=None, solver=solver, C=4)

    lr_none = LogisticRegression(penalty=None, solver=solver, random_state=0)
    lr_l2_C_inf = LogisticRegression(
        penalty="l2", C=np.inf, solver=solver, random_state=0
    )
    pred_none = lr_none.fit(X, y).predict(X)
    pred_l2_C_inf = lr_l2_C_inf.fit(X, y).predict(X)
    assert_array_equal(pred_none, pred_l2_C_inf)

def test_logisticregression_liblinear_sample_weight(params):
    # check that we support sample_weight with liblinear in all possible cases:
    # l1-primal, l2-primal, l2-dual
        [
            [1, 3],
            [1, 3],
            [1, 3],
            [1, 3],
            [2, 1],
            [2, 1],
            [2, 1],
            [2, 1],
            [3, 3],
            [3, 3],
            [3, 3],
            [3, 3],
            [4, 1],
            [4, 1],
            [4, 1],
            [4, 1],
        ],
    )
    )

    base_clf = LogisticRegression(solver="liblinear", random_state=42)
    base_clf.set_params(**params)
    clf_no_weight = clone(base_clf).fit(X, y)
    clf_with_weight = clone(base_clf).fit(X2, y2, sample_weight=sample_weight)

    for method in ("predict", "predict_proba", "decision_function"):
        X_clf_no_weight = getattr(clf_no_weight, method)(X)
        X_clf_with_weight = getattr(clf_with_weight, method)(X)
        assert_allclose(X_clf_no_weight, X_clf_with_weight)

def test_scores_attribute_layout_elasticnet():
    # Non regression test for issue #14955.
    # when penalty is elastic net the scores_ attribute has shape
    # (n_classes, n_Cs, n_l1_ratios)
    # We here make sure that the second dimension indeed corresponds to Cs and
    # the third dimension corresponds to l1_ratios.

    cv = StratifiedKFold(n_splits=5)

    l1_ratios = [0.1, 0.9]
    Cs = [0.1, 1, 10]

        Cs=Cs,
        cv=cv,
        max_iter=250,
        tol=1e-3,
    )

    avg_scores_lrcv = lrcv.scores_[1].mean(axis=0)  # average over folds

        for j, l1_ratio in enumerate(l1_ratios):
                penalty="elasticnet",
                solver="saga",
                C=C,
                max_iter=250,
                tol=1e-3,
            )

            avg_score_lr = cross_val_score(lr, X, y, cv=cv).mean()
            assert avg_scores_lrcv[i, j] == pytest.approx(avg_score_lr)

def test_multinomial_identifiability_on_iris(solver, fit_intercept):
    """Test that the multinomial classification is identifiable.

    A multinomial with c classes can be modeled with
    probability_k = exp(X@coef_k) / sum(exp(X@coef_l), l=1..c) for k=1..c.
    This is not identifiable, unless one chooses a further constraint.
    According to [1], the maximum of the L2 penalized likelihood automatically
    satisfies the symmetric constraint:
    sum(coef_k, k=1..c) = 0

    Further details can be found in [2].

    Reference
    ---------
    .. [1] :doi:`Zhu, Ji and Trevor J. Hastie. "Classification of gene microarrays by
           penalized logistic regression". Biostatistics 5 3 (2004): 427-43.
           <10.1093/biostatistics/kxg046>`

    .. [2] :arxiv:`Noah Simon and Jerome Friedman and Trevor Hastie. (2013)
           "A Blockwise Descent Algorithm for Group-penalized Multiresponse and
           Multinomial Regression". <1311.6529>`
    """
    # Test logistic regression with the iris dataset

        C=len(iris.data),
    )
    # Scaling X to ease convergence.
    X_scaled = scale(iris.data)
    clf.fit(X_scaled, target)

    # axis=0 is sum over classes
    assert_allclose(clf.coef_.sum(axis=0), 0, atol=1e-10)
        assert clf.intercept_.sum(axis=0) == pytest.approx(0, abs=1e-11)

def test_sample_weight_not_modified(multi_class, class_weight):
    n_features = len(X)
    W = np.ones(n_features)
    W[: n_features // 2] = 2

    expected = W.copy()

        random_state=0, class_weight=class_weight, max_iter=200, multi_class=multi_class
    )
    clf.fit(X, y, sample_weight=W)
    assert_allclose(expected, W)

def test_large_sparse_matrix(solver, global_random_seed, csr_container):
    # Solvers either accept large sparse matrices, or raise helpful error.
    # Non-regression test for pull-request #21093.

    # generate sparse matrix with int64 indices
    X = csr_container(sparse.rand(20, 10, random_state=global_random_seed))
    for attr in ["indices", "indptr"]:
        setattr(X, attr, getattr(X, attr).astype("int64"))
    y = rng.randint(2, size=X.shape[0])

    if solver in ["liblinear", "sag", "saga"]:
        msg = "Only sparse matrices with 32-bit integer indices"
            LogisticRegression(solver=solver).fit(X, y)
    else:
        LogisticRegression(solver=solver).fit(X, y)

def test_single_feature_newton_cg():
    # Test that Newton-CG works with a single feature and intercept.
    # Non-regression test for issue #23605.

    X = np.array([[0.5, 0.65, 1.1, 1.25, 0.8, 0.54, 0.95, 0.7]]).T
    y = np.array([1, 1, 0, 0, 1, 1, 0, 1])
    assert X.shape[1] == 1
    LogisticRegression(solver="newton-cg", fit_intercept=True).fit(X, y)

def test_liblinear_not_stuck():
    # Non-regression https://github.com/scikit-learn/scikit-learn/issues/18264
    y = iris.target.copy()
    X = X[y != 2]
    y = y[y != 2]
    X_prep = StandardScaler().fit_transform(X)

    C = l1_min_c(X, y, loss="log") * 10 ** (10 / 29)
        tol=1e-6,
        intercept_scaling=10000.0,
        C=C,
    )

    # test that the fit does not raise a ConvergenceWarning
        clf.fit(X_prep, y)

def test_lr_cv_scores_differ_when_sample_weight_is_requested():
    """Test that `sample_weight` is correctly passed to the scorer in
    `LogisticRegressionCV.fit` and `LogisticRegressionCV.score` by
    checking the difference in scores with the case when `sample_weight`
    is not requested.
    """
    rng = np.random.RandomState(10)
    X, y = make_classification(n_samples=10, random_state=rng)
    X_t, y_t = make_classification(n_samples=10, random_state=rng)
    sample_weight = np.ones(len(y))
    sample_weight[: len(y) // 2] = 2
    kwargs = {"sample_weight": sample_weight}

    scorer1 = get_scorer("accuracy")
    lr_cv1 = LogisticRegressionCV(scoring=scorer1)
    lr_cv1.fit(X, y, **kwargs)

    scorer2 = get_scorer("accuracy")
    scorer2.set_score_request(sample_weight=True)
    lr_cv2 = LogisticRegressionCV(scoring=scorer2)
    lr_cv2.fit(X, y, **kwargs)

    assert not np.allclose(lr_cv1.scores_[1], lr_cv2.scores_[1])

    score_1 = lr_cv1.score(X_t, y_t, **kwargs)
    score_2 = lr_cv2.score(X_t, y_t, **kwargs)

    assert not np.allclose(score_1, score_2)

def test_lr_cv_scores_without_enabling_metadata_routing():
    """Test that `sample_weight` is passed correctly to the scorer in
    `LogisticRegressionCV.fit` and `LogisticRegressionCV.score` even
    when `enable_metadata_routing=False`
    """

        scorer1 = get_scorer("accuracy")
        lr_cv1 = LogisticRegressionCV(scoring=scorer1)
        lr_cv1.fit(X, y, **kwargs)
        score_1 = lr_cv1.score(X_t, y_t, **kwargs)

    with config_context(enable_metadata_routing=True):
        scorer2 = get_scorer("accuracy")
        scorer2.set_score_request(sample_weight=True)
        lr_cv2 = LogisticRegressionCV(scoring=scorer2)
        lr_cv2.fit(X, y, **kwargs)
        score_2 = lr_cv2.score(X_t, y_t, **kwargs)

    assert_allclose(lr_cv1.scores_[1], lr_cv2.scores_[1])
    assert_allclose(score_1, score_2)

def test_zero_max_iter(solver):
    # Make sure we can inspect the state of LogisticRegression right after
    # initialization (before the first weight update).
    y = y == 2
        clf = LogisticRegression(solver=solver, max_iter=0).fit(X, y)
    if solver not in ["saga", "sag"]:
        # XXX: sag and saga have n_iter_ = [1]...
        assert clf.n_iter_ == 0

    if solver != "lbfgs":
        # XXX: lbfgs has already started to update the coefficients...
        assert_allclose(clf.coef_, np.zeros_like(clf.coef_))
            clf.decision_function(X),
            np.full(shape=X.shape[0], fill_value=clf.intercept_),
        )
            clf.predict_proba(X),
            np.full(shape=(X.shape[0], 2), fill_value=0.5),
        )
    assert clf.score(X, y) < 0.7

def test_passing_params_without_enabling_metadata_routing():
    """Test that the right error message is raised when metadata params
    are passed while not supported when `enable_metadata_routing=False`."""
    X, y = make_classification(n_samples=10, random_state=0)
    lr_cv = LogisticRegressionCV()
    msg = "is only supported if enable_metadata_routing=True"

        params = {"extra_param": 1.0}

            lr_cv.fit(X, y, **params)

            lr_cv.score(X, y, **params)

def test_multi_class_deprecated():
    """Check `multi_class` parameter deprecated."""
    X, y = make_classification(n_classes=3, n_samples=50, n_informative=6)
    lr = LogisticRegression(multi_class="ovr")
    msg = "'multi_class' was deprecated"

    lrCV = LogisticRegressionCV(multi_class="ovr")
        lrCV.fit(X, y)

    # Special warning for "binary multinomial"
    X, y = make_classification(n_classes=2, n_samples=50, n_informative=6)
    lr = LogisticRegression(multi_class="multinomial")
    msg = "'multi_class' was deprecated.*binary problems"

    lrCV = LogisticRegressionCV(multi_class="multinomial")

def test_newton_cholesky_fallback_to_lbfgs(global_random_seed):
    # Wide data matrix should lead to a rank-deficient Hessian matrix
    # hence make the Newton-Cholesky solver raise a warning and fallback to
    # lbfgs.
        n_samples=10, n_features=20, random_state=global_random_seed
    )
    C = 1e30  # very high C to nearly disable regularization

    # Check that LBFGS can converge without any warning on this problem.
    lr_lbfgs = LogisticRegression(solver="lbfgs", C=C)
        lr_lbfgs.fit(X, y)
        n_iter_lbfgs = lr_lbfgs.n_iter_[0]

    assert n_iter_lbfgs >= 1

    # Check that the Newton-Cholesky solver raises a warning and falls back to
    # LBFGS. This should converge with the same number of iterations as the
    # above call of lbfgs since the Newton-Cholesky triggers the fallback
    # before completing the first iteration, for the problem setting at hand.
    lr_nc = LogisticRegression(solver="newton-cholesky", C=C)
    with ignore_warnings(category=LinAlgWarning):
        lr_nc.fit(X, y)
        n_iter_nc = lr_nc.n_iter_[0]

    assert n_iter_nc == n_iter_lbfgs

    # Trying to fit the same model again with a small iteration budget should
    # therefore raise a ConvergenceWarning:
    lr_nc_limited = LogisticRegression(
        solver="newton-cholesky", C=C, max_iter=n_iter_lbfgs - 1
    )
        with pytest.warns(ConvergenceWarning, match="lbfgs failed to converge"):
            lr_nc_limited.fit(X, y)
            n_iter_nc_limited = lr_nc_limited.n_iter_[0]

    assert n_iter_nc_limited == lr_nc_limited.max_iter - 1

        return obj

class MyPassiveAggressive(ClassifierMixin):
        self,
        C=1.0,
        epsilon=0.01,
        n_iter=1,
    ):
        self.C = C

        self.w = np.zeros(n_features, dtype=np.float64)
        self.b = 0.0

        for t in range(self.n_iter):
                p = self.project(X[i])
                if self.loss in ("hinge", "squared_hinge"):
                    loss = max(1 - y[i] * p, 0)
                else:
                    loss = max(np.abs(p - y[i]) - self.epsilon, 0)

                sqnorm = np.dot(X[i], X[i])

                if self.loss in ("hinge", "epsilon_insensitive"):
                    step = min(self.C, loss / sqnorm)
                elif self.loss in ("squared_hinge", "squared_epsilon_insensitive"):
                    step = loss / (sqnorm + 1.0 / (2 * self.C))

                    step *= y[i]
                else:
                    step *= np.sign(y[i] - p)

                self.w += step * X[i]
                    self.b += step

    def project(self, X):
        return np.dot(X, self.w) + self.b

def test_classifier_accuracy(csr_container, fit_intercept, average):
    data = csr_container(X) if csr_container is not None else X
    clf = PassiveAggressiveClassifier(
        C=1.0,
        tol=None,
    )
    clf.fit(data, y)
    score = clf.score(data, y)
    assert score > 0.79
    if average:
        assert hasattr(clf, "_average_coef")
        assert hasattr(clf, "_average_intercept")
        assert hasattr(clf, "_standard_intercept")
        assert hasattr(clf, "_standard_coef")

def test_classifier_partial_fit(csr_container, average):
    clf = PassiveAggressiveClassifier(random_state=0, average=average, max_iter=5)
    for t in range(30):
        clf.partial_fit(data, y, classes)

def test_classifier_refit():
    # Classifier can be retrained on different labels and features.
    clf = PassiveAggressiveClassifier(max_iter=5).fit(X, y)

    clf.fit(X[:, :-1], iris.target_names[y])
    assert_array_equal(clf.classes_, iris.target_names)

def test_classifier_correctness(loss, csr_container):
    y_bin[y != 1] = -1

    clf1 = MyPassiveAggressive(loss=loss, n_iter=2)
    clf1.fit(X, y_bin)

    clf2 = PassiveAggressiveClassifier(loss=loss, max_iter=2, shuffle=False, tol=None)
    clf2.fit(data, y_bin)

    assert_array_almost_equal(clf1.w, clf2.coef_.ravel(), decimal=2)

def test_classifier_undefined_methods(response_method):
    clf = PassiveAggressiveClassifier(max_iter=100)
        getattr(clf, response_method)

    # Test class weights.
    X2 = np.array([[-1.0, -1.0], [-1.0, 0], [-0.8, -1.0], [1.0, 1.0], [1.0, 0.0]])
    y2 = [1, 1, 1, -1, -1]

        C=0.1, max_iter=100, class_weight=None, random_state=100
    )
    clf.fit(X2, y2)
    assert_array_equal(clf.predict([[0.2, -1.0]]), np.array([1]))

        C=0.1, max_iter=100, class_weight={1: 0.001}, random_state=100
    )

    # now the hyperplane should rotate clock-wise and
    # the prediction on this point should shift
    assert_array_equal(clf.predict([[0.2, -1.0]]), np.array([-1]))

def test_partial_fit_weight_class_balanced():
    # partial_fit with class_weight='balanced' not supported
    clf = PassiveAggressiveClassifier(class_weight="balanced", max_iter=100)
        clf.partial_fit(X, y, classes=np.unique(y))

def test_equal_class_weight():
    X2 = [[1, 0], [1, 0], [0, 1], [0, 1]]
    y2 = [0, 0, 1, 1]
    clf = PassiveAggressiveClassifier(C=0.1, tol=None, class_weight=None)

    # Already balanced, so "balanced" weights should have no effect
    clf_balanced = PassiveAggressiveClassifier(C=0.1, tol=None, class_weight="balanced")
    clf_balanced.fit(X2, y2)

    clf_weighted = PassiveAggressiveClassifier(
        C=0.1, tol=None, class_weight={0: 0.5, 1: 0.5}
    )
    clf_weighted.fit(X2, y2)

    # should be similar up to some epsilon due to learning rate schedule
    assert_almost_equal(clf.coef_, clf_weighted.coef_, decimal=2)
    assert_almost_equal(clf.coef_, clf_balanced.coef_, decimal=2)

def test_wrong_class_weight_label():
    # ValueError due to wrong class_weight label.

    clf = PassiveAggressiveClassifier(class_weight={0: 0.5}, max_iter=100)
        clf.fit(X2, y2)

def test_regressor_mse(csr_container, fit_intercept, average):

    reg = PassiveAggressiveRegressor(
        C=1.0,
        max_iter=5,
    )
    reg.fit(data, y_bin)
    pred = reg.predict(data)
    assert np.mean((pred - y_bin) ** 2) < 1.7
        assert hasattr(reg, "_average_coef")
        assert hasattr(reg, "_average_intercept")
        assert hasattr(reg, "_standard_intercept")
        assert hasattr(reg, "_standard_coef")

def test_regressor_partial_fit(csr_container, average):

    reg = PassiveAggressiveRegressor(random_state=0, average=average, max_iter=100)
    for t in range(50):
        reg.partial_fit(data, y_bin)

def test_regressor_correctness(loss, csr_container):

    reg1 = MyPassiveAggressive(loss=loss, n_iter=2)
    reg1.fit(X, y_bin)

    reg2 = PassiveAggressiveRegressor(tol=None, loss=loss, max_iter=2, shuffle=False)
    reg2.fit(data, y_bin)

    assert_array_almost_equal(reg1.w, reg2.coef_.ravel(), decimal=2)

def test_regressor_undefined_methods():
    reg = PassiveAggressiveRegressor(max_iter=100)
        reg.transform(X)

        self,
        C=1.0,
        n_iter=1,
    ):
        self.C = C

                else:

                else:

def test_set_order_dense(order, input_order):
    """Check that _set_order returns arrays with promised order."""
    X = np.array([[0], [0], [0]], order=input_order)
    y = np.array([0, 0, 0], order=input_order)
    X2, y2 = _set_order(X, y, order=order)
        assert X2.flags["C_CONTIGUOUS"]
        assert y2.flags["C_CONTIGUOUS"]
    elif order == "F":
        assert X2.flags["F_CONTIGUOUS"]
        assert y2.flags["F_CONTIGUOUS"]

    if order == input_order:
        assert X is X2
        assert y is y2

def test_set_order_sparse(order, input_order, coo_container):
    """Check that _set_order returns sparse matrices in promised format."""
    X = coo_container(np.array([[0], [0], [0]]))
    y = coo_container(np.array([0, 0, 0]))
    sparse_format = "csc" if input_order == "F" else "csr"
    X = X.asformat(sparse_format)
    y = X.asformat(sparse_format)

    format = "csc" if order == "F" else "csr"
    assert sparse.issparse(X2) and X2.format == format
    assert sparse.issparse(y2) and y2.format == format

def test_lasso_zero():
    # Check that the lasso can handle zero data without crashing
    X = [[0], [0], [0]]
    y = [0, 0, 0]
    # _cd_fast.pyx tests for gap < tol, but here we get 0.0 < 0.0
    # should probably be changed to gap <= tol ?
        clf = Lasso(alpha=0.1).fit(X, y)
    pred = clf.predict([[1], [2], [3]])
    assert_array_almost_equal(clf.coef_, [0])
    assert_array_almost_equal(pred, [0, 0, 0])
    assert_almost_equal(clf.dual_gap_, 0)

def test_enet_nonfinite_params():
    # Check ElasticNet throws ValueError when dealing with non-finite parameter
    # values

    clf = ElasticNet(alpha=0.1)
    msg = "Coordinate descent iterations resulted in non-finite parameter values"

def test_lasso_toy():
    # Test Lasso on a toy example for various values of alpha.
    # When validating this against glmnet notice that glmnet divides it
    # against nobs.

    X = [[-1], [0], [1]]
    Y = [-1, 0, 1]  # just a straight line
    T = [[2], [3], [4]]  # test sample

    clf = Lasso(alpha=1e-8)
    assert_array_almost_equal(clf.coef_, [1])
    assert_array_almost_equal(pred, [2, 3, 4])

    clf = Lasso(alpha=0.1)
    assert_array_almost_equal(clf.coef_, [0.85])
    assert_array_almost_equal(pred, [1.7, 2.55, 3.4])

    clf = Lasso(alpha=0.5)
    assert_array_almost_equal(clf.coef_, [0.25])
    assert_array_almost_equal(pred, [0.5, 0.75, 1.0])

    clf = Lasso(alpha=1)
    assert_array_almost_equal(clf.coef_, [0.0])

def test_enet_toy():
    # Test ElasticNet for various parameters of alpha and l1_ratio.
    # Actually, the parameters alpha = 0 should not be allowed. However,
    # we test it as a border case.
    # ElasticNet is tested with and without precomputed Gram matrix

    X = np.array([[-1.0], [0.0], [1.0]])
    T = [[2.0], [3.0], [4.0]]  # test sample

    # this should be the same as lasso
    clf = ElasticNet(alpha=1e-8, l1_ratio=1.0)

    clf = ElasticNet(alpha=0.5, l1_ratio=0.3, max_iter=100, precompute=False)
    assert_array_almost_equal(clf.coef_, [0.50819], decimal=3)
    assert_array_almost_equal(pred, [1.0163, 1.5245, 2.0327], decimal=3)

    clf.set_params(max_iter=100, precompute=True)
    clf.fit(X, Y)  # with Gram

    clf.set_params(max_iter=100, precompute=np.dot(X.T, X))

    clf = ElasticNet(alpha=0.5, l1_ratio=0.5)
    assert_array_almost_equal(clf.coef_, [0.45454], 3)
    assert_array_almost_equal(pred, [0.9090, 1.3636, 1.8181], 3)

def test_lasso_dual_gap():
    """
    Check that Lasso.dual_gap_ matches its objective formulation, with the
    datafit normalized by n_samples
    """
    X, y, _, _ = build_dataset(n_samples=10, n_features=30)
    alpha = 0.01 * np.max(np.abs(X.T @ y)) / n_samples
    clf = Lasso(alpha=alpha, fit_intercept=False).fit(X, y)
    w = clf.coef_
    R = y - X @ w
    primal = 0.5 * np.mean(R**2) + clf.alpha * np.sum(np.abs(w))
    # dual pt: R / n_samples, dual constraint: norm(X.T @ theta, inf) <= alpha
    R /= np.max(np.abs(X.T @ R) / (n_samples * alpha))
    dual = 0.5 * (np.mean(y**2) - np.mean((y - R) ** 2))
    assert_allclose(clf.dual_gap_, primal - dual)

def build_dataset(n_samples=50, n_features=200, n_informative_features=10, n_targets=1):
    """
    build an ill-posed linear regression problem with many noisy features and
    comparatively few samples
    """
    if n_targets > 1:
        w = random_state.randn(n_features, n_targets)
    else:
        w = random_state.randn(n_features)
    w[n_informative_features:] = 0.0
    y = np.dot(X, w)
    X_test = random_state.randn(n_samples, n_features)
    y_test = np.dot(X_test, w)
    return X, y, X_test, y_test

def test_lasso_cv():
    X, y, X_test, y_test = build_dataset()
    max_iter = 150
    clf = LassoCV(alphas=10, eps=1e-3, max_iter=max_iter, cv=3).fit(X, y)
    assert_almost_equal(clf.alpha_, 0.056, 2)

    clf = LassoCV(alphas=10, eps=1e-3, max_iter=max_iter, precompute=True, cv=3)

    # Check that the lars and the coordinate descent implementation
    # select a similar alpha
    lars = LassoLarsCV(max_iter=30, cv=3).fit(X, y)
    # for this we check that they don't fall in the grid of
    # clf.alphas further than 1
    assert (
        np.abs(
            np.searchsorted(clf.alphas_[::-1], lars.alpha_)
            - np.searchsorted(clf.alphas_[::-1], clf.alpha_)
        )
        <= 1
    )
    # check that they also give a similar MSE
    mse_lars = interpolate.interp1d(lars.cv_alphas_, lars.mse_path_.T)
    assert_allclose(mse_lars(clf.alphas_[5]).mean(), clf.mse_path_[5].mean(), rtol=1e-2)

    # test set
    assert clf.score(X_test, y_test) > 0.99

def test_lasso_cv_with_some_model_selection():
    from sklearn import datasets
    from sklearn.model_selection import ShuffleSplit

    X = diabetes.data
    y = diabetes.target

    pipe = make_pipeline(StandardScaler(), LassoCV(cv=ShuffleSplit(random_state=0)))

def test_lasso_cv_positive_constraint():

    # Ensure the unconstrained fit has a negative coefficient
    clf_unconstrained = LassoCV(alphas=3, eps=1e-1, max_iter=max_iter, cv=2, n_jobs=1)
    clf_unconstrained.fit(X, y)
    assert min(clf_unconstrained.coef_) < 0

    # On same data, constrained fit has non-negative coefficients
    clf_constrained = LassoCV(
        alphas=3, eps=1e-1, max_iter=max_iter, positive=True, cv=2, n_jobs=1
    )
    clf_constrained.fit(X, y)
    assert min(clf_constrained.coef_) >= 0

def test_lassocv_alphas_validation(alphas, err_type, err_msg):
    """Check the `alphas` validation in LassoCV."""

    n_samples, n_features = 5, 5
    y = rng.randint(0, 2, n_samples)
    lassocv = LassoCV(alphas=alphas)
        lassocv.fit(X, y)

def _scale_alpha_inplace(estimator, n_samples):
    """Rescale the parameter alpha from when the estimator is evoked with
    normalize set to True as if it were evoked in a Pipeline with normalize set
    to False and with a StandardScaler.
    """
    if ("alpha" not in estimator.get_params()) and (
        "alphas" not in estimator.get_params()
    ):
        return

    if isinstance(estimator, (RidgeCV, RidgeClassifierCV)):
        # alphas is not validated at this point and can be a list.
        # We convert it to a np.ndarray to make sure broadcasting
        # is used.
        alphas = np.asarray(estimator.alphas) * n_samples
        return estimator.set_params(alphas=alphas)
    if isinstance(estimator, (Lasso, LassoLars, MultiTaskLasso)):
        alpha = estimator.alpha * np.sqrt(n_samples)
    if isinstance(estimator, (Ridge, RidgeClassifier)):
        alpha = estimator.alpha * n_samples
    if isinstance(estimator, (ElasticNet, MultiTaskElasticNet)):
        if estimator.l1_ratio == 1:
            alpha = estimator.alpha * np.sqrt(n_samples)
        elif estimator.l1_ratio == 0:
            alpha = estimator.alpha * n_samples
        else:
            # To avoid silent errors in case of refactoring

    estimator.set_params(alpha=alpha)

def test_model_pipeline_same_dense_and_sparse(LinearModel, params, csr_container):
    # Test that linear model preceded by StandardScaler in the pipeline and
    # with normalize set to False gives the same y_pred and the same .coef_
    # given X sparse or dense

    model_dense = make_pipeline(StandardScaler(with_mean=False), LinearModel(**params))

    model_sparse = make_pipeline(StandardScaler(with_mean=False), LinearModel(**params))

    # prepare the data
    X[X < 0.1] = 0.0

    if is_classifier(model_dense):
        y = np.sign(y)

    model_dense.fit(X, y)
    model_sparse.fit(X_sparse, y)

    assert_allclose(model_sparse[1].coef_, model_dense[1].coef_)
    y_pred_dense = model_dense.predict(X)
    y_pred_sparse = model_sparse.predict(X_sparse)
    assert_allclose(y_pred_dense, y_pred_sparse)

    assert_allclose(model_dense[1].intercept_, model_sparse[1].intercept_)

def test_lasso_path_return_models_vs_new_return_gives_same_coefficients():
    # Test that lasso_path with lars_path style output gives the
    # same result

    # Some toy data
    X = np.array([[1, 2, 3.1], [2.3, 5.4, 4.3]]).T
    y = np.array([1, 2, 3.1])
    alphas = [5.0, 1.0, 0.5]

    # Use lars_path and lasso_path(new output) with 1D linear interpolation
    # to compute the same path
    alphas_lars, _, coef_path_lars = lars_path(X, y, method="lasso")
    coef_path_cont_lars = interpolate.interp1d(
        alphas_lars[::-1], coef_path_lars[:, ::-1]
    )
    alphas_lasso2, coef_path_lasso2, _ = lasso_path(X, y, alphas=alphas)
    coef_path_cont_lasso = interpolate.interp1d(
        alphas_lasso2[::-1], coef_path_lasso2[:, ::-1]
    )

        coef_path_cont_lasso(alphas), coef_path_cont_lars(alphas), decimal=1
    )

def test_enet_path():
    # We use a large number of samples and of informative features so that
    # the l1_ratio selected is more toward ridge than lasso
    X, y, X_test, y_test = build_dataset(
        n_samples=200, n_features=100, n_informative_features=100
    )

    # Here we have a small number of iterations, and thus the
    # ElasticNet might not converge. This is to speed up tests
    clf = ElasticNetCV(
        alphas=[0.01, 0.05, 0.1], eps=2e-3, l1_ratio=[0.5, 0.7], cv=3, max_iter=max_iter
    )
    ignore_warnings(clf.fit)(X, y)
    # Well-conditioned settings, we should have selected our
    # smallest penalty
    assert_almost_equal(clf.alpha_, min(clf.alphas_))
    # Non-sparse ground truth: we should have selected an elastic-net
    # that is closer to ridge than to lasso
    assert clf.l1_ratio_ == min(clf.l1_ratio)

        alphas=[0.01, 0.05, 0.1],
        eps=2e-3,
        l1_ratio=[0.5, 0.7],
        cv=3,
        precompute=True,
    )

    # We are in well-conditioned settings with low noise: we should
    # have a good test-set performance

    # Multi-output/target case
    X, y, X_test, y_test = build_dataset(n_features=10, n_targets=3)
    clf = MultiTaskElasticNetCV(
        alphas=5, eps=2e-3, l1_ratio=[0.5, 0.7], cv=3, max_iter=max_iter
    )
    assert clf.coef_.shape == (3, 10)

    # Mono-output should have same cross-validated alpha_ and l1_ratio_
    # in both cases.
    X, y, _, _ = build_dataset(n_features=10)
    clf1 = ElasticNetCV(alphas=5, eps=2e-3, l1_ratio=[0.5, 0.7])
    clf1.fit(X, y)
    clf2 = MultiTaskElasticNetCV(alphas=5, eps=2e-3, l1_ratio=[0.5, 0.7])
    clf2.fit(X, y[:, np.newaxis])
    assert_almost_equal(clf1.l1_ratio_, clf2.l1_ratio_)
    assert_almost_equal(clf1.alpha_, clf2.alpha_)

def test_path_parameters():
    X, y, _, _ = build_dataset()
    max_iter = 100

    clf = ElasticNetCV(alphas=50, eps=1e-3, max_iter=max_iter, l1_ratio=0.5, tol=1e-3)
    clf.fit(X, y)  # new params
    assert_almost_equal(0.5, clf.l1_ratio)
    assert 50 == clf._alphas
    assert 50 == len(clf.alphas_)

    clf = ElasticNet(alpha=0.1, max_iter=5, warm_start=True)
    ignore_warnings(clf.fit)(X, y)  # do a second round with 5 iterations

    clf2 = ElasticNet(alpha=0.1, max_iter=10)
    ignore_warnings(clf2.fit)(X, y)
    assert_array_almost_equal(clf2.coef_, clf.coef_)

def test_lasso_alpha_warning():

    clf = Lasso(alpha=0)
        "With alpha=0, this algorithm does not "
        "converge well. You are advised to use the "
        "LinearRegression estimator"
    )

def test_lasso_positive_constraint():
    y = [1, 0, -1]  # just a straight line with negative slope

    lasso = Lasso(alpha=0.1, positive=True)
    lasso.fit(X, y)
    assert min(lasso.coef_) >= 0

    lasso = Lasso(alpha=0.1, precompute=True, positive=True)

def test_enet_positive_constraint():

    enet = ElasticNet(alpha=0.1, positive=True)
    enet.fit(X, y)
    assert min(enet.coef_) >= 0

def test_enet_cv_positive_constraint():

    enetcv_unconstrained = ElasticNetCV(
        alphas=3, eps=1e-1, max_iter=max_iter, cv=2, n_jobs=1
    )
    enetcv_unconstrained.fit(X, y)
    assert min(enetcv_unconstrained.coef_) < 0

    enetcv_constrained = ElasticNetCV(
        alphas=3, eps=1e-1, max_iter=max_iter, cv=2, positive=True, n_jobs=1
    )
    enetcv_constrained.fit(X, y)
    assert min(enetcv_constrained.coef_) >= 0

def test_uniform_targets():
    enet = ElasticNetCV(alphas=3)
    m_enet = MultiTaskElasticNetCV(alphas=3)
    lasso = LassoCV(alphas=3)
    m_lasso = MultiTaskLassoCV(alphas=3)

    models_single_task = (enet, lasso)
    models_multi_task = (m_enet, m_lasso)

    X_train = rng.random_sample(size=(10, 3))
    X_test = rng.random_sample(size=(10, 3))

    y1 = np.empty(10)
    y2 = np.empty((10, 2))

    for model in models_single_task:
        for y_values in (0, 5):
            y1.fill(y_values)
            with ignore_warnings(category=ConvergenceWarning):
                assert_array_equal(model.fit(X_train, y1).predict(X_test), y1)
            assert_array_equal(model.alphas_, [np.finfo(float).resolution] * 3)

    for model in models_multi_task:
            y2[:, 0].fill(y_values)
            y2[:, 1].fill(2 * y_values)
                assert_array_equal(model.fit(X_train, y2).predict(X_test), y2)

def test_multi_task_lasso_and_enet():
    Y = np.c_[y, y]
    # Y_test = np.c_[y_test, y_test]
    clf = MultiTaskLasso(alpha=1, tol=1e-8).fit(X, Y)
    assert 0 < clf.dual_gap_ < 1e-5
    assert_array_almost_equal(clf.coef_[0], clf.coef_[1])

    clf = MultiTaskElasticNet(alpha=1, tol=1e-8).fit(X, Y)

    clf = MultiTaskElasticNet(alpha=1.0, tol=1e-8, max_iter=1)
        "Objective did not converge. You might want to "
        "increase the number of iterations."
    )

def test_lasso_readonly_data():
    X = np.array([[-1], [0], [1]])
    Y = np.array([-1, 0, 1])  # just a straight line
    T = np.array([[2], [3], [4]])  # test sample
    with TempMemmap((X, Y)) as (X, Y):
        clf = Lasso(alpha=0.5)
        pred = clf.predict(T)
        assert_array_almost_equal(clf.coef_, [0.25])
        assert_array_almost_equal(pred, [0.5, 0.75, 1.0])
        assert_almost_equal(clf.dual_gap_, 0)

def test_multi_task_lasso_readonly_data():
        Y = np.c_[y, y]
        clf = MultiTaskLasso(alpha=1, tol=1e-8).fit(X, Y)
        assert 0 < clf.dual_gap_ < 1e-5
        assert_array_almost_equal(clf.coef_[0], clf.coef_[1])

def test_enet_multitarget():
    n_targets = 3
    X, y, _, _ = build_dataset(
        n_samples=10, n_features=8, n_informative_features=10, n_targets=n_targets
    )
    estimator = ElasticNet(alpha=0.01)
    coef, intercept, dual_gap = (
        estimator.coef_,
        estimator.intercept_,
        estimator.dual_gap_,
    )

    for k in range(n_targets):
        estimator.fit(X, y[:, k])
        assert_array_almost_equal(coef[k, :], estimator.coef_)
        assert_array_almost_equal(intercept[k], estimator.intercept_)
        assert_array_almost_equal(dual_gap[k], estimator.dual_gap_)

def test_multioutput_enetcv_error():
    clf = ElasticNetCV()

def test_multitask_enet_and_lasso_cv():
    X, y, _, _ = build_dataset(n_features=50, n_targets=3)
    clf = MultiTaskElasticNetCV(cv=3).fit(X, y)
    assert_almost_equal(clf.alpha_, 0.00556, 3)
    clf = MultiTaskLassoCV(cv=3).fit(X, y)
    assert_almost_equal(clf.alpha_, 0.00278, 3)

    X, y, _, _ = build_dataset(n_targets=3)
        alphas=10, eps=1e-3, max_iter=200, l1_ratio=[0.3, 0.5], tol=1e-3, cv=3
    )
    assert 0.5 == clf.l1_ratio_
    assert (3, X.shape[1]) == clf.coef_.shape
    assert (3,) == clf.intercept_.shape
    assert (2, 10, 3) == clf.mse_path_.shape
    assert (2, 10) == clf.alphas_.shape

    clf = MultiTaskLassoCV(alphas=10, eps=1e-3, max_iter=500, tol=1e-3, cv=3)
    assert (10, 3) == clf.mse_path_.shape
    assert 10 == len(clf.alphas_)

def test_1d_multioutput_enet_and_multitask_enet_cv():
    y = y[:, np.newaxis]
    clf = ElasticNetCV(alphas=5, eps=2e-3, l1_ratio=[0.5, 0.7])
    clf.fit(X, y[:, 0])
    clf1 = MultiTaskElasticNetCV(alphas=5, eps=2e-3, l1_ratio=[0.5, 0.7])
    assert_almost_equal(clf.l1_ratio_, clf1.l1_ratio_)
    assert_almost_equal(clf.alpha_, clf1.alpha_)
    assert_almost_equal(clf.coef_, clf1.coef_[0])
    assert_almost_equal(clf.intercept_, clf1.intercept_[0])

def test_1d_multioutput_lasso_and_multitask_lasso_cv():
    clf = LassoCV(alphas=5, eps=2e-3)
    clf1 = MultiTaskLassoCV(alphas=5, eps=2e-3)

def test_sparse_input_dtype_enet_and_lassocv(csr_container):
    clf = ElasticNetCV(alphas=5)
    clf1 = ElasticNetCV(alphas=5)
    clf1.fit(csr_container(X, dtype=np.float32), y)
    assert_almost_equal(clf.alpha_, clf1.alpha_, decimal=6)
    assert_almost_equal(clf.coef_, clf1.coef_, decimal=6)

    clf = LassoCV(alphas=5)
    clf1 = LassoCV(alphas=5)

def test_elasticnet_precompute_incorrect_gram():
    # check that passing an invalid precomputed Gram matrix will raise an
    # error.

    X_centered = X - np.average(X, axis=0)
    garbage = rng.standard_normal(X.shape)
    precompute = np.dot(garbage.T, garbage)

    clf = ElasticNet(alpha=0.01, precompute=precompute)
    msg = "Gram matrix.*did not pass validation.*"
        clf.fit(X_centered, y)

def test_elasticnet_precompute_gram_weighted_samples():
    # check the equivalence between passing a precomputed Gram matrix and
    # internal computation using sample weights.

    sample_weight = rng.lognormal(size=y.shape)

    w_norm = sample_weight * (y.shape / np.sum(sample_weight))
    X_c = X - np.average(X, axis=0, weights=w_norm)
    X_r = X_c * np.sqrt(w_norm)[:, np.newaxis]
    gram = np.dot(X_r.T, X_r)

    clf1 = ElasticNet(alpha=0.01, precompute=gram)
    clf1.fit(X_c, y, sample_weight=sample_weight)

    clf2 = ElasticNet(alpha=0.01, precompute=False)

    assert_allclose(clf1.coef_, clf2.coef_)

def test_elasticnet_precompute_gram():
    # Check the dtype-aware check for a precomputed Gram matrix
    # (see https://github.com/scikit-learn/scikit-learn/pull/22059
    # and https://github.com/scikit-learn/scikit-learn/issues/21997).
    # Here: (X_c.T, X_c)[2, 3] is not equal to np.dot(X_c[:, 2], X_c[:, 3])
    # but within tolerance for np.float32

    rng = np.random.RandomState(58)
    X = rng.binomial(1, 0.25, (1000, 4)).astype(np.float32)
    y = rng.rand(1000).astype(np.float32)

    X_c = X - np.average(X, axis=0)
    gram = np.dot(X_c.T, X_c)

    clf1.fit(X_c, y)

    clf2.fit(X, y)

def test_warm_start_convergence():
    model = ElasticNet(alpha=1e-3, tol=1e-3).fit(X, y)
    n_iter_reference = model.n_iter_

    # This dataset is not trivial enough for the model to converge in one pass.
    assert n_iter_reference > 2

    # Check that n_iter_ is invariant to multiple calls to fit
    # when warm_start=False, all else being equal.
    n_iter_cold_start = model.n_iter_
    assert n_iter_cold_start == n_iter_reference

    # Fit the same model again, using a warm start: the optimizer just performs
    # a single pass before checking that it has already converged
    model.set_params(warm_start=True)
    n_iter_warm_start = model.n_iter_
    assert n_iter_warm_start == 1

def test_warm_start_convergence_with_regularizer_decrement():

    # Train a model to converge on a lightly regularized problem
    final_alpha = 1e-5
    low_reg_model = ElasticNet(alpha=final_alpha).fit(X, y)

    # Fitting a new model on a more regularized version of the same problem.
    # Fitting with high regularization is easier it should converge faster
    # in general.
    high_reg_model = ElasticNet(alpha=final_alpha * 10).fit(X, y)
    assert low_reg_model.n_iter_ > high_reg_model.n_iter_

    # Fit the solution to the original, less regularized version of the
    # problem but from the solution of the highly regularized variant of
    # the problem as a better starting point. This should also converge
    # faster than the original model that starts from zero.
    warm_low_reg_model = deepcopy(high_reg_model)
    warm_low_reg_model.set_params(warm_start=True, alpha=final_alpha)
    warm_low_reg_model.fit(X, y)
    assert low_reg_model.n_iter_ > warm_low_reg_model.n_iter_

def test_random_descent(csr_container):
    # Test that both random and cyclic selection give the same results.
    # Ensure that the test models fully converge and check a wide
    # range of conditions.

    # This uses the coordinate descent algo using the gram trick.
    X, y, _, _ = build_dataset(n_samples=50, n_features=20)
    clf_cyclic = ElasticNet(selection="cyclic", tol=1e-8)
    clf_cyclic.fit(X, y)
    clf_random = ElasticNet(selection="random", tol=1e-8, random_state=42)
    clf_random.fit(X, y)
    assert_array_almost_equal(clf_cyclic.coef_, clf_random.coef_)
    assert_almost_equal(clf_cyclic.intercept_, clf_random.intercept_)

    # This uses the descent algo without the gram trick
    clf_cyclic.fit(X.T, y[:20])
    clf_random.fit(X.T, y[:20])

    # Sparse Case
    clf_cyclic.fit(csr_container(X), y)
    clf_random.fit(csr_container(X), y)

    # Multioutput case.
    new_y = np.hstack((y[:, np.newaxis], y[:, np.newaxis]))
    clf_cyclic = MultiTaskElasticNet(selection="cyclic", tol=1e-8)
    clf_cyclic.fit(X, new_y)
    clf_random = MultiTaskElasticNet(selection="random", tol=1e-8, random_state=42)
    clf_random.fit(X, new_y)

def test_enet_path_positive():
    # Test positive parameter

    X, Y, _, _ = build_dataset(n_samples=50, n_features=50, n_targets=2)

    # For mono output
    # Test that the coefs returned by positive=True in enet_path are positive
    for path in [enet_path, lasso_path]:
        pos_path_coef = path(X, Y[:, 0], positive=True)[1]
        assert np.all(pos_path_coef >= 0)

    # For multi output, positive parameter is not allowed
    # Test that an error is raised
            path(X, Y, positive=True)

def test_sparse_dense_descent_paths(csr_container):
    # Test that dense and sparse input give the same input for descent paths.
        _, coefs, _ = path(X, y)
        _, sparse_coefs, _ = path(csr, y)
        assert_array_almost_equal(coefs, sparse_coefs)

def test_path_unknown_parameter(path_func):
    """Check that passing parameter not used by the coordinate descent solver
    will raise an error."""
    err_msg = "Unexpected parameters in params"
        path_func(X, y, normalize=True, fit_intercept=True)

def test_check_input_false():
    X, y, _, _ = build_dataset(n_samples=20, n_features=10)
    X = check_array(X, order="F", dtype="float64")
    y = check_array(X, order="F", dtype="float64")
    clf = ElasticNet(selection="cyclic", tol=1e-8)
    # Check that no error is raised if data is provided in the right format
    clf.fit(X, y, check_input=False)
    # With check_input=False, an exhaustive check is not made on y but its
    # dtype is still cast in _preprocess_data to X's dtype. So the test should
    # pass anyway
    X = check_array(X, order="F", dtype="float32")
        clf.fit(X, y, check_input=False)
    # With no input checking, providing X in C order should result in false
    # computation
    X = check_array(X, order="C", dtype="float64")

def test_enet_copy_X_True(check_input):
    X = X.copy(order="F")

    original_X = X.copy()
    enet = ElasticNet(copy_X=True)
    enet.fit(X, y, check_input=check_input)

    assert_array_equal(original_X, X)

def test_enet_copy_X_False_check_input_False():

    enet = ElasticNet(copy_X=False)
    enet.fit(X, y, check_input=False)

    # No copying, X is overwritten
    assert np.any(np.not_equal(original_X, X))

def test_overrided_gram_matrix():
    Gram = X.T.dot(X)
    clf = ElasticNet(selection="cyclic", tol=1e-8, precompute=Gram)
        "Gram matrix was provided but X was centered"
        " to fit intercept: recomputing Gram matrix."
    )

def test_lasso_non_float_y(model):
    X = [[0, 0], [1, 1], [-1, -1]]
    y = [0, 1, 2]
    y_float = [0.0, 1.0, 2.0]

    clf = model(fit_intercept=False)
    clf_float = model(fit_intercept=False)
    clf_float.fit(X, y_float)
    assert_array_equal(clf.coef_, clf_float.coef_)

def test_enet_float_precision():
    # Generate dataset
    X, y, X_test, y_test = build_dataset(n_samples=20, n_features=10)

    for fit_intercept in [True, False]:
        coef = {}
        intercept = {}
        for dtype in [np.float64, np.float32]:
            clf = ElasticNet(
                alpha=0.5,
                max_iter=100,
            )

            X = dtype(X)
            y = dtype(y)
            ignore_warnings(clf.fit)(X, y)

            coef[("simple", dtype)] = clf.coef_
            intercept[("simple", dtype)] = clf.intercept_

            assert clf.coef_.dtype == dtype

            # test precompute Gram array
            Gram = X.T.dot(X)
            clf_precompute = ElasticNet(
                precompute=Gram,
            )
            ignore_warnings(clf_precompute.fit)(X, y)
            assert_array_almost_equal(clf.coef_, clf_precompute.coef_)
            assert_array_almost_equal(clf.intercept_, clf_precompute.intercept_)

            # test multi task enet
            multi_y = np.hstack((y[:, np.newaxis], y[:, np.newaxis]))
            clf_multioutput = MultiTaskElasticNet(
            )
            clf_multioutput.fit(X, multi_y)
            coef[("multi", dtype)] = clf_multioutput.coef_
            intercept[("multi", dtype)] = clf_multioutput.intercept_

        for v in ["simple", "multi"]:
                coef[(v, np.float32)], coef[(v, np.float64)], decimal=4
            )
                intercept[(v, np.float32)], intercept[(v, np.float64)], decimal=4
            )

def test_enet_l1_ratio():
    # Test that an error message is raised if an estimator that
    # uses _alpha_grid is called with l1_ratio=0
    msg = (
        "Automatic alpha grid generation is not supported for l1_ratio=0. "
        "Please supply a grid by providing your estimator with the "
        "appropriate `alphas=` argument."
    )
    X = np.array([[1, 2, 4, 5, 8], [3, 5, 7, 7, 8]]).T
    y = np.array([12, 10, 11, 21, 5])

        ElasticNetCV(l1_ratio=0, random_state=42).fit(X, y)

        MultiTaskElasticNetCV(l1_ratio=0, random_state=42).fit(X, y[:, None])

    # Test that l1_ratio=0 with alpha>0 produces user warning
        "Coordinate descent without L1 regularization may "
        "lead to unexpected results and is discouraged. "
        "Set l1_ratio > 0 to add L1 regularization."
    )
    est = ElasticNetCV(l1_ratio=[0], alphas=[1])

    # Test that l1_ratio=0 is allowed if we supply a grid manually
    alphas = [0.1, 10]
    estkwds = {"alphas": alphas, "random_state": 42}
    est_desired = ElasticNetCV(l1_ratio=0.00001, **estkwds)
    est = ElasticNetCV(l1_ratio=0, **estkwds)
        est_desired.fit(X, y)
    assert_array_almost_equal(est.coef_, est_desired.coef_, decimal=5)

    est_desired = MultiTaskElasticNetCV(l1_ratio=0.00001, **estkwds)
    est = MultiTaskElasticNetCV(l1_ratio=0, **estkwds)
        est.fit(X, y[:, None])
        est_desired.fit(X, y[:, None])

def test_coef_shape_not_zero():
    est_no_intercept = Lasso(fit_intercept=False)
    est_no_intercept.fit(np.c_[np.ones(3)], np.ones(3))
    assert est_no_intercept.coef_.shape == (1,)

def test_warm_start_multitask_lasso():
    clf = MultiTaskLasso(alpha=0.1, max_iter=5, warm_start=True)
    ignore_warnings(clf.fit)(X, Y)
    ignore_warnings(clf.fit)(X, Y)  # do a second round with 5 iterations

    clf2 = MultiTaskLasso(alpha=0.1, max_iter=10)
    ignore_warnings(clf2.fit)(X, Y)

def test_enet_coordinate_descent(klass, n_classes, kwargs):
    """Test that a warning is issued if model does not converge"""
    clf = klass(max_iter=2, **kwargs)
    X = np.ones((n_samples, n_features)) * 1e50
    y = np.ones((n_samples, n_classes))
    if klass == Lasso:
        y = y.ravel()
        "Objective did not converge. You might want to"
        " increase the number of iterations."
    )

def test_convergence_warnings():
    X = random_state.standard_normal((1000, 500))
    y = random_state.standard_normal((1000, 3))

    # check that the model converges w/o convergence warnings
        MultiTaskElasticNet().fit(X, y)

def test_sparse_input_convergence_warning(csr_container):
    X, y, _, _ = build_dataset(n_samples=1000, n_features=500)

        ElasticNet(max_iter=1, tol=0).fit(csr_container(X, dtype=np.float32), y)

        Lasso().fit(csr_container(X, dtype=np.float32), y)

def test_lassoCV_does_not_set_precompute(monkeypatch, precompute, inner_precompute):
    calls = 0

    class LassoMock(Lasso):
            nonlocal calls
            calls += 1
            assert self.precompute == inner_precompute

    monkeypatch.setattr("sklearn.linear_model._coordinate_descent.Lasso", LassoMock)
    clf = LassoCV(precompute=precompute)
    assert calls > 0

def test_multi_task_lasso_cv_dtype():
    n_samples, n_features = 10, 3
    X = rng.binomial(1, 0.5, size=(n_samples, n_features))
    X = X.astype(int)  # make it explicit that X is int
    y = X[:, [0, 0]].copy()
    est = MultiTaskLassoCV(alphas=5, fit_intercept=True).fit(X, y)
    assert_array_almost_equal(est.coef_, [[1, 0, 0]] * 2, decimal=3)

def test_enet_sample_weight_consistency(
    fit_intercept, alpha, precompute, sparse_container, global_random_seed
):
    """Test that the impact of sample_weight is consistent.

    Note that this test is stricter than the common test
    check_sample_weight_equivalence alone and also tests sparse X.
    """

        tol=1e-6,
    )

    reg = ElasticNet(**params).fit(X, y)
    coef = reg.coef_.copy()
        intercept = reg.intercept_

    # 1) sample_weight=np.ones(..) should be equivalent to sample_weight=None
    reg.fit(X, y, sample_weight=sample_weight)
    assert_allclose(reg.coef_, coef, rtol=1e-6)
        assert_allclose(reg.intercept_, intercept)

    # 2) sample_weight=None should be equivalent to sample_weight = number
    sample_weight = 123.0

    # 3) scaling of sample_weight should have no effect, cf. np.average()
    sample_weight = rng.uniform(low=0.01, high=2, size=X.shape[0])
    reg = reg.fit(X, y, sample_weight=sample_weight)

    reg.fit(X, y, sample_weight=np.pi * sample_weight)

    # 4) setting elements of sample_weight to 0 is equivalent to removing these samples
    sample_weight_0 = sample_weight.copy()
    sample_weight_0[-5:] = 0
    y[-5:] *= 1000  # to make excluding those samples important
    reg.fit(X, y, sample_weight=sample_weight_0)
    coef_0 = reg.coef_.copy()
        intercept_0 = reg.intercept_
    reg.fit(X[:-5], y[:-5], sample_weight=sample_weight[:-5])
    assert_allclose(reg.coef_, coef_0, rtol=1e-6)
        assert_allclose(reg.intercept_, intercept_0)

    # 5) check that multiplying sample_weight by 2 is equivalent to repeating
    # corresponding samples twice
        X2 = sparse.vstack([X, X[: n_samples // 2]], format="csc")
    else:
        X2 = np.concatenate([X, X[: n_samples // 2]], axis=0)
    sample_weight_1 = sample_weight.copy()
    sample_weight_1[: n_samples // 2] *= 2
    sample_weight_2 = np.concatenate(
        [sample_weight, sample_weight[: n_samples // 2]], axis=0
    )

    reg1 = ElasticNet(**params).fit(X, y, sample_weight=sample_weight_1)
    reg2 = ElasticNet(**params).fit(X2, y2, sample_weight=sample_weight_2)
    assert_allclose(reg1.coef_, reg2.coef_, rtol=1e-6)

def test_enet_cv_sample_weight_correctness(
    fit_intercept, sparse_container, global_random_seed
):
    """Test that ElasticNetCV with sample weights gives correct results.

    We fit the same model twice, once with weighted training data, once with repeated
    data points in the training data and check that both models converge to the
    same solution.

    Since this model uses an internal cross-validation scheme to tune the alpha
    regularization parameter, we make sure that the repetitions only occur within
    a specific CV group. Data points belonging to other CV groups stay
    unit-weighted / "unrepeated".
    """
    n_splits, n_samples_per_cv, n_features = 3, 10, 5
    X_with_weights = rng.rand(n_splits * n_samples_per_cv, n_features)
    beta = rng.rand(n_features)
    beta[0:2] = 0
    y_with_weights = X_with_weights @ beta + rng.rand(n_splits * n_samples_per_cv)

        X_with_weights = sparse_container(X_with_weights)
    params = dict(tol=1e-6)

    # Assign random integer weights only to the first cross-validation group.
    # The samples in the other cross-validation groups are left with unit
    # weights.

    sw = np.ones_like(y_with_weights)
    sw[:n_samples_per_cv] = rng.randint(0, 5, size=n_samples_per_cv)
    groups_with_weights = np.concatenate(
        [
            np.full(n_samples_per_cv, 0),
            np.full(n_samples_per_cv, 1),
            np.full(n_samples_per_cv, 2),
        ]
    )
    splits_with_weights = list(
        LeaveOneGroupOut().split(X_with_weights, groups=groups_with_weights)
    )
    reg_with_weights = ElasticNetCV(
        cv=splits_with_weights, fit_intercept=fit_intercept, **params
    )

    reg_with_weights.fit(X_with_weights, y_with_weights, sample_weight=sw)

        X_with_weights = X_with_weights.toarray()
    X_with_repetitions = np.repeat(X_with_weights, sw.astype(int), axis=0)
        X_with_repetitions = sparse_container(X_with_repetitions)

    y_with_repetitions = np.repeat(y_with_weights, sw.astype(int), axis=0)
    groups_with_repetitions = np.repeat(groups_with_weights, sw.astype(int), axis=0)

    splits_with_repetitions = list(
        LeaveOneGroupOut().split(X_with_repetitions, groups=groups_with_repetitions)
    )
    reg_with_repetitions = ElasticNetCV(
        cv=splits_with_repetitions, fit_intercept=fit_intercept, **params
    )
    reg_with_repetitions.fit(X_with_repetitions, y_with_repetitions)

    # Check that the alpha selection process is the same:
    assert_allclose(reg_with_weights.mse_path_, reg_with_repetitions.mse_path_)
    assert_allclose(reg_with_weights.alphas_, reg_with_repetitions.alphas_)
    assert reg_with_weights.alpha_ == pytest.approx(reg_with_repetitions.alpha_)

    # Check that the final model coefficients are the same:
    assert_allclose(reg_with_weights.coef_, reg_with_repetitions.coef_, atol=1e-10)
    assert reg_with_weights.intercept_ == pytest.approx(reg_with_repetitions.intercept_)

def test_enet_cv_grid_search(sample_weight):
    """Test that ElasticNetCV gives same result as GridSearchCV."""
    n_samples, n_features = 200, 10
    cv = 5
        effective_rank=10,
        n_informative=n_features - 4,
        noise=10,
    )
    if sample_weight:
        sample_weight = np.linspace(1, 5, num=n_samples)
    else:

    alphas = np.logspace(np.log10(1e-5), np.log10(1), num=10)
    l1_ratios = [0.1, 0.5, 0.9]
    reg = ElasticNetCV(cv=cv, alphas=alphas, l1_ratio=l1_ratios)

    param = {"alpha": alphas, "l1_ratio": l1_ratios}
    gs = GridSearchCV(
        estimator=ElasticNet(),
        cv=cv,
        scoring="neg_mean_squared_error",
    ).fit(X, y, sample_weight=sample_weight)

    assert reg.l1_ratio_ == pytest.approx(gs.best_params_["l1_ratio"])
    assert reg.alpha_ == pytest.approx(gs.best_params_["alpha"])

def test_enet_cv_sample_weight_consistency(
    fit_intercept, l1_ratio, precompute, sparse_container
):
    """Test that the impact of sample_weight is consistent."""

    y = X.sum(axis=1) + rng.rand(n_samples)
        tol=1e-6,
        cv=3,
    )

        params.pop("l1_ratio", None)
        reg = LassoCV(**params).fit(X, y)
    else:
        reg = ElasticNetCV(**params).fit(X, y)

    # sample_weight=None should be equivalent to sample_weight = number

    # scaling of sample_weight should have no effect, cf. np.average()
    sample_weight = 2 * np.ones_like(y)

def test_enet_alpha_max_sample_weight(X_is_sparse, fit_intercept, sample_weight):
    X = np.array([[3.0, 1.0], [2.0, 5.0], [5.0, 3.0], [1.0, 4.0]])
    beta = np.array([1, 1])
    y = X @ beta
    if X_is_sparse:
        X = sparse.csc_matrix(X)
    # Test alpha_max makes coefs zero.
    reg = ElasticNetCV(alphas=1, cv=2, eps=1, fit_intercept=fit_intercept)
    assert_allclose(reg.coef_, 0, atol=1e-5)
    alpha_max = reg.alpha_
    # Test smaller alpha makes coefs nonzero.
    reg = ElasticNet(alpha=0.99 * alpha_max, fit_intercept=fit_intercept)
    assert_array_less(1e-3, np.max(np.abs(reg.coef_)))

def test_linear_models_cv_fit_with_loky(estimator):
    # LinearModelsCV.fit performs operations on fancy-indexed memmapped
    # data when using the loky backend, causing an error due to unexpected
    # behavior of fancy indexing of read-only memmaps (cf. numpy#14132).

    # Create a problem sufficiently large to cause memmapping (1MB).
    # Unfortunately the scikit-learn and joblib APIs do not make it possible to
    # change the max_nbyte of the inner Parallel call.
    X, y = make_regression(int(1e6) // 8 + 1, 1)
    assert X.nbytes > 1e6  # 1 MB
    with joblib.parallel_backend("loky"):
        estimator(n_jobs=2, cv=3).fit(X, y)

def test_enet_sample_weight_does_not_overwrite_sample_weight(check_input):
    """Check that ElasticNet does not overwrite sample_weights."""

    sample_weight_1_25 = 1.25 * np.ones_like(y)
    sample_weight = sample_weight_1_25.copy()

    reg = ElasticNet()
    reg.fit(X, y, sample_weight=sample_weight, check_input=check_input)

    assert_array_equal(sample_weight, sample_weight_1_25)

def test_enet_ridge_consistency(ridge_alpha):
    # Check that ElasticNet(l1_ratio=0) converges to the same solution as Ridge
    # provided that the value of alpha is adapted.
    #
    # XXX: this test does not pass for weaker regularization (lower values of
    # ridge_alpha): it could be either a problem of ElasticNet or Ridge (less
    # likely) and depends on the dataset statistics: lower values for
    # effective_rank are more problematic in particular.

        n_informative=50,
    )
    sw = rng.uniform(low=0.01, high=10, size=X.shape[0])
    alpha = 1.0
    )
    ridge = Ridge(alpha=alpha, **common_params).fit(X, y, sample_weight=sw)

    alpha_enet = alpha / sw.sum()
    enet = ElasticNet(alpha=alpha_enet, l1_ratio=0, **common_params).fit(
        X, y, sample_weight=sw
    )
    assert_allclose(ridge.coef_, enet.coef_)
    assert_allclose(ridge.intercept_, enet.intercept_)

def test_sample_weight_invariance(estimator):
        n_features=300,
    )
    sw = rng.uniform(low=0.01, high=2, size=X.shape[0])
    params = dict(tol=1e-12)

    # Check that setting some weights to 0 is equivalent to trimming the
    # samples:
    cutoff = X.shape[0] // 3
    sw_with_null = sw.copy()
    sw_with_null[:cutoff] = 0.0
    X_trimmed, y_trimmed = X[cutoff:, :], y[cutoff:]
    sw_trimmed = sw[cutoff:]

    reg_trimmed = (
        clone(estimator)
        .set_params(**params)
        .fit(X_trimmed, y_trimmed, sample_weight=sw_trimmed)
    )
    reg_null_weighted = (
        clone(estimator).set_params(**params).fit(X, y, sample_weight=sw_with_null)
    )
    assert_allclose(reg_null_weighted.coef_, reg_trimmed.coef_)
    assert_allclose(reg_null_weighted.intercept_, reg_trimmed.intercept_)

    # Check that duplicating the training dataset is equivalent to multiplying
    # the weights by 2:
    X_dup = np.concatenate([X, X], axis=0)
    y_dup = np.concatenate([y, y], axis=0)
    sw_dup = np.concatenate([sw, sw], axis=0)

    reg_2sw = clone(estimator).set_params(**params).fit(X, y, sample_weight=2 * sw)
    reg_dup = (
        clone(estimator).set_params(**params).fit(X_dup, y_dup, sample_weight=sw_dup)
    )

    assert_allclose(reg_2sw.coef_, reg_dup.coef_)
    assert_allclose(reg_2sw.intercept_, reg_dup.intercept_)

def test_read_only_buffer():
    """Test that sparse coordinate descent works for read-only buffers"""

    clf = ElasticNet(alpha=0.1, copy_X=True, random_state=rng)
    X = np.asfortranarray(rng.uniform(size=(100, 10)))
    X.setflags(write=False)

    y = rng.rand(100)

def test_cv_estimators_reject_params_with_no_routing_enabled(EstimatorCV):
    """Check that the models inheriting from class:`LinearModelCV` raise an
    error when any `params` are passed when routing is not enabled.
    """
    X, y = make_regression(random_state=42)
    groups = np.array([0, 1] * (len(y) // 2))
    estimator = EstimatorCV()
        estimator.fit(X, y, groups=groups)

def test_multitask_cv_estimators_with_sample_weight(MultiTaskEstimatorCV):
    """Check that for :class:`MultiTaskElasticNetCV` and
    class:`MultiTaskLassoCV` if `sample_weight` is passed and the
    CV splitter does not support `sample_weight` an error is raised.
    On the other hand if the splitter does support `sample_weight`
    while `sample_weight` is passed there is no error and process
    completes smoothly as before.
    """

    class CVSplitter(GroupsConsumerMixin, BaseCrossValidator):
        def get_n_splits(self, X=None, y=None, groups=None, metadata=None):
            pass  # pragma: nocover

    class CVSplitterSampleWeight(CVSplitter):
        def split(self, X, y=None, groups=None, sample_weight=None):
            split_index = len(X) // 2
            train_indices = list(range(0, split_index))
            test_indices = list(range(split_index, len(X)))
            yield test_indices, train_indices
            yield train_indices, test_indices

    X, y = make_regression(random_state=42, n_targets=2)

    # If CV splitter does not support sample_weight an error is raised
    splitter = CVSplitter().set_split_request(groups=True)
    estimator = MultiTaskEstimatorCV(cv=splitter)
    msg = "do not support sample weights"

    # If CV splitter does support sample_weight no error is raised
    splitter = CVSplitterSampleWeight().set_split_request(
        groups=True, sample_weight=True
    )
    estimator.fit(X, y, sample_weight=sample_weight)

def test_linear_model_cv_deprecated_n_alphas(Estimator):
    """Check the deprecation of n_alphas in favor of alphas."""
    X, y = make_regression(n_targets=2, random_state=42)

    # Asses warning message raised by LinearModelCV when n_alphas is used
        FutureWarning,
        match="'n_alphas' was deprecated in 1.7 and will be removed in 1.9",
    ):
        clf = Estimator(n_alphas=5)
        if clf._is_multitask():
            clf = clf.fit(X, y)
        else:
            clf = clf.fit(X, y[:, 0])

    # Asses no warning message raised when n_alphas is not used
        clf = Estimator(alphas=5)
        else:

def test_linear_model_cv_deprecated_alphas_none(Estimator):
    """Check the deprecation of alphas=None."""

        FutureWarning, match="'alphas=None' is deprecated and will be removed in 1.9"
    ):
        clf = Estimator(alphas=None)
        else:
            clf.fit(X, y[:, 0])

def test_linear_model_cv_alphas_n_alphas_unset(Estimator):
    """Check that no warning is raised when both n_alphas and alphas are unset."""

        clf = Estimator()
        else:

def test_linear_model_cv_alphas(Estimator):
    """Check that the behavior of alphas is consistent with n_alphas."""

    # n_alphas is set, alphas is not => n_alphas is used
    clf = Estimator(n_alphas=5)
    if clf._is_multitask():
    else:
        clf.fit(X, y[:, 0])
    assert len(clf.alphas_) == 5

    # n_alphas is set, alphas is set => alphas has priority
    clf = Estimator(n_alphas=5, alphas=10)
    else:
    assert len(clf.alphas_) == 10

    # same with alphas array-like
    clf = Estimator(n_alphas=5, alphas=np.arange(10))
    else:

    # n_alphas is not set, alphas is set => alphas is used
    clf = Estimator(alphas=10)
    else:

    clf = Estimator(alphas=np.arange(10))
    else:

    # both are not set => default = 100
    clf = Estimator()
    else:
    assert len(clf.alphas_) == 100

            calls += 1

            calls += 1

def _update_kwargs(kwargs):
    if "random_state" not in kwargs:
        kwargs["random_state"] = 42

    if "tol" not in kwargs:
        kwargs["tol"] = None
    if "max_iter" not in kwargs:
        kwargs["max_iter"] = 5

class _SparseSGDClassifier(linear_model.SGDClassifier):
    def fit(self, X, y, *args, **kw):
        X = sp.csr_matrix(X)
        return super().fit(X, y, *args, **kw)

    def partial_fit(self, X, y, *args, **kw):
        return super().partial_fit(X, y, *args, **kw)

class _SparseSGDRegressor(linear_model.SGDRegressor):
        return linear_model.SGDRegressor.fit(self, X, y, *args, **kw)

        return linear_model.SGDRegressor.partial_fit(self, X, y, *args, **kw)

    def decision_function(self, X, *args, **kw):
        # XXX untested as of v0.22
        return linear_model.SGDRegressor.decision_function(self, X, *args, **kw)

class _SparseSGDOneClassSVM(linear_model.SGDOneClassSVM):
    def fit(self, X, *args, **kw):
        return linear_model.SGDOneClassSVM.fit(self, X, *args, **kw)

    def partial_fit(self, X, *args, **kw):
        return linear_model.SGDOneClassSVM.partial_fit(self, X, *args, **kw)

        return linear_model.SGDOneClassSVM.decision_function(self, X, *args, **kw)

def SGDClassifier(**kwargs):
    _update_kwargs(kwargs)
    return linear_model.SGDClassifier(**kwargs)

def SGDRegressor(**kwargs):
    return linear_model.SGDRegressor(**kwargs)

def SGDOneClassSVM(**kwargs):
    return linear_model.SGDOneClassSVM(**kwargs)

def SparseSGDClassifier(**kwargs):
    return _SparseSGDClassifier(**kwargs)

def SparseSGDRegressor(**kwargs):
    return _SparseSGDRegressor(**kwargs)

def SparseSGDOneClassSVM(**kwargs):
    return _SparseSGDOneClassSVM(**kwargs)

def asgd(klass, X, y, eta, alpha, weight_init=None, intercept_init=0.0):
    if weight_init is None:
        weights = np.zeros(X.shape[1])
    else:
        weights = weight_init

    average_weights = np.zeros(X.shape[1])
    intercept = intercept_init
    average_intercept = 0.0
    decay = 1.0

    # sparse data has a fixed decay of .01
    if klass in (SparseSGDClassifier, SparseSGDRegressor):
        decay = 0.01

    for i, entry in enumerate(X):
        p = np.dot(entry, weights)
        p += intercept
        gradient = p - y[i]
        weights *= 1.0 - (eta * alpha)
        weights += -(eta * gradient * entry)
        intercept += -(eta * gradient) * decay

        average_weights *= i
        average_weights += weights
        average_weights /= i + 1.0

        average_intercept *= i
        average_intercept += intercept
        average_intercept /= i + 1.0

    return average_weights, average_intercept

def _test_warm_start(klass, X, Y, lr):
    # Test that explicit warm restart...
    clf = klass(alpha=0.01, eta0=0.01, shuffle=False, learning_rate=lr)

    clf2 = klass(alpha=0.001, eta0=0.01, shuffle=False, learning_rate=lr)
    clf2.fit(X, Y, coef_init=clf.coef_.copy(), intercept_init=clf.intercept_.copy())

    # ... and implicit warm restart are equivalent.
    clf3 = klass(
        alpha=0.01, eta0=0.01, shuffle=False, warm_start=True, learning_rate=lr
    )
    clf3.fit(X, Y)

    assert clf3.t_ == clf.t_
    assert_array_almost_equal(clf3.coef_, clf.coef_)

    clf3.set_params(alpha=0.001)

    assert clf3.t_ == clf2.t_
    assert_array_almost_equal(clf3.coef_, clf2.coef_)

def test_warm_start(klass, lr):
    _test_warm_start(klass, X, Y, lr)

def test_input_format(klass):
    # Input format tests.
    clf = klass(alpha=0.01, shuffle=False)
    Y_ = np.array(Y)[:, np.newaxis]

    Y_ = np.c_[Y_, Y_]
        clf.fit(X, Y_)

def test_clone(klass):
    # Test whether clone works ok.
    clf = klass(alpha=0.01, penalty="l1")
    clf.set_params(penalty="l2")

    clf2 = klass(alpha=0.01, penalty="l2")
    clf2.fit(X, Y)

    assert_array_equal(clf.coef_, clf2.coef_)

def test_plain_has_no_average_attr(klass):
    clf = klass(average=True, eta0=0.01)

    assert hasattr(clf, "_average_coef")
    assert hasattr(clf, "_average_intercept")
    assert hasattr(clf, "_standard_intercept")
    assert hasattr(clf, "_standard_coef")

    clf = klass()

    assert not hasattr(clf, "_average_coef")
    assert not hasattr(clf, "_average_intercept")
    assert not hasattr(clf, "_standard_intercept")
    assert not hasattr(clf, "_standard_coef")

def test_late_onset_averaging_not_reached(klass):
    clf1 = klass(average=600)
    clf2 = klass()
        if is_classifier(clf1):
            clf1.partial_fit(X, Y, classes=np.unique(Y))
            clf2.partial_fit(X, Y, classes=np.unique(Y))
        else:
            clf1.partial_fit(X, Y)
            clf2.partial_fit(X, Y)

    assert_array_almost_equal(clf1.coef_, clf2.coef_, decimal=16)
    if klass in [SGDClassifier, SparseSGDClassifier, SGDRegressor, SparseSGDRegressor]:
        assert_almost_equal(clf1.intercept_, clf2.intercept_, decimal=16)
    elif klass in [SGDOneClassSVM, SparseSGDOneClassSVM]:
        assert_allclose(clf1.offset_, clf2.offset_)

def test_late_onset_averaging_reached(klass):
    eta0 = 0.001
    alpha = 0.0001
    Y_encode = np.array(Y)
    Y_encode[Y_encode == 1] = -1.0
    Y_encode[Y_encode == 2] = 1.0

    clf1 = klass(
        average=7,
        eta0=eta0,
    )
    clf2 = klass(
    )

    clf1.fit(X, Y_encode)
    clf2.fit(X, Y_encode)

    average_weights, average_intercept = asgd(
        klass,
        X,
        Y_encode,
        eta0,
        alpha,
        weight_init=clf2.coef_.ravel(),
        intercept_init=clf2.intercept_,
    )

    assert_array_almost_equal(clf1.coef_.ravel(), average_weights.ravel(), decimal=16)
    assert_almost_equal(clf1.intercept_, average_intercept, decimal=16)

def test_early_stopping(klass):
    X = iris.data[iris.target > 0]
    Y = iris.target[iris.target > 0]
    for early_stopping in [True, False]:
        clf = klass(early_stopping=early_stopping, tol=1e-3, max_iter=max_iter).fit(
            X, Y
        )
        assert clf.n_iter_ < max_iter

def test_adaptive_longer_than_constant(klass):
    clf1 = klass(learning_rate="adaptive", eta0=0.01, tol=1e-3, max_iter=100)
    clf2 = klass(learning_rate="constant", eta0=0.01, tol=1e-3, max_iter=100)
    assert clf1.n_iter_ > clf2.n_iter_

def test_validation_set_not_used_for_training(klass):
    validation_fraction = 0.4
    seed = 42
    shuffle = False
        tol=None,
    )
    clf1.fit(X, Y)
    assert clf1.n_iter_ == max_iter

        tol=None,
    )

    if is_classifier(clf2):
        cv = StratifiedShuffleSplit(test_size=validation_fraction, random_state=seed)
    else:
        cv = ShuffleSplit(test_size=validation_fraction, random_state=seed)
    idx_train, idx_val = next(cv.split(X, Y))
    idx_train = np.sort(idx_train)  # remove shuffling
    clf2.fit(X[idx_train], Y[idx_train])
    assert clf2.n_iter_ == max_iter

    assert_array_equal(clf1.coef_, clf2.coef_)

def test_n_iter_no_change(klass):
    # test that n_iter_ increases monotonically with n_iter_no_change
        n_iter_list = [
            klass(
                early_stopping=early_stopping,
                n_iter_no_change=n_iter_no_change,
                tol=1e-4,
            )
            .fit(X, Y)
            .n_iter_
            for n_iter_no_change in [2, 3, 10]
        ]
        assert_array_equal(n_iter_list, sorted(n_iter_list))

def test_not_enough_sample_for_early_stopping(klass):
    # test an error is raised if the training or validation set is empty
    clf = klass(early_stopping=True, validation_fraction=0.99)
        clf.fit(X3, Y3)

def test_sgd_l1_ratio_not_used(Estimator, l1_ratio):
    """Check that l1_ratio is not used when penalty is not 'elasticnet'"""
    clf1 = Estimator(penalty="l1", l1_ratio=None, random_state=0).fit(X, Y)
    clf2 = Estimator(penalty="l1", l1_ratio=l1_ratio, random_state=0).fit(X, Y)

def test_sgd_failing_penalty_validation(Estimator):
    clf = Estimator(penalty="elasticnet", l1_ratio=None)
        ValueError, match="l1_ratio must be set when penalty is 'elasticnet'"
    ):

def test_sgd_clf(klass):
    # Check that SGD gives any results :-)

    for loss in ("hinge", "squared_hinge", "log_loss", "modified_huber"):
        clf = klass(
            alpha=0.01,
            fit_intercept=True,
            max_iter=10,
        )
        # assert_almost_equal(clf.coef_[0], clf.coef_[1], decimal=7)

def test_provide_coef(klass):
    """Check that the shape of `coef_init` is validated."""
    with pytest.raises(ValueError, match="Provided coef_init does not match dataset"):
        klass().fit(X, Y, coef_init=np.zeros((3,)))

def test_set_intercept_offset(klass, fit_params):
    """Check that `intercept_init` or `offset_init` is validated."""
    sgd_estimator = klass()
    with pytest.raises(ValueError, match="does not match dataset"):
        sgd_estimator.fit(X, Y, **fit_params)

def test_sgd_early_stopping_with_partial_fit(klass):
    """Check that we raise an error for `early_stopping` used with
    `partial_fit`.
    """
    err_msg = "early_stopping should be False with partial_fit"
        klass(early_stopping=True).partial_fit(X, Y)

def test_set_intercept_offset_binary(klass, fit_params):
    """Check that we can pass a scaler with binary classification to
    `intercept_init` or `offset_init`."""
    klass().fit(X5, Y5, **fit_params)

def test_average_binary_computed_correctly(klass):
    # Checks the SGDClassifier correctly computes the average weights
    eta = 0.1
    alpha = 2.0
    w = rng.normal(size=n_features)

    clf = klass(
        eta0=eta,
        average=True,
    )

    # simple linear function without noise
    y = np.sign(y)

    average_weights, average_intercept = asgd(klass, X, y, eta, alpha)
    average_weights = average_weights.reshape(1, -1)
    assert_array_almost_equal(clf.coef_, average_weights, decimal=14)
    assert_almost_equal(clf.intercept_, average_intercept, decimal=14)

def test_set_intercept_to_intercept(klass):
    # Checks intercept_ shape consistency for the warm starts
    # Inconsistent intercept_ shape.
    clf = klass().fit(X5, Y5)
    klass().fit(X5, Y5, intercept_init=clf.intercept_)
    clf = klass().fit(X, Y)
    klass().fit(X, Y, intercept_init=clf.intercept_)

def test_sgd_at_least_two_labels(klass):
    # Target must have at least two labels
    clf = klass(alpha=0.01, max_iter=20)
        clf.fit(X2, np.ones(9))

def test_partial_fit_weight_class_balanced(klass):
    # partial_fit with class_weight='balanced' not supported"""
    regex = (
        r"class_weight 'balanced' is not supported for "
        r"partial_fit\. In order to use 'balanced' weights, "
        r"use compute_class_weight\('balanced', classes=classes, y=y\). "
        r"In place of y you can use a large enough sample "
        r"of the full training set target to properly "
        r"estimate the class frequency distributions\. "
        r"Pass the resulting weights as the class_weight "
        r"parameter\."
    )
        klass(class_weight="balanced").partial_fit(X, Y, classes=np.unique(Y))

def test_sgd_multiclass(klass):
    # Multi-class test case
    clf = klass(alpha=0.01, max_iter=20).fit(X2, Y2)
    assert clf.coef_.shape == (3, 2)
    assert clf.intercept_.shape == (3,)
    assert clf.decision_function([[0, 0]]).shape == (1, 3)
    pred = clf.predict(T2)
    assert_array_equal(pred, true_result2)

def test_sgd_multiclass_average(klass):
    eta = 0.001
    alpha = 0.01
    # Multi-class average test case
        eta0=eta,
    )

    np_Y2 = np.array(Y2)
    clf.fit(X2, np_Y2)
    classes = np.unique(np_Y2)

    for i, cl in enumerate(classes):
        y_i = np.ones(np_Y2.shape[0])
        y_i[np_Y2 != cl] = -1
        average_coef, average_intercept = asgd(klass, X2, y_i, eta, alpha)
        assert_array_almost_equal(average_coef, clf.coef_[i], decimal=16)
        assert_almost_equal(average_intercept, clf.intercept_[i], decimal=16)

def test_sgd_multiclass_with_init_coef(klass):
    clf.fit(X2, Y2, coef_init=np.zeros((3, 2)), intercept_init=np.zeros(3))
    assert clf.intercept_.shape, (3,)

def test_sgd_multiclass_njobs(klass):
    # Multi-class test case with multi-core support
    clf = klass(alpha=0.01, max_iter=20, n_jobs=2).fit(X2, Y2)

def test_set_coef_multiclass(klass):
    # Checks coef_init and intercept_init shape for multi-class
    # problems
    # Provided coef_ does not match dataset
        clf.fit(X2, Y2, coef_init=np.zeros((2, 2)))

    # Provided coef_ does match dataset
    clf = klass().fit(X2, Y2, coef_init=np.zeros((3, 2)))

    # Provided intercept_ does not match dataset
        clf.fit(X2, Y2, intercept_init=np.zeros((1,)))

    # Provided intercept_ does match dataset.
    clf = klass().fit(X2, Y2, intercept_init=np.zeros((3,)))

def test_sgd_predict_proba_method_access(klass):
    # Checks that SGDClassifier predict_proba and predict_log_proba methods
    # can either be accessed or raise an appropriate error message
    # otherwise. See
    # https://github.com/scikit-learn/scikit-learn/issues/10938 for more
    # details.
    for loss in linear_model.SGDClassifier.loss_functions:
        clf = SGDClassifier(loss=loss)
        if loss in ("log_loss", "modified_huber"):
            assert hasattr(clf, "predict_proba")
            assert hasattr(clf, "predict_log_proba")
        else:
            inner_msg = "probability estimates are not available for loss={!r}".format(
                loss
            )
            assert not hasattr(clf, "predict_proba")
            assert not hasattr(clf, "predict_log_proba")
            with pytest.raises(
                AttributeError, match="has no attribute 'predict_proba'"
            ) as exec_info:
                clf.predict_proba

            assert isinstance(exec_info.value.__cause__, AttributeError)
            assert inner_msg in str(exec_info.value.__cause__)

                AttributeError, match="has no attribute 'predict_log_proba'"
                clf.predict_log_proba

def test_sgd_proba(klass):
    # Check SGD.predict_proba

    # Hinge loss does not allow for conditional prob estimate.
    # We cannot use the factory here, because it defines predict_proba
    # anyway.
    clf = SGDClassifier(loss="hinge", alpha=0.01, max_iter=10, tol=None).fit(X, Y)
    assert not hasattr(clf, "predict_proba")
    assert not hasattr(clf, "predict_log_proba")

    # log and modified_huber losses can output probability estimates
    for loss in ["log_loss", "modified_huber"]:
        clf = klass(loss=loss, alpha=0.01, max_iter=10)
        p = clf.predict_proba([[3, 2]])
        assert p[0, 1] > 0.5
        p = clf.predict_proba([[-1, -1]])
        assert p[0, 1] < 0.5

        # If predict_proba is 0, we get "RuntimeWarning: divide by zero encountered
        # in log". We avoid it here.
            p = clf.predict_log_proba([[3, 2]])
            assert p[0, 1] > p[0, 0]
            p = clf.predict_log_proba([[-1, -1]])
            assert p[0, 1] < p[0, 0]

    # log loss multiclass probability estimates
    clf = klass(loss="log_loss", alpha=0.01, max_iter=10).fit(X2, Y2)

    d = clf.decision_function([[0.1, -0.1], [0.3, 0.2]])
    p = clf.predict_proba([[0.1, -0.1], [0.3, 0.2]])
    assert_array_equal(np.argmax(p, axis=1), np.argmax(d, axis=1))
    assert_almost_equal(p[0].sum(), 1)
    assert np.all(p[0] >= 0)

    p = clf.predict_proba([[-1, -1]])
    d = clf.decision_function([[-1, -1]])
    assert_array_equal(np.argsort(p[0]), np.argsort(d[0]))

    lp = clf.predict_log_proba([[3, 2]])
    p = clf.predict_proba([[3, 2]])
    assert_array_almost_equal(np.log(p), lp)

    lp = clf.predict_log_proba([[-1, -1]])

    # Modified Huber multiclass probability estimates; requires a separate
    # test because the hard zero/one probabilities may destroy the
    # ordering present in decision_function output.
    clf = klass(loss="modified_huber", alpha=0.01, max_iter=10)
    d = clf.decision_function([[3, 2]])
    if klass != SparseSGDClassifier:
        assert np.argmax(d, axis=1) == np.argmax(p, axis=1)
    else:  # XXX the sparse test gets a different X2 (?)
        assert np.argmin(d, axis=1) == np.argmin(p, axis=1)

    # the following sample produces decision_function values < -1,
    # which would cause naive normalization to fail (see comment
    # in SGDClassifier.predict_proba)
    x = X.mean(axis=0)
    d = clf.decision_function([x])
    if np.all(d < -1):  # XXX not true in sparse test case (why?)
        p = clf.predict_proba([x])
        assert_array_almost_equal(p[0], [1 / 3.0] * 3)

def test_sgd_l1(klass):
    # Test L1 regularization
    n = len(X4)
    rng = np.random.RandomState(13)
    idx = np.arange(n)
    rng.shuffle(idx)

    X = X4[idx, :]
    Y = Y4[idx]

        tol=None,
    )
    assert_array_equal(clf.coef_[0, 1:-1], np.zeros((4,)))
    assert_array_equal(pred, Y)

    # test sparsify with dense inputs
    assert sp.issparse(clf.coef_)

    # pickle and unpickle with sparse coef_
    clf = pickle.loads(pickle.dumps(clf))

def test_class_weights(klass):
    X = np.array([[-1.0, -1.0], [-1.0, 0], [-0.8, -1.0], [1.0, 1.0], [1.0, 0.0]])
    y = [1, 1, 1, -1, -1]

    clf = klass(alpha=0.1, max_iter=1000, fit_intercept=False, class_weight=None)

    clf = klass(alpha=0.1, max_iter=1000, fit_intercept=False, class_weight={1: 0.001})

def test_equal_class_weight(klass):
    # Test if equal class weights approx. equals no class weights.
    X = [[1, 0], [1, 0], [0, 1], [0, 1]]
    clf = klass(alpha=0.1, max_iter=1000, class_weight=None)

    X = [[1, 0], [0, 1]]
    y = [0, 1]
    clf_weighted = klass(alpha=0.1, max_iter=1000, class_weight={0: 0.5, 1: 0.5})
    clf_weighted.fit(X, y)

def test_wrong_class_weight_label(klass):
    # ValueError due to not existing class label.
    clf = klass(alpha=0.1, max_iter=1000, class_weight={0: 0.5})

def test_weights_multiplied(klass):
    # Tests that class_weight and sample_weight are multiplicative
    class_weights = {1: 0.6, 2: 0.3}
    sample_weights = rng.random_sample(Y4.shape[0])
    multiplied_together = np.copy(sample_weights)
    multiplied_together[Y4 == 1] *= class_weights[1]
    multiplied_together[Y4 == 2] *= class_weights[2]

    clf1 = klass(alpha=0.1, max_iter=20, class_weight=class_weights)
    clf2 = klass(alpha=0.1, max_iter=20)

    clf1.fit(X4, Y4, sample_weight=sample_weights)
    clf2.fit(X4, Y4, sample_weight=multiplied_together)

    assert_almost_equal(clf1.coef_, clf2.coef_)

def test_balanced_weight(klass):
    # Test class weights for imbalanced data"""
    # compute reference metrics on iris dataset that is quite balanced by
    # default
    idx = np.arange(X.shape[0])
    rng = np.random.RandomState(6)
    X = X[idx]
    y = y[idx]
    clf = klass(alpha=0.0001, max_iter=1000, class_weight=None, shuffle=False).fit(X, y)
    f1 = metrics.f1_score(y, clf.predict(X), average="weighted")
    assert_almost_equal(f1, 0.96, decimal=1)

    # make the same prediction using balanced class_weight
    clf_balanced = klass(
        alpha=0.0001, max_iter=1000, class_weight="balanced", shuffle=False
    f1 = metrics.f1_score(y, clf_balanced.predict(X), average="weighted")

    # Make sure that in the balanced case it does not change anything
    # to use "balanced"
    assert_array_almost_equal(clf.coef_, clf_balanced.coef_, 6)

    # build an very very imbalanced dataset out of iris data
    X_0 = X[y == 0, :]
    y_0 = y[y == 0]

    X_imbalanced = np.vstack([X] + [X_0] * 10)
    y_imbalanced = np.concatenate([y] + [y_0] * 10)

    # fit a model on the imbalanced data without class weight info
    clf = klass(max_iter=1000, class_weight=None, shuffle=False)
    clf.fit(X_imbalanced, y_imbalanced)
    assert metrics.f1_score(y, y_pred, average="weighted") < 0.96

    # fit a model with balanced class_weight enabled
    clf = klass(max_iter=1000, class_weight="balanced", shuffle=False)
    assert metrics.f1_score(y, y_pred, average="weighted") > 0.96

def test_sample_weights(klass):

    clf = klass(alpha=0.1, max_iter=1000, fit_intercept=False)

    clf.fit(X, y, sample_weight=[0.001] * 3 + [1] * 2)

def test_wrong_sample_weights(klass):
    # Test if ValueError is raised if sample_weight has wrong shape
    if klass in [SGDClassifier, SparseSGDClassifier]:
        clf = klass(alpha=0.1, max_iter=1000, fit_intercept=False)
        clf = klass(nu=0.1, max_iter=1000, fit_intercept=False)
    # provided sample_weight too long
        clf.fit(X, Y, sample_weight=np.arange(7))

def test_partial_fit_exception(klass):
    clf = klass(alpha=0.01)
    # classes was not specified
        clf.partial_fit(X3, Y3)

def test_partial_fit_binary(klass):
    third = X.shape[0] // 3
    classes = np.unique(Y)

    clf.partial_fit(X[:third], Y[:third], classes=classes)
    assert clf.coef_.shape == (1, X.shape[1])
    assert clf.decision_function([[0, 0]]).shape == (1,)
    id1 = id(clf.coef_.data)

    clf.partial_fit(X[third:], Y[third:])
    id2 = id(clf.coef_.data)
    # check that coef_ haven't been re-allocated
    assert id1, id2

    y_pred = clf.predict(T)

def test_partial_fit_multiclass(klass):
    third = X2.shape[0] // 3
    classes = np.unique(Y2)

    clf.partial_fit(X2[:third], Y2[:third], classes=classes)
    assert clf.coef_.shape == (3, X2.shape[1])

    clf.partial_fit(X2[third:], Y2[third:])

def test_partial_fit_multiclass_average(klass):
    clf = klass(alpha=0.01, average=X2.shape[0])

def test_fit_then_partial_fit(klass):
    # Partial_fit should work after initial fit in the multiclass case.
    # Non-regression test for #2496; fit would previously produce a
    # Fortran-ordered coef_ that subsequent partial_fit couldn't handle.
    clf.partial_fit(X2, Y2)  # no exception here

def test_partial_fit_equal_fit_classif(klass, lr):
    for X_, Y_, T_ in ((X, Y, T), (X2, Y2, T2)):
        clf = klass(alpha=0.01, eta0=0.01, max_iter=2, learning_rate=lr, shuffle=False)
        clf.fit(X_, Y_)
        y_pred = clf.decision_function(T_)
        t = clf.t_

        classes = np.unique(Y_)
        clf = klass(alpha=0.01, eta0=0.01, learning_rate=lr, shuffle=False)
        for i in range(2):
            clf.partial_fit(X_, Y_, classes=classes)
        y_pred2 = clf.decision_function(T_)

        assert clf.t_ == t
        assert_array_almost_equal(y_pred, y_pred2, decimal=2)

def test_regression_losses(klass):
    random_state = np.random.RandomState(1)
        eta0=0.1,
    )
    assert 1.0 == np.mean(clf.predict(X) == Y)

        eta0=0.1,
        loss="squared_epsilon_insensitive",
    )

    clf = klass(alpha=0.01, loss="huber", random_state=random_state)

    )

def test_warm_start_multiclass(klass):
    _test_warm_start(klass, X2, Y2, "optimal")

def test_multiple_fit(klass):
    # Test multiple calls of fit w/ different shaped inputs.
    assert hasattr(clf, "coef_")

    # Non-regression test: try fitting with a different label set.
    y = [["ham", "spam"][i] for i in LabelEncoder().fit_transform(Y)]
    clf.fit(X[:, :-1], y)

def test_sgd_reg(klass):
    # Check that SGD gives any results.
    clf = klass(alpha=0.1, max_iter=2, fit_intercept=False)
    clf.fit([[0, 0], [1, 1], [2, 2]], [0, 1, 2])
    assert clf.coef_[0] == clf.coef_[1]

def test_sgd_averaged_computed_correctly(klass):
    # Tests the average regressor matches the naive implementation

    eta = 0.001

        eta0=eta,
    )

    assert_array_almost_equal(clf.coef_, average_weights, decimal=16)
    assert_almost_equal(clf.intercept_, average_intercept, decimal=16)

def test_sgd_averaged_partial_fit(klass):
    # Tests whether the partial fit yields the same average as the fit
    eta = 0.001

        eta0=eta,
    )

    clf.partial_fit(X[: int(n_samples / 2)][:], y[: int(n_samples / 2)])
    clf.partial_fit(X[int(n_samples / 2) :][:], y[int(n_samples / 2) :])

    assert_almost_equal(clf.intercept_[0], average_intercept, decimal=16)

def test_average_sparse(klass):
    # Checks the average weights on data with 0s

    eta = 0.001
        eta0=eta,
    )

    n_samples = Y3.shape[0]

    clf.partial_fit(X3[: int(n_samples / 2)][:], Y3[: int(n_samples / 2)])
    clf.partial_fit(X3[int(n_samples / 2) :][:], Y3[int(n_samples / 2) :])
    average_weights, average_intercept = asgd(klass, X3, Y3, eta, alpha)

def test_sgd_least_squares_fit(klass):
    xmin, xmax = -5, 5
    X = np.linspace(xmin, xmax, n_samples).reshape(n_samples, 1)

    y = 0.5 * X.ravel()

    clf = klass(loss="squared_error", alpha=0.1, max_iter=20, fit_intercept=False)
    assert score > 0.99

    # simple linear function with noise
    y = 0.5 * X.ravel() + rng.randn(n_samples, 1).ravel()

    assert score > 0.5

def test_sgd_epsilon_insensitive(klass):

    )

    )

def test_sgd_huber_fit(klass):

    clf = klass(loss="huber", epsilon=0.1, alpha=0.1, max_iter=20, fit_intercept=False)

def test_elasticnet_convergence(klass):
    # Check that the SGD output is consistent with coordinate descent

    n_samples, n_features = 1000, 5
    # ground_truth linear model that generate y from X and to which the
    # models should converge if the regularizer would be set to 0.0
    ground_truth_coef = rng.randn(n_features)
    y = np.dot(X, ground_truth_coef)

    # XXX: alpha = 0.1 seems to cause convergence problems
    for alpha in [0.01, 0.001]:
        for l1_ratio in [0.5, 0.8, 1.0]:
            cd = linear_model.ElasticNet(
                alpha=alpha, l1_ratio=l1_ratio, fit_intercept=False
            )
            cd.fit(X, y)
            sgd = klass(
                max_iter=50,
            )
            sgd.fit(X, y)
            err_msg = (
                "cd and sgd did not converge to comparable "
                "results for alpha=%f and l1_ratio=%f" % (alpha, l1_ratio)
            )
            assert_almost_equal(cd.coef_, sgd.coef_, decimal=2, err_msg=err_msg)

def test_partial_fit(klass):

    clf.partial_fit(X[:third], Y[:third])
    assert clf.coef_.shape == (X.shape[1],)
    assert clf.predict([[0, 0]]).shape == (1,)

def test_partial_fit_equal_fit(klass, lr):
    clf = klass(alpha=0.01, max_iter=2, eta0=0.01, learning_rate=lr, shuffle=False)
    t = clf.t_

    clf = klass(alpha=0.01, eta0=0.01, learning_rate=lr, shuffle=False)
        clf.partial_fit(X, Y)
    y_pred2 = clf.predict(T)

    assert clf.t_ == t
    assert_array_almost_equal(y_pred, y_pred2, decimal=2)

def test_loss_function_epsilon(klass):
    clf = klass(epsilon=0.9)
    clf.set_params(epsilon=0.1)
    assert clf.loss_functions["huber"][1] == 0.1

def asgd_oneclass(klass, X, eta, nu, coef_init=None, offset_init=0.0):
        coef = np.zeros(X.shape[1])
    else:
        coef = coef_init

    average_coef = np.zeros(X.shape[1])
    offset = offset_init
    intercept = 1 - offset
    decay = 1.0

    if klass == SparseSGDOneClassSVM:

        p = np.dot(entry, coef)
        if p <= 1.0:
            gradient = -1
        else:
            gradient = 0
        coef *= max(0, 1.0 - (eta * nu / 2))
        coef += -(eta * gradient * entry)
        intercept += -(eta * (nu + gradient)) * decay

        average_coef *= i
        average_coef += coef
        average_coef /= i + 1.0

    return average_coef, 1 - average_intercept

def _test_warm_start_oneclass(klass, X, lr):
    clf = klass(nu=0.5, eta0=0.01, shuffle=False, learning_rate=lr)

    clf2 = klass(nu=0.1, eta0=0.01, shuffle=False, learning_rate=lr)
    clf2.fit(X, coef_init=clf.coef_.copy(), offset_init=clf.offset_.copy())

    clf3 = klass(nu=0.5, eta0=0.01, shuffle=False, warm_start=True, learning_rate=lr)
    clf3.fit(X)

    assert_allclose(clf3.coef_, clf.coef_)

    clf3.set_params(nu=0.1)

    assert_allclose(clf3.coef_, clf2.coef_)

def test_warm_start_oneclass(klass, lr):
    _test_warm_start_oneclass(klass, X, lr)

def test_clone_oneclass(klass):
    clf = klass(nu=0.5)
    clf.set_params(nu=0.1)

    clf2 = klass(nu=0.1)
    clf2.fit(X)

def test_partial_fit_oneclass(klass):
    clf = klass(nu=0.1)

    clf.partial_fit(X[:third])
    assert clf.offset_.shape == (1,)
    previous_coefs = clf.coef_

    clf.partial_fit(X[third:])
    assert clf.coef_ is previous_coefs

    # raises ValueError if number of features does not match previous data
        clf.partial_fit(X[:, 1])

def test_partial_fit_equal_fit_oneclass(klass, lr):
    clf = klass(nu=0.05, max_iter=2, eta0=0.01, learning_rate=lr, shuffle=False)
    y_scores = clf.decision_function(T)
    t = clf.t_
    coef = clf.coef_
    offset = clf.offset_

    clf = klass(nu=0.05, eta0=0.01, max_iter=1, learning_rate=lr, shuffle=False)
    for _ in range(2):
        clf.partial_fit(X)
    y_scores2 = clf.decision_function(T)

    assert_allclose(y_scores, y_scores2)
    assert_allclose(clf.coef_, coef)
    assert_allclose(clf.offset_, offset)

def test_late_onset_averaging_reached_oneclass(klass):
    # Test average
    nu = 0.05

    # 2 passes over the training set but average only at second pass
        average=7, learning_rate="constant", eta0=eta0, nu=nu, max_iter=2, shuffle=False
    )
    # 1 pass over the training set with no averaging
        nu=nu,
    )

    clf1.fit(X)

    # Start from clf2 solution, compute averaging using asgd function and
    # compare with clf1 solution
    average_coef, average_offset = asgd_oneclass(
        klass, X, eta0, nu, coef_init=clf2.coef_.ravel(), offset_init=clf2.offset_
    )

    assert_allclose(clf1.coef_.ravel(), average_coef.ravel())
    assert_allclose(clf1.offset_, average_offset)

def test_sgd_averaged_computed_correctly_oneclass(klass):
    # Tests the average SGD One-Class SVM matches the naive implementation
    eta = 0.001
    nu = 0.05

        eta0=eta,
        nu=nu,
    )

    average_coef, average_offset = asgd_oneclass(klass, X, eta, nu)

    assert_allclose(clf.coef_, average_coef)
    assert_allclose(clf.offset_, average_offset)

def test_sgd_averaged_partial_fit_oneclass(klass):
    eta = 0.001
    nu = 0.05

        eta0=eta,
        nu=nu,
    )

    clf.partial_fit(X[: int(n_samples / 2)][:])
    clf.partial_fit(X[int(n_samples / 2) :][:])

def test_average_sparse_oneclass(klass):
    # Checks the average coef on data with 0s
    eta = 0.001
    nu = 0.01
        eta0=eta,
        nu=nu,
    )

    n_samples = X3.shape[0]

    clf.partial_fit(X3[: int(n_samples / 2)])
    clf.partial_fit(X3[int(n_samples / 2) :])
    average_coef, average_offset = asgd_oneclass(klass, X3, eta, nu)

def test_sgd_oneclass():
    # Test fit, decision_function, predict and score_samples on a toy
    # dataset
    X_train = np.array([[-2, -1], [-1, -1], [1, 1]])
    X_test = np.array([[0.5, -2], [2, 2]])
    clf = SGDOneClassSVM(
        nu=0.5, eta0=1, learning_rate="constant", shuffle=False, max_iter=1
    )
    assert_allclose(clf.coef_, np.array([-0.125, 0.4375]))
    assert clf.offset_[0] == -0.5

    scores = clf.score_samples(X_test)
    assert_allclose(scores, np.array([-0.9375, 0.625]))

    dec = clf.score_samples(X_test) - clf.offset_
    assert_allclose(clf.decision_function(X_test), dec)

    pred = clf.predict(X_test)
    assert_array_equal(pred, np.array([-1, 1]))

def test_ocsvm_vs_sgdocsvm():
    # Checks SGDOneClass SVM gives a good approximation of kernelized
    # One-Class SVM
    nu = 0.05
    gamma = 2.0
    random_state = 42

    # Generate train and test data
    X = 0.3 * rng.randn(500, 2)
    X = 0.3 * rng.randn(100, 2)

    clf = OneClassSVM(gamma=gamma, kernel="rbf", nu=nu)
    y_pred_ocsvm = clf.predict(X_test)
    dec_ocsvm = clf.decision_function(X_test).reshape(1, -1)

    # SGDOneClassSVM using kernel approximation
    max_iter = 15
    transform = Nystroem(gamma=gamma, random_state=random_state)
    clf_sgd = SGDOneClassSVM(
        nu=nu,
        tol=None,
    )
    pipe_sgd = make_pipeline(transform, clf_sgd)
    pipe_sgd.fit(X_train)
    y_pred_sgdocsvm = pipe_sgd.predict(X_test)
    dec_sgdocsvm = pipe_sgd.decision_function(X_test).reshape(1, -1)

    assert np.mean(y_pred_sgdocsvm == y_pred_ocsvm) >= 0.99
    corrcoef = np.corrcoef(np.concatenate((dec_ocsvm, dec_sgdocsvm)))[0, 1]
    assert corrcoef >= 0.9

def test_l1_ratio():
    # Test if l1 ratio extremes match L1 and L2 penalty settings.
        n_samples=1000, n_features=100, n_informative=20, random_state=1234
    )

    # test if elasticnet with l1_ratio near 1 gives same result as pure l1
    est_en = SGDClassifier(
        alpha=0.001,
        tol=None,
        max_iter=6,
        l1_ratio=0.9999999999,
    est_l1 = SGDClassifier(
        alpha=0.001, penalty="l1", max_iter=6, random_state=42, tol=None
    assert_array_almost_equal(est_en.coef_, est_l1.coef_)

    # test if elasticnet with l1_ratio near 0 gives same result as pure l2
        tol=None,
        l1_ratio=0.0000000001,
    est_l2 = SGDClassifier(
        alpha=0.001, penalty="l2", max_iter=6, random_state=42, tol=None
    assert_array_almost_equal(est_en.coef_, est_l2.coef_)

def test_underflow_or_overlow():
        # Generate some weird data with hugely unscaled features
        n_samples = 100

        X = rng.normal(size=(n_samples, n_features))
        X[:, :2] *= 1e300
        assert np.isfinite(X).all()

        # Use MinMaxScaler to scale the data without introducing a numerical
        # instability (computing the standard deviation naively is not possible
        # on this data)
        X_scaled = MinMaxScaler().fit_transform(X)
        assert np.isfinite(X_scaled).all()

        # Define a ground truth on the scaled data
        ground_truth = rng.normal(size=n_features)
        y = (np.dot(X_scaled, ground_truth) > 0.0).astype(np.int32)
        assert_array_equal(np.unique(y), [0, 1])

        model = SGDClassifier(alpha=0.1, loss="squared_hinge", max_iter=500)

        # smoke test: model is stable on scaled data
        model.fit(X_scaled, y)
        assert np.isfinite(model.coef_).all()

        # model is numerically unstable on unscaled data
        msg_regxp = (
            r"Floating-point under-/overflow occurred at epoch #.*"
            " Scaling input data with StandardScaler or MinMaxScaler"
            " might help."
        )
        with pytest.raises(ValueError, match=msg_regxp):

def test_numerical_stability_large_gradient():
    # Non regression test case for numerical stability on scaled problems
    # where the gradient can still explode with some losses
    model = SGDClassifier(
        l1_ratio=0.3,
        eta0=0.001,
        tol=None,
    )
        model.fit(iris.data, iris.target)
    assert np.isfinite(model.coef_).all()

def test_large_regularization(penalty):
    # Non regression tests for numerical stability issues caused by large
    # regularization parameters
        alpha=1e5,
        eta0=0.1,
        tol=None,
    )
    assert_array_almost_equal(model.coef_, np.zeros_like(model.coef_))

def test_tol_parameter():
    # Test that the tol parameter behaves as expected
    X = StandardScaler().fit_transform(iris.data)
    y = iris.target == 1

    # With tol is None, the number of iteration should be equal to max_iter
    max_iter = 42
    model_0 = SGDClassifier(tol=None, random_state=0, max_iter=max_iter)
    model_0.fit(X, y)
    assert max_iter == model_0.n_iter_

    # If tol is not None, the number of iteration should be less than max_iter
    max_iter = 2000
    model_1 = SGDClassifier(tol=0, random_state=0, max_iter=max_iter)
    model_1.fit(X, y)
    assert max_iter > model_1.n_iter_
    assert model_1.n_iter_ > 5

    # A larger tol should yield a smaller number of iteration
    model_2 = SGDClassifier(tol=0.1, random_state=0, max_iter=max_iter)
    model_2.fit(X, y)
    assert model_1.n_iter_ > model_2.n_iter_
    assert model_2.n_iter_ > 3

    # Strict tolerance and small max_iter should trigger a warning
    model_3 = SGDClassifier(max_iter=3, tol=1e-3, random_state=0)
        "Maximum number of iteration reached before "
        "convergence. Consider increasing max_iter to "
        "improve the fit."
    )
        model_3.fit(X, y)
    assert model_3.n_iter_ == 3

def _test_loss_common(loss_function, cases):
    # Test the different loss functions
    # cases is a list of (p, y, expected)
    for p, y, expected_loss, expected_dloss in cases:
        assert_almost_equal(loss_function.py_loss(p, y), expected_loss)
        assert_almost_equal(loss_function.py_dloss(p, y), expected_dloss)

def test_loss_hinge():
    # Test Hinge (hinge / perceptron)
    # hinge
    loss = sgd_fast.Hinge(1.0)
    cases = [
        # (p, y, expected_loss, expected_dloss)
        (1.1, 1.0, 0.0, 0.0),
        (-2.0, -1.0, 0.0, 0.0),
        (1.0, 1.0, 0.0, -1.0),
        (-1.0, -1.0, 0.0, 1.0),
        (0.5, 1.0, 0.5, -1.0),
        (2.0, -1.0, 3.0, 1.0),
        (-0.5, -1.0, 0.5, 1.0),
        (0.0, 1.0, 1, -1.0),
    ]
    _test_loss_common(loss, cases)

    # perceptron
    loss = sgd_fast.Hinge(0.0)
    cases = [
        (1.0, 1.0, 0.0, 0.0),
        (-0.1, -1.0, 0.0, 0.0),
        (0.0, 1.0, 0.0, -1.0),
        (0.0, -1.0, 0.0, 1.0),
        (0.5, -1.0, 0.5, 1.0),
        (2.0, -1.0, 2.0, 1.0),
        (-0.5, 1.0, 0.5, -1.0),
        (-1.0, 1.0, 1.0, -1.0),
    ]

def test_gradient_squared_hinge():
    # Test SquaredHinge
    loss = sgd_fast.SquaredHinge(1.0)
    cases = [
        (1.0, -1.0, 4.0, 4.0),
        (-1.0, 1.0, 4.0, -4.0),
        (0.5, 1.0, 0.25, -1.0),
        (0.5, -1.0, 2.25, 3.0),
    ]

def test_loss_modified_huber():
    # (p, y, expected_loss, expected_dloss)
    loss = sgd_fast.ModifiedHuber()
    cases = [
        (-1.0, -1.0, 0.0, 0.0),
        (2.0, 1.0, 0.0, 0.0),
        (0.0, 1.0, 1.0, -2.0),
        (-2.0, 1.0, 8, -4.0),
        (-3.0, 1.0, 12, -4.0),
    ]

def test_loss_epsilon_insensitive():
    # Test EpsilonInsensitive
    loss = sgd_fast.EpsilonInsensitive(0.1)
    cases = [
        (0.0, 0.0, 0.0, 0.0),
        (0.1, 0.0, 0.0, 0.0),
        (-2.05, -2.0, 0.0, 0.0),
        (3.05, 3.0, 0.0, 0.0),
        (2.2, 2.0, 0.1, 1.0),
        (2.0, -1.0, 2.9, 1.0),
        (2.0, 2.2, 0.1, -1.0),
        (-2.0, 1.0, 2.9, -1.0),
    ]

def test_loss_squared_epsilon_insensitive():
    # Test SquaredEpsilonInsensitive
    loss = sgd_fast.SquaredEpsilonInsensitive(0.1)
    cases = [
        (2.2, 2.0, 0.01, 0.2),
        (2.0, -1.0, 8.41, 5.8),
        (2.0, 2.2, 0.01, -0.2),
        (-2.0, 1.0, 8.41, -5.8),
    ]

def test_multi_thread_multi_class_and_early_stopping():
    # This is a non-regression test for a bad interaction between
    # early stopping internal attribute and thread-based parallelism.
    clf = SGDClassifier(
        alpha=1e-3,
        tol=1e-3,
        n_iter_no_change=100,
        n_jobs=2,
    )
    assert clf.n_iter_ > clf.n_iter_no_change
    assert clf.n_iter_ < clf.n_iter_no_change + 20
    assert clf.score(iris.data, iris.target) > 0.8

def test_multi_core_gridsearch_and_early_stopping():
    # early stopping internal attribute and process-based multi-core
    # parallelism.
        "alpha": np.logspace(-4, 4, 9),
        "n_iter_no_change": [5, 10, 50],
    }

    clf = SGDClassifier(tol=1e-2, max_iter=1000, early_stopping=True, random_state=0)
    search = RandomizedSearchCV(clf, param_grid, n_iter=5, n_jobs=2, random_state=0)
    search.fit(iris.data, iris.target)
    assert search.best_score_ > 0.8

def test_SGDClassifier_fit_for_all_backends(backend):
    # This is a non-regression smoke test. In the multi-class case,
    # SGDClassifier.fit fits each class in a one-versus-all fashion using
    # joblib.Parallel.  However, each OvA step updates the coef_ attribute of
    # the estimator in-place. Internally, SGDClassifier calls Parallel using
    # require='sharedmem'. This test makes sure SGDClassifier.fit works
    # consistently even when the user asks for a backend that does not provide
    # sharedmem semantics.

    # We further test a case where memmapping would have been used if
    # SGDClassifier.fit was called from a loky or multiprocessing backend. In
    # this specific case, in-place modification of clf.coef_ would have caused
    # a segmentation fault when trying to write in a readonly memory mapped
    # buffer.

    random_state = np.random.RandomState(42)

    # Create a classification problem with 50000 features and 20 classes. Using
    # loky or multiprocessing this make the clf.coef_ exceed the threshold
    # above which memmaping is used in joblib and loky (1MB as of 2018/11/1).
    X = sp.random(500, 2000, density=0.02, format="csr", random_state=random_state)
    y = random_state.choice(20, 500)

    # Begin by fitting a SGD classifier sequentially
    clf_sequential = SGDClassifier(max_iter=1000, n_jobs=1, random_state=42)
    clf_sequential.fit(X, y)

    # Fit a SGDClassifier using the specified backend, and make sure the
    # coefficients are equal to those obtained using a sequential fit
    clf_parallel = SGDClassifier(max_iter=1000, n_jobs=4, random_state=42)
    with joblib.parallel_backend(backend=backend):
        clf_parallel.fit(X, y)
    assert_array_almost_equal(clf_sequential.coef_, clf_parallel.coef_)

def test_sgd_random_state(Estimator, global_random_seed):
    # Train the same model on the same data without converging and check that we
    # get reproducible results by fixing the random seed.
    if Estimator == linear_model.SGDRegressor:
        X, y = datasets.make_regression(random_state=global_random_seed)
    else:
        X, y = datasets.make_classification(random_state=global_random_seed)

    # Fitting twice a model with the same hyper-parameters on the same training
    # set with the same seed leads to the same results deterministically.

    est = Estimator(random_state=global_random_seed, max_iter=1)
        coef_same_seed_a = est.fit(X, y).coef_
        assert est.n_iter_ == 1

        coef_same_seed_b = est.fit(X, y).coef_

    assert_allclose(coef_same_seed_a, coef_same_seed_b)

    # set but with different random seed leads to different results after one
    # epoch because of the random shuffling of the dataset.

    est = Estimator(random_state=global_random_seed + 1, max_iter=1)
        coef_other_seed = est.fit(X, y).coef_

    assert np.abs(coef_same_seed_a - coef_other_seed).max() > 1.0

def test_validation_mask_correctly_subsets(monkeypatch):
    """Test that data passed to validation callback correctly subsets.

    Non-regression test for #23255.
    """
    clf = linear_model.SGDClassifier(
        tol=1e-3,
    )

    mock = Mock(side_effect=_stochastic_gradient._ValidationScoreCallback)
    monkeypatch.setattr(_stochastic_gradient, "_ValidationScoreCallback", mock)

    X_val, y_val = mock.call_args[0][1:3]
    assert X_val.shape[0] == int(n_samples * validation_fraction)
    assert y_val.shape[0] == int(n_samples * validation_fraction)

def test_sgd_error_on_zero_validation_weight():
    # Test that SGDClassifier raises error when all the validation samples
    # have zero sample_weight. Non-regression test for #17229.
    sample_weight = np.zeros_like(Y)

        early_stopping=True, validation_fraction=validation_fraction, random_state=0
    )

        "The sample weights for validation set are all zero, consider using a"
        " different random state."
    )

def test_sgd_verbose(Estimator):
    """non-regression test for gh #25249"""
    Estimator(verbose=1).fit(X, Y)

def test_sgd_dtype_match(SGDEstimator, data_type):
    _X = X.astype(data_type)
    _Y = np.array(Y, dtype=data_type)
    sgd_model = SGDEstimator()
    sgd_model.fit(_X, _Y)
    assert sgd_model.coef_.dtype == data_type

def test_sgd_numerical_consistency(SGDEstimator):
    X_64 = X.astype(dtype=np.float64)
    Y_64 = np.array(Y, dtype=np.float64)

    X_32 = X.astype(dtype=np.float32)
    Y_32 = np.array(Y, dtype=np.float32)

    sgd_64 = SGDEstimator(max_iter=20)
    sgd_64.fit(X_64, Y_64)

    sgd_32 = SGDEstimator(max_iter=20)
    sgd_32.fit(X_32, Y_32)

    assert_allclose(sgd_64.coef_, sgd_32.coef_)

def test_sgd_one_class_svm_estimator_type():
    """Check that SGDOneClassSVM has the correct estimator type.

    Non-regression test for if the mixin was not on the left.
    """
    sgd_ocsvm = SGDOneClassSVM()
    assert get_tags(sgd_ocsvm).estimator_type == "outlier_detector"

def test_simple():
    # Principle of Lars is to keep covariances tied and decreasing

    # also test verbose output
    import sys

    try:

        _, _, coef_path_ = linear_model.lars_path(X, y, method="lar", verbose=10)

        for i, coef_ in enumerate(coef_path_.T):
            res = y - np.dot(X, coef_)
            cov = np.dot(X.T, res)
            C = np.max(abs(cov))
            eps = 1e-3
            ocur = len(cov[C - eps < abs(cov)])
            if i < X.shape[1]:
                assert ocur == i + 1
            else:
                # no more than max_pred variables can go into the active set
                assert ocur == X.shape[1]
    finally:

def test_simple_precomputed():
    # The same, with precomputed Gram matrix

    _, _, coef_path_ = linear_model.lars_path(X, y, Gram=G, method="lar")

    for i, coef_ in enumerate(coef_path_.T):
        res = y - np.dot(X, coef_)
        cov = np.dot(X.T, res)
        C = np.max(abs(cov))
        eps = 1e-3
        ocur = len(cov[C - eps < abs(cov)])
        if i < X.shape[1]:
            assert ocur == i + 1
        else:
            # no more than max_pred variables can go into the active set
            assert ocur == X.shape[1]

def _assert_same_lars_path_result(output1, output2):
    assert len(output1) == len(output2)
    for o1, o2 in zip(output1, output2):
        assert_allclose(o1, o2)

def test_lars_path_gram_equivalent(method, return_path):
    _assert_same_lars_path_result(
        linear_model.lars_path_gram(
            Xy=Xy, Gram=G, n_samples=n_samples, method=method, return_path=return_path
        ),
        linear_model.lars_path(X, y, Gram=G, method=method, return_path=return_path),
    )

def test_x_none_gram_none_raises_value_error():
    # Test that lars_path with no X and Gram raises exception
    Xy = np.dot(X.T, y)
    with pytest.raises(ValueError, match="X and Gram cannot both be unspecified"):
        linear_model.lars_path(None, y, Gram=None, Xy=Xy)

def test_all_precomputed():
    # Test that lars_path with precomputed Gram and Xy gives the right answer
    G = np.dot(X.T, X)
    for method in "lar", "lasso":
        output = linear_model.lars_path(X, y, method=method)
        output_pre = linear_model.lars_path(X, y, Gram=G, Xy=Xy, method=method)
        for expected, got in zip(output, output_pre):
            assert_array_almost_equal(expected, got)

def test_lars_lstsq():
    # Test that Lars gives least square solution at the end
    # of the path
    X1 = 3 * X  # use un-normalized dataset
    clf = linear_model.LassoLars(alpha=0.0)
    clf.fit(X1, y)
    coef_lstsq = np.linalg.lstsq(X1, y)[0]
    assert_array_almost_equal(clf.coef_, coef_lstsq)

def test_lasso_gives_lstsq_solution():
    # Test that Lars Lasso gives least square solution at the end
    _, _, coef_path_ = linear_model.lars_path(X, y, method="lasso")
    coef_lstsq = np.linalg.lstsq(X, y)[0]
    assert_array_almost_equal(coef_lstsq, coef_path_[:, -1])

def test_collinearity():
    # Check that lars_path is robust to collinearity in input
    X = np.array([[3.0, 3.0, 1.0], [2.0, 2.0, 0.0], [1.0, 1.0, 0]])
    y = np.array([1.0, 0.0, 0])

    _, _, coef_path_ = f(linear_model.lars_path)(X, y, alpha_min=0.01)
    assert not np.isnan(coef_path_).any()
    residual = np.dot(X, coef_path_[:, -1]) - y
    assert (residual**2).sum() < 1.0  # just make sure it's bounded

    X = rng.rand(n_samples, 5)
    _, _, coef_path_ = linear_model.lars_path(
        X,
        y,
        Gram="auto",
        alpha_min=0.0,
        method="lasso",
    )
    assert_array_almost_equal(coef_path_, np.zeros_like(coef_path_))

def test_no_path():
    # Test that the ``return_path=False`` option returns the correct output
    alphas_, _, coef_path_ = linear_model.lars_path(X, y, method="lar")
    alpha_, _, coef = linear_model.lars_path(X, y, method="lar", return_path=False)

    assert_array_almost_equal(coef, coef_path_[:, -1])
    assert alpha_ == alphas_[-1]

def test_no_path_precomputed():
    # Test that the ``return_path=False`` option with Gram remains correct
    alphas_, _, coef_path_ = linear_model.lars_path(X, y, method="lar", Gram=G)
    alpha_, _, coef = linear_model.lars_path(
        X, y, method="lar", Gram=G, return_path=False
    )

def test_no_path_all_precomputed():
    # Test that the ``return_path=False`` option with Gram and Xy remains
    # correct
    X, y = 3 * diabetes.data, diabetes.target
    alphas_, _, coef_path_ = linear_model.lars_path(
        X, y, method="lasso", Xy=Xy, Gram=G, alpha_min=0.9
    )
        X, y, method="lasso", Gram=G, Xy=Xy, alpha_min=0.9, return_path=False
    )

def test_lars_precompute(classifier):
    # Check for different values of precompute

    clf = classifier(precompute=G)
    output_1 = ignore_warnings(clf.fit)(X, y).coef_
    for precompute in [True, False, "auto", None]:
        clf = classifier(precompute=precompute)
        output_2 = clf.fit(X, y).coef_
        assert_array_almost_equal(output_1, output_2, decimal=8)

    # Test when input is a singular matrix
    X1 = np.array([[1, 1.0], [1.0, 1.0]])
    y1 = np.array([1, 1])
    _, _, coef_path = linear_model.lars_path(X1, y1)
    assert_array_almost_equal(coef_path.T, [[0, 0], [1, 0]])

def test_rank_deficient_design():
    # consistency test that checks that LARS Lasso is handling rank
    # deficient input data (with n_features < rank) in the same way
    # as coordinate descent Lasso
    y = [5, 0, 5]
    for X in ([[5, 0], [0, 5], [10, 10]], [[10, 10, 0], [1e-32, 0, 0], [0, 0, 1]]):
        # To be able to use the coefs to compute the objective function,
        # we need to turn off normalization
        lars = linear_model.LassoLars(0.1)
        coef_lars_ = lars.fit(X, y).coef_
        obj_lars = 1.0 / (2.0 * 3.0) * linalg.norm(
            y - np.dot(X, coef_lars_)
        ) ** 2 + 0.1 * linalg.norm(coef_lars_, 1)
        coord_descent = linear_model.Lasso(0.1, tol=1e-6)
        coef_cd_ = coord_descent.fit(X, y).coef_
        obj_cd = (1.0 / (2.0 * 3.0)) * linalg.norm(
            y - np.dot(X, coef_cd_)
        ) ** 2 + 0.1 * linalg.norm(coef_cd_, 1)
        assert obj_lars < obj_cd * (1.0 + 1e-8)

def test_lasso_lars_vs_lasso_cd():
    # Test that LassoLars and Lasso using coordinate descent give the
    # same results.
    X = 3 * diabetes.data

    alphas, _, lasso_path = linear_model.lars_path(X, y, method="lasso")
    lasso_cd = linear_model.Lasso(fit_intercept=False, tol=1e-8)
    for c, a in zip(lasso_path.T, alphas):
        if a == 0:
            continue
        lasso_cd.alpha = a
        lasso_cd.fit(X, y)
        error = linalg.norm(c - lasso_cd.coef_)
        assert error < 0.01

    # similar test, with the classifiers
    for alpha in np.linspace(1e-2, 1 - 1e-2, 20):
        clf1 = linear_model.LassoLars(alpha=alpha).fit(X, y)
        clf2 = linear_model.Lasso(alpha=alpha, tol=1e-8).fit(X, y)
        err = linalg.norm(clf1.coef_ - clf2.coef_)
        assert err < 1e-3

    # same test, with normalized data
    X = X - X.sum(axis=0)
    X /= np.linalg.norm(X, axis=0)
        if a == 0:
            continue

def test_lasso_lars_vs_lasso_cd_early_stopping():
    # same results when early stopping is used.
    # (test : before, in the middle, and in the last part of the path)
    alphas_min = [10, 0.9, 1e-4]

    for alpha_min in alphas_min:
        alphas, _, lasso_path = linear_model.lars_path(
            X, y, method="lasso", alpha_min=alpha_min
        )
        lasso_cd = linear_model.Lasso(fit_intercept=False, tol=1e-8)
        lasso_cd.alpha = alphas[-1]
        error = linalg.norm(lasso_path[:, -1] - lasso_cd.coef_)

    # same test, with normalization
    X = diabetes.data - diabetes.data.sum(axis=0)

        )
        lasso_cd = linear_model.Lasso(tol=1e-8)

def test_lasso_lars_path_length():
    # Test that the path length of the LassoLars is right
    lasso = linear_model.LassoLars()
    lasso2 = linear_model.LassoLars(alpha=lasso.alphas_[2])
    lasso2.fit(X, y)
    assert_array_almost_equal(lasso.alphas_[:3], lasso2.alphas_)
    # Also check that the sequence of alphas is always decreasing
    assert np.all(np.diff(lasso.alphas_) < 0)

def test_lasso_lars_vs_lasso_cd_ill_conditioned():
    # Test lasso lars on a very ill-conditioned design, and check that
    # it does not blow up, and stays somewhat close to a solution given
    # by the coordinate descent solver
    # Also test that lasso_path (using lars_path output style) gives
    # the same result as lars_path and previous lasso output style
    # under these conditions.

    # Generate data
    n, m = 70, 100
    k = 5
    X = rng.randn(n, m)
    w = np.zeros((m, 1))
    i = np.arange(0, m)
    rng.shuffle(i)
    supp = i[:k]
    w[supp] = np.sign(rng.randn(k, 1)) * (rng.rand(k, 1) + 1)
    sigma = 0.2
    y += sigma * rng.rand(*y.shape)
    y = y.squeeze()
    lars_alphas, _, lars_coef = linear_model.lars_path(X, y, method="lasso")

    _, lasso_coef2, _ = linear_model.lasso_path(X, y, alphas=lars_alphas, tol=1e-6)

    assert_array_almost_equal(lars_coef, lasso_coef2, decimal=1)

def test_lasso_lars_vs_lasso_cd_ill_conditioned2():
    # Create an ill-conditioned situation in which the LARS has to go
    # far in the path to converge, and check that LARS and coordinate
    # descent give the same answers
    # Note it used to be the case that Lars had to use the drop for good
    # strategy for this but this is no longer the case with the
    # equality_tolerance checks
    X = [[1e20, 1e20, 0], [-1e-32, 0, 0], [1, 1, 1]]
    y = [10, 10, 1]

    def objective_function(coef):
        return 1.0 / (2.0 * len(X)) * linalg.norm(
            y - np.dot(X, coef)
        ) ** 2 + alpha * linalg.norm(coef, 1)

    lars = linear_model.LassoLars(alpha=alpha)
    warning_message = "Regressors in active set degenerate."
        lars.fit(X, y)
    lars_coef_ = lars.coef_
    lars_obj = objective_function(lars_coef_)

    coord_descent = linear_model.Lasso(alpha=alpha, tol=1e-4)
    cd_coef_ = coord_descent.fit(X, y).coef_
    cd_obj = objective_function(cd_coef_)

    assert lars_obj < cd_obj * (1.0 + 1e-8)

def test_lars_add_features():
    # assure that at least some features get added if necessary
    # test for 6d2b4c
    # Hilbert matrix
    n = 5
    H = 1.0 / (np.arange(1, n + 1) + np.arange(n)[:, np.newaxis])
    clf = linear_model.Lars(fit_intercept=False).fit(H, np.arange(n))
    assert np.all(np.isfinite(clf.coef_))

def test_lars_n_nonzero_coefs(verbose=False):
    lars = linear_model.Lars(n_nonzero_coefs=6, verbose=verbose)
    lars.fit(X, y)
    assert len(lars.coef_.nonzero()[0]) == 6
    # The path should be of length 6 + 1 in a Lars going down to 6
    # non-zero coefs
    assert len(lars.alphas_) == 7

def test_multitarget():
    # Assure that estimators receiving multidimensional y do the right thing
    Y = np.vstack([y, y**2]).T
    n_targets = Y.shape[1]
        linear_model.LassoLars(),
        linear_model.Lars(),
        # regression test for gh-1615
        linear_model.LassoLars(fit_intercept=False),
        linear_model.Lars(fit_intercept=False),
    ]

    for estimator in estimators:
        estimator.fit(X, Y)
        Y_pred = estimator.predict(X)
        alphas, active, coef, path = (
            estimator.alphas_,
            estimator.active_,
            estimator.coef_,
            estimator.coef_path_,
        )
            estimator.fit(X, Y[:, k])
            assert_array_almost_equal(alphas[k], estimator.alphas_)
            assert_array_almost_equal(active[k], estimator.active_)
            assert_array_almost_equal(coef[k], estimator.coef_)
            assert_array_almost_equal(path[k], estimator.coef_path_)
            assert_array_almost_equal(Y_pred[:, k], y_pred)

def test_lars_cv():
    # Test the LassoLarsCV object by checking that the optimal alpha
    # increases as the number of samples increases.
    # This property is not actually guaranteed in general and is just a
    # property of the given dataset, with the given steps chosen.
    old_alpha = 0
    lars_cv = linear_model.LassoLarsCV()
    for length in (400, 200, 100):
        X = diabetes.data[:length]
        y = diabetes.target[:length]
        lars_cv.fit(X, y)
        np.testing.assert_array_less(old_alpha, lars_cv.alpha_)
        old_alpha = lars_cv.alpha_
    assert not hasattr(lars_cv, "n_nonzero_coefs")

def test_lars_cv_max_iter(recwarn):
    warnings.simplefilter("always")
    with np.errstate(divide="raise", invalid="raise"):
        X = diabetes.data
        y = diabetes.target
        x = rng.randn(len(y))
        X = np.c_[X, x, x]  # add correlated features
        X = StandardScaler().fit_transform(X)
        lars_cv = linear_model.LassoLarsCV(max_iter=5, cv=5)

    # Check that there is no warning in general and no ConvergenceWarning
    # in particular.
    # Materialize the string representation of the warning to get a more
    # informative error message in case of AssertionError.
    recorded_warnings = [str(w) for w in recwarn]
    assert len(recorded_warnings) == 0

def test_lasso_lars_ic():
    # Test the LassoLarsIC object by checking that
    # - some good features are selected.
    # - alpha_bic > alpha_aic
    # - n_nonzero_bic < n_nonzero_aic
    lars_bic = linear_model.LassoLarsIC("bic")
    lars_aic = linear_model.LassoLarsIC("aic")
    X = np.c_[X, rng.randn(X.shape[0], 5)]  # add 5 bad features
    lars_bic.fit(X, y)
    lars_aic.fit(X, y)
    nonzero_bic = np.where(lars_bic.coef_)[0]
    nonzero_aic = np.where(lars_aic.coef_)[0]
    assert lars_bic.alpha_ > lars_aic.alpha_
    assert len(nonzero_bic) < len(nonzero_aic)
    assert np.max(nonzero_bic) < diabetes.data.shape[1]

def test_lars_path_readonly_data():
    # When using automated memory mapping on large input, the
    # fold data is in read-only mode
    # This is a non-regression test for:
    # https://github.com/scikit-learn/scikit-learn/issues/4597
    splitted_data = train_test_split(X, y, random_state=42)
    with TempMemmap(splitted_data) as (X_train, X_test, y_train, y_test):
        # The following should not fail despite copy=False
        _lars_path_residues(X_train, y_train, X_test, y_test, copy=False)

def test_lars_path_positive_constraint():
    # this is the main test for the positive parameter on the lars_path method
    # the estimator classes just make use of this function

    # we do the test on the diabetes dataset

    # ensure that we get negative coefficients when positive=False
    # and all positive when positive=True
    # for method 'lar' (default) and lasso

    err_msg = "Positive constraint not supported for 'lar' coding method."
        linear_model.lars_path(
            diabetes["data"], diabetes["target"], method="lar", positive=True
        )

    _, _, coefs = linear_model.lars_path(
        X, y, return_path=True, method=method, positive=False
    )
    assert coefs.min() < 0

        X, y, return_path=True, method=method, positive=True
    )
    assert coefs.min() >= 0

def test_estimatorclasses_positive_constraint():
    # testing the transmissibility for the positive option of all estimator
    # classes in this same function here
    default_parameter = {"fit_intercept": False}

    estimator_parameter_map = {
        "LassoLars": {"alpha": 0.1},
        "LassoLarsCV": {},
        "LassoLarsIC": {},
    }
    for estname in estimator_parameter_map:
        params = default_parameter.copy()
        params.update(estimator_parameter_map[estname])
        estimator = getattr(linear_model, estname)(positive=False, **params)
        assert estimator.coef_.min() < 0
        estimator = getattr(linear_model, estname)(positive=True, **params)
        assert min(estimator.coef_) >= 0

def test_lasso_lars_vs_lasso_cd_positive():
    # same results when using the positive option

    # This test is basically a copy of the above with additional positive
    # option. However for the middle part, the comparison of coefficient values
    # for a range of alphas, we had to make an adaptations. See below.

    # not normalized data

    alphas, _, lasso_path = linear_model.lars_path(X, y, method="lasso", positive=True)
    lasso_cd = linear_model.Lasso(fit_intercept=False, tol=1e-8, positive=True)
        if a == 0:
            continue

    # The range of alphas chosen for coefficient comparison here is restricted
    # as compared with the above test without the positive option. This is due
    # to the circumstance that the Lars-Lasso algorithm does not converge to
    # the least-squares-solution for small alphas, see 'Least Angle Regression'
    # by Efron et al 2004. The coefficients are typically in congruence up to
    # the smallest alpha reached by the Lars-Lasso algorithm and start to
    # diverge thereafter.  See
    # https://gist.github.com/michigraber/7e7d7c75eca694c7a6ff

    for alpha in np.linspace(6e-1, 1 - 1e-2, 20):
        clf1 = linear_model.LassoLars(
            fit_intercept=False, alpha=alpha, positive=True
        clf2 = linear_model.Lasso(
            fit_intercept=False, alpha=alpha, tol=1e-8, positive=True

    # normalized data
    for c, a in zip(lasso_path.T[:-1], alphas[:-1]):  # don't include alpha=0

def test_lasso_lars_vs_R_implementation():
    # Test that sklearn LassoLars implementation agrees with the LassoLars
    # implementation available in R (lars library) when fit_intercept=False.

    # Let's generate the data used in the bug report 7778
    y = np.array([-6.45006793, -3.51251449, -8.52445396, 6.12277822, -19.42109366])
        [
            [0.47299829, 0, 0, 0, 0],
            [0.08239882, 0.85784863, 0, 0, 0],
            [0.30114139, -0.07501577, 0.80895216, 0, 0],
            [-0.01460346, -0.1015233, 0.0407278, 0.80338378, 0],
            [-0.69363927, 0.06754067, 0.18064514, -0.0803561, 0.40427291],
        ]
    )

    X = x.T

    # The R result was obtained using the following code:
    #
    # library(lars)
    # model_lasso_lars = lars(X, t(y), type="lasso", intercept=FALSE,
    #                         trace=TRUE, normalize=FALSE)
    # r = t(model_lasso_lars$beta)
    #

    r = np.array(
        [
            [
                0,
                0,
                0,
                0,
                0,
                -79.810362809499026,
                -83.528788732782829,
                -83.777653739190711,
                -83.784156932888934,
                -84.033390591756657,
            ],
            [0, 0, 0, 0, -0.476624256777266, 0, 0, 0, 0, 0.025219751009936],
            [
                0,
                -3.577397088285891,
                -4.702795355871871,
                -7.016748621359461,
                -7.614898471899412,
                -0.336938391359179,
                0,
                0,
                0.001213370600853,
                0.048162321585148,
            ],
            [
                0,
                0,
                0,
                2.231558436628169,
                2.723267514525966,
                2.811549786389614,
                2.813766976061531,
                2.817462468949557,
                2.817368178703816,
                2.816221090636795,
            ],
            [
                0,
                0,
                -1.218422599914637,
                -3.457726183014808,
                -4.021304522060710,
                -45.827461592423745,
                -47.776608869312305,
                -47.911561610746404,
                -47.914845922736234,
                -48.039562334265717,
            ],
        ]
    )

    model_lasso_lars = linear_model.LassoLars(alpha=0, fit_intercept=False)
    model_lasso_lars.fit(X, y)
    skl_betas = model_lasso_lars.coef_path_

    assert_array_almost_equal(r, skl_betas, decimal=12)

def test_lasso_lars_copyX_behaviour(copy_X):
    """
    Test that user input regarding copy_X is not being overridden (it was until
    at least version 0.21)

    """
    lasso_lars = LassoLarsIC(copy_X=copy_X, precompute=False)
    X = rng.normal(0, 1, (100, 5))
    y = X[:, 2]
    lasso_lars.fit(X, y)
    assert copy_X == np.array_equal(X, X_copy)

def test_lasso_lars_fit_copyX_behaviour(copy_X):
    """
    Test that user input to .fit for copy_X overrides default __init__ value

    """
    lasso_lars = LassoLarsIC(precompute=False)
    y = X[:, 2]
    lasso_lars.fit(X, y, copy_X=copy_X)

def test_lars_with_jitter(est):
    # Test that a small amount of jitter helps stability,
    # using example provided in issue #2746

    X = np.array([[0.0, 0.0, 0.0, -1.0, 0.0], [0.0, -1.0, 0.0, 0.0, 0.0]])
    y = [-2.5, -2.5]
    expected_coef = [0, 2.5, 0, 2.5, 0]

    # set to fit_intercept to False since target is constant and we want check
    # the value of coef. coef would be all zeros otherwise.
    est.set_params(fit_intercept=False)
    est_jitter = clone(est).set_params(jitter=10e-8, random_state=0)

    est_jitter.fit(X, y)

    assert np.mean((est.coef_ - est_jitter.coef_) ** 2) > 0.1
    np.testing.assert_allclose(est_jitter.coef_, expected_coef, rtol=1e-3)

def test_X_none_gram_not_none():
    with pytest.raises(ValueError, match="X cannot be None if Gram is not None"):
        lars_path(X=None, y=np.array([1]), Gram=True)

def test_copy_X_with_auto_gram():
    # Non-regression test for #17789, `copy_X=True` and Gram='auto' does not
    # overwrite X
    X = rng.rand(6, 6)
    y = rng.rand(6)

    X_before = X.copy()
    linear_model.lars_path(X, y, Gram="auto", copy_X=True, method="lasso")
    # X did not change
    assert_allclose(X, X_before)

def test_lars_dtype_match(LARS, has_coef_path, args, dtype):
    # The test ensures that the fit method preserves input dtype
    X = rng.rand(20, 6).astype(dtype)
    y = rng.rand(20).astype(dtype)

    model = LARS(**args)
    assert model.coef_.dtype == dtype
    if has_coef_path:
        assert model.coef_path_.dtype == dtype
    assert model.intercept_.dtype == dtype

def test_lars_numeric_consistency(LARS, has_coef_path, args):
    # The test ensures numerical consistency between trained coefficients
    # of float32 and float64.
    rtol = 1e-5
    atol = 1e-5

    X_64 = rng.rand(10, 6)
    y_64 = rng.rand(10)

    model_64 = LARS(**args).fit(X_64, y_64)
    model_32 = LARS(**args).fit(X_64.astype(np.float32), y_64.astype(np.float32))

    assert_allclose(model_64.coef_, model_32.coef_, rtol=rtol, atol=atol)
        assert_allclose(model_64.coef_path_, model_32.coef_path_, rtol=rtol, atol=atol)
    assert_allclose(model_64.intercept_, model_32.intercept_, rtol=rtol, atol=atol)

def test_lassolarsic_alpha_selection(criterion):
    """Check that we properly compute the AIC and BIC score.

    In this test, we reproduce the example of the Fig. 2 of Zou et al.
    (reference [1] in LassoLarsIC) In this example, only 7 features should be
    selected.
    """
    model = make_pipeline(StandardScaler(), LassoLarsIC(criterion=criterion))

    best_alpha_selected = np.argmin(model[-1].criterion_)
    assert best_alpha_selected == 7

def test_lassolarsic_noise_variance(fit_intercept):
    """Check the behaviour when `n_samples` < `n_features` and that one needs
    to provide the noise variance."""
        n_samples=10, n_features=11 - fit_intercept, random_state=rng
    )

    model = make_pipeline(StandardScaler(), LassoLarsIC(fit_intercept=fit_intercept))

    err_msg = (
        "You are using LassoLarsIC in the case where the number of samples is smaller"
        " than the number of features"
    )

    model.set_params(lassolarsic__noise_variance=1.0)
    model.fit(X, y).predict(X)

def test_bayesian_ridge_scores():
    """Check scores attribute shape"""

    clf = BayesianRidge(compute_score=True)

    assert clf.scores_.shape == (clf.n_iter_ + 1,)

def test_bayesian_ridge_score_values():
    """Check value of score on toy example.

    Compute log marginal likelihood with equation (36) in Sparse Bayesian
    Learning and the Relevance Vector Machine (Tipping, 2001):

    - 0.5 * (log |Id/alpha + X.X^T/lambda| +
             y^T.(Id/alpha + X.X^T/lambda).y + n * log(2 * pi))
    + lambda_1 * log(lambda) - lambda_2 * lambda
    + alpha_1 * log(alpha) - alpha_2 * alpha

    and check equality with the score computed during training.
    """

    # check with initial values of alpha and lambda (see code for the values)
    alpha_ = 1.0 / (np.var(y) + eps)
    lambda_ = 1.0

    # value of the parameters of the Gamma hyperpriors
    alpha_1 = 0.1
    alpha_2 = 0.1
    lambda_1 = 0.1
    lambda_2 = 0.1

    # compute score using formula of docstring
    score = lambda_1 * log(lambda_) - lambda_2 * lambda_
    score += alpha_1 * log(alpha_) - alpha_2 * alpha_
    M = 1.0 / alpha_ * np.eye(n_samples) + 1.0 / lambda_ * np.dot(X, X.T)
    M_inv_dot_y = np.linalg.solve(M, y)
    score += -0.5 * (
        fast_logdet(M) + np.dot(y.T, M_inv_dot_y) + n_samples * log(2 * np.pi)
    )

    # compute score with BayesianRidge
    clf = BayesianRidge(
        alpha_1=alpha_1,
        alpha_2=alpha_2,
        lambda_1=lambda_1,
        lambda_2=lambda_2,
        compute_score=True,
    )

    assert_almost_equal(clf.scores_[0], score, decimal=9)

def test_bayesian_ridge_parameter():
    # Test correctness of lambda_ and alpha_ parameters (GitHub issue #8224)
    X = np.array([[1, 1], [3, 4], [5, 7], [4, 1], [2, 6], [3, 10], [3, 2]])
    y = np.array([1, 2, 3, 2, 0, 4, 5]).T

    # A Ridge regression model using an alpha value equal to the ratio of
    # lambda_ and alpha_ from the Bayesian Ridge model must be identical
    br_model = BayesianRidge(compute_score=True).fit(X, y)
    rr_model = Ridge(alpha=br_model.lambda_ / br_model.alpha_).fit(X, y)
    assert_array_almost_equal(rr_model.coef_, br_model.coef_)
    assert_almost_equal(rr_model.intercept_, br_model.intercept_)

def test_bayesian_covariance_matrix(n_samples, n_features, global_random_seed):
    """Check the posterior covariance matrix sigma_

    Non-regression test for https://github.com/scikit-learn/scikit-learn/issues/31093
    """
        n_samples, n_features, random_state=global_random_seed
    )
    reg = BayesianRidge(fit_intercept=False).fit(X, y)
    covariance_matrix = np.linalg.inv(
        reg.lambda_ * np.identity(n_features) + reg.alpha_ * np.dot(X.T, X)
    )
    assert_allclose(reg.sigma_, covariance_matrix, rtol=1e-6)

def test_bayesian_sample_weights():
    # Test correctness of the sample_weights method
    w = np.array([4, 3, 3, 1, 1, 2, 3]).T

    br_model = BayesianRidge(compute_score=True).fit(X, y, sample_weight=w)
    rr_model = Ridge(alpha=br_model.lambda_ / br_model.alpha_).fit(
        X, y, sample_weight=w
    )

def test_toy_bayesian_ridge_object():
    # Test BayesianRidge on toy
    X = np.array([[1], [2], [6], [8], [10]])
    Y = np.array([1, 2, 6, 8, 10])

    # Check that the model could approximately learn the identity function
    test = [[1], [3], [4]]
    assert_array_almost_equal(clf.predict(test), [1, 3, 4], 2)

def test_bayesian_initial_params():
    # Test BayesianRidge with initial values (alpha_init, lambda_init)
    X = np.vander(np.linspace(0, 4, 5), 4)
    y = np.array([0.0, 1.0, 0.0, -1.0, 0.0])  # y = (x^3 - 6x^2 + 8x) / 3

    # In this case, starting from the default initial values will increase
    # the bias of the fitted curve. So, lambda_init should be small.
    reg = BayesianRidge(alpha_init=1.0, lambda_init=1e-3)
    # Check the R2 score nearly equals to one.
    r2 = reg.fit(X, y).score(X, y)
    assert_almost_equal(r2, 1.0)

def test_prediction_bayesian_ridge_ard_with_constant_input():
    # Test BayesianRidge and ARDRegression predictions for edge case of
    # constant target vectors
    n_samples = 4
    constant_value = random_state.rand()
    X = random_state.random_sample((n_samples, n_features))
    y = np.full(n_samples, constant_value, dtype=np.array(constant_value).dtype)
    expected = np.full(n_samples, constant_value, dtype=np.array(constant_value).dtype)

    for clf in [BayesianRidge(), ARDRegression()]:
        assert_array_almost_equal(y_pred, expected)

def test_std_bayesian_ridge_ard_with_constant_input():
    # Test BayesianRidge and ARDRegression standard dev. for edge case of
    # constant target vector
    # The standard dev. should be relatively small (< 0.01 is tested here)
    expected_upper_boundary = 0.01

        _, y_std = clf.fit(X, y).predict(X, return_std=True)
        assert_array_less(y_std, expected_upper_boundary)

def test_update_of_sigma_in_ard():
    # Checks that `sigma_` is updated correctly after the last iteration
    # of the ARDRegression algorithm. See issue #10128.
    X = np.array([[1, 0], [0, 0]])
    y = np.array([0, 0])
    clf = ARDRegression(max_iter=1)
    # With the inputs above, ARDRegression prunes both of the two coefficients
    # in the first iteration. Hence, the expected shape of `sigma_` is (0, 0).
    assert clf.sigma_.shape == (0, 0)
    # Ensure that no error is thrown at prediction stage
    clf.predict(X, return_std=True)

def test_toy_ard_object():
    # Test BayesianRegression ARD classifier
    X = np.array([[1], [2], [3]])
    Y = np.array([1, 2, 3])
    clf = ARDRegression(compute_score=True)

def test_ard_accuracy_on_easy_problem(global_random_seed, n_samples, n_features):
    # Check that ARD converges with reasonable accuracy on an easy problem
    # (Github issue #14055)
    X = np.random.RandomState(global_random_seed).normal(size=(250, 3))
    y = X[:, 1]

    regressor = ARDRegression()

    abs_coef_error = np.abs(1 - regressor.coef_[1])
    assert abs_coef_error < 1e-10

def test_return_std(constructor_name):
    # Test return_std option for both Bayesian regressors
    def f(X):
        return np.dot(X, w) + b

    def f_noise(X, noise_mult):
        return f(X) + np.random.randn(X.shape[0]) * noise_mult

    d = 5
    n_train = 50
    n_test = 10

    w = np.array([1.0, 0.0, 1.0, -1.0, 0.0])
    b = 1.0

    X = np.random.random((n_train, d))
    X = _convert_container(X, constructor_name)

    X_test = np.random.random((n_test, d))
    X_test = _convert_container(X_test, constructor_name)

    for decimal, noise_mult in enumerate([1, 0.1, 0.01]):
        y = f_noise(X, noise_mult)

        m1 = BayesianRidge()
        m1.fit(X, y)
        y_mean1, y_std1 = m1.predict(X_test, return_std=True)
        assert_array_almost_equal(y_std1, noise_mult, decimal=decimal)

        m2 = ARDRegression()
        m2.fit(X, y)
        y_mean2, y_std2 = m2.predict(X_test, return_std=True)
        assert_array_almost_equal(y_std2, noise_mult, decimal=decimal)

def test_update_sigma(global_random_seed):
    # make sure the two update_sigma() helpers are equivalent. The woodbury
    # formula is used when n_samples < n_features, and the other one is used
    # otherwise.

    # set n_samples == n_features to avoid instability issues when inverting
    # the matrices. Using the woodbury formula would be unstable when
    # n_samples > n_features
    n_samples = n_features = 10
    alpha = 1
    lmbda = np.arange(1, n_features + 1)
    keep_lambda = np.array([True] * n_features)

    reg = ARDRegression()

    sigma = reg._update_sigma(X, alpha, lmbda, keep_lambda)
    sigma_woodbury = reg._update_sigma_woodbury(X, alpha, lmbda, keep_lambda)

    np.testing.assert_allclose(sigma, sigma_woodbury)

def test_dtype_match(dtype, Estimator):
    X = np.array([[1, 1], [3, 4], [5, 7], [4, 1], [2, 6], [3, 10], [3, 2]], dtype=dtype)

    model = Estimator()
    # check type consistency
    attributes = ["coef_", "sigma_"]
    for attribute in attributes:
        assert getattr(model, attribute).dtype == X.dtype

    y_mean, y_std = model.predict(X, return_std=True)
    assert y_mean.dtype == X.dtype
    assert y_std.dtype == X.dtype

def test_dtype_correctness(Estimator):
    coef_32 = model.fit(X.astype(np.float32), y).coef_
    coef_64 = model.fit(X.astype(np.float64), y).coef_
    np.testing.assert_allclose(coef_32, coef_64, rtol=1e-4)

def test_sparse_coef():
    # Check that the sparse_coef property works
    clf = ElasticNet()
    clf.coef_ = [1, 2, 3]

    assert sp.issparse(clf.sparse_coef_)
    assert clf.sparse_coef_.toarray().tolist()[0] == clf.coef_

def test_lasso_zero(csc_container):
    # Check that the sparse lasso can handle zero data without crashing
    X = csc_container((3, 1))
    y = [0, 0, 0]
    T = np.array([[1], [2], [3]])
    clf = Lasso().fit(X, y)

def test_enet_toy_list_input(with_sample_weight, csc_container):
    # Test ElasticNet for various values of alpha and l1_ratio with list X

        sw = np.array([2.0, 2, 2])
    else:
        sw = None

    # this should be the same as unregularized least squares
    clf = ElasticNet(alpha=0, l1_ratio=1.0)
    # catch warning about alpha=0.
    # this is discouraged but should work.
    ignore_warnings(clf.fit)(X, Y, sample_weight=sw)

    clf = ElasticNet(alpha=0.5, l1_ratio=0.3)
    clf.fit(X, Y, sample_weight=sw)

def test_enet_toy_explicit_sparse_input(lil_container):
    # Test ElasticNet for various values of alpha and l1_ratio with sparse X
    # training samples
    X = lil_container((3, 1))
    X[0, 0] = -1
    # X[1, 0] = 0
    X[2, 0] = 1
    Y = [-1, 0, 1]  # just a straight line (the identity function)

    # test samples
    T = lil_container((3, 1))
    T[0, 0] = 2
    T[1, 0] = 3
    T[2, 0] = 4

    f(clf.fit)(X, Y)

def make_sparse_data(
    sparse_container,
    seed=42,
):

    # build an ill-posed linear regression problem with many noisy features and
    # comparatively few samples

    # generate a ground truth model
    w = random_state.randn(n_features, n_targets)
    w[n_informative:] = 0.0  # only the top features are impacting the model
        w = np.abs(w)

    rnd = random_state.uniform(size=(n_samples, n_features))
    X[rnd > 0.5] = 0.0  # 50% of zeros in input signal

    # generate training ground truth labels
        y = np.ravel(y)
    return X, y

def test_sparse_enet_not_as_toy_dataset(csc_container, alpha, fit_intercept, positive):
    n_samples, n_features, max_iter = 100, 100, 1000
    n_informative = 10

    X, y = make_sparse_data(
        csc_container, n_samples, n_features, n_informative, positive=positive
    )

    X_train, X_test = X[n_samples // 2 :], X[: n_samples // 2]
    y_train, y_test = y[n_samples // 2 :], y[: n_samples // 2]

    s_clf = ElasticNet(
        l1_ratio=0.8,
        tol=1e-7,
    )
    s_clf.fit(X_train, y_train)

    assert_almost_equal(s_clf.dual_gap_, 0, 4)
    assert s_clf.score(X_test, y_test) > 0.85

    # check the convergence is the same as the dense version
    d_clf = ElasticNet(
        tol=1e-7,
    )
    d_clf.fit(X_train.toarray(), y_train)

    assert_almost_equal(d_clf.dual_gap_, 0, 4)
    assert d_clf.score(X_test, y_test) > 0.85

    assert_almost_equal(s_clf.coef_, d_clf.coef_, 5)
    assert_almost_equal(s_clf.intercept_, d_clf.intercept_, 5)

    # check that the coefs are sparse
    assert np.sum(s_clf.coef_ != 0.0) < 2 * n_informative

def test_sparse_lasso_not_as_toy_dataset(csc_container):
        csc_container, n_samples=n_samples, n_informative=n_informative
    )

    s_clf = Lasso(alpha=0.1, fit_intercept=False, max_iter=max_iter, tol=1e-7)

    d_clf = Lasso(alpha=0.1, fit_intercept=False, max_iter=max_iter, tol=1e-7)

    assert np.sum(s_clf.coef_ != 0.0) == n_informative

def test_enet_multitarget(csc_container):
    X, y = make_sparse_data(csc_container, n_targets=n_targets)

    estimator = ElasticNet(alpha=0.01, precompute=False)
    # XXX: There is a bug when precompute is not False!
    )

def test_path_parameters(csc_container):
    X, y = make_sparse_data(csc_container)
    n_alphas = 10
        eps=1e-3,
    )
    ignore_warnings(clf.fit)(X, y)  # new params
    assert n_alphas == clf.n_alphas
    assert n_alphas == len(clf.alphas_)
    sparse_mse_path = clf.mse_path_
    ignore_warnings(clf.fit)(X.toarray(), y)  # compare with dense data
    assert_almost_equal(clf.mse_path_, sparse_mse_path)

def test_sparse_dense_equality(
    Model, fit_intercept, n_samples, n_features, with_sample_weight, csc_container
):
        effective_rank=n_features // 2,
        n_informative=n_features // 2,
        bias=4 * fit_intercept,
        noise=1,
    )
        sw = np.abs(np.random.RandomState(42).normal(scale=10, size=y.shape))
    else:
        sw = None
    Xs = csc_container(X)
    params = {"fit_intercept": fit_intercept}
    reg_dense = Model(**params).fit(X, y, sample_weight=sw)
    reg_sparse = Model(**params).fit(Xs, y, sample_weight=sw)
        assert reg_sparse.intercept_ == pytest.approx(reg_dense.intercept_)
        # balance property
        assert np.average(reg_sparse.predict(X), weights=sw) == pytest.approx(
            np.average(y, weights=sw)
        )
    assert_allclose(reg_sparse.coef_, reg_dense.coef_)

def test_same_output_sparse_dense_lasso_and_enet_cv(csc_container):
    X, y = make_sparse_data(csc_container, n_samples=40, n_features=10)
    clfs = ElasticNetCV(max_iter=100)
    clfs.fit(X, y)
    clfd = ElasticNetCV(max_iter=100)
    clfd.fit(X.toarray(), y)
    assert_almost_equal(clfs.alpha_, clfd.alpha_, 7)
    assert_almost_equal(clfs.intercept_, clfd.intercept_, 7)
    assert_array_almost_equal(clfs.mse_path_, clfd.mse_path_)
    assert_array_almost_equal(clfs.alphas_, clfd.alphas_)

    clfs = LassoCV(max_iter=100, cv=4)
    clfd = LassoCV(max_iter=100, cv=4)

def test_same_multiple_output_sparse_dense(coo_container):
    l = ElasticNet()
    X = [
        [0, 1, 2, 3, 4],
        [0, 2, 5, 8, 11],
        [9, 10, 11, 12, 13],
        [10, 11, 12, 13, 14],
    ]
    y = [
        [1, 2, 3, 4, 5],
        [1, 3, 6, 9, 12],
        [11, 12, 13, 14, 15],
    ]
    l.fit(X, y)
    sample = np.array([1, 2, 3, 4, 5]).reshape(1, -1)
    predict_dense = l.predict(sample)

    l_sp = ElasticNet()
    X_sp = coo_container(X)
    l_sp.fit(X_sp, y)
    sample_sparse = coo_container(sample)
    predict_sparse = l_sp.predict(sample_sparse)

    assert_array_almost_equal(predict_sparse, predict_dense)

def test_sparse_enet_coordinate_descent(csc_container):
    clf = Lasso(max_iter=2)
    X = csc_container((n_samples, n_features)) * 1e50
    y = np.ones(n_samples)
        "Objective did not converge. You might want "
        "to increase the number of iterations."
    )

def test_sparse_read_only_buffer(copy_X):

    clf = ElasticNet(alpha=0.1, copy_X=copy_X, random_state=rng)
    X = sp.random(100, 20, format="csc", random_state=rng)

    # Make X.data read-only
    X.data = create_memmap_backed_data(X.data)

def test_ransac_inliers_outliers():
    ransac_estimator = RANSACRegressor(
        estimator, min_samples=2, residual_threshold=5, random_state=0
    )

    # Estimate parameters of corrupted data
    ransac_estimator.fit(X, y)

    # Ground truth / reference inlier mask
    ref_inlier_mask = np.ones_like(ransac_estimator.inlier_mask_).astype(np.bool_)
    ref_inlier_mask[outliers] = False

    assert_array_equal(ransac_estimator.inlier_mask_, ref_inlier_mask)

def test_ransac_is_data_valid():
    def is_data_valid(X, y):
        assert X.shape[0] == 2
        assert y.shape[0] == 2

    y = rng.rand(10, 1)

        min_samples=2,
        residual_threshold=5,
        is_data_valid=is_data_valid,
    )
        ransac_estimator.fit(X, y)

def test_ransac_is_model_valid():
    def is_model_valid(estimator, X, y):

        is_model_valid=is_model_valid,
    )

def test_ransac_max_trials():

        max_trials=0,
    )

    # there is a 1e-9 chance it will take these many trials. No good reason
    # 1e-2 isn't enough, can still happen
    # 2 is the what ransac defines  as min_samples = X.shape[1] + 1
    max_trials = _dynamic_max_trials(len(X) - len(outliers), X.shape[0], 2, 1 - 1e-9)
    ransac_estimator = RANSACRegressor(estimator, min_samples=2)
    for i in range(50):
        ransac_estimator.set_params(min_samples=2, random_state=i)
        assert ransac_estimator.n_trials_ < max_trials + 1

def test_ransac_stop_n_inliers():
        stop_n_inliers=2,
    )

    assert ransac_estimator.n_trials_ == 1

def test_ransac_stop_score():
        stop_score=0,
    )

def test_ransac_score():
    X = np.arange(100)[:, None]
    y = np.zeros((100,))
    y[0] = 1
    y[1] = 100

        estimator, min_samples=2, residual_threshold=0.5, random_state=0
    )

    assert ransac_estimator.score(X[2:], y[2:]) == 1
    assert ransac_estimator.score(X[:2], y[:2]) < 1

def test_ransac_predict():
    y[0] = 1
    y[1] = 100

    )

    assert_array_equal(ransac_estimator.predict(X), np.zeros(100))

def test_ransac_no_valid_data():

        estimator, is_data_valid=is_data_valid, max_trials=5
    )

    msg = "RANSAC could not find a valid consensus set"
    assert ransac_estimator.n_skips_no_inliers_ == 0
    assert ransac_estimator.n_skips_invalid_data_ == 5
    assert ransac_estimator.n_skips_invalid_model_ == 0

def test_ransac_no_valid_model():

        estimator, is_model_valid=is_model_valid, max_trials=5
    )

    assert ransac_estimator.n_skips_invalid_data_ == 0
    assert ransac_estimator.n_skips_invalid_model_ == 5

def test_ransac_exceed_max_skips():

        estimator, is_data_valid=is_data_valid, max_trials=5, max_skips=3
    )

    msg = "RANSAC skipped more iterations than `max_skips`"
    assert ransac_estimator.n_skips_invalid_data_ == 4

def test_ransac_warn_exceed_max_skips():
    global cause_skip
    cause_skip = False

        global cause_skip
        if not cause_skip:
            cause_skip = True
        else:

        estimator, is_data_valid=is_data_valid, max_skips=3, max_trials=5
    )
        "RANSAC found a valid consensus set but exited "
        "early due to skipping more iterations than "
        "`max_skips`. See estimator attributes for "
        "diagnostics."
    )

def test_ransac_sparse(sparse_container):

    )
    ransac_estimator.fit(X_sparse, y)

def test_ransac_none_estimator():

    )
    ransac_none_estimator = RANSACRegressor(
        None, min_samples=2, residual_threshold=5, random_state=0
    )

    ransac_none_estimator.fit(X, y)

        ransac_estimator.predict(X), ransac_none_estimator.predict(X)
    )

def test_ransac_min_n_samples():
    ransac_estimator1 = RANSACRegressor(
    )
    ransac_estimator2 = RANSACRegressor(
        min_samples=2.0 / X.shape[0],
    )
    ransac_estimator5 = RANSACRegressor(
    )
    ransac_estimator6 = RANSACRegressor(estimator, residual_threshold=5, random_state=0)
    ransac_estimator7 = RANSACRegressor(
        estimator, min_samples=X.shape[0] + 1, residual_threshold=5, random_state=0
    )
    # GH #19390
    ransac_estimator8 = RANSACRegressor(
        Ridge(), min_samples=None, residual_threshold=5, random_state=0
    )

    ransac_estimator1.fit(X, y)
    ransac_estimator2.fit(X, y)
    ransac_estimator5.fit(X, y)
    ransac_estimator6.fit(X, y)

        ransac_estimator1.predict(X), ransac_estimator2.predict(X)
    )
        ransac_estimator1.predict(X), ransac_estimator5.predict(X)
    )
        ransac_estimator1.predict(X), ransac_estimator6.predict(X)
    )

        ransac_estimator7.fit(X, y)

    err_msg = "`min_samples` needs to be explicitly set"
        ransac_estimator8.fit(X, y)

def test_ransac_multi_dimensional_targets():
    )

    # 3-D target values
    yyy = np.column_stack([y, y, y])

    ransac_estimator.fit(X, yyy)

def test_ransac_residual_loss():
    def loss_multi1(y_true, y_pred):
        return np.sum(np.abs(y_true - y_pred), axis=1)

    def loss_multi2(y_true, y_pred):
        return np.sum((y_true - y_pred) ** 2, axis=1)

    def loss_mono(y_true, y_pred):
        return np.abs(y_true - y_pred)

    ransac_estimator0 = RANSACRegressor(
    )
        loss=loss_multi1,
    )
        loss=loss_multi2,
    )

    # multi-dimensional
    ransac_estimator0.fit(X, yyy)
    ransac_estimator1.fit(X, yyy)
    ransac_estimator2.fit(X, yyy)
        ransac_estimator0.predict(X), ransac_estimator1.predict(X)
    )
        ransac_estimator0.predict(X), ransac_estimator2.predict(X)
    )

    # one-dimensional
    ransac_estimator0.fit(X, y)
    ransac_estimator2.loss = loss_mono
    )
    ransac_estimator3 = RANSACRegressor(
    )
    ransac_estimator3.fit(X, y)
    )

def test_ransac_default_residual_threshold():
    ransac_estimator = RANSACRegressor(estimator, min_samples=2, random_state=0)

def test_ransac_dynamic_max_trials():
    # Numbers hand-calculated and confirmed on page 119 (Table 4.3) in
    #   Hartley, R.~I. and Zisserman, A., 2004,
    #   Multiple View Geometry in Computer Vision, Second Edition,
    #   Cambridge University Press, ISBN: 0521540518

    # e = 0%, min_samples = X
    assert _dynamic_max_trials(100, 100, 2, 0.99) == 1

    # e = 5%, min_samples = 2
    assert _dynamic_max_trials(95, 100, 2, 0.99) == 2
    # e = 10%, min_samples = 2
    assert _dynamic_max_trials(90, 100, 2, 0.99) == 3
    # e = 30%, min_samples = 2
    assert _dynamic_max_trials(70, 100, 2, 0.99) == 7
    # e = 50%, min_samples = 2
    assert _dynamic_max_trials(50, 100, 2, 0.99) == 17

    # e = 5%, min_samples = 8
    assert _dynamic_max_trials(95, 100, 8, 0.99) == 5
    # e = 10%, min_samples = 8
    assert _dynamic_max_trials(90, 100, 8, 0.99) == 9
    # e = 30%, min_samples = 8
    assert _dynamic_max_trials(70, 100, 8, 0.99) == 78
    # e = 50%, min_samples = 8
    assert _dynamic_max_trials(50, 100, 8, 0.99) == 1177

    # e = 0%, min_samples = 10
    assert _dynamic_max_trials(1, 100, 10, 0) == 0
    assert _dynamic_max_trials(1, 100, 10, 1) == float("inf")

def test_ransac_fit_sample_weight():
    ransac_estimator = RANSACRegressor(random_state=0)
    n_samples = y.shape[0]
    weights = np.ones(n_samples)
    ransac_estimator.fit(X, y, sample_weight=weights)
    assert ransac_estimator.inlier_mask_.shape[0] == n_samples

    # check that mask is correct

    #   X = X1 repeated n1 times, X2 repeated n2 times and so forth
    X_ = random_state.randint(0, 200, [10, 1])
    y_ = np.ndarray.flatten(0.2 * X_ + 2)
    sample_weight = random_state.randint(0, 10, 10)
    outlier_X = random_state.randint(0, 1000, [1, 1])
    outlier_weight = random_state.randint(0, 10, 1)
    outlier_y = random_state.randint(-1000, 0, 1)

    X_flat = np.append(
        np.repeat(X_, sample_weight, axis=0),
        np.repeat(outlier_X, outlier_weight, axis=0),
        axis=0,
    )
    y_flat = np.ndarray.flatten(
        np.append(
            np.repeat(y_, sample_weight, axis=0),
            np.repeat(outlier_y, outlier_weight, axis=0),
            axis=0,
        )
    )
    ransac_estimator.fit(X_flat, y_flat)
    ref_coef_ = ransac_estimator.estimator_.coef_

    sample_weight = np.append(sample_weight, outlier_weight)
    X_ = np.append(X_, outlier_X, axis=0)
    y_ = np.append(y_, outlier_y)
    ransac_estimator.fit(X_, y_, sample_weight=sample_weight)

    assert_allclose(ransac_estimator.estimator_.coef_, ref_coef_)

    # check that if estimator.fit doesn't support
    # sample_weight, raises error
    estimator = OrthogonalMatchingPursuit()
    ransac_estimator = RANSACRegressor(estimator, min_samples=10)

    err_msg = f"{estimator.__class__.__name__} does not support sample_weight."
        ransac_estimator.fit(X, y, sample_weight=weights)

def test_ransac_final_model_fit_sample_weight():
    X, y = make_regression(n_samples=1000, random_state=10)
    sample_weight = rng.randint(1, 4, size=y.shape[0])
    sample_weight = sample_weight / sample_weight.sum()
    ransac = RANSACRegressor(random_state=0)
    ransac.fit(X, y, sample_weight=sample_weight)

    final_model = LinearRegression()
    mask_samples = ransac.inlier_mask_
    final_model.fit(
        X[mask_samples], y[mask_samples], sample_weight=sample_weight[mask_samples]
    )

    assert_allclose(ransac.estimator_.coef_, final_model.coef_, atol=1e-12)

def test_perfect_horizontal_line():
    """Check that we can fit a line where all samples are inliers.
    https://github.com/scikit-learn/scikit-learn/issues/19497
    """

    ransac_estimator = RANSACRegressor(estimator, random_state=0)

    assert_allclose(ransac_estimator.estimator_.coef_, 0.0)
    assert_allclose(ransac_estimator.estimator_.intercept_, 0.0)

        else:

    assert orthogonal_mp(X, y[:, 0], n_nonzero_coefs=5).shape == (n_features,)
    assert orthogonal_mp(X, y, n_nonzero_coefs=5).shape == (n_features, 3)

def test_correct_shapes_gram():
    assert orthogonal_mp_gram(G, Xy[:, 0], n_nonzero_coefs=5).shape == (n_features,)
    assert orthogonal_mp_gram(G, Xy, n_nonzero_coefs=5).shape == (n_features, 3)

def test_n_nonzero_coefs():
    assert np.count_nonzero(orthogonal_mp(X, y[:, 0], n_nonzero_coefs=5)) <= 5
    assert (
        np.count_nonzero(orthogonal_mp(X, y[:, 0], n_nonzero_coefs=5, precompute=True))
        <= 5
    )

def test_tol():
    tol = 0.5
    gamma = orthogonal_mp(X, y[:, 0], tol=tol)
    gamma_gram = orthogonal_mp(X, y[:, 0], tol=tol, precompute=True)
    assert np.sum((y[:, 0] - np.dot(X, gamma)) ** 2) <= tol
    assert np.sum((y[:, 0] - np.dot(X, gamma_gram)) ** 2) <= tol

def test_with_without_gram():
        orthogonal_mp(X, y, n_nonzero_coefs=5),
        orthogonal_mp(X, y, n_nonzero_coefs=5, precompute=True),
    )

def test_with_without_gram_tol():
        orthogonal_mp(X, y, tol=1.0), orthogonal_mp(X, y, tol=1.0, precompute=True)
    )

def test_unreachable_accuracy():
        orthogonal_mp(X, y, tol=0), orthogonal_mp(X, y, n_nonzero_coefs=n_features)
    )
        "Orthogonal matching pursuit ended prematurely "
        "due to linear dependence in the dictionary. "
        "The requested precision might not have been met."
    )
    with pytest.warns(RuntimeWarning, match=warning_message):
            orthogonal_mp(X, y, tol=0, precompute=True),
            orthogonal_mp(X, y, precompute=True, n_nonzero_coefs=n_features),
        )

def test_bad_input(positional_params, keyword_params):
        orthogonal_mp(*positional_params, **keyword_params)

def test_perfect_signal_recovery():
    (idx,) = gamma[:, 0].nonzero()
    gamma_rec = orthogonal_mp(X, y[:, 0], n_nonzero_coefs=5)
    gamma_gram = orthogonal_mp_gram(G, Xy[:, 0], n_nonzero_coefs=5)
    assert_array_equal(idx, np.flatnonzero(gamma_rec))
    assert_array_equal(idx, np.flatnonzero(gamma_gram))
    assert_array_almost_equal(gamma[:, 0], gamma_rec, decimal=2)
    assert_array_almost_equal(gamma[:, 0], gamma_gram, decimal=2)

def test_orthogonal_mp_gram_readonly():
    G_readonly = G.copy()
    G_readonly.setflags(write=False)
    Xy_readonly = Xy.copy()
    Xy_readonly.setflags(write=False)
    gamma_gram = orthogonal_mp_gram(
        G_readonly, Xy_readonly[:, 0], n_nonzero_coefs=5, copy_Gram=False, copy_Xy=False
    )

    omp = OrthogonalMatchingPursuit(n_nonzero_coefs=n_nonzero_coefs)
    omp.fit(X, y[:, 0])
    assert omp.coef_.shape == (n_features,)
    assert omp.intercept_.shape == ()
    assert np.count_nonzero(omp.coef_) <= n_nonzero_coefs

    omp.fit(X, y)
    assert omp.coef_.shape == (n_targets, n_features)
    assert omp.intercept_.shape == (n_targets,)
    assert np.count_nonzero(omp.coef_) <= n_targets * n_nonzero_coefs

    coef_normalized = omp.coef_[0].copy()
    omp.set_params(fit_intercept=True)
    assert_array_almost_equal(coef_normalized, omp.coef_)

    omp.set_params(fit_intercept=False)
    assert omp.intercept_ == 0

def test_estimator_n_nonzero_coefs():
    """Check `n_nonzero_coefs_` correct when `tol` is and isn't set."""
    assert omp.n_nonzero_coefs_ == n_nonzero_coefs

    omp = OrthogonalMatchingPursuit(n_nonzero_coefs=n_nonzero_coefs, tol=0.5)
    assert omp.n_nonzero_coefs_ is None

def test_identical_regressors():
    newX = X.copy()
    newX[:, 1] = newX[:, 0]
    gamma = np.zeros(n_features)
    gamma[0] = gamma[1] = 1.0
    newy = np.dot(newX, gamma)
    )
        orthogonal_mp(newX, newy, n_nonzero_coefs=2)

def test_swapped_regressors():
    # X[:, 21] should be selected first, then X[:, 0] selected second,
    # which will take X[:, 21]'s place in case the algorithm does
    # column swapping for optimization (which is the case at the moment)
    gamma[21] = 1.0
    gamma[0] = 0.5
    new_y = np.dot(X, gamma)
    new_Xy = np.dot(X.T, new_y)
    gamma_hat = orthogonal_mp(X, new_y, n_nonzero_coefs=2)
    gamma_hat_gram = orthogonal_mp_gram(G, new_Xy, n_nonzero_coefs=2)
    assert_array_equal(np.flatnonzero(gamma_hat), [0, 21])
    assert_array_equal(np.flatnonzero(gamma_hat_gram), [0, 21])

def test_no_atoms():
    y_empty = np.zeros_like(y)
    Xy_empty = np.dot(X.T, y_empty)
    gamma_empty = ignore_warnings(orthogonal_mp)(X, y_empty, n_nonzero_coefs=1)
    gamma_empty_gram = ignore_warnings(orthogonal_mp)(G, Xy_empty, n_nonzero_coefs=1)
    assert np.all(gamma_empty == 0)
    assert np.all(gamma_empty_gram == 0)

def test_omp_path():
    path = orthogonal_mp(X, y, n_nonzero_coefs=5, return_path=True)
    last = orthogonal_mp(X, y, n_nonzero_coefs=5, return_path=False)
    assert path.shape == (n_features, n_targets, 5)
    assert_array_almost_equal(path[:, :, -1], last)
    path = orthogonal_mp_gram(G, Xy, n_nonzero_coefs=5, return_path=True)
    last = orthogonal_mp_gram(G, Xy, n_nonzero_coefs=5, return_path=False)

def test_omp_return_path_prop_with_gram():
    path = orthogonal_mp(X, y, n_nonzero_coefs=5, return_path=True, precompute=True)
    last = orthogonal_mp(X, y, n_nonzero_coefs=5, return_path=False, precompute=True)

def test_omp_cv():
    y_ = y[:, 0]
    gamma_ = gamma[:, 0]
    ompcv = OrthogonalMatchingPursuitCV(fit_intercept=False, max_iter=10)
    ompcv.fit(X, y_)
    assert ompcv.n_nonzero_coefs_ == n_nonzero_coefs
    assert_array_almost_equal(ompcv.coef_, gamma_)
    omp = OrthogonalMatchingPursuit(
        fit_intercept=False, n_nonzero_coefs=ompcv.n_nonzero_coefs_
    )
    omp.fit(X, y_)
    assert_array_almost_equal(ompcv.coef_, omp.coef_)

def test_omp_reaches_least_squares():
    # Use small simple data; it's a sanity check but OMP can stop early
    n_samples, n_features = (10, 8)
    Y = rng.randn(n_samples, n_targets)
    omp = OrthogonalMatchingPursuit(n_nonzero_coefs=n_features)
    lstsq = LinearRegression()
    omp.fit(X, Y)
    lstsq.fit(X, Y)
    assert_array_almost_equal(omp.coef_, lstsq.coef_)

def test_omp_gram_dtype_match(data_type):
    # verify matching input data type and output data type
    coef = orthogonal_mp_gram(
        G.astype(data_type), Xy.astype(data_type), n_nonzero_coefs=5
    )
    assert coef.dtype == data_type

def test_omp_gram_numerical_consistency():
    # verify numericaly consistency among np.float32 and np.float64
    coef_32 = orthogonal_mp_gram(
        G.astype(np.float32), Xy.astype(np.float32), n_nonzero_coefs=5
    )
    coef_64 = orthogonal_mp_gram(
        G.astype(np.float32), Xy.astype(np.float64), n_nonzero_coefs=5
    )
    assert_allclose(coef_32, coef_64)

def X_y_data():
    X, y = make_regression(n_samples=10, n_features=1, random_state=0, noise=1)
    return X, y

def test_incompatible_solver_for_sparse_input(X_y_data, solver, csc_container):
    X, y = X_y_data
    err_msg = (
        f"Solver {solver} does not support sparse X. Use solver 'highs' for example."
    )
        QuantileRegressor(solver=solver).fit(X_sparse, y)

def test_quantile_toy_example(quantile, alpha, intercept, coef):
    # test how different parameters affect a small intuitive example
    X = [[0], [1], [1]]
    y = [1, 2, 11]
    model = QuantileRegressor(quantile=quantile, alpha=alpha).fit(X, y)
    assert_allclose(model.intercept_, intercept, atol=1e-2)
        assert_allclose(model.coef_[0], coef, atol=1e-2)
    if alpha < 100:
        assert model.coef_[0] >= 1
    assert model.coef_[0] <= 10

def test_quantile_equals_huber_for_low_epsilon(fit_intercept):
    X, y = make_regression(n_samples=100, n_features=20, random_state=0, noise=1.0)
    alpha = 1e-4
    huber = HuberRegressor(
        epsilon=1 + 1e-4, alpha=alpha, fit_intercept=fit_intercept
    quant = QuantileRegressor(alpha=alpha, fit_intercept=fit_intercept).fit(X, y)
    assert_allclose(huber.coef_, quant.coef_, atol=1e-1)
        assert huber.intercept_ == approx(quant.intercept_, abs=1e-1)
        # check that we still predict fraction
        assert np.mean(y < quant.predict(X)) == approx(0.5, abs=1e-1)

def test_quantile_estimates_calibration(q):
    # Test that model estimates percentage of points below the prediction
    X, y = make_regression(n_samples=1000, n_features=20, random_state=0, noise=1.0)
    quant = QuantileRegressor(quantile=q, alpha=0).fit(X, y)
    assert np.mean(y < quant.predict(X)) == approx(q, abs=1e-2)

def test_quantile_sample_weight():
    # test that with unequal sample weights we still estimate weighted fraction
    n = 1000
    X, y = make_regression(n_samples=n, n_features=5, random_state=0, noise=10.0)
    weight = np.ones(n)
    # when we increase weight of upper observations,
    # estimate of quantile should go up
    weight[y > y.mean()] = 100
    quant = QuantileRegressor(quantile=0.5, alpha=1e-8)
    quant.fit(X, y, sample_weight=weight)
    fraction_below = np.mean(y < quant.predict(X))
    assert fraction_below > 0.5
    weighted_fraction_below = np.average(y < quant.predict(X), weights=weight)
    assert weighted_fraction_below == approx(0.5, abs=3e-2)

def test_asymmetric_error(quantile):
        (
        ),
        axis=1,
    )
    #  Take care that X @ coef + intercept > 0
    assert np.min(X @ coef + intercept) > 0
    #   scale = 1/lambda = -quantile(q) / log(1 - q)
    )
    model = QuantileRegressor(
        alpha=0,
    # This test can be made to pass with any solver but in the interest
    # of sparing continuous integration resources, the test is performed
    # with the fastest solver only.

    assert model.intercept_ == approx(intercept, rel=0.2)
    assert_allclose(model.coef_, coef, rtol=0.6)
    assert_allclose(np.mean(model.predict(X) > y), quantile, atol=1e-2)

    # Now compare to Nelder-Mead optimization with L1 penalty
    model.set_params(alpha=alpha).fit(X, y)
    model_coef = np.r_[model.intercept_, model.coef_]

    def func(coef):
        loss = mean_pinball_loss(y, X @ coef[1:] + coef[0], alpha=quantile)
        L1 = np.sum(np.abs(coef[1:]))
        return loss + alpha * L1

    res = minimize(
        fun=func,
        x0=[1, 0, -1],
        method="Nelder-Mead",
        options={"maxiter": 2000},
    )

    assert func(model_coef) == approx(func(res.x))
    assert_allclose(model.intercept_, res.x[0])
    assert_allclose(model.coef_, res.x[1:])

def test_equivariance(quantile):
    """Test equivariace of quantile regression.

    See Koenker (2005) Quantile Regression, Chapter 2.2.3.
    """
    n_samples, n_features = 100, 5
        noise=0,
    )
    # make y asymmetric
    y += rng.exponential(scale=100, size=y.shape)
    params = dict(alpha=0)
    model1 = QuantileRegressor(quantile=quantile, **params).fit(X, y)

    # coef(q; a*y, X) = a * coef(q; y, X)
    a = 2.5
    model2 = QuantileRegressor(quantile=quantile, **params).fit(X, a * y)
    assert model2.intercept_ == approx(a * model1.intercept_, rel=1e-5)
    assert_allclose(model2.coef_, a * model1.coef_, rtol=1e-5)

    # coef(1-q; -a*y, X) = -a * coef(q; y, X)
    model2 = QuantileRegressor(quantile=1 - quantile, **params).fit(X, -a * y)
    assert model2.intercept_ == approx(-a * model1.intercept_, rel=1e-5)
    assert_allclose(model2.coef_, -a * model1.coef_, rtol=1e-5)

    # coef(q; y + X @ g, X) = coef(q; y, X) + g
    g_intercept, g_coef = rng.randn(), rng.randn(n_features)
    model2 = QuantileRegressor(quantile=quantile, **params)
    model2.fit(X, y + X @ g_coef + g_intercept)
    assert model2.intercept_ == approx(model1.intercept_ + g_intercept)
    assert_allclose(model2.coef_, model1.coef_ + g_coef, rtol=1e-6)

    # coef(q; y, X @ A) = A^-1 @ coef(q; y, X)
    A = rng.randn(n_features, n_features)
    model2.fit(X @ A, y)
    assert model2.intercept_ == approx(model1.intercept_, rel=1e-5)
    assert_allclose(model2.coef_, np.linalg.solve(A, model1.coef_), rtol=1e-5)

def test_linprog_failure():
    """Test that linprog fails."""
    X = np.linspace(0, 10, num=10).reshape(-1, 1)
    y = np.linspace(0, 10, num=10)
    reg = QuantileRegressor(
        alpha=0, solver="interior-point", solver_options={"maxiter": 1}
    )

    msg = "Linear programming for QuantileRegressor did not succeed."

def test_sparse_input(sparse_container, solver, fit_intercept, global_random_seed):
    """Test that sparse and dense X give same results."""
    quantile_level = 0.6
    )
    alpha = 0.1
    quant_dense = QuantileRegressor(
        quantile=quantile_level, alpha=alpha, fit_intercept=fit_intercept
    quant_sparse = QuantileRegressor(
        quantile=quantile_level, alpha=alpha, fit_intercept=fit_intercept, solver=solver
    assert_allclose(quant_sparse.coef_, quant_dense.coef_, rtol=1e-2)
    sparse_support = quant_sparse.coef_ != 0
    dense_support = quant_dense.coef_ != 0
    assert dense_support.sum() == pytest.approx(n_informative, abs=1)
    assert sparse_support.sum() == pytest.approx(n_informative, abs=1)
        assert quant_sparse.intercept_ == approx(quant_dense.intercept_)
        empirical_coverage = np.mean(y < quant_sparse.predict(X_sparse))
        assert empirical_coverage == approx(quantile_level, abs=3e-2)

def test_error_interior_point_future(X_y_data, monkeypatch):
    """Check that we will raise a proper error when requesting
    `solver='interior-point'` in SciPy >= 1.11.
    """
    import sklearn.linear_model._quantile

    with monkeypatch.context() as m:
        m.setattr(sklearn.linear_model._quantile, "sp_version", parse_version("1.11.0"))
        err_msg = "Solver interior-point is not anymore available in SciPy >= 1.11.0."
            QuantileRegressor(solver="interior-point").fit(X, y)

def make_regression_with_outliers(n_samples=50, n_features=20):
    # Generate data with outliers by replacing 10% of the samples with noise.
        n_samples=n_samples, n_features=n_features, random_state=0, noise=0.05
    )

    # Replace 10% of the sample with noise.
    num_noise = int(0.1 * n_samples)
    random_samples = rng.randint(0, n_samples, num_noise)
    X[random_samples, :] = 2.0 * rng.normal(0, 1, (num_noise, X.shape[1]))
    return X, y

def test_huber_equals_lr_for_high_epsilon():
    # Test that Ridge matches LinearRegression for large epsilon
    X, y = make_regression_with_outliers()
    lr = LinearRegression()
    huber = HuberRegressor(epsilon=1e3, alpha=0.0)
    huber.fit(X, y)
    assert_almost_equal(huber.coef_, lr.coef_, 3)
    assert_almost_equal(huber.intercept_, lr.intercept_, 2)

def test_huber_max_iter():
    huber = HuberRegressor(max_iter=1)
    assert huber.n_iter_ == huber.max_iter

def test_huber_gradient():
    # Test that the gradient calculated by _huber_loss_and_gradient is correct
    sample_weight = rng.randint(1, 3, (y.shape[0]))

    def loss_func(x, *args):
        return _huber_loss_and_gradient(x, *args)[0]

    def grad_func(x, *args):
        return _huber_loss_and_gradient(x, *args)[1]

    # Check using optimize.check_grad that the gradients are equal.
        # Check for both fit_intercept and otherwise.
        for n_features in [X.shape[1] + 1, X.shape[1] + 2]:
            w = rng.randn(n_features)
            w[-1] = np.abs(w[-1])
            grad_same = optimize.check_grad(
                loss_func, grad_func, w, X, y, 0.01, 0.1, sample_weight
            )
            assert_almost_equal(grad_same, 1e-6, 4)

def test_huber_sample_weights(csr_container):
    # Test sample_weights implementation in HuberRegressor"""

    huber = HuberRegressor()
    huber_coef = huber.coef_
    huber_intercept = huber.intercept_

    # Rescale coefs before comparing with assert_array_almost_equal to make
    # sure that the number of decimal places used is somewhat insensitive to
    # the amplitude of the coefficients and therefore to the scale of the
    # data and the regularization parameter
    scale = max(np.mean(np.abs(huber.coef_)), np.mean(np.abs(huber.intercept_)))

    huber.fit(X, y, sample_weight=np.ones(y.shape[0]))
    assert_array_almost_equal(huber.coef_ / scale, huber_coef / scale)
    assert_array_almost_equal(huber.intercept_ / scale, huber_intercept / scale)

    X, y = make_regression_with_outliers(n_samples=5, n_features=20)
    X_new = np.vstack((X, np.vstack((X[1], X[1], X[3]))))
    y_new = np.concatenate((y, [y[1]], [y[1]], [y[3]]))
    huber.fit(X_new, y_new)
    sample_weight[1] = 3
    sample_weight[3] = 2
    huber.fit(X, y, sample_weight=sample_weight)

    # Test sparse implementation with sample weights.
    huber_sparse = HuberRegressor()
    huber_sparse.fit(X_csr, y, sample_weight=sample_weight)
    assert_array_almost_equal(huber_sparse.coef_ / scale, huber_coef / scale)

def test_huber_sparse(csr_container):
    huber = HuberRegressor(alpha=0.1)

    huber_sparse = HuberRegressor(alpha=0.1)
    huber_sparse.fit(X_csr, y)
    assert_array_almost_equal(huber_sparse.coef_, huber.coef_)
    assert_array_equal(huber.outliers_, huber_sparse.outliers_)

def test_huber_scaling_invariant():
    # Test that outliers filtering is scaling independent.
    huber = HuberRegressor(fit_intercept=False, alpha=0.0)
    n_outliers_mask_1 = huber.outliers_
    assert not np.all(n_outliers_mask_1)

    huber.fit(X, 2.0 * y)
    n_outliers_mask_2 = huber.outliers_
    assert_array_equal(n_outliers_mask_2, n_outliers_mask_1)

    huber.fit(2.0 * X, 2.0 * y)
    n_outliers_mask_3 = huber.outliers_
    assert_array_equal(n_outliers_mask_3, n_outliers_mask_1)

def test_huber_and_sgd_same_results():
    # Test they should converge to same coefficients for same parameters

    X, y = make_regression_with_outliers(n_samples=10, n_features=2)

    # Fit once to find out the scale parameter. Scale down X and y by scale
    # so that the scale parameter is optimized to 1.0
    huber = HuberRegressor(fit_intercept=False, alpha=0.0, epsilon=1.35)
    X_scale = X / huber.scale_
    y_scale = y / huber.scale_
    huber.fit(X_scale, y_scale)
    assert_almost_equal(huber.scale_, 1.0, 3)

    sgdreg = SGDRegressor(
        alpha=0.0,
        tol=None,
    )
    sgdreg.fit(X_scale, y_scale)
    assert_array_almost_equal(huber.coef_, sgdreg.coef_, 1)

def test_huber_warm_start():
    huber_warm = HuberRegressor(alpha=1.0, max_iter=10000, warm_start=True, tol=1e-1)

    huber_warm.fit(X, y)
    huber_warm_coef = huber_warm.coef_.copy()

    # SciPy performs the tol check after doing the coef updates, so
    # these would be almost same but not equal.
    assert_array_almost_equal(huber_warm.coef_, huber_warm_coef, 1)

    assert huber_warm.n_iter_ == 0

def test_huber_better_r2_score():
    # Test that huber returns a better r2 score than non-outliers"""
    huber = HuberRegressor(alpha=0.01)
    linear_loss = np.dot(X, huber.coef_) + huber.intercept_ - y
    mask = np.abs(linear_loss) < huber.epsilon * huber.scale_
    huber_score = huber.score(X[mask], y[mask])
    huber_outlier_score = huber.score(X[~mask], y[~mask])

    # The Ridge regressor should be influenced by the outliers and hence
    # give a worse score on the non-outliers as compared to the huber
    # regressor.
    ridge = Ridge(alpha=0.01)
    ridge.fit(X, y)
    ridge_score = ridge.score(X[mask], y[mask])
    ridge_outlier_score = ridge.score(X[~mask], y[~mask])
    assert huber_score > ridge_score

    # The huber model should also fit poorly on the outliers.
    assert ridge_outlier_score > huber_outlier_score

def test_huber_bool():
    # Test that it does not crash with bool data
    X, y = make_regression(n_samples=200, n_features=2, noise=4.0, random_state=0)
    X_bool = X > 0
    HuberRegressor().fit(X_bool, y)

def random_X_y_coef(
    linear_model_loss, n_samples, n_features, coef_bound=(-2, 2), seed=42
):
    """Random generate y, X and coef in valid range."""
    n_dof = n_features + linear_model_loss.fit_intercept
    )
    coef = linear_model_loss.init_zero_coef(X)

    if linear_model_loss.base_loss.is_multiclass:
        n_classes = linear_model_loss.base_loss.n_classes
        coef.flat[:] = rng.uniform(
            low=coef_bound[0],
            high=coef_bound[1],
            size=n_classes * n_dof,
        )
        if linear_model_loss.fit_intercept:
            raw_prediction = X @ coef[:, :-1].T + coef[:, -1]
        else:
            raw_prediction = X @ coef.T
        proba = linear_model_loss.base_loss.link.inverse(raw_prediction)

        # y = rng.choice(np.arange(n_classes), p=proba) does not work.
        # See https://stackoverflow.com/a/34190035/16761084
        def choice_vectorized(items, p):
            s = p.cumsum(axis=1)
            r = rng.rand(p.shape[0])[:, None]
            k = (s < r).sum(axis=1)
            return items[k]

        y = choice_vectorized(np.arange(n_classes), p=proba).astype(np.float64)
    else:
            size=n_dof,
        )
            raw_prediction = X @ coef[:-1] + coef[-1]
        else:
            raw_prediction = X @ coef
        y = linear_model_loss.base_loss.link.inverse(
            raw_prediction + rng.uniform(low=-1, high=1, size=n_samples)
        )

    return X, y, coef

def test_init_zero_coef(
    base_loss, fit_intercept, n_features, dtype, global_random_seed
):
    """Test that init_zero_coef initializes coef correctly."""
    loss = LinearModelLoss(base_loss=base_loss(), fit_intercept=fit_intercept)
    X = rng.normal(size=(5, n_features))
    coef = loss.init_zero_coef(X, dtype=dtype)
    if loss.base_loss.is_multiclass:
        n_classes = loss.base_loss.n_classes
        assert coef.shape == (n_classes, n_features + fit_intercept)
        assert coef.flags["F_CONTIGUOUS"]
    else:
        assert coef.shape == (n_features + fit_intercept,)

        assert coef.dtype == X.dtype
    else:
        assert coef.dtype == dtype

    assert np.count_nonzero(coef) == 0

def test_loss_grad_hess_are_the_same(
    base_loss,
    l2_reg_strength,
):
    """Test that loss and gradient are the same across different functions."""
    X, y, coef = random_X_y_coef(
        linear_model_loss=loss, n_samples=10, n_features=5, seed=global_random_seed
    )
    X_old, y_old, coef_old = X.copy(), y.copy(), coef.copy()

        sample_weight = np.linspace(1, y.shape[0], num=y.shape[0])

        coef, X, y, sample_weight=sample_weight, l2_reg_strength=l2_reg_strength
    )
    )
    )
    g3, h3 = loss.gradient_hessian_product(
    )
    g4, h4, _ = loss.gradient_hessian(
    )
    assert_allclose(g1, g4)
    # The ravelling only takes effect for multiclass.
    assert_allclose(h4 @ g4.ravel(order="F"), h3(g3).ravel(order="F"))
    # Test that gradient_out and hessian_out are considered properly.
    g_out = np.empty_like(coef)
    h_out = np.empty_like(coef, shape=(coef.size, coef.size))
    g5, h5, _ = loss.gradient_hessian(
        coef,
        X,
        y,
        gradient_out=g_out,
        hessian_out=h_out,
    )
    assert np.shares_memory(g5, g_out)
    assert np.shares_memory(h5, h_out)
    assert_allclose(g5, g_out)
    assert_allclose(h5, h_out)
    assert_allclose(g1, g5)
    assert_allclose(h5, h4)

    # same for sparse X
    Xs = csr_container(X)
    l1_sp = loss.loss(
        coef, Xs, y, sample_weight=sample_weight, l2_reg_strength=l2_reg_strength
    )
    g1_sp = loss.gradient(
    )
    l2_sp, g2_sp = loss.loss_gradient(
    )
    g3_sp, h3_sp = loss.gradient_hessian_product(
    )
    g4_sp, h4_sp, _ = loss.gradient_hessian(
    )
    assert_allclose(l1, l1_sp)
    assert_allclose(l1, l2_sp)
    assert_allclose(g1, g1_sp)
    assert_allclose(g1, g2_sp)
    assert_allclose(g1, g3_sp)
    assert_allclose(h3(g1), h3_sp(g1_sp))
    assert_allclose(g1, g4_sp)
    assert_allclose(h4, h4_sp)

    # X, y and coef should not have changed
    assert_allclose(X, X_old)
    assert_allclose(Xs.toarray(), X_old)
    assert_allclose(y, y_old)
    assert_allclose(coef, coef_old)

def test_loss_gradients_hessp_intercept(
    base_loss, sample_weight, l2_reg_strength, X_container, global_random_seed
):
    """Test that loss and gradient handle intercept correctly."""
    loss = LinearModelLoss(base_loss=base_loss(), fit_intercept=False)
    loss_inter = LinearModelLoss(base_loss=base_loss(), fit_intercept=True)
        linear_model_loss=loss,
    )

    X[:, -1] = 1  # make last column of 1 to mimic intercept term
    X_inter = X[
        :, :-1
    ]  # exclude intercept column as it is added automatically by loss_inter

    if X_container is not None:
        X = X_container(X)

    )
    _, hessp = loss.gradient_hessian_product(
    )
    l_inter, g_inter = loss_inter.loss_gradient(
        coef, X_inter, y, sample_weight=sample_weight, l2_reg_strength=l2_reg_strength
    )
    _, hessp_inter = loss_inter.gradient_hessian_product(
    )

    # Note, that intercept gets no L2 penalty.
    assert l == pytest.approx(
        l_inter + 0.5 * l2_reg_strength * squared_norm(coef.T[-1])
    )

    g_inter_corrected = g_inter
    g_inter_corrected.T[-1] += l2_reg_strength * coef.T[-1]
    assert_allclose(g, g_inter_corrected)

    s = np.random.RandomState(global_random_seed).randn(*coef.shape)
    h = hessp(s)
    h_inter = hessp_inter(s)
    h_inter_corrected = h_inter
    h_inter_corrected.T[-1] += l2_reg_strength * s.T[-1]
    assert_allclose(h, h_inter_corrected)

def test_gradients_hessians_numerically(
    base_loss, fit_intercept, sample_weight, l2_reg_strength, global_random_seed
):

    """
    )
    coef = coef.ravel(order="F")  # this is important only for multinomial loss

    # 1. Check gradients numerically
    eps = 1e-6
    g, hessp = loss.gradient_hessian_product(
    )
    # Use a trick to get central finite difference of accuracy 4 (five-point stencil)
    # approx_g1 = (f(x + eps) - f(x - eps)) / (2*eps)
    approx_g1 = optimize.approx_fprime(
        coef,
        lambda coef: loss.loss(
            coef - eps,
            X,
            y,
            l2_reg_strength=l2_reg_strength,
        ),
        2 * eps,
    )
    # approx_g2 = (f(x + 2*eps) - f(x - 2*eps)) / (4*eps)
    approx_g2 = optimize.approx_fprime(
        coef,
            coef - 2 * eps,
            X,
            y,
        ),
        4 * eps,
    )
    # Five-point stencil approximation
    # See: https://en.wikipedia.org/wiki/Five-point_stencil#1D_first_derivative
    approx_g = (4 * approx_g1 - approx_g2) / 3
    assert_allclose(g, approx_g, rtol=1e-2, atol=1e-8)

    # 2. Check hessp numerically along the second direction of the gradient
    vector = np.zeros_like(g)
    vector[1] = 1
    hess_col = hessp(vector)
    # Computation of the Hessian is particularly fragile to numerical errors when doing
    # simple finite differences. Here we compute the grad along a path in the direction
    # of the vector and then use a least-square regression to estimate the slope
    eps = 1e-3
    d_x = np.linspace(-eps, eps, 30)
    d_grad = np.array(
        [
            loss.gradient(
                coef + t * vector,
                X,
                y,
            )
            for t in d_x
        ]
    )
    d_grad -= d_grad.mean(axis=0)
    approx_hess_col = linalg.lstsq(d_x[:, np.newaxis], d_grad)[0].ravel()
    assert_allclose(approx_hess_col, hess_col, rtol=1e-3)

def test_multinomial_coef_shape(fit_intercept, global_random_seed):
    """Test that multinomial LinearModelLoss respects shape of coef."""
    loss = LinearModelLoss(base_loss=HalfMultinomialLoss(), fit_intercept=fit_intercept)
    )

    l, g = loss.loss_gradient(coef, X, y)
    g1 = loss.gradient(coef, X, y)
    g2, hessp = loss.gradient_hessian_product(coef, X, y)
    assert g.shape == coef.shape
    assert h.shape == coef.shape
    assert_allclose(g, g1)
    assert_allclose(g, g2)
    g3, hess, _ = loss.gradient_hessian(coef, X, y)
    assert g3.shape == coef.shape
    # But full hessian is always 2d.
    assert hess.shape == (coef.size, coef.size)

    coef_r = coef.ravel(order="F")
    s_r = s.ravel(order="F")
    l_r, g_r = loss.loss_gradient(coef_r, X, y)
    g1_r = loss.gradient(coef_r, X, y)
    g2_r, hessp_r = loss.gradient_hessian_product(coef_r, X, y)
    h_r = hessp_r(s_r)
    assert g_r.shape == coef_r.shape
    assert h_r.shape == coef_r.shape
    assert_allclose(g_r, g1_r)
    assert_allclose(g_r, g2_r)

    assert_allclose(g, g_r.reshape(loss.base_loss.n_classes, -1, order="F"))
    assert_allclose(h, h_r.reshape(loss.base_loss.n_classes, -1, order="F"))

def test_multinomial_hessian_3_classes(sample_weight, global_random_seed):
    """Test multinomial hessian for 3 classes and 2 points.

    For n_classes = 3 and n_samples = 2, we have
      p0 = [p0_0, p0_1]
      p1 = [p1_0, p1_1]
      p2 = [p2_0, p2_1]
    and with 2 x 2 diagonal subblocks
      H = [p0 * (1-p0),    -p0 * p1,    -p0 * p2]
          [   -p0 * p1, p1 * (1-p1),    -p1 * p2]
          [   -p0 * p2,    -p1 * p2, p2 * (1-p2)]
      hess = X' H X
    """
    n_samples, n_features, n_classes = 2, 5, 3
    loss = LinearModelLoss(
        base_loss=HalfMultinomialLoss(n_classes=n_classes), fit_intercept=False
    )
    )

    grad, hess, _ = loss.gradient_hessian(
        coef,
        X,
        y,
    )
    # Hessian must be a symmetrix matrix.
    assert_allclose(hess, hess.T)

    weights, intercept, raw_prediction = loss.weight_intercept_raw(coef, X)
    grad_pointwise, proba = loss.base_loss.gradient_proba(
        y_true=y,
    )
    p0d, p1d, p2d, oned = (
        np.diag(proba[:, 0]),
        np.diag(proba[:, 1]),
        np.diag(proba[:, 2]),
        np.diag(np.ones(2)),
    )
    h = np.block(
        [
            [p0d * (oned - p0d), -p0d * p1d, -p0d * p2d],
            [-p0d * p1d, p1d * (oned - p1d), -p1d * p2d],
            [-p0d * p2d, -p1d * p2d, p2d * (oned - p2d)],
        ]
    )
    h = h.reshape((n_classes, n_samples, n_classes, n_samples))
        h /= n_samples
    else:
        h *= sample_weight / np.sum(sample_weight)
    # hess_expected.shape = (n_features, n_classes, n_classes, n_features)
    hess_expected = np.einsum("ij, mini, ik->jmnk", X, h, X)
    hess_expected = np.moveaxis(hess_expected, 2, 3)
    hess_expected = hess_expected.reshape(
        n_classes * n_features, n_classes * n_features, order="C"
    )
    assert_allclose(hess_expected, hess_expected.T)
    assert_allclose(hess, hess_expected)

def test_linear_loss_gradient_hessian_raises_wrong_out_parameters():
    """Test that wrong gradient_out and hessian_out raises errors."""
    n_samples, n_features, n_classes = 5, 2, 3
    loss = LinearModelLoss(base_loss=HalfBinomialLoss(), fit_intercept=False)
    X = np.ones((n_samples, n_features))
    coef = loss.init_zero_coef(X)
    gradient_out = np.zeros(1)
        ValueError, match="gradient_out is required to have shape coef.shape"
    ):
        loss.gradient_hessian(
            coef=coef,
            X=X,
            y=y,
            hessian_out=None,
        )
    hessian_out = np.zeros(1)
    with pytest.raises(ValueError, match="hessian_out is required to have shape"):
            X=X,
            y=y,
            gradient_out=None,
        )

    loss = LinearModelLoss(base_loss=HalfMultinomialLoss(), fit_intercept=False)
    gradient_out = np.zeros((2 * n_classes, n_features))[::2]
    with pytest.raises(ValueError, match="gradient_out must be F-contiguous"):
            X=X,
            y=y,
        )
    hessian_out = np.zeros((2 * n_classes * n_features, n_classes * n_features))[::2]
    with pytest.raises(ValueError, match="hessian_out must be contiguous"):
            X=X,
            y=y,
        )

def _accuracy_callable(y_test, y_pred, **kwargs):
    return np.mean(y_test == y_pred)

def _mean_squared_error_callable(y_test, y_pred):
    return ((y_test - y_pred) ** 2).mean()

def ols_ridge_dataset(global_random_seed, request):
    """Dataset with OLS and Ridge solutions, well conditioned X.

    For "wide", we return the minimum norm solution w = X' (XX')^-1 y:

        min ||w||_2 subject to X w = y

    Returns
    -------
    X : ndarray
    y : ndarray
    coef_ols : ndarray of shape
        Minimum norm OLS solutions, i.e. min ||X w - y||_2_2 (with minimum ||w||_2 in
    coef_ridge : ndarray of shape (5,)
        Ridge solution with alpha=1, i.e. min ||X w - y||_2_2 + ||w||_2^2.
    """
    if request.param == "long":
    else:
        n_samples=n_samples, n_features=n_features, effective_rank=k, random_state=rng
    )
    U, s, Vt = linalg.svd(X)
    U1, U2 = U[:, :k], U[:, k:]
    Vt1, _ = Vt[:k, :], Vt[k:, :]

        # Add a term that vanishes in the product X'y
        coef_ols = rng.uniform(low=-10, high=10, size=n_features)
        y = X @ coef_ols
        y += U2 @ rng.normal(size=n_samples - n_features) ** 2
    else:
        y = rng.uniform(low=-10, high=10, size=n_samples)
        # w = X'(XX')^-1 y = V s^-1 U' y
        coef_ols = Vt1.T @ np.diag(1 / s) @ U1.T @ y

    # Add penalty alpha * ||coef||_2^2 for alpha=1 and solve via normal equations.
    # Note that the problem is well conditioned such that we get accurate results.
    alpha = 1
    d = alpha * np.identity(n_features)
    d[-1, -1] = 0  # intercept gets no penalty
    coef_ridge = linalg.solve(X.T @ X + d, X.T @ y)

    # To be sure
    R_OLS = y - X @ coef_ols
    R_Ridge = y - X @ coef_ridge
    assert np.linalg.norm(R_OLS) < np.linalg.norm(R_Ridge)

    return X, y, coef_ols, coef_ridge

def test_ridge_regression(solver, fit_intercept, ols_ridge_dataset, global_random_seed):
    """Test that Ridge converges for all solvers to correct solution.

    """
    X, y, _, coef = ols_ridge_dataset
    alpha = 1.0  # because ols_ridge_dataset uses this.
        tol=1e-15 if solver in ("sag", "saga") else 1e-10,
    )

    # Calculate residuals and R2.
    res_null = y - np.mean(y)
    res_Ridge = y - X @ coef
    R2_Ridge = 1 - np.sum(res_Ridge**2) / np.sum(res_null**2)

    model = Ridge(**params)
    else:
        X = X - X.mean(axis=0)
        y = y - y.mean()
    coef = coef[:-1]

    assert model.intercept_ == pytest.approx(intercept)
    assert_allclose(model.coef_, coef)
    assert model.score(X, y) == pytest.approx(R2_Ridge)

    model = Ridge(**params).fit(X, y, sample_weight=np.ones(X.shape[0]))

    assert model.solver_ == solver

def test_ridge_regression_hstacked_X(
    solver, fit_intercept, ols_ridge_dataset, global_random_seed
):
    """Test that Ridge converges for all solvers to correct solution on hstacked data.

    For long X, [X, X] is a singular matrix.
    """

    model = Ridge(
    )
    else:

    # coefficients are not all on the same magnitude, adding a small atol to
    # make this test less brittle
    assert_allclose(model.coef_, np.r_[coef, coef], atol=1e-8)

def test_ridge_regression_vstacked_X(
):
    """Test that Ridge converges for all solvers to correct solution on vstacked data.

                                                [X], [y] with 2 * alpha.
    """

        alpha=2 * alpha,
    )
    else:

    assert_allclose(model.coef_, coef, atol=1e-8)

def test_ridge_regression_unpenalized(
):
    """Test that unpenalized Ridge = OLS converges for all solvers to correct solution.

    """
    X, y, coef, _ = ols_ridge_dataset
    alpha = 0  # OLS
    )

    # Note that cholesky might give a warning: "Singular matrix in solving dual
    # problem. Using least-squares solution instead."
    else:

    # for the wide/fat case with n_features > n_samples. The current Ridge solvers do
    if n_samples > n_features or not fit_intercept:
        assert_allclose(model.coef_, coef)
    else:
        # As it is an underdetermined problem, residuals = 0. This shows that we get
        # a solution to X w = y ....
        assert_allclose(model.predict(X), y)
        assert_allclose(X @ coef + intercept, y)
        # But it is not the minimum norm solution. (This should be equal.)
        assert np.linalg.norm(np.r_[model.intercept_, model.coef_]) > np.linalg.norm(
            np.r_[intercept, coef]
        )

        pytest.xfail(reason="Ridge does not provide the minimum norm solution.")

def test_ridge_regression_unpenalized_hstacked_X(
):

    OLS fit on [X] is the same as fit on [X, X]/2.
    solution:
        min ||w||_2 subject to min ||X w - y||_2
    """

    )
    else:

        if solver == "cholesky":
            # Cholesky is a bad choice for singular X.
            pytest.skip()
        assert_allclose(model.coef_, np.r_[coef, coef])
    else:
        # FIXME: Same as in test_ridge_regression_unpenalized.
            np.r_[intercept, coef, coef]
        )

def test_ridge_regression_unpenalized_vstacked_X(
):

    OLS fit on [X] is the same as fit on [X], [y]
                                         [X], [y].
    solution:
    """

    )

    else:

    else:
        )

def test_ridge_regression_sample_weights(
    solver,
    alpha,
    ols_ridge_dataset,
):
    """Test that Ridge with sample weights gives correct results.

    We use the following trick:
        ||y - Xw||_2 = (z - Aw)' W (z - Aw)
    for z=[y, y], A' = [X', X'] (vstacked), and W[:n/2] + W[n/2:] = 1, W=diag(W)
    """
        if fit_intercept and solver not in SPARSE_SOLVERS_WITH_INTERCEPT:
        elif not fit_intercept and solver not in SPARSE_SOLVERS_WITHOUT_INTERCEPT:
    sw = rng.uniform(low=0, high=1, size=n_samples)

        tol=1e-15 if solver in ["sag", "saga"] else 1e-10,
        max_iter=100_000,
    )
    sw = np.r_[sw, 1 - sw] * alpha
    else:
    model.fit(X, y, sample_weight=sw)

def test_primal_dual_relationship():
    y = y_diabetes.reshape(-1, 1)
    coef = _solve_cholesky(X_diabetes, y, alpha=[1e-2])
    K = np.dot(X_diabetes, X_diabetes.T)
    dual_coef = _solve_cholesky_kernel(K, y, alpha=[1e-2])
    coef2 = np.dot(X_diabetes.T, dual_coef).T
    assert_array_almost_equal(coef, coef2)

def test_ridge_regression_convergence_fail():
    warning_message = r"sparse_cg did not converge after [0-9]+ iterations."
        ridge_regression(
            X, y, alpha=1.0, solver="sparse_cg", tol=0.0, max_iter=None, verbose=1
        )

def test_ridge_shapes_type():
    # Test shape of coef_ and intercept_
    n_samples, n_features = 5, 10
    y = rng.randn(n_samples)
    Y1 = y[:, np.newaxis]
    Y = np.c_[y, 1 + y]

    ridge = Ridge()

    assert ridge.coef_.shape == (n_features,)
    assert ridge.intercept_.shape == ()
    assert isinstance(ridge.coef_, np.ndarray)
    assert isinstance(ridge.intercept_, float)

    ridge.fit(X, Y1)
    assert ridge.intercept_.shape == (1,)
    assert isinstance(ridge.intercept_, np.ndarray)

    ridge.fit(X, Y)
    assert ridge.coef_.shape == (2, n_features)
    assert ridge.intercept_.shape == (2,)

def test_ridge_intercept():
    # Test intercept with multiple targets GH issue #708
    Y = np.c_[y, 1.0 + y]

    intercept = ridge.intercept_

    assert_almost_equal(ridge.intercept_[0], intercept)
    assert_almost_equal(ridge.intercept_[1], intercept + 1.0)

def test_ridge_vs_lstsq():
    # On alpha=0., Ridge and OLS yield the same solution.

    # we need more samples than features
    n_samples, n_features = 5, 4

    ridge = Ridge(alpha=0.0, fit_intercept=False)
    ols = LinearRegression(fit_intercept=False)

    ols.fit(X, y)
    assert_almost_equal(ridge.coef_, ols.coef_)

def test_ridge_individual_penalties():
    # Tests the ridge object using individual penalties

    n_samples, n_features, n_targets = 20, 10, 5

    penalties = np.arange(n_targets)

    coef_cholesky = np.array(
        [
            Ridge(alpha=alpha, solver="cholesky").fit(X, target).coef_
            for alpha, target in zip(penalties, y.T)
        ]
    )

    coefs_indiv_pen = [
        Ridge(alpha=penalties, solver=solver, tol=1e-12).fit(X, y).coef_
        for solver in ["svd", "sparse_cg", "lsqr", "cholesky", "sag", "saga"]
    ]
    for coef_indiv_pen in coefs_indiv_pen:
        assert_array_almost_equal(coef_cholesky, coef_indiv_pen)

    # Test error is raised when number of targets and penalties do not match.
    ridge = Ridge(alpha=penalties[:-1])
    err_msg = "Number of targets and number of penalties do not correspond: 4 != 5"
        ridge.fit(X, y)

def test_X_CenterStackOp(n_col, csr_container):
    X = rng.randn(11, 8)
    X_m = rng.randn(8)
    sqrt_sw = rng.randn(len(X))
    Y = rng.randn(11, *n_col)
    A = rng.randn(9, *n_col)
    operator = _X_CenterStackOp(csr_container(X), X_m, sqrt_sw)
    reference_operator = np.hstack([X - sqrt_sw[:, None] * X_m, sqrt_sw[:, None]])
    assert_allclose(reference_operator.dot(A), operator.dot(A))
    assert_allclose(reference_operator.T.dot(Y), operator.T.dot(Y))

def test_compute_gram(shape, uniform_weights, csr_container):
    X = rng.randn(*shape)
    if uniform_weights:
    else:
        sw = rng.chisquare(1, shape[0])
    sqrt_sw = np.sqrt(sw)
    X_mean = np.average(X, axis=0, weights=sw)
    X_centered = (X - X_mean) * sqrt_sw[:, None]
    true_gram = X_centered.dot(X_centered.T)
    X_sparse = csr_container(X * sqrt_sw[:, None])
    gcv = _RidgeGCV(fit_intercept=True)
    computed_gram, computed_mean = gcv._compute_gram(X_sparse, sqrt_sw)
    assert_allclose(X_mean, computed_mean)
    assert_allclose(true_gram, computed_gram)

def test_compute_covariance(shape, uniform_weights, csr_container):
    else:
    true_covariance = X_centered.T.dot(X_centered)
    computed_cov, computed_mean = gcv._compute_covariance(X_sparse, sqrt_sw)
    assert_allclose(true_covariance, computed_cov)

def _make_sparse_offset_regression(
    proportion_nonzero=0.5,
    bias=13.0,
    X_offset=30.0,
    noise=30.0,
):
        n_informative=n_informative,
        n_targets=n_targets,
        bias=bias,
        noise=noise,
    )
        c = np.asarray([c])
    X += X_offset
    mask = (
        np.random.RandomState(random_state).binomial(1, proportion_nonzero, X.shape) > 0
    )
    removed_X = X.copy()
    X[~mask] = 0.0
    removed_X[mask] = 0.0
    y -= removed_X.dot(c)
        y += X.dot(np.abs(c) + 1 - c)
        c = np.abs(c) + 1
        c = c[0]
    if coef:
        return X, y, c
    return X, y

def test_solver_consistency(
    solver, proportion_nonzero, n_samples, dtype, sparse_container, seed
):
    alpha = 1.0
    noise = 50.0 if proportion_nonzero > 0.9 else 500.0
    X, y = _make_sparse_offset_regression(
        bias=10,
        n_features=30,
        proportion_nonzero=proportion_nonzero,
    )

    # Manually scale the data to avoid pathological cases. We use
    # minmax_scale to deal with the sparse case without breaking
    # the sparsity pattern.
    X = minmax_scale(X)

    svd_ridge = Ridge(solver="svd", alpha=alpha).fit(X, y)
    X = X.astype(dtype, copy=False)
    y = y.astype(dtype, copy=False)
    if solver == "ridgecv":
        ridge = RidgeCV(alphas=[alpha])
    else:
        ridge = Ridge(solver=solver, tol=1e-10, alpha=alpha)
    assert_allclose(ridge.coef_, svd_ridge.coef_, atol=1e-3, rtol=1e-3)
    assert_allclose(ridge.intercept_, svd_ridge.intercept_, atol=1e-3, rtol=1e-3)

def test_ridge_gcv_vs_ridge_loo_cv(
    gcv_mode, X_container, X_shape, y_shape, fit_intercept, noise
):
    n_samples, n_features = X_shape
    n_targets = y_shape[-1] if len(y_shape) == 2 else 1
    )
    y = y.reshape(y_shape)

    alphas = [1e-3, 0.1, 1.0, 10.0, 1e3]
    loo_ridge = RidgeCV(
        cv=n_samples,
    )
    gcv_ridge = RidgeCV(
        gcv_mode=gcv_mode,
    )

    loo_ridge.fit(X, y)

    X_gcv = X_container(X)
    gcv_ridge.fit(X_gcv, y)

    assert gcv_ridge.alpha_ == pytest.approx(loo_ridge.alpha_)
    assert_allclose(gcv_ridge.coef_, loo_ridge.coef_, rtol=1e-3)
    assert_allclose(gcv_ridge.intercept_, loo_ridge.intercept_, rtol=1e-3)

def test_ridge_loo_cv_asym_scoring():
    # checking on asymmetric scoring
    scoring = "explained_variance"
    n_targets = 1
        noise=1,
    )

        cv=n_samples, fit_intercept=True, alphas=alphas, scoring=scoring
    )

    gcv_ridge = RidgeCV(fit_intercept=True, alphas=alphas, scoring=scoring)

    gcv_ridge.fit(X, y)

    assert gcv_ridge.alpha_ == pytest.approx(loo_ridge.alpha_), (
        f"{gcv_ridge.alpha_=}, {loo_ridge.alpha_=}"
    )

def test_ridge_gcv_sample_weights(
    gcv_mode, X_container, fit_intercept, n_features, y_shape, noise
):
        n_samples=11,
    )

    sample_weight = 3 * rng.randn(len(X))
    sample_weight = (sample_weight - sample_weight.min() + 1).astype(int)
    indices = np.repeat(np.arange(X.shape[0]), sample_weight)
    sample_weight = sample_weight.astype(float)
    X_tiled, y_tiled = X[indices], y[indices]

    cv = GroupKFold(n_splits=X.shape[0])
    splits = cv.split(X_tiled, y_tiled, groups=indices)
    kfold = RidgeCV(
        cv=splits,
    )
    kfold.fit(X_tiled, y_tiled)

    ridge_reg = Ridge(alpha=kfold.alpha_, fit_intercept=fit_intercept)
    predictions = cross_val_predict(ridge_reg, X_tiled, y_tiled, cv=splits)
    if predictions.shape != y_tiled.shape:
        predictions = predictions.reshape(y_tiled.shape)
    kfold_errors = (y_tiled - predictions) ** 2
    kfold_errors = [
        np.sum(kfold_errors[indices == i], axis=0) for i in np.arange(X.shape[0])
    ]
    kfold_errors = np.asarray(kfold_errors)

        store_cv_results=True,
    )
    gcv_ridge.fit(X_gcv, y, sample_weight=sample_weight)
    if len(y_shape) == 2:
        gcv_errors = gcv_ridge.cv_results_[:, :, alphas.index(kfold.alpha_)]
    else:
        gcv_errors = gcv_ridge.cv_results_[:, alphas.index(kfold.alpha_)]

    assert kfold.alpha_ == pytest.approx(gcv_ridge.alpha_)
    assert_allclose(gcv_errors, kfold_errors, rtol=1e-3)
    assert_allclose(gcv_ridge.coef_, kfold.coef_, rtol=1e-3)
    assert_allclose(gcv_ridge.intercept_, kfold.intercept_, rtol=1e-3)

def test_check_gcv_mode_choice(
    sparse_container, mode, mode_n_greater_than_p, mode_p_greater_than_n
):
    X, _ = make_regression(n_samples=5, n_features=2)
    assert _check_gcv_mode(X, mode) == mode_n_greater_than_p
    assert _check_gcv_mode(X.T, mode) == mode_p_greater_than_n

def _test_ridge_loo(sparse_container):
    # test that can work with both dense or sparse matrices
    n_samples = X_diabetes.shape[0]

    ret = []

        X, fit_intercept = X_diabetes, True
    else:
        X, fit_intercept = sparse_container(X_diabetes), False
    ridge_gcv = _RidgeGCV(fit_intercept=fit_intercept)

    # check best alpha
    ridge_gcv.fit(X, y_diabetes)
    alpha_ = ridge_gcv.alpha_
    ret.append(alpha_)

    # check that we get same best alpha with custom loss_func
    scoring = make_scorer(mean_squared_error, greater_is_better=False)
    ridge_gcv2 = RidgeCV(fit_intercept=False, scoring=scoring)
    f(ridge_gcv2.fit)(X, y_diabetes)
    assert ridge_gcv2.alpha_ == pytest.approx(alpha_)

    # check that we get same best alpha with custom score_func
    def func(x, y):
        return -mean_squared_error(x, y)

    scoring = make_scorer(func)
    ridge_gcv3 = RidgeCV(fit_intercept=False, scoring=scoring)
    f(ridge_gcv3.fit)(X, y_diabetes)
    assert ridge_gcv3.alpha_ == pytest.approx(alpha_)

    # check that we get same best alpha with a scorer
    scorer = get_scorer("neg_mean_squared_error")
    ridge_gcv4 = RidgeCV(fit_intercept=False, scoring=scorer)
    ridge_gcv4.fit(X, y_diabetes)
    assert ridge_gcv4.alpha_ == pytest.approx(alpha_)

    # check that we get same best alpha with sample weights
        ridge_gcv.fit(X, y_diabetes, sample_weight=np.ones(n_samples))
        assert ridge_gcv.alpha_ == pytest.approx(alpha_)

    # simulate several responses
    Y = np.vstack((y_diabetes, y_diabetes)).T

    ridge_gcv.fit(X, Y)
    Y_pred = ridge_gcv.predict(X)
    y_pred = ridge_gcv.predict(X)

    assert_allclose(np.vstack((y_pred, y_pred)).T, Y_pred, rtol=1e-5)

    return ret

def _test_ridge_cv(sparse_container):
    X = X_diabetes if sparse_container is None else sparse_container(X_diabetes)
    ridge_cv = RidgeCV()
    ridge_cv.fit(X, y_diabetes)
    ridge_cv.predict(X)

    assert len(ridge_cv.coef_.shape) == 1
    assert type(ridge_cv.intercept_) is np.float64

    cv = KFold(5)
    ridge_cv.set_params(cv=cv)

def test_ridge_gcv_cv_results_not_stored(ridge, make_dataset):
    # Check that `cv_results_` is not stored when store_cv_results is False
    X, y = make_dataset(n_samples=6, random_state=42)
    assert not hasattr(ridge, "cv_results_")

def test_ridge_best_score(ridge, make_dataset, cv):
    # check that the best_score_ is store
    ridge.set_params(store_cv_results=False, cv=cv)
    assert hasattr(ridge, "best_score_")
    assert isinstance(ridge.best_score_, float)

def test_ridge_cv_individual_penalties():
    # Tests the ridge_cv object optimizing individual penalties for each target

    # Create random dataset with multiple targets. Each target should have
    # a different optimal alpha.
    n_samples, n_features, n_targets = 20, 5, 3
    X = (
        np.dot(y[:, [0]], np.ones((1, n_features)))
        + np.dot(y[:, [1]], 0.05 * np.ones((1, n_features)))
        + np.dot(y[:, [2]], 0.001 * np.ones((1, n_features)))
        + rng.randn(n_samples, n_features)
    )

    alphas = (1, 100, 1000)

    # Find optimal alpha for each target
    optimal_alphas = [RidgeCV(alphas=alphas).fit(X, target).alpha_ for target in y.T]

    # Find optimal alphas for all targets simultaneously
    ridge_cv = RidgeCV(alphas=alphas, alpha_per_target=True).fit(X, y)
    assert_array_equal(optimal_alphas, ridge_cv.alpha_)

    # The resulting regression weights should incorporate the different
    # alpha values.
        Ridge(alpha=ridge_cv.alpha_).fit(X, y).coef_, ridge_cv.coef_
    )

    # Test shape of alpha_ and cv_results_
    ridge_cv = RidgeCV(alphas=alphas, alpha_per_target=True, store_cv_results=True).fit(
        X, y
    )
    assert ridge_cv.alpha_.shape == (n_targets,)
    assert ridge_cv.best_score_.shape == (n_targets,)
    assert ridge_cv.cv_results_.shape == (n_samples, len(alphas), n_targets)

    # Test edge case of there being only one alpha value
    ridge_cv = RidgeCV(alphas=1, alpha_per_target=True, store_cv_results=True).fit(X, y)
    assert ridge_cv.cv_results_.shape == (n_samples, n_targets, 1)

    # Test edge case of there being only one target
        X, y[:, 0]
    )
    assert np.isscalar(ridge_cv.alpha_)
    assert np.isscalar(ridge_cv.best_score_)
    assert ridge_cv.cv_results_.shape == (n_samples, len(alphas))

    # Try with a custom scoring function
    ridge_cv = RidgeCV(alphas=alphas, alpha_per_target=True, scoring="r2").fit(X, y)
    )

    # Using a custom CV object should throw an error in combination with
    # alpha_per_target=True
    ridge_cv = RidgeCV(alphas=alphas, cv=LeaveOneOut(), alpha_per_target=True)
    msg = "cv!=None and alpha_per_target=True are incompatible"
        ridge_cv.fit(X, y)
    ridge_cv = RidgeCV(alphas=alphas, cv=6, alpha_per_target=True)

def _test_ridge_diabetes(sparse_container):
    ridge = Ridge(fit_intercept=False)
    ridge.fit(X, y_diabetes)
    return np.round(ridge.score(X, y_diabetes), 5)

def _test_multi_ridge_diabetes(sparse_container):
    n_features = X_diabetes.shape[1]

    Y_pred = ridge.predict(X)
    y_pred = ridge.predict(X)
    assert_array_almost_equal(np.vstack((y_pred, y_pred)).T, Y_pred, decimal=3)

def _test_ridge_classifiers(sparse_container):
    n_classes = np.unique(y_iris).shape[0]
    n_features = X_iris.shape[1]
    X = X_iris if sparse_container is None else sparse_container(X_iris)

    for reg in (RidgeClassifier(), RidgeClassifierCV()):
        reg.fit(X, y_iris)
        assert reg.coef_.shape == (n_classes, n_features)
        y_pred = reg.predict(X)
        assert np.mean(y_iris == y_pred) > 0.79

    reg = RidgeClassifierCV(cv=cv)
    reg.fit(X, y_iris)
    assert np.mean(y_iris == y_pred) >= 0.8

def test_ridge_classifier_with_scoring(sparse_container, scoring, cv):
    # non-regression test for #14672
    # check that RidgeClassifierCV works with all sort of scoring and
    # cross-validation
    scoring_ = make_scorer(scoring) if callable(scoring) else scoring
    clf = RidgeClassifierCV(scoring=scoring_, cv=cv)
    # Smoke test to check that fit/predict does not raise error
    clf.fit(X, y_iris).predict(X)

def test_ridge_regression_custom_scoring(sparse_container, cv):
    # check that custom scoring is working as expected
    # check the tie breaking strategy (keep the first alpha tried)

    def _dummy_score(y_test, y_pred, **kwargs):
        return 0.42

    alphas = np.logspace(-2, 2, num=5)
    clf = RidgeClassifierCV(alphas=alphas, scoring=make_scorer(_dummy_score), cv=cv)
    clf.fit(X, y_iris)
    assert clf.best_score_ == pytest.approx(0.42)
    # In case of tie score, the first alphas will be kept
    assert clf.alpha_ == pytest.approx(alphas[0])

def _test_tolerance(sparse_container):

    ridge = Ridge(tol=1e-5, fit_intercept=False)
    score = ridge.score(X, y_diabetes)

    ridge2 = Ridge(tol=1e-3, fit_intercept=False)
    ridge2.fit(X, y_diabetes)
    score2 = ridge2.score(X, y_diabetes)

    assert score >= score2

def check_array_api_attributes(name, estimator, array_namespace, device, dtype_name):

    X_iris_np = X_iris.astype(dtype_name)
    y_iris_np = y_iris.astype(dtype_name)

    X_iris_xp = xp.asarray(X_iris_np, device=device)
    y_iris_xp = xp.asarray(y_iris_np, device=device)

    estimator.fit(X_iris_np, y_iris_np)
    coef_np = estimator.coef_
    intercept_np = estimator.intercept_

        estimator_xp = clone(estimator).fit(X_iris_xp, y_iris_xp)
        coef_xp = estimator_xp.coef_
        assert coef_xp.shape == (4,)
        assert coef_xp.dtype == X_iris_xp.dtype

            _convert_to_numpy(coef_xp, xp=xp),
            coef_np,
        )
        intercept_xp = estimator_xp.intercept_
        assert intercept_xp.shape == ()
        assert intercept_xp.dtype == X_iris_xp.dtype

            _convert_to_numpy(intercept_xp, xp=xp),
            intercept_np,
        )

def test_ridge_array_api_compliance(
):

def test_array_api_error_and_warnings_for_solver_parameter(array_namespace):
    xp = _array_api_for_tests(array_namespace, device=None)

    X_iris_xp = xp.asarray(X_iris[:5])
    y_iris_xp = xp.asarray(y_iris[:5])

    available_solvers = Ridge._parameter_constraints["solver"][0].options
    for solver in available_solvers - {"auto", "svd"}:
        ridge = Ridge(solver=solver, positive=solver == "lbfgs")
        )

            with config_context(array_api_dispatch=True):
                ridge.fit(X_iris_xp, y_iris_xp)

    ridge = Ridge(solver="auto", positive=True)
        "The solvers that support positive fitting do not support "
        f"Array API dispatch to namespace {xp.__name__}. Please "
        "either disable Array API dispatch, or use a numpy-like "
        "namespace, or set `positive=False`."
    )

            ridge.fit(X_iris_xp, y_iris_xp)

        f"Using Array API dispatch to namespace {xp.__name__} with `solver='auto'` "
        "will result in using the solver 'svd'. The results may differ from those "
        "when using a Numpy array, because in that case the preferred solver would "
        "be cholesky. Set `solver='svd'` to suppress this warning."
    )

def test_array_api_numpy_namespace_no_warning(array_namespace):

        "Results might be different than when Array API dispatch is "
        "disabled, or when a numpy-like namespace is used"
    )

        warnings.filterwarnings("error", message=expected_msg, category=UserWarning)

    # All numpy namespaces are compatible with all solver, in particular
    # solvers that support `positive=True` (like 'lbfgs') should work.
        Ridge(solver="auto", positive=True).fit(X_iris_xp, y_iris_xp)

def test_dense_sparse(test_func, csr_container):
    # test dense matrix
    ret_dense = test_func(None)
    # test sparse matrix
    ret_sparse = test_func(csr_container)
    # test that the outputs are the same
    if ret_dense is not None and ret_sparse is not None:
        assert_array_almost_equal(ret_dense, ret_sparse, decimal=3)

    reg = RidgeClassifier(class_weight=None)
    assert_array_equal(reg.predict([[0.2, -1.0]]), np.array([1]))

    reg = RidgeClassifier(class_weight={1: 0.001})

    assert_array_equal(reg.predict([[0.2, -1.0]]), np.array([-1]))

    # check if class_weight = 'balanced' can handle negative labels.
    reg = RidgeClassifier(class_weight="balanced")

    # class_weight = 'balanced', and class_weight = None should return
    # same values when y has equal number of all labels
    X = np.array([[-1.0, -1.0], [-1.0, 0], [-0.8, -1.0], [1.0, 1.0]])
    y = [1, 1, -1, -1]
    rega = RidgeClassifier(class_weight="balanced")
    rega.fit(X, y)
    assert len(rega.classes_) == 2
    assert_array_almost_equal(reg.coef_, rega.coef_)
    assert_array_almost_equal(reg.intercept_, rega.intercept_)

def test_class_weight_vs_sample_weight(reg):
    """Check class_weights resemble sample_weights behavior."""

    reg1 = reg()
    reg1.fit(iris.data, iris.target)
    reg2 = reg(class_weight="balanced")
    reg2.fit(iris.data, iris.target)
    assert_almost_equal(reg1.coef_, reg2.coef_)

    reg1.fit(iris.data, iris.target, sample_weight)
    reg2 = reg(class_weight=class_weight)

    reg1.fit(iris.data, iris.target, sample_weight**2)
    reg2.fit(iris.data, iris.target, sample_weight)

def test_class_weights_cv():
    # Test class weights for cross validated ridge classifier.

    reg = RidgeClassifierCV(class_weight=None, alphas=[0.01, 0.1, 1])

    reg = RidgeClassifierCV(class_weight={1: 0.001}, alphas=[0.01, 0.1, 1, 10])

    assert_array_equal(reg.predict([[-0.2, 2]]), np.array([-1]))

def test_ridgecv_store_cv_results(scoring):

    n_samples = 8
    x = rng.randn(n_samples, n_features)
    alphas = [1e-1, 1e0, 1e1]

    r = RidgeCV(alphas=alphas, cv=None, store_cv_results=True, scoring=scoring_)

    # with len(y.shape) == 1
    r.fit(x, y)
    assert r.cv_results_.shape == (n_samples, n_alphas)

    # with len(y.shape) == 2
    assert r.cv_results_.shape == (n_samples, n_targets, n_alphas)

    r = RidgeCV(cv=3, store_cv_results=True, scoring=scoring)
    with pytest.raises(ValueError, match="cv!=None and store_cv_results"):
        r.fit(x, y)

def test_ridge_classifier_cv_store_cv_results(scoring):
    x = np.array([[-1.0, -1.0], [-1.0, 0], [-0.8, -1.0], [1.0, 1.0], [1.0, 0.0]])
    y = np.array([1, 1, 1, -1, -1])

    n_samples = x.shape[0]

    r = RidgeClassifierCV(
        alphas=alphas, cv=None, store_cv_results=True, scoring=scoring_
    )

        [[1, 1, 1, -1, -1], [1, -1, 1, -1, 1], [-1, -1, 1, -1, -1]]

def test_ridgecv_alphas_conversion(Estimator):
    alphas = (0.1, 1.0, 10.0)

    if Estimator is RidgeCV:
        y = rng.randn(n_samples)
    else:
        y = rng.randint(0, 2, n_samples)

    ridge_est = Estimator(alphas=alphas)
    assert ridge_est.alphas is alphas, (
        f"`alphas` was mutated in `{Estimator.__name__}.__init__`"
    )

    ridge_est.fit(X, y)
    assert_array_equal(ridge_est.alphas, np.asarray(alphas))

def test_ridgecv_alphas_zero(cv, Estimator):
    """Check alpha=0.0 raises error only when `cv=None`."""
    alphas = (0.0, 1.0, 10.0)

    else:

    ridge_est = Estimator(alphas=alphas, cv=cv)
    if cv is None:
        with pytest.raises(ValueError, match=r"alphas\[0\] == 0.0, must be > 0.0."):
            ridge_est.fit(X, y)
    else:
        ridge_est.fit(X, y)

def test_ridgecv_sample_weight():

    # There are different algorithms for n_samples > n_features
    # and the opposite, so test them both.
    for n_samples, n_features in ((6, 5), (5, 10)):
        sample_weight = 1.0 + rng.rand(n_samples)

        cv = KFold(5)
        ridgecv = RidgeCV(alphas=alphas, cv=cv)
        ridgecv.fit(X, y, sample_weight=sample_weight)

        # Check using GridSearchCV directly
        parameters = {"alpha": alphas}
        gs = GridSearchCV(Ridge(), parameters, cv=cv)
        gs.fit(X, y, sample_weight=sample_weight)

        assert ridgecv.alpha_ == gs.best_estimator_.alpha
        assert_array_almost_equal(ridgecv.coef_, gs.best_estimator_.coef_)

def test_raises_value_error_if_sample_weights_greater_than_1d():
    # Sample weights must be either scalar or 1D

    n_sampless = [2, 3]
    n_featuress = [3, 2]

    for n_samples, n_features in zip(n_sampless, n_featuress):
        sample_weights_OK = rng.randn(n_samples) ** 2 + 1
        sample_weights_OK_1 = 1.0
        sample_weights_OK_2 = 2.0
        sample_weights_not_OK = sample_weights_OK[:, np.newaxis]
        sample_weights_not_OK_2 = sample_weights_OK[np.newaxis, :]

        ridge = Ridge(alpha=1)

        # make sure the "OK" sample weights actually work
        ridge.fit(X, y, sample_weights_OK)
        ridge.fit(X, y, sample_weights_OK_1)
        ridge.fit(X, y, sample_weights_OK_2)

        def fit_ridge_not_ok():
            ridge.fit(X, y, sample_weights_not_OK)

        def fit_ridge_not_ok_2():
            ridge.fit(X, y, sample_weights_not_OK_2)

        err_msg = "Sample weights must be 1D array or scalar"
            fit_ridge_not_ok()

            fit_ridge_not_ok_2()

def test_sparse_design_with_sample_weights(n_samples, n_features, sparse_container):
    # Sample weights must work with sparse matrices

    sparse_ridge = Ridge(alpha=1.0, fit_intercept=False)
    dense_ridge = Ridge(alpha=1.0, fit_intercept=False)

    sample_weights = rng.randn(n_samples) ** 2 + 1
    sparse_ridge.fit(X_sparse, y, sample_weight=sample_weights)
    dense_ridge.fit(X, y, sample_weight=sample_weights)

    assert_array_almost_equal(sparse_ridge.coef_, dense_ridge.coef_, decimal=6)

def test_ridgecv_int_alphas():

    # Integers
    ridge = RidgeCV(alphas=(1, 10, 100))

def test_ridgecv_alphas_validation(Estimator, params, err_type, err_msg):
    """Check the `alphas` validation in RidgeCV and RidgeClassifierCV."""

        Estimator(**params).fit(X, y)

def test_ridgecv_alphas_scalar(Estimator):
    """Check the case when `alphas` is a scalar.
    This case was supported in the past when `alphas` where converted
    into array in `__init__`.
    We add this test to ensure backward compatibility.
    """

    else:

    Estimator(alphas=1).fit(X, y)

def test_sparse_cg_max_iter():
    reg = Ridge(solver="sparse_cg", max_iter=1)
    reg.fit(X_diabetes, y_diabetes)
    assert reg.coef_.shape[0] == X_diabetes.shape[1]

def test_n_iter():
    # Test that self.n_iter_ is correct.
    n_targets = 2
    X, y = X_diabetes, y_diabetes
    y_n = np.tile(y, (n_targets, 1)).T

    for max_iter in range(1, 4):
        for solver in ("sag", "saga", "lsqr"):
            reg = Ridge(solver=solver, max_iter=max_iter, tol=1e-12)
            reg.fit(X, y_n)
            assert_array_equal(reg.n_iter_, np.tile(max_iter, n_targets))

    for solver in ("sparse_cg", "svd", "cholesky"):
        reg = Ridge(solver=solver, max_iter=1, tol=1e-1)
        reg.fit(X, y_n)
        assert reg.n_iter_ is None

def test_ridge_fit_intercept_sparse(
    solver, with_sample_weight, global_random_seed, csr_container
):
    """Check that ridge finds the same coefs and intercept on dense and sparse input
    in the presence of sample weights.

    For now only sparse_cg and lbfgs can correctly fit an intercept
    with sparse X with default tol and max_iter.
    'sag' is tested separately in test_ridge_fit_intercept_sparse_sag because it
    requires more iterations and should raise a warning if default max_iter is used.
    Other solvers raise an exception, as checked in
    test_ridge_fit_intercept_sparse_error
    """
    positive = solver == "lbfgs"
        n_features=20, random_state=global_random_seed, positive=positive
    )

    sample_weight = None
        sample_weight = 1.0 + rng.uniform(size=X.shape[0])

    # "auto" should switch to "sparse_cg" when X is sparse
    # so the reference we use for both ("auto" and "sparse_cg") is
    # Ridge(solver="sparse_cg"), fitted using the dense representation (note
    # that "sparse_cg" can fit sparse or dense data)
    dense_solver = "sparse_cg" if solver == "auto" else solver
    dense_ridge = Ridge(solver=dense_solver, tol=1e-12, positive=positive)
    sparse_ridge = Ridge(solver=solver, tol=1e-12, positive=positive)

    dense_ridge.fit(X, y, sample_weight=sample_weight)
    sparse_ridge.fit(csr_container(X), y, sample_weight=sample_weight)

    assert_allclose(dense_ridge.intercept_, sparse_ridge.intercept_)
    assert_allclose(dense_ridge.coef_, sparse_ridge.coef_, rtol=5e-7)

def test_ridge_fit_intercept_sparse_error(solver, csr_container):
    X, y = _make_sparse_offset_regression(n_features=20, random_state=0)
    sparse_ridge = Ridge(solver=solver)
    err_msg = "solver='{}' does not support".format(solver)
        sparse_ridge.fit(X_csr, y)

def test_ridge_fit_intercept_sparse_sag(
    with_sample_weight, global_random_seed, csr_container
):
        n_features=5, n_samples=20, random_state=global_random_seed, X_offset=5.0
    )
    else:

        alpha=1.0, solver="sag", fit_intercept=True, tol=1e-10, max_iter=100000
    )
    dense_ridge = Ridge(**params)
    sparse_ridge = Ridge(**params)
        sparse_ridge.fit(X_csr, y, sample_weight=sample_weight)
    assert_allclose(dense_ridge.intercept_, sparse_ridge.intercept_, rtol=1e-4)
    assert_allclose(dense_ridge.coef_, sparse_ridge.coef_, rtol=1e-4)
    with pytest.warns(UserWarning, match='"sag" solver requires.*'):
        Ridge(solver="sag", fit_intercept=True, tol=1e-3, max_iter=None).fit(X_csr, y)

def test_ridge_regression_check_arguments_validity(
    return_intercept, sample_weight, container, solver
):
    """check if all combinations of arguments give valid estimations"""

    # test excludes 'svd' solver because it raises exception for sparse inputs

    X = rng.rand(1000, 3)
    true_coefs = [1, 2, 0.1]
    y = np.dot(X, true_coefs)
    true_intercept = 0.0
        true_intercept = 10000.0
    y += true_intercept
    X_testing = container(X)

    alpha, tol = 1e-3, 1e-6
    atol = 1e-3 if _IS_32BIT else 1e-4

    if solver not in ["sag", "auto"] and return_intercept:
        with pytest.raises(ValueError, match="In Ridge, only 'sag' solver"):
            ridge_regression(
                X_testing,
                y,
                return_intercept=return_intercept,
                tol=tol,
            )
        return

    out = ridge_regression(
        X_testing,
        y,
        tol=tol,
    )

        coef, intercept = out
        assert_allclose(coef, true_coefs, rtol=0, atol=atol)
        assert_allclose(intercept, true_intercept, rtol=0, atol=atol)
    else:
        assert_allclose(out, true_coefs, rtol=0, atol=atol)

def test_dtype_match(solver):
    alpha = 1.0

    n_samples, n_features = 6, 5
    X_64 = rng.randn(n_samples, n_features)
    y_64 = rng.randn(n_samples)
    X_32 = X_64.astype(np.float32)
    y_32 = y_64.astype(np.float32)

    tol = 2 * np.finfo(np.float32).resolution
    # Check type consistency 32bits
    ridge_32 = Ridge(
        alpha=alpha, solver=solver, max_iter=500, tol=tol, positive=positive
    )
    ridge_32.fit(X_32, y_32)
    coef_32 = ridge_32.coef_

    # Check type consistency 64 bits
    ridge_64 = Ridge(
    )
    ridge_64.fit(X_64, y_64)
    coef_64 = ridge_64.coef_

    # Do the actual checks at once for easier debug
    assert coef_32.dtype == X_32.dtype
    assert coef_64.dtype == X_64.dtype
    assert ridge_32.predict(X_32).dtype == X_32.dtype
    assert ridge_64.predict(X_64).dtype == X_64.dtype
    assert_allclose(ridge_32.coef_, ridge_64.coef_, rtol=1e-4, atol=5e-4)

def test_dtype_match_cholesky():
    # Test different alphas in cholesky solver to ensure full coverage.
    # This test is separated from test_dtype_match for clarity.
    alpha = np.array([1.0, 0.5])

    n_samples, n_features, n_target = 6, 7, 2
    y_64 = rng.randn(n_samples, n_target)

    ridge_32 = Ridge(alpha=alpha, solver="cholesky")

    ridge_64 = Ridge(alpha=alpha, solver="cholesky")

    # Do all the checks at once, like this is easier to debug
    assert_almost_equal(ridge_32.coef_, ridge_64.coef_, decimal=5)

def test_ridge_regression_dtype_stability(solver, seed):
    coef = random_state.randn(n_features)
    y = np.dot(X, coef) + 0.01 * random_state.randn(n_samples)
    alpha = 1.0
    results = dict()
    # XXX: Sparse CG seems to be far less numerically stable than the
    # others, maybe we should not enable float32 for this one.
    atol = 1e-3 if solver == "sparse_cg" else 1e-5
    for current_dtype in (np.float32, np.float64):
        results[current_dtype] = ridge_regression(
            X.astype(current_dtype),
            y.astype(current_dtype),
            tol=1e-10,
            return_n_iter=False,
            return_intercept=False,
        )

    assert results[np.float32].dtype == np.float32
    assert results[np.float64].dtype == np.float64
    assert_allclose(results[np.float32], results[np.float64], atol=atol)

def test_ridge_sag_with_X_fortran():
    # check that Fortran array are converted when using SAG solver
    # for the order of X and y to not be C-ordered arrays
    X = np.asfortranarray(X)
    X = X[::2, :]
    y = y[::2]
    Ridge(solver="sag").fit(X, y)

def test_ridgeclassifier_multilabel(Classifier, params):
    """Check that multilabel classification is supported and give meaningful
    results."""
    X, y = make_multilabel_classification(n_classes=1, random_state=0)
    y = y.reshape(-1, 1)
    Y = np.concatenate([y, y], axis=1)
    clf = Classifier(**params).fit(X, Y)
    Y_pred = clf.predict(X)

    assert_array_equal(Y_pred[:, 0], Y_pred[:, 1])

def test_ridge_positive_regression_test(solver, fit_intercept, alpha):
    """Test that positive Ridge finds true positive coefficients."""
    X = np.array([[1, 2], [3, 4], [5, 6], [7, 8]])
    coef = np.array([1, -10])
        intercept = 20
        y = X.dot(coef) + intercept
    else:
        y = X.dot(coef)

        alpha=alpha, positive=True, solver=solver, fit_intercept=fit_intercept
    )
    assert np.all(model.coef_ >= 0)

def test_ridge_ground_truth_positive_test(fit_intercept, alpha):
    """Test that Ridge w/wo positive converges to the same solution.

    Ridge with positive=True and positive=False must give the same
    when the ground truth coefs are all positive.
    """
    X = rng.randn(300, 100)
    coef = rng.uniform(0.1, 1.0, size=X.shape[1])
        intercept = 1
        y = X @ coef + intercept
    else:
        y = X @ coef
    y += rng.normal(size=X.shape[0]) * 0.01

    for positive in [True, False]:
        model = Ridge(
            alpha=alpha, positive=positive, fit_intercept=fit_intercept, tol=1e-10
        )
        results.append(model.fit(X, y).coef_)
    assert_allclose(*results, atol=1e-6, rtol=0)

def test_ridge_positive_error_test(solver):
    """Test input validation for positive argument in Ridge."""
    alpha = 0.1
    coef = np.array([1, -1])
    y = X @ coef

    model = Ridge(alpha=alpha, positive=True, solver=solver, fit_intercept=False)
    with pytest.raises(ValueError, match="does not support positive"):

    with pytest.raises(ValueError, match="only 'lbfgs' solver can be used"):
        _, _ = ridge_regression(
            X, y, alpha, positive=True, solver=solver, return_intercept=False
        )

def test_positive_ridge_loss(alpha):
    """Check ridge loss consistency when positive argument is enabled."""
    X, y = make_regression(n_samples=300, n_features=300, random_state=42)
    alpha = 0.10
    n_checks = 100

    def ridge_loss(model, random_state=None, noise_scale=1e-8):
        intercept = model.intercept_
            rng = np.random.RandomState(random_state)
            coef = model.coef_ + rng.uniform(0, noise_scale, size=model.coef_.shape)
        else:
            coef = model.coef_

        return 0.5 * np.sum((y - X @ coef - intercept) ** 2) + 0.5 * alpha * np.sum(
            coef**2
        )

    model = Ridge(alpha=alpha).fit(X, y)
    model_positive = Ridge(alpha=alpha, positive=True).fit(X, y)

    # Check 1:
    #   Loss for solution found by Ridge(positive=False)
    #   is lower than that for solution found by Ridge(positive=True)
    loss = ridge_loss(model)
    loss_positive = ridge_loss(model_positive)
    assert loss <= loss_positive

    # Check 2:
    #   Loss for solution found by Ridge(positive=True)
    #   is lower than that for small random positive perturbation
    #   of the positive solution.
    for random_state in range(n_checks):
        loss_perturbed = ridge_loss(model_positive, random_state=random_state)
        assert loss_positive <= loss_perturbed

def test_lbfgs_solver_consistency(alpha):
    """Test that LBGFS gets almost the same coef of svd when positive=False."""
    y = np.expand_dims(y, 1)
    alpha = np.asarray([alpha])
    config = {
        "positive": False,
        "tol": 1e-16,
        "max_iter": 500000,
    }

    coef_lbfgs = _solve_lbfgs(X, y, alpha, **config)
    coef_cholesky = _solve_svd(X, y, alpha)
    assert_allclose(coef_lbfgs, coef_cholesky, atol=1e-4, rtol=0)

def test_lbfgs_solver_error():
    """Test that LBFGS solver raises ConvergenceWarning."""
    X = np.array([[1, -1], [1, 1]])
    y = np.array([-1e10, 1e10])

        positive=True,
    )
    with pytest.warns(ConvergenceWarning, match="lbfgs solver did not converge"):

def test_ridge_sample_weight_consistency(
    fit_intercept, sparse_container, data, solver, global_random_seed
):

    check_sample_weight_equivalence alone.
    """
    # filter out solver that do not support sparse input
        if solver == "svd" or (solver in ("cholesky", "saga") and fit_intercept):
            pytest.skip("unsupported configuration")

    # XXX: this test is quite sensitive to the seed used to generate the data:
    # ideally we would like the test to pass for any global_random_seed but this is not
    # the case at the moment.
    n_samples = 12
    if data == "tall":
        n_features = n_samples // 2
    else:
        n_features = n_samples * 2

        positive=(solver == "lbfgs"),
        random_state=global_random_seed,  # for sag/saga
    )

    # 1) sample_weight=np.ones(..) should be equivalent to sample_weight=None,
    # a special case of check_sample_weight_equivalence(name, reg), but we also
    # test with sparse input.
    reg = Ridge(**params).fit(X, y, sample_weight=None)

    # 2) setting elements of sample_weight to 0 is equivalent to removing these samples,
    # another special case of check_sample_weight_equivalence(name, reg), but we
    # also test with sparse input
    sample_weight[-5:] = 0
    reg.fit(X[:-5, :], y[:-5], sample_weight=sample_weight[:-5])

    # 3) scaling of sample_weight should have no effect
    # Note: For models with penalty, scaling the penalty term might work.
    reg2 = Ridge(**params).set_params(alpha=np.pi * params["alpha"])
    reg2.fit(X, y, sample_weight=np.pi * sample_weight)
    if solver in ("sag", "saga") and not fit_intercept:
        pytest.xfail(f"Solver {solver} does fail test for scaling of sample_weight.")
    assert_allclose(reg2.coef_, coef, rtol=1e-6)
        assert_allclose(reg2.intercept_, intercept)

    # 4) check that multiplying sample_weight by 2 is equivalent
    )
        X2 = sparse_container(X2)
    reg1 = Ridge(**params).fit(X, y, sample_weight=sample_weight_1)
    reg2 = Ridge(**params).fit(X2, y2, sample_weight=sample_weight_2)
    assert_allclose(reg1.coef_, reg2.coef_)
        assert_allclose(reg1.intercept_, reg2.intercept_)

def test_ridge_cv_results_predictions(with_sample_weight, fit_intercept, n_targets):
    """Check that the predictions stored in `cv_results_` are on the original scale.

    The GCV approach works on scaled data: centered by an offset and scaled by the
    square root of the sample weights. Thus, prior to computing scores, the
    predictions need to be scaled back to the original scale. These predictions are
    the ones stored in `cv_results_` in `RidgeCV`.

    In this test, we check that the internal predictions stored in `cv_results_` are
    equivalent to a naive LOO-CV grid search with a `Ridge` estimator.

    https://github.com/scikit-learn/scikit-learn/issues/13998
    """
        n_samples=100, n_features=10, n_targets=n_targets, random_state=0
    )
    sample_weight = np.ones(shape=(X.shape[0],))
        sample_weight[::2] = 0.5

    # scoring should be set to store predictions and not the squared error
    ridge_cv = RidgeCV(
    )
    ridge_cv.fit(X, y, sample_weight=sample_weight)

    # manual grid-search with a `Ridge` estimator
    predictions = np.empty(shape=(*y.shape, len(alphas)))
    cv = LeaveOneOut()
    for alpha_idx, alpha in enumerate(alphas):
        for idx, (train_idx, test_idx) in enumerate(cv.split(X, y)):
            ridge = Ridge(alpha=alpha, fit_intercept=fit_intercept)
            ridge.fit(X[train_idx], y[train_idx], sample_weight[train_idx])
            predictions[idx, ..., alpha_idx] = ridge.predict(X[test_idx])
    assert_allclose(ridge_cv.cv_results_, predictions)

def test_ridge_cv_multioutput_sample_weight(global_random_seed):
    """Check that `RidgeCV` works properly with multioutput and sample_weight
    when `scoring != None`.

    We check the error reported by the RidgeCV is close to a naive LOO-CV using a
    Ridge estimator.
    """
    X, y = make_regression(n_targets=2, random_state=global_random_seed)

    ridge_cv = RidgeCV(scoring="neg_mean_squared_error", store_cv_results=True)

    ridge = Ridge(alpha=ridge_cv.alpha_)
    y_pred_loo = np.squeeze(
        [
            ridge.fit(X[train], y[train], sample_weight=sample_weight[train]).predict(
                X[test]
            )
            for train, test in cv.split(X)
        ]
    )
    assert_allclose(ridge_cv.best_score_, -mean_squared_error(y, y_pred_loo))

def test_ridge_cv_custom_multioutput_scorer():
    """Check that `RidgeCV` works properly with a custom multioutput scorer."""
    X, y = make_regression(n_targets=2, random_state=0)

    def custom_error(y_true, y_pred):
        errors = (y_true - y_pred) ** 2
        mean_errors = np.mean(errors, axis=0)
        if mean_errors.ndim == 1:
            # case of multioutput
            return -np.average(mean_errors, weights=[2, 1])
        # single output - this part of the code should not be reached in the case of
        # multioutput scoring
        return -mean_errors  # pragma: no cover

    def custom_multioutput_scorer(estimator, X, y):
        """Multioutput score that give twice more importance to the second target."""
        return -custom_error(y, estimator.predict(X))

    ridge_cv = RidgeCV(scoring=custom_multioutput_scorer)
    ridge_cv.fit(X, y)

        [ridge.fit(X[train], y[train]).predict(X[test]) for train, test in cv.split(X)]
    )

    assert_allclose(ridge_cv.best_score_, -custom_error(y, y_pred_loo))

def test_metadata_routing_with_default_scoring(metaestimator):
    """Test that `RidgeCV` or `RidgeClassifierCV` with default `scoring`
    argument (`None`), don't enter into `RecursionError` when metadata is routed.
    """
    metaestimator().get_metadata_routing()

def test_set_score_request_with_default_scoring(metaestimator, make_dataset):
    """Test that `set_score_request` is set within `RidgeCV.fit()` and
    `RidgeClassifierCV.fit()` when using the default scoring and no
    UnsetMetadataPassedError is raised. Regression test for the fix in PR #29634."""
    X, y = make_dataset(n_samples=100, n_features=5, random_state=42)
    metaestimator.fit(X, y, sample_weight=np.ones(X.shape[0]))

        else:

            coef**2
        )

def test_linear_regression():
    # Test LinearRegression on a simple dataset.
    # a simple dataset
    X = [[1], [2]]
    Y = [1, 2]

    reg = LinearRegression()
    reg.fit(X, Y)

    assert_array_almost_equal(reg.coef_, [1])
    assert_array_almost_equal(reg.intercept_, [0])
    assert_array_almost_equal(reg.predict(X), [1, 2])

    # test it also for degenerate input
    X = [[1]]
    Y = [0]

    assert_array_almost_equal(reg.coef_, [0])
    assert_array_almost_equal(reg.predict(X), [0])

def test_linear_regression_sample_weights(
    sparse_container, fit_intercept, global_random_seed
):

    # It would not work with under-determined systems

    sample_weight = 1.0 + rng.uniform(size=n_samples)

    # LinearRegression with explicit sample_weight
    reg = LinearRegression(fit_intercept=fit_intercept, tol=1e-16)
    coefs1 = reg.coef_
    inter1 = reg.intercept_

    assert reg.coef_.shape == (X.shape[1],)  # sanity checks

    # Closed form of the weighted least square
    # theta = (X^T W X)^(-1) @ X^T W y
    W = np.diag(sample_weight)
    X_aug = X if not fit_intercept else add_dummy_feature(X)

    Xw = X_aug.T @ W @ X_aug
    yw = X_aug.T @ W @ y
    coefs2 = linalg.solve(Xw, yw)

    if not fit_intercept:
        assert_allclose(coefs1, coefs2)
    else:
        assert_allclose(coefs1, coefs2[1:])
        assert_allclose(inter1, coefs2[0])

def test_raises_value_error_if_positive_and_sparse():
    error_msg = "Sparse data was passed for X, but dense data is required."
    # X must not be sparse if positive == True
    X = sparse.eye(10)

    reg = LinearRegression(positive=True)

    with pytest.raises(TypeError, match=error_msg):

def test_raises_value_error_if_sample_weights_greater_than_1d(n_samples, n_features):
    sample_weights_OK = rng.randn(n_samples) ** 2 + 1
    sample_weights_OK_1 = 1.0
    sample_weights_OK_2 = 2.0

    # make sure the "OK" sample weights actually work
    reg.fit(X, y, sample_weights_OK)
    reg.fit(X, y, sample_weights_OK_1)
    reg.fit(X, y, sample_weights_OK_2)

def test_fit_intercept():
    # Test assertions on betas shape.
    X2 = np.array([[0.38349978, 0.61650022], [0.58853682, 0.41146318]])
    X3 = np.array(
        [[0.27677969, 0.70693172, 0.01628859], [0.08385139, 0.20692515, 0.70922346]]
    )
    y = np.array([1, 1])

    lr2_without_intercept = LinearRegression(fit_intercept=False).fit(X2, y)
    lr2_with_intercept = LinearRegression().fit(X2, y)

    lr3_without_intercept = LinearRegression(fit_intercept=False).fit(X3, y)
    lr3_with_intercept = LinearRegression().fit(X3, y)

    assert lr2_with_intercept.coef_.shape == lr2_without_intercept.coef_.shape
    assert lr3_with_intercept.coef_.shape == lr3_without_intercept.coef_.shape
    assert lr2_without_intercept.coef_.ndim == lr3_without_intercept.coef_.ndim

def test_linear_regression_sparse(global_random_seed):
    # Test that linear regression also works with sparse data
    n = 100
    X = sparse.eye(n, n)
    beta = rng.rand(n)
    y = X @ beta

    ols = LinearRegression()
    ols.fit(X, y.ravel())
    assert_array_almost_equal(beta, ols.coef_ + ols.intercept_)

    assert_array_almost_equal(ols.predict(X) - y.ravel(), 0)

def test_linear_regression_sparse_equal_dense(fit_intercept, csr_container):
    # Test that linear regression agrees between sparse and dense
    params = dict(fit_intercept=fit_intercept)
    clf_dense = LinearRegression(**params)
    clf_sparse = LinearRegression(**params)
    clf_dense.fit(X, y)
    clf_sparse.fit(Xcsr, y)
    assert clf_dense.intercept_ == pytest.approx(clf_sparse.intercept_)
    assert_allclose(clf_dense.coef_, clf_sparse.coef_)

def test_linear_regression_multiple_outcome():
    # Test multiple-outcome linear regressions
    X, y = make_regression(random_state=rng)

    Y = np.vstack((y, y)).T

    reg.fit((X), Y)
    assert reg.coef_.shape == (2, n_features)
    Y_pred = reg.predict(X)

def test_linear_regression_sparse_multiple_outcome(global_random_seed, coo_container):
    # Test multiple-outcome linear regressions with sparse data
    X, y = make_sparse_uncorrelated(random_state=rng)
    X = coo_container(X)

    ols.fit(X, Y)
    assert ols.coef_.shape == (2, n_features)
    Y_pred = ols.predict(X)
    y_pred = ols.predict(X)

def test_linear_regression_positive():
    # Test nonnegative LinearRegression on a simple dataset.
    y = [1, 2]

    X = [[1]]
    y = [0]

    assert_allclose(reg.coef_, [0])
    assert_allclose(reg.intercept_, [0])
    assert_allclose(reg.predict(X), [0])

def test_linear_regression_positive_multiple_outcome(global_random_seed):
    # Test multiple-outcome nonnegative linear regressions

    ols = LinearRegression(positive=True)
    assert np.all(ols.coef_ >= 0.0)
    assert_allclose(np.vstack((y_pred, y_pred)).T, Y_pred)

def test_linear_regression_positive_vs_nonpositive(global_random_seed):
    # Test differences with LinearRegression when positive=False.

    regn = LinearRegression(positive=False)
    regn.fit(X, y)

    assert np.mean((reg.coef_ - regn.coef_) ** 2) > 1e-3

def test_linear_regression_positive_vs_nonpositive_when_positive(global_random_seed):
    # Test LinearRegression fitted coefficients
    # when the problem is positive.
    y = X[:, 0] + 2 * X[:, 1] + 3 * X[:, 2] + 1.5 * X[:, 3]

    assert np.mean((reg.coef_ - regn.coef_) ** 2) < 1e-6

def test_inplace_data_preprocessing(sparse_container, use_sw, global_random_seed):
    # Check that the data is not modified inplace by the linear regression
    original_X_data = rng.randn(10, 12)
    original_y_data = rng.randn(10, 2)
    orginal_sw_data = rng.rand(10)

        X = sparse_container(original_X_data)
    else:
        X = original_X_data.copy()
    y = original_y_data.copy()
    # XXX: Note hat y_sparse is not supported (broken?) in the current
    # implementation of LinearRegression.

    if use_sw:
        sample_weight = orginal_sw_data.copy()
    else:

    # Do not allow inplace preprocessing of X and y:
        assert_allclose(X.toarray(), original_X_data)
    else:
        assert_allclose(X, original_X_data)
    assert_allclose(y, original_y_data)

    if use_sw:
        assert_allclose(sample_weight, orginal_sw_data)

    # Allow inplace preprocessing of X and y
    reg = LinearRegression(copy_X=False)
        # No optimization relying on the inplace modification of sparse input
        # data has been implemented at this time.
    else:
        # X has been offset (and optionally rescaled by sample weights)
        # inplace. The 0.42 threshold is arbitrary and has been found to be
        # robust to any random seed in the admissible range.
        assert np.linalg.norm(X - original_X_data) > 0.42

    # y should not have been modified inplace by LinearRegression.fit.

    if use_sw:
        # Sample weights have no reason to ever be modified inplace.

def test_linear_regression_pd_sparse_dataframe_warning():

    # Warning is raised only when some of the columns is sparse
    df = pd.DataFrame({"0": np.random.randn(10)})
    for col in range(1, 4):
        arr = np.random.randn(10)
        arr[:8] = 0
        # all columns but the first column is sparse
        if col != 0:
            arr = pd.arrays.SparseArray(arr, fill_value=0)
        df[str(col)] = arr

    msg = "pandas.DataFrame with sparse columns found."

        reg.fit(df.iloc[:, 0:2], df.iloc[:, 3])

    # does not warn when the whole dataframe is sparse
    df["0"] = pd.arrays.SparseArray(df["0"], fill_value=0)
    assert hasattr(df, "sparse")

def test_preprocess_data(global_random_seed):
    expected_X_mean = np.mean(X, axis=0)
    expected_y_mean = np.mean(y, axis=0)

    Xt, yt, X_mean, y_mean, X_scale = _preprocess_data(X, y, fit_intercept=False)
    assert_array_almost_equal(X_mean, np.zeros(n_features))
    assert_array_almost_equal(y_mean, 0)
    assert_array_almost_equal(X_scale, np.ones(n_features))
    assert_array_almost_equal(Xt, X)
    assert_array_almost_equal(yt, y)

    Xt, yt, X_mean, y_mean, X_scale = _preprocess_data(X, y, fit_intercept=True)
    assert_array_almost_equal(X_mean, expected_X_mean)
    assert_array_almost_equal(y_mean, expected_y_mean)
    assert_array_almost_equal(Xt, X - expected_X_mean)
    assert_array_almost_equal(yt, y - expected_y_mean)

def test_preprocess_data_multioutput(global_random_seed, sparse_container):
    n_outputs = 2
    y = rng.rand(n_samples, n_outputs)

    _, yt, _, y_mean, _ = _preprocess_data(X, y, fit_intercept=False)
    assert_array_almost_equal(y_mean, np.zeros(n_outputs))

    _, yt, _, y_mean, _ = _preprocess_data(X, y, fit_intercept=True)
    assert_array_almost_equal(yt, y - y_mean)

def test_preprocess_data_weighted(sparse_container, global_random_seed):
    # Generate random data with 50% of zero values to make sure
    # that the sparse variant of this test is actually sparse. This also
    # shifts the mean value for each columns in X further away from
    # zero.
    X[X < 0.5] = 0.0

    # Scale the first feature of X to be 10 larger than the other to
    # better check the impact of feature scaling.
    X[:, 0] *= 10

    # Constant non-zero feature.
    X[:, 2] = 1.0

    # Constant zero feature (non-materialized in the sparse case)
    X[:, 3] = 0.0

    sample_weight = rng.rand(n_samples)
    expected_X_mean = np.average(X, axis=0, weights=sample_weight)
    expected_y_mean = np.average(y, axis=0, weights=sample_weight)

    X_sample_weight_avg = np.average(X, weights=sample_weight, axis=0)
    X_sample_weight_var = np.average(
        (X - X_sample_weight_avg) ** 2, weights=sample_weight, axis=0
    )
    constant_mask = X_sample_weight_var < 10 * np.finfo(X.dtype).eps
    assert_array_equal(constant_mask, [0, 0, 1, 1])
    expected_X_scale = np.sqrt(X_sample_weight_var) * np.sqrt(sample_weight.sum())

    # near constant features should not be scaled
    expected_X_scale[constant_mask] = 1

    # normalize is False
    Xt, yt, X_mean, y_mean, X_scale = _preprocess_data(
        X,
        y,
    )
        assert_array_almost_equal(Xt.toarray(), X.toarray())
    else:
        assert_array_almost_equal(Xt, X - expected_X_mean)

def test_sparse_preprocess_data_offsets(global_random_seed, lil_container):
    X = sparse.rand(n_samples, n_features, density=0.5, random_state=rng)
    X = lil_container(X)
    XA = X.toarray()

    assert_array_almost_equal(Xt.toarray(), XA)

    assert_array_almost_equal(X_mean, np.mean(XA, axis=0))
    assert_array_almost_equal(y_mean, np.mean(y, axis=0))
    assert_array_almost_equal(yt, y - np.mean(y, axis=0))

def test_csr_preprocess_data(csr_container):
    # Test output format of _preprocess_data, when input is csr
    X, y = make_regression()
    X[X < 2.5] = 0.0
    csr_, y, _, _, _ = _preprocess_data(csr, y, fit_intercept=True)
    assert csr_.format == "csr"

def test_preprocess_copy_data_no_checks(sparse_container, to_copy):

    X_, y_, _, _, _ = _preprocess_data(
        X, y, fit_intercept=True, copy=to_copy, check_input=False
    )

    if to_copy and sparse_container is not None:
        assert not np.may_share_memory(X_.data, X.data)
    elif to_copy:
        assert not np.may_share_memory(X_, X)
    elif sparse_container is not None:
        assert np.may_share_memory(X_.data, X.data)
    else:
        assert np.may_share_memory(X_, X)

def test_dtype_preprocess_data(global_random_seed):

    X_32 = np.asarray(X, dtype=np.float32)
    y_32 = np.asarray(y, dtype=np.float32)
    X_64 = np.asarray(X, dtype=np.float64)
    y_64 = np.asarray(y, dtype=np.float64)

        Xt_32, yt_32, X_mean_32, y_mean_32, X_scale_32 = _preprocess_data(
            X_32,
            y_32,
        )

        Xt_64, yt_64, X_mean_64, y_mean_64, X_scale_64 = _preprocess_data(
            X_64,
            y_64,
        )

        Xt_3264, yt_3264, X_mean_3264, y_mean_3264, X_scale_3264 = _preprocess_data(
            X_32,
            y_64,
        )

        Xt_6432, yt_6432, X_mean_6432, y_mean_6432, X_scale_6432 = _preprocess_data(
            X_64,
            y_32,
        )

        assert Xt_32.dtype == np.float32
        assert yt_32.dtype == np.float32
        assert X_mean_32.dtype == np.float32
        assert y_mean_32.dtype == np.float32
        assert X_scale_32.dtype == np.float32

        assert Xt_64.dtype == np.float64
        assert yt_64.dtype == np.float64
        assert X_mean_64.dtype == np.float64
        assert y_mean_64.dtype == np.float64
        assert X_scale_64.dtype == np.float64

        assert Xt_3264.dtype == np.float32
        assert yt_3264.dtype == np.float32
        assert X_mean_3264.dtype == np.float32
        assert y_mean_3264.dtype == np.float32
        assert X_scale_3264.dtype == np.float32

        assert Xt_6432.dtype == np.float64
        assert yt_6432.dtype == np.float64
        assert X_mean_6432.dtype == np.float64
        assert y_mean_6432.dtype == np.float64
        assert X_scale_6432.dtype == np.float64

        assert X_32.dtype == np.float32
        assert y_32.dtype == np.float32
        assert X_64.dtype == np.float64
        assert y_64.dtype == np.float64

        assert_array_almost_equal(Xt_32, Xt_64)
        assert_array_almost_equal(yt_32, yt_64)
        assert_array_almost_equal(X_mean_32, X_mean_64)
        assert_array_almost_equal(y_mean_32, y_mean_64)
        assert_array_almost_equal(X_scale_32, X_scale_64)

def test_rescale_data(n_targets, sparse_container, global_random_seed):

    sample_weight = 1.0 + rng.rand(n_samples)
    if n_targets is None:
        y = rng.rand(n_samples)
    else:
        y = rng.rand(n_samples, n_targets)

    expected_sqrt_sw = np.sqrt(sample_weight)
    expected_rescaled_X = X * expected_sqrt_sw[:, np.newaxis]

        expected_rescaled_y = y * expected_sqrt_sw
    else:
        expected_rescaled_y = y * expected_sqrt_sw[:, np.newaxis]

        if n_targets is None:
            y = sparse_container(y.reshape(-1, 1))
        else:
            y = sparse_container(y)

    rescaled_X, rescaled_y, sqrt_sw = _rescale_data(X, y, sample_weight)

    assert_allclose(sqrt_sw, expected_sqrt_sw)

        rescaled_X = rescaled_X.toarray()
        rescaled_y = rescaled_y.toarray()
            rescaled_y = rescaled_y.ravel()

    assert_allclose(rescaled_X, expected_rescaled_X)
    assert_allclose(rescaled_y, expected_rescaled_y)

def test_fused_types_make_dataset(csr_container):

    X_32 = iris.data.astype(np.float32)
    y_32 = iris.target.astype(np.float32)
    X_csr_32 = csr_container(X_32)
    sample_weight_32 = np.arange(y_32.size, dtype=np.float32)

    X_64 = iris.data.astype(np.float64)
    y_64 = iris.target.astype(np.float64)
    X_csr_64 = csr_container(X_64)
    sample_weight_64 = np.arange(y_64.size, dtype=np.float64)

    # array
    dataset_32, _ = make_dataset(X_32, y_32, sample_weight_32)
    dataset_64, _ = make_dataset(X_64, y_64, sample_weight_64)
    xi_32, yi_32, _, _ = dataset_32._next_py()
    xi_64, yi_64, _, _ = dataset_64._next_py()
    xi_data_32, _, _ = xi_32
    xi_data_64, _, _ = xi_64

    assert xi_data_32.dtype == np.float32
    assert xi_data_64.dtype == np.float64
    assert_allclose(yi_64, yi_32, rtol=rtol)

    # csr
    datasetcsr_32, _ = make_dataset(X_csr_32, y_32, sample_weight_32)
    datasetcsr_64, _ = make_dataset(X_csr_64, y_64, sample_weight_64)
    xicsr_32, yicsr_32, _, _ = datasetcsr_32._next_py()
    xicsr_64, yicsr_64, _, _ = datasetcsr_64._next_py()
    xicsr_data_32, _, _ = xicsr_32
    xicsr_data_64, _, _ = xicsr_64

    assert xicsr_data_32.dtype == np.float32
    assert xicsr_data_64.dtype == np.float64

    assert_allclose(xicsr_data_64, xicsr_data_32, rtol=rtol)
    assert_allclose(yicsr_64, yicsr_32, rtol=rtol)

    assert_array_equal(xi_data_32, xicsr_data_32)
    assert_array_equal(xi_data_64, xicsr_data_64)
    assert_array_equal(yi_32, yicsr_32)
    assert_array_equal(yi_64, yicsr_64)

def test_linear_regression_sample_weight_consistency(
    X_shape, sparse_container, fit_intercept, global_random_seed
):

    It is very similar to test_enet_sample_weight_consistency.
    """

    reg = LinearRegression(**params).fit(X, y, sample_weight=None)

    # 1) sample_weight=np.ones(..) must be equivalent to sample_weight=None,

    assert_allclose(reg.coef_, coef, rtol=1e-6 if sparse_container is None else 1e-5)

    assert_allclose(reg.coef_, coef_0, rtol=1e-5)

    else:
    )

    reg1 = LinearRegression(**params).fit(X, y, sample_weight=sample_weight_1)
    reg2 = LinearRegression(**params).fit(X2, y2, sample_weight=sample_weight_2)

def log_dloss(p, y):
    z = p * y
    # approximately equal and saves the computation of the log
    if z > 18.0:
        return math.exp(-z) * -y
    if z < -18.0:
        return -y
    return -y / (math.exp(z) + 1.0)

def log_loss(p, y):
    return np.mean(np.log(1.0 + np.exp(-y * p)))

def squared_loss(p, y):
    return np.mean(0.5 * (p - y) * (p - y))

def get_pobj(w, alpha, myX, myy, loss):
    w = w.ravel()
    pred = np.dot(myX, w)
    p = loss(pred, myy)
    p += alpha * w.dot(w) / 2.0
    return p

def sag(
    X,
    y,
    step_size,
    alpha,
    n_iter=1,
    dloss=None,
    saga=False,
):

    weights = np.zeros(X.shape[1])
    sum_gradient = np.zeros(X.shape[1])
    gradient_memory = np.zeros((n_samples, n_features))

    intercept = 0.0
    intercept_sum_gradient = 0.0
    intercept_gradient_memory = np.zeros(n_samples)

    rng = np.random.RandomState(77)
    decay = 1.0
    seen = set()

    if sparse:

    for epoch in range(n_iter):
        for k in range(n_samples):
            idx = int(rng.rand() * n_samples)
            # idx = k
            entry = X[idx]
            seen.add(idx)
            p = np.dot(entry, weights) + intercept
            gradient = dloss(p, y[idx])
                gradient *= sample_weight[idx]
            update = entry * gradient + alpha * weights
            gradient_correction = update - gradient_memory[idx]
            sum_gradient += gradient_correction
            gradient_memory[idx] = update
            if saga:
                weights -= gradient_correction * step_size * (1 - 1.0 / len(seen))

                gradient_correction = gradient - intercept_gradient_memory[idx]
                intercept_gradient_memory[idx] = gradient
                intercept_sum_gradient += gradient_correction
                gradient_correction *= step_size * (1.0 - 1.0 / len(seen))
                if saga:
                    intercept -= (
                        step_size * intercept_sum_gradient / len(seen) * decay
                    ) + gradient_correction
                else:
                    intercept -= step_size * intercept_sum_gradient / len(seen) * decay

            weights -= step_size * sum_gradient / len(seen)

    return weights, intercept

def sag_sparse(
    X,
    y,
    alpha,
    n_iter=1,
):
    if step_size * alpha == 1.0:
            "Sparse sag does not handle the case step_size * alpha == 1"
        )

    weights = np.zeros(n_features)
    sum_gradient = np.zeros(n_features)
    last_updated = np.zeros(n_features, dtype=int)
    gradient_memory = np.zeros(n_samples)
    wscale = 1.0
    decay = 1.0

    c_sum = np.zeros(n_iter * n_samples)

    if sparse:

    counter = 0
            # idx = k

            if counter >= 1:
                for j in range(n_features):
                    if last_updated[j] == 0:
                        weights[j] -= c_sum[counter - 1] * sum_gradient[j]
                    else:
                        weights[j] -= (
                            c_sum[counter - 1] - c_sum[last_updated[j] - 1]
                        ) * sum_gradient[j]
                    last_updated[j] = counter

            p = (wscale * np.dot(entry, weights)) + intercept

            update = entry * gradient
            gradient_correction = update - (gradient_memory[idx] * entry)
            if saga:
                    weights[j] -= (
                        gradient_correction[j]
                        * step_size
                        * (1 - 1.0 / len(seen))
                        / wscale
                    )

                gradient_correction = gradient - gradient_memory[idx]
                if saga:
                else:

            gradient_memory[idx] = gradient

            wscale *= 1.0 - alpha * step_size
            if counter == 0:
                c_sum[0] = step_size / (wscale * len(seen))
            else:
                c_sum[counter] = c_sum[counter - 1] + step_size / (wscale * len(seen))

            if counter >= 1 and wscale < 1e-9:
                        weights[j] -= c_sum[counter] * sum_gradient[j]
                    else:
                            c_sum[counter] - c_sum[last_updated[j] - 1]
                    last_updated[j] = counter + 1
                c_sum[counter] = 0
                weights *= wscale
                wscale = 1.0

            counter += 1

        if last_updated[j] == 0:
            weights[j] -= c_sum[counter - 1] * sum_gradient[j]
        else:
            weights[j] -= (
                c_sum[counter - 1] - c_sum[last_updated[j] - 1]
            ) * sum_gradient[j]
    weights *= wscale

def get_step_size(X, alpha, fit_intercept, classification=True):
    if classification:
        return 4.0 / (np.max(np.sum(X * X, axis=1)) + fit_intercept + 4.0 * alpha)
    else:
        return 1.0 / (np.max(np.sum(X * X, axis=1)) + fit_intercept + alpha)

def test_classifier_matching():
    X, y = make_blobs(n_samples=n_samples, centers=2, random_state=0, cluster_std=0.1)
    # y must be 0 or 1
    alpha = 1.1
    fit_intercept = True
    step_size = get_step_size(X, alpha, fit_intercept)
        if solver == "sag":
            n_iter = 80
        else:
            # SAGA variance w.r.t. stream order is higher
            n_iter = 300
        clf = LogisticRegression(
            tol=1e-11,
            C=1.0 / alpha / n_samples,
            random_state=10,
        )

        weights, intercept = sag_sparse(
            X,
            2 * y - 1,  # y must be -1 or +1
            step_size,
            alpha,
            n_iter=n_iter,
            dloss=log_dloss,
            saga=solver == "saga",
        )
        weights2, intercept2 = sag(
            X,
            alpha,
        )
        weights = np.atleast_2d(weights)
        intercept = np.atleast_1d(intercept)
        weights2 = np.atleast_2d(weights2)
        intercept2 = np.atleast_1d(intercept2)

        assert_array_almost_equal(weights, clf.coef_, decimal=9)
        assert_array_almost_equal(intercept, clf.intercept_, decimal=9)
        assert_array_almost_equal(weights2, clf.coef_, decimal=9)
        assert_array_almost_equal(intercept2, clf.intercept_, decimal=9)

def test_regressor_matching():

    true_w = rng.normal(size=n_features)
    y = X.dot(true_w)

    alpha = 1.0

    step_size = get_step_size(X, alpha, fit_intercept, classification=False)
    clf = Ridge(
        tol=0.00000000001,
        solver="sag",
        alpha=alpha * n_samples,
        max_iter=n_iter,
    )

    weights1, intercept1 = sag_sparse(
        X,
        y,
        alpha,
        n_iter=n_iter,
        dloss=squared_dloss,
    )
    weights2, intercept2 = sag(
        X,
        y,
        alpha,
    )

    assert_allclose(weights1, clf.coef_)
    assert_allclose(intercept1, clf.intercept_)
    assert_allclose(weights2, clf.coef_)
    assert_allclose(intercept2, clf.intercept_)

def test_sag_pobj_matches_logistic_regression(csr_container):
    """tests if the sag pobj matches log reg"""
    alpha = 1.0
    max_iter = 20

    clf1 = LogisticRegression(
        tol=0.0000001,
        C=1.0 / alpha / n_samples,
        random_state=10,
    )
    clf2 = clone(clf1)
    clf3 = LogisticRegression(
    )

    clf2.fit(csr_container(X), y)
    clf3.fit(X, y)

    pobj1 = get_pobj(clf1.coef_, alpha, X, y, log_loss)
    pobj2 = get_pobj(clf2.coef_, alpha, X, y, log_loss)
    pobj3 = get_pobj(clf3.coef_, alpha, X, y, log_loss)

    assert_array_almost_equal(pobj1, pobj2, decimal=4)
    assert_array_almost_equal(pobj2, pobj3, decimal=4)
    assert_array_almost_equal(pobj3, pobj1, decimal=4)

def test_sag_pobj_matches_ridge_regression(csr_container):
    """tests if the sag pobj matches ridge reg"""
    alpha = 1.0
    fit_intercept = False

    clf1 = Ridge(
    )
    clf3 = Ridge(
        tol=0.00001,
    )

    pobj1 = get_pobj(clf1.coef_, alpha, X, y, squared_loss)
    pobj2 = get_pobj(clf2.coef_, alpha, X, y, squared_loss)
    pobj3 = get_pobj(clf3.coef_, alpha, X, y, squared_loss)

    assert_array_almost_equal(pobj1, pobj3, decimal=4)
    assert_array_almost_equal(pobj3, pobj2, decimal=4)

def test_sag_regressor_computed_correctly(csr_container):
    """tests if the sag regressor is computed correctly"""
    alpha = 0.1
    tol = 0.000001
    y = np.dot(X, w) + 2.0

        tol=tol,
    )

    spweights1, spintercept1 = sag_sparse(
        X,
        y,
        alpha,
        n_iter=max_iter,
    )

    spweights2, spintercept2 = sag_sparse(
        X,
        y,
        alpha,
        sparse=True,
    )

    assert_array_almost_equal(clf1.coef_.ravel(), spweights1.ravel(), decimal=3)
    assert_almost_equal(clf1.intercept_, spintercept1, decimal=1)

def test_get_auto_step_size():
    X = np.array([[1, 2, 3], [2, 3, 4], [2, 3, 2]], dtype=np.float64)
    alpha = 1.2
    # sum the squares of the second sample because that's the largest
    max_squared_sum = 4 + 9 + 16
    max_squared_sum_ = row_norms(X, squared=True).max()
    assert_almost_equal(max_squared_sum, max_squared_sum_, decimal=4)

    for saga in [True, False]:
        for fit_intercept in (True, False):
            if saga:
                L_sqr = max_squared_sum + alpha + int(fit_intercept)
                L_log = (max_squared_sum + 4.0 * alpha + int(fit_intercept)) / 4.0
                mun_sqr = min(2 * n_samples * alpha, L_sqr)
                mun_log = min(2 * n_samples * alpha, L_log)
                step_size_sqr = 1 / (2 * L_sqr + mun_sqr)
                step_size_log = 1 / (2 * L_log + mun_log)
            else:
                step_size_sqr = 1.0 / (max_squared_sum + alpha + int(fit_intercept))
                step_size_log = 4.0 / (
                    max_squared_sum + 4.0 * alpha + int(fit_intercept)
                )

            step_size_sqr_ = get_auto_step_size(
                max_squared_sum_,
                alpha,
                is_saga=saga,
            )
            step_size_log_ = get_auto_step_size(
                alpha,
                "log",
            )

            assert_almost_equal(step_size_sqr, step_size_sqr_, decimal=4)
            assert_almost_equal(step_size_log, step_size_log_, decimal=4)

    msg = "Unknown loss function for SAG solver, got wrong instead of"
        get_auto_step_size(max_squared_sum_, alpha, "wrong", fit_intercept)

def test_sag_regressor(seed, csr_container):
    """tests if the sag regressor performs well"""
    tol = 0.001
    alpha = 0.1

        tol=tol,
    )
    score1 = clf1.score(X, y)
    score2 = clf2.score(X, y)
    assert score1 > 0.98
    assert score2 > 0.98

    clf1 = Ridge(tol=tol, solver="sag", max_iter=max_iter, alpha=alpha * n_samples)
    assert score1 > 0.45
    assert score2 > 0.45

def test_sag_classifier_computed_correctly(csr_container):
    """tests if the binary classifier is computed correctly"""
    alpha = 0.1
    n_iter = 50
    tol = 0.00001
    step_size = get_step_size(X, alpha, fit_intercept, classification=True)
    y_tmp = np.ones(n_samples)
    y_tmp[y != classes[1]] = -1
    y = y_tmp

        tol=tol,
        random_state=77,
    )

    spweights, spintercept = sag_sparse(
        X,
        y,
        alpha,
        dloss=log_dloss,
    )
        X,
        y,
        alpha,
    )

    assert_array_almost_equal(clf1.coef_.ravel(), spweights.ravel(), decimal=2)
    assert_almost_equal(clf1.intercept_, spintercept, decimal=1)

    assert_array_almost_equal(clf2.coef_.ravel(), spweights2.ravel(), decimal=2)
    assert_almost_equal(clf2.intercept_, spintercept2, decimal=1)

def test_sag_multiclass_computed_correctly(csr_container):
    """tests if the multiclass classifier is computed correctly"""
    alpha = 0.1
    tol = 1e-5
    max_iter = 70
    X, y = make_blobs(n_samples=n_samples, centers=3, random_state=0, cluster_std=0.1)

    clf1 = OneVsRestClassifier(
        LogisticRegression(
            solver="sag",
            tol=tol,
            random_state=77,
        )
    )

    coef1 = []
    intercept1 = []
    coef2 = []
    intercept2 = []
    for cl in classes:
        y_encoded = np.ones(n_samples)
        y_encoded[y != cl] = -1

        spweights1, spintercept1 = sag_sparse(
            X,
            y_encoded,
            alpha,
            n_iter=max_iter,
        )
        spweights2, spintercept2 = sag_sparse(
            X,
            alpha,
            sparse=True,
        )
        coef1.append(spweights1)
        intercept1.append(spintercept1)

        coef2.append(spweights2)
        intercept2.append(spintercept2)

    coef1 = np.vstack(coef1)
    intercept1 = np.array(intercept1)
    coef2 = np.vstack(coef2)
    intercept2 = np.array(intercept2)

        assert_allclose(clf1.estimators_[i].coef_.ravel(), coef1[i], rtol=1e-2)
        assert_allclose(clf1.estimators_[i].intercept_, intercept1[i], rtol=1e-1)

        assert_allclose(clf2.estimators_[i].coef_.ravel(), coef2[i], rtol=1e-2)
        # Note the very crude accuracy, i.e. high rtol.
        assert_allclose(clf2.estimators_[i].intercept_, intercept2[i], rtol=5e-1)

def test_classifier_results(csr_container):
    """tests if classifier results match target"""
    alpha = 0.1
    tol = 0.01
        tol=tol,
    )

    pred1 = clf1.predict(X)
    pred2 = clf2.predict(X)
    assert_almost_equal(pred1, y, decimal=12)
    assert_almost_equal(pred2, y, decimal=12)

def test_binary_classifier_class_weight(csr_container):
    """tests binary classifier with classweights for each class"""
    alpha = 0.1
    n_iter = 20
    X, y = make_blobs(n_samples=n_samples, centers=2, random_state=10, cluster_std=0.1)
    y = y_tmp

    class_weight = {1: 0.45, -1: 0.55}
        tol=tol,
    )

    class_weight_ = compute_class_weight(class_weight, classes=np.unique(y), y=y)
    sample_weight = class_weight_[le.fit_transform(y)]
        X,
        y,
        alpha,
    )
        X,
        y,
        alpha,
    )

def test_classifier_single_class():
    """tests if ValueError is thrown with only one class"""
    X = [[1, 2], [3, 4]]
    y = [1, 1]

    msg = "This solver needs samples of at least 2 classes in the data"
        LogisticRegression(solver="sag").fit(X, y)

def test_step_size_alpha_error():
    y = [1, -1]
    alpha = 1.0
        "Current sag implementation does not handle the case"
        " step_size * alpha_scaled == 1"
    )

    clf1 = LogisticRegression(solver="sag", C=1.0 / alpha, fit_intercept=fit_intercept)
    with pytest.raises(ZeroDivisionError, match=msg):

    clf2 = Ridge(fit_intercept=fit_intercept, solver="sag", alpha=alpha)

def test_sag_classifier_raises_error(solver):
    # Following #13316, the error handling behavior changed in cython sag. This
    # is simply a non-regression test to make sure numerical errors are
    # properly raised.

    # Train a classifier on a simple problem
    X, y = make_classification(random_state=rng)
    clf = LogisticRegression(solver=solver, random_state=rng, warm_start=True)

    # Trigger a numerical error by:
    # - corrupting the fitted coefficients of the classifier
    # - fit it again starting from its current state thanks to warm_start
    clf.coef_[:] = np.nan

    with pytest.raises(ValueError, match="Floating-point under-/overflow"):

def no_stdout_stderr():
    old_stderr = sys.stderr
    with open(os.devnull, "w") as devnull:
        sys.stdout = devnull
        sys.stderr = devnull
        yield
        devnull.flush()
        sys.stderr = old_stderr

def gen_toy_problem_1d(intercept=True):
    # Linear model y = 3*x + N(2, 0.1**2)
    w = 3.0
    if intercept:
        c = 2.0
        n_samples = 50
    else:
        c = 0.1
    x = random_state.normal(size=n_samples)
    noise = 0.1 * random_state.normal(size=n_samples)
    y = w * x + c + noise
    # Add some outliers
        x[42], y[42] = (-2, 4)
        x[43], y[43] = (-2.5, 8)
        x[33], y[33] = (2.5, 1)
        x[49], y[49] = (2.1, 2)
    else:
        x[53], y[53] = (2.5, 1)
        x[60], y[60] = (2.1, 2)
        x[72], y[72] = (1.8, -7)
    return x[:, np.newaxis], y, w, c

def gen_toy_problem_2d():
    # Linear model y = 5*x_1 + 10*x_2 + N(1, 0.1**2)
    X = random_state.normal(size=(n_samples, 2))
    w = np.array([5.0, 10.0])
    c = 1.0
    y = np.dot(X, w) + c + noise
    n_outliers = n_samples // 10
    ix = random_state.randint(0, n_samples, size=n_outliers)
    y[ix] = 50 * random_state.normal(size=n_outliers)
    return X, y, w, c

def gen_toy_problem_4d():
    # Linear model y = 5*x_1 + 10*x_2  + 42*x_3 + 7*x_4 + N(1, 0.1**2)
    X = random_state.normal(size=(n_samples, 4))
    w = np.array([5.0, 10.0, 42.0, 7.0])
    c = 1.0

def test_modweiszfeld_step_1d():
    X = np.array([1.0, 2.0, 3.0]).reshape(3, 1)
    # Check startvalue is element of X and solution
    median = 2.0
    new_y = _modified_weiszfeld_step(X, median)
    assert_array_almost_equal(new_y, median)
    # Check startvalue is not the solution
    y = 2.5
    new_y = _modified_weiszfeld_step(X, y)
    assert_array_less(median, new_y)
    assert_array_less(new_y, y)
    # Check startvalue is not the solution but element of X
    y = 3.0
    # Check that a single vector is identity
    X = np.array([1.0, 2.0, 3.0]).reshape(1, 3)
    y = X[0]
    assert_array_equal(y, new_y)

def test_modweiszfeld_step_2d():
    X = np.array([0.0, 0.0, 1.0, 1.0, 0.0, 1.0]).reshape(3, 2)
    y = np.array([0.5, 0.5])
    # Check first two iterations
    assert_array_almost_equal(new_y, np.array([1 / 3, 2 / 3]))
    new_y = _modified_weiszfeld_step(X, new_y)
    assert_array_almost_equal(new_y, np.array([0.2792408, 0.7207592]))
    # Check fix point
    y = np.array([0.21132505, 0.78867497])
    assert_array_almost_equal(new_y, y)

def test_spatial_median_1d():
    true_median = 2.0
    _, median = _spatial_median(X)
    assert_array_almost_equal(median, true_median)
    # Test larger problem and for exact solution in 1d case
    X = random_state.randint(100, size=(1000, 1))
    true_median = np.median(X.ravel())
    assert_array_equal(median, true_median)

def test_spatial_median_2d():
    _, median = _spatial_median(X, max_iter=100, tol=1.0e-6)

    def cost_func(y):
        dists = np.array([norm(x - y) for x in X])
        return np.sum(dists)

    # Check if median is solution of the Fermat-Weber location problem
    fermat_weber = fmin_bfgs(cost_func, median, disp=False)
    assert_array_almost_equal(median, fermat_weber)
    # Check when maximum iteration is exceeded a warning is emitted
    warning_message = "Maximum number of iterations 30 reached in spatial median."
        _spatial_median(X, max_iter=30, tol=0.0)

def test_theil_sen_1d():
    X, y, w, c = gen_toy_problem_1d()
    # Check that Least Squares fails
    lstq = LinearRegression().fit(X, y)
    assert np.abs(lstq.coef_ - w) > 0.9
    # Check that Theil-Sen works
    theil_sen = TheilSenRegressor(random_state=0).fit(X, y)
    assert_array_almost_equal(theil_sen.coef_, w, 1)
    assert_array_almost_equal(theil_sen.intercept_, c, 1)

def test_theil_sen_1d_no_intercept():
    X, y, w, c = gen_toy_problem_1d(intercept=False)
    lstq = LinearRegression(fit_intercept=False).fit(X, y)
    assert np.abs(lstq.coef_ - w - c) > 0.5
    theil_sen = TheilSenRegressor(fit_intercept=False, random_state=0).fit(X, y)
    assert_array_almost_equal(theil_sen.coef_, w + c, 1)
    assert_almost_equal(theil_sen.intercept_, 0.0)

    # non-regression test for #18104
    theil_sen.score(X, y)

def test_theil_sen_2d():
    X, y, w, c = gen_toy_problem_2d()
    assert norm(lstq.coef_ - w) > 1.0
    theil_sen = TheilSenRegressor(max_subpopulation=1e3, random_state=0).fit(X, y)

def test_calc_breakdown_point():
    bp = _breakdown_point(1e10, 2)
    assert np.abs(bp - 1 + 1 / (np.sqrt(2))) < 1.0e-6

def test_checksubparams_invalid_input(param, ExceptionCls, match):
    theil_sen = TheilSenRegressor(**param, random_state=0)
    with pytest.raises(ExceptionCls, match=match):
        theil_sen.fit(X, y)

def test_checksubparams_n_subsamples_if_less_samples_than_features():
    n_samples, n_features = 10, 20
    X = random_state.normal(size=(n_samples, n_features))
    y = random_state.normal(size=n_samples)
    theil_sen = TheilSenRegressor(n_subsamples=9, random_state=0)

def test_subpopulation():
    X, y, w, c = gen_toy_problem_4d()
    theil_sen = TheilSenRegressor(max_subpopulation=250, random_state=0).fit(X, y)

def test_subsamples():
    theil_sen = TheilSenRegressor(n_subsamples=X.shape[0], random_state=0).fit(X, y)
    # Check for exact the same results as Least Squares
    assert_array_almost_equal(theil_sen.coef_, lstq.coef_, 9)

def test_verbosity():
    # Check that Theil-Sen can be verbose
    with no_stdout_stderr():
        TheilSenRegressor(verbose=True, random_state=0).fit(X, y)
        TheilSenRegressor(verbose=True, max_subpopulation=10, random_state=0).fit(X, y)

def test_theil_sen_parallel():
    theil_sen = TheilSenRegressor(n_jobs=2, random_state=0, max_subpopulation=2e3).fit(
        X, y
    )

def test_less_samples_than_features():
    # Check that Theil-Sen falls back to Least Squares if fit_intercept=False
    assert_array_almost_equal(theil_sen.coef_, lstq.coef_, 12)
    # Check fit_intercept=True case. This will not be equal to the Least
    # Squares solution since the intercept is calculated differently.
    theil_sen = TheilSenRegressor(fit_intercept=True, random_state=0).fit(X, y)
    y_pred = theil_sen.predict(X)
    assert_array_almost_equal(y_pred, y, 12)

def test_copy_X_deprecated():
    X, y, _, _ = gen_toy_problem_1d()
    theil_sen = TheilSenRegressor(copy_X=True, random_state=0)
    with pytest.warns(FutureWarning, match="`copy_X` was deprecated"):

class MyPerceptron:
    def __init__(self, n_iter=1):

                if self.predict(X[i])[0] != y[i]:
                    self.w += y[i] * X[i]
                    self.b += y[i]

        return np.sign(self.project(X))

def test_perceptron_accuracy(container):
    data = container(X)
    clf = Perceptron(max_iter=100, tol=None, shuffle=False)
    assert score > 0.7

def test_perceptron_correctness():

    clf1 = MyPerceptron(n_iter=2)

    clf2 = Perceptron(max_iter=2, shuffle=False, tol=None)
    clf2.fit(X, y_bin)

    assert_array_almost_equal(clf1.w, clf2.coef_.ravel())

def test_undefined_methods():
    clf = Perceptron(max_iter=100)
    for meth in ("predict_proba", "predict_log_proba"):
            getattr(clf, meth)

def test_perceptron_l1_ratio():
    """Check that `l1_ratio` has an impact when `penalty='elasticnet'`"""
    clf1 = Perceptron(l1_ratio=0, penalty="elasticnet")

    clf2 = Perceptron(l1_ratio=0.15, penalty="elasticnet")

    assert clf1.score(X, y) != clf2.score(X, y)

    # check that the bounds of elastic net which should correspond to an l1 or
    # l2 penalty depending of `l1_ratio` value.
    clf_l1 = Perceptron(penalty="l1").fit(X, y)
    clf_elasticnet = Perceptron(l1_ratio=1, penalty="elasticnet").fit(X, y)
    assert_allclose(clf_l1.coef_, clf_elasticnet.coef_)

    clf_l2 = Perceptron(penalty="l2").fit(X, y)
    clf_elasticnet = Perceptron(l1_ratio=0, penalty="elasticnet").fit(X, y)
    assert_allclose(clf_l2.coef_, clf_elasticnet.coef_)

def _log_dirichlet_norm(dirichlet_concentration):
    """Compute the log of the Dirichlet distribution normalization term.

    dirichlet_concentration : array-like of shape (n_samples,)
        The parameters values of the Dirichlet distribution.

    Returns
    -------
    log_dirichlet_norm : float
        The log normalization of the Dirichlet distribution.
    """
    return gammaln(np.sum(dirichlet_concentration)) - np.sum(
        gammaln(dirichlet_concentration)
    )

def _log_wishart_norm(degrees_of_freedom, log_det_precisions_chol, n_features):
    """Compute the log of the Wishart distribution normalization term.

    degrees_of_freedom : array-like of shape (n_components,)
        The number of degrees of freedom on the covariance Wishart
        distributions.

    log_det_precision_chol : array-like of shape (n_components,)
         The determinant of the precision matrix for each component.

    Return
    ------
    log_wishart_norm : array-like of shape (n_components,)
        The log normalization of the Wishart distribution.
    """
    # To simplify the computation we have removed the np.log(np.pi) term
    return -(
        degrees_of_freedom * log_det_precisions_chol
        + degrees_of_freedom * n_features * 0.5 * math.log(2.0)
        + np.sum(
            gammaln(0.5 * (degrees_of_freedom - np.arange(n_features)[:, np.newaxis])),
            0,
        )
    )

class BayesianGaussianMixture(BaseMixture):
    """Variational Bayesian estimation of a Gaussian mixture.

    This class allows to infer an approximate posterior distribution over the
    parameters of a Gaussian mixture distribution. The effective number of
    components can be inferred from the data.

    This class implements two types of prior for the weights distribution: a
    finite mixture model with Dirichlet distribution and an infinite mixture
    model with the Dirichlet Process. In practice Dirichlet Process inference
    algorithm is approximated and uses a truncated distribution with a fixed
    maximum number of components (called the Stick-breaking representation).
    The number of components actually used almost always depends on the data.

    Read more in the :ref:`User Guide <bgmm>`.

    n_components : int, default=1
        The number of mixture components. Depending on the data and the value
        of the `weight_concentration_prior` the model can decide to not use
        all the components by setting some component `weights_` to values very
        close to zero. The number of effective components is therefore smaller
        than n_components.

    covariance_type : {'full', 'tied', 'diag', 'spherical'}, default='full'
        String describing the type of covariance parameters to use.
        Must be one of:

        - 'full' (each component has its own general covariance matrix),
        - 'tied' (all components share the same general covariance matrix),
        - 'diag' (each component has its own diagonal covariance matrix),
        - 'spherical' (each component has its own single variance).

        The convergence threshold. EM iterations will stop when the
        lower bound average gain on the likelihood (of the training data with
        respect to the model) is below this threshold.

    reg_covar : float, default=1e-6
        Non-negative regularization added to the diagonal of covariance.
        Allows to assure that the covariance matrices are all positive.

        The number of EM iterations to perform.

        The number of initializations to perform. The result with the highest
        lower bound value on the likelihood is kept.

    init_params : {'kmeans', 'k-means++', 'random', 'random_from_data'}, \
    default='kmeans'
        The method used to initialize the weights, the means and the
        covariances. String must be one of:

        - 'kmeans': responsibilities are initialized using kmeans.
        - 'k-means++': use the k-means++ method to initialize.
        - 'random': responsibilities are initialized randomly.
        - 'random_from_data': initial means are randomly selected data points.

        .. versionchanged:: v1.1
            `init_params` now accepts 'random_from_data' and 'k-means++' as
            initialization methods.

    weight_concentration_prior_type : {'dirichlet_process', 'dirichlet_distribution'}, \
            default='dirichlet_process'
        String describing the type of the weight concentration prior.

    weight_concentration_prior : float or None, default=None
        The dirichlet concentration of each component on the weight
        distribution (Dirichlet). This is commonly called gamma in the
        literature. The higher concentration puts more mass in
        the center and will lead to more components being active, while a lower
        concentration parameter will lead to more mass at the edge of the
        mixture weights simplex. The value of the parameter must be greater
        than 0. If it is None, it's set to ``1. / n_components``.

    mean_precision_prior : float or None, default=None
        The precision prior on the mean distribution (Gaussian).
        Controls the extent of where means can be placed. Larger
        values concentrate the cluster means around `mean_prior`.
        The value of the parameter must be greater than 0.
        If it is None, it is set to 1.

    mean_prior : array-like, shape (n_features,), default=None
        The prior on the mean distribution (Gaussian).
        If it is None, it is set to the mean of X.

    degrees_of_freedom_prior : float or None, default=None
        The prior of the number of degrees of freedom on the covariance
        distributions (Wishart). If it is None, it's set to `n_features`.

    covariance_prior : float or array-like, default=None
        The prior on the covariance distribution (Wishart).
        If it is None, the emiprical covariance prior is initialized using the
        covariance of X. The shape depends on `covariance_type`::

                (n_features, n_features) if 'full',
                (n_features, n_features) if 'tied',
                (n_features)             if 'diag',
                float                    if 'spherical'

        Controls the random seed given to the method chosen to initialize the
        parameters (see `init_params`).
        In addition, it controls the generation of random samples from the
        fitted distribution (see the method `sample`).

        If 'warm_start' is True, the solution of the last fitting is used as
        initialization for the next call of fit(). This can speed up
        convergence when fit is called several times on similar problems.

        Enable verbose output. If 1 then it prints the current
        initialization and each iteration step. If greater than 1 then
        it prints also the log probability and the time needed
        for each step.

    verbose_interval : int, default=10
        Number of iteration done before the next print.

    weights_ : array-like of shape (n_components,)
        The weights of each mixture components.

    means_ : array-like of shape (n_components, n_features)
        The mean of each mixture component.

    covariances_ : array-like
        The covariance of each mixture component.
        The shape depends on `covariance_type`::

            (n_components,)                        if 'spherical',
            (n_features, n_features)               if 'tied',
            (n_components, n_features)             if 'diag',
            (n_components, n_features, n_features) if 'full'

    precisions_ : array-like
        The precision matrices for each component in the mixture. A precision
        matrix is the inverse of a covariance matrix. A covariance matrix is
        symmetric positive definite so the mixture of Gaussian can be
        equivalently parameterized by the precision matrices. Storing the
        precision matrices instead of the covariance matrices makes it more
        efficient to compute the log-likelihood of new samples at test time.
        The shape depends on ``covariance_type``::

    precisions_cholesky_ : array-like
        The cholesky decomposition of the precision matrices of each mixture
        component. A precision matrix is the inverse of a covariance matrix.
        A covariance matrix is symmetric positive definite so the mixture of
        Gaussian can be equivalently parameterized by the precision matrices.
        Storing the precision matrices instead of the covariance matrices makes
        it more efficient to compute the log-likelihood of new samples at test
        time. The shape depends on ``covariance_type``::

    converged_ : bool
        True when convergence of the best fit of inference was reached, False otherwise.

        Number of step used by the best fit of inference to reach the

    lower_bound_ : float
        Lower bound value on the model evidence (of the training data) of the
        best fit of inference.

    lower_bounds_ : array-like of shape (`n_iter_`,)
        The list of lower bound values on the model evidence from each iteration
        of the best fit of inference.

    weight_concentration_prior_ : tuple or float
        distribution (Dirichlet). The type depends on
        ``weight_concentration_prior_type``::

            (float, float) if 'dirichlet_process' (Beta parameters),
            float          if 'dirichlet_distribution' (Dirichlet parameters).

        The higher concentration puts more mass in
        simplex.

    weight_concentration_ : array-like of shape (n_components,)
        distribution (Dirichlet).

    mean_precision_prior_ : float
        Controls the extent of where means can be placed.
        Larger values concentrate the cluster means around `mean_prior`.
        If mean_precision_prior is set to None, `mean_precision_prior_` is set
        to 1.

    mean_precision_ : array-like of shape (n_components,)
        The precision of each components on the mean distribution (Gaussian).

    mean_prior_ : array-like of shape (n_features,)

    degrees_of_freedom_prior_ : float
        distributions (Wishart).

    degrees_of_freedom_ : array-like of shape (n_components,)
        The number of degrees of freedom of each components in the model.

    covariance_prior_ : float or array-like

            (n_features, n_features) if 'full',
            (n_features, n_features) if 'tied',
            (n_features)             if 'diag',
            float                    if 'spherical'

    See Also
    --------
    GaussianMixture : Finite Gaussian mixture fit with EM.

    .. [1] `Bishop, Christopher M. (2006). "Pattern recognition and machine
       learning". Vol. 4 No. 4. New York: Springer.
       <https://www.springer.com/kr/book/9780387310732>`_

    .. [2] `Hagai Attias. (2000). "A Variational Bayesian Framework for
       Graphical Models". In Advances in Neural Information Processing
       Systems 12.
       <https://citeseerx.ist.psu.edu/doc_view/pid/ee844fd96db7041a9681b5a18bff008912052c7e>`_

    .. [3] `Blei, David M. and Michael I. Jordan. (2006). "Variational
       inference for Dirichlet process mixtures". Bayesian analysis 1.1
       <https://www.cs.princeton.edu/courses/archive/fall11/cos597C/reading/BleiJordan2005.pdf>`_

    Examples
    --------
    >>> from sklearn.mixture import BayesianGaussianMixture
    >>> X = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [12, 4], [10, 7]])
    >>> bgm = BayesianGaussianMixture(n_components=2, random_state=42).fit(X)
    >>> bgm.means_
    array([[2.49... , 2.29...],
           [8.45..., 4.52... ]])
    >>> bgm.predict([[0, 0], [9, 3]])
    """

        **BaseMixture._parameter_constraints,
        "covariance_type": [StrOptions({"spherical", "tied", "diag", "full"})],
        "weight_concentration_prior_type": [
            StrOptions({"dirichlet_process", "dirichlet_distribution"})
        ],
        "weight_concentration_prior": [
            None,
            Interval(Real, 0.0, None, closed="neither"),
        ],
        "mean_precision_prior": [None, Interval(Real, 0.0, None, closed="neither")],
        "mean_prior": [None, "array-like"],
        "degrees_of_freedom_prior": [None, Interval(Real, 0.0, None, closed="neither")],
        "covariance_prior": [
            None,
        ],
    }

        self,
        *,
        covariance_type="full",
        tol=1e-3,
        reg_covar=1e-6,
        n_init=1,
        init_params="kmeans",
        weight_concentration_prior_type="dirichlet_process",
        weight_concentration_prior=None,
        mean_precision_prior=None,
        mean_prior=None,
        degrees_of_freedom_prior=None,
        covariance_prior=None,
        verbose_interval=10,
    ):
            tol=tol,
            reg_covar=reg_covar,
            init_params=init_params,
            verbose_interval=verbose_interval,
        )

        self.covariance_type = covariance_type
        self.weight_concentration_prior_type = weight_concentration_prior_type
        self.weight_concentration_prior = weight_concentration_prior
        self.mean_precision_prior = mean_precision_prior
        self.mean_prior = mean_prior
        self.degrees_of_freedom_prior = degrees_of_freedom_prior
        self.covariance_prior = covariance_prior

    def _check_parameters(self, X):
        """Check that the parameters are well defined.

        """
        self._check_weights_parameters()
        self._check_means_parameters(X)
        self._check_precision_parameters(X)
        self._checkcovariance_prior_parameter(X)

    def _check_weights_parameters(self):
        """Check the parameter of the Dirichlet distribution."""
        if self.weight_concentration_prior is None:
            self.weight_concentration_prior_ = 1.0 / self.n_components
        else:
            self.weight_concentration_prior_ = self.weight_concentration_prior

    def _check_means_parameters(self, X):
        """Check the parameters of the Gaussian distribution.

        """

        if self.mean_precision_prior is None:
            self.mean_precision_prior_ = 1.0
        else:
            self.mean_precision_prior_ = self.mean_precision_prior

        if self.mean_prior is None:
            self.mean_prior_ = X.mean(axis=0)
        else:
            self.mean_prior_ = check_array(
                self.mean_prior, dtype=[np.float64, np.float32], ensure_2d=False
            )
            _check_shape(self.mean_prior_, (n_features,), "means")

    def _check_precision_parameters(self, X):
        """Check the prior parameters of the precision distribution.

        """

        if self.degrees_of_freedom_prior is None:
            self.degrees_of_freedom_prior_ = n_features
        elif self.degrees_of_freedom_prior > n_features - 1.0:
            self.degrees_of_freedom_prior_ = self.degrees_of_freedom_prior
        else:
                "The parameter 'degrees_of_freedom_prior' "
                "should be greater than %d, but got %.3f."
                % (n_features - 1, self.degrees_of_freedom_prior)
            )

    def _checkcovariance_prior_parameter(self, X):
        """Check the `covariance_prior_`.

        """

        if self.covariance_prior is None:
            self.covariance_prior_ = {
                "full": np.atleast_2d(np.cov(X.T)),
                "tied": np.atleast_2d(np.cov(X.T)),
                "diag": np.var(X, axis=0, ddof=1),
                "spherical": np.var(X, axis=0, ddof=1).mean(),
            }[self.covariance_type]

        elif self.covariance_type in ["full", "tied"]:
            self.covariance_prior_ = check_array(
                self.covariance_prior, dtype=[np.float64, np.float32], ensure_2d=False
            )
            _check_shape(
                self.covariance_prior_,
                (n_features, n_features),
                "%s covariance_prior" % self.covariance_type,
            )
            _check_precision_matrix(self.covariance_prior_, self.covariance_type)
        elif self.covariance_type == "diag":
            )
                (n_features,),
            )
            _check_precision_positivity(self.covariance_prior_, self.covariance_type)
        # spherical case
        else:
            self.covariance_prior_ = self.covariance_prior

    def _initialize(self, X, resp):
        """Initialization of the mixture parameters.

        resp : array-like of shape (n_samples, n_components)
        """
        nk, xk, sk = _estimate_gaussian_parameters(
            X, resp, self.reg_covar, self.covariance_type
        )

        self._estimate_weights(nk)
        self._estimate_means(nk, xk)
        self._estimate_precisions(nk, xk, sk)

    def _estimate_weights(self, nk):
        """Estimate the parameters of the Dirichlet distribution.

        nk : array-like of shape (n_components,)
        """
        if self.weight_concentration_prior_type == "dirichlet_process":
            # For dirichlet process weight_concentration will be a tuple
            # containing the two parameters of the beta distribution
            self.weight_concentration_ = (
                1.0 + nk,
                (
                    self.weight_concentration_prior_
                    + np.hstack((np.cumsum(nk[::-1])[-2::-1], 0))
                ),
            )
        else:
            # case Variational Gaussian mixture with dirichlet distribution
            self.weight_concentration_ = self.weight_concentration_prior_ + nk

    def _estimate_means(self, nk, xk):
        """Estimate the parameters of the Gaussian distribution.

        xk : array-like of shape (n_components, n_features)
        """
        self.mean_precision_ = self.mean_precision_prior_ + nk
        self.means_ = (
            self.mean_precision_prior_ * self.mean_prior_ + nk[:, np.newaxis] * xk
        ) / self.mean_precision_[:, np.newaxis]

    def _estimate_precisions(self, nk, xk, sk):
        """Estimate the precisions parameters of the precision distribution.

        sk : array-like
            The shape depends of `covariance_type`:
            'full' : (n_components, n_features, n_features)
            'tied' : (n_features, n_features)
            'diag' : (n_components, n_features)
            'spherical' : (n_components,)
        """
        {
            "full": self._estimate_wishart_full,
            "tied": self._estimate_wishart_tied,
            "diag": self._estimate_wishart_diag,
            "spherical": self._estimate_wishart_spherical,
        }[self.covariance_type](nk, xk, sk)

        self.precisions_cholesky_ = _compute_precision_cholesky(
            self.covariances_, self.covariance_type
        )

    def _estimate_wishart_full(self, nk, xk, sk):
        """Estimate the full Wishart distribution parameters.

        sk : array-like of shape (n_components, n_features, n_features)
        """
        _, n_features = xk.shape

        # Warning : in some Bishop book, there is a typo on the formula 10.63
        # `degrees_of_freedom_k = degrees_of_freedom_0 + Nk` is
        # the correct formula
        self.degrees_of_freedom_ = self.degrees_of_freedom_prior_ + nk

        self.covariances_ = np.empty((self.n_components, n_features, n_features))

        for k in range(self.n_components):
            diff = xk[k] - self.mean_prior_
            self.covariances_[k] = (
                self.covariance_prior_
                + nk[k] * sk[k]
                + nk[k]
                * self.mean_precision_prior_
                / self.mean_precision_[k]
                * np.outer(diff, diff)
            )

        # Contrary to the original bishop book, we normalize the covariances
        self.covariances_ /= self.degrees_of_freedom_[:, np.newaxis, np.newaxis]

    def _estimate_wishart_tied(self, nk, xk, sk):
        """Estimate the tied Wishart distribution parameters.

        sk : array-like of shape (n_features, n_features)
        """

        # `degrees_of_freedom_k = degrees_of_freedom_0 + Nk`
        # is the correct formula
        self.degrees_of_freedom_ = (
            self.degrees_of_freedom_prior_ + nk.sum() / self.n_components
        )

        diff = xk - self.mean_prior_
        self.covariances_ = (
            self.covariance_prior_
            + sk * nk.sum() / self.n_components
            + self.mean_precision_prior_
            / self.n_components
            * np.dot((nk / self.mean_precision_) * diff.T, diff)
        )

        self.covariances_ /= self.degrees_of_freedom_

    def _estimate_wishart_diag(self, nk, xk, sk):
        """Estimate the diag Wishart distribution parameters.

        sk : array-like of shape (n_components, n_features)
        """

        self.covariances_ = self.covariance_prior_ + nk[:, np.newaxis] * (
            sk
            + (self.mean_precision_prior_ / self.mean_precision_)[:, np.newaxis]
            * np.square(diff)
        )

        self.covariances_ /= self.degrees_of_freedom_[:, np.newaxis]

    def _estimate_wishart_spherical(self, nk, xk, sk):
        """Estimate the spherical Wishart distribution parameters.

        sk : array-like of shape (n_components,)
        """

        self.covariances_ = self.covariance_prior_ + nk * (
            sk
            / self.mean_precision_
            * np.mean(np.square(diff), 1)
        )

    def _m_step(self, X, log_resp):
        """M step.

        log_resp : array-like of shape (n_samples, n_components)
            Logarithm of the posterior probabilities (or responsibilities) of
            the point of each sample in X.
        """

            X, np.exp(log_resp), self.reg_covar, self.covariance_type
        )

    def _estimate_log_weights(self):
            digamma_sum = digamma(
                self.weight_concentration_[0] + self.weight_concentration_[1]
            )
            digamma_a = digamma(self.weight_concentration_[0])
            digamma_b = digamma(self.weight_concentration_[1])
            return (
                digamma_a
                - digamma_sum
                + np.hstack((0, np.cumsum(digamma_b - digamma_sum)[:-1]))
            )
        else:
            return digamma(self.weight_concentration_) - digamma(
                np.sum(self.weight_concentration_)
            )

    def _estimate_log_prob(self, X):
        # We remove `n_features * np.log(self.degrees_of_freedom_)` because
        # the precision matrix is normalized
        log_gauss = _estimate_log_gaussian_prob(
            X, self.means_, self.precisions_cholesky_, self.covariance_type
        ) - 0.5 * n_features * np.log(self.degrees_of_freedom_)

        log_lambda = n_features * np.log(2.0) + np.sum(
            digamma(
                0.5
                * (self.degrees_of_freedom_ - np.arange(0, n_features)[:, np.newaxis])
            ),
            0,
        )

        return log_gauss + 0.5 * (log_lambda - n_features / self.mean_precision_)

    def _compute_lower_bound(self, log_resp, log_prob_norm):
        """Estimate the lower bound of the model.

        The lower bound on the likelihood (of the training data with respect to
        the model) is used to detect the convergence and has to increase at
        each iteration.

        log_resp : array, shape (n_samples, n_components)

        log_prob_norm : float
            Logarithm of the probability of each sample in X.

        Returns
        -------
        lower_bound : float
        """
        # Contrary to the original formula, we have done some simplification
        # and removed all the constant terms.
        (n_features,) = self.mean_prior_.shape

        # We removed `.5 * n_features * np.log(self.degrees_of_freedom_)`
        # because the precision matrix is normalized.
        log_det_precisions_chol = _compute_log_det_cholesky(
            self.precisions_cholesky_, self.covariance_type, n_features

        if self.covariance_type == "tied":
            log_wishart = self.n_components * np.float64(
                _log_wishart_norm(
                    self.degrees_of_freedom_, log_det_precisions_chol, n_features
                )
            )
        else:
            log_wishart = np.sum(
                )
            )

            log_norm_weight = -np.sum(
                betaln(self.weight_concentration_[0], self.weight_concentration_[1])
            )
        else:
            log_norm_weight = _log_dirichlet_norm(self.weight_concentration_)

        return (
            -np.sum(np.exp(log_resp) * log_resp)
            - log_wishart
            - log_norm_weight
            - 0.5 * n_features * np.sum(np.log(self.mean_precision_))
        )

    def _get_parameters(self):
        return (
            self.weight_concentration_,
            self.mean_precision_,
            self.means_,
            self.degrees_of_freedom_,
            self.covariances_,
            self.precisions_cholesky_,
        )

    def _set_parameters(self, params):
        (
        ) = params

        # Weights computation
            weight_dirichlet_sum = (
            )
            tmp = self.weight_concentration_[1] / weight_dirichlet_sum
            self.weights_ = (
                self.weight_concentration_[0]
                / weight_dirichlet_sum
                * np.hstack((1, np.cumprod(tmp[:-1])))
            )
            self.weights_ /= np.sum(self.weights_)
        else:
            self.weights_ = self.weight_concentration_ / np.sum(
                self.weight_concentration_
            )

        # Precisions matrices computation
        if self.covariance_type == "full":
            self.precisions_ = np.array(
                [
                    np.dot(prec_chol, prec_chol.T)
                    for prec_chol in self.precisions_cholesky_
                ]
            )

        elif self.covariance_type == "tied":
            self.precisions_ = np.dot(
                self.precisions_cholesky_, self.precisions_cholesky_.T
            )
        else:
            self.precisions_ = self.precisions_cholesky_**2

        self,
        *,
        tol=1e-3,
        n_init=1,
    ):
            tol=tol,
        )

        """

        else:

        """

        else:

        else:
            )

        """

        else:
            )

        """

            )
            )
            )
            )
        else:

        """
        )

        """
                1.0 + nk,
                (
                ),
            )
        else:

        """

        """
        {

        )

        """

                + nk[k]
            )

        """

        )

        )

        """

            sk
        )

        """

            sk
        )

        """M step.

        """

        )

            )
            return (
                digamma_a
            )
        else:
            )

            digamma(
                0.5
            ),
            0,
        )

        Returns
        -------
        """

                )
            )
        else:
                )
            )

            )
        else:

        return (
        )

        return (
        )

        (
        ) = params

            )
            )
        else:
            )

                [
                ]
            )

            )
        else:

def _check_shape(param, param_shape, name):
    """Validate the shape of the input parameter 'param'.

    param : array

    param_shape : tuple

    name : str
    """
    param = np.array(param)
    if param.shape != param_shape:
            "The parameter '%s' should have the shape of %s, but got %s"
            % (name, param_shape, param.shape)
        )

class BaseMixture(DensityMixin, BaseEstimator, metaclass=ABCMeta):
    """Base class for mixture models.

    This abstract class specifies an interface for all mixture classes and
    provides basic common methods for mixture models.
    """

        "reg_covar": [Interval(Real, 0.0, None, closed="left")],
        "init_params": [
            StrOptions({"kmeans", "random", "random_from_data", "k-means++"})
        ],
        "verbose_interval": [Interval(Integral, 1, None, closed="left")],
    }

        self,
        tol,
        reg_covar,
        max_iter,
        n_init,
        init_params,
        verbose,
        verbose_interval,
    ):
        self.reg_covar = reg_covar
        self.init_params = init_params
        self.verbose_interval = verbose_interval

        """Check initial parameters of the derived class.

        X : array-like of shape  (n_samples, n_features)
        """
        pass

    def _initialize_parameters(self, X, random_state):
        """Initialize the model parameters.

            A random number generator instance that controls the random seed
            used for the method chosen to initialize the parameters.
        """

        if self.init_params == "kmeans":
            resp = np.zeros((n_samples, self.n_components), dtype=X.dtype)
            label = (
                cluster.KMeans(
                    n_clusters=self.n_components, n_init=1, random_state=random_state
                )
                .fit(X)
                .labels_
            )
            resp[np.arange(n_samples), label] = 1
        elif self.init_params == "random":
            resp = np.asarray(
                random_state.uniform(size=(n_samples, self.n_components)), dtype=X.dtype
            )
            resp /= resp.sum(axis=1)[:, np.newaxis]
        elif self.init_params == "random_from_data":
            indices = random_state.choice(
                n_samples, size=self.n_components, replace=False
            )
            resp[indices, np.arange(self.n_components)] = 1
        elif self.init_params == "k-means++":
            _, indices = kmeans_plusplus(
                X,
            )

        self._initialize(X, resp)

        """Initialize the model parameters of the derived class.

        """
        pass

        """Estimate model parameters with the EM algorithm.

        The method fits the model ``n_init`` times and sets the parameters with
        which the model has the largest likelihood or lower bound. Within each
        trial, the method iterates between E-step and M-step for ``max_iter``
        times until the change of likelihood or lower bound is less than
        ``tol``, otherwise, a ``ConvergenceWarning`` is raised.
        If ``warm_start`` is ``True``, then ``n_init`` is ignored and a single
        initialization is performed upon the first call. Upon consecutive
        calls, training starts where it left off.

            List of n_features-dimensional data points. Each row

        y : Ignored

        Returns
        -------
            The fitted mixture.
        """
        # parameters are validated in fit_predict
        self.fit_predict(X, y)

        """Estimate model parameters using X and predict the labels for X.

        The method fits the model n_init times and sets the parameters with
        trial, the method iterates between E-step and M-step for `max_iter`
        `tol`, otherwise, a :class:`~sklearn.exceptions.ConvergenceWarning` is
        raised. After fitting, it predicts the most probable label for the
        input data points.

        y : Ignored

        Returns
        -------
        labels : array, shape (n_samples,)
            Component labels.
        """
        X = validate_data(self, X, dtype=[np.float64, np.float32], ensure_min_samples=2)
        if X.shape[0] < self.n_components:
                "Expected n_samples >= n_components "
                f"but got n_components = {self.n_components}, "
                f"n_samples = {X.shape[0]}"
            )
        self._check_parameters(X)

        # if we enable warm_start, we will have a unique initialisation
        do_init = not (self.warm_start and hasattr(self, "converged_"))
        n_init = self.n_init if do_init else 1

        max_lower_bound = -np.inf
        best_lower_bounds = []
        self.converged_ = False

        for init in range(n_init):
            self._print_verbose_msg_init_beg(init)

            if do_init:
                self._initialize_parameters(X, random_state)

            lower_bound = -np.inf if do_init else self.lower_bound_
            current_lower_bounds = []

            if self.max_iter == 0:
                best_params = self._get_parameters()
                best_n_iter = 0
            else:
                converged = False
                for n_iter in range(1, self.max_iter + 1):
                    prev_lower_bound = lower_bound

                    log_prob_norm, log_resp = self._e_step(X)
                    self._m_step(X, log_resp)
                    lower_bound = self._compute_lower_bound(log_resp, log_prob_norm)
                    current_lower_bounds.append(lower_bound)

                    change = lower_bound - prev_lower_bound
                    self._print_verbose_msg_iter_end(n_iter, change)

                    if abs(change) < self.tol:
                        converged = True
                        break

                self._print_verbose_msg_init_end(lower_bound, converged)

                if lower_bound > max_lower_bound or max_lower_bound == -np.inf:
                    max_lower_bound = lower_bound
                    best_params = self._get_parameters()
                    best_n_iter = n_iter
                    best_lower_bounds = current_lower_bounds
                    self.converged_ = converged

        # Should only warn about convergence if max_iter > 0, otherwise
        # the user is assumed to have used 0-iters initialization
        # to get the initial means.
        if not self.converged_ and self.max_iter > 0:
                (
                    "Best performing initialization did not converge. "
                    "Try different init parameters, or increase max_iter, "
                    "tol, or check for degenerate data."
                ),
            )

        self._set_parameters(best_params)
        self.lower_bound_ = max_lower_bound
        self.lower_bounds_ = best_lower_bounds

        # Always do a final e-step to guarantee that the labels returned by
        # fit_predict(X) are always consistent with fit(X).predict(X)
        # for any value of max_iter and tol (and any random_state).
        _, log_resp = self._e_step(X)

        return log_resp.argmax(axis=1)

    def _e_step(self, X):
        """E step.

        Returns
        -------
            Mean of the logarithms of the probabilities of each sample in X

        log_responsibility : array, shape (n_samples, n_components)
        """
        log_prob_norm, log_resp = self._estimate_log_prob_resp(X)
        return np.mean(log_prob_norm), log_resp

        """M step.

        """
        pass

        pass

        pass

        Returns
        -------
        log_prob : array, shape (n_samples,)
            Log-likelihood of each sample in `X` under the current model.
        """

        return logsumexp(self._estimate_weighted_log_prob(X), axis=1)

        """Compute the per-sample average log-likelihood of the given data X.

        X : array-like of shape (n_samples, n_dimensions)

        y : Ignored

        Returns
        -------
            Log-likelihood of `X` under the Gaussian mixture model.
        """
        return self.score_samples(X).mean()

        """Predict the labels for the data samples in X using trained model.

        Returns
        -------
        """
        return self._estimate_weighted_log_prob(X).argmax(axis=1)

        """Evaluate the components' density for each sample.

        Returns
        -------
        resp : array, shape (n_samples, n_components)
            Density of each Gaussian component for each sample in X.
        """
        _, log_resp = self._estimate_log_prob_resp(X)
        return np.exp(log_resp)

    def sample(self, n_samples=1):
        """Generate random samples from the fitted Gaussian distribution.

        Returns
        -------
        X : array, shape (n_samples, n_features)
            Randomly generated sample.

        y : array, shape (nsamples,)
        """

        if n_samples < 1:
                "Invalid value for 'n_samples': %d . The sampling requires at "
                "least one sample." % (self.n_components)
            )

        _, n_features = self.means_.shape
        n_samples_comp = rng.multinomial(n_samples, self.weights_)

                [
                    rng.multivariate_normal(mean, covariance, int(sample))
                    for (mean, covariance, sample) in zip(
                        self.means_, self.covariances_, n_samples_comp
                    )
                ]
            )
                [
                    rng.multivariate_normal(mean, self.covariances_, int(sample))
                    for (mean, sample) in zip(self.means_, n_samples_comp)
                ]
            )
        else:
                [
                    mean
                    + rng.standard_normal(size=(sample, n_features))
                    * np.sqrt(covariance)
                    )
                ]
            )

        y = np.concatenate(
            [np.full(sample, j, dtype=int) for j, sample in enumerate(n_samples_comp)]
        )

        return (X, y)

    def _estimate_weighted_log_prob(self, X):
        """Estimate the weighted log-probabilities, log P(X | Z) + log weights.

        Returns
        -------
        weighted_log_prob : array, shape (n_samples, n_component)
        """
        return self._estimate_log_prob(X) + self._estimate_log_weights()

        """Estimate log-weights in EM algorithm, E[ log pi ] in VB algorithm.

        Returns
        -------
        log_weight : array, shape (n_components, )
        """
        pass

        """Estimate the log-probabilities log P(X | Z).

        Compute the log-probabilities per each component for each sample.

        Returns
        -------
        log_prob : array, shape (n_samples, n_component)
        """
        pass

    def _estimate_log_prob_resp(self, X):
        """Estimate log probabilities and responsibilities for each sample.

        Compute the log probabilities, weighted log probabilities per
        component and responsibilities for each sample in X with respect to
        the current state of the model.

        Returns
        -------
        log_prob_norm : array, shape (n_samples,)
            log p(X)

        log_responsibilities : array, shape (n_samples, n_components)
            logarithm of the responsibilities
        """
        weighted_log_prob = self._estimate_weighted_log_prob(X)
        log_prob_norm = logsumexp(weighted_log_prob, axis=1)
        with np.errstate(under="ignore"):
            # ignore underflow
            log_resp = weighted_log_prob - log_prob_norm[:, np.newaxis]
        return log_prob_norm, log_resp

    def _print_verbose_msg_init_beg(self, n_init):
        """Print verbose message on initialization."""
        if self.verbose == 1:
            print("Initialization %d" % n_init)
        elif self.verbose >= 2:
            self._init_prev_time = time()
            self._iter_prev_time = self._init_prev_time

    def _print_verbose_msg_iter_end(self, n_iter, diff_ll):
        if n_iter % self.verbose_interval == 0:
            if self.verbose == 1:
                print("  Iteration %d" % n_iter)
            elif self.verbose >= 2:
                cur_time = time()
                print(
                    "  Iteration %d\t time lapse %.5fs\t ll change %.5f"
                    % (n_iter, cur_time - self._iter_prev_time, diff_ll)
                )
                self._iter_prev_time = cur_time

    def _print_verbose_msg_init_end(self, lb, init_has_converged):
        """Print verbose message on the end of iteration."""
        converged_msg = "converged" if init_has_converged else "did not converge"
            print(f"Initialization {converged_msg}.")
            t = time() - self._init_prev_time
            print(
                f"Initialization {converged_msg}. time lapse {t:.5f}s\t lower bound"
                f" {lb:.5f}."
            )

        self,
        tol,
        max_iter,
        n_init,
        verbose,
    ):

        """
        pass

        """

            label = (
                )
                .fit(X)
                .labels_
            )
            )
            )
                X,
            )

        """
        pass

        y : Ignored

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """
            )

            else:

                        break

                (
                ),
            )

        """E step.

        Returns
        -------

        """

        """M step.

        """
        pass

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------

        """

            )

                [
                    )
                ]
            )
                [
                ]
            )
        else:
                [
                    mean
                    )
                ]
            )

        )

        Returns
        -------
        """

        Returns
        -------
        """
        pass

        Returns
        -------
        """
        pass

        Returns
        -------
            log p(X)

        """

                print(
                )

            print(
            )

def _check_weights(weights, n_components):
    """Check the user provided 'weights'.

    weights : array-like of shape (n_components,)
        The proportions of components of each mixture.

        Number of components.

    Returns
    -------
    weights : array, shape (n_components,)
    """
    weights = check_array(weights, dtype=[np.float64, np.float32], ensure_2d=False)
    _check_shape(weights, (n_components,), "weights")

    # check range
    if any(np.less(weights, 0.0)) or any(np.greater(weights, 1.0)):
            "The parameter 'weights' should be in the range "
            "[0, 1], but got max value %.5f, min value %.5f"
            % (np.min(weights), np.max(weights))
        )

    atol = 1e-6 if weights.dtype == np.float32 else 1e-8
    if not np.allclose(np.abs(1.0 - np.sum(weights)), 0.0, atol=atol):
            "The parameter 'weights' should be normalized, but got sum(weights) = %.5f"
            % np.sum(weights)
        )

def _check_means(means, n_components, n_features):
    """Validate the provided 'means'.

    means : array-like of shape (n_components, n_features)
        The centers of the current components.

        Number of features.

    Returns
    -------
    means : array, (n_components, n_features)
    """
    means = check_array(means, dtype=[np.float64, np.float32], ensure_2d=False)
    _check_shape(means, (n_components, n_features), "means")

def _check_precision_positivity(precision, covariance_type):
    """Check a precision vector is positive-definite."""
    if np.any(np.less_equal(precision, 0.0)):
        raise ValueError("'%s precision' should be positive" % covariance_type)

def _check_precision_matrix(precision, covariance_type):
    """Check a precision matrix is symmetric and positive-definite."""
    if not (
        np.allclose(precision, precision.T) and np.all(linalg.eigvalsh(precision) > 0.0)
    ):
            "'%s precision' should be symmetric, positive-definite" % covariance_type
        )

def _check_precisions_full(precisions, covariance_type):
    """Check the precision matrices are symmetric and positive-definite."""
    for prec in precisions:
        _check_precision_matrix(prec, covariance_type)

def _check_precisions(precisions, covariance_type, n_components, n_features):
    """Validate user provided precisions.

    precisions : array-like
        'full' : shape of (n_components, n_features, n_features)
        'tied' : shape of (n_features, n_features)
        'diag' : shape of (n_components, n_features)
        'spherical' : shape of (n_components,)

    covariance_type : str

    Returns
    -------
    precisions : array
    """
    precisions = check_array(
        precisions,
        dtype=[np.float64, np.float32],
        allow_nd=covariance_type == "full",
    )

    precisions_shape = {
        "full": (n_components, n_features, n_features),
        "tied": (n_features, n_features),
        "diag": (n_components, n_features),
        "spherical": (n_components,),
    }
    _check_shape(
        precisions, precisions_shape[covariance_type], "%s precision" % covariance_type
    )

    _check_precisions = {
        "full": _check_precisions_full,
        "tied": _check_precision_matrix,
        "diag": _check_precision_positivity,
        "spherical": _check_precision_positivity,
    }
    _check_precisions[covariance_type](precisions, covariance_type)
    return precisions

def _estimate_gaussian_covariances_full(resp, X, nk, means, reg_covar):
    """Estimate the full covariance matrices.

    resp : array-like of shape (n_samples, n_components)

    nk : array-like of shape (n_components,)

    reg_covar : float

    Returns
    -------
    covariances : array, shape (n_components, n_features, n_features)
        The covariance matrix of the current components.
    """
    n_components, n_features = means.shape
    covariances = np.empty((n_components, n_features, n_features), dtype=X.dtype)
        diff = X - means[k]
        covariances[k] = np.dot(resp[:, k] * diff.T, diff) / nk[k]
        covariances[k].flat[:: n_features + 1] += reg_covar
    return covariances

def _estimate_gaussian_covariances_tied(resp, X, nk, means, reg_covar):
    """Estimate the tied covariance matrix.

    Returns
    -------
    covariance : array, shape (n_features, n_features)
        The tied covariance matrix of the components.
    """
    avg_X2 = np.dot(X.T, X)
    avg_means2 = np.dot(nk * means.T, means)
    covariance = avg_X2 - avg_means2
    covariance /= nk.sum()
    covariance.flat[:: len(covariance) + 1] += reg_covar

def _estimate_gaussian_covariances_diag(resp, X, nk, means, reg_covar):
    """Estimate the diagonal covariance vectors.

    responsibilities : array-like of shape (n_samples, n_components)

    Returns
    -------
    covariances : array, shape (n_components, n_features)
        The covariance vector of the current components.
    """
    avg_X2 = np.dot(resp.T, X * X) / nk[:, np.newaxis]
    avg_means2 = means**2
    return avg_X2 - avg_means2 + reg_covar

def _estimate_gaussian_covariances_spherical(resp, X, nk, means, reg_covar):
    """Estimate the spherical variance values.

    Returns
    -------
    variances : array, shape (n_components,)
        The variance values of each components.
    """
    return _estimate_gaussian_covariances_diag(resp, X, nk, means, reg_covar).mean(1)

def _estimate_gaussian_parameters(X, resp, reg_covar, covariance_type):
    """Estimate the Gaussian distribution parameters.

        The input data array.

        The responsibilities for each data sample in X.

        The regularization added to the diagonal of the covariance matrices.

    covariance_type : {'full', 'tied', 'diag', 'spherical'}
        The type of precision matrices.

    Returns
    -------
        The numbers of data samples in the current components.

    covariances : array-like
        The shape depends of the covariance_type.
    """
    nk = resp.sum(axis=0) + 10 * np.finfo(resp.dtype).eps
    means = np.dot(resp.T, X) / nk[:, np.newaxis]
    covariances = {
        "full": _estimate_gaussian_covariances_full,
        "tied": _estimate_gaussian_covariances_tied,
        "diag": _estimate_gaussian_covariances_diag,
        "spherical": _estimate_gaussian_covariances_spherical,
    }[covariance_type](resp, X, nk, means, reg_covar)
    return nk, means, covariances

def _compute_precision_cholesky(covariances, covariance_type):
    """Compute the Cholesky decomposition of the precisions.

    Returns
    -------
    precisions_cholesky : array-like
        The cholesky decomposition of sample precisions of the current
        components. The shape depends of the covariance_type.
    """
    estimate_precision_error_message = (
        "Fitting the mixture model failed because some components have "
        "ill-defined empirical covariance (for instance caused by singleton "
        "or collapsed samples). Try to decrease the number of components, "
        "increase reg_covar, or scale the input data."
    )
    dtype = covariances.dtype
    if dtype == np.float32:
        estimate_precision_error_message += (
            " The numerical accuracy can also be improved by passing float64"
            " data instead of float32."
        )

    if covariance_type == "full":
        n_components, n_features, _ = covariances.shape
        precisions_chol = np.empty((n_components, n_features, n_features), dtype=dtype)
        for k, covariance in enumerate(covariances):
            try:
                cov_chol = linalg.cholesky(covariance, lower=True)
                raise ValueError(estimate_precision_error_message)
            precisions_chol[k] = linalg.solve_triangular(
                cov_chol, np.eye(n_features, dtype=dtype), lower=True
            ).T
    elif covariance_type == "tied":
        _, n_features = covariances.shape
        try:
            cov_chol = linalg.cholesky(covariances, lower=True)
        except linalg.LinAlgError:
            raise ValueError(estimate_precision_error_message)
        precisions_chol = linalg.solve_triangular(
            cov_chol, np.eye(n_features, dtype=dtype), lower=True
        ).T
    else:
        if np.any(np.less_equal(covariances, 0.0)):
        precisions_chol = 1.0 / np.sqrt(covariances)
    return precisions_chol

def _flipudlr(array):
    """Reverse the rows and columns of an array."""
    return np.flipud(np.fliplr(array))

def _compute_precision_cholesky_from_precisions(precisions, covariance_type):
    r"""Compute the Cholesky decomposition of precisions using precisions themselves.

    As implemented in :func:`_compute_precision_cholesky`, the `precisions_cholesky_` is
    an upper-triangular matrix for each Gaussian component, which can be expressed as
    the $UU^T$ factorization of the precision matrix for each Gaussian component, where
    $U$ is an upper-triangular matrix.

    In order to use the Cholesky decomposition to get $UU^T$, the precision matrix
    $\Lambda$ needs to be permutated such that its rows and columns are reversed, which
    can be done by applying a similarity transformation with an exchange matrix $J$,
    where the 1 elements reside on the anti-diagonal and all other elements are 0. In
    particular, the Cholesky decomposition of the transformed precision matrix is
    $J\Lambda J=LL^T$, where $L$ is a lower-triangular matrix. Because $\Lambda=UU^T$
    and $J=J^{-1}=J^T$, the `precisions_cholesky_` for each Gaussian component can be
    expressed as $JLJ$.

    Refer to #26415 for details.

        The precision matrix of the current components.
        The shape depends on the covariance_type.

    Returns
    -------
        components. The shape depends on the covariance_type.
    """
        precisions_cholesky = np.array(
            [
                _flipudlr(linalg.cholesky(_flipudlr(precision), lower=True))
                for precision in precisions
            ]
        )
        precisions_cholesky = _flipudlr(
            linalg.cholesky(_flipudlr(precisions), lower=True)
        )
    else:
        precisions_cholesky = np.sqrt(precisions)
    return precisions_cholesky

def _compute_log_det_cholesky(matrix_chol, covariance_type, n_features):
    """Compute the log-det of the cholesky decomposition of matrices.

    matrix_chol : array-like
        Cholesky decompositions of the matrices.

    Returns
    -------
        The determinant of the precision matrix for each component.
    """
        n_components, _, _ = matrix_chol.shape
        log_det_chol = np.sum(
            np.log(matrix_chol.reshape(n_components, -1)[:, :: n_features + 1]), axis=1
        )

        log_det_chol = np.sum(np.log(np.diag(matrix_chol)))

    elif covariance_type == "diag":
        log_det_chol = np.sum(np.log(matrix_chol), axis=1)

    else:
        log_det_chol = n_features * np.log(matrix_chol)

    return log_det_chol

def _estimate_log_gaussian_prob(X, means, precisions_chol, covariance_type):
    """Estimate the log Gaussian probability.

    precisions_chol : array-like
        Cholesky decompositions of the precision matrices.

    Returns
    -------
    log_prob : array, shape (n_samples, n_components)
    """
    n_components, _ = means.shape
    # The determinant of the precision matrix from the Cholesky decomposition
    # corresponds to the negative half of the determinant of the full precision
    # matrix.
    # In short: det(precision_chol) = - det(precision) / 2
    log_det = _compute_log_det_cholesky(precisions_chol, covariance_type, n_features)

        log_prob = np.empty((n_samples, n_components), dtype=X.dtype)
        for k, (mu, prec_chol) in enumerate(zip(means, precisions_chol)):
            y = np.dot(X, prec_chol) - np.dot(mu, prec_chol)
            log_prob[:, k] = np.sum(np.square(y), axis=1)

        for k, mu in enumerate(means):
            y = np.dot(X, precisions_chol) - np.dot(mu, precisions_chol)

        precisions = precisions_chol**2
        log_prob = (
            np.sum((means**2 * precisions), 1)
            - 2.0 * np.dot(X, (means * precisions).T)
            + np.dot(X**2, precisions.T)
        )

    elif covariance_type == "spherical":
            np.sum(means**2, 1) * precisions
            - 2 * np.dot(X, means.T * precisions)
            + np.outer(row_norms(X, squared=True), precisions)
        )
    # Since we are using the precision of the Cholesky decomposition,
    # `- 0.5 * log_det_precision` becomes `+ log_det_precision_chol`
    return -0.5 * (n_features * np.log(2 * np.pi).astype(X.dtype) + log_prob) + log_det

class GaussianMixture(BaseMixture):
    """Gaussian Mixture.

    Representation of a Gaussian mixture model probability distribution.
    This class allows to estimate the parameters of a Gaussian mixture
    distribution.

    Read more in the :ref:`User Guide <gmm>`.

        The number of mixture components.

        - 'full': each component has its own general covariance matrix.
        - 'tied': all components share the same general covariance matrix.
        - 'diag': each component has its own diagonal covariance matrix.
        - 'spherical': each component has its own single variance.

        For an example of using `covariance_type`, refer to
        :ref:`sphx_glr_auto_examples_mixture_plot_gmm_selection.py`.

        lower bound average gain is below this threshold.

        The number of initializations to perform. The best results are kept.

        precisions.
        String must be one of:

        - 'kmeans' : responsibilities are initialized using kmeans.
        - 'k-means++' : use the k-means++ method to initialize.
        - 'random' : responsibilities are initialized randomly.
        - 'random_from_data' : initial means are randomly selected data points.

    weights_init : array-like of shape (n_components, ), default=None
        The user-provided initial weights.
        If it is None, weights are initialized using the `init_params` method.

    means_init : array-like of shape (n_components, n_features), default=None
        The user-provided initial means,
        If it is None, means are initialized using the `init_params` method.

    precisions_init : array-like, default=None
        The user-provided initial precisions (inverse of the covariance
        matrices).
        If it is None, precisions are initialized using the 'init_params'
        method.
        The shape depends on 'covariance_type'::

        In that case, 'n_init' is ignored and only a single initialization
        occurs upon the first call.

        time. The shape depends on `covariance_type`::

        True when convergence of the best fit of EM was reached, False otherwise.

        Number of step used by the best fit of EM to reach the convergence.

        Lower bound value on the log-likelihood (of the training data with
        respect to the model) of the best fit of EM.

        The list of lower bound values on the log-likelihood from each
        iteration of the best fit of EM.

    See Also
    --------
    BayesianGaussianMixture : Gaussian mixture model fit with a variational
        inference.

    Examples
    --------
    >>> from sklearn.mixture import GaussianMixture
    >>> X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
    >>> gm = GaussianMixture(n_components=2, random_state=0).fit(X)
    >>> gm.means_
           [ 1.,  2.]])
    >>> gm.predict([[0, 0], [12, 3]])

    For a comparison of Gaussian Mixture with other clustering algorithms, see
    """

        "covariance_type": [StrOptions({"full", "tied", "diag", "spherical"})],
        "weights_init": ["array-like", None],
        "means_init": ["array-like", None],
        "precisions_init": ["array-like", None],
    }

        self,
        *,
        tol=1e-3,
        n_init=1,
        weights_init=None,
        means_init=None,
        precisions_init=None,
    ):
            tol=tol,
        )

        self.weights_init = weights_init
        self.means_init = means_init
        self.precisions_init = precisions_init

        """Check the Gaussian mixture parameters are well defined."""

        if self.weights_init is not None:
            self.weights_init = _check_weights(self.weights_init, self.n_components)

        if self.means_init is not None:
            self.means_init = _check_means(
                self.means_init, self.n_components, n_features
            )

        if self.precisions_init is not None:
            self.precisions_init = _check_precisions(
                self.precisions_init,
                self.covariance_type,
            )

        # If all the initial parameters are all provided, then there is no need to run
        # the initialization.
        compute_resp = (
            self.weights_init is None
            or self.means_init is None
            or self.precisions_init is None
        )
        if compute_resp:
            super()._initialize_parameters(X, random_state)
        else:
            self._initialize(X, None)

        """Initialization of the Gaussian mixture parameters.

        """
        weights, means, covariances = None, None, None
        if resp is not None:
            weights, means, covariances = _estimate_gaussian_parameters(
                X, resp, self.reg_covar, self.covariance_type
            )
            if self.weights_init is None:
                weights /= n_samples

        self.weights_ = weights if self.weights_init is None else self.weights_init
        self.means_ = means if self.means_init is None else self.means_init

        if self.precisions_init is None:
            self.covariances_ = covariances
            self.precisions_cholesky_ = _compute_precision_cholesky(
                covariances, self.covariance_type
            )
        else:
            self.precisions_cholesky_ = _compute_precision_cholesky_from_precisions(
                self.precisions_init, self.covariance_type
            )

        """M step.

        """
        self.weights_, self.means_, self.covariances_ = _estimate_gaussian_parameters(
        )
        self.weights_ /= self.weights_.sum()
        )

        return _estimate_log_gaussian_prob(
        )

        return np.log(self.weights_)

    def _compute_lower_bound(self, _, log_prob_norm):
        return log_prob_norm

        return (
            self.weights_,
        )

        (
        ) = params

        # Attributes computation

        dtype = self.precisions_cholesky_.dtype
            self.precisions_ = np.empty_like(self.precisions_cholesky_)
            for k, prec_chol in enumerate(self.precisions_cholesky_):
                self.precisions_[k] = np.dot(prec_chol, prec_chol.T)

            )
        else:

    def _n_parameters(self):
        """Return the number of free parameters in the model."""
            cov_params = self.n_components * n_features * (n_features + 1) / 2.0
            cov_params = self.n_components * n_features
            cov_params = n_features * (n_features + 1) / 2.0
        elif self.covariance_type == "spherical":
            cov_params = self.n_components
        mean_params = n_features * self.n_components
        return int(cov_params + mean_params + self.n_components - 1)

    def bic(self, X):
        """Bayesian information criterion for the current model on the input X.

        You can refer to this :ref:`mathematical section <aic_bic>` for more
        details regarding the formulation of the BIC used.

        For an example of GMM selection using `bic` information criterion,
        refer to :ref:`sphx_glr_auto_examples_mixture_plot_gmm_selection.py`.

        X : array of shape (n_samples, n_dimensions)

        Returns
        -------
        bic : float
            The lower the better.
        """
        return -2 * self.score(X) * X.shape[0] + self._n_parameters() * np.log(
            X.shape[0]
        )

    def aic(self, X):
        """Akaike information criterion for the current model on the input X.

        details regarding the formulation of the AIC used.

        Returns
        -------
        aic : float
        """
        return -2 * self.score(X) * X.shape[0] + 2 * self._n_parameters()

        self,
        *,
        tol=1e-3,
        n_init=1,
    ):
            tol=tol,
        )

            )

            )

        )
        else:

        """
            )

            )
        else:
            )

        """M step.

        """
        )
        )

        )

        return (
        )

        (
        ) = params

            )
        else:

        Returns
        -------
        bic : float
        """
        )

        Returns
        -------
        aic : float
        """

def test_log_dirichlet_norm():

    weight_concentration = rng.rand(2)
    expected_norm = gammaln(np.sum(weight_concentration)) - np.sum(
        gammaln(weight_concentration)
    )
    predected_norm = _log_dirichlet_norm(weight_concentration)

    assert_almost_equal(expected_norm, predected_norm)

def test_log_wishart_norm():

    n_components, n_features = 5, 2
    degrees_of_freedom = np.abs(rng.rand(n_components)) + 1.0
    log_det_precisions_chol = n_features * np.log(range(2, 2 + n_components))

    expected_norm = np.empty(5)
    for k, (degrees_of_freedom_k, log_det_k) in enumerate(
        zip(degrees_of_freedom, log_det_precisions_chol)
    ):
        expected_norm[k] = -(
            degrees_of_freedom_k * (log_det_k + 0.5 * n_features * np.log(2.0))
            + np.sum(
                gammaln(
                    0.5
                    * (degrees_of_freedom_k - np.arange(0, n_features)[:, np.newaxis])
                ),
                0,
            )
        ).item()
    predected_norm = _log_wishart_norm(
        degrees_of_freedom, log_det_precisions_chol, n_features
    )

def test_bayesian_mixture_weights_prior_initialisation():
    n_samples, n_components, n_features = 10, 5, 2

    # Check correct init for a given value of weight_concentration_prior
    weight_concentration_prior = rng.rand()
    bgmm = BayesianGaussianMixture(
        weight_concentration_prior=weight_concentration_prior, random_state=rng
    ).fit(X)
    assert_almost_equal(weight_concentration_prior, bgmm.weight_concentration_prior_)

    # Check correct init for the default value of weight_concentration_prior
    bgmm = BayesianGaussianMixture(n_components=n_components, random_state=rng).fit(X)
    assert_almost_equal(1.0 / n_components, bgmm.weight_concentration_prior_)

def test_bayesian_mixture_mean_prior_initialisation():
    n_samples, n_components, n_features = 10, 3, 2

    # Check correct init for a given value of mean_precision_prior
    mean_precision_prior = rng.rand()
        mean_precision_prior=mean_precision_prior, random_state=rng
    ).fit(X)
    assert_almost_equal(mean_precision_prior, bgmm.mean_precision_prior_)

    # Check correct init for the default value of mean_precision_prior
    bgmm = BayesianGaussianMixture(random_state=rng).fit(X)
    assert_almost_equal(1.0, bgmm.mean_precision_prior_)

    # Check correct init for a given value of mean_prior
    mean_prior = rng.rand(n_features)
        n_components=n_components, mean_prior=mean_prior, random_state=rng
    ).fit(X)
    assert_almost_equal(mean_prior, bgmm.mean_prior_)

    # Check correct init for the default value of bemean_priorta
    assert_almost_equal(X.mean(axis=0), bgmm.mean_prior_)

def test_bayesian_mixture_precisions_prior_initialisation():
    n_samples, n_features = 10, 2

    # Check raise message for a bad value of degrees_of_freedom_prior
    bad_degrees_of_freedom_prior_ = n_features - 1.0
        degrees_of_freedom_prior=bad_degrees_of_freedom_prior_, random_state=rng
    )
    msg = (
        "The parameter 'degrees_of_freedom_prior' should be greater than"
        f" {n_features - 1}, but got {bad_degrees_of_freedom_prior_:.3f}."
    )
        bgmm.fit(X)

    # Check correct init for a given value of degrees_of_freedom_prior
    degrees_of_freedom_prior = rng.rand() + n_features - 1.0
        degrees_of_freedom_prior=degrees_of_freedom_prior, random_state=rng
    ).fit(X)
    assert_almost_equal(degrees_of_freedom_prior, bgmm.degrees_of_freedom_prior_)

    # Check correct init for the default value of degrees_of_freedom_prior
    degrees_of_freedom_prior_default = n_features
        degrees_of_freedom_prior=degrees_of_freedom_prior_default, random_state=rng
    ).fit(X)
        degrees_of_freedom_prior_default, bgmm.degrees_of_freedom_prior_
    )

    # Check correct init for a given value of covariance_prior
    covariance_prior = {
        "full": np.cov(X.T, bias=1) + 10,
        "tied": np.cov(X.T, bias=1) + 5,
        "diag": np.diag(np.atleast_2d(np.cov(X.T, bias=1))) + 3,
        "spherical": rng.rand(),
    }

    bgmm = BayesianGaussianMixture(random_state=rng)
    for cov_type in ["full", "tied", "diag", "spherical"]:
        bgmm.covariance_type = cov_type
        bgmm.covariance_prior = covariance_prior[cov_type]
        assert_almost_equal(covariance_prior[cov_type], bgmm.covariance_prior_)

    # Check correct init for the default value of covariance_prior
    covariance_prior_default = {
        "full": np.atleast_2d(np.cov(X.T)),
        "tied": np.atleast_2d(np.cov(X.T)),
        "diag": np.var(X, axis=0, ddof=1),
        "spherical": np.var(X, axis=0, ddof=1).mean(),
    }

    bgmm = BayesianGaussianMixture(random_state=0)
        assert_almost_equal(covariance_prior_default[cov_type], bgmm.covariance_prior_)

def test_bayesian_mixture_check_is_fitted():

    # Check raise message

    msg = "This BayesianGaussianMixture instance is not fitted yet."
        bgmm.score(X)

def test_bayesian_mixture_weights():

    # Case Dirichlet distribution for the weight concentration prior type
        weight_concentration_prior_type="dirichlet_distribution",
    ).fit(X)

    expected_weights = bgmm.weight_concentration_ / np.sum(bgmm.weight_concentration_)
    assert_almost_equal(expected_weights, bgmm.weights_)
    assert_almost_equal(np.sum(bgmm.weights_), 1.0)

    # Case Dirichlet process for the weight concentration prior type
    dpgmm = BayesianGaussianMixture(
    ).fit(X)
    weight_dirichlet_sum = (
        dpgmm.weight_concentration_[0] + dpgmm.weight_concentration_[1]
    )
    tmp = dpgmm.weight_concentration_[1] / weight_dirichlet_sum
    expected_weights = (
        dpgmm.weight_concentration_[0]
        / weight_dirichlet_sum
        * np.hstack((1, np.cumprod(tmp[:-1])))
    )
    expected_weights /= np.sum(expected_weights)
    assert_almost_equal(expected_weights, dpgmm.weights_)
    assert_almost_equal(np.sum(dpgmm.weights_), 1.0)

def test_monotonic_likelihood():
    # We check that each step of the each step of variational inference without
    # regularization improve monotonically the training set of the bound
    rand_data = RandomData(rng, scale=20)
    n_components = rand_data.n_components

    for prior_type in PRIOR_TYPE:
        for covar_type in COVARIANCE_TYPE:
            X = rand_data.X[covar_type]
            bgmm = BayesianGaussianMixture(
                weight_concentration_prior_type=prior_type,
                n_components=2 * n_components,
                covariance_type=covar_type,
                warm_start=True,
                tol=1e-3,
            )
            current_lower_bound = -np.inf
            # Do one training iteration at a time so we can make sure that the
            # training log likelihood increases after each iteration.
            for _ in range(600):
                prev_lower_bound = current_lower_bound
                current_lower_bound = bgmm.fit(X).lower_bound_
                assert current_lower_bound >= prev_lower_bound

                if bgmm.converged_:
                    break
            assert bgmm.converged_

def test_compare_covar_type():
    # We can compare the 'full' precision with the other cov_type if we apply
    # 1 iter of the M-step (done during _initialize_parameters).
    rand_data = RandomData(rng, scale=7)
    X = rand_data.X["full"]

        # Computation of the full_covariance
        bgmm = BayesianGaussianMixture(
            weight_concentration_prior_type=prior_type,
            n_components=2 * n_components,
            covariance_type="full",
            tol=1e-7,
        )
        bgmm._check_parameters(X)
        bgmm._initialize_parameters(X, np.random.RandomState(0))
        full_covariances = (
            bgmm.covariances_ * bgmm.degrees_of_freedom_[:, np.newaxis, np.newaxis]
        )

        # Check tied_covariance = mean(full_covariances, 0)
            covariance_type="tied",
            tol=1e-7,
        )

        tied_covariance = bgmm.covariances_ * bgmm.degrees_of_freedom_
        assert_almost_equal(tied_covariance, np.mean(full_covariances, 0))

        # Check diag_covariance = diag(full_covariances)
            covariance_type="diag",
            tol=1e-7,
        )

        diag_covariances = bgmm.covariances_ * bgmm.degrees_of_freedom_[:, np.newaxis]
            diag_covariances, np.array([np.diag(cov) for cov in full_covariances])
        )

        # Check spherical_covariance = np.mean(diag_covariances, 0)
            covariance_type="spherical",
            tol=1e-7,
        )

        spherical_covariances = bgmm.covariances_ * bgmm.degrees_of_freedom_
        assert_almost_equal(spherical_covariances, np.mean(diag_covariances, 1))

def test_check_covariance_precision():
    # We check that the dot product of the covariance and the precision
    # matrices is identity.
    n_components, n_features = 2 * rand_data.n_components, 2

    # Computation of the full_covariance
        n_components=n_components, max_iter=100, random_state=rng, tol=1e-3, reg_covar=0
    )
    for covar_type in COVARIANCE_TYPE:
        bgmm.covariance_type = covar_type
        bgmm.fit(rand_data.X[covar_type])

        if covar_type == "full":
            for covar, precision in zip(bgmm.covariances_, bgmm.precisions_):
                assert_almost_equal(np.dot(covar, precision), np.eye(n_features))
        elif covar_type == "tied":
                np.dot(bgmm.covariances_, bgmm.precisions_), np.eye(n_features)
            )

        elif covar_type == "diag":
                bgmm.covariances_ * bgmm.precisions_,
                np.ones((n_components, n_features)),
            )

        else:
                bgmm.covariances_ * bgmm.precisions_, np.ones(n_components)
            )

def test_invariant_translation():
    # We check here that adding a constant in the data change correctly the
    # parameters of the mixture
    rand_data = RandomData(rng, scale=100)
    n_components = 2 * rand_data.n_components

            bgmm1 = BayesianGaussianMixture(
                tol=1e-3,
                reg_covar=0,
            ).fit(X)
            bgmm2 = BayesianGaussianMixture(
                tol=1e-3,
            ).fit(X + 100)

            assert_almost_equal(bgmm1.means_, bgmm2.means_ - 100)
            assert_almost_equal(bgmm1.weights_, bgmm2.weights_)
            assert_almost_equal(bgmm1.covariances_, bgmm2.covariances_)

def test_bayesian_mixture_fit_predict(seed, max_iter, tol):
    rand_data = RandomData(rng, n_samples=50, scale=7)

        bgmm1 = BayesianGaussianMixture(
            tol=tol,
            reg_covar=0,
        )
        bgmm1.covariance_type = covar_type
        bgmm2 = copy.deepcopy(bgmm1)
        X = rand_data.X[covar_type]

        Y_pred1 = bgmm1.fit(X).predict(X)
        Y_pred2 = bgmm2.fit_predict(X)
        assert_array_equal(Y_pred1, Y_pred2)

def test_bayesian_mixture_fit_predict_n_init():
    # Check that fit_predict is equivalent to fit.predict, when n_init > 1
    X = np.random.RandomState(0).randn(50, 5)
    gm = BayesianGaussianMixture(n_components=5, n_init=10, random_state=0)
    y_pred1 = gm.fit_predict(X)
    y_pred2 = gm.predict(X)

def test_bayesian_mixture_predict_predict_proba():
    # this is the same test as test_gaussian_mixture_predict_predict_proba()
    rand_data = RandomData(rng)
            Y = rand_data.Y
                n_components=rand_data.n_components,
            )

            # Check a warning message arrive if we don't do fit
            msg = (
                "This BayesianGaussianMixture instance is not fitted yet. "
                "Call 'fit' with appropriate arguments before using this "
            )
            with pytest.raises(NotFittedError, match=msg):
                bgmm.predict(X)

            bgmm.fit(X)
            Y_pred = bgmm.predict(X)
            Y_pred_proba = bgmm.predict_proba(X).argmax(axis=1)
            assert_array_equal(Y_pred, Y_pred_proba)
            assert adjusted_rand_score(Y, Y_pred) >= 0.95

def test_gaussian_mixture_n_iter(estimator):
    # check that n_iter is the number of iteration performed.
    X = rng.rand(10, 5)
    estimator.set_params(max_iter=max_iter)
    assert estimator.n_iter_ == max_iter

def test_mixture_n_components_greater_than_n_samples_error(estimator):
    """Check error when n_components <= n_samples"""
    estimator.set_params(n_components=12)

    msg = "Expected n_samples >= n_components"
        estimator.fit(X)

def generate_data(
    n_samples, n_features, weights, means, precisions, covariance_type, dtype=np.float64
):

    X = []
    if covariance_type == "spherical":
        for _, (w, m, c) in enumerate(zip(weights, means, precisions["spherical"])):
            X.append(
                    m, c * np.eye(n_features), int(np.round(w * n_samples))
                ).astype(dtype)
            )
    if covariance_type == "diag":
        for _, (w, m, c) in enumerate(zip(weights, means, precisions["diag"])):
            X.append(
                    m, np.diag(c), int(np.round(w * n_samples))
            )
    if covariance_type == "tied":
        for _, (w, m) in enumerate(zip(weights, means)):
            X.append(
                    m, precisions["tied"], int(np.round(w * n_samples))
            )
        for _, (w, m, c) in enumerate(zip(weights, means, precisions["full"])):
            X.append(
                rng.multivariate_normal(m, c, int(np.round(w * n_samples))).astype(
                    dtype
                )
            )

    X = np.vstack(X)
    return X

class RandomData:
        self,
        rng,
        scale=50,
    ):
        self.n_samples = n_samples

        self.weights = rng.rand(n_components).astype(dtype)
        self.weights = self.weights.astype(dtype) / self.weights.sum()
        self.means = rng.rand(n_components, n_features).astype(dtype) * scale
        self.covariances = {
            "spherical": 0.5 + rng.rand(n_components).astype(dtype),
            "diag": (0.5 + rng.rand(n_components, n_features).astype(dtype)) ** 2,
            "tied": make_spd_matrix(n_features, random_state=rng).astype(dtype),
            "full": np.array(
                [
                    make_spd_matrix(n_features, random_state=rng).astype(dtype) * 0.5
                    for _ in range(n_components)
                ]
            ),
        }
        self.precisions = {
            "spherical": 1.0 / self.covariances["spherical"],
            "diag": 1.0 / self.covariances["diag"],
            "tied": linalg.inv(self.covariances["tied"]),
                [linalg.inv(covariance) for covariance in self.covariances["full"]]
            ),
        }

        self.X = dict(
            zip(
                COVARIANCE_TYPE,
                [
                    generate_data(
                        n_samples,
                        self.weights,
                        self.means,
                        self.covariances,
                        covar_type,
                        dtype=dtype,
                    )
                    for covar_type in COVARIANCE_TYPE
                ],
            )
        )
        self.Y = np.hstack(
            [
                np.full(int(np.round(w * n_samples)), k, dtype=int)
                for k, w in enumerate(self.weights)
            ]
        )

def test_gaussian_mixture_attributes():
    # test bad parameters

    # test good parameters
    n_components, tol, n_init, max_iter, reg_covar = 2, 1e-4, 3, 30, 1e-1
    covariance_type, init_params = "full", "random"
    gmm = GaussianMixture(
        tol=tol,
        reg_covar=reg_covar,
        covariance_type=covariance_type,
        init_params=init_params,
    ).fit(X)

    assert gmm.n_components == n_components
    assert gmm.covariance_type == covariance_type
    assert gmm.tol == tol
    assert gmm.reg_covar == reg_covar
    assert gmm.max_iter == max_iter
    assert gmm.n_init == n_init
    assert gmm.init_params == init_params

def test_check_weights():

    g = GaussianMixture(n_components=n_components)

    # Check bad shape
    weights_bad_shape = rng.rand(n_components, 1)
    g.weights_init = weights_bad_shape
        "The parameter 'weights' should have the shape of "
        f"({n_components},), but got {weights_bad_shape.shape}"
    )
        g.fit(X)

    # Check bad range
    weights_bad_range = rng.rand(n_components) + 1
    g.weights_init = weights_bad_range
        "The parameter 'weights' should be in the range [0, 1], but got"
        f" max value {np.min(weights_bad_range):.5f}, "
        f"min value {np.max(weights_bad_range):.5f}"
    )
        g.fit(X)

    # Check bad normalization
    weights_bad_norm = rng.rand(n_components)
    weights_bad_norm = weights_bad_norm / (weights_bad_norm.sum() + 1)
    g.weights_init = weights_bad_norm
        "The parameter 'weights' should be normalized, "
        f"but got sum(weights) = {np.sum(weights_bad_norm):.5f}"
    )
        g.fit(X)

    # Check good weights matrix
    weights = rand_data.weights
    g = GaussianMixture(weights_init=weights, n_components=n_components)
    g.fit(X)
    assert_array_equal(weights, g.weights_init)

def test_check_means():

    n_components, n_features = rand_data.n_components, rand_data.n_features

    # Check means bad shape
    means_bad_shape = rng.rand(n_components + 1, n_features)
    g.means_init = means_bad_shape
    msg = "The parameter 'means' should have the shape of "
        g.fit(X)

    # Check good means matrix
    means = rand_data.means
    g.means_init = means
    g.fit(X)
    assert_array_equal(means, g.means_init)

def test_check_precisions():

    # Define the bad precisions for each covariance_type
    precisions_bad_shape = {
        "full": np.ones((n_components + 1, n_features, n_features)),
        "tied": np.ones((n_features + 1, n_features + 1)),
        "diag": np.ones((n_components + 1, n_features)),
        "spherical": np.ones((n_components + 1)),
    }

    # Define not positive-definite precisions
    precisions_not_pos = np.ones((n_components, n_features, n_features))
    precisions_not_pos[0] = np.eye(n_features)
    precisions_not_pos[0, 0, 0] = -1.0

    precisions_not_positive = {
        "full": precisions_not_pos,
        "tied": precisions_not_pos[0],
        "diag": np.full((n_components, n_features), -1.0),
        "spherical": np.full(n_components, -1.0),
    }

    not_positive_errors = {
        "full": "symmetric, positive-definite",
        "tied": "symmetric, positive-definite",
        "diag": "positive",
        "spherical": "positive",
    }

        X = RandomData(rng).X[covar_type]
        g = GaussianMixture(
            n_components=n_components, covariance_type=covar_type, random_state=rng
        )

        # Check precisions with bad shapes
        g.precisions_init = precisions_bad_shape[covar_type]
        msg = f"The parameter '{covar_type} precision' should have the shape of"
            g.fit(X)

        # Check not positive precisions
        g.precisions_init = precisions_not_positive[covar_type]
        msg = f"'{covar_type} precision' should be {not_positive_errors[covar_type]}"
            g.fit(X)

        # Check the correct init of precisions_init
        g.precisions_init = rand_data.precisions[covar_type]
        g.fit(X)
        assert_array_equal(rand_data.precisions[covar_type], g.precisions_init)

def test_suffstat_sk_full():
    # compare the precision matrix compute from the
    # EmpiricalCovariance.covariance fitted on X*sqrt(resp)
    # with _sufficient_sk_full, n_components=1
    n_samples, n_features = 500, 2

    # special case 1, assuming data is "centered"
    resp = rng.rand(n_samples, 1)
    X_resp = np.sqrt(resp) * X
    nk = np.array([n_samples])
    xk = np.zeros((1, n_features))
    covars_pred = _estimate_gaussian_covariances_full(resp, X, nk, xk, 0)
    ecov = EmpiricalCovariance(assume_centered=True)
    ecov.fit(X_resp)
    assert_almost_equal(ecov.error_norm(covars_pred[0], norm="frobenius"), 0)
    assert_almost_equal(ecov.error_norm(covars_pred[0], norm="spectral"), 0)

    # check the precision computation
    precs_chol_pred = _compute_precision_cholesky(covars_pred, "full")
    precs_pred = np.array([np.dot(prec, prec.T) for prec in precs_chol_pred])
    precs_est = np.array([linalg.inv(cov) for cov in covars_pred])
    assert_array_almost_equal(precs_est, precs_pred)

    # special case 2, assuming resp are all ones
    resp = np.ones((n_samples, 1))
    xk = X.mean(axis=0).reshape((1, -1))
    ecov = EmpiricalCovariance(assume_centered=False)
    ecov.fit(X)

def test_suffstat_sk_tied():
    # use equation Nk * Sk / N = S_tied
    n_samples, n_features, n_components = 500, 2, 2

    resp = rng.rand(n_samples, n_components)
    resp = resp / resp.sum(axis=1)[:, np.newaxis]
    nk = resp.sum(axis=0)
    xk = np.dot(resp.T, X) / nk[:, np.newaxis]

    covars_pred_full = _estimate_gaussian_covariances_full(resp, X, nk, xk, 0)
    covars_pred_full = (
        np.sum(nk[:, np.newaxis, np.newaxis] * covars_pred_full, 0) / n_samples
    )

    covars_pred_tied = _estimate_gaussian_covariances_tied(resp, X, nk, xk, 0)

    ecov = EmpiricalCovariance()
    ecov.covariance_ = covars_pred_full
    assert_almost_equal(ecov.error_norm(covars_pred_tied, norm="frobenius"), 0)
    assert_almost_equal(ecov.error_norm(covars_pred_tied, norm="spectral"), 0)

    precs_chol_pred = _compute_precision_cholesky(covars_pred_tied, "tied")
    precs_pred = np.dot(precs_chol_pred, precs_chol_pred.T)
    precs_est = linalg.inv(covars_pred_tied)

def test_suffstat_sk_diag():
    # test against 'full' case

    covars_pred_diag = _estimate_gaussian_covariances_diag(resp, X, nk, xk, 0)

    for cov_full, cov_diag in zip(covars_pred_full, covars_pred_diag):
        ecov.covariance_ = np.diag(np.diag(cov_full))
        cov_diag = np.diag(cov_diag)
        assert_almost_equal(ecov.error_norm(cov_diag, norm="frobenius"), 0)
        assert_almost_equal(ecov.error_norm(cov_diag, norm="spectral"), 0)

    precs_chol_pred = _compute_precision_cholesky(covars_pred_diag, "diag")
    assert_almost_equal(covars_pred_diag, 1.0 / precs_chol_pred**2)

def test_gaussian_suffstat_sk_spherical(global_dtype):
    # computing spherical covariance equals to the variance of one-dimension
    # data after flattening, n_components=1

    X = rng.rand(n_samples, n_features).astype(global_dtype)
    X = X - X.mean()
    resp = np.ones((n_samples, 1), dtype=global_dtype)
    nk = np.array([n_samples], dtype=global_dtype)
    xk = X.mean()
    covars_pred_spherical = _estimate_gaussian_covariances_spherical(resp, X, nk, xk, 0)
    covars_pred_spherical2 = np.dot(X.flatten().T, X.flatten()) / (
        n_features * n_samples
    )
    assert_almost_equal(covars_pred_spherical, covars_pred_spherical2)
    assert covars_pred_spherical.dtype == global_dtype

    precs_chol_pred = _compute_precision_cholesky(covars_pred_spherical, "spherical")
    assert_almost_equal(covars_pred_spherical, 1.0 / precs_chol_pred**2)
    assert precs_chol_pred.dtype == global_dtype

def test_compute_log_det_cholesky(global_dtype):
    rand_data = RandomData(np.random.RandomState(0), dtype=global_dtype)

        covariance = rand_data.covariances[covar_type]

            predected_det = np.array([linalg.det(cov) for cov in covariance])
            predected_det = linalg.det(covariance)
            predected_det = np.array([np.prod(cov) for cov in covariance])
        elif covar_type == "spherical":
            predected_det = covariance**n_features

        # We compute the cholesky decomposition of the covariance matrix
        assert covariance.dtype == global_dtype
        expected_det = _compute_log_det_cholesky(
            _compute_precision_cholesky(covariance, covar_type),
            covar_type,
        )
        assert_array_almost_equal(expected_det, -0.5 * np.log(predected_det))
        assert expected_det.dtype == global_dtype

def _naive_lmvnpdf_diag(X, means, covars):
    resp = np.empty((len(X), len(means)))
    stds = np.sqrt(covars)
    for i, (mean, std) in enumerate(zip(means, stds)):
        resp[:, i] = stats.norm.logpdf(X, mean, std).sum(axis=1)
    return resp

def test_gaussian_mixture_log_probabilities():
    from sklearn.mixture._gaussian_mixture import _estimate_log_gaussian_prob

    # test against with _naive_lmvnpdf_diag
    n_features = rand_data.n_features

    covars_diag = rng.rand(n_components, n_features)
    log_prob_naive = _naive_lmvnpdf_diag(X, means, covars_diag)

    # full covariances
    precs_full = np.array([np.diag(1.0 / np.sqrt(x)) for x in covars_diag])

    log_prob = _estimate_log_gaussian_prob(X, means, precs_full, "full")
    assert_array_almost_equal(log_prob, log_prob_naive)

    # diag covariances
    precs_chol_diag = 1.0 / np.sqrt(covars_diag)
    log_prob = _estimate_log_gaussian_prob(X, means, precs_chol_diag, "diag")

    # tied
    covars_tied = np.array([x for x in covars_diag]).mean(axis=0)
    precs_tied = np.diag(np.sqrt(1.0 / covars_tied))

    log_prob_naive = _naive_lmvnpdf_diag(X, means, [covars_tied] * n_components)
    log_prob = _estimate_log_gaussian_prob(X, means, precs_tied, "tied")

    # spherical
    covars_spherical = covars_diag.mean(axis=1)
    precs_spherical = 1.0 / np.sqrt(covars_diag.mean(axis=1))
    log_prob_naive = _naive_lmvnpdf_diag(
        X, means, [[k] * n_features for k in covars_spherical]
    )
    log_prob = _estimate_log_gaussian_prob(X, means, precs_spherical, "spherical")

def test_gaussian_mixture_estimate_log_prob_resp():
    # test whether responsibilities are normalized
    rand_data = RandomData(rng, scale=5)
    n_samples = rand_data.n_samples

        weights = rand_data.weights
        means = rand_data.means
        precisions = rand_data.precisions[covar_type]
            weights_init=weights,
            means_init=means,
            precisions_init=precisions,
            covariance_type=covar_type,
        )
        g.fit(X)
        resp = g.predict_proba(X)
        assert_array_almost_equal(resp.sum(axis=1), np.ones(n_samples))
        assert_array_equal(g.weights_init, weights)
        assert_array_equal(g.means_init, means)
        assert_array_equal(g.precisions_init, precisions)

def test_gaussian_mixture_predict_predict_proba():
        Y = rand_data.Y
            n_components=rand_data.n_components,
            weights_init=rand_data.weights,
            means_init=rand_data.means,
            precisions_init=rand_data.precisions[covar_type],
        )

        # Check a warning message arrive if we don't do fit
        msg = (
            "This GaussianMixture instance is not fitted yet. Call 'fit' "
            "with appropriate arguments before using this estimator."
        )
        with pytest.raises(NotFittedError, match=msg):
            g.predict(X)

        g.fit(X)
        Y_pred = g.predict(X)
        Y_pred_proba = g.predict_proba(X).argmax(axis=1)
        assert_array_equal(Y_pred, Y_pred_proba)
        assert adjusted_rand_score(Y, Y_pred) > 0.95

def test_gaussian_mixture_fit_predict(seed, max_iter, tol, global_dtype):
    rand_data = RandomData(rng, dtype=global_dtype)
            tol=tol,
        )

        # check if fit_predict(X) is equivalent to fit(X).predict(X)
        f = copy.deepcopy(g)
        Y_pred1 = f.fit(X).predict(X)
        Y_pred2 = g.fit_predict(X)
        assert adjusted_rand_score(Y, Y_pred2) > 0.95
        assert g.means_.dtype == global_dtype
        assert g.weights_.dtype == global_dtype
        assert g.precisions_.dtype == global_dtype

def test_gaussian_mixture_fit_predict_n_init():
    X = np.random.RandomState(0).randn(1000, 5)
    gm = GaussianMixture(n_components=5, n_init=5, random_state=0)

def test_gaussian_mixture_fit(global_dtype):
    # recover the ground truth

            n_init=20,
        )
        g.fit(X)

        # needs more data to pass the test with rtol=1e-7
            np.sort(g.weights_), np.sort(rand_data.weights), rtol=0.1, atol=1e-2
        )

        arg_idx1 = g.means_[:, 0].argsort()
        arg_idx2 = rand_data.means[:, 0].argsort()
            g.means_[arg_idx1], rand_data.means[arg_idx2], rtol=0.1, atol=1e-2
        )

            prec_pred = g.precisions_
            prec_test = rand_data.precisions["full"]
            prec_pred = np.array([g.precisions_] * n_components)
            prec_test = np.array([rand_data.precisions["tied"]] * n_components)
            prec_pred = np.array([np.eye(n_features) * c for c in g.precisions_])
            prec_test = np.array(
                [np.eye(n_features) * c for c in rand_data.precisions["spherical"]]
            )
            prec_pred = np.array([np.diag(d) for d in g.precisions_])
            prec_test = np.array([np.diag(d) for d in rand_data.precisions["diag"]])

        arg_idx1 = np.trace(prec_pred, axis1=1, axis2=2).argsort()
        arg_idx2 = np.trace(prec_test, axis1=1, axis2=2).argsort()
        for k, h in zip(arg_idx1, arg_idx2):
            ecov = EmpiricalCovariance()
            ecov.covariance_ = prec_test[h]
            # the accuracy depends on the number of data and randomness, rng
            assert_allclose(ecov.error_norm(prec_pred[k]), 0, atol=0.15)

        assert g.covariances_.dtype == global_dtype

def test_gaussian_mixture_fit_best_params():
    n_init = 10
            n_init=1,
        )
        ll = []
        for _ in range(n_init):
            g.fit(X)
            ll.append(g.score(X))
        ll = np.array(ll)
        g_best = GaussianMixture(
        )
        g_best.fit(X)
        assert_almost_equal(ll.min(), g_best.score(X))

def test_gaussian_mixture_fit_convergence_warning():
    rand_data = RandomData(rng, scale=1)
            n_init=1,
        )
        msg = (
            "Best performing initialization did not converge. "
            "Try different init parameters, or increase max_iter, "
            "tol, or check for degenerate data."
        )
        with pytest.warns(ConvergenceWarning, match=msg):
            g.fit(X)

def test_multiple_init():
    # Test that multiple inits does not much worse than a single one
    n_samples, n_features, n_components = 50, 5, 2
    for cv_type in COVARIANCE_TYPE:
        train1 = (
            GaussianMixture(
                n_components=n_components, covariance_type=cv_type, random_state=0
            )
            .fit(X)
            .score(X)
        )
        train2 = (
                covariance_type=cv_type,
                n_init=5,
            )
            .fit(X)
            .score(X)
        )
        assert train2 >= train1

def test_gaussian_mixture_n_parameters():
    # Test that the right number of parameters is estimated
    n_params = {"spherical": 13, "diag": 21, "tied": 26, "full": 41}
            n_components=n_components, covariance_type=cv_type, random_state=rng
        ).fit(X)
        assert g._n_parameters() == n_params[cv_type]

def test_bic_1d_1component():
    # Test all of the covariance_types return the same BIC score for
    # 1-dimensional, 1 component fits.
    n_samples, n_dim, n_components = 100, 1, 1
    X = rng.randn(n_samples, n_dim)
    bic_full = (
        GaussianMixture(
            n_components=n_components, covariance_type="full", random_state=rng
        )
        .fit(X)
        .bic(X)
    )
    for covariance_type in ["tied", "diag", "spherical"]:
        bic = (
                covariance_type=covariance_type,
            )
            .fit(X)
            .bic(X)
        )
        assert_almost_equal(bic_full, bic)

def test_gaussian_mixture_aic_bic():
    # Test the aic and bic criteria
    n_samples, n_features, n_components = 50, 3, 2
    # standard gaussian entropy
    sgh = 0.5 * (
        fast_logdet(np.cov(X.T, bias=1)) + n_features * (1 + np.log(2 * np.pi))
    )
            covariance_type=cv_type,
        )
        g.fit(X)
        aic = 2 * n_samples * sgh + 2 * g._n_parameters()
        bic = 2 * n_samples * sgh + np.log(n_samples) * g._n_parameters()
        bound = n_features / np.sqrt(n_samples)
        assert (g.aic(X) - aic) / n_samples < bound
        assert (g.bic(X) - bic) / n_samples < bound

def test_gaussian_mixture_verbose():
            n_init=1,
        )
        h = GaussianMixture(
            n_init=1,
        )
        try:
            g.fit(X)
            h.fit(X)
        finally:

def test_warm_start(seed):
    random_state = seed

    # Assert the warm_start give the same result for the same number of iter
    g = GaussianMixture(
        n_init=1,
        reg_covar=0,
    )
    h = GaussianMixture(
        n_init=1,
    )

    g.fit(X)
    score1 = h.fit(X).score(X)
    score2 = h.fit(X).score(X)

    assert_almost_equal(g.weights_, h.weights_)
    assert_almost_equal(g.means_, h.means_)
    assert_almost_equal(g.precisions_, h.precisions_)
    assert score2 > score1

    # Assert that by using warm_start we can converge to a good solution
        n_init=1,
        tol=1e-6,
    )
        n_init=1,
        tol=1e-6,
    )

    g.fit(X)
    assert not g.converged_

    h.fit(X)
    # depending on the data there is large variability in the number of
    # refit necessary to converge due to the complete randomness of the
    # data
    for _ in range(1000):
        h.fit(X)
        if h.converged_:
            break
    assert h.converged_

def test_convergence_detected_with_warm_start():
    # We check that convergence is detected when warm_start=True

    for max_iter in (1, 2, 50):
        gmm = GaussianMixture(
        )
        for _ in range(100):
            gmm.fit(X)
            if gmm.converged_:
                break
        assert gmm.converged_
        assert max_iter >= gmm.n_iter_

def test_score(global_dtype):
    covar_type = "full"
    rand_data = RandomData(rng, scale=7, dtype=global_dtype)
    X = rand_data.X[covar_type]
    assert X.dtype == global_dtype

    # Check the error message if we don't call fit
    gmm1 = GaussianMixture(
        n_init=1,
        covariance_type=covar_type,
    )
    msg = (
        "This GaussianMixture instance is not fitted yet. Call 'fit' with "
        "appropriate arguments before using this estimator."
    )
        gmm1.score(X)

    # Check score value
        gmm1.fit(X)

    assert gmm1.means_.dtype == global_dtype
    assert gmm1.covariances_.dtype == global_dtype

    gmm_score = gmm1.score(X)
    gmm_score_proba = gmm1.score_samples(X).mean()
    assert_almost_equal(gmm_score, gmm_score_proba)
    assert gmm_score_proba.dtype == global_dtype

    # Check if the score increase
    gmm2 = GaussianMixture(
        n_init=1,
    ).fit(X)
    assert gmm2.score(X) > gmm1.score(X)

        n_init=1,
    )
    msg = (
    )
        gmm.score_samples(X)

    gmm_score_samples = gmm.fit(X).score_samples(X)
    assert gmm_score_samples.shape[0] == rand_data.n_samples

    # We check that each step of the EM without regularization improve
    # monotonically the training set likelihood

            tol=1e-7,
        )
        current_log_likelihood = -np.inf
                prev_log_likelihood = current_log_likelihood
                current_log_likelihood = gmm.fit(X).score(X)
                assert current_log_likelihood >= prev_log_likelihood

                if gmm.converged_:
                    break

            assert gmm.converged_

def test_regularisation():
    # We train the GaussianMixture on degenerate data by defining two clusters
    # of a 0 covariance.

        (np.ones((n_samples // 2, n_features)), np.zeros((n_samples // 2, n_features)))
    )

            n_components=n_samples,
        )

            warnings.simplefilter("ignore", RuntimeWarning)
            msg = re.escape(
                "Fitting the mixture model failed because some components have"
                " ill-defined empirical covariance (for instance caused by "
                "singleton or collapsed samples). Try to decrease the number "
                "of components, increase reg_covar, or scale the input data."
            )
                gmm.fit(X)

            gmm.set_params(reg_covar=1e-6).fit(X)

def test_fitted_precision_covariance_concistency(covar_type, global_dtype):

        n_init=5,
    )
    gmm.fit(X)
    assert gmm.precisions_.dtype == global_dtype
    assert gmm.covariances_.dtype == global_dtype
    if covar_type == "full":
        for prec, covar in zip(gmm.precisions_, gmm.covariances_):
            assert_array_almost_equal(linalg.inv(prec), covar)
    elif covar_type == "tied":
        assert_array_almost_equal(linalg.inv(gmm.precisions_), gmm.covariances_)
    else:
        assert_array_almost_equal(gmm.precisions_, 1.0 / gmm.covariances_)

def test_sample():
    rand_data = RandomData(rng, scale=7, n_components=3)
    n_features, n_components = rand_data.n_features, rand_data.n_components

        )
        # To sample we need that GaussianMixture is fitted
        msg = "This GaussianMixture instance is not fitted"
            gmm.sample(0)
        gmm.fit(X)

        msg = "Invalid value for 'n_samples'"

        # Just to make sure the class samples correctly
        n_samples = 20000
        X_s, y_s = gmm.sample(n_samples)

            if covar_type == "full":
                    gmm.covariances_[k], np.cov(X_s[y_s == k].T), decimal=1
                )
            elif covar_type == "tied":
                    gmm.covariances_, np.cov(X_s[y_s == k].T), decimal=1
                )
            elif covar_type == "diag":
                    gmm.covariances_[k], np.diag(np.cov(X_s[y_s == k].T)), decimal=1
                )
            else:
                    gmm.covariances_[k],
                    np.var(X_s[y_s == k] - gmm.means_[k]),
                    decimal=1,
                )

        means_s = np.array([np.mean(X_s[y_s == k], 0) for k in range(n_components)])
        assert_array_almost_equal(gmm.means_, means_s, decimal=1)

        # Check shapes of sampled data, see
        # https://github.com/scikit-learn/scikit-learn/issues/7701
        assert X_s.shape == (n_samples, n_features)

        for sample_size in range(1, 100):
            X_s, _ = gmm.sample(sample_size)
            assert X_s.shape == (sample_size, n_features)

def test_init():
    # We check that by increasing the n_init number we have a better solution
    for random_state in range(15):
        rand_data = RandomData(
            np.random.RandomState(random_state), n_samples=50, scale=1
        )
        n_components = rand_data.n_components
        X = rand_data.X["full"]

        gmm1 = GaussianMixture(
            n_components=n_components, n_init=1, max_iter=1, random_state=random_state
        ).fit(X)
        gmm2 = GaussianMixture(
            n_components=n_components, n_init=10, max_iter=1, random_state=random_state
        ).fit(X)

        assert gmm2.lower_bound_ >= gmm1.lower_bound_

def test_gaussian_mixture_setting_best_params():
    """`GaussianMixture`'s best_parameters, `n_iter_` and `lower_bound_`
    must be set appropriately in the case of divergence.

    https://github.com/scikit-learn/scikit-learn/issues/18216
    """
    X = rnd.uniform(size=(n_samples, 3))

    # following initialization parameters were found to lead to divergence
    means_init = np.array(
        [
            [0.670637869618158, 0.21038256107384043, 0.12892629765485303],
            [0.09394051075844147, 0.5759464955561779, 0.929296197576212],
            [0.5033230372781258, 0.9569852381759425, 0.08654043447295741],
            [0.18578301420435747, 0.5531158970919143, 0.19388943970532435],
            [0.4548589928173794, 0.35182513658825276, 0.568146063202464],
            [0.609279894978321, 0.7929063819678847, 0.9620097270828052],
        ]
    )
    precisions_init = np.array(
        [
            999999.999604483,
            999999.9990869573,
            553.7603944542167,
            204.78596008931834,
            15.867423501783637,
            85.4595728389735,
        ]
    )
    weights_init = [
        0.03333333333333341,
        0.06666666666666674,
        0.7000000000000001,
        0.10000000000000007,
    ]

        covariance_type="spherical",
        means_init=means_init,
        weights_init=weights_init,
        random_state=rnd,
        n_components=len(weights_init),
        precisions_init=precisions_init,
    )
    # ensure that no error is thrown during fit

    # check that the fit did not converge
    assert not gmm.converged_

    # check that parameters are set for gmm
        "weights_",
        "means_",
        "covariances_",
        "precisions_cholesky_",
        "n_iter_",
        "lower_bound_",
        "lower_bounds_",
    ]:
        assert hasattr(gmm, attr)

def test_init_means_not_duplicated(init_params, global_random_seed):
    # Check that all initialisations provide not duplicated starting means

        n_components=n_components, init_params=init_params, random_state=rng, max_iter=0
    )

    means = gmm.means_
    for i_mean, j_mean in itertools.combinations(means, r=2):
        assert not np.allclose(i_mean, j_mean)

def test_means_for_all_inits(init_params, global_random_seed, global_dtype):
    # Check fitted means properties for all initializations
    rand_data = RandomData(rng, scale=5, dtype=global_dtype)

        n_components=n_components, init_params=init_params, random_state=rng
    )

    assert gmm.means_.shape == (n_components, X.shape[1])
    assert np.all(X.min(axis=0) <= gmm.means_)
    assert np.all(gmm.means_ <= X.max(axis=0))
    assert gmm.converged_
    assert gmm.means_.dtype == global_dtype
    assert gmm.weights_.dtype == global_dtype

def test_max_iter_zero():
    # Check that max_iter=0 returns initialisation as expected
    # Pick arbitrary initial means and check equal to max_iter=0
    means_init = [[20, 30], [30, 25]]
    )

    assert_allclose(gmm.means_, means_init)

def test_gaussian_mixture_precisions_init_diag(global_dtype):
    """Check that we properly initialize `precision_cholesky_` when we manually
    provide the precision matrix.

    In this regard, we check the consistency between estimating the precision
    matrix and providing the same precision matrix as initialization. It should
    lead to the same results with the same number of iterations.

    If the initialization is wrong then the number of iterations will increase.

    https://github.com/scikit-learn/scikit-learn/issues/16944
    """
    # generate a toy dataset
    shifted_gaussian = rng.randn(n_samples, 2) + np.array([20, 20])
    C = np.array([[0.0, -0.7], [3.5, 0.7]])
    stretched_gaussian = np.dot(rng.randn(n_samples, 2), C)
    X = np.vstack([shifted_gaussian, stretched_gaussian]).astype(global_dtype)

    # common parameters to check the consistency of precision initialization
    n_components, covariance_type, reg_covar, random_state = 2, "diag", 1e-6, 0

    # execute the manual initialization to compute the precision matrix:
    # - run KMeans to have an initial guess
    # - estimate the covariance
    # - compute the precision matrix from the estimated covariance
    resp = np.zeros((X.shape[0], n_components)).astype(global_dtype)
    label = (
        KMeans(n_clusters=n_components, n_init=1, random_state=random_state)
        .fit(X)
        .labels_
    )
    resp[np.arange(X.shape[0]), label] = 1
    _, _, covariance = _estimate_gaussian_parameters(
        X, resp, reg_covar=reg_covar, covariance_type=covariance_type
    )
    assert covariance.dtype == global_dtype
    precisions_init = 1 / covariance

    gm_with_init = GaussianMixture(
    ).fit(X)
    assert gm_with_init.means_.dtype == global_dtype
    assert gm_with_init.covariances_.dtype == global_dtype
    assert gm_with_init.precisions_cholesky_.dtype == global_dtype

    gm_without_init = GaussianMixture(
    ).fit(X)
    assert gm_without_init.means_.dtype == global_dtype
    assert gm_without_init.covariances_.dtype == global_dtype
    assert gm_without_init.precisions_cholesky_.dtype == global_dtype

    assert gm_without_init.n_iter_ == gm_with_init.n_iter_
        gm_with_init.precisions_cholesky_, gm_without_init.precisions_cholesky_
    )

def _generate_data(seed, n_samples, n_features, n_components, dtype=np.float64):
    """Randomly generate samples and responsibilities."""
    rs = np.random.RandomState(seed)
    X = rs.random_sample((n_samples, n_features)).astype(dtype)
    resp = rs.random_sample((n_samples, n_components)).astype(dtype)
    resp /= resp.sum(axis=1)[:, np.newaxis]
    return X, resp

def _calculate_precisions(X, resp, covariance_type):
    """Calculate precision matrix of X and its Cholesky decomposition
    for the given covariance type.
    """
    reg_covar = 1e-6
    weights, means, covariances = _estimate_gaussian_parameters(
        X, resp, reg_covar, covariance_type
    )
    precisions_cholesky = _compute_precision_cholesky(covariances, covariance_type)

    _, n_components = resp.shape
    # Instantiate a `GaussianMixture` model in order to use its
    # `_set_parameters` method to return the `precisions_` and
    #  `precisions_cholesky_` from matching the `covariance_type`
    # provided.
    gmm = GaussianMixture(n_components=n_components, covariance_type=covariance_type)
    params = (weights, means, covariances, precisions_cholesky)
    gmm._set_parameters(params)
    return gmm.precisions_, gmm.precisions_cholesky_

def test_gaussian_mixture_precisions_init(
    covariance_type, global_random_seed, global_dtype
):
    """Non-regression test for #26415."""

    X, resp = _generate_data(
    )

    precisions_init, desired_precisions_cholesky = _calculate_precisions(
        X, resp, covariance_type
    )
    assert precisions_init.dtype == global_dtype
    assert desired_precisions_cholesky.dtype == global_dtype

        covariance_type=covariance_type, precisions_init=precisions_init
    )
    gmm._initialize(X, resp)
    actual_precisions_cholesky = gmm.precisions_cholesky_
    assert_allclose(actual_precisions_cholesky, desired_precisions_cholesky)

def test_gaussian_mixture_single_component_stable():
    """
    Non-regression test for #23032 ensuring 1-component GM works on only a
    few samples.
    """
    X = rng.multivariate_normal(np.zeros(2), np.identity(2), size=3)
    gm = GaussianMixture(n_components=1)
    gm.fit(X).sample()

def test_gaussian_mixture_all_init_does_not_estimate_gaussian_parameters(
):
    """When all init parameters are provided, the Gaussian parameters
    are not estimated.

    Non-regression test for gh-26015.
    """

    mock = Mock(side_effect=_estimate_gaussian_parameters)
        sklearn.mixture._gaussian_mixture, "_estimate_gaussian_parameters", mock
    )

    gm = GaussianMixture(
        n_components=rand_data.n_components,
        weights_init=rand_data.weights,
        means_init=rand_data.means,
        precisions_init=rand_data.precisions["full"],
    )
    gm.fit(rand_data.X["full"])
    # The initial gaussian parameters are not estimated. They are estimated for every
    # m_step.
    assert mock.call_count == gm.n_iter_

        self,
        rng,
        scale=50,
    ):

                [
                ]
            ),
        }
            ),
        }

            zip(
                [
                    )
                ],
            )
        )
            [
            ]
        )

def _grid_from_X(X, percentiles, is_categorical, grid_resolution, custom_values):
    """Generate a grid of points based on the percentiles of X.

    The grid is a cartesian product between the columns of ``values``. The
    ith column of ``values`` consists in ``grid_resolution`` equally-spaced
    points between the percentiles of the jth column of X.

    If ``grid_resolution`` is bigger than the number of unique values in the
    j-th column of X or if the feature is a categorical feature (by inspecting
    `is_categorical`) , then those unique values will be used instead.

    X : array-like of shape (n_samples, n_target_features)
        The data.

    percentiles : tuple of float
        The percentiles which are used to construct the extreme values of
        the grid. Must be in [0, 1].

    is_categorical : list of bool
        For each feature, tells whether it is categorical or not. If a feature
        is categorical, then the values used will be the unique ones
        (i.e. categories) instead of the percentiles.

    grid_resolution : int
        The number of equally spaced points to be placed on the grid for each
        feature.

    custom_values: dict
        Mapping from column index of X to an array-like of values where
        the partial dependence should be calculated for that feature

    Returns
    -------
    grid : ndarray of shape (n_points, n_target_features)
        A value for each feature at each point in the grid. ``n_points`` is
        always ``<= grid_resolution ** X.shape[1]``.

    values : list of 1d ndarrays
        The values with which the grid has been created. The size of each
        array ``values[j]`` is either ``grid_resolution``, the number of
        unique values in ``X[:, j]``, if j is not in ``custom_range``.
        If j is in ``custom_range``, then it is the length of ``custom_range[j]``.
    """
    if not isinstance(percentiles, Iterable) or len(percentiles) != 2:
        raise ValueError("'percentiles' must be a sequence of 2 elements.")
    if not all(0 <= x <= 1 for x in percentiles):
        raise ValueError("'percentiles' values must be in [0, 1].")
    if percentiles[0] >= percentiles[1]:
        raise ValueError("percentiles[0] must be strictly less than percentiles[1].")

    if grid_resolution <= 1:
        raise ValueError("'grid_resolution' must be strictly greater than 1.")

    def _convert_custom_values(values):
        # Convert custom types such that object types are always used for string arrays
        dtype = object if any(isinstance(v, str) for v in values) else None
        return np.asarray(values, dtype=dtype)

    custom_values = {k: _convert_custom_values(v) for k, v in custom_values.items()}
    if any(v.ndim != 1 for v in custom_values.values()):
        error_string = ", ".join(
            f"Feature {k}: {v.ndim} dimensions"
            for k, v in custom_values.items()
            if v.ndim != 1
        )

            "The custom grid for some features is not a one-dimensional array. "
            f"{error_string}"
        )

    values = []
    # TODO: we should handle missing values (i.e. `np.nan`) specifically and store them
    # in a different Bunch attribute.
    for feature, is_cat in enumerate(is_categorical):
        if feature in custom_values:
            # Use values in the custom range
            axis = custom_values[feature]
        else:
            try:
                uniques = np.unique(_safe_indexing(X, feature, axis=1))
            except TypeError as exc:
                # `np.unique` will fail in the presence of `np.nan` and `str` categories
                # due to sorting. Temporary, we reraise an error explaining the problem.
                    f"The column #{feature} contains mixed data types. Finding unique "
                    "categories fail due to sorting. It usually means that the column "
                    "contains `np.nan` values together with `str` categories. Such use "
                    "case is not yet supported in scikit-learn."
                ) from exc

            if is_cat or uniques.shape[0] < grid_resolution:
                # Use the unique values either because:
                # - feature has low resolution use unique values
                # - feature is categorical
                axis = uniques
            else:
                # create axis based on percentiles and grid resolution
                emp_percentiles = mquantiles(
                    _safe_indexing(X, feature, axis=1), prob=percentiles, axis=0
                )
                if np.allclose(emp_percentiles[0], emp_percentiles[1]):
                        "percentiles are too close to each other, "
                        "unable to build the grid. Please choose percentiles "
                        "that are further apart."
                    )
                axis = np.linspace(
                    emp_percentiles[0],
                    emp_percentiles[1],
                    num=grid_resolution,
                    endpoint=True,
                )
        values.append(axis)

    return cartesian(values), values

def _partial_dependence_recursion(est, grid, features):
    """Calculate partial dependence via the recursion method.

    The recursion method is in particular enabled for tree-based estimators.

    For each `grid` value, a weighted tree traversal is performed: if a split node
    involves an input feature of interest, the corresponding left or right branch
    is followed; otherwise both branches are followed, each branch being weighted
    by the fraction of training samples that entered that branch. Finally, the
    partial dependence is given by a weighted average of all the visited leaves
    values.

    This method is more efficient in terms of speed than the `'brute'` method
    (:func:`~sklearn.inspection._partial_dependence._partial_dependence_brute`).
    However, here, the partial dependence computation is done explicitly with the
    `X` used during training of `est`.

    est : BaseEstimator
        A fitted estimator object implementing :term:`predict` or
        :term:`decision_function`. Multioutput-multiclass classifiers are not
        supported. Note that `'recursion'` is only supported for some tree-based
        estimators (namely
        :class:`~sklearn.ensemble.GradientBoostingClassifier`,
        :class:`~sklearn.ensemble.GradientBoostingRegressor`,
        :class:`~sklearn.ensemble.HistGradientBoostingClassifier`,
        :class:`~sklearn.ensemble.HistGradientBoostingRegressor`,
        :class:`~sklearn.tree.DecisionTreeRegressor`,
        :class:`~sklearn.ensemble.RandomForestRegressor`,
        ).

    grid : array-like of shape (n_points, n_target_features)
        The grid of feature values for which the partial dependence is calculated.
        Note that `n_points` is the number of points in the grid and `n_target_features`
        is the number of features you are doing partial dependence at.

    features : array-like of {int, str}
        The feature (e.g. `[0]`) or pair of interacting features
        (e.g. `[(0, 1)]`) for which the partial dependency should be computed.

    Returns
    -------
    averaged_predictions : array-like of shape (n_targets, n_points)
        The averaged predictions for the given `grid` of features values.
        Note that `n_targets` is the number of targets (e.g. 1 for binary
        classification, `n_tasks` for multi-output regression, and `n_classes` for
        multiclass classification) and `n_points` is the number of points in the `grid`.
    """
    averaged_predictions = est._compute_partial_dependence_recursion(grid, features)
    if averaged_predictions.ndim == 1:
        # reshape to (1, n_points) for consistency with
        # _partial_dependence_brute
        averaged_predictions = averaged_predictions.reshape(1, -1)

    return averaged_predictions

def _partial_dependence_brute(
    est, grid, features, X, response_method, sample_weight=None
):
    """Calculate partial dependence via the brute force method.

    The brute method explicitly averages the predictions of an estimator over a
    grid of feature values.

    For each `grid` value, all the samples from `X` have their variables of
    interest replaced by that specific `grid` value. The predictions are then made
    and averaged across the samples.

    This method is slower than the `'recursion'`
    (:func:`~sklearn.inspection._partial_dependence._partial_dependence_recursion`)
    version for estimators with this second option. However, with the `'brute'`
    force method, the average will be done with the given `X` and not the `X`
    used during training, as it is done in the `'recursion'` version. Therefore
    the average can always accept `sample_weight` (even when the estimator was
    fitted without).

        A fitted estimator object implementing :term:`predict`,
        :term:`predict_proba`, or :term:`decision_function`.
        Multioutput-multiclass classifiers are not supported.

        `X` is used to generate values for the complement features. That is, for
        each value in `grid`, the method will average the prediction of each
        sample from `X` having that grid value for `features`.

    response_method : {'auto', 'predict_proba', 'decision_function'}, \
        Specifies whether to use :term:`predict_proba` or
        :term:`decision_function` as the target response. For regressors
        this parameter is ignored and the response is always the output of
        :term:`predict`. By default, :term:`predict_proba` is tried first
        and we revert to :term:`decision_function` if it doesn't exist.

        Sample weights are used to calculate weighted means when averaging the
        model output. If `None`, then samples are equally weighted. Note that
        `sample_weight` does not change the individual predictions.

    Returns
    -------

    predictions : array-like
        The predictions for the given `grid` of features values over the samples
        from `X`. For non-multioutput regression and binary classification the
        shape is `(n_instances, n_points)` and for multi-output regression and
        multiclass classification the shape is `(n_targets, n_instances, n_points)`,
        where `n_targets` is the number of targets (`n_tasks` for multi-output
        regression, and `n_classes` for multiclass classification), `n_instances`
        is the number of instances in `X`, and `n_points` is the number of points
        in the `grid`.
    """
    predictions = []
    averaged_predictions = []

    if response_method == "auto":
        response_method = (
            "predict" if is_regressor(est) else ["predict_proba", "decision_function"]
        )

    X_eval = X.copy()
    for new_values in grid:
        for i, variable in enumerate(features):
            _safe_assign(X_eval, new_values[i], column_indexer=variable)

        # Note: predictions is of shape
        # (n_points,) for non-multioutput regressors
        # (n_points, n_tasks) for multioutput regressors
        # (n_points, 1) for the regressors in cross_decomposition (I think)
        # (n_points, 1) for binary classification (positive class already selected)
        # (n_points, n_classes) for multiclass classification
        pred, _ = _get_response_values(est, X_eval, response_method=response_method)

        predictions.append(pred)
        # average over samples
        averaged_predictions.append(np.average(pred, axis=0, weights=sample_weight))

    # reshape to (n_targets, n_instances, n_points) where n_targets is:
    # - 1 for non-multioutput regression and binary classification (shape is
    #   already correct in those cases)
    # - n_tasks for multi-output regression
    # - n_classes for multiclass classification.
    predictions = np.array(predictions).T
    if is_regressor(est) and predictions.ndim == 2:
        # non-multioutput regression, shape is (n_instances, n_points,)
        predictions = predictions.reshape(n_samples, -1)
    elif is_classifier(est) and predictions.shape[0] == 2:
        # Binary classification, shape is (2, n_instances, n_points).
        # we output the effect of **positive** class
        predictions = predictions[1]

    # reshape averaged_predictions to (n_targets, n_points) where n_targets is:
    averaged_predictions = np.array(averaged_predictions).T
        # _partial_dependence_recursion

    return averaged_predictions, predictions

def partial_dependence(
    X,
    features,
    *,
    categorical_features=None,
    response_method="auto",
    percentiles=(0.05, 0.95),
    grid_resolution=100,
    custom_values=None,
    method="auto",
    kind="average",
):
    """Partial dependence of ``features``.

    Partial dependence of a feature (or a set of features) corresponds to
    the average response of an estimator for each possible value of the
    feature.

    Read more in
    :ref:`sphx_glr_auto_examples_inspection_plot_partial_dependence.py`
    and the :ref:`User Guide <partial_dependence>`.

        For :class:`~sklearn.ensemble.GradientBoostingClassifier` and
        :class:`~sklearn.ensemble.GradientBoostingRegressor`, the
        `'recursion'` method (used by default) will not account for the `init`
        predictor of the boosting process. In practice, this will produce
        the same values as `'brute'` up to a constant offset in the target
        response, provided that `init` is a constant estimator (which is the
        default). However, if `init` is not a constant estimator, the
        partial dependence values are incorrect for `'recursion'` because the
        offset will be sample-dependent. It is preferable to use the `'brute'`
        method. Note that this only applies to
        :class:`~sklearn.ensemble.GradientBoostingClassifier` and
        :class:`~sklearn.ensemble.GradientBoostingRegressor`, not to
        :class:`~sklearn.ensemble.HistGradientBoostingClassifier` and
        :class:`~sklearn.ensemble.HistGradientBoostingRegressor`.

    estimator : BaseEstimator

    X : {array-like, sparse matrix or dataframe} of shape (n_samples, n_features)
        ``X`` is used to generate a grid of values for the target
        ``features`` (where the partial dependence will be evaluated), and
        also to generate values for the complement features when the
        `method` is 'brute'.

    features : array-like of {int, str, bool} or int or str

        model output. If `None`, then samples are equally weighted. If
        `sample_weight` is not `None`, then `method` will be set to `'brute'`.
        Note that `sample_weight` is ignored for `kind='individual'`.

    categorical_features : array-like of shape (n_features,) or shape \
            (n_categorical_features,), dtype={bool, int, str}, default=None

        - `None`: no feature will be considered categorical;
        - boolean array-like: boolean mask of shape `(n_features,)`
            indicating which features are categorical. Thus, this array has
            the same shape has `X.shape[1]`;
        - integer or string array-like: integer indices or strings
            indicating categorical features.

    feature_names : array-like of shape (n_features,), dtype=str, default=None
        Name of each feature; `feature_names[i]` holds the name of the feature
        with index `i`.
        By default, the name of the feature corresponds to their numerical
        index for NumPy array and their column name for pandas dataframe.

        and we revert to :term:`decision_function` if it doesn't exist. If
        ``method`` is 'recursion', the response is always the output of
        :term:`decision_function`.

    percentiles : tuple of float, default=(0.05, 0.95)
        The lower and upper percentile used to create the extreme values
        for the grid. Must be in [0, 1].
        This parameter is overridden by `custom_values` if that parameter is set.

    grid_resolution : int, default=100
        The number of equally spaced points on the grid, for each target
        feature.

    custom_values : dict
        A dictionary mapping the index of an element of `features` to an array
        of values where the partial dependence should be calculated
        for that feature. Setting a range of values for a feature overrides
        `grid_resolution` and `percentiles`.

        See :ref:`how to use partial_dependence
        <plt_partial_dependence_custom_values>` for an example of how this parameter can
        be used.

    method : {'auto', 'recursion', 'brute'}, default='auto'
        The method used to calculate the averaged predictions:

        - `'recursion'` is only supported for some tree-based estimators
          (namely
          :class:`~sklearn.ensemble.GradientBoostingClassifier`,
          :class:`~sklearn.ensemble.GradientBoostingRegressor`,
          :class:`~sklearn.ensemble.HistGradientBoostingClassifier`,
          :class:`~sklearn.ensemble.HistGradientBoostingRegressor`,
          :class:`~sklearn.tree.DecisionTreeRegressor`,
          :class:`~sklearn.ensemble.RandomForestRegressor`,
          ) when `kind='average'`.
          This is more efficient in terms of speed.
          With this method, the target response of a
          classifier is always the decision function, not the predicted
          probabilities. Since the `'recursion'` method implicitly computes
          the average of the Individual Conditional Expectation (ICE) by
          design, it is not compatible with ICE and thus `kind` must be
          `'average'`.

        - `'brute'` is supported for any estimator, but is more
          computationally intensive.

        - `'auto'`: the `'recursion'` is used for estimators that support it,
          and `'brute'` is used otherwise. If `sample_weight` is not `None`,
          then `'brute'` is used regardless of the estimator.

        Please see :ref:`this note <pdp_method_differences>` for
        differences between the `'brute'` and `'recursion'` method.

    kind : {'average', 'individual', 'both'}, default='average'
        Whether to return the partial dependence averaged across all the
        samples in the dataset or one value per sample or both.
        See Returns below.

        Note that the fast `method='recursion'` option is only available for
        `kind='average'` and `sample_weights=None`. Computing individual
        dependencies and doing weighted averages requires using the slower
        `method='brute'`.

    Returns
    -------
    predictions : :class:`~sklearn.utils.Bunch`

        individual : ndarray of shape (n_outputs, n_instances, \
                len(values[0]), len(values[1]), ...)
            The predictions for all the points in the grid for all
            samples in X. This is also known as Individual
            Conditional Expectation (ICE).
            Only available when `kind='individual'` or `kind='both'`.

        average : ndarray of shape (n_outputs, len(values[0]), \
                len(values[1]), ...)
            The predictions for all the points in the grid, averaged
            over all samples in X (or over the training data if
            `method` is 'recursion').
            Only available when `kind='average'` or `kind='both'`.

        grid_values : seq of 1d ndarrays
            The values with which the grid has been created. The generated
            grid is a cartesian product of the arrays in `grid_values` where
            `len(grid_values) == len(features)`. The size of each array
            `grid_values[j]` is either `grid_resolution`, or the number of
            unique values in `X[:, j]`, whichever is smaller.

        `n_outputs` corresponds to the number of classes in a multi-class
        setting, or to the number of tasks for multi-output regression.
        For classical regression and binary classification `n_outputs==1`.
        `n_values_feature_j` corresponds to the size `grid_values[j]`.

    See Also
    --------
    PartialDependenceDisplay.from_estimator : Plot Partial Dependence.
    PartialDependenceDisplay : Partial Dependence visualization.

    Examples
    --------
    >>> X = [[0, 0, 2], [1, 0, 0]]
    >>> y = [0, 1]
    >>> gb = GradientBoostingClassifier(random_state=0).fit(X, y)
    >>> partial_dependence(gb, features=[0], X=X, percentiles=(0, 1),
    ...                    grid_resolution=2) # doctest: +SKIP
    (array([[-4.52...,  4.52...]]), [array([ 0.,  1.])])
    """
    check_is_fitted(estimator)

    if not (is_classifier(estimator) or is_regressor(estimator)):
        raise ValueError("'estimator' must be a fitted regressor or classifier.")

    if is_classifier(estimator) and isinstance(estimator.classes_[0], np.ndarray):
        raise ValueError("Multiclass-multioutput estimators are not supported")

    # Use check_array only on lists and other non-array-likes / sparse. Do not
    # convert DataFrame into a NumPy array.
    if not (hasattr(X, "__array__") or sparse.issparse(X)):
        X = check_array(X, ensure_all_finite="allow-nan", dtype=object)

    if is_regressor(estimator) and response_method != "auto":
            "The response_method parameter is ignored for regressors and "
            "must be 'auto'."
        )

    if kind != "average":
        if method == "recursion":
                "The 'recursion' method only applies when 'kind' is set to 'average'"
            )
        method = "brute"

    if method == "recursion" and sample_weight is not None:
            "The 'recursion' method can only be applied when sample_weight is None."
        )

    if method == "auto":
            method = "brute"
        elif isinstance(estimator, BaseGradientBoosting) and estimator.init is None:
            method = "recursion"
        elif isinstance(
            (BaseHistGradientBoosting, DecisionTreeRegressor, RandomForestRegressor),
        ):
        else:

    if method == "recursion":
        if not isinstance(
            (
                BaseGradientBoosting,
                BaseHistGradientBoosting,
                DecisionTreeRegressor,
                RandomForestRegressor,
            ),
        ):
            supported_classes_recursion = (
                "GradientBoostingClassifier",
                "GradientBoostingRegressor",
                "HistGradientBoostingClassifier",
                "HistGradientBoostingRegressor",
                "DecisionTreeRegressor",
                "RandomForestRegressor",
            )
                "Only the following estimators support the 'recursion' "
                "method: {}. Try using method='brute'.".format(
                    ", ".join(supported_classes_recursion)
                )
            )
        if response_method == "auto":
            response_method = "decision_function"

        if response_method != "decision_function":
                "With the 'recursion' method, the response_method must be "
                "'decision_function'. Got {}.".format(response_method)
            )

    if _determine_key_type(features, accept_slice=False) == "int":
        # _get_column_indices() supports negative indexing. Here, we limit
        # the indexing to be positive. The upper bound will be checked
        # by _get_column_indices()
        if np.any(np.less(features, 0)):
            raise ValueError("all features must be in [0, {}]".format(X.shape[1] - 1))

    features_indices = np.asarray(
        _get_column_indices(X, features), dtype=np.intp, order="C"
    ).ravel()

    feature_names = _check_feature_names(X, feature_names)

    if categorical_features is None:
        is_categorical = [False] * len(features_indices)
    else:
                "Passing an empty list (`[]`) to `categorical_features` is not "
                "supported. Use `None` instead to indicate that there are no "
                "categorical features."
            )
        if categorical_features.dtype.kind == "b":
            # categorical features provided as a list of boolean
            if categorical_features.size != n_features:
                    "When `categorical_features` is a boolean array-like, "
                    "the array should be of shape (n_features,). Got "
                    f"{categorical_features.size} elements while `X` contains "
                    f"{n_features} features."
                )
            is_categorical = [categorical_features[idx] for idx in features_indices]
        elif categorical_features.dtype.kind in ("i", "O", "U"):
            # categorical features provided as a list of indices or feature names
            categorical_features_idx = [
                _get_feature_index(cat, feature_names=feature_names)
                for cat in categorical_features
            ]
            is_categorical = [
                idx in categorical_features_idx for idx in features_indices
            ]
        else:
                "Expected `categorical_features` to be an array-like of boolean,"
                f" integer, or string. Got {categorical_features.dtype} instead."
            )

    custom_values = custom_values or {}
    if isinstance(features, (str, int)):
        features = [features]

    for feature_idx, feature, is_cat in zip(features_indices, features, is_categorical):
        if is_cat:
            continue

        if _safe_indexing(X, feature_idx, axis=1).dtype.kind in "iu":
            # TODO(1.9): raise a ValueError instead.
                f"The column {feature!r} contains integer data. Partial "
                "dependence plots are not supported for integer data: this "
                "can lead to implicit rounding with NumPy arrays or even errors "
                "with newer pandas versions. Please convert numerical features"
                "to floating point dtypes ahead of time to avoid problems. "
                "This will raise ValueError in scikit-learn 1.9.",
            )
            # Do not warn again for other features to avoid spamming the caller.
            break

    X_subset = _safe_indexing(X, features_indices, axis=1)

    custom_values_for_X_subset = {
        index: custom_values.get(feature)
        for index, feature in enumerate(features)
        if feature in custom_values
    }

    grid, values = _grid_from_X(
        X_subset,
        percentiles,
        grid_resolution,
        custom_values_for_X_subset,
    )

    if method == "brute":
        averaged_predictions, predictions = _partial_dependence_brute(
            estimator, grid, features_indices, X, response_method, sample_weight
        )

        # reshape predictions to
        # (n_outputs, n_instances, n_values_feature_0, n_values_feature_1, ...)
        predictions = predictions.reshape(
            -1, X.shape[0], *[val.shape[0] for val in values]
        )
    else:
        averaged_predictions = _partial_dependence_recursion(
            estimator, grid, features_indices
        )

    # reshape averaged_predictions to
    # (n_outputs, n_values_feature_0, n_values_feature_1, ...)
    averaged_predictions = averaged_predictions.reshape(
        -1, *[val.shape[0] for val in values]
    )
    pdp_results = Bunch(grid_values=values)

    if kind == "average":
        pdp_results["average"] = averaged_predictions
    elif kind == "individual":
        pdp_results["individual"] = predictions
    else:  # kind='both'

    return pdp_results

def _weights_scorer(scorer, estimator, X, y, sample_weight):
        return scorer(estimator, X, y, sample_weight=sample_weight)
    return scorer(estimator, X, y)

def _calculate_permutation_scores(
    X,
    y,
    col_idx,
    n_repeats,
    scorer,
):
    """Calculate score when `col_idx` is permuted."""

    # Work on a copy of X to ensure thread-safety in case of threading based
    # parallelism. Furthermore, making a copy is also useful when the joblib
    # backend is 'loky' (default) or the old 'multiprocessing': in those cases,
    # if X is large it will be automatically be backed by a readonly memory map
    # (memmap). X.copy() on the other hand is always guaranteed to return a
    # writable data-structure whose columns can be shuffled inplace.
    if max_samples < X.shape[0]:
        row_indices = _generate_indices(
            n_population=X.shape[0],
            n_samples=max_samples,
        )
        X_permuted = _safe_indexing(X, row_indices, axis=0)
        y = _safe_indexing(y, row_indices, axis=0)
            sample_weight = _safe_indexing(sample_weight, row_indices, axis=0)
    else:
        X_permuted = X.copy()

    scores = []
    shuffling_idx = np.arange(X_permuted.shape[0])
    for _ in range(n_repeats):
        random_state.shuffle(shuffling_idx)
        if hasattr(X_permuted, "iloc"):
            col = X_permuted.iloc[shuffling_idx, col_idx]
            col.index = X_permuted.index
            X_permuted[X_permuted.columns[col_idx]] = col
        else:
            X_permuted[:, col_idx] = X_permuted[shuffling_idx, col_idx]
        scores.append(_weights_scorer(scorer, estimator, X_permuted, y, sample_weight))

    if isinstance(scores[0], dict):
        scores = _aggregate_score_dicts(scores)
    else:
        scores = np.array(scores)

def _create_importances_bunch(baseline_score, permuted_score):
    """Compute the importances as the decrease in score.

    baseline_score : ndarray of shape (n_features,)
        The baseline score without permutation.
    permuted_score : ndarray of shape (n_features, n_repeats)
        The permuted scores for the `n` repetitions.

    Returns
    -------
    importances : :class:`~sklearn.utils.Bunch`
        importances_mean : ndarray, shape (n_features, )
            Mean of feature importance over `n_repeats`.
        importances_std : ndarray, shape (n_features, )
            Standard deviation over `n_repeats`.
        importances : ndarray, shape (n_features, n_repeats)
            Raw permutation importance scores.
    """
    importances = baseline_score - permuted_score
        importances_mean=np.mean(importances, axis=1),
        importances_std=np.std(importances, axis=1),
        importances=importances,
    )

def permutation_importance(
    X,
    y,
    *,
    scoring=None,
    n_repeats=5,
    max_samples=1.0,
):
    """Permutation importance for feature evaluation [BRE]_.

    The :term:`estimator` is required to be a fitted estimator. `X` can be the
    data set used to train the estimator or a hold-out set. The permutation
    importance of a feature is calculated as follows. First, a baseline metric,
    defined by :term:`scoring`, is evaluated on a (potentially different)
    dataset defined by the `X`. Next, a feature column from the validation set
    is permuted and the metric is evaluated again. The permutation importance
    is defined to be the difference between the baseline metric and metric from
    permutating the feature column.

    Read more in the :ref:`User Guide <permutation_importance>`.

        An estimator that has already been :term:`fitted` and is compatible
        with :term:`scorer`.

    X : ndarray or DataFrame, shape (n_samples, n_features)
        Data on which permutation importance will be computed.

    y : array-like or None, shape (n_samples, ) or (n_samples, n_classes)
        Targets for supervised or `None` for unsupervised.

    scoring : str, callable, list, tuple, or dict, default=None
        Scorer to use.
        If `scoring` represents a single score, one can use:

        - `None`: the `estimator`'s

        - a callable returning a dictionary where the keys are the metric

        Passing multiple scores to `scoring` is more efficient than calling
        `permutation_importance` for each of the scores as it reuses
        predictions to avoid redundant computation.

    n_repeats : int, default=5
        Number of times to permute a feature.

        Number of jobs to run in parallel. The computation is done by computing
        permutation score for each columns and parallelized over the columns.

        Pseudo-random number generator to control the permutations of each
        feature.
        Pass an int to get reproducible results across function calls.

        Sample weights used in scoring.

        The number of samples to draw from X to compute feature importance
        in each repeat (without replacement).

        - If `max_samples` is equal to `1.0` or `X.shape[0]`, all samples
          will be used.

        While using this option may provide less accurate importance estimates,
        it keeps the method tractable when evaluating feature importance on
        large datasets. In combination with `n_repeats`, this allows to control
        the computational speed vs statistical accuracy trade-off of this method.

    Returns
    -------
    result : :class:`~sklearn.utils.Bunch` or dict of such instances

        importances_mean : ndarray of shape (n_features, )
        importances_std : ndarray of shape (n_features, )
        importances : ndarray of shape (n_features, n_repeats)

        If there are multiple scoring metrics in the scoring parameter
        `result` is a dict with scorer names as keys (e.g. 'roc_auc') and
        `Bunch` objects like above as values.

    .. [BRE] :doi:`L. Breiman, "Random Forests", Machine Learning, 45(1), 5-32,
             2001. <10.1023/A:1010933404324>`

    Examples
    --------
    >>> from sklearn.inspection import permutation_importance
    >>> X = [[1, 9, 9],[1, 9, 9],[1, 9, 9],
    ...      [0, 9, 9],[0, 9, 9],[0, 9, 9]]
    >>> y = [1, 1, 1, 0, 0, 0]
    >>> clf = LogisticRegression().fit(X, y)
    >>> result = permutation_importance(clf, X, y, n_repeats=10,
    ...                                 random_state=0)
    >>> result.importances_mean
    array([0.4666..., 0.       , 0.       ])
    >>> result.importances_std
    array([0.2211..., 0.       , 0.       ])
    """
    if not hasattr(X, "iloc"):
        X = check_array(X, ensure_all_finite="allow-nan", dtype=None)

    # Precompute random seed from the random state to be used
    # to get a fresh independent RandomState instance for each
    # parallel call to _calculate_permutation_scores, irrespective of
    # the fact that variables are shared or not depending on the active
    # joblib backend (sequential, thread-based or process-based).
    random_seed = random_state.randint(np.iinfo(np.int32).max + 1)

    if not isinstance(max_samples, numbers.Integral):
        max_samples = int(max_samples * X.shape[0])
    elif max_samples > X.shape[0]:
        raise ValueError("max_samples must be <= n_samples")

    scorer = check_scoring(estimator, scoring=scoring)
    baseline_score = _weights_scorer(scorer, estimator, X, y, sample_weight)

    scores = Parallel(n_jobs=n_jobs)(
        delayed(_calculate_permutation_scores)(
            X,
            y,
            col_idx,
            n_repeats,
            scorer,
        )
        for col_idx in range(X.shape[1])
    )

    if isinstance(baseline_score, dict):
        return {
            name: _create_importances_bunch(
                baseline_score[name],
                # unpack the permuted scores
                np.array([scores[col_idx][name] for col_idx in range(X.shape[1])]),
            )
            for name in baseline_score
        }
    else:
        return _create_importances_bunch(baseline_score, np.array(scores))

def _check_feature_names(X, feature_names=None):
    """Check feature names.

    feature_names : None or array-like of shape (n_names,), dtype=str
        Feature names to check or `None`.

    Returns
    -------
        Feature names validated. If `feature_names` is `None`, then a list of
        feature names is provided, i.e. the column names of a pandas dataframe
        or a generic list of feature names (e.g. `["x0", "x1", ...]`) for a
        NumPy array.
    """
    if feature_names is None:
        if hasattr(X, "columns") and hasattr(X.columns, "tolist"):
            # get the column names for a pandas dataframe
            feature_names = X.columns.tolist()
        else:
            # define a list of numbered indices for a numpy array
            feature_names = [f"x{i}" for i in range(X.shape[1])]
    elif hasattr(feature_names, "tolist"):
        # convert numpy array or pandas index to a list
        feature_names = feature_names.tolist()
    if len(set(feature_names)) != len(feature_names):
        raise ValueError("feature_names should not contain duplicates.")

    return feature_names

def _get_feature_index(fx, feature_names=None):
    """Get feature index.

    fx : int or str
        Feature index or name.

    feature_names : list of str, default=None
        All feature names from which to search the indices.

    Returns
    -------
    idx : int
        Feature index.
    """
    if isinstance(fx, str):
        if feature_names is None:
                f"Cannot plot partial dependence for feature {fx!r} since "
                "the list of feature names was not provided, neither as "
                "column names of a pandas data-frame nor via the feature_names "
                "parameter."
            )
        try:
            return feature_names.index(fx)
            raise ValueError(f"Feature {fx!r} not in feature_names") from e
    return fx

def test_check_feature_names(feature_names, array_type, expected_feature_names):
    X = np.random.randn(10, 3)
    column_names = ["a", "b", "c"]
    X = _convert_container(X, constructor_name=array_type, columns_name=column_names)
    feature_names_validated = _check_feature_names(X, feature_names)
    assert feature_names_validated == expected_feature_names

def test_check_feature_names_error():
    feature_names = ["a", "b", "c", "a"]
    msg = "feature_names should not contain duplicates."
        _check_feature_names(X, feature_names)

def test_get_feature_index(fx, idx):
    feature_names = ["a", "b", "c"]
    assert _get_feature_index(fx, feature_names) == idx

def test_get_feature_names_error(fx, feature_names, err_msg):
        _get_feature_index(fx, feature_names)

def test_permutation_importance_correlated_feature_regression(
    n_jobs, max_samples, sample_weight
):
    # Make sure that feature highly correlated to the target have a higher
    # importance
    n_repeats = 5

    y_with_little_noise = (y + rng.normal(scale=0.001, size=y.shape[0])).reshape(-1, 1)

    X = np.hstack([X, y_with_little_noise])

    weights = np.ones_like(y) if sample_weight == "ones" else sample_weight
    clf = RandomForestRegressor(n_estimators=10, random_state=42)

    result = permutation_importance(
        clf,
        X,
        y,
        sample_weight=weights,
        n_repeats=n_repeats,
    )

    assert result.importances.shape == (X.shape[1], n_repeats)

    # the correlated feature with y was added as the last column and should
    # have the highest importance
    assert np.all(result.importances_mean[-1] > result.importances_mean[:-1])

def test_permutation_importance_correlated_feature_regression_pandas(
    n_jobs, max_samples
):

    dataset = load_iris()

    # Adds feature correlated with y as the last column
    X = pd.DataFrame(X, columns=dataset.feature_names)
    X["correlated_feature"] = y_with_little_noise

    clf = RandomForestClassifier(n_estimators=10, random_state=42)

        clf,
        X,
        y,
    )

def test_robustness_to_high_cardinality_noisy_feature(n_jobs, max_samples, seed=42):
    # Permutation variable importance should not be affected by the high
    # cardinality bias of traditional feature importances, especially when
    # computed on a held-out test set:
    n_informative_features = 2
    n_noise_features = 1
    n_features = n_informative_features + n_noise_features

    # Generate a multiclass classification dataset and a set of informative
    # binary features that can be used to predict some classes of y exactly
    # while leaving some classes unexplained to make the problem harder.
    classes = np.arange(n_classes)
    y = rng.choice(classes, size=n_samples)
    X = np.hstack([(y == c).reshape(-1, 1) for c in classes[:n_informative_features]])
    X = X.astype(np.float32)

    # Not all target classes are explained by the binary class indicator
    # features:
    assert n_informative_features < n_classes

    # Add 10 other noisy features with high cardinality (numerical) values
    # that can be used to overfit the training data.
    X = np.concatenate([X, rng.randn(n_samples, n_noise_features)], axis=1)

    # Split the dataset to be able to evaluate on a held-out test set. The
    # Test size should be large enough for importance measurements to be
    # stable:
        X, y, test_size=0.5, random_state=rng
    )
    clf = RandomForestClassifier(n_estimators=5, random_state=rng)

    # Variable importances computed by impurity decrease on the tree node
    # splits often use the noisy features in splits. This can give misleading
    # impression that high cardinality noisy variables are the most important:
    tree_importances = clf.feature_importances_
    informative_tree_importances = tree_importances[:n_informative_features]
    noisy_tree_importances = tree_importances[n_informative_features:]
    assert informative_tree_importances.max() < noisy_tree_importances.min()

    # Let's check that permutation-based feature importances do not have this
    # problem.
    r = permutation_importance(
        clf,
        X_test,
        y_test,
    )

    assert r.importances.shape == (X.shape[1], n_repeats)

    # Split the importances between informative and noisy features
    informative_importances = r.importances_mean[:n_informative_features]
    noisy_importances = r.importances_mean[n_informative_features:]

    # Because we do not have a binary variable explaining each target classes,
    # the RF model will have to use the random variable to make some
    # (overfitting) splits (as max_depth is not set). Therefore the noisy
    # variables will be non-zero but with small values oscillating around
    # zero:
    assert max(np.abs(noisy_importances)) > 1e-7
    assert noisy_importances.max() < 0.05

    # The binary features correlated with y should have a higher importance
    # than the high cardinality noisy features.
    # The maximum test accuracy is 2 / 5 == 0.4, each informative feature
    # contributing approximately a bit more than 0.2 of accuracy.
    assert informative_importances.min() > 0.15

def test_permutation_importance_mixed_types():
    n_repeats = 4

    # Last column is correlated with y
    X = np.array([[1.0, 2.0, 3.0, np.nan], [2, 1, 2, 1]]).T
    y = np.array([0, 1, 0, 1])

    clf = make_pipeline(SimpleImputer(), LogisticRegression(solver="lbfgs"))
    result = permutation_importance(clf, X, y, n_repeats=n_repeats, random_state=rng)

    # the correlated feature with y is the last column and should

    # use another random state
    result2 = permutation_importance(clf, X, y, n_repeats=n_repeats, random_state=rng)
    assert result2.importances.shape == (X.shape[1], n_repeats)

    assert not np.allclose(result.importances, result2.importances)

    assert np.all(result2.importances_mean[-1] > result2.importances_mean[:-1])

def test_permutation_importance_mixed_types_pandas():

    X = pd.DataFrame({"col1": [1.0, 2.0, 3.0, np.nan], "col2": ["a", "b", "a", "b"]})

    num_preprocess = make_pipeline(SimpleImputer(), StandardScaler())
    preprocess = ColumnTransformer(
        [("num", num_preprocess, ["col1"]), ("cat", OneHotEncoder(), ["col2"])]
    )
    clf = make_pipeline(preprocess, LogisticRegression(solver="lbfgs"))

def test_permutation_importance_linear_regresssion():
    X, y = make_regression(n_samples=500, n_features=10, random_state=0)

    y = scale(y)

    # this relationship can be computed in closed form
    expected_importances = 2 * lr.coef_**2
    results = permutation_importance(
        lr, X, y, n_repeats=50, scoring="neg_mean_squared_error"
    )
        expected_importances, results.importances_mean, rtol=1e-1, atol=1e-6
    )

def test_permutation_importance_equivalence_sequential_parallel(max_samples):
    # regression test to make sure that sequential and parallel calls will
    # output the same results.
    # Also tests that max_samples equal to number of samples is equivalent to 1.0

    importance_sequential = permutation_importance(
        lr, X, y, n_repeats=5, random_state=0, n_jobs=1, max_samples=max_samples
    )

    # First check that the problem is structured enough and that the model is
    # complex enough to not yield trivial, constant importances:
    imp_min = importance_sequential["importances"].min()
    imp_max = importance_sequential["importances"].max()
    assert imp_max - imp_min > 0.3

    # The actually check that parallelism does not impact the results
    # either with shared memory (threading) or without isolated memory
    # via process-based parallelism using the default backend
    # ('loky' or 'multiprocessing') depending on the joblib version:

    # process-based parallelism (by default):
    importance_processes = permutation_importance(
        lr, X, y, n_repeats=5, random_state=0, n_jobs=2
    )
        importance_processes["importances"], importance_sequential["importances"]
    )

    # thread-based parallelism:
    with parallel_backend("threading"):
        importance_threading = permutation_importance(
            lr, X, y, n_repeats=5, random_state=0, n_jobs=2
        )
        importance_threading["importances"], importance_sequential["importances"]
    )

def test_permutation_importance_equivalence_array_dataframe(n_jobs, max_samples):
    # This test checks that the column shuffling logic has the same behavior
    # both a dataframe and a simple numpy array.

    X, y = make_regression(n_samples=100, n_features=5, random_state=0)
    X_df = pd.DataFrame(X)

    # Add a categorical feature that is statistically linked to y:
    binner = KBinsDiscretizer(
        n_bins=3,
        encode="ordinal",
        quantile_method="averaged_inverted_cdf",
    )
    cat_column = binner.fit_transform(y.reshape(-1, 1))

    # Concatenate the extra column to the numpy array: integers will be
    # cast to float values
    X = np.hstack([X, cat_column])
    assert X.dtype.kind == "f"

    # Insert extra column as a non-numpy-native dtype:
    cat_column = pd.Categorical(cat_column.ravel())
    new_col_idx = len(X_df.columns)
    X_df[new_col_idx] = cat_column
    assert X_df[new_col_idx].dtype == cat_column.dtype

    # Stich an arbitrary index to the dataframe:
    X_df.index = np.arange(len(X_df)).astype(str)

    rf = RandomForestRegressor(n_estimators=5, max_depth=3, random_state=0)
    rf.fit(X, y)

    n_repeats = 3
    importance_array = permutation_importance(
        rf,
        X,
        y,
    )

    imp_min = importance_array["importances"].min()
    imp_max = importance_array["importances"].max()

    # Now check that importances computed on dataframe matche the values
    # of those computed on the array with the same data.
    importance_dataframe = permutation_importance(
        rf,
        X_df,
        y,
    )
        importance_array["importances"], importance_dataframe["importances"]
    )

def test_permutation_importance_large_memmaped_data(input_type):
    # Smoke, non-regression test for:
    # https://github.com/scikit-learn/scikit-learn/issues/15810
    n_samples, n_features = int(5e4), 4
        n_samples=n_samples, n_features=n_features, random_state=0
    )
    assert X.nbytes > 1e6  # trigger joblib memmaping

    X = _convert_container(X, input_type)
    clf = DummyClassifier(strategy="prior").fit(X, y)

    # Actual smoke test: should not raise any error:
    r = permutation_importance(clf, X, y, n_repeats=n_repeats, n_jobs=2)

    # Auxiliary check: DummyClassifier is feature independent:
    # permutating feature should not change the predictions
    expected_importances = np.zeros((n_features, n_repeats))
    assert_allclose(expected_importances, r.importances)

def test_permutation_importance_sample_weight():
    # Creating data with 2 features and 1000 samples, where the target
    # variable is a linear combination of the two features, such that
    # in half of the samples the impact of feature 1 is twice the impact of
    # feature 2, and vice versa on the other half of the samples.
    n_half_samples = n_samples // 2
    x = rng.normal(0.0, 0.001, (n_samples, n_features))
    y[:n_half_samples] = 2 * x[:n_half_samples, 0] + x[:n_half_samples, 1]
    y[n_half_samples:] = x[n_half_samples:, 0] + 2 * x[n_half_samples:, 1]

    # Fitting linear regression with perfect prediction
    lr = LinearRegression(fit_intercept=False)
    lr.fit(x, y)

    # When all samples are weighted with the same weights, the ratio of
    # the two features importance should equal to 1 on expectation (when using
    # mean absolutes error as the loss function).
    pi = permutation_importance(
        lr, x, y, random_state=1, scoring="neg_mean_absolute_error", n_repeats=200
    )
    x1_x2_imp_ratio_w_none = pi.importances_mean[0] / pi.importances_mean[1]
    assert x1_x2_imp_ratio_w_none == pytest.approx(1, 0.01)

    # When passing a vector of ones as the sample_weight, results should be
    # the same as in the case that sample_weight=None.
    w = np.ones(n_samples)
        lr,
        x,
        y,
        scoring="neg_mean_absolute_error",
        n_repeats=200,
        sample_weight=w,
    )
    x1_x2_imp_ratio_w_ones = pi.importances_mean[0] / pi.importances_mean[1]
    assert x1_x2_imp_ratio_w_ones == pytest.approx(x1_x2_imp_ratio_w_none, 0.01)

    # When the ratio between the weights of the first half of the samples and
    # the second half of the samples approaches to infinity, the ratio of
    # the two features importance should equal to 2 on expectation (when using
    w = np.hstack([np.repeat(10.0**10, n_half_samples), np.repeat(1.0, n_half_samples)])
    lr.fit(x, y, w)
        lr,
        x,
        y,
    )
    x1_x2_imp_ratio_w = pi.importances_mean[0] / pi.importances_mean[1]
    assert x1_x2_imp_ratio_w / x1_x2_imp_ratio_w_none == pytest.approx(2, 0.01)

def test_permutation_importance_no_weights_scoring_function():
    # Creating a scorer function that does not takes sample_weight
    def my_scorer(estimator, X, y):
        return 1

    # Creating some data and estimator for the permutation test
    x = np.array([[1, 2], [3, 4]])
    w = np.array([1, 1])

    # test that permutation_importance does not return error when
    # sample_weight is None
    try:
        permutation_importance(lr, x, y, random_state=1, scoring=my_scorer, n_repeats=1)
        pytest.fail(
            "permutation_test raised an error when using a scorer "
            "function that does not accept sample_weight even though "
            "sample_weight was None"
        )

    # test that permutation_importance raise exception when sample_weight is
    # not None
        permutation_importance(
            lr, x, y, random_state=1, scoring=my_scorer, n_repeats=1, sample_weight=w
        )

def test_permutation_importance_multi_metric(list_single_scorer, multi_scorer):
    # Test permutation importance when scoring contains multiple scorers

    x, y = make_regression(n_samples=500, n_features=10, random_state=0)
    lr = LinearRegression().fit(x, y)

    multi_importance = permutation_importance(
        lr, x, y, random_state=1, scoring=multi_scorer, n_repeats=2
    )
    assert set(multi_importance.keys()) == set(list_single_scorer)

    for scorer in list_single_scorer:
        multi_result = multi_importance[scorer]
        single_result = permutation_importance(
            lr, x, y, random_state=1, scoring=scorer, n_repeats=2
        )

        assert_allclose(multi_result.importances, single_result.importances)

def test_permutation_importance_max_samples_error():
    """Check that a proper error message is raised when `max_samples` is not
    set to a valid input value.
    """
    X = np.array([(1.0, 2.0, 3.0, 4.0)]).T

    err_msg = r"max_samples must be <= n_samples"

        permutation_importance(clf, X, y, max_samples=5)

def test_output_shape(
    Estimator, method, data, grid_resolution, features, kind, use_custom_values
):
    # Check that partial_dependence has consistent output shape for different
    # kinds of estimators:
    # - classifiers with binary and multiclass settings
    # - regressors
    # - multi-task regressors

        est.set_params(n_estimators=2)  # speed-up computations

    # n_target corresponds to the number of classes (1 for binary classif) or
    # the number of tasks / outputs in multi task settings. It's equal to 1 for
    # classical regression_data.
    (X, y), n_targets = data
    n_instances = X.shape[0]

    custom_values = None
    if use_custom_values:
        grid_resolution = 5
        custom_values = {f: X[:grid_resolution, f] for f in features}

    result = partial_dependence(
        est,
        X=X,
        features=features,
        kind=kind,
        grid_resolution=grid_resolution,
        custom_values=custom_values,
    )
    pdp, axes = result, result["grid_values"]

    expected_pdp_shape = (n_targets, *[grid_resolution for _ in range(len(features))])
    expected_ice_shape = (
        n_targets,
        n_instances,
        *[grid_resolution for _ in range(len(features))],
    )
        assert pdp.average.shape == expected_pdp_shape
        assert pdp.individual.shape == expected_ice_shape
    else:  # 'both'

    expected_axes_shape = (len(features), grid_resolution)
    assert axes is not None
    assert np.asarray(axes).shape == expected_axes_shape

def test_grid_from_X():
    # tests for _grid_from_X: sanity check for output, and for shapes.

    # Make sure that the grid is a cartesian product of the input (it will use
    # the unique values instead of the percentiles)
    percentiles = (0.05, 0.95)
    grid_resolution = 100
    is_categorical = [False, False]
    X = np.asarray([[1, 2], [3, 4]])
    grid, axes = _grid_from_X(X, percentiles, is_categorical, grid_resolution, {})
    assert_array_equal(grid, [[1, 2], [1, 4], [3, 2], [3, 4]])
    assert_array_equal(axes, X.T)

    # test shapes of returned objects depending on the number of unique values
    # for a feature.
    grid_resolution = 15

    # n_unique_values > grid_resolution
    X = rng.normal(size=(20, 2))
    grid, axes = _grid_from_X(
        X,
        custom_values={},
    )
    assert grid.shape == (grid_resolution * grid_resolution, X.shape[1])
    assert np.asarray(axes).shape == (2, grid_resolution)
    assert grid.dtype == X.dtype

    # n_unique_values < grid_resolution, will use actual values
    n_unique_values = 12
    X[n_unique_values - 1 :, 0] = 12345
    rng.shuffle(X)  # just to make sure the order is irrelevant
        X,
    )
    assert grid.shape == (n_unique_values * grid_resolution, X.shape[1])
    # axes is a list of arrays of different shapes
    assert axes[0].shape == (n_unique_values,)
    assert axes[1].shape == (grid_resolution,)

    # Check that uses custom_range
    col_1_range = [0, 2, 3]
        X,
        custom_values={1: col_1_range},
    )
    assert grid.shape == (n_unique_values * len(col_1_range), X.shape[1])
    assert axes[1].shape == (len(col_1_range),)

    # Check that grid_resolution does not impact custom_range
    col_0_range = [0, 2, 3, 4, 5, 6]
    grid_resolution = 5
        X,
        custom_values={0: col_0_range},
    )
    assert grid.shape == (grid_resolution * len(col_0_range), X.shape[1])
    assert axes[0].shape == (len(col_0_range),)
    assert grid.dtype == np.result_type(X, np.asarray(col_0_range).dtype)

    X = np.array([[0, "a"], [1, "b"], [2, "c"]])

        X,
        custom_values={1: ["a", "b", "c"]},
    )
    assert grid.dtype == object

def test_grid_from_X_with_categorical(grid_resolution):
    """Check that `_grid_from_X` always sample from categories and does not
    depend from the percentiles.
    """
    is_categorical = [True]
    X = pd.DataFrame({"cat_feature": ["A", "B", "C", "A", "B", "D", "E"]})
        X,
    )
    assert grid.shape == (5, X.shape[1])
    assert axes[0].shape == (5,)

def test_grid_from_X_heterogeneous_type(grid_resolution):
    """
    is_categorical = [True, False]
        {
            "cat": ["A", "B", "C", "A", "B", "D", "E", "A", "B", "D"],
            "num": [1, 1, 1, 2, 5, 6, 6, 6, 6, 8],
        }
    )
    nunique = X.nunique()

        X,
    )
    if grid_resolution == 3:
        assert grid.shape == (15, 2)
        assert axes[0].shape[0] == nunique["num"]
        assert axes[1].shape[0] == grid_resolution
    else:
        assert grid.shape == (25, 2)
        assert axes[0].shape[0] == nunique["cat"]
        assert axes[1].shape[0] == nunique["cat"]

def test_grid_from_X_error(grid_resolution, percentiles, err_msg):
    is_categorical = [False]
        _grid_from_X(X, percentiles, is_categorical, grid_resolution, custom_values={})

def test_partial_dependence_helpers(est, method, target_feature):
    # Check that what is returned by _partial_dependence_brute or
    # _partial_dependence_recursion is equivalent to manually setting a target
    # feature to a given value, and computing the average prediction over all
    # samples.
    # This also checks that the brute and recursion methods give the same
    # output.
    # Note that even on the trainset, the brute and the recursion methods
    # aren't always strictly equivalent, in particular when the slow method
    # generates unrealistic samples that have low mass in the joint
    # distribution of the input features, and when some of the features are
    # dependent. Hence the high tolerance on the checks.

    X, y = make_regression(random_state=0, n_features=5, n_informative=5)
    # The 'init' estimator for GBDT (here the average prediction) isn't taken
    # into account with the recursion method, for technical reasons. We set
    # the mean to 0 to that this 'bug' doesn't have any effect.
    y = y - y.mean()

    # Clone is necessary to make the test thread-safe.
    est = clone(est).fit(X, y)

    # target feature will be set to .5 and then to 123
    features = np.array([target_feature], dtype=np.intp)
    grid = np.array([[0.5], [123]])

        pdp, predictions = _partial_dependence_brute(
            est, grid, features, X, response_method="auto"
        )
    else:
        pdp = _partial_dependence_recursion(est, grid, features)

    mean_predictions = []
    for val in (0.5, 123):
        X_ = X.copy()
        X_[:, target_feature] = val
        mean_predictions.append(est.predict(X_).mean())

    pdp = pdp[0]  # (shape is (1, 2) so make it (2,))

    # allow for greater margin for error with recursion method
    rtol = 1e-1 if method == "recursion" else 1e-3
    assert np.allclose(pdp, mean_predictions, rtol=rtol)

def test_recursion_decision_tree_vs_forest_and_gbdt(seed):
    # Make sure that the recursion method gives the same results on a
    # DecisionTreeRegressor and a GradientBoostingRegressor or a
    # RandomForestRegressor with 1 tree and equivalent parameters.

    # Purely random dataset to avoid correlated features
    y = rng.randn(n_samples) * 10

    # set max_depth not too high to avoid splits with same gain but different
    # features
    max_depth = 5

    tree_seed = 0
    forest = RandomForestRegressor(
        random_state=tree_seed,
    )
    # The forest will use ensemble.base._set_random_states to set the
    # random_state of the tree sub-estimator. We simulate this here to have
    # equivalent estimators.
    equiv_random_state = check_random_state(tree_seed).randint(np.iinfo(np.int32).max)
    gbdt = GradientBoostingRegressor(
        random_state=equiv_random_state,
    )
    tree = DecisionTreeRegressor(max_depth=max_depth, random_state=equiv_random_state)

    # sanity check: if the trees aren't the same, the PD values won't be equal
    try:
        assert_is_subtree(tree.tree_, gbdt[0, 0].tree_)
        assert_is_subtree(tree.tree_, forest[0].tree_)
        # For some reason the trees aren't exactly equal on 32bits, so the PDs
        # cannot be equal either. See
        # https://github.com/scikit-learn/scikit-learn/issues/8853
        assert _IS_32BIT, "this should only fail on 32 bit platforms"
        return

    grid = rng.randn(50).reshape(-1, 1)
    for f in range(n_features):
        features = np.array([f], dtype=np.intp)

        pdp_forest = _partial_dependence_recursion(forest, grid, features)
        pdp_gbdt = _partial_dependence_recursion(gbdt, grid, features)
        pdp_tree = _partial_dependence_recursion(tree, grid, features)

        np.testing.assert_allclose(pdp_gbdt, pdp_tree)
        np.testing.assert_allclose(pdp_forest, pdp_tree)

def test_recursion_decision_function(est, target_feature):
    # Make sure the recursion method (implicitly uses decision_function) has
    # the same result as using brute method with
    # response_method=decision_function

    X, y = make_classification(n_classes=2, n_clusters_per_class=1, random_state=1)
    assert np.mean(y) == 0.5  # make sure the init estimator predicts 0 anyway

    preds_1 = partial_dependence(
        est,
        X,
        [target_feature],
        response_method="decision_function",
        method="recursion",
        kind="average",
    )
    preds_2 = partial_dependence(
        est,
        X,
        method="brute",
    )

    assert_allclose(preds_1["average"], preds_2["average"], atol=1e-7)

def test_partial_dependence_easy_target(est, power):
    # If the target y only depends on one feature in an obvious way (linear or
    # quadratic) then the partial dependence for that feature should reflect
    # it.
    # We here fit a linear regression_data model (with polynomial features if
    # needed) and compute r_squared to check that the partial dependence
    # correctly reflects the target.

    target_variable = 2
    X = rng.normal(size=(n_samples, 5))
    y = X[:, target_variable] ** power

    pdp = partial_dependence(
        est, features=[target_variable], X=X, grid_resolution=1000, kind="average"
    )

    new_X = pdp["grid_values"][0].reshape(-1, 1)
    new_y = pdp["average"][0]
    # add polynomial features if needed
    new_X = PolynomialFeatures(degree=power).fit_transform(new_X)

    lr = LinearRegression().fit(new_X, new_y)
    r2 = r2_score(new_y, lr.predict(new_X))

    assert r2 > 0.99

def test_multiclass_multioutput(Estimator):
    # Make sure error is raised for multiclass-multioutput classifiers

    # make multiclass-multioutput dataset
    X, y = make_classification(n_classes=3, n_clusters_per_class=1, random_state=0)
    y = np.array([y, y]).T

        ValueError, match="Multiclass-multioutput estimators are not supported"
    ):
        partial_dependence(est, X, [0])

class NoPredictProbaNoDecisionFunction(ClassifierMixin, BaseEstimator):
        # simulate that we have some classes
        self.classes_ = [0, 1]

def test_partial_dependence_error(estimator, params, err_msg):
    estimator = clone(estimator).fit(X, y)

        partial_dependence(estimator, X, **params)

def test_partial_dependence_unknown_feature_indices(estimator, features):

    err_msg = "all features must be in"
        partial_dependence(estimator, X, [features])

def test_partial_dependence_unknown_feature_string(estimator):
    df = pd.DataFrame(X)
    estimator = clone(estimator).fit(df, y)

    features = ["random"]
    err_msg = "A given column is not a column of the dataframe"
        partial_dependence(estimator, df, features)

def test_partial_dependence_X_list(estimator):
    # check that array-like objects are accepted
    partial_dependence(estimator, list(X), [0], kind="average")

def test_warning_recursion_non_constant_init():
    # make sure that passing a non-constant init parameter to a GBDT and using
    # recursion method yields a warning.

    gbc = GradientBoostingClassifier(init=DummyClassifier(), random_state=0)

        UserWarning, match="Using recursion method with a non-constant init predictor"
    ):
        partial_dependence(gbc, X, [0], method="recursion", kind="average")

    ):

def test_partial_dependence_sample_weight_of_fitted_estimator():
    # Test near perfect correlation between partial dependence and diagonal
    # when sample weights emphasize y = x predictions
    # non-regression test for #13193
    # TODO: extend to HistGradientBoosting once sample_weight is supported
    N = 1000
    rng = np.random.RandomState(123456)
    mask = rng.randint(2, size=N, dtype=bool)

    x = rng.rand(N)
    # set y = x on mask and y = -x outside
    y[~mask] = -y[~mask]
    X = np.c_[mask, x]
    # sample weights to emphasize data points where y = x
    sample_weight = np.ones(N)
    sample_weight[mask] = 1000.0

    clf = GradientBoostingRegressor(n_estimators=10, random_state=1)

    pdp = partial_dependence(clf, X, features=[1], kind="average")

    assert np.corrcoef(pdp["average"], pdp["grid_values"])[0, 1] > 0.99

def test_hist_gbdt_sw_not_supported():
    # TODO: remove/fix when PDP supports HGBT with sample weights
    clf = HistGradientBoostingRegressor(random_state=1)
    clf.fit(X, y, sample_weight=np.ones(len(X)))

        NotImplementedError, match="does not support partial dependence"
    ):
        partial_dependence(clf, X, features=[1])

def test_partial_dependence_pipeline():
    # check that the partial dependence support pipeline

    clf = DummyClassifier(random_state=42)
    pipe = make_pipeline(scaler, clf)

    clf.fit(scaler.fit_transform(iris.data), iris.target)

    features = 0
    pdp_pipe = partial_dependence(
        pipe, iris.data, features=[features], grid_resolution=10, kind="average"
    )
    pdp_clf = partial_dependence(
        clf,
        scaler.transform(iris.data),
        features=[features],
        grid_resolution=10,
    )
    assert_allclose(pdp_pipe["average"], pdp_clf["average"])
        pdp_pipe["grid_values"][0],
        pdp_clf["grid_values"][0] * scaler.scale_[features] + scaler.mean_[features],
    )

def test_partial_dependence_binary_model_grid_resolution(
    features, grid_resolution, n_vals_expected
):
    model = DummyClassifier()

        {
            "a": rng.randint(0, 10, size=100).astype(np.float64),
            "b": rng.randint(0, 10, size=100).astype(np.float64),
        }
    )
    y = pd.Series(rng.randint(0, 2, size=100))

    part_dep = partial_dependence(
        model,
        X,
    )
    assert part_dep["average"].size == n_vals_expected

def test_partial_dependence_binary_model_custom_values(
    features, custom_values, n_vals_expected
):

    X = pd.DataFrame({"a": [1.0, 2.0, 3.0, 4.0], "b": [6.0, 7.0, 8.0, 9.0]})
    y = pd.Series([0, 1, 0, 1])

        model,
        X,
        grid_resolution=3,
    )

def test_partial_dependence_pipeline_custom_values(
):
    pl = make_pipeline(
        SimpleImputer(strategy="most_frequent"), OneHotEncoder(), DummyClassifier()
    )

    X = pd.DataFrame({"a": [1.0, 2.0, 3.0, 4.0], "b": ["a", "b", "a", "b"]})
    pl.fit(X, y)

    X_holdout = pd.DataFrame({"a": [1.0, 2.0, 3.0, 4.0], "b": ["a", "b", "a", None]})
        pl,
        X_holdout,
    )

def test_partial_dependence_dataframe(estimator, preprocessor, features):
    # check that the partial dependence support dataframe and pipeline
    # including a column transformer
    df = pd.DataFrame(scale(iris.data), columns=iris.feature_names)

    pipe = make_pipeline(preprocessor, clone(estimator))
    pipe.fit(df, iris.target)
        pipe, df, features=features, grid_resolution=10, kind="average"
    )

    # the column transformer will reorder the column when transforming
    # we mixed the index to be sure that we are computing the partial
    # dependence of the right columns
    if preprocessor is not None:
        X_proc = clone(preprocessor).fit_transform(df)
        features_clf = [0, 1]
    else:
        X_proc = df
        features_clf = [0, 2]

    clf = clone(estimator).fit(X_proc, iris.target)
        clf,
        X_proc,
        features=features_clf,
    )

        scaler = preprocessor.named_transformers_["standardscaler"]
            pdp_pipe["grid_values"][1],
            pdp_clf["grid_values"][1] * scaler.scale_[1] + scaler.mean_[1],
        )
    else:
        assert_allclose(pdp_pipe["grid_values"][1], pdp_clf["grid_values"][1])

def test_partial_dependence_feature_type(features, custom_values, expected_pd_shape):
    # check all possible features type supported in PDP

    preprocessor = make_column_transformer(
        (StandardScaler(), [iris.feature_names[i] for i in (0, 2)]),
        (RobustScaler(), [iris.feature_names[i] for i in (1, 3)]),
    )
    pipe = make_pipeline(
        preprocessor, LogisticRegression(max_iter=1000, random_state=0)
    )
        pipe,
        df,
    )
    assert pdp_pipe["average"].shape == expected_pd_shape
    assert len(pdp_pipe["grid_values"]) == len(pdp_pipe["average"].shape) - 1

def test_partial_dependence_unfitted(estimator):
        (StandardScaler(), [0, 2]), (RobustScaler(), [1, 3])
    )
    pipe = make_pipeline(preprocessor, estimator)
    with pytest.raises(NotFittedError, match="is not fitted yet"):
        partial_dependence(pipe, X, features=[0, 2], grid_resolution=10)
        partial_dependence(estimator, X, features=[0, 2], grid_resolution=10)

def test_kind_average_and_average_of_individual(Estimator, data):

    pdp_avg = partial_dependence(est, X=X, features=[1, 2], kind="average")
    pdp_ind = partial_dependence(est, X=X, features=[1, 2], kind="individual")
    avg_ind = np.mean(pdp_ind["individual"], axis=1)
    assert_allclose(avg_ind, pdp_avg["average"])

def test_partial_dependence_kind_individual_ignores_sample_weight(Estimator, data):
    """Check that `sample_weight` does not have any effect on reported ICE."""
    sample_weight = np.arange(X.shape[0])

    pdp_nsw = partial_dependence(est, X=X, features=[1, 2], kind="individual")
    pdp_sw = partial_dependence(
        est, X=X, features=[1, 2], kind="individual", sample_weight=sample_weight
    )
    assert_allclose(pdp_nsw["individual"], pdp_sw["individual"])
    assert_allclose(pdp_nsw["grid_values"], pdp_sw["grid_values"])

def test_partial_dependence_non_null_weight_idx(estimator, non_null_weight_idx):
    """Check that if we pass a `sample_weight` of zeros with only one index with
    sample weight equals one, then the average `partial_dependence` with this
    `sample_weight` is equal to the individual `partial_dependence` of the
    corresponding index.
    """
    )
    pipe = make_pipeline(preprocessor, clone(estimator)).fit(X, y)

    sample_weight = np.zeros_like(y)
    sample_weight[non_null_weight_idx] = 1
        pipe,
        X,
        [2, 3],
    )
    pdp_ind = partial_dependence(pipe, X, [2, 3], kind="individual", grid_resolution=10)
    output_dim = 1 if is_regressor(pipe) else len(np.unique(y))
    for i in range(output_dim):
            pdp_ind["individual"][i][non_null_weight_idx],
            pdp_sw["average"][i],
        )

def test_partial_dependence_equivalence_equal_sample_weight(Estimator, data):
    """Check that `sample_weight=None` is equivalent to having equal weights."""

    sample_weight, params = None, {"X": X, "features": [1, 2], "kind": "average"}
    pdp_sw_none = partial_dependence(est, **params, sample_weight=sample_weight)
    pdp_sw_unit = partial_dependence(est, **params, sample_weight=sample_weight)
    assert_allclose(pdp_sw_none["average"], pdp_sw_unit["average"])
    sample_weight = 2 * np.ones(len(y))
    pdp_sw_doubling = partial_dependence(est, **params, sample_weight=sample_weight)
    assert_allclose(pdp_sw_none["average"], pdp_sw_doubling["average"])

def test_partial_dependence_sample_weight_size_error():
    """Check that we raise an error when the size of `sample_weight` is not
    consistent with `X` and `y`.
    """
    (X, y), n_targets = binary_classification_data

    with pytest.raises(ValueError, match="sample_weight.shape =="):
        partial_dependence(
            est, X, features=[0], sample_weight=sample_weight[1:], grid_resolution=10
        )

def test_partial_dependence_sample_weight_with_recursion():
    """Check that we raise an error when `sample_weight` is provided with
    `"recursion"` method.
    """
    est = RandomForestRegressor()
    (X, y), n_targets = regression_data

    with pytest.raises(ValueError, match="'recursion' method can only be applied when"):
            est, X, features=[0], method="recursion", sample_weight=sample_weight
        )

def test_mixed_type_categorical():
    """Check that we raise a proper error when a column has mixed types and
    the sorting of `np.unique` will fail."""
    X = np.array(["A", "B", "C", np.nan], dtype=object).reshape(-1, 1)

    from sklearn.preprocessing import OrdinalEncoder

    clf = make_pipeline(
        OrdinalEncoder(encoded_missing_value=-1),
    with pytest.raises(ValueError, match="The column #0 contains mixed data types"):
        partial_dependence(clf, X, features=[0])

def test_reject_array_with_integer_dtype():
    X = np.arange(8).reshape(4, 2)
    clf = DummyClassifier()
        FutureWarning, match=re.escape("The column 0 contains integer data.")
    ):
        partial_dependence(clf, X, features=0)

        FutureWarning, match=re.escape("The column 1 contains integer data.")
    ):
        partial_dependence(clf, X, features=[1], categorical_features=[0])

    ):
        partial_dependence(clf, X, features=[0, 1])

    # The following should not raise as we do not compute numerical partial
    # dependence on integer columns.
        partial_dependence(clf, X, features=1, categorical_features=[1])

def test_reject_pandas_with_integer_dtype():
        {
            "a": [1.0, 2.0, 3.0],
            "b": [1, 2, 3],
            "c": [1, 2, 3],
        }
    )

        FutureWarning, match=re.escape("The column 'c' contains integer data.")
    ):
        partial_dependence(clf, X, features="c")

    ):
        partial_dependence(clf, X, features=["a", "c"])

        partial_dependence(clf, X, features=["a"])
        partial_dependence(clf, X, features=["c"], categorical_features=["c"])

def test_partial_dependence_empty_categorical_features():
    """Check that we raise the proper exception when `categorical_features`
    is an empty list"""

            "Passing an empty list (`[]`) to `categorical_features` is not "
            "supported. Use `None` instead to indicate that there are no "
            "categorical features."
        ),
    ):
            estimator=clf, X=iris.data, features=[0], categorical_features=[]
        )

def _check_boundary_response_method(estimator, response_method, class_of_interest):
    """Validate the response methods to be used with the fitted estimator.

        Fitted estimator to check.

    response_method : {'auto', 'decision_function', 'predict_proba', 'predict'}
        Specifies whether to use :term:`decision_function`, :term:`predict_proba`,
        :term:`predict` as the target response. If set to 'auto', the response method is
        tried in the before mentioned order.

    class_of_interest : int, float, bool, str or None
        The class considered when plotting the decision. Cannot be None if
        multiclass and `response_method` is 'predict_proba' or 'decision_function'.

    Returns
    -------
    prediction_method : list of str or str
        The name or list of names of the response methods to use.
    """
    has_classes = hasattr(estimator, "classes_")
    if has_classes and _is_arraylike_not_scalar(estimator.classes_[0]):
        msg = "Multi-label and multi-output multi-class classifiers are not supported"

        if is_regressor(estimator):
            prediction_method = "predict"
        else:
            prediction_method = ["decision_function", "predict_proba", "predict"]
    else:
        prediction_method = response_method

    return prediction_method

class DecisionBoundaryDisplay:
    """Decisions boundary visualization.

    :func:`~sklearn.inspection.DecisionBoundaryDisplay.from_estimator`
    to create a :class:`DecisionBoundaryDisplay`. All parameters are stored as

    For a detailed example comparing the decision boundaries of multinomial and
    one-vs-rest logistic regression, please see
    :ref:`sphx_glr_auto_examples_linear_model_plot_logistic_multinomial.py`.

    xx0 : ndarray of shape (grid_resolution, grid_resolution)
        First output of :func:`meshgrid <numpy.meshgrid>`.

    xx1 : ndarray of shape (grid_resolution, grid_resolution)
        Second output of :func:`meshgrid <numpy.meshgrid>`.

    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.
        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

        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`.

    xlabel : str, default=None
        Default label to place on x axis.

    ylabel : str, default=None
        Default label to place on y axis.

    surface_ : matplotlib `QuadContourSet` or `QuadMesh` or list of such objects
        If `plot_method` is 'contour' or 'contourf', `surface_` is
        :class:`QuadContourSet <matplotlib.contour.QuadContourSet>`. If
        `plot_method` is 'pcolormesh', `surface_` is
        :class:`QuadMesh <matplotlib.collections.QuadMesh>`.

    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.

        Axes with decision boundary.

        Figure containing the decision boundary.

    See Also
    --------
    DecisionBoundaryDisplay.from_estimator : Plot decision boundary given an estimator.

    Examples
    --------
    >>> from sklearn.inspection import DecisionBoundaryDisplay
    >>> feature_1, feature_2 = np.meshgrid(
    ...     np.linspace(iris.data[:, 0].min(), iris.data[:, 0].max()),
    ...     np.linspace(iris.data[:, 1].min(), iris.data[:, 1].max())
    ... )
    >>> grid = np.vstack([feature_1.ravel(), feature_2.ravel()]).T
    >>> 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
    ... )
    <...>
    >>> display.ax_.scatter(
    ...     iris.data[:, 0], iris.data[:, 1], c=iris.target, edgecolor="black"
    ... )
    <...>
    """

        self, *, xx0, xx1, response, multiclass_colors=None, xlabel=None, ylabel=None
    ):
        self.xx0 = xx0
        self.xx1 = xx1
        self.response = response
        self.multiclass_colors = multiclass_colors
        self.xlabel = xlabel
        self.ylabel = ylabel

    def plot(self, plot_method="contourf", ax=None, xlabel=None, ylabel=None, **kwargs):

        plot_method : {'contourf', 'contour', 'pcolormesh'}, default='contourf'
            Plotting method to call when plotting the response. Please refer
            to the following matplotlib documentation for details:
            :func:`contourf <matplotlib.pyplot.contourf>`,
            :func:`contour <matplotlib.pyplot.contour>`,
            :func:`pcolormesh <matplotlib.pyplot.pcolormesh>`.

        ax : Matplotlib axes, default=None
            created.

        xlabel : str, default=None
            Overwrite the x-axis label.

        ylabel : str, default=None
            Overwrite the y-axis label.

            Additional keyword arguments to be passed to the `plot_method`.

        Returns
        -------
        display: :class:`~sklearn.inspection.DecisionBoundaryDisplay`
        """
        check_matplotlib_support("DecisionBoundaryDisplay.plot")
        import matplotlib as mpl

        if plot_method not in ("contourf", "contour", "pcolormesh"):
                "plot_method must be 'contourf', 'contour', or 'pcolormesh'. "
                f"Got {plot_method} instead."
            )

        plot_func = getattr(ax, plot_method)
        if self.response.ndim == 2:
            self.surface_ = plot_func(self.xx0, self.xx1, self.response, **kwargs)
        else:  # self.response.ndim == 3
            n_responses = self.response.shape[-1]
            if (
                isinstance(self.multiclass_colors, str)
                or self.multiclass_colors is None
            ):
                if isinstance(self.multiclass_colors, str):
                    cmap = self.multiclass_colors
                else:
                    if n_responses <= 10:
                        cmap = "tab10"
                    else:
                        cmap = "gist_rainbow"

                # 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]
                else:
                    colors = plt.get_cmap(cmap, n_responses).colors
            elif isinstance(self.multiclass_colors, str):
                colors = colors = plt.get_cmap(
                    self.multiclass_colors, n_responses
                ).colors
            else:
                colors = [mpl.colors.to_rgba(color) for color in self.multiclass_colors]

            self.multiclass_colors_ = colors
            multiclass_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)
            ]

            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),
                )
                # `cmap` should not be in kwargs
                safe_kwargs = kwargs.copy()
                if "cmap" in safe_kwargs:
                    del safe_kwargs["cmap"]
                        "Plotting max class of multiclass 'decision_function' or "
                        "'predict_proba', thus 'multiclass_colors' used and "
                        "'cmap' kwarg ignored."
                    )
                self.surface_.append(
                    plot_func(self.xx0, self.xx1, response, cmap=cmap, **safe_kwargs)
                )

        if xlabel is not None or not ax.get_xlabel():
            xlabel = self.xlabel if xlabel is None else xlabel
            ax.set_xlabel(xlabel)
        if ylabel is not None or not ax.get_ylabel():
            ylabel = self.ylabel if ylabel is None else ylabel
            ax.set_ylabel(ylabel)

        cls,
        X,
        *,
        grid_resolution=100,
        eps=1.0,
        plot_method="contourf",
        class_of_interest=None,
        multiclass_colors=None,
        xlabel=None,
        ylabel=None,
        ax=None,
        **kwargs,
    ):
        """Plot decision boundary given an estimator.

        Read more in the :ref:`User Guide <visualizations>`.

            Trained estimator used to plot the decision boundary.

        X : {array-like, sparse matrix, dataframe} of shape (n_samples, 2)
            Input data that should be only 2-dimensional.

        grid_resolution : int, default=100
            Number of grid points to use for plotting decision boundary.
            Higher values will make the plot look nicer but be slower to
            render.

        eps : float, default=1.0
            Extends the minimum and maximum values of X for evaluating the
            response function.

        response_method : {'auto', 'decision_function', 'predict_proba', \
                'predict'}, default='auto'
            Specifies whether to use :term:`decision_function`,
            :term:`predict_proba` or :term:`predict` as the target response.
            If set to 'auto', the response method is tried in the order as
            listed above.

                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
            at each point is plotted. The color of each class can be set via
            `multiclass_colors`.

        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.

            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: '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 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`.

            The label used for the x-axis. If `None`, an attempt is made to
            extract a label from `X` if it is a dataframe, otherwise an empty
            string is used.

            The label used for the y-axis. If `None`, an attempt is made to

            created.

            Additional keyword arguments to be passed to the
            `plot_method`.

        Returns
        -------
        display : :class:`~sklearn.inspection.DecisionBoundaryDisplay`
            Object that stores the result.

        See Also
        --------
        DecisionBoundaryDisplay : Decision boundary visualization.
        sklearn.metrics.ConfusionMatrixDisplay.from_estimator : Plot the
            confusion matrix given an estimator, the data, and the label.
        sklearn.metrics.ConfusionMatrixDisplay.from_predictions : Plot the
            confusion matrix given the true and predicted labels.

        Examples
        --------
        >>> from sklearn.datasets import load_iris
        >>> from sklearn.inspection import DecisionBoundaryDisplay
        >>> iris = load_iris()
        >>> X = iris.data[:, :2]
        >>> classifier = LogisticRegression().fit(X, iris.target)
        >>> disp = DecisionBoundaryDisplay.from_estimator(
        ...     classifier, X, response_method="predict",
        ...     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")
        <...>
        """

        if not grid_resolution > 1:
                "grid_resolution must be greater than 1. Got"
                f" {grid_resolution} instead."
            )

        if not eps >= 0:
                f"eps must be greater than or equal to 0. Got {eps} instead."
            )

        possible_plot_methods = ("contourf", "contour", "pcolormesh")
        if plot_method not in possible_plot_methods:
            available_methods = ", ".join(possible_plot_methods)
                f"plot_method must be one of {available_methods}. "
            )

        num_features = _num_features(X)
        if num_features != 2:
                f"n_features must be equal to 2. Got {num_features} instead."
            )

        if (
            response_method in ("predict_proba", "decision_function", "auto")
            and multiclass_colors is not None
            and hasattr(estimator, "classes_")
            and (n_classes := len(estimator.classes_)) > 2
        ):
            if isinstance(multiclass_colors, list):
                if len(multiclass_colors) != n_classes:
                        "When 'multiclass_colors' is a list, it must be of the same "
                        f"length as 'estimator.classes_' ({n_classes}), got: "
                        f"{len(multiclass_colors)}."
                    )
                elif any(
                    not mpl.colors.is_color_like(col) for col in multiclass_colors
                ):
                        "When 'multiclass_colors' is a list, it can only contain valid"
                        f" Matplotlib color names. Got: {multiclass_colors}"
                    )
            if isinstance(multiclass_colors, str):
                if multiclass_colors not in mpl.pyplot.colormaps():
                        "When 'multiclass_colors' is a string, it must be a valid "
                        f"Matplotlib colormap. Got: {multiclass_colors}"
                    )

        x0, x1 = _safe_indexing(X, 0, axis=1), _safe_indexing(X, 1, axis=1)

        x0_min, x0_max = x0.min() - eps, x0.max() + eps
        x1_min, x1_max = x1.min() - eps, x1.max() + eps

        xx0, xx1 = np.meshgrid(
            np.linspace(x0_min, x0_max, grid_resolution),
            np.linspace(x1_min, x1_max, grid_resolution),
        )

        X_grid = np.c_[xx0.ravel(), xx1.ravel()]
        if _is_pandas_df(X) or _is_polars_df(X):
            adapter = _get_adapter_from_container(X)
            X_grid = adapter.create_container(
                X_grid,
                X_grid,
                columns=X.columns,
            )

        prediction_method = _check_boundary_response_method(
            estimator, response_method, class_of_interest
        )
        try:
            response, _, response_method_used = _get_response_values(
                X_grid,
                response_method=prediction_method,
                pos_label=class_of_interest,
                return_response_method_used=True,
            )
            if "is not a valid label" in str(exc):
                # re-raise a more informative error message since `pos_label` is unknown
                # to our user when interacting with
                # `DecisionBoundaryDisplay.from_estimator`
                    f"class_of_interest={class_of_interest} is not a valid label: It "
                    f"should be one of {estimator.classes_}"
                ) from exc
            raise

        # convert classes predictions into integers
        if response_method_used == "predict" and hasattr(estimator, "classes_"):
            encoder = LabelEncoder()
            encoder.classes_ = estimator.classes_
            response = encoder.transform(response)

        if response.ndim == 1:
            response = response.reshape(*xx0.shape)
        else:
            if is_regressor(estimator):
                raise ValueError("Multi-output regressors are not supported")

            if class_of_interest is not None:
                # For the multiclass case, `_get_response_values` returns the response
                # as-is. Thus, we have a column per class and we need to select the
                # column corresponding to the positive class.
                col_idx = np.flatnonzero(estimator.classes_ == class_of_interest)[0]
                response = response[:, col_idx].reshape(*xx0.shape)
            else:
                response = response.reshape(*xx0.shape, response.shape[-1])

        if xlabel is None:
            xlabel = X.columns[0] if hasattr(X, "columns") else ""

        if ylabel is None:
            ylabel = X.columns[1] if hasattr(X, "columns") else ""

        display = cls(
            xx0=xx0,
            xx1=xx1,
            response=response,
            multiclass_colors=multiclass_colors,
        )
        return display.plot(ax=ax, plot_method=plot_method, **kwargs)

    ):

            created.

        Returns
        -------
        """

            )

            if (
            ):
                else:
                    else:

                else:
                ).colors
            else:

                )
            ]

                )
                    )
                )

        cls,
        X,
        *,
        eps=1.0,
        ax=None,
        **kwargs,
    ):

            render.

                colormap.

            created.

        Returns
        -------

        See Also
        --------

        Examples
        --------
        ... )
        <...>
        """

            )

            )

            )

            )

        if (
        ):
                    )
                elif any(
                ):
                    )
                    )

        )

                X_grid,
                X_grid,
            )

        )
        try:
                X_grid,
            )
                ) from exc
            raise

        else:

            else:

            xx0=xx0,
            xx1=xx1,
        )

class PartialDependenceDisplay:
    """Partial Dependence Plot (PDP).

    This can also display individual partial dependencies which are often
    referred to as: Individual Condition Expectation (ICE).

    :func:`~sklearn.inspection.PartialDependenceDisplay.from_estimator` to create a
    :class:`~sklearn.inspection.PartialDependenceDisplay`. All parameters are

    :ref:`sphx_glr_auto_examples_miscellaneous_plot_partial_dependence_visualization_api.py`

    pd_results : list of Bunch
        Results of :func:`~sklearn.inspection.partial_dependence` for
        ``features``.

    features : list of (int,) or list of (int, int)
        Indices of features for a given plot. A tuple of one integer will plot
        a partial dependence curve of one feature. A tuple of two integers will
        plot a two-way partial dependence curve as a contour plot.

        Feature names corresponding to the indices in ``features``.

    target_idx : int

        - In a multiclass setting, specifies the class for which the PDPs
          should be computed. Note that for binary classification, the
          positive class (index 1) is always used.
        - In a multioutput setting, specifies the task for which the PDPs
          should be computed.

        Ignored in binary classification or classical regression settings.

    deciles : dict
        Deciles for feature indices in ``features``.

    kind : {'average', 'individual', 'both'} or list of such str, \
            default='average'
        Whether to plot the partial dependence averaged across all the samples
        in the dataset or one line per sample or both.

        - ``kind='average'`` results in the traditional PD plot;
        - ``kind='individual'`` results in the ICE plot;
        - ``kind='both'`` results in plotting both the ICE and PD on the same
          plot.

        A list of such strings can be provided to specify `kind` on a per-plot
        basis. The length of the list should be the same as the number of
        interaction requested in `features`.

        .. note::
           ICE ('individual' or 'both') is not a valid option for 2-ways
           interactions plot. As a result, an error will be raised.
           2-ways interaction plots should always be configured to
           use the 'average' kind instead.

        .. note::
           The fast ``method='recursion'`` option is only available for
           `kind='average'` and `sample_weights=None`. Computing individual
           dependencies and doing weighted averages requires using the slower
           `method='brute'`.

           Add `kind` parameter with `'average'`, `'individual'`, and `'both'`
           options.

           Add the possibility to pass a list of string specifying `kind`
           for each plot.

    subsample : float, int or None, default=1000
        Sampling for ICE curves when `kind` is 'individual' or 'both'.
        If float, should be between 0.0 and 1.0 and represent the proportion
        of the dataset to be used to plot ICE curves. If int, represents the
        maximum absolute number of samples to use.

        Note that the full dataset is still used to calculate partial
        dependence when `kind='both'`.

        Controls the randomness of the selected samples when subsamples is not
        `None`. See :term:`Glossary <random_state>` for details.

    is_categorical : list of (bool,) or list of (bool, bool), default=None
        Whether each target feature in `features` is categorical or not.
        The list should be same size as `features`. If `None`, all features
        are assumed to be continuous.

    bounding_ax_ : matplotlib Axes or None
        If `ax` is an axes or None, the `bounding_ax_` is the axes where the
        grid of partial dependence plots are drawn. If `ax` is a list of axes
        or a numpy array of axes, `bounding_ax_` is None.

    axes_ : ndarray of matplotlib Axes
        If `ax` is an axes or None, `axes_[i, j]` is the axes on the i-th row
        and j-th column. If `ax` is a list of axes, `axes_[i]` is the i-th item
        in `ax`. Elements that are None correspond to a nonexisting axes in
        that position.

    lines_ : ndarray of matplotlib Artists
        If `ax` is an axes or None, `lines_[i, j]` is the partial dependence
        curve on the i-th row and j-th column. If `ax` is a list of axes,
        `lines_[i]` is the partial dependence curve corresponding to the i-th
        item in `ax`. Elements that are None correspond to a nonexisting axes
        or an axes that does not include a line plot.

    deciles_vlines_ : ndarray of matplotlib LineCollection
        If `ax` is an axes or None, `vlines_[i, j]` is the line collection
        representing the x axis deciles of the i-th row and j-th column. If
        `ax` is a list of axes, `vlines_[i]` corresponds to the i-th item in
        `ax`. Elements that are None correspond to a nonexisting axes or an
        axes that does not include a PDP plot.

    deciles_hlines_ : ndarray of matplotlib LineCollection
        representing the y axis deciles of the i-th row and j-th column. If
        axes that does not include a 2-way plot.

    contours_ : ndarray of matplotlib Artists
        If `ax` is an axes or None, `contours_[i, j]` is the partial dependence
        plot on the i-th row and j-th column. If `ax` is a list of axes,
        `contours_[i]` is the partial dependence plot corresponding to the i-th
        or an axes that does not include a contour plot.

    bars_ : ndarray of matplotlib Artists
        If `ax` is an axes or None, `bars_[i, j]` is the partial dependence bar
        plot on the i-th row and j-th column (for a categorical feature).
        If `ax` is a list of axes, `bars_[i]` is the partial dependence bar
        plot corresponding to the i-th item in `ax`. Elements that are None
        correspond to a nonexisting axes or an axes that does not include a
        bar plot.

    heatmaps_ : ndarray of matplotlib Artists
        If `ax` is an axes or None, `heatmaps_[i, j]` is the partial dependence
        heatmap on the i-th row and j-th column (for a pair of categorical
        features) . If `ax` is a list of axes, `heatmaps_[i]` is the partial
        dependence heatmap corresponding to the i-th item in `ax`. Elements
        that are None correspond to a nonexisting axes or an axes that does not
        include a heatmap.

        Figure containing partial dependence plots.

    See Also
    --------
    partial_dependence : Compute Partial Dependence values.

    Examples
    --------
    >>> from sklearn.inspection import PartialDependenceDisplay
    >>> from sklearn.inspection import partial_dependence
    >>> X, y = make_friedman1()
    >>> clf = GradientBoostingRegressor(n_estimators=10).fit(X, y)
    >>> features, feature_names = [(0,)], [f"Features #{i}" for i in range(X.shape[1])]
    >>> deciles = {0: np.linspace(0, 1, num=5)}
    >>> pd_results = partial_dependence(
    ...     clf, X, features=0, kind="average", grid_resolution=5)
    >>> display = PartialDependenceDisplay(
    ...     [pd_results], features=features, feature_names=feature_names,
    ...     target_idx=0, deciles=deciles
    ... )
    >>> display.plot(pdp_lim={1: (-1.38, 0.66)})
    <...>
    """

        self,
        pd_results,
        *,
        features,
        target_idx,
        deciles,
        subsample=1000,
    ):
        self.pd_results = pd_results
        self.target_idx = target_idx
        self.deciles = deciles
        self.kind = kind

        cls,
        X,
        features,
        *,
        categorical_features=None,
        target=None,
        n_cols=3,
        percentiles=(0.05, 0.95),
        custom_values=None,
        method="auto",
        line_kw=None,
        ice_lines_kw=None,
        pd_line_kw=None,
        contour_kw=None,
        ax=None,
        centered=False,
    ):
        """Partial dependence (PD) and individual conditional expectation (ICE) plots.

        Partial dependence plots, individual conditional expectation plots or an
        overlay of both of them can be plotted by setting the ``kind``
        parameter. The ``len(features)`` plots are arranged in a grid with
        ``n_cols`` columns. Two-way partial dependence plots are plotted as
        contour plots. The deciles of the feature values will be shown with tick
        marks on the x-axes for one-way plots, and on both axes for two-way
        plots.

        Read more in
        :ref:`sphx_glr_auto_examples_inspection_plot_partial_dependence.py`
        and the :ref:`User Guide <partial_dependence>`.

        .. note::

            :func:`PartialDependenceDisplay.from_estimator` does not support using the
            same axes with multiple calls. To plot the partial dependence for
            multiple estimators, please pass the axes created by the first call to the
            second call::

               >>> from sklearn.inspection import PartialDependenceDisplay
               >>> from sklearn.datasets import make_friedman1
               >>> from sklearn.linear_model import LinearRegression
               >>> from sklearn.ensemble import RandomForestRegressor
               >>> X, y = make_friedman1()
               >>> est1 = LinearRegression().fit(X, y)
               >>> est2 = RandomForestRegressor().fit(X, y)
               >>> disp1 = PartialDependenceDisplay.from_estimator(est1, X,
               ...                                                 [1, 2])
               >>> disp2 = PartialDependenceDisplay.from_estimator(est2, X, [1, 2],
               ...                                                 ax=disp1.axes_)

            For :class:`~sklearn.ensemble.GradientBoostingClassifier` and
            :class:`~sklearn.ensemble.GradientBoostingRegressor`, the
            `'recursion'` method (used by default) will not account for the `init`
            predictor of the boosting process. In practice, this will produce
            the same values as `'brute'` up to a constant offset in the target
            response, provided that `init` is a constant estimator (which is the
            default). However, if `init` is not a constant estimator, the
            partial dependence values are incorrect for `'recursion'` because the
            offset will be sample-dependent. It is preferable to use the `'brute'`
            method. Note that this only applies to
            :class:`~sklearn.ensemble.GradientBoostingClassifier` and
            :class:`~sklearn.ensemble.GradientBoostingRegressor`, not to
            :class:`~sklearn.ensemble.HistGradientBoostingClassifier` and
            :class:`~sklearn.ensemble.HistGradientBoostingRegressor`.

        estimator : BaseEstimator
            A fitted estimator object implementing :term:`predict`,
            :term:`predict_proba`, or :term:`decision_function`.
            Multioutput-multiclass classifiers are not supported.

        X : {array-like, dataframe} of shape (n_samples, n_features)
            ``X`` is used to generate a grid of values for the target
            ``features`` (where the partial dependence will be evaluated), and
            also to generate values for the complement features when the
            `method` is `'brute'`.

        features : list of {int, str, pair of int, pair of str}
            The target features for which to create the PDPs.
            If `features[i]` is an integer or a string, a one-way PDP is created;
            if `features[i]` is a tuple, a two-way PDP is created (only supported
            with `kind='average'`). Each tuple must be of size 2.
            If any entry is a string, then it must be in ``feature_names``.

            Sample weights are used to calculate weighted means when averaging the
            model output. If `None`, then samples are equally weighted. If
            `sample_weight` is not `None`, then `method` will be set to `'brute'`.
            Note that `sample_weight` is ignored for `kind='individual'`.

        categorical_features : array-like of shape (n_features,) or shape \
                (n_categorical_features,), dtype={bool, int, str}, default=None
            Indicates the categorical features.

            - `None`: no feature will be considered categorical;
            - boolean array-like: boolean mask of shape `(n_features,)`
              indicating which features are categorical. Thus, this array has
              the same shape has `X.shape[1]`;
            - integer or string array-like: integer indices or strings
              indicating categorical features.

        feature_names : array-like of shape (n_features,), dtype=str, default=None
            Name of each feature; `feature_names[i]` holds the name of the feature
            with index `i`.
            By default, the name of the feature corresponds to their numerical
            index for NumPy array and their column name for pandas dataframe.

        target : int, default=None
            - In a multiclass setting, specifies the class for which the PDPs
              should be computed. Note that for binary classification, the
              positive class (index 1) is always used.
            - In a multioutput setting, specifies the task for which the PDPs
              should be computed.

            Ignored in binary classification or classical regression settings.

        response_method : {'auto', 'predict_proba', 'decision_function'}, \
            :term:`decision_function` as the target response. For regressors
            this parameter is ignored and the response is always the output of
            :term:`predict`. By default, :term:`predict_proba` is tried first
            and we revert to :term:`decision_function` if it doesn't exist. If
            ``method`` is `'recursion'`, the response is always the output of
            :term:`decision_function`.

        n_cols : int, default=3
            The maximum number of columns in the grid plot. Only active when `ax`
            is a single axis or `None`.

            The number of equally spaced points on the axes of the plots, for each
            target feature.
            This parameter is overridden by `custom_values` if that parameter is set.

        percentiles : tuple of float, default=(0.05, 0.95)
            The lower and upper percentile used to create the extreme values
            for the PDP axes. Must be in [0, 1].

        custom_values : dict
            A dictionary mapping the index of an element of `features` to an
            array of values where the partial dependence should be calculated
            for that feature. Setting a range of values for a feature overrides
            `grid_resolution` and `percentiles`.

        method : str, default='auto'
            The method used to calculate the averaged predictions:

            - `'recursion'` is only supported for some tree-based estimators
              (namely
              :class:`~sklearn.ensemble.GradientBoostingClassifier`,
              :class:`~sklearn.ensemble.GradientBoostingRegressor`,
              :class:`~sklearn.ensemble.HistGradientBoostingClassifier`,
              :class:`~sklearn.ensemble.HistGradientBoostingRegressor`,
              :class:`~sklearn.tree.DecisionTreeRegressor`,
              :class:`~sklearn.ensemble.RandomForestRegressor`
              but is more efficient in terms of speed.
              With this method, the target response of a
              classifier is always the decision function, not the predicted
              probabilities. Since the `'recursion'` method implicitly computes
              the average of the ICEs by design, it is not compatible with ICE and
              thus `kind` must be `'average'`.

            - `'brute'` is supported for any estimator, but is more
              computationally intensive.

            - `'auto'`: the `'recursion'` is used for estimators that support it,
              and `'brute'` is used otherwise. If `sample_weight` is not `None`,
              then `'brute'` is used regardless of the estimator.

            Please see :ref:`this note <pdp_method_differences>` for
            differences between the `'brute'` and `'recursion'` method.

        n_jobs : int, default=None
            The number of CPUs to use to compute the partial dependences.
            Computation is parallelized over features specified by the `features`

            ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
            ``-1`` means using all processors. See :term:`Glossary <n_jobs>`
            for more details.

        verbose : int, default=0
            Verbose output during PD computations.

        line_kw : dict, default=None
            Dict with keywords passed to the ``matplotlib.pyplot.plot`` call.
            For one-way partial dependence plots. It can be used to define common
            properties for both `ice_lines_kw` and `pdp_line_kw`.

        ice_lines_kw : dict, default=None
            Dictionary with keywords passed to the `matplotlib.pyplot.plot` call.
            For ICE lines in the one-way partial dependence plots.
            The key value pairs defined in `ice_lines_kw` takes priority over
            `line_kw`.

        pd_line_kw : dict, default=None
            For partial dependence in one-way partial dependence plots.
            The key value pairs defined in `pd_line_kw` takes priority over

        contour_kw : dict, default=None
            Dict with keywords passed to the ``matplotlib.pyplot.contourf`` call.
            For two-way partial dependence plots.

        ax : Matplotlib axes or array-like of Matplotlib axes, default=None
            - If a single axis is passed in, it is treated as a bounding axes
              and a grid of partial dependence plots will be drawn within
              these bounds. The `n_cols` parameter controls the number of
              columns in the grid.
            - If an array-like of axes are passed in, the partial dependence
              plots will be drawn directly into these axes.
            - If `None`, a figure and a bounding axes is created and treated
              as the single axes case.

        kind : {'average', 'individual', 'both'}, default='average'
            Whether to plot the partial dependence averaged across all the samples
            in the dataset or one line per sample or both.

            - ``kind='average'`` results in the traditional PD plot;
            - ``kind='individual'`` results in the ICE plot.

            Note that the fast `method='recursion'` option is only available for
            `kind='average'` and `sample_weights=None`. Computing individual
            dependencies and doing weighted averages requires using the slower
            `method='brute'`.

        centered : bool, default=False
            If `True`, the ICE and PD lines will start at the origin of the
            y-axis. By default, no centering is done.

        subsample : float, int or None, default=1000
            Sampling for ICE curves when `kind` is 'individual' or 'both'.
            If `float`, should be between 0.0 and 1.0 and represent the proportion
            of the dataset to be used to plot ICE curves. If `int`, represents the
            absolute number samples to use.

            Note that the full dataset is still used to calculate averaged partial
            dependence when `kind='both'`.

            Controls the randomness of the selected samples when subsamples is not
            `None` and `kind` is either `'both'` or `'individual'`.

        Returns
        -------
        display : :class:`~sklearn.inspection.PartialDependenceDisplay`

        See Also
        --------
        partial_dependence : Compute Partial Dependence values.

        Examples
        --------
        >>> from sklearn.datasets import make_friedman1
        >>> from sklearn.ensemble import GradientBoostingRegressor
        >>> from sklearn.inspection import PartialDependenceDisplay
        >>> X, y = make_friedman1()
        >>> clf = GradientBoostingRegressor(n_estimators=10).fit(X, y)
        >>> PartialDependenceDisplay.from_estimator(clf, X, [0, (0, 1)])
        <...>
        """

        # set target_idx for multi-class estimators
        if hasattr(estimator, "classes_") and np.size(estimator.classes_) > 2:
            if target is None:
                raise ValueError("target must be specified for multi-class")
            target_idx = np.searchsorted(estimator.classes_, target)
            if (
                not (0 <= target_idx < len(estimator.classes_))
                or estimator.classes_[target_idx] != target
            ):
                raise ValueError("target not in est.classes_, got {}".format(target))
        else:
            # regression and binary classification
            target_idx = 0

        # Use check_array only on lists and other non-array-likes / sparse. Do not
        # convert DataFrame into a NumPy array.
        if not (hasattr(X, "__array__") or sparse.issparse(X)):
            X = check_array(X, ensure_all_finite="allow-nan", dtype=object)

        feature_names = _check_feature_names(X, feature_names)
        # expand kind to always be a list of str
        kind_ = [kind] * len(features) if isinstance(kind, str) else kind
        if len(kind_) != len(features):
                "When `kind` is provided as a list of strings, it should contain "
                f"as many elements as `features`. `kind` contains {len(kind_)} "
                f"element(s) and `features` contains {len(features)} element(s)."
            )

        # convert features into a seq of int tuples
        tmp_features, ice_for_two_way_pd = [], []
        for kind_plot, fxs in zip(kind_, features):
            if isinstance(fxs, (numbers.Integral, str)):
                fxs = (fxs,)
            try:
                fxs = tuple(
                    _get_feature_index(fx, feature_names=feature_names) for fx in fxs
                )
                    "Each entry in features must be either an int, "
                    "a string, or an iterable of size at most 2."
                ) from e
            if not 1 <= np.size(fxs) <= 2:
                )
            # store the information if 2-way PD was requested with ICE to later
            # raise a ValueError with an exhaustive list of problematic
            # settings.
            ice_for_two_way_pd.append(kind_plot != "average" and np.size(fxs) > 1)

            tmp_features.append(fxs)

        if any(ice_for_two_way_pd):
            # raise an error and be specific regarding the parameter values
            # when 1- and 2-way PD were requested
            kind_ = [
                "average" if forcing_average else kind_plot
                for forcing_average, kind_plot in zip(ice_for_two_way_pd, kind_)
            ]
                "ICE plot cannot be rendered for 2-way feature interactions. "
                "2-way feature interactions mandates PD plots using the "
                "'average' kind: "
                f"features={features!r} should be configured to use "
                f"kind={kind_!r} explicitly."
            )
        features = tmp_features

        if categorical_features is None:
                (False,) if len(fxs) == 1 else (False, False) for fxs in features
            ]
        else:
            # we need to create a boolean indicator of which features are
            # categorical from the categorical_features list.
            if categorical_features.dtype.kind == "b":
                # categorical features provided as a list of boolean
                if categorical_features.size != n_features:
                        "When `categorical_features` is a boolean array-like, "
                        "the array should be of shape (n_features,). Got "
                        f"{categorical_features.size} elements while `X` contains "
                        f"{n_features} features."
                    )
                is_categorical = [
                    tuple(categorical_features[fx] for fx in fxs) for fxs in features
                ]
            elif categorical_features.dtype.kind in ("i", "O", "U"):
                # categorical features provided as a list of indices or feature names
                categorical_features_idx = [
                    _get_feature_index(cat, feature_names=feature_names)
                    for cat in categorical_features
                ]
                    tuple([idx in categorical_features_idx for idx in fxs])
                    for fxs in features
                ]
            else:
                    "Expected `categorical_features` to be an array-like of boolean,"
                    f" integer, or string. Got {categorical_features.dtype} instead."
                )

            for cats in is_categorical:
                if np.size(cats) == 2 and (cats[0] != cats[1]):
                        "Two-way partial dependence plots are not supported for pairs"
                        " of continuous and categorical features."
                    )

            # collect the indices of the categorical features targeted by the partial
            # dependence computation
            categorical_features_targeted = set(
                [
                    fx
                    for fxs, cats in zip(features, is_categorical)
                    for fx in fxs
                    if any(cats)
                ]
            )
            if categorical_features_targeted:
                min_n_cats = min(
                    [
                        len(_unique(_safe_indexing(X, idx, axis=1)))
                        for idx in categorical_features_targeted
                    ]
                )
                if grid_resolution < min_n_cats:
                        "The resolution of the computed grid is less than the "
                        "minimum number of categories in the targeted categorical "
                        "features. Expect the `grid_resolution` to be greater than "
                        f"{min_n_cats}. Got {grid_resolution} instead."
                    )

            for is_cat, kind_plot in zip(is_categorical, kind_):
                if any(is_cat) and kind_plot != "average":
                        "It is not possible to display individual effects for"
                        " categorical features."
                    )

        # Early exit if the axes does not have the correct number of axes
        if ax is not None and not isinstance(ax, plt.Axes):
            axes = np.asarray(ax, dtype=object)
            if axes.size != len(features):
                    "Expected ax to have {} axes, got {}".format(
                        len(features), axes.size
                    )
                )

        for i in chain.from_iterable(features):
            if i >= len(feature_names):
                    "All entries of features must be less than "
                    "len(feature_names) = {0}, got {1}.".format(len(feature_names), i)
                )

                )
                    f"When a floating-point, subsample={subsample} should be in "
                    "the (0, 1) range."
                )

        # compute predictions and/or averaged predictions
        pd_results = Parallel(n_jobs=n_jobs, verbose=verbose)(
            delayed(partial_dependence)(
                X,
                fxs,
                feature_names=feature_names,
                categorical_features=categorical_features,
                response_method=response_method,
                grid_resolution=grid_resolution,
                percentiles=percentiles,
                kind=kind_plot,
                custom_values=custom_values,
            )
            for kind_plot, fxs in zip(kind_, features)
        )

        # For multioutput regression, we can only check the validity of target
        # now that we have the predictions.
        # Also note: as multiclass-multioutput classifiers are not supported,
        # multiclass and multioutput scenario are mutually exclusive. So there is
        # no risk of overwriting target_idx here.
        pd_result = pd_results[0]  # checking the first result is enough
        n_tasks = (
            pd_result.average.shape[0]
            if kind_[0] == "average"
            else pd_result.individual.shape[0]
        )
        if is_regressor(estimator) and n_tasks > 1:
                raise ValueError("target must be specified for multi-output regressors")
            if not 0 <= target <= n_tasks:
                    "target must be in [0, n_tasks], got {}.".format(target)
                )
            target_idx = target

        deciles = {}
        for fxs, cats in zip(features, is_categorical):
            for fx, cat in zip(fxs, cats):
                if not cat and fx not in deciles:
                    X_col = _safe_indexing(X, fx, axis=1)
                    deciles[fx] = mquantiles(X_col, prob=np.arange(0.1, 1.0, 0.1))

            pd_results=pd_results,
            features=features,
            target_idx=target_idx,
            deciles=deciles,
        )
        return display.plot(
            ax=ax,
            n_cols=n_cols,
            line_kw=line_kw,
            ice_lines_kw=ice_lines_kw,
            pd_line_kw=pd_line_kw,
            contour_kw=contour_kw,
            centered=centered,
        )

    def _get_sample_count(self, n_samples):
        """Compute the number of samples as an integer."""
        if isinstance(self.subsample, numbers.Integral):
            if self.subsample < n_samples:
                return self.subsample
            return n_samples
        elif isinstance(self.subsample, numbers.Real):
            return ceil(n_samples * self.subsample)

    def _plot_ice_lines(
        self,
        preds,
        feature_values,
        n_ice_to_plot,
        ax,
        pd_plot_idx,
        n_total_lines_by_plot,
        individual_line_kw,
    ):
        """Plot the ICE lines.

        preds : ndarray of shape \
                (n_instances, n_grid_points)
            The predictions computed for all points of `feature_values` for a
            given feature for all samples in `X`.
        feature_values : ndarray of shape (n_grid_points,)
            The feature values for which the predictions have been computed.
        n_ice_to_plot : int
            The number of ICE lines to plot.
        ax : Matplotlib axes
            The axis on which to plot the ICE lines.
        pd_plot_idx : int
            The sequential index of the plot. It will be unraveled to find the
            matching 2D position in the grid layout.
        n_total_lines_by_plot : int
            The total number of lines expected to be plot on the axis.
        individual_line_kw : dict
            Dict with keywords passed when plotting the ICE lines.
        """
        # subsample ice
        ice_lines_idx = rng.choice(
            preds.shape[0],
            n_ice_to_plot,
            replace=False,
        )
        ice_lines_subsampled = preds[ice_lines_idx, :]
        # plot the subsampled ice
        for ice_idx, ice in enumerate(ice_lines_subsampled):
            line_idx = np.unravel_index(
                pd_plot_idx * n_total_lines_by_plot + ice_idx, self.lines_.shape
            )
            self.lines_[line_idx] = ax.plot(
                feature_values, ice.ravel(), **individual_line_kw
            )[0]

    def _plot_average_dependence(
        self,
        avg_preds,
        ax,
        pd_line_idx,
        line_kw,
        categorical,
        bar_kw,
    ):
        """Plot the average partial dependence.

        avg_preds : ndarray of shape (n_grid_points,)
            The average predictions for all points of `feature_values` for a
            The axis on which to plot the average PD.
        pd_line_idx : int
        line_kw : dict
            Dict with keywords passed when plotting the PD plot.
        categorical : bool
            Whether feature is categorical.
        bar_kw: dict
            Dict with keywords passed when plotting the PD bars (categorical).
        """
        if categorical:
            bar_idx = np.unravel_index(pd_line_idx, self.bars_.shape)
            self.bars_[bar_idx] = ax.bar(feature_values, avg_preds, **bar_kw)[0]
            ax.tick_params(axis="x", rotation=90)
        else:
            line_idx = np.unravel_index(pd_line_idx, self.lines_.shape)
                feature_values,
                avg_preds,
                **line_kw,
            )[0]

    def _plot_one_way_partial_dependence(
        self,
        kind,
        preds,
        feature_idx,
        n_ice_lines,
        ax,
        n_cols,
        n_lines,
        ice_lines_kw,
        pd_line_kw,
        bar_kw,
        pdp_lim,
    ):
        """Plot 1-way partial dependence: ICE and PDP.

        kind : str
            The kind of partial plot to draw.
                (n_instances, n_grid_points) or None
        feature_idx : int
            The index corresponding to the target feature.
        n_ice_lines : int
            The axis on which to plot the ICE and PDP lines.
        n_cols : int or None
            The number of column in the axis.
        n_lines : int
        ice_lines_kw : dict
        pd_line_kw : dict
        pdp_lim : dict
            Global min and max average predictions, such that all plots will
            have the same scale and y limits. `pdp_lim[1]` is the global min
            and max for single partial dependence curves.
        """
        from matplotlib import transforms

        if kind in ("individual", "both"):
            self._plot_ice_lines(
                preds[self.target_idx],
                n_ice_lines,
                ax,
                pd_plot_idx,
                n_lines,
                ice_lines_kw,
            )

        if kind in ("average", "both"):
            # the average is stored as the last line
            if kind == "average":
                pd_line_idx = pd_plot_idx
            else:
                pd_line_idx = pd_plot_idx * n_lines + n_ice_lines
            self._plot_average_dependence(
                avg_preds[self.target_idx].ravel(),
                ax,
                pd_line_idx,
                pd_line_kw,
                categorical,
                bar_kw,
            )

        trans = transforms.blended_transform_factory(ax.transData, ax.transAxes)
        # create the decile line for the vertical axis
        vlines_idx = np.unravel_index(pd_plot_idx, self.deciles_vlines_.shape)
        if self.deciles.get(feature_idx[0], None) is not None:
            self.deciles_vlines_[vlines_idx] = ax.vlines(
                self.deciles[feature_idx[0]],
                0,
                0.05,
                transform=trans,
                color="k",
            )
        # reset ylim which was overwritten by vlines
        min_val = min(val[0] for val in pdp_lim.values())
        max_val = max(val[1] for val in pdp_lim.values())
        ax.set_ylim([min_val, max_val])

        # Set xlabel if it is not already set
        if not ax.get_xlabel():
            ax.set_xlabel(self.feature_names[feature_idx[0]])

        if n_cols is None or pd_plot_idx % n_cols == 0:
            if not ax.get_ylabel():
                ax.set_ylabel("Partial dependence")
        else:
            ax.set_yticklabels([])

        if pd_line_kw.get("label", None) and kind != "individual" and not categorical:
            ax.legend()

    def _plot_two_way_partial_dependence(
        self,
        ax,
        Z_level,
        contour_kw,
        heatmap_kw,
    ):
        """Plot 2-way partial dependence.

        avg_preds : ndarray of shape \
                (n_instances, n_grid_points, n_grid_points)
            The average predictions for all points of `feature_values[0]` and
            `feature_values[1]` for some given features for all samples in `X`.
        feature_values : seq of 1d array
            A sequence of array of the feature values for which the predictions
            have been computed.
        feature_idx : tuple of int
            The indices of the target features
        Z_level : ndarray of shape (8, 8)
            The Z-level used to encode the average predictions.
        contour_kw : dict
            Dict with keywords passed when plotting the contours.
            Whether features are categorical.
        heatmap_kw: dict
            Dict with keywords passed when plotting the PD heatmap
            (categorical).
        """
            import matplotlib.pyplot as plt

            default_im_kw = dict(interpolation="nearest", cmap="viridis")
            im_kw = {**default_im_kw, **heatmap_kw}

            data = avg_preds[self.target_idx]
            im = ax.imshow(data, **im_kw)
            text = None
            cmap_min, cmap_max = im.cmap(0), im.cmap(1.0)

            text = np.empty_like(data, dtype=object)
            thresh = (data.max() + data.min()) / 2.0

            for flat_index in range(data.size):
                row, col = np.unravel_index(flat_index, data.shape)
                color = cmap_max if data[row, col] < thresh else cmap_min

                values_format = ".2f"
                text_data = format(data[row, col], values_format)

                text_kwargs = dict(ha="center", va="center", color=color)
                text[row, col] = ax.text(col, row, text_data, **text_kwargs)

            fig.colorbar(im, ax=ax)
            ax.set(
                xticks=np.arange(len(feature_values[1])),
                yticks=np.arange(len(feature_values[0])),
                xticklabels=feature_values[1],
                yticklabels=feature_values[0],
                xlabel=self.feature_names[feature_idx[1]],
                ylabel=self.feature_names[feature_idx[0]],
            )

            plt.setp(ax.get_xticklabels(), rotation="vertical")

            heatmap_idx = np.unravel_index(pd_plot_idx, self.heatmaps_.shape)
            self.heatmaps_[heatmap_idx] = im
        else:
            from matplotlib import transforms

            XX, YY = np.meshgrid(feature_values[0], feature_values[1])
            Z = avg_preds[self.target_idx].T
            CS = ax.contour(XX, YY, Z, levels=Z_level, linewidths=0.5, colors="k")
            contour_idx = np.unravel_index(pd_plot_idx, self.contours_.shape)
            self.contours_[contour_idx] = ax.contourf(
                XX,
                YY,
                Z,
                levels=Z_level,
                vmax=Z_level[-1],
                vmin=Z_level[0],
                **contour_kw,
            )
            ax.clabel(CS, fmt="%2.2f", colors="k", fontsize=10, inline=True)

            trans = transforms.blended_transform_factory(ax.transData, ax.transAxes)
            # create the decile line for the vertical axis
            xlim, ylim = ax.get_xlim(), ax.get_ylim()
            vlines_idx = np.unravel_index(pd_plot_idx, self.deciles_vlines_.shape)
                0,
                0.05,
            )
            # create the decile line for the horizontal axis
            hlines_idx = np.unravel_index(pd_plot_idx, self.deciles_hlines_.shape)
            self.deciles_hlines_[hlines_idx] = ax.hlines(
                self.deciles[feature_idx[1]],
                0,
                0.05,
            )
            # reset xlim and ylim since they are overwritten by hlines and
            # vlines
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)

            # set xlabel if it is not already set
            if not ax.get_xlabel():
                ax.set_xlabel(self.feature_names[feature_idx[0]])
            ax.set_ylabel(self.feature_names[feature_idx[1]])

    def plot(
        self,
        *,
        ax=None,
        n_cols=3,
        bar_kw=None,
        heatmap_kw=None,
        pdp_lim=None,
    ):
        """Plot partial dependence plots.

                and a grid of partial dependence plots will be drawn within
                these bounds. The `n_cols` parameter controls the number of
                columns in the grid.
                plots will be drawn directly into these axes.
                as the single axes case.

            The maximum number of columns in the grid plot. Only active when
            `ax` is a single axes or `None`.

            Dict with keywords passed to the `matplotlib.pyplot.plot` call.
            For one-way partial dependence plots.

            Dict with keywords passed to the `matplotlib.pyplot.contourf`
            call for two-way partial dependence plots.

        bar_kw : dict, default=None
            Dict with keywords passed to the `matplotlib.pyplot.bar`
            call for one-way categorical partial dependence plots.

        heatmap_kw : dict, default=None
            Dict with keywords passed to the `matplotlib.pyplot.imshow`
            call for two-way categorical partial dependence plots.

        pdp_lim : dict, default=None
            Global min and max average predictions, such that all plots will have the
            same scale and y limits. `pdp_lim[1]` is the global min and max for single
            partial dependence curves. `pdp_lim[2]` is the global min and max for
            two-way partial dependence curves. If `None` (default), the limit will be
            inferred from the global minimum and maximum of all predictions.

        Returns
        -------
            Returns a :class:`~sklearn.inspection.PartialDependenceDisplay`
            object that contains the partial dependence plots.
        """

        check_matplotlib_support("plot_partial_dependence")
        from matplotlib.gridspec import GridSpecFromSubplotSpec

        if isinstance(self.kind, str):
            kind = [self.kind] * len(self.features)
        else:
            kind = self.kind

                (False,) if len(fx) == 1 else (False, False) for fx in self.features
            ]
        else:
            is_categorical = self.is_categorical

        if len(kind) != len(self.features):
                "When `kind` is provided as a list of strings, it should "
                "contain as many elements as `features`. `kind` contains "
                f"{len(kind)} element(s) and `features` contains "
                f"{len(self.features)} element(s)."
            )

        valid_kinds = {"average", "individual", "both"}
        if any([k not in valid_kinds for k in kind]):
                f"Values provided to `kind` must be one of: {valid_kinds!r} or a list"
                f" of such values. Currently, kind={self.kind!r}"
            )

        # Center results before plotting
        if not centered:
            pd_results_ = self.pd_results
        else:
            pd_results_ = []
            for kind_plot, pd_result in zip(kind, self.pd_results):
                current_results = {"grid_values": pd_result["grid_values"]}

                if kind_plot in ("individual", "both"):
                    preds = pd_result.individual
                    preds = preds - preds[self.target_idx, :, 0, None]
                    current_results["individual"] = preds

                if kind_plot in ("average", "both"):
                    avg_preds = pd_result.average
                    avg_preds = avg_preds - avg_preds[self.target_idx, 0, None]
                    current_results["average"] = avg_preds

                pd_results_.append(Bunch(**current_results))

        if pdp_lim is None:
            # get global min and max average predictions of PD grouped by plot type
            pdp_lim = {}
            for kind_plot, pdp in zip(kind, pd_results_):
                values = pdp["grid_values"]
                preds = pdp.average if kind_plot == "average" else pdp.individual
                min_pd = preds[self.target_idx].min()
                max_pd = preds[self.target_idx].max()

                # expand the limits to account so that the plotted lines do not touch
                # the edges of the plot
                span = max_pd - min_pd
                min_pd -= 0.05 * span
                max_pd += 0.05 * span

                n_fx = len(values)
                old_min_pd, old_max_pd = pdp_lim.get(n_fx, (min_pd, max_pd))
                min_pd = min(min_pd, old_min_pd)
                max_pd = max(max_pd, old_max_pd)
                pdp_lim[n_fx] = (min_pd, max_pd)

        if line_kw is None:
            line_kw = {}
        if ice_lines_kw is None:
            ice_lines_kw = {}
        if pd_line_kw is None:
            pd_line_kw = {}
        if bar_kw is None:
            bar_kw = {}
        if heatmap_kw is None:
            heatmap_kw = {}

        if contour_kw is None:
            contour_kw = {}
        default_contour_kws = {"alpha": 0.75}
        contour_kw = _validate_style_kwargs(default_contour_kws, contour_kw)

        n_features = len(self.features)
        is_average_plot = [kind_plot == "average" for kind_plot in kind]
        if all(is_average_plot):
            # only average plots are requested
            n_ice_lines = 0
            n_lines = 1
        else:
            # we need to determine the number of ICE samples computed
            ice_plot_idx = is_average_plot.index(False)
            n_ice_lines = self._get_sample_count(
                len(pd_results_[ice_plot_idx].individual[0])
            )
            if any([kind_plot == "both" for kind_plot in kind]):
                n_lines = n_ice_lines + 1  # account for the average line
            else:
                n_lines = n_ice_lines

        if isinstance(ax, plt.Axes):
            # If ax was set off, it has most likely been set to off
            # by a previous call to plot.
            if not ax.axison:
                    "The ax was already used in another plot "
                    "function, please set ax=display.axes_ "
                    "instead"
                )

            ax.set_axis_off()
            self.bounding_ax_ = ax
            self.figure_ = ax.figure

            n_cols = min(n_cols, n_features)
            n_rows = int(np.ceil(n_features / float(n_cols)))

            self.axes_ = np.empty((n_rows, n_cols), dtype=object)
            if all(is_average_plot):
                self.lines_ = np.empty((n_rows, n_cols), dtype=object)
            else:
                self.lines_ = np.empty((n_rows, n_cols, n_lines), dtype=object)
            self.contours_ = np.empty((n_rows, n_cols), dtype=object)
            self.bars_ = np.empty((n_rows, n_cols), dtype=object)
            self.heatmaps_ = np.empty((n_rows, n_cols), dtype=object)

            axes_ravel = self.axes_.ravel()

            gs = GridSpecFromSubplotSpec(
                n_rows, n_cols, subplot_spec=ax.get_subplotspec()
            )
            for i, spec in zip(range(n_features), gs):
                axes_ravel[i] = self.figure_.add_subplot(spec)

        else:  # array-like
            ax = np.asarray(ax, dtype=object)
            if ax.size != n_features:
                    "Expected ax to have {} axes, got {}".format(n_features, ax.size)
                )

            if ax.ndim == 2:
                n_cols = ax.shape[1]
            else:
                n_cols = None

            self.bounding_ax_ = None
            self.figure_ = ax.ravel()[0].figure
            self.axes_ = ax
                self.lines_ = np.empty_like(ax, dtype=object)
            else:
                self.lines_ = np.empty(ax.shape + (n_lines,), dtype=object)
            self.contours_ = np.empty_like(ax, dtype=object)
            self.bars_ = np.empty_like(ax, dtype=object)
            self.heatmaps_ = np.empty_like(ax, dtype=object)

        # create contour levels for two-way plots
        if 2 in pdp_lim:
            Z_level = np.linspace(*pdp_lim[2], num=8)

        self.deciles_vlines_ = np.empty_like(self.axes_, dtype=object)
        self.deciles_hlines_ = np.empty_like(self.axes_, dtype=object)

        for pd_plot_idx, (axi, feature_idx, cat, pd_result, kind_plot) in enumerate(
            zip(
                self.axes_.ravel(),
                self.features,
                pd_results_,
                kind,
            )
        ):
            avg_preds = None
            preds = None
            feature_values = pd_result["grid_values"]
            if kind_plot == "individual":
                preds = pd_result.individual
            elif kind_plot == "average":
                avg_preds = pd_result.average
            else:  # kind_plot == 'both'

            if len(feature_values) == 1:
                # define the line-style for the current plot
                default_line_kws = {
                    "color": "C0",
                    "label": "average" if kind_plot == "both" else None,
                }
                if kind_plot == "individual":
                    default_ice_lines_kws = {"alpha": 0.3, "linewidth": 0.5}
                    default_pd_lines_kws = {}
                elif kind_plot == "both":
                    # by default, we need to distinguish the average line from
                    # the individual lines via color and line style
                    default_ice_lines_kws = {
                        "alpha": 0.3,
                        "linewidth": 0.5,
                        "color": "tab:blue",
                    }
                    default_pd_lines_kws = {
                        "color": "tab:orange",
                        "linestyle": "--",
                    }
                else:
                    default_ice_lines_kws = {}

                default_ice_lines_kws = {**default_line_kws, **default_ice_lines_kws}
                default_pd_lines_kws = {**default_line_kws, **default_pd_lines_kws}

                line_kw = _validate_style_kwargs(default_line_kws, line_kw)

                ice_lines_kw = _validate_style_kwargs(
                    _validate_style_kwargs(default_ice_lines_kws, line_kw), ice_lines_kw
                )
                del ice_lines_kw["label"]

                pd_line_kw = _validate_style_kwargs(
                    _validate_style_kwargs(default_pd_lines_kws, line_kw), pd_line_kw
                )

                default_bar_kws = {"color": "C0"}
                bar_kw = _validate_style_kwargs(default_bar_kws, bar_kw)

                default_heatmap_kw = {}
                heatmap_kw = _validate_style_kwargs(default_heatmap_kw, heatmap_kw)

                self._plot_one_way_partial_dependence(
                    kind_plot,
                    preds,
                    avg_preds,
                    feature_values[0],
                    feature_idx,
                    n_ice_lines,
                    axi,
                    n_cols,
                    pd_plot_idx,
                    n_lines,
                    ice_lines_kw,
                    pd_line_kw,
                    cat[0],
                    bar_kw,
                    pdp_lim,
                )
            else:
                self._plot_two_way_partial_dependence(
                    feature_values,
                    axi,
                    Z_level,
                    contour_kw,
                    cat[0] and cat[1],
                    heatmap_kw,
                )

        self,
        *,
        features,
        deciles,
    ):

        cls,
        X,
        features,
        *,
        n_cols=3,
        ax=None,
    ):

        plots.

        .. note::

               ...                                                 [1, 2])

              (namely

        Returns
        -------

        See Also
        --------

        Examples
        --------
        <...>
        """

            if (
            ):
        else:

            )

            try:
                )
                ) from e
                )

            kind_ = [
            ]
            )

            ]
        else:
                    )
                ]
                ]
                ]
            else:
                )

                    )

                [
                    fx
                ]
            )
                    [
                    ]
                )
                    )

                    )

                    )
                )

                )

                )
                )

                X,
                fxs,
            )
        )

        n_tasks = (
        )
                )

        )
            ax=ax,
        )

        self,
        preds,
        ax,
    ):

        """
        )
            )
            )[0]

        self,
        ax,
        line_kw,
        bar_kw,
    ):

        """
        else:
            )[0]

        self,
        kind,
        preds,
        ax,
        n_cols,
        n_lines,
        bar_kw,
        pdp_lim,
    ):

        kind : str
        """

                ax,
                n_lines,
            )

            else:
                ax,
                bar_kw,
            )

                0,
                0.05,
            )

        else:

        self,
        ax,
        Z_level,
    ):

        """

            text = None

            ax.set(
            )

        else:

                XX,
                YY,
                Z,
            )

                0,
                0.05,
            )
                0,
                0.05,
            )
            # vlines

    def plot(
        self,
        *,
        ax=None,
        n_cols=3,
    ):

        Returns
        -------
        """

        else:

            ]
        else:

            )

            )

        else:

            bar_kw = {}

            n_lines = 1
        else:
            )
            else:

                    "instead"
                )

            else:

            )

                )

            else:

            else:

            zip(
                kind,
            )
        ):

                }
                    }
                    }
                else:

                )

                )

                    preds,
                    axi,
                    n_cols,
                    n_lines,
                    cat[0],
                    bar_kw,
                    pdp_lim,
                )
            else:
                    axi,
                    Z_level,
                )

def load_iris_2d_scaled():
    X = scale(X)[:, :2]
    return X, y

def test_input_data_dimension(pyplot):
    """Check that we raise an error when `X` does not have exactly 2 features."""
    X, y = make_classification(n_samples=10, n_features=4, random_state=0)

    msg = "n_features must be equal to 2. Got 4 instead."
        DecisionBoundaryDisplay.from_estimator(estimator=clf, X=X)

def test_check_boundary_response_method_error():
    """Check error raised for multi-output multi-class classifiers by
    `_check_boundary_response_method`.
    """

    class MultiLabelClassifier:
        classes_ = [np.array([0, 1]), np.array([0, 1])]

    err_msg = "Multi-label and multi-output multi-class classifiers are not supported"
        _check_boundary_response_method(MultiLabelClassifier(), "predict", None)

def test_check_boundary_response_method(
    estimator, response_method, class_of_interest, expected_prediction_method
):
    """Check the behaviour of `_check_boundary_response_method` for the supported
    cases.
    """
    prediction_method = _check_boundary_response_method(
        estimator, response_method, class_of_interest
    )
    assert prediction_method == expected_prediction_method

def test_multiclass_predict(pyplot):
    """Check multiclass `response=predict` gives expected results."""
    grid_resolution = 10
    eps = 1.0
    X, y = make_classification(n_classes=3, n_informative=3, random_state=0)
    X = X[:, [0, 1]]
    lr = LogisticRegression(random_state=0).fit(X, y)

    disp = DecisionBoundaryDisplay.from_estimator(
        lr, X, response_method="predict", grid_resolution=grid_resolution, eps=1.0
    )

    x0_min, x0_max = X[:, 0].min() - eps, X[:, 0].max() + eps
    x1_min, x1_max = X[:, 1].min() - eps, X[:, 1].max() + eps
    xx0, xx1 = np.meshgrid(
        np.linspace(x0_min, x0_max, grid_resolution),
        np.linspace(x1_min, x1_max, grid_resolution),
    )
    response = lr.predict(np.c_[xx0.ravel(), xx1.ravel()])
    assert_allclose(disp.response, response.reshape(xx0.shape))
    assert_allclose(disp.xx0, xx0)
    assert_allclose(disp.xx1, xx1)

def test_input_validation_errors(pyplot, kwargs, error_msg, fitted_clf):
    """Check input validation from_estimator."""
        DecisionBoundaryDisplay.from_estimator(fitted_clf, X, **kwargs)

def test_input_validation_errors_multiclass_colors(pyplot, kwargs, error_msg):
    """Check input validation for `multiclass_colors` in `from_estimator`."""
    X, y = load_iris_2d_scaled()
        DecisionBoundaryDisplay.from_estimator(clf, X, **kwargs)

def test_display_plot_input_error(pyplot, fitted_clf):
    """Check input validation for `plot`."""
    disp = DecisionBoundaryDisplay.from_estimator(fitted_clf, X, grid_resolution=5)

    with pytest.raises(ValueError, match="plot_method must be 'contourf'"):
        disp.plot(plot_method="hello_world")

def test_decision_boundary_display_classifier(
    pyplot, fitted_clf, response_method, plot_method
):
    """Check that decision boundary is correct."""
    fig, ax = pyplot.subplots()
    eps = 2.0
        fitted_clf,
        X,
        grid_resolution=5,
        response_method=response_method,
        plot_method=plot_method,
        eps=eps,
        ax=ax,
    )
    assert isinstance(disp.surface_, pyplot.matplotlib.contour.QuadContourSet)
    assert disp.figure_ == fig

    x0, x1 = X[:, 0], X[:, 1]

    x0_min, x0_max = x0.min() - eps, x0.max() + eps
    x1_min, x1_max = x1.min() - eps, x1.max() + eps

    assert disp.xx0.min() == pytest.approx(x0_min)
    assert disp.xx0.max() == pytest.approx(x0_max)
    assert disp.xx1.min() == pytest.approx(x1_min)
    assert disp.xx1.max() == pytest.approx(x1_max)

    fig2, ax2 = pyplot.subplots()
    # change plotting method for second plot
    disp.plot(plot_method="pcolormesh", ax=ax2, shading="auto")
    assert isinstance(disp.surface_, pyplot.matplotlib.collections.QuadMesh)
    assert disp.ax_ == ax2
    assert disp.figure_ == fig2

def test_decision_boundary_display_outlier_detector(
    pyplot, response_method, plot_method
):
    """Check that decision boundary is correct for outlier detector."""
    eps = 2.0
    outlier_detector = IsolationForest(random_state=0).fit(X, y)
        outlier_detector,
        X,
        eps=eps,
        ax=ax,
    )

def test_decision_boundary_display_regressor(pyplot, response_method, plot_method):
    """Check that we can display the decision boundary for a regressor."""
    X = X[:, :2]
    tree = DecisionTreeRegressor().fit(X, y)
    eps = 2.0
        tree,
        X,
        ax=ax,
        eps=eps,
    )

def test_error_bad_response(pyplot, response_method, msg):
    """Check errors for bad response."""

        DecisionBoundaryDisplay.from_estimator(clf, X, response_method=response_method)

def test_multilabel_classifier_error(pyplot, response_method):
    """Check that multilabel classifier raises correct error."""
    X, y = make_multilabel_classification(random_state=0)
    X = X[:, :2]
    tree = DecisionTreeClassifier().fit(X, y)

    msg = "Multi-label and multi-output multi-class classifiers are not supported"
        DecisionBoundaryDisplay.from_estimator(
            tree,
            X,
        )

def test_multi_output_multi_class_classifier_error(pyplot, response_method):
    """Check that multi-output multi-class classifier raises correct error."""
    X = np.asarray([[0, 1], [1, 2]])
    y = np.asarray([["tree", "cat"], ["cat", "tree"]])

            tree,
            X,
        )

def test_multioutput_regressor_error(pyplot):
    """Check that multioutput regressor raises correct error."""
    y = np.asarray([[0, 1], [4, 1]])
    with pytest.raises(ValueError, match="Multi-output regressors are not supported"):
        DecisionBoundaryDisplay.from_estimator(tree, X, response_method="predict")

def test_regressor_unsupported_response(pyplot, response_method):
    X = X[:, :2]
    err_msg = "should either be a classifier to be used with response_method"
        DecisionBoundaryDisplay.from_estimator(tree, X, response_method=response_method)

def test_dataframe_labels_used(pyplot, fitted_clf):
    """Check that column names are used for pandas."""
    df = pd.DataFrame(X, columns=["col_x", "col_y"])

    # pandas column names are used by default
    disp = DecisionBoundaryDisplay.from_estimator(fitted_clf, df, ax=ax)
    assert ax.get_xlabel() == "col_x"
    assert ax.get_ylabel() == "col_y"

    # second call to plot will have the names
    disp.plot(ax=ax)

    # axes with a label will not get overridden
    ax.set(xlabel="hello", ylabel="world")
    assert ax.get_xlabel() == "hello"
    assert ax.get_ylabel() == "world"

    # labels get overridden only if provided to the `plot` method
    disp.plot(ax=ax, xlabel="overwritten_x", ylabel="overwritten_y")
    assert ax.get_xlabel() == "overwritten_x"
    assert ax.get_ylabel() == "overwritten_y"

    # labels do not get inferred if provided to `from_estimator`
        fitted_clf, df, ax=ax, xlabel="overwritten_x", ylabel="overwritten_y"
    )

def test_string_target(pyplot):
    """Check that decision boundary works with classifiers trained on string labels."""
    X = iris.data[:, [0, 1]]

    # Use strings as target
    y = iris.target_names[iris.target]
    log_reg = LogisticRegression().fit(X, y)

    DecisionBoundaryDisplay.from_estimator(
        log_reg,
        X,
        response_method="predict",
    )

def test_dataframe_support(pyplot, constructor_name):
    """Check that passing a dataframe at fit and to the Display does not
    raise warnings.

    * https://github.com/scikit-learn/scikit-learn/issues/23311
    * https://github.com/scikit-learn/scikit-learn/issues/28717
    """
        X, constructor_name=constructor_name, columns_name=["col_x", "col_y"]
    )
    estimator = LogisticRegression().fit(df, y)

        # no warnings linked to feature names validation should be raised
        DecisionBoundaryDisplay.from_estimator(estimator, df, response_method="predict")

def test_class_of_interest_binary(pyplot, response_method):
    """Check the behaviour of passing `class_of_interest` for plotting the output of
    `predict_proba` and `decision_function` in the binary case.
    """
    X = iris.data[:100, :2]
    y = iris.target[:100]
    assert_array_equal(np.unique(y), [0, 1])

    # We will check that `class_of_interest=None` is equivalent to
    # `class_of_interest=estimator.classes_[1]`
    disp_default = DecisionBoundaryDisplay.from_estimator(
        X,
    )
    disp_class_1 = DecisionBoundaryDisplay.from_estimator(
        X,
        class_of_interest=estimator.classes_[1],
    )

    assert_allclose(disp_default.response, disp_class_1.response)

    # we can check that `_get_response_values` modifies the response when targeting
    # the other class, i.e. 1 - p(y=1|x) for `predict_proba` and -decision_function
    # for `decision_function`.
    disp_class_0 = DecisionBoundaryDisplay.from_estimator(
        X,
        class_of_interest=estimator.classes_[0],
    )

    if response_method == "predict_proba":
        assert_allclose(disp_default.response, 1 - disp_class_0.response)
    else:
        assert response_method == "decision_function"
        assert_allclose(disp_default.response, -disp_class_0.response)

def test_class_of_interest_multiclass(pyplot, response_method):
    `predict_proba` and `decision_function` in the multiclass case.
    """
    X = iris.data[:, :2]
    y = iris.target  # the target are numerical labels
    class_of_interest_idx = 2

        X,
        class_of_interest=class_of_interest_idx,
    )

    # we will check that we plot the expected values as response
    grid = np.concatenate([disp.xx0.reshape(-1, 1), disp.xx1.reshape(-1, 1)], axis=1)
    response = getattr(estimator, response_method)(grid)[:, class_of_interest_idx]
    assert_allclose(response.reshape(*disp.response.shape), disp.response)

    # make the same test but this time using target as strings

        X,
        class_of_interest=iris.target_names[class_of_interest_idx],
    )

    # check that we raise an error for unknown labels
    # this test should already be handled in `_get_response_values` but we can have this
    # test here as well
    err_msg = "class_of_interest=2 is not a valid label: It should be one of"
            X,
            class_of_interest=class_of_interest_idx,
        )

def test_multiclass_plot_max_class(pyplot, response_method):
    """Check plot correct when plotting max multiclass class."""

    # In matplotlib < v3.5, default value of `pcolormesh(shading)` is 'flat', which
    # results in the last row and column being dropped. Thus older versions produce
    # a 99x99 grid, while newer versions produce a 100x100 grid.
    if parse_version(mpl.__version__) < parse_version("3.5"):
        pytest.skip("`pcolormesh` in Matplotlib >= 3.5 gives smaller grid size.")

        clf,
        X,
        plot_method="pcolormesh",
    )

    response = getattr(clf, response_method)(grid).reshape(*disp.response.shape)
    assert_allclose(response, disp.response)

    assert len(disp.surface_) == len(clf.classes_)
    # Get which class has highest response and check it is plotted
    highest_class = np.argmax(response, axis=2)
    for idx, quadmesh in enumerate(disp.surface_):
        # Note quadmesh mask is True (i.e. masked) when `idx` is NOT the highest class
            highest_class != idx,
            quadmesh.get_array().mask.reshape(*highest_class.shape),
        )

def test_multiclass_colors_cmap(pyplot, plot_method, multiclass_colors):
    """Check correct cmap used for all `multiclass_colors` inputs."""

            "Matplotlib >= 3.5 is needed for `==` to check equivalence of colormaps"
        )

        clf,
        X,
        multiclass_colors=multiclass_colors,
    )

    if multiclass_colors == "plasma":
        colors = mpl.pyplot.get_cmap(multiclass_colors, len(clf.classes_)).colors
    else:
        colors = [mpl.colors.to_rgba(color) for color in multiclass_colors]

    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)
    ]

    for idx, quad in enumerate(disp.surface_):
        assert quad.cmap == cmaps[idx]

def test_multiclass_plot_max_class_cmap_kwarg(pyplot):
    """Check `cmap` kwarg ignored when using plotting max multiclass class."""

    msg = (
        "Plotting max class of multiclass 'decision_function' or 'predict_proba', "
        "thus 'multiclass_colors' used and 'cmap' kwarg ignored."
    )
        DecisionBoundaryDisplay.from_estimator(clf, X, cmap="viridis")

def test_subclass_named_constructors_return_type_is_subclass(pyplot):

    """

    class SubclassOfDisplay(DecisionBoundaryDisplay):
        pass

    curve = SubclassOfDisplay.from_estimator(estimator=clf, X=X)

        pass

def diabetes():
    # diabetes dataset, subsampled for speed
    data = load_diabetes()
    data.data = data.data[:50]
    data.target = data.target[:50]

def clf_diabetes(diabetes):
    return clf

def custom_values_helper(feature, grid_resolution):
    return np.linspace(
        *mquantiles(feature, (0.05, 0.95), axis=0), num=grid_resolution, endpoint=True
    )

def test_plot_partial_dependence(
    use_custom_values,
    grid_resolution,
    pyplot,
    clf_diabetes,
    diabetes,
):
    # Test partial dependence plot function.
    # Use columns 0 & 2 as 1 is not quantitative (sex)
    feature_names = diabetes.feature_names
        custom_values = {
            0: custom_values_helper(diabetes.data[:, 0], grid_resolution),
            2: custom_values_helper(diabetes.data[:, 2], grid_resolution),
        }
    disp = PartialDependenceDisplay.from_estimator(
        clf_diabetes,
        diabetes.data,
        [0, 2, (0, 2)],
        contour_kw={"cmap": "jet"},
    )
    fig = pyplot.gcf()
    axs = fig.get_axes()
    assert disp.figure_ is fig
    assert len(axs) == 4

    assert disp.bounding_ax_ is not None
    assert disp.axes_.shape == (1, 3)
    assert disp.lines_.shape == (1, 3)
    assert disp.contours_.shape == (1, 3)
    assert disp.deciles_vlines_.shape == (1, 3)
    assert disp.deciles_hlines_.shape == (1, 3)

    assert disp.lines_[0, 2] is None
    assert disp.contours_[0, 0] is None
    assert disp.contours_[0, 1] is None

    # deciles lines: always show on xaxis, only show on yaxis if 2-way PDP
        assert disp.deciles_vlines_[0, i] is not None
    assert disp.deciles_hlines_[0, 0] is None
    assert disp.deciles_hlines_[0, 1] is None
    assert disp.deciles_hlines_[0, 2] is not None

    assert disp.features == [(0,), (2,), (0, 2)]
    assert np.all(disp.feature_names == feature_names)
    assert len(disp.deciles) == 2
    for i in [0, 2]:
            disp.deciles[i],
            mquantiles(diabetes.data[:, i], prob=np.arange(0.1, 1.0, 0.1)),
        )

    single_feature_positions = [(0, (0, 0)), (2, (0, 1))]
    expected_ylabels = ["Partial dependence", ""]

    for i, (feat_col, pos) in enumerate(single_feature_positions):
        ax = disp.axes_[pos]
        assert ax.get_ylabel() == expected_ylabels[i]
        assert ax.get_xlabel() == diabetes.feature_names[feat_col]

        line = disp.lines_[pos]

        avg_preds = disp.pd_results[i]
        assert avg_preds.average.shape == (1, grid_resolution)
        target_idx = disp.target_idx

        line_data = line.get_data()
        assert_allclose(line_data[0], avg_preds["grid_values"][0])
        assert_allclose(line_data[1], avg_preds.average[target_idx].ravel())

    # two feature position
    ax = disp.axes_[0, 2]
    coutour = disp.contours_[0, 2]
    assert coutour.get_cmap().name == "jet"
    assert ax.get_xlabel() == diabetes.feature_names[0]
    assert ax.get_ylabel() == diabetes.feature_names[2]

def test_plot_partial_dependence_kind(
    pyplot,
    kind,
    centered,
    subsample,
    shape,
    diabetes,
):
        [0, 1, 2],
        centered=centered,
    )

    assert disp.lines_.shape == shape

    assert disp.contours_[0, 2] is None

    if centered:
        assert all([ln._y[0] == 0.0 for ln in disp.lines_.ravel() if ln is not None])
    else:
        assert all([ln._y[0] != 0.0 for ln in disp.lines_.ravel() if ln is not None])

def test_plot_partial_dependence_str_features(
    pyplot,
    diabetes,
    input_type,
    feature_names_type,
):
    age = diabetes.data[:, diabetes.feature_names.index("age")]
    bmi = diabetes.data[:, diabetes.feature_names.index("bmi")]

    if input_type == "dataframe":
        X = pd.DataFrame(diabetes.data, columns=diabetes.feature_names)
    elif input_type == "list":
        X = diabetes.data.tolist()
    else:

    if feature_names_type is None:
        feature_names = None
    else:
        feature_names = _convert_container(diabetes.feature_names, feature_names_type)

    grid_resolution = 25
            "age": custom_values_helper(age, grid_resolution),
            "bmi": custom_values_helper(bmi, grid_resolution),
        }
    # check with str features and array feature names and single column
        X,
        [("age", "bmi"), "bmi"],
        n_cols=1,
        line_kw={"alpha": 0.8},
    )
    assert len(axs) == 3

    assert disp.axes_.shape == (2, 1)
    assert disp.lines_.shape == (2, 1)
    assert disp.contours_.shape == (2, 1)
    assert disp.deciles_vlines_.shape == (2, 1)
    assert disp.deciles_hlines_.shape == (2, 1)

    assert disp.lines_[0, 0] is None
    assert disp.deciles_vlines_[0, 0] is not None
    assert disp.deciles_hlines_[0, 0] is not None
    assert disp.contours_[1, 0] is None
    assert disp.deciles_hlines_[1, 0] is None
    assert disp.deciles_vlines_[1, 0] is not None

    # line
    ax = disp.axes_[1, 0]
    assert ax.get_xlabel() == "bmi"
    assert ax.get_ylabel() == "Partial dependence"

    line = disp.lines_[1, 0]
    avg_preds = disp.pd_results[1]
    target_idx = disp.target_idx
    assert line.get_alpha() == 0.8

    line_data = line.get_data()
    assert_allclose(line_data[0], avg_preds["grid_values"][0])
    assert_allclose(line_data[1], avg_preds.average[target_idx].ravel())

    # contour
    ax = disp.axes_[0, 0]
    assert ax.get_xlabel() == "age"
    assert ax.get_ylabel() == "bmi"

def test_plot_partial_dependence_custom_axes(
    use_custom_values, pyplot, clf_diabetes, diabetes
):
    fig, (ax1, ax2) = pyplot.subplots(1, 2)

        }

        ["age", ("age", "bmi")],
        feature_names=diabetes.feature_names,
        ax=[ax1, ax2],
    )
    assert fig is disp.figure_
    assert disp.bounding_ax_ is None
    assert disp.axes_.shape == (2,)
    assert disp.axes_[0] is ax1
    assert disp.axes_[1] is ax2

    ax = disp.axes_[0]

    line = disp.lines_[0]
    avg_preds = disp.pd_results[0]

    # contour
    ax = disp.axes_[1]

def test_plot_partial_dependence_passing_numpy_axes(
    pyplot,
    diabetes,
    kind,
    lines,
):

        }

    disp1 = PartialDependenceDisplay.from_estimator(
        ["age", "bmi"],
    )
    assert disp1.axes_.shape == (1, 2)
    assert disp1.axes_[0, 0].get_ylabel() == "Partial dependence"
    assert disp1.axes_[0, 1].get_ylabel() == ""
    assert len(disp1.axes_[0, 0].get_lines()) == lines
    assert len(disp1.axes_[0, 1].get_lines()) == lines

    lr.fit(diabetes.data, diabetes.target)

    disp2 = PartialDependenceDisplay.from_estimator(
        lr,
        ax=disp1.axes_,
    )

    assert np.all(disp1.axes_ == disp2.axes_)
    assert len(disp2.axes_[0, 0].get_lines()) == 2 * lines
    assert len(disp2.axes_[0, 1].get_lines()) == 2 * lines

def test_plot_partial_dependence_incorrent_num_axes(
    pyplot,
    diabetes,
    nrows,
    ncols,
):
    fig, axes = pyplot.subplots(nrows, ncols)
    axes_formats = [list(axes.ravel()), tuple(axes.ravel()), axes]

    msg = "Expected ax to have 2 axes, got {}".format(nrows * ncols)

        }

        }

    )

    for ax_format in axes_formats:
            PartialDependenceDisplay.from_estimator(
                clf_diabetes,
                diabetes.data,
                ["age", "bmi"],
                feature_names=diabetes.feature_names,
                ax=ax_format,
            )

        # with axes object
            disp.plot(ax=ax_format)

def test_plot_partial_dependence_with_same_axes(
):
    # The first call to plot_partial_dependence will create two new axes to
    # place in the space of the passed in axes, which results in a total of
    # three axes in the figure.
    # Currently the API does not allow for the second call to
    # plot_partial_dependence to use the same axes again, because it will
    # create two new axes in the space resulting in five axes. To get the
    # expected behavior one needs to pass the generated axes into the second
    # call:
    # disp1 = plot_partial_dependence(...)
    # disp2 = plot_partial_dependence(..., ax=disp1.axes_)

        }

    PartialDependenceDisplay.from_estimator(
        ax=ax,
    )

    msg = (
        "The ax was already used in another plot function, please set "
        "ax=display.axes_ instead"
    )

        PartialDependenceDisplay.from_estimator(
            clf_diabetes,
            diabetes.data,
            ["age", "bmi"],
            grid_resolution=grid_resolution,
            feature_names=diabetes.feature_names,
            custom_values=custom_values,
            ax=ax,
        )

def test_plot_partial_dependence_feature_name_reuse(
):
    # second call to plot does not change the feature names from the first
    # call

            1: custom_values_helper(diabetes.data[:, 1], grid_resolution),
        }

        [0, 1],
    )

        [0, 1],
        ax=disp.axes_,
    )

    for i, ax in enumerate(disp.axes_.ravel()):
        assert ax.get_xlabel() == feature_names[i]

def test_plot_partial_dependence_multiclass(use_custom_values, pyplot):
    clf_int = GradientBoostingClassifier(n_estimators=10, random_state=1)

            0: custom_values_helper(iris.data[:, 0], grid_resolution),
            1: custom_values_helper(iris.data[:, 1], grid_resolution),
        }

    # Test partial dependence plot function on multi-class input.
    clf_int.fit(iris.data, iris.target)

    disp_target_0 = PartialDependenceDisplay.from_estimator(
        clf_int,
        iris.data,
        [0, 1],
        target=0,
    )
    assert disp_target_0.figure_ is pyplot.gcf()
    assert disp_target_0.axes_.shape == (1, 2)
    assert disp_target_0.lines_.shape == (1, 2)
    assert disp_target_0.contours_.shape == (1, 2)
    assert disp_target_0.deciles_vlines_.shape == (1, 2)
    assert disp_target_0.deciles_hlines_.shape == (1, 2)
    assert all(c is None for c in disp_target_0.contours_.flat)
    assert disp_target_0.target_idx == 0

    # now with symbol labels
    clf_symbol = GradientBoostingClassifier(n_estimators=10, random_state=1)
    clf_symbol.fit(iris.data, target)

    disp_symbol = PartialDependenceDisplay.from_estimator(
        clf_symbol,
        [0, 1],
        target="setosa",
    )
    assert disp_symbol.figure_ is pyplot.gcf()
    assert disp_symbol.axes_.shape == (1, 2)
    assert disp_symbol.lines_.shape == (1, 2)
    assert disp_symbol.contours_.shape == (1, 2)
    assert disp_symbol.deciles_vlines_.shape == (1, 2)
    assert disp_symbol.deciles_hlines_.shape == (1, 2)
    assert all(c is None for c in disp_symbol.contours_.flat)
    assert disp_symbol.target_idx == 0

    for int_result, symbol_result in zip(
        disp_target_0.pd_results, disp_symbol.pd_results
    ):
        assert_allclose(int_result.average, symbol_result.average)
        assert_allclose(int_result["grid_values"], symbol_result["grid_values"])

    # check that the pd plots are different for another target

    disp_target_1 = PartialDependenceDisplay.from_estimator(
        clf_int,
        [0, 3],
        target=1,
    )
    target_0_data_y = disp_target_0.lines_[0, 0].get_data()[1]
    target_1_data_y = disp_target_1.lines_[0, 0].get_data()[1]
    assert any(target_0_data_y != target_1_data_y)

def test_plot_partial_dependence_multioutput(use_custom_values, pyplot, target):
    # Test partial dependence plot function on multi-output input.
    X, y = multioutput_regression_data
    clf = LinearRegression().fit(X, y)

            0: custom_values_helper(X[:, 0], grid_resolution),
            1: custom_values_helper(X[:, 1], grid_resolution),
        }

        clf,
        X,
        [0, 1],
    )
    assert disp.target_idx == target

    positions = [(0, 0), (0, 1)]
    expected_label = ["Partial dependence", ""]

    for i, pos in enumerate(positions):
        assert ax.get_ylabel() == expected_label[i]
        assert ax.get_xlabel() == f"x{i}"

def test_plot_partial_dependence_dataframe(pyplot, clf_diabetes, diabetes):
    df = pd.DataFrame(diabetes.data, columns=diabetes.feature_names)

        df,
        ["bp", "s1"],
        feature_names=df.columns.tolist(),
    )

def test_plot_partial_dependence_error(pyplot, data, params, err_msg):
    X, y = data

        PartialDependenceDisplay.from_estimator(estimator, X, **params)

def test_plot_partial_dependence_multiclass_error(pyplot, params, err_msg):
    clf = GradientBoostingClassifier(n_estimators=10, random_state=1)

        PartialDependenceDisplay.from_estimator(clf, iris.data, **params)

def test_plot_partial_dependence_does_not_override_ylabel(
    pyplot, clf_diabetes, diabetes
):
    # Non-regression test to be sure to not override the ylabel if it has been
    # See https://github.com/scikit-learn/scikit-learn/issues/15772
    _, axes = pyplot.subplots(1, 2)
    axes[0].set_ylabel("Hello world")
        clf_diabetes, diabetes.data, [0, 1], ax=axes
    )

    assert axes[0].get_ylabel() == "Hello world"
    assert axes[1].get_ylabel() == "Partial dependence"

def test_plot_partial_dependence_with_categorical(
    pyplot, categorical_features, array_type
):
    X = [[1, 1, "A"], [2, 0, "C"], [3, 2, "B"]]
    column_name = ["col_A", "col_B", "col_C"]
    X = _convert_container(X, array_type, columns_name=column_name)
    y = np.array([1.2, 0.5, 0.45]).T

    preprocessor = make_column_transformer((OneHotEncoder(), categorical_features))
    model = make_pipeline(preprocessor, LinearRegression())

    # single feature
        model,
        X,
        features=["col_C"],
        feature_names=column_name,
        categorical_features=categorical_features,
    )

    assert disp.figure_ is pyplot.gcf()
    assert disp.bars_.shape == (1, 1)
    assert disp.bars_[0][0] is not None
    assert disp.lines_.shape == (1, 1)
    assert disp.lines_[0][0] is None
    assert disp.contours_.shape == (1, 1)
    assert disp.contours_[0][0] is None
    assert disp.deciles_vlines_.shape == (1, 1)
    assert disp.deciles_vlines_[0][0] is None
    assert disp.deciles_hlines_.shape == (1, 1)
    assert disp.deciles_hlines_[0][0] is None
    assert disp.axes_[0, 0].get_legend() is None

    # interaction between two features
        model,
        X,
        features=[("col_A", "col_C")],
    )

    assert disp.bars_[0][0] is None

def test_plot_partial_dependence_legend(pyplot):
        {
            "col_A": ["A", "B", "C"],
            "col_B": [1.0, 0.0, 2.0],
            "col_C": ["C", "B", "A"],
        }
    )

    categorical_features = ["col_A", "col_C"]

        model,
        X,
        features=["col_B", "col_C"],
        kind=["both", "average"],
    )

    legend_text = disp.axes_[0, 0].get_legend().get_texts()
    assert len(legend_text) == 1
    assert legend_text[0].get_text() == "average"
    assert disp.axes_[0, 1].get_legend() is None

def test_plot_partial_dependence_subsampling(
    pyplot,
    diabetes,
    kind,
    expected_shape,
):
    # check that the subsampling is properly working
    # https://github.com/scikit-learn/scikit-learn/pull/18359
    matplotlib = pytest.importorskip("matplotlib")

        }

        subsample=20,
    )

    assert disp1.lines_.shape == expected_shape
        [isinstance(line, matplotlib.lines.Line2D) for line in disp1.lines_.ravel()]
    )

def test_partial_dependence_overwrite_labels(
    pyplot,
    diabetes,
    kind,
    line_kw,
    label,
):
    """Test that make sure that we can overwrite the label of the PDP plot"""
        [0, 2],
        grid_resolution=25,
        line_kw=line_kw,
    )

    for ax in disp.axes_.ravel():
        if label is None:
            assert ax.get_legend() is None
        else:
            legend_text = ax.get_legend().get_texts()
            assert len(legend_text) == 1
            assert legend_text[0].get_text() == label

def test_grid_resolution_with_categorical(pyplot, categorical_features, array_type):
    """Check that we raise a ValueError when the grid_resolution is too small
    respect to the number of categories in the categorical features targeted.
    """
    X = [["A", 1, "A"], ["B", 0, "C"], ["C", 2, "B"]]

    err_msg = (
        "resolution of the computed grid is less than the minimum number of categories"
    )
            model,
            X,
            features=["col_C"],
            feature_names=column_name,
            grid_resolution=2,
        )

def test_partial_dependence_plot_limits_one_way(
    pyplot, clf_diabetes, diabetes, kind, centered
):
    """Check that the PD limit on the plots are properly set on one-way plots."""
        features=(0, 1),
    )

    range_pd = np.array([-1, 1], dtype=np.float64)
    for pd in disp.pd_results:
        if "average" in pd:
            pd["average"][...] = range_pd[1]
            pd["average"][0, 0] = range_pd[0]
        if "individual" in pd:
            pd["individual"][...] = range_pd[1]
            pd["individual"][0, 0, 0] = range_pd[0]

    disp.plot(centered=centered)
    # check that we anchor to zero x-axis when centering
    y_lim = range_pd - range_pd[0] if centered else range_pd
    padding = 0.05 * (y_lim[1] - y_lim[0])
    y_lim[0] -= padding
    y_lim[1] += padding
        assert_allclose(ax.get_ylim(), y_lim)

def test_partial_dependence_plot_limits_two_way(
    pyplot, clf_diabetes, diabetes, centered
):
    """Check that the PD limit on the plots are properly set on two-way plots."""
        features=[(0, 1)],
    )

        pd["average"][...] = range_pd[1]
        pd["average"][0, 0] = range_pd[0]

    contours = disp.contours_[0, 0]
    levels = range_pd - range_pd[0] if centered else range_pd

    padding = 0.05 * (levels[1] - levels[0])
    levels[0] -= padding
    levels[1] += padding
    expect_levels = np.linspace(*levels, num=8)
    assert_allclose(contours.levels, expect_levels)

def test_partial_dependence_kind_list(
    pyplot,
    diabetes,
):
    """Check that we can provide a list of strings to kind parameter."""

        features=[0, 2, (1, 2)],
        grid_resolution=20,
        kind=["both", "both", "average"],
    )

    for idx in [0, 1]:
            [
                isinstance(line, matplotlib.lines.Line2D)
                for line in disp.lines_[0, idx].ravel()
            ]
        )
        assert disp.contours_[0, idx] is None

    assert disp.contours_[0, 2] is not None
    assert all([line is None for line in disp.lines_[0, 2].ravel()])

def test_partial_dependence_kind_error(
    pyplot,
    diabetes,
    features,
    kind,
):
    """Check that we raise an informative error when 2-way PD is requested
    together with 1-way PD/ICE"""
        "ICE plot cannot be rendered for 2-way feature interactions. 2-way "
        "feature interactions mandates PD plots using the 'average' kind"
    )
    with pytest.raises(ValueError, match=warn_msg):
            grid_resolution=20,
        )

def test_plot_partial_dependence_lines_kw(
    pyplot,
    diabetes,
    line_kw,
    pd_line_kw,
    ice_lines_kw,
    expected_colors,
):
    """Check that passing `pd_line_kw` and `ice_lines_kw` will act on the
    specific lines in the plot.
    """

        [0, 2],
        n_cols=2,
        kind="both",
        pd_line_kw=pd_line_kw,
        ice_lines_kw=ice_lines_kw,
    )

    line = disp.lines_[0, 0, -1]
    assert line.get_color() == expected_colors[0], (
        f"{line.get_color()}!={expected_colors[0]}\n{line_kw} and {pd_line_kw}"
    )
    if pd_line_kw is not None:
        if "linestyle" in pd_line_kw:
            assert line.get_linestyle() == pd_line_kw["linestyle"]
        elif "ls" in pd_line_kw:
            assert line.get_linestyle() == pd_line_kw["ls"]
    else:
        assert line.get_linestyle() == "--"

    line = disp.lines_[0, 0, 0]
    assert line.get_color() == expected_colors[1], (
        f"{line.get_color()}!={expected_colors[1]}"
    )
    if ice_lines_kw is not None:
        if "linestyle" in ice_lines_kw:
            assert line.get_linestyle() == ice_lines_kw["linestyle"]
        elif "ls" in ice_lines_kw:
            assert line.get_linestyle() == ice_lines_kw["ls"]
    else:
        assert line.get_linestyle() == "-"

def test_partial_dependence_display_wrong_len_kind(
    pyplot,
    diabetes,
):
    """Check that we raise an error when `kind` is a list with a wrong length.

    This case can only be triggered using the `PartialDependenceDisplay.from_estimator`
    method.
    """
        features=[0, 2],
        kind="average",  # len(kind) != len(features)
    )

    # alter `kind` to be a list with a length different from length of `features`
    disp.kind = ["average"]
    err_msg = (
        r"When `kind` is provided as a list of strings, it should contain as many"
        r" elements as `features`. `kind` contains 1 element\(s\) and `features`"
        r" contains 2 element\(s\)."
    )
        disp.plot()

def test_partial_dependence_display_kind_centered_interaction(
    pyplot,
    kind,
    diabetes,
):
    """Check that we properly center ICE and PD when passing kind as a string and as a
    list."""
        [0, 1],
        centered=True,
        subsample=5,
    )

    assert all([ln._y[0] == 0.0 for ln in disp.lines_.ravel() if ln is not None])

def test_partial_dependence_display_with_constant_sample_weight(
    pyplot,
    diabetes,
):
    """Check that the utilization of a constant sample weight maintains the
    standard behavior.
    """
        [0, 1],
    )

    sample_weight = np.ones_like(diabetes.target)
    disp_sw = PartialDependenceDisplay.from_estimator(
        [0, 1],
    )

        disp.pd_results[0]["average"], disp_sw.pd_results[0]["average"]
    )

def test_subclass_named_constructors_return_type_is_subclass(
    pyplot, diabetes, clf_diabetes
):

    """

    class SubclassOfDisplay(PartialDependenceDisplay):
        pass

    curve = SubclassOfDisplay.from_estimator(
    )

        pass

def _get_sys_info():
    """System information

    Returns
    -------
    sys_info : dict
        system and Python version information

    """
    python = sys.version.replace("\n", " ")

    blob = [
        ("python", python),
        ("executable", sys.executable),
        ("machine", platform.platform()),
    ]

    return dict(blob)

def _get_deps_info():
    """Overview of the installed version of main dependencies

    This function does not import the modules to collect the version numbers
    but instead relies on standard Python package metadata.

    Returns
    -------
    deps_info: dict
        version information on relevant Python libraries

    """
    deps = [
        "pip",
        "setuptools",
        "numpy",
        "scipy",
        "Cython",
        "pandas",
        "matplotlib",
        "joblib",
        "threadpoolctl",
    ]

    deps_info = {
        "sklearn": __version__,
    }

    from importlib.metadata import PackageNotFoundError, version

    for modname in deps:
        try:
            deps_info[modname] = version(modname)
        except PackageNotFoundError:
            deps_info[modname] = None
    return deps_info

def show_versions():
    """Print useful debugging information"

    Examples
    --------
    >>> from sklearn import show_versions
    >>> show_versions()  # doctest: +SKIP
    """

    sys_info = _get_sys_info()
    deps_info = _get_deps_info()

    print("\nSystem:")
    for k, stat in sys_info.items():
        print("{k:>10}: {stat}".format(k=k, stat=stat))

    print("\nPython dependencies:")
    for k, stat in deps_info.items():
        print("{k:>13}: {stat}".format(k=k, stat=stat))

    print(
        "\n{k}: {stat}".format(
            k="Built with OpenMP", stat=_openmp_parallelism_enabled()
        )
    )

    # show threadpoolctl results
    threadpool_results = threadpool_info()
    if threadpool_results:
        print()
        print("threadpoolctl info:")

        for i, result in enumerate(threadpool_results):
            for key, val in result.items():
                print(f"{key:>15}: {val}")
            if i != len(threadpool_results) - 1:
                print()

def _raise_typeerror(X):
    """Raises a TypeError if X is not a CSR or CSC matrix"""
    input_type = X.format if sp.issparse(X) else type(X)
    err = "Expected a CSR or CSC sparse matrix, got %s." % input_type
    raise TypeError(err)

def _raise_error_wrong_axis(axis):
    if axis not in (0, 1):
            "Unknown axis value: %d. Use 0 for rows, or 1 for columns" % axis
        )

def inplace_csr_column_scale(X, scale):
    """Inplace column scaling of a CSR matrix.

    Scale each feature of the data matrix by multiplying with specific scale
    provided by the caller assuming a (n_samples, n_features) shape.

    X : sparse matrix of shape (n_samples, n_features)
        Matrix to normalize using the variance of the features.
        It should be of CSR format.

    scale : ndarray of shape (n_features,), dtype={np.float32, np.float64}
        Array of precomputed feature-wise values to use for scaling.

    Examples
    --------
    >>> from sklearn.utils import sparsefuncs
    >>> indptr = np.array([0, 3, 4, 4, 4])
    >>> indices = np.array([0, 1, 2, 2])
    >>> data = np.array([8, 1, 2, 5])
    >>> scale = np.array([2, 3, 2])
    >>> csr = sparse.csr_matrix((data, indices, indptr))
    >>> csr.todense()
    matrix([[8, 1, 2],
            [0, 0, 5],
            [0, 0, 0],
            [0, 0, 0]])
    >>> sparsefuncs.inplace_csr_column_scale(csr, scale)
    matrix([[16,  3,  4],
            [ 0,  0, 10],
            [ 0,  0,  0],
            [ 0,  0,  0]])
    """
    assert scale.shape[0] == X.shape[1]
    X.data *= scale.take(X.indices, mode="clip")

def inplace_csr_row_scale(X, scale):
    """Inplace row scaling of a CSR matrix.

    Scale each sample of the data matrix by multiplying with specific scale

        Matrix to be scaled. It should be of CSR format.

    scale : ndarray of float of shape (n_samples,)
        Array of precomputed sample-wise values to use for scaling.
    """
    assert scale.shape[0] == X.shape[0]
    X.data *= np.repeat(scale, np.diff(X.indptr))

def mean_variance_axis(X, axis, weights=None, return_sum_weights=False):
    """Compute mean and variance along an axis on a CSR or CSC matrix.

        Input data. It can be of CSR or CSC format.

    axis : {0, 1}
        Axis along which the axis should be computed.

    weights : ndarray of shape (n_samples,) or (n_features,), default=None
        If axis is set to 0 shape is (n_samples,) or
        if axis is set to 1 shape is (n_features,).
        If it is set to None, then samples are equally weighted.

    return_sum_weights : bool, default=False
        If True, returns the sum of weights seen for each feature
        if `axis=0` or each sample if `axis=1`.

    Returns
    -------

    means : ndarray of shape (n_features,), dtype=floating
        Feature-wise means.

    variances : ndarray of shape (n_features,), dtype=floating
        Feature-wise variances.

    sum_weights : ndarray of shape (n_features,), dtype=floating
        Returned if `return_sum_weights` is `True`.

    Examples
    --------
            [0, 0, 5],
            [0, 0, 0],
            [0, 0, 0]])
    >>> sparsefuncs.mean_variance_axis(csr, axis=0)
    (array([2.  , 0.25, 1.75]), array([12.    ,  0.1875,  4.1875]))
    """
    _raise_error_wrong_axis(axis)

        if axis == 0:
            return _csr_mean_var_axis0(
                X, weights=weights, return_sum_weights=return_sum_weights
            )
        else:
            return _csc_mean_var_axis0(
                X.T, weights=weights, return_sum_weights=return_sum_weights
            )
    elif sp.issparse(X) and X.format == "csc":
            )
        else:
            )
    else:
        _raise_typeerror(X)

def incr_mean_variance_axis(X, *, axis, last_mean, last_var, last_n, weights=None):
    """Compute incremental mean and variance along an axis on a CSR or CSC matrix.

    last_mean, last_var are the statistics computed at the last step by this
    function. Both must be initialized to 0-arrays of the proper size, i.e.
    the number of features in X. last_n is the number of samples encountered
    until now.

    X : CSR or CSC sparse matrix of shape (n_samples, n_features)

    last_mean : ndarray of shape (n_features,) or (n_samples,), dtype=floating
        Array of means to update with the new data X.
        Should be of shape (n_features,) if axis=0 or (n_samples,) if axis=1.

    last_var : ndarray of shape (n_features,) or (n_samples,), dtype=floating
        Array of variances to update with the new data X.

    last_n : float or ndarray of shape (n_features,) or (n_samples,), \
            dtype=floating
        Sum of the weights seen so far, excluding the current weights
        If not float, it should be of shape (n_features,) if
        axis=0 or (n_samples,) if axis=1. If float it corresponds to
        having same weights for all samples (or features).

    Returns
    -------
    means : ndarray of shape (n_features,) or (n_samples,), dtype=floating
        Updated feature-wise means if axis = 0 or
        sample-wise means if axis = 1.

    variances : ndarray of shape (n_features,) or (n_samples,), dtype=floating
        Updated feature-wise variances if axis = 0 or
        sample-wise variances if axis = 1.

    n : ndarray of shape (n_features,) or (n_samples,), dtype=integral
        Updated number of seen samples per feature if axis=0
        or number of seen features per sample if axis=1.

        If weights is not None, n is a sum of the weights of the seen
        samples or features instead of the actual number of seen
        samples or features.

    Notes
    -----
    NaNs are ignored in the algorithm.

    Examples
    --------
            [0, 0, 5],
            [0, 0, 0],
            [0, 0, 0]])
    >>> sparsefuncs.incr_mean_variance_axis(
    ...     csr, axis=0, last_mean=np.zeros(3), last_var=np.zeros(3), last_n=2
    ... )
    (array([1.3..., 0.1..., 1.1...]), array([8.8..., 0.1..., 3.4...]),
    array([6., 6., 6.]))
    """

    if not (sp.issparse(X) and X.format in ("csc", "csr")):

    if np.size(last_n) == 1:
        last_n = np.full(last_mean.shape, last_n, dtype=last_mean.dtype)

    if not (np.size(last_mean) == np.size(last_var) == np.size(last_n)):
        raise ValueError("last_mean, last_var, last_n do not have the same shapes.")

    if axis == 1:
        if np.size(last_mean) != X.shape[0]:
                "If axis=1, then last_mean, last_n, last_var should be of "
                f"size n_samples {X.shape[0]} (Got {np.size(last_mean)})."
            )
    else:  # axis == 0
        if np.size(last_mean) != X.shape[1]:
                "If axis=0, then last_mean, last_n, last_var should be of "
                f"size n_features {X.shape[1]} (Got {np.size(last_mean)})."
            )

    X = X.T if axis == 1 else X

        weights = _check_sample_weight(weights, X, dtype=X.dtype)

    return _incr_mean_var_axis0(
        X, last_mean=last_mean, last_var=last_var, last_n=last_n, weights=weights
    )

def inplace_column_scale(X, scale):
    """Inplace column scaling of a CSC/CSR matrix.

        Matrix to normalize using the variance of the features. It should be
        of CSC or CSR format.

    Examples
    --------
            [0, 0, 5],
            [0, 0, 0],
            [0, 0, 0]])
    >>> sparsefuncs.inplace_column_scale(csr, scale)
            [ 0,  0, 10],
            [ 0,  0,  0],
            [ 0,  0,  0]])
    """
    if sp.issparse(X) and X.format == "csc":
        inplace_csr_row_scale(X.T, scale)
    elif sp.issparse(X) and X.format == "csr":
        inplace_csr_column_scale(X, scale)
    else:

def inplace_row_scale(X, scale):
    """Inplace row scaling of a CSR or CSC matrix.

    Scale each row of the data matrix by multiplying with specific scale

        Matrix to be scaled. It should be of CSR or CSC format.

    Examples
    --------
    >>> indptr = np.array([0, 2, 3, 4, 5])
    >>> indices = np.array([0, 1, 2, 3, 3])
    >>> data = np.array([8, 1, 2, 5, 6])
    >>> scale = np.array([2, 3, 4, 5])
    matrix([[8, 1, 0, 0],
            [0, 0, 2, 0],
            [0, 0, 0, 5],
            [0, 0, 0, 6]])
    >>> sparsefuncs.inplace_row_scale(csr, scale)
     matrix([[16,  2,  0,  0],
             [ 0,  0,  6,  0],
             [ 0,  0,  0, 20],
             [ 0,  0,  0, 30]])
    """
        inplace_csr_column_scale(X.T, scale)
        inplace_csr_row_scale(X, scale)
    else:

def inplace_swap_row_csc(X, m, n):
    """Swap two rows of a CSC matrix in-place.

        Matrix whose two rows are to be swapped. It should be of
        CSC format.

    m : int
        Index of the row of X to be swapped.

    n : int
    """
    for t in [m, n]:
        if isinstance(t, np.ndarray):
            raise TypeError("m and n should be valid integers")

    if m < 0:
        m += X.shape[0]
    if n < 0:
        n += X.shape[0]

    m_mask = X.indices == m
    X.indices[X.indices == n] = m
    X.indices[m_mask] = n

def inplace_swap_row_csr(X, m, n):
    """Swap two rows of a CSR matrix in-place.

        CSR format.

    m : int

    n : int
    """

    if m < 0:
    if n < 0:

    # The following swapping makes life easier since m is assumed to be the
    # smaller integer below.
    if m > n:
        m, n = n, m

    indptr = X.indptr
    m_start = indptr[m]
    m_stop = indptr[m + 1]
    n_start = indptr[n]
    n_stop = indptr[n + 1]
    nz_m = m_stop - m_start
    nz_n = n_stop - n_start

    if nz_m != nz_n:
        # Modify indptr first
        X.indptr[m + 2 : n] += nz_n - nz_m
        X.indptr[m + 1] = m_start + nz_n
        X.indptr[n] = n_stop - nz_m

    X.indices = np.concatenate(
        [
            X.indices[:m_start],
            X.indices[n_start:n_stop],
            X.indices[m_stop:n_start],
            X.indices[m_start:m_stop],
            X.indices[n_stop:],
        ]
    )
    X.data = np.concatenate(
        [
            X.data[:m_start],
            X.data[n_start:n_stop],
            X.data[m_stop:n_start],
            X.data[m_start:m_stop],
            X.data[n_stop:],
        ]
    )

def inplace_swap_row(X, m, n):
    """
    Swap two rows of a CSC/CSR matrix in-place.

        Matrix whose two rows are to be swapped. It should be of CSR or

    m : int

    n : int

    Examples
    --------
    >>> indptr = np.array([0, 2, 3, 3, 3])
    >>> indices = np.array([0, 2, 2])
    >>> data = np.array([8, 2, 5])
    matrix([[8, 0, 2],
            [0, 0, 5],
            [0, 0, 0],
            [0, 0, 0]])
    >>> sparsefuncs.inplace_swap_row(csr, 0, 1)
    matrix([[0, 0, 5],
            [8, 0, 2],
            [0, 0, 0],
            [0, 0, 0]])
    """
        inplace_swap_row_csc(X, m, n)
        inplace_swap_row_csr(X, m, n)
    else:

def inplace_swap_column(X, m, n):
    """
    Swap two columns of a CSC/CSR matrix in-place.

        Matrix whose two columns are to be swapped. It should be of
        CSR or CSC format.

    m : int
        Index of the column of X to be swapped.

    n : int

    Examples
    --------
            [0, 0, 5],
            [0, 0, 0],
            [0, 0, 0]])
    >>> sparsefuncs.inplace_swap_column(csr, 0, 1)
    matrix([[0, 8, 2],
            [0, 0, 5],
            [0, 0, 0],
            [0, 0, 0]])
    """
    if m < 0:
        m += X.shape[1]
    if n < 0:
        n += X.shape[1]
    else:

def min_max_axis(X, axis, ignore_nan=False):
    """Compute minimum and maximum along an axis on a CSR or CSC matrix.

     Optionally ignore NaN values.

        Input data. It should be of CSR or CSC format.

    ignore_nan : bool, default=False
        Ignore or passing through NaN values.

    Returns
    -------

    mins : ndarray of shape (n_features,), dtype={np.float32, np.float64}
        Feature-wise minima.

    maxs : ndarray of shape (n_features,), dtype={np.float32, np.float64}
        Feature-wise maxima.
    """
    if sp.issparse(X) and X.format in ("csr", "csc"):
        if ignore_nan:
            return _sparse_nan_min_max(X, axis=axis)
        else:
            return _sparse_min_max(X, axis=axis)
    else:

def count_nonzero(X, axis=None, sample_weight=None):
    """A variant of X.getnnz() with extension to weighting on axis 0.

    Useful in efficiently calculating multilabel metrics.

    X : sparse matrix of shape (n_samples, n_labels)
        Input data. It should be of CSR format.

    axis : {0, 1}, default=None
        The axis on which the data is aggregated.

        Weight for each row of X.

    Returns
    -------
    nnz : int, float, ndarray of shape (n_samples,) or ndarray of shape (n_features,)
        Number of non-zero values in the array along a given axis. Otherwise,
        the total number of non-zero values in the array is returned.
    """
    if axis == -1:
        axis = 1
    elif axis == -2:
        axis = 0
    elif X.format != "csr":
        raise TypeError("Expected CSR sparse format, got {0}".format(X.format))

    # We rely here on the fact that np.diff(Y.indptr) for a CSR
    # will return the number of nonzero entries in each row.
    # A bincount over Y.indices will return the number of nonzeros
    # in each column. See ``csr_matrix.getnnz`` in scipy >= 0.14.
            return X.nnz
        else:
            return np.dot(np.diff(X.indptr), sample_weight)
    elif axis == 1:
        out = np.diff(X.indptr)
            # astype here is for consistency with axis=0 dtype
            return out.astype("intp")
        return out * sample_weight
    elif axis == 0:
            return np.bincount(X.indices, minlength=X.shape[1])
        else:
            weights = np.repeat(sample_weight, np.diff(X.indptr))
            return np.bincount(X.indices, minlength=X.shape[1], weights=weights)
    else:
        raise ValueError("Unsupported axis: {0}".format(axis))

def _get_median(data, n_zeros):
    """Compute the median of data with n_zeros additional zeros.

    This function is used to support sparse matrices; it modifies data
    in-place.
    """
    n_elems = len(data) + n_zeros
    if not n_elems:
    n_negative = np.count_nonzero(data < 0)
    middle, is_odd = divmod(n_elems, 2)
    data.sort()

    if is_odd:
        return _get_elem_at_rank(middle, data, n_negative, n_zeros)

    return (
        _get_elem_at_rank(middle - 1, data, n_negative, n_zeros)
        + _get_elem_at_rank(middle, data, n_negative, n_zeros)
    ) / 2.0

def _get_elem_at_rank(rank, data, n_negative, n_zeros):
    """Find the value in data augmented with n_zeros for the given rank"""
    if rank < n_negative:
        return data[rank]
    if rank - n_negative < n_zeros:
        return 0
    return data[rank - n_zeros]

def csc_median_axis_0(X):
    """Find the median across axis 0 of a CSC matrix.

    It is equivalent to doing np.median(X, axis=0).

        Input data. It should be of CSC format.

    Returns
    -------
    median : ndarray of shape (n_features,)
        Median.
    """
    if not (sp.issparse(X) and X.format == "csc"):
        raise TypeError("Expected matrix of CSC format, got %s" % X.format)

    median = np.zeros(n_features)

    for f_ind, (start, end) in enumerate(itertools.pairwise(indptr)):
        # Prevent modifying X in place
        data = np.copy(X.data[start:end])
        nz = n_samples - data.size
        median[f_ind] = _get_median(data, nz)

    return median

def _implicit_column_offset(X, offset):
    """Create an implicitly offset linear operator.

    This is used by PCA on sparse data to avoid densifying the whole data
    matrix.

    Params
    ------
        offset : ndarray of shape (n_features,)

    Returns
    -------
    centered : LinearOperator
    """
    offset = offset[None, :]
    XT = X.T
    return LinearOperator(
        matvec=lambda x: X @ x - offset @ x,
        matmat=lambda x: X @ x - offset @ x,
        rmatvec=lambda x: XT @ x - (offset * x.sum()),
        rmatmat=lambda x: XT @ x - offset.T @ x.sum(axis=0)[None, :],
        dtype=X.dtype,
        shape=X.shape,
    )

class InputTags:
    """Tags for the input data.

    one_d_array : bool, default=False
        Whether the input can be a 1D array.

    two_d_array : bool, default=True
        Whether the input can be a 2D array. Note that most common
        tests currently run only if this flag is set to ``True``.

    three_d_array : bool, default=False
        Whether the input can be a 3D array.

        Whether the input can be a sparse matrix.

    categorical : bool, default=False
        Whether the input can be categorical.

    string : bool, default=False
        Whether the input can be an array-like of strings.

    dict : bool, default=False
        Whether the input can be a dictionary.

    positive_only : bool, default=False
        Whether the estimator requires positive X.

    allow_nan : bool, default=False
        Whether the estimator supports data with missing values encoded as `np.nan`.

    pairwise : bool, default=False
        This boolean attribute indicates whether the data (`X`),
        :term:`fit` and similar methods consists of pairwise measures
        over samples rather than a feature representation for each
        sample.  It is usually `True` where an estimator has a
        `metric` or `affinity` or `kernel` parameter with value
        'precomputed'. Its primary purpose is to support a
        :term:`meta-estimator` or a cross validation procedure that
        extracts a sub-sample of data intended for a pairwise
        estimator, where the data needs to be indexed on both axes.
        Specifically, this tag is used by
        `sklearn.utils.metaestimators._safe_split` to slice rows and
        columns.

        Note that if setting this tag to ``True`` means the estimator can take only
        positive values, the `positive_only` tag must reflect it and also be set to
        ``True``.
    """

    one_d_array: bool = False
    two_d_array: bool = True
    three_d_array: bool = False
    sparse: bool = False
    categorical: bool = False
    string: bool = False
    dict: bool = False
    positive_only: bool = False
    allow_nan: bool = False
    pairwise: bool = False

class TargetTags:
    """Tags for the target data.

    required : bool
        Whether the estimator requires y to be passed to `fit`,
        `fit_predict` or `fit_transform` methods. The tag is ``True``
        for estimators inheriting from `~sklearn.base.RegressorMixin`
        and `~sklearn.base.ClassifierMixin`.

    one_d_labels : bool, default=False
        Whether the input is a 1D labels (y).

    two_d_labels : bool, default=False
        Whether the input is a 2D labels (y).

        Whether the estimator requires a positive y (only applicable
        for regression).

    multi_output : bool, default=False
        Whether a regressor supports multi-target outputs or a classifier supports
        multi-class multi-output.

        See :term:`multi-output` in the glossary.

    single_output : bool, default=True
        Whether the target can be single-output. This can be ``False`` if the
        estimator supports only multi-output cases.
    """

    required: bool
    one_d_labels: bool = False
    two_d_labels: bool = False
    multi_output: bool = False
    single_output: bool = True

class TransformerTags:
    """Tags for the transformer.

    preserves_dtype : list[str], default=["float64"]
        Applies only on transformers. It corresponds to the data types
        which will be preserved such that `X_trans.dtype` is the same
        as `X.dtype` after calling `transformer.transform(X)`. If this
        list is empty, then the transformer is not expected to
        preserve the data type. The first value in the list is
        considered as the default data type, corresponding to the data
        type of the output when the input data type is not going to be
        preserved.
    """

    preserves_dtype: list[str] = field(default_factory=lambda: ["float64"])

class ClassifierTags:
    """Tags for the classifier.

    poor_score : bool, default=False
        Whether the estimator fails to provide a "reasonable" test-set
        score, which currently for classification is an accuracy of
        0.83 on ``make_blobs(n_samples=300, random_state=0)``. The
        datasets and values are based on current estimators in scikit-learn
        and might be replaced by something more systematic.

    multi_class : bool, default=True
        Whether the classifier can handle multi-class
        classification. Note that all classifiers support binary
        classification. Therefore this flag indicates whether the
        classifier is a binary-classifier-only or not.

        See :term:`multi-class` in the glossary.

    multi_label : bool, default=False
        Whether the classifier supports multi-label output: a data point can
        be predicted to belong to a variable number of classes.

        See :term:`multi-label` in the glossary.
    """

    poor_score: bool = False
    multi_class: bool = True
    multi_label: bool = False

class RegressorTags:
    """Tags for the regressor.

        score, which currently for regression is an R2 of 0.5 on
        ``make_regression(n_samples=200, n_features=10,
        n_informative=1, bias=5.0, noise=20, random_state=42)``. The
        dataset and values are based on current estimators in scikit-learn
    """

class Tags:
    """Tags for the estimator.

    See :ref:`estimator_tags` for more information.

    estimator_type : str or None
        The type of the estimator. Can be one of:
        - "classifier"
        - "regressor"
        - "transformer"
        - "clusterer"
        - "outlier_detector"
        - "density_estimator"

    target_tags : :class:`TargetTags`
        The target(y) tags.

    transformer_tags : :class:`TransformerTags` or None
        The transformer tags.

    classifier_tags : :class:`ClassifierTags` or None
        The classifier tags.

    regressor_tags : :class:`RegressorTags` or None
        The regressor tags.

    array_api_support : bool, default=False
        Whether the estimator supports Array API compatible inputs.

    no_validation : bool, default=False
        Whether the estimator skips input-validation. This is only meant for
        stateless and dummy transformers!

    non_deterministic : bool, default=False
        Whether the estimator is not deterministic given a fixed ``random_state``.

    requires_fit : bool, default=True
        Whether the estimator requires to be fitted before calling one of
        `transform`, `predict`, `predict_proba`, or `decision_function`.

    _skip_test : bool, default=False
        Whether to skip common tests entirely. Don't use this unless
        you have a *very good* reason.

    input_tags : :class:`InputTags`
        The input data(X) tags.
    """

    estimator_type: str | None
    target_tags: TargetTags
    transformer_tags: TransformerTags | None = None
    classifier_tags: ClassifierTags | None = None
    regressor_tags: RegressorTags | None = None
    array_api_support: bool = False
    no_validation: bool = False
    non_deterministic: bool = False
    requires_fit: bool = True
    _skip_test: bool = False
    input_tags: InputTags = field(default_factory=InputTags)

def default_tags(estimator) -> Tags:
    """Get the default tags for an estimator.

    This ignores any ``__sklearn_tags__`` method that the estimator may have.

    If the estimator is a classifier or a regressor, ``target_tags.required``
    will be set to ``True``, otherwise it will be set to ``False``.

    ``transformer_tags`` will be set to :class:`~.sklearn.utils. TransformerTags` if the
    estimator has a ``transform`` or ``fit_transform`` method, otherwise it will be set
    to ``None``.

    ``classifier_tags`` will be set to :class:`~.sklearn.utils.ClassifierTags` if the
    estimator is a classifier, otherwise it will be set to ``None``.
    a classifier, otherwise it will be set to ``None``.

    ``regressor_tags`` will be set to :class:`~.sklearn.utils.RegressorTags` if the
    estimator is a regressor, otherwise it will be set to ``None``.

        The estimator for which to get the default tags.

    Returns
    -------
    tags : Tags
        The default tags for the estimator.
    """
    est_is_classifier = getattr(estimator, "_estimator_type", None) == "classifier"
    est_is_regressor = getattr(estimator, "_estimator_type", None) == "regressor"
    target_required = est_is_classifier or est_is_regressor

    return Tags(
        estimator_type=getattr(estimator, "_estimator_type", None),
        target_tags=TargetTags(required=target_required),
        transformer_tags=(
            TransformerTags()
            if hasattr(estimator, "transform") or hasattr(estimator, "fit_transform")
            else None
        ),
        classifier_tags=ClassifierTags() if est_is_classifier else None,
        regressor_tags=RegressorTags() if est_is_regressor else None,
    )

def get_tags(estimator) -> Tags:
    """Get estimator tags.

    :class:`~sklearn.BaseEstimator` provides the estimator tags machinery.
    However, if an estimator does not inherit from this base class, we should
    fall-back to the default tags.

    For scikit-learn built-in estimators, we should still rely on
    `self.__sklearn_tags__()`. `get_tags(est)` should be used when we
    are not sure where `est` comes from: typically
    `get_tags(self.estimator)` where `self` is a meta-estimator, or in
    the common checks.

        The estimator from which to get the tag.

    Returns
    -------
    tags : :class:`~.sklearn.utils.Tags`
        The estimator tags.
    """

    try:
        tags = estimator.__sklearn_tags__()
    except AttributeError as exc:
        # TODO(1.8): turn the warning into an error
        if "object has no attribute '__sklearn_tags__'" in str(exc):
            # Fall back to the default tags if the estimator does not
            # implement __sklearn_tags__.
            # In particular, workaround the regression reported in
            # https://github.com/scikit-learn/scikit-learn/issues/30479
            # `__sklearn_tags__` is implemented by calling
            # `super().__sklearn_tags__()` but there is no `__sklearn_tags__`
            # method in the base class. Typically happens when only inheriting
            # from Mixins.

                f"The following error was raised: {exc}. It seems that "
                "there are no classes that implement `__sklearn_tags__` "
                "in the MRO and/or all classes in the MRO call "
                "`super().__sklearn_tags__()`. Make sure to inherit from "
                "`BaseEstimator` which implements `__sklearn_tags__` (or "
                "alternatively define `__sklearn_tags__` but we don't recommend "
                "this approach). Note that `BaseEstimator` needs to be on the "
                "right side of other Mixins in the inheritance order. The "
                "default are now used instead since retrieving tags failed. "
                "This warning will be replaced by an error in 1.8.",
                category=DeprecationWarning,
            )
            tags = default_tags(estimator)
        else:
            raise

    return tags

def _line_search_wolfe12(
    f, fprime, xk, pk, gfk, old_fval, old_old_fval, verbose=0, **kwargs
):
    """
    Same as line_search_wolfe1, but fall back to line_search_wolfe2 if
    suitable step length is not found, and raise an exception if a
    suitable step length is not found.

    Raises
    ------
    _LineSearchError
        If no suitable step size is found.

    """
    is_verbose = verbose >= 2
    eps = 16 * np.finfo(np.asarray(old_fval).dtype).eps
    if is_verbose:
        print("  Line Search")
        print(f"    eps=16 * finfo.eps={eps}")
        print("    try line search wolfe1")

    ret = line_search_wolfe1(f, fprime, xk, pk, gfk, old_fval, old_old_fval, **kwargs)

        _not_ = "not " if ret[0] is None else ""
        print("    wolfe1 line search was " + _not_ + "successful")

    if ret[0] is None:
        # Have a look at the line_search method of our NewtonSolver class. We borrow
        # the logic from there
        # Deal with relative loss differences around machine precision.
        args = kwargs.get("args", tuple())
        fval = f(xk + pk, *args)
        tiny_loss = np.abs(old_fval * eps)
        loss_improvement = fval - old_fval
        check = np.abs(loss_improvement) <= tiny_loss
            print(
                "    check loss |improvement| <= eps * |loss_old|:"
                f" {np.abs(loss_improvement)} <= {tiny_loss} {check}"
            )
        if check:
            # 2.1 Check sum of absolute gradients as alternative condition.
            sum_abs_grad_old = scipy.linalg.norm(gfk, ord=1)
            grad = fprime(xk + pk, *args)
            sum_abs_grad = scipy.linalg.norm(grad, ord=1)
            check = sum_abs_grad < sum_abs_grad_old
                print(
                    "    check sum(|gradient|) < sum(|gradient_old|): "
                    f"{sum_abs_grad} < {sum_abs_grad_old} {check}"
                )
            if check:
                ret = (
                    1.0,  # step size
                    ret[1] + 1,  # number of function evaluations
                    ret[2] + 1,  # number of gradient evaluations
                    fval,
                    old_fval,
                    grad,
                )

        # line search failed: try different one.
        # TODO: It seems that the new check for the sum of absolute gradients above
        # catches all cases that, earlier, ended up here. In fact, our tests never
        # trigger this "if branch" here and we can consider to remove it.
            print("    last resort: try line search wolfe2")
        ret = line_search_wolfe2(
            f, fprime, xk, pk, gfk, old_fval, old_old_fval, **kwargs
        )
            _not_ = "not " if ret[0] is None else ""
            print("    wolfe2 line search was " + _not_ + "successful")

        raise _LineSearchError()

    return ret

def _cg(fhess_p, fgrad, maxiter, tol, verbose=0):
    """
    Solve iteratively the linear system 'fhess_p . xsupi = fgrad'
    with a conjugate gradient descent.

    fhess_p : callable
        Function that takes the gradient as a parameter and returns the
        matrix product of the Hessian and gradient.

    fgrad : ndarray of shape (n_features,) or (n_features + 1,)
        Gradient vector.

    maxiter : int
        Number of CG iterations.

    tol : float
        Stopping criterion.

    Returns
    -------
    xsupi : ndarray of shape (n_features,) or (n_features + 1,)
        Estimated solution.
    """
    eps = 16 * np.finfo(np.float64).eps
    xsupi = np.zeros(len(fgrad), dtype=fgrad.dtype)
    ri = np.copy(fgrad)  # residual = fgrad - fhess_p @ xsupi
    psupi = -ri
    i = 0
    dri0 = np.dot(ri, ri)
    # We also keep track of |p_i|^2.
    psupi_norm2 = dri0

    while i <= maxiter:
        if np.sum(np.abs(ri)) <= tol:
                print(
                    f"  Inner CG solver iteration {i} stopped with\n"
                    f"    sum(|residuals|) <= tol: {np.sum(np.abs(ri))} <= {tol}"
                )
            break

        Ap = fhess_p(psupi)
        # check curvature
        curv = np.dot(psupi, Ap)
        if 0 <= curv <= eps * psupi_norm2:
            # See https://arxiv.org/abs/1803.02924, Algo 1 Capped Conjugate Gradient.
                print(
                    f"    tiny_|p| = eps * ||p||^2, eps = {eps}, "
                    f"squared L2 norm ||p||^2 = {psupi_norm2}\n"
                    f"    curvature <= tiny_|p|: {curv} <= {eps * psupi_norm2}"
                )
            break
        elif curv < 0:
            if i > 0:
                    print(
                        f"  Inner CG solver iteration {i} stopped with negative "
                        f"curvature, curvature = {curv}"
                    )
                break
            else:
                # fall back to steepest descent direction
                xsupi += dri0 / curv * psupi
                    print("  Inner CG solver iteration 0 fell back to steepest descent")
                break
        alphai = dri0 / curv
        xsupi += alphai * psupi
        ri += alphai * Ap
        dri1 = np.dot(ri, ri)
        betai = dri1 / dri0
        psupi = -ri + betai * psupi
        # We use  |p_i|^2 = |r_i|^2 + beta_i^2 |p_{i-1}|^2
        psupi_norm2 = dri1 + betai**2 * psupi_norm2
        i = i + 1
        dri0 = dri1  # update np.dot(ri,ri) for next time.
    if is_verbose and i > maxiter:
        print(
            f"  Inner CG solver stopped reaching maxiter={i - 1} with "
            f"sum(|residuals|) = {np.sum(np.abs(ri))}"
        )
    return xsupi

def _newton_cg(
    grad_hess,
    func,
    grad,
    x0,
    args=(),
    tol=1e-4,
    maxiter=100,
    maxinner=200,
    line_search=True,
    warn=True,
):
    """
    Minimization of scalar function of one or more variables using the
    Newton-CG algorithm.

    grad_hess : callable
        Should return the gradient and a callable returning the matvec product
        of the Hessian.

        Should return the value of the function.

    grad : callable
        Should return the function value and the gradient. This is used
        by the linesearch functions.

    x0 : array of float
        Initial guess.

    args : tuple, default=()
        Arguments passed to func_grad_hess, func and grad.

        Stopping criterion. The iteration will stop when
        ``max{|g_i | i = 1, ..., n} <= tol``

    maxiter : int, default=100
        Number of Newton iterations.

    maxinner : int, default=200

    line_search : bool, default=True
        Whether to use a line search or not.

    warn : bool, default=True
        Whether to warn when didn't converge.

    Returns
    -------
    xk : ndarray of float
        Estimated minimum.
    """
    x0 = np.asarray(x0).flatten()
    xk = np.copy(x0)
    k = 0

    if line_search:
        old_fval = func(x0, *args)
        old_old_fval = None
    else:
        old_fval = 0

    is_verbose = verbose > 0

    # Outer loop: our Newton iteration
    while k < maxiter:
        # Compute a search direction pk by applying the CG method to
        #  del2 f(xk) p = - fgrad f(xk) starting from 0.
        fgrad, fhess_p = grad_hess(xk, *args)

        absgrad = np.abs(fgrad)
        max_absgrad = np.max(absgrad)
        check = max_absgrad <= tol
            print(f"Newton-CG iter = {k}")
            print(f"    max |gradient| <= tol: {max_absgrad} <= {tol} {check}")
        if check:
            break

        maggrad = np.sum(absgrad)
        eta = min([0.5, np.sqrt(maggrad)])
        termcond = eta * maggrad

        # Inner loop: solve the Newton update by conjugate gradient, to
        # avoid inverting the Hessian
        xsupi = _cg(fhess_p, fgrad, maxiter=maxinner, tol=termcond, verbose=verbose)

        alphak = 1.0

        if line_search:
            try:
                alphak, fc, gc, old_fval, old_old_fval, gfkp1 = _line_search_wolfe12(
                    func,
                    grad,
                    xk,
                    xsupi,
                    fgrad,
                    old_fval,
                    old_old_fval,
                    args=args,
                )
            except _LineSearchError:
                warnings.warn("Line Search failed")
                break

        xk += alphak * xsupi  # upcast if necessary
        k += 1

    if warn and k >= maxiter:
            (
                f"newton-cg failed to converge at loss = {old_fval}. Increase the"
                " number of iterations."
            ),
        )
    elif is_verbose:
        print(f"  Solver did converge at loss = {old_fval}.")
    return xk, k

def _check_optimize_result(solver, result, max_iter=None, extra_warning_msg=None):
    """Check the OptimizeResult for successful convergence

    solver : str
       Solver name. Currently only `lbfgs` is supported.

    result : OptimizeResult
       Result of the scipy.optimize.minimize function.

       Expected maximum number of iterations.

    extra_warning_msg : str, default=None
        Extra warning message.

    Returns
    -------
       Number of iterations.
    """
    # handle both scipy and scikit-learn solver names
        if result.status != 0:
            result_message = result.message

                "{} failed to converge (status={}):\n{}.\n\n"
                "Increase the number of iterations (max_iter) "
                "or scale the data as shown in:\n"
                "    https://scikit-learn.org/stable/modules/"
                "preprocessing.html"
            ).format(solver, result.status, result_message)
            if extra_warning_msg is not None:
                warning_msg += "\n" + extra_warning_msg
            warnings.warn(warning_msg, ConvergenceWarning, stacklevel=2)
        if max_iter is not None:
            # In scipy <= 1.0.0, nit may exceed maxiter for lbfgs.
            # See https://github.com/scipy/scipy/issues/7854
            n_iter_i = min(result.nit, max_iter)
        else:
            n_iter_i = result.nit
    else:

    return n_iter_i

def _mode(a, axis=0):
    if sp_version >= parse_version("1.9.0"):
        mode = scipy.stats.mode(a, axis=axis, keepdims=True)
        if sp_version >= parse_version("1.10.999"):
            # scipy.stats.mode has changed returned array shape with axis=None
            # and keepdims=True, see https://github.com/scipy/scipy/pull/17561
            if axis is None:
                mode = np.ravel(mode)
        return mode
    return scipy.stats.mode(a, axis=axis)

def pd_fillna(pd, frame):
    pd_version = parse_version(pd.__version__).base_version
    if parse_version(pd_version) < parse_version("2.2"):
        frame = frame.fillna(value=np.nan)
    else:
        infer_objects_kwargs = (
            {} if parse_version(pd_version) >= parse_version("3") else {"copy": False}
        )
        with pd.option_context("future.no_silent_downcasting", True):
            frame = frame.fillna(value=np.nan).infer_objects(**infer_objects_kwargs)
    return frame

def _preserve_dia_indices_dtype(
    sparse_container, original_container_format, requested_sparse_format
):
    """Preserve indices dtype for SciPy < 1.12 when converting from DIA to CSR/CSC.

    For SciPy < 1.12, DIA arrays indices are upcasted to `np.int64` that is
    inconsistent with DIA matrices. We downcast the indices dtype to `np.int32` to
    be consistent with DIA matrices.

    The converted indices arrays are affected back inplace to the sparse container.

    sparse_container : sparse container
        Sparse container to be checked.
    requested_sparse_format : str or bool
        The type of format of `sparse_container`.

    Notes
    -----
    See https://github.com/scipy/scipy/issues/19245 for more details.
    """
    if original_container_format == "dia_array" and requested_sparse_format in (
        "csr",
        "coo",
    ):
        if requested_sparse_format == "csr":
            index_dtype = _smallest_admissible_index_dtype(
                arrays=(sparse_container.indptr, sparse_container.indices),
                maxval=max(sparse_container.nnz, sparse_container.shape[1]),
                check_contents=True,
            )
            sparse_container.indices = sparse_container.indices.astype(
                index_dtype, copy=False
            )
            sparse_container.indptr = sparse_container.indptr.astype(
            )
        else:  # requested_sparse_format == "coo"
                maxval=max(sparse_container.shape)
            )
            sparse_container.row = sparse_container.row.astype(index_dtype, copy=False)
            sparse_container.col = sparse_container.col.astype(index_dtype, copy=False)

def _smallest_admissible_index_dtype(arrays=(), maxval=None, check_contents=False):
    """Based on input (integer) arrays `a`, determine a suitable index data
    type that can hold the data in the arrays.

    This function returns `np.int64` if it either required by `maxval` or based on the
    largest precision of the dtype of the arrays passed as argument, or by the their
    contents (when `check_contents is True`). If none of the condition requires
    `np.int64` then this function returns `np.int32`.

    arrays : ndarray or tuple of ndarrays, default=()
        Input arrays whose types/contents to check.

    maxval : float, default=None
        Maximum value needed.

    check_contents : bool, default=False
        Whether to check the values in the arrays and not just their types.
        By default, check only the types.

    Returns
    -------
    dtype : {np.int32, np.int64}
        Suitable index data type (int32 or int64).
    """

    int32min = np.int32(np.iinfo(np.int32).min)
    int32max = np.int32(np.iinfo(np.int32).max)

    if maxval is not None:
        if maxval > np.iinfo(np.int64).max:
                f"maxval={maxval} is to large to be represented as np.int64."
            )
        if maxval > int32max:
            return np.int64

    if isinstance(arrays, np.ndarray):
        arrays = (arrays,)

    for arr in arrays:
        if not isinstance(arr, np.ndarray):
                f"Arrays should be of type np.ndarray, got {type(arr)} instead."
            )
        if not np.issubdtype(arr.dtype, np.integer):
                f"Array dtype {arr.dtype} is not supported for index dtype. We expect "
                "integral values."
            )
        if not np.can_cast(arr.dtype, np.int32):
            if not check_contents:
                # when `check_contents` is False, we stay on the safe side and return
                # np.int64.
                return np.int64
            if arr.size == 0:
                # a bigger type not needed yet, let's look at the next array
                continue
            else:
                maxval = arr.max()
                minval = arr.min()
                if minval < int32min or maxval > int32max:
                    # a big index type is actually needed
                    return np.int64

    return np.int32

def _in_unstable_openblas_configuration():
    """Return True if in an unstable configuration for OpenBLAS"""

    # Import libraries which might load OpenBLAS.
    import numpy  # noqa: F401
    import scipy  # noqa: F401

    modules_info = _get_threadpool_controller().info()

    open_blas_used = any(info["internal_api"] == "openblas" for info in modules_info)
    if not open_blas_used:

    # OpenBLAS 0.3.16 fixed instability for arm64, see:
    # https://github.com/xianyi/OpenBLAS/blob/1b6db3dbba672b4f8af935bd43a1ff6cff4d20b7/Changelog.txt#L56-L58
    openblas_arm64_stable_version = parse_version("0.3.16")
    for info in modules_info:
        if info["internal_api"] != "openblas":
            continue
        openblas_version = info.get("version")
        openblas_architecture = info.get("architecture")
        if openblas_version is None or openblas_architecture is None:
            # Cannot be sure that OpenBLAS is good enough. Assume unstable:
            return True  # pragma: no cover
        if (
            openblas_architecture == "neoversen1"
            and parse_version(openblas_version) < openblas_arm64_stable_version
        ):
            # See discussions in https://github.com/numpy/numpy/issues/19411
    return False

    def _sparse_linalg_cg(A, b, **kwargs):
        if "rtol" in kwargs:
            kwargs["tol"] = kwargs.pop("rtol")
        if "atol" not in kwargs:
            kwargs["atol"] = "legacy"
        return scipy.sparse.linalg.cg(A, b, **kwargs)

    def _sparse_min_max(X, axis):
        the_min = X.min(axis=axis)
        the_max = X.max(axis=axis)

            the_min = the_min.toarray().ravel()
            the_max = the_max.toarray().ravel()

        return the_min, the_max

    def _sparse_nan_min_max(X, axis):
        the_min = X.nanmin(axis=axis)
        the_max = X.nanmax(axis=axis)

    def _minor_reduce(X, ufunc):
        major_index = np.flatnonzero(np.diff(X.indptr))

        # reduceat tries casts X.indptr to intp, which errors
        # if it is int64 on a 32 bit system.
        # Reinitializing prevents this where possible, see #13737
        X = type(X)((X.data, X.indices, X.indptr), shape=X.shape)
        value = ufunc.reduceat(X.data, X.indptr[major_index])
        return major_index, value

    def _min_or_max_axis(X, axis, min_or_max):
        N = X.shape[axis]
        if N == 0:
            raise ValueError("zero-size array to reduction operation")
        M = X.shape[1 - axis]
        mat = X.tocsc() if axis == 0 else X.tocsr()
        mat.sum_duplicates()
        major_index, value = _minor_reduce(mat, min_or_max)
        not_full = np.diff(mat.indptr)[major_index] < N
        value[not_full] = min_or_max(value[not_full], 0)
        mask = value != 0
        major_index = np.compress(mask, major_index)
        value = np.compress(mask, value)

            res = scipy.sparse.coo_matrix(
                (value, (np.zeros(len(value)), major_index)),
                shape=(1, M),
            )
        else:
                (value, (major_index, np.zeros(len(value)))),
                shape=(M, 1),
            )
        return res.toarray().ravel()

    def _sparse_min_or_max(X, axis, min_or_max):
        if axis is None:
            if 0 in X.shape:
                raise ValueError("zero-size array to reduction operation")
            zero = X.dtype.type(0)
            if X.nnz == 0:
                return zero
            m = min_or_max.reduce(X.data.ravel())
            if X.nnz != np.prod(X.shape):
                m = min_or_max(zero, m)
            return m
        if axis < 0:
            axis += 2
        if (axis == 0) or (axis == 1):
            return _min_or_max_axis(X, axis, min_or_max)
        else:
            raise ValueError("invalid axis, use 0 for rows, or 1 for columns")

        return (
            _sparse_min_or_max(X, axis, np.minimum),
            _sparse_min_or_max(X, axis, np.maximum),
        )

        return (
            _sparse_min_or_max(X, axis, np.fmin),
            _sparse_min_or_max(X, axis, np.fmax),
        )

def _process_predict_proba(*, y_pred, target_type, classes, pos_label):
    """Get the response values when the response method is `predict_proba`.

    This function process the `y_pred` array in the binary and multi-label cases.
    In the binary case, it selects the column corresponding to the positive
    class. In the multi-label case, it stacks the predictions if they are not
    in the "compressed" format `(n_samples, n_outputs)`.

    y_pred : ndarray
        Output of `estimator.predict_proba`. The shape depends on the target type:

        - for binary classification, it is a 2d array of shape `(n_samples, 2)`;
        - for multiclass classification, it is a 2d array of shape
          `(n_samples, n_classes)`;
        - for multilabel classification, it is either a list of 2d arrays of shape
          `(n_samples, 2)` (e.g. `RandomForestClassifier` or `KNeighborsClassifier`) or
          an array of shape `(n_samples, n_outputs)` (e.g. `MLPClassifier` or
          `RidgeClassifier`).

    target_type : {"binary", "multiclass", "multilabel-indicator"}
        Type of the target.

    classes : ndarray of shape (n_classes,) or list of such arrays
        Class labels as reported by `estimator.classes_`.

    pos_label : int, float, bool or str
        Only used with binary and multiclass targets.

    Returns
    -------
    y_pred : ndarray of shape (n_samples,), (n_samples, n_classes) or \
            (n_samples, n_output)
        Compressed predictions format as requested by the metrics.
    """
    if target_type == "binary" and y_pred.shape[1] < 2:
        # We don't handle classifiers trained on a single class.
            f"Got predict_proba of shape {y_pred.shape}, but need "
            "classifier with two classes."
        )

    if target_type == "binary":
        col_idx = np.flatnonzero(classes == pos_label)[0]
        return y_pred[:, col_idx]
    elif target_type == "multilabel-indicator":
        # Use a compress format of shape `(n_samples, n_output)`.
        # Only `MLPClassifier` and `RidgeClassifier` return an array of shape
        # `(n_samples, n_outputs)`.
        if isinstance(y_pred, list):
            # list of arrays of shape `(n_samples, 2)`
            return np.vstack([p[:, -1] for p in y_pred]).T
        else:
            # array of shape `(n_samples, n_outputs)`

    return y_pred

def _process_decision_function(*, y_pred, target_type, classes, pos_label):
    """Get the response values when the response method is `decision_function`.

    In the binary case, it inverts the sign of the score if the positive label
    is not `classes[1]`. In the multi-label case, it stacks the predictions if
    they are not in the "compressed" format `(n_samples, n_outputs)`.

        Output of `estimator.decision_function`. The shape depends on the target type:

        - for binary classification, it is a 1d array of shape `(n_samples,)` where the
          sign is assuming that `classes[1]` is the positive class;
        - for multilabel classification, it is a 2d array of shape `(n_samples,
          n_outputs)`.

    Returns
    -------
    """
    if target_type == "binary" and pos_label == classes[0]:
        return -1 * y_pred

def _get_response_values(
    X,
    return_response_method_used=False,
):
    """Compute the response values of a classifier, an outlier detector, or a regressor.

    The response values are predictions such that it follows the following shape:

    - for binary classification, it is a 1d array of shape `(n_samples,)`;
    - for multiclass classification, it is a 2d array of shape `(n_samples, n_classes)`;
    - for multilabel classification, it is a 2d array of shape `(n_samples, n_outputs)`;
    - for outlier detection, it is a 1d array of shape `(n_samples,)`;
    - for regression, it is a 1d array of shape `(n_samples,)`.

    If `estimator` is a binary classifier, also return the label for the
    effective positive class.

    This utility is used primarily in the displays and the scikit-learn scorers.

        Fitted classifier, outlier detector, or regressor or a
        fitted :class:`~sklearn.pipeline.Pipeline` in which the last estimator is a
        classifier, an outlier detector, or a regressor.

        Input values.

    response_method : {"predict_proba", "predict_log_proba", "decision_function", \
            "predict"} or list of such str
        (i.e. :term:`predict_proba`, :term:`predict_log_proba`,
        :term:`decision_function` or :term:`predict`). Possible choices are:

        - if a list of `str`, it provides the method names in order of

        The class considered as the positive class when computing
        the metrics. If `None` and target is 'binary', `estimators.classes_[1]` is
        considered as the positive class.

    return_response_method_used : bool, default=False
        Whether to return the response method used to compute the response
        values.

    Returns
    -------
            (n_samples, n_outputs)
        Target scores calculated from the provided `response_method`
        and `pos_label`.

    pos_label : int, float, bool, str or None
        the metrics. Returns `None` if `estimator` is a regressor or an outlier
        detector.

    response_method_used : str
        The response method used to compute the response values. Only returned
        if `return_response_method_used` is `True`.

    Raises
    ------
        If `pos_label` is not a valid label.
        If the shape of `y_pred` is not consistent for binary classifier.
        If the response method can be applied to a classifier only and
        `estimator` is a regressor.
    """
    from sklearn.base import is_classifier, is_outlier_detector

    if is_classifier(estimator):
        prediction_method = _check_response_method(estimator, response_method)
        classes = estimator.classes_
        target_type = type_of_target(classes)

        if target_type in ("binary", "multiclass"):
            if pos_label is not None and pos_label not in classes.tolist():
                    f"pos_label={pos_label} is not a valid label: It should be "
                    f"one of {classes}"
                )
            elif pos_label is None and target_type == "binary":
                pos_label = classes[-1]

        y_pred = prediction_method(X)

        if prediction_method.__name__ in ("predict_proba", "predict_log_proba"):
            y_pred = _process_predict_proba(
                y_pred=y_pred,
                target_type=target_type,
                classes=classes,
                pos_label=pos_label,
            )
        elif prediction_method.__name__ == "decision_function":
            y_pred = _process_decision_function(
            )
    elif is_outlier_detector(estimator):
        y_pred, pos_label = prediction_method(X), None
    else:  # estimator is a regressor
        if response_method != "predict":
                f"{estimator.__class__.__name__} should either be a classifier to be "
                f"used with response_method={response_method} or the response_method "
                "should be 'predict'. Got a regressor with response_method="
                f"{response_method} instead."
            )
        prediction_method = estimator.predict

    if return_response_method_used:
        return y_pred, pos_label, prediction_method.__name__
    return y_pred, pos_label

def _get_response_values_binary(
    estimator, X, response_method, pos_label=None, return_response_method_used=False
):
    """Compute the response values of a binary classifier.

        Fitted classifier or a fitted :class:`~sklearn.pipeline.Pipeline`
        in which the last estimator is a binary classifier.

    response_method : {'auto', 'predict_proba', 'decision_function'}
        :term:`decision_function` as the target response. If set to 'auto',
        :term:`predict_proba` is tried first and if it does not exist
        :term:`decision_function` is tried next.

        the metrics. By default, `estimators.classes_[1]` is

        values.

    Returns
    -------
        Target scores calculated from the provided response_method
        and pos_label.

        the metrics.

    """
    classification_error = "Expected 'estimator' to be a binary classifier."

    if not is_classifier(estimator):
            classification_error + f" Got {estimator.__class__.__name__} instead."
        )
    elif len(estimator.classes_) != 2:
            classification_error + f" Got {len(estimator.classes_)} classes instead."
        )

        response_method = ["predict_proba", "decision_function"]

    return _get_response_values(
        X,
        response_method,
        return_response_method_used=return_response_method_used,
    )

def all_estimators(type_filter=None):
    """Get a list of all estimators from `sklearn`.

    This function crawls the module and gets all classes that inherit
    from BaseEstimator. Classes that are defined in test-modules are not
    included.

    type_filter : {"classifier", "regressor", "cluster", "transformer"} \
            or list of such str, default=None
        Which kind of estimators should be returned. If None, no filter is
        applied and all estimators are returned.  Possible values are
        'classifier', 'regressor', 'cluster' and 'transformer' to get
        estimators only of these specific types, or a list of these to
        get the estimators that fit at least one of the types.

    Returns
    -------
    estimators : list of tuples
        List of (name, class), where ``name`` is the class name as string
        and ``class`` is the actual type of the class.

    Examples
    --------
    >>> from sklearn.utils.discovery import all_estimators
    >>> estimators = all_estimators()
    >>> type(estimators)
    >>> type(estimators[0])
    <class 'tuple'>
    >>> estimators[:2]
    [('ARDRegression', <class 'sklearn.linear_model._bayes.ARDRegression'>),
     ('AdaBoostClassifier',
      <class 'sklearn.ensemble._weight_boosting.AdaBoostClassifier'>)]
    >>> classifiers = all_estimators(type_filter="classifier")
    >>> classifiers[:2]
    [('AdaBoostClassifier',
      <class 'sklearn.ensemble._weight_boosting.AdaBoostClassifier'>),
     ('BaggingClassifier', <class 'sklearn.ensemble._bagging.BaggingClassifier'>)]
    >>> regressors = all_estimators(type_filter="regressor")
    >>> regressors[:2]
     ('AdaBoostRegressor',
      <class 'sklearn.ensemble._weight_boosting.AdaBoostRegressor'>)]
    >>> both = all_estimators(type_filter=["classifier", "regressor"])
    >>> both[:2]
    """
    # lazy import to avoid circular imports from sklearn.base
    from ..base import (
        BaseEstimator,
        ClassifierMixin,
        ClusterMixin,
        RegressorMixin,
        TransformerMixin,
    )
    from ._testing import ignore_warnings

    def is_abstract(c):
        if not (hasattr(c, "__abstractmethods__")):
        if not len(c.__abstractmethods__):

    all_classes = []
    root = str(Path(__file__).parent.parent)  # sklearn package
    # Ignore deprecation warnings triggered at import time and from walking
    # packages
    with ignore_warnings(category=FutureWarning):
        for _, module_name, _ in pkgutil.walk_packages(path=[root], prefix="sklearn."):
            module_parts = module_name.split(".")
            if (
                any(part in _MODULE_TO_IGNORE for part in module_parts)
                or "._" in module_name
            ):
                continue
            module = import_module(module_name)
            classes = inspect.getmembers(module, inspect.isclass)
            classes = [
                (name, est_cls) for name, est_cls in classes if not name.startswith("_")
            ]

            all_classes.extend(classes)

    all_classes = set(all_classes)

        c
        for c in all_classes
        if (issubclass(c[1], BaseEstimator) and c[0] != "BaseEstimator")
    ]
    # get rid of abstract base classes
    estimators = [c for c in estimators if not is_abstract(c[1])]

    if type_filter is not None:
        if not isinstance(type_filter, list):
            type_filter = [type_filter]
        else:
            type_filter = list(type_filter)  # copy
        filtered_estimators = []
        filters = {
            "classifier": ClassifierMixin,
            "regressor": RegressorMixin,
            "transformer": TransformerMixin,
            "cluster": ClusterMixin,
        }
        for name, mixin in filters.items():
            if name in type_filter:
                type_filter.remove(name)
                filtered_estimators.extend(
                    [est for est in estimators if issubclass(est[1], mixin)]
                )
        estimators = filtered_estimators
        if type_filter:
                "Parameter type_filter must be 'classifier', "
                "'regressor', 'transformer', 'cluster' or "
                "None, got"
                f" {type_filter!r}."
            )

    # drop duplicates, sort for reproducibility
    # itemgetter is used to ensure the sort does not extend to the 2nd item of
    # the tuple
    return sorted(set(estimators), key=itemgetter(0))

def all_displays():
    """Get a list of all displays from `sklearn`.

    Returns
    -------
    displays : list of tuples
        List of (name, class), where ``name`` is the display class name as
        string and ``class`` is the actual type of the class.

    Examples
    --------
    >>> from sklearn.utils.discovery import all_displays
    >>> displays = all_displays()
    >>> displays[0]
    ('CalibrationDisplay', <class 'sklearn.calibration.CalibrationDisplay'>)
    """

    # packages
            if (
            ):
                continue
            classes = [
                (name, display_class)
                for name, display_class in classes
                if not name.startswith("_") and name.endswith("Display")
            ]

    return sorted(set(all_classes), key=itemgetter(0))

def _is_checked_function(item):
    if not inspect.isfunction(item):

    if item.__name__.startswith("_"):

    mod = item.__module__
    if not mod.startswith("sklearn.") or mod.endswith("estimator_checks"):

def all_functions():
    """Get a list of all functions from `sklearn`.

    Returns
    -------
    functions : list of tuples
        List of (name, function), where ``name`` is the function name as
        string and ``function`` is the actual function.

    Examples
    --------
    >>> from sklearn.utils.discovery import all_functions
    >>> functions = all_functions()
    >>> name, function = functions[0]
    >>> name
    'accuracy_score'
    """

    all_functions = []
    # packages
            if (
            ):
                continue

            functions = inspect.getmembers(module, _is_checked_function)
            functions = [
                (func.__name__, func)
                for name, func in functions
                if not name.startswith("_")
            ]
            all_functions.extend(functions)

    # the tuple
    return sorted(set(all_functions), key=itemgetter(0))

def _raise_for_missing_tags(estimator, tag_name, Mixin):
    tags = get_tags(estimator)
    estimator_type = Mixin.__name__.replace("Mixin", "")
    if getattr(tags, tag_name) is None:
        raise RuntimeError(
            f"Estimator {estimator.__class__.__name__} seems to be a {estimator_type},"
            f" but the `{tag_name}` tag is not set. Either set the tag manually"
            f" or inherit from the {Mixin.__name__}. Note that the order of inheritance"
            f" matters, the {Mixin.__name__} should come before BaseEstimator."
        )

def _yield_api_checks(estimator):
    if not isinstance(estimator, BaseEstimator):
            f"Estimator {estimator.__class__.__name__} does not inherit from"
            " `sklearn.base.BaseEstimator`. This might lead to unexpected behavior, or"
            " even errors when collecting tests.",
            category=UserWarning,
        )

    yield check_estimator_cloneable
    yield check_estimator_tags_renamed
    yield check_valid_tag_types
    yield check_estimator_repr
    yield check_no_attributes_set_in_init
    yield check_fit_score_takes_y
    yield check_estimators_overwrite_params
    yield check_dont_overwrite_parameters
    yield check_estimators_fit_returns_self
    yield check_readonly_memmap_input
    if tags.requires_fit:
        yield check_estimators_unfitted
    yield check_do_not_raise_errors_in_init_or_set_params
    yield check_n_features_in_after_fitting
    yield check_mixin_order
    yield check_positive_only_tag_during_fit

def _yield_checks(estimator):

    yield check_estimators_dtypes
    if has_fit_parameter(estimator, "sample_weight"):
        yield check_sample_weights_pandas_series
        yield check_sample_weights_not_an_array
        yield check_sample_weights_list
        if not tags.input_tags.pairwise:
            # We skip pairwise because the data is not pairwise
            yield check_sample_weights_shape
            yield check_sample_weights_not_overwritten
            yield check_sample_weight_equivalence_on_dense_data
            if tags.input_tags.sparse:
                yield check_sample_weight_equivalence_on_sparse_data

    # Check that all estimator yield informative messages when
    # trained on empty datasets
    if not tags.no_validation:
        yield check_complex_data
        yield check_dtype_object
        yield check_estimators_empty_data_messages

    if name not in CROSS_DECOMPOSITION:
        # cross-decomposition's "transform" returns X and Y
        yield check_pipeline_consistency

    if not tags.input_tags.allow_nan and not tags.no_validation:
        # Test that all estimators check their input for NaN's and infs
        yield check_estimators_nan_inf

    if tags.input_tags.pairwise:
        # Check that pairwise estimator throws error on non-square input
        yield check_nonsquare_error

    if hasattr(estimator, "sparsify"):
        yield check_sparsify_coefficients

    yield check_estimator_sparse_tag
    yield check_estimator_sparse_array
    yield check_estimator_sparse_matrix

    # Test that estimators can be pickled, and once pickled
    # give the same answer as before.
    yield check_estimators_pickle
    yield partial(check_estimators_pickle, readonly_memmap=True)

    if tags.array_api_support:
        for check in _yield_array_api_checks(estimator):
            yield check

    yield check_f_contiguous_array_estimator

def _yield_classifier_checks(classifier):
    _raise_for_missing_tags(classifier, "classifier_tags", ClassifierMixin)
    tags = get_tags(classifier)

    # test classifiers can handle non-array data and pandas objects
    yield check_classifier_data_not_an_array
    # test classifiers trained on a single label always return this label
    yield check_classifiers_one_label
    yield check_classifiers_one_label_sample_weights
    yield check_classifiers_classes
    yield check_estimators_partial_fit_n_features
    if tags.target_tags.multi_output:
        yield check_classifier_multioutput
    # basic consistency testing
    yield check_classifiers_train
    yield partial(check_classifiers_train, readonly_memmap=True)
    yield partial(check_classifiers_train, readonly_memmap=True, X_dtype="float32")
    yield check_classifiers_regression_target
    if tags.classifier_tags.multi_label:
        yield check_classifiers_multilabel_representation_invariance
        yield check_classifiers_multilabel_output_format_predict
        yield check_classifiers_multilabel_output_format_predict_proba
        yield check_classifiers_multilabel_output_format_decision_function
        yield check_supervised_y_no_nan
        if tags.target_tags.single_output:
            yield check_supervised_y_2d
    if "class_weight" in classifier.get_params().keys():
        yield check_class_weight_classifiers

    yield check_non_transformer_estimators_n_iter
    # test if predict_proba is a monotonic transformation of decision_function
    yield check_decision_proba_consistency

    if isinstance(classifier, LinearClassifierMixin):
        if "class_weight" in classifier.get_params().keys():
            yield check_class_weight_balanced_linear_classifier
    if (
        isinstance(classifier, LinearClassifierMixin)
        and "class_weight" in classifier.get_params().keys()
    ):
        yield check_class_weight_balanced_linear_classifier

    if not tags.classifier_tags.multi_class:
        yield check_classifier_not_supporting_multiclass

def _yield_regressor_checks(regressor):
    _raise_for_missing_tags(regressor, "regressor_tags", RegressorMixin)
    tags = get_tags(regressor)
    # TODO: test with intercept
    # TODO: test with multiple responses
    # basic testing
    yield check_regressors_train
    yield partial(check_regressors_train, readonly_memmap=True)
    yield partial(check_regressors_train, readonly_memmap=True, X_dtype="float32")
    yield check_regressor_data_not_an_array
        yield check_regressor_multioutput
    yield check_regressors_no_decision_function
    if not tags.no_validation and tags.target_tags.single_output:
        yield check_supervised_y_2d
    yield check_supervised_y_no_nan
    name = regressor.__class__.__name__
    if name != "CCA":
        # check that the regressor handles int input
        yield check_regressors_int

def _yield_transformer_checks(transformer):
    _raise_for_missing_tags(transformer, "transformer_tags", TransformerMixin)
    tags = get_tags(transformer)
    # All transformers should either deal with sparse data or raise an
    # exception with type TypeError and an intelligible error message
        yield check_transformer_data_not_an_array
    # these don't actually fit the data, so don't raise errors
    yield check_transformer_general
    if tags.transformer_tags.preserves_dtype:
        yield check_transformer_preserve_dtypes
    yield partial(check_transformer_general, readonly_memmap=True)
    if get_tags(transformer).requires_fit:
        yield check_transformers_unfitted
    else:
        yield check_transformers_unfitted_stateless
    # Dependent on external solvers and hence accessing the iter
    # param is non-trivial.
    external_solver = [
        "Isomap",
        "KernelPCA",
        "LocallyLinearEmbedding",
        "LogisticRegressionCV",
        "BisectingKMeans",
    ]

    name = transformer.__class__.__name__
    if name not in external_solver:
        yield check_transformer_n_iter

def _yield_clustering_checks(clusterer):
    yield check_clusterer_compute_labels_predict
    name = clusterer.__class__.__name__
    if name not in ("WardAgglomeration", "FeatureAgglomeration"):
        # this is clustering on the features
        # let's not test that here.
        yield check_clustering
        yield partial(check_clustering, readonly_memmap=True)
        yield check_estimators_partial_fit_n_features
    if not hasattr(clusterer, "transform"):
        yield check_non_transformer_estimators_n_iter

def _yield_outliers_checks(estimator):
    # checks for the contamination parameter
    if hasattr(estimator, "contamination"):
        yield check_outlier_contamination

    # checks for outlier detectors that have a fit_predict method
    if hasattr(estimator, "fit_predict"):
        yield check_outliers_fit_predict

    # checks for estimators that can be used on a test set
    if hasattr(estimator, "predict"):
        yield check_outliers_train
        yield partial(check_outliers_train, readonly_memmap=True)
        # test outlier detectors can handle non-array data
        yield check_classifier_data_not_an_array

def _yield_array_api_checks(estimator):
    for (
        device,
    ) in yield_namespace_device_dtype_combinations():
        yield partial(
            check_array_api_input,
            array_namespace=array_namespace,
            dtype_name=dtype_name,
            device=device,
        )

def _yield_all_checks(estimator, legacy: bool):
    if not tags.input_tags.two_d_array:
            "Can't test estimator {} which requires input  of type {}".format(
                name, tags.input_tags
            ),
            SkipTestWarning,
        )
        return
    if tags._skip_test:
            "Explicit SKIP via _skip_test tag for estimator {}.".format(name),
        )
        return

    for check in _yield_api_checks(estimator):
        yield check

    if not legacy:
        return  # pragma: no cover

    for check in _yield_checks(estimator):
        for check in _yield_classifier_checks(estimator):
        for check in _yield_regressor_checks(estimator):
    if hasattr(estimator, "transform"):
        for check in _yield_transformer_checks(estimator):
    if isinstance(estimator, ClusterMixin):
        for check in _yield_clustering_checks(estimator):
    if is_outlier_detector(estimator):
        for check in _yield_outliers_checks(estimator):
    yield check_parameters_default_constructible
    if not tags.non_deterministic:
        yield check_methods_sample_order_invariance
        yield check_methods_subset_invariance
    yield check_fit2d_1sample
    yield check_fit2d_1feature
    yield check_get_params_invariance
    yield check_set_params
    yield check_dict_unchanged
    yield check_fit_idempotent
    yield check_fit_check_is_fitted
        yield check_n_features_in
        yield check_fit1d
        yield check_fit2d_predict1d
        if tags.target_tags.required:
            yield check_requires_y_none
    if tags.input_tags.positive_only:
        yield check_fit_non_negative

def _check_name(check):
    if hasattr(check, "__wrapped__"):
        return _check_name(check.__wrapped__)
    return check.func.__name__ if isinstance(check, partial) else check.__name__

def _maybe_mark(
    check,
    expected_failed_checks: dict[str, str] | None = None,
    mark: Literal["xfail", "skip", None] = None,
    pytest=None,
):
    """Mark the test as xfail or skip if needed.

        Estimator instance for which to generate checks.
    check : partial or callable
        Check to be marked.
    expected_failed_checks : dict[str, str], default=None
        Dictionary of the form {check_name: reason} for checks that are expected to
        fail.
    mark : "xfail" or "skip" or None
        Whether to mark the check as xfail or skip.
    pytest : pytest module, default=None
        Pytest module to use to mark the check. This is only needed if ``mark`` is
        `"xfail"`. Note that one can run `check_estimator` without having `pytest`
        installed. This is used in combination with `parametrize_with_checks` only.
    """
    should_be_marked, reason = _should_be_skipped_or_marked(
        estimator, check, expected_failed_checks
    )
    if not should_be_marked or mark is None:
        return estimator, check

    if mark == "xfail":
        return pytest.param(estimator, check, marks=pytest.mark.xfail(reason=reason))
    else:

        @wraps(check)
        def wrapped(*args, **kwargs):
            raise SkipTest(
                f"Skipping {_check_name(check)} for {estimator_name}: {reason}"
            )

        return estimator, wrapped

def _should_be_skipped_or_marked(
    estimator, check, expected_failed_checks: dict[str, str] | None = None
) -> tuple[bool, str]:
    """Check whether a check should be skipped or marked as xfail.

        fail.

    Returns
    -------
    should_be_marked : bool
        Whether the check should be marked as xfail or skipped.
        Reason for skipping the check.
    """

    expected_failed_checks = expected_failed_checks or {}

    check_name = _check_name(check)
    if check_name in expected_failed_checks:
        return True, expected_failed_checks[check_name]

    return False, "Check is not expected to fail"

def estimator_checks_generator(
    *,
    legacy: bool = True,
):
    """Iteratively yield all check callables for an estimator.

    legacy : bool, default=True
        Whether to include legacy checks. Over time we remove checks from this category
        and move them into their specific category.
        fail.
    mark : {"xfail", "skip"} or None, default=None
        Whether to mark the checks that are expected to fail as
        xfail(`pytest.mark.xfail`) or skip. Marking a test as "skip" is done via
        wrapping the check in a function that raises a
        :class:`~sklearn.exceptions.SkipTest` exception.

    Returns
    -------
    estimator_checks_generator : generator
        Generator that yields (estimator, check) tuples.
    """
        import pytest
    else:
        pytest = None  # type: ignore[assignment]

    name = type(estimator).__name__
    # First check that the estimator is cloneable which is needed for the rest
    # of the checks to run
    yield estimator, partial(check_estimator_cloneable, name)
    for check in _yield_all_checks(estimator, legacy=legacy):
        check_with_name = partial(check, name)
        for check_instance in _yield_instances_for_check(check, estimator):
            yield _maybe_mark(
                check_instance,
                check_with_name,
                expected_failed_checks=expected_failed_checks,
                mark=mark,
                pytest=pytest,
            )

def parametrize_with_checks(
    *,
    expected_failed_checks: Callable | None = None,
):
    """Pytest specific decorator for parametrizing estimator checks.

    Checks are categorised into the following groups:

    - API checks: a set of checks to ensure API compatibility with scikit-learn.
      Refer to https://scikit-learn.org/dev/developers/develop.html a requirement of
      scikit-learn estimators.
    - legacy: a set of checks which gradually will be grouped into other categories.

    The `id` of each check is set to be a pprint version of the estimator
    and the name of the check with its keyword arguments.
    This allows to use `pytest -k` to specify which tests to run::

        pytest test_check_estimators.py -k check_estimators_fit_returns_self

    estimators : list of estimators instances
        Estimators to generated checks for.

           Passing a class was deprecated in version 0.23, and support for
           classes was removed in 0.24. Pass an instance instead.

    expected_failed_checks : callable, default=None
        A callable that takes an estimator as input and returns a dictionary of the
        form::

            {
                "check_name": "my reason",
            }

        Where `"check_name"` is the name of the check, and `"my reason"` is why
        the check fails. These tests will be marked as xfail if the check fails.

    Returns
    -------
    decorator : `pytest.mark.parametrize`

    See Also
    --------
    check_estimator : Check if estimator adheres to scikit-learn conventions.

    Examples
    --------
    >>> from sklearn.utils.estimator_checks import parametrize_with_checks

    >>> @parametrize_with_checks([LogisticRegression(),
    ...                           DecisionTreeRegressor()])
    ... def test_sklearn_compatible_estimator(estimator, check):
    ...     check(estimator)

    """

    if any(isinstance(est, type) for est in estimators):
        msg = (
            "Passing a class was deprecated in version 0.23 "
            "and isn't supported anymore from 0.24."
            "Please pass an instance instead."
        )

    def _checks_generator(estimators, legacy, expected_failed_checks):
        for estimator in estimators:
            args = {"estimator": estimator, "legacy": legacy, "mark": "xfail"}
            if callable(expected_failed_checks):
                args["expected_failed_checks"] = expected_failed_checks(estimator)
            yield from estimator_checks_generator(**args)

    return pytest.mark.parametrize(
        "estimator, check",
        _checks_generator(estimators, legacy, expected_failed_checks),
        ids=_get_check_estimator_ids,
    )

def check_estimator(
    estimator=None,
    generate_only=False,
    *,
    on_skip: Literal["warn"] | None = "warn",
    on_fail: Literal["raise", "warn"] | None = "raise",
    callback: Callable | None = None,
):
    """Check if estimator adheres to scikit-learn conventions.

    This function will run an extensive test-suite for input validation,
    shapes, etc, making sure that the estimator complies with `scikit-learn`
    conventions as detailed in :ref:`rolling_your_own_estimator`.
    Additional tests for classifiers, regressors, clustering or transformers
    will be run if the Estimator class inherits from the corresponding mixin
    from sklearn.base.

    scikit-learn also provides a pytest specific decorator,
    :func:`~sklearn.utils.estimator_checks.parametrize_with_checks`, making it
    easier to test multiple estimators.

        Estimator instance to check.

    generate_only : bool, default=False
        When `False`, checks are evaluated when `check_estimator` is called.
        When `True`, `check_estimator` returns a generator that yields
        (estimator, check) tuples. The check is run by calling
        `check(estimator)`.

            `generate_only` will be removed in 1.8. Use
            :func:`~sklearn.utils.estimator_checks.estimator_checks_generator` instead.

    expected_failed_checks : dict, default=None
        A dictionary of the form::

            {
                "check_name": "this check is expected to fail because ...",
            }

        the check fails.

    on_skip : "warn", None, default="warn"
        This parameter controls what happens when a check is skipped.

        - "warn": A :class:`~sklearn.exceptions.SkipTestWarning` is logged
          and running tests continue.
        - None: No warning is logged and running tests continue.

    on_fail : {"raise", "warn"}, None, default="raise"
        This parameter controls what happens when a check fails.

        - "raise": The exception raised by the first failing check is raised and
          running tests are aborted. This does not included tests that are expected
          to fail.
        - "warn": A :class:`~sklearn.exceptions.EstimatorCheckFailedWarning` is logged
        - None: No exception is raised and no warning is logged.

        Note that if ``on_fail != "raise"``, no exception is raised, even if the checks
        fail. You'd need to inspect the return result of ``check_estimator`` to check
        if any checks failed.

    callback : callable, or None, default=None
        This callback will be called with the estimator and the check name,
        the exception (if any), the status of the check (xfail, failed, skipped,
        passed), and the reason for the expected failure if the check is
        expected to fail. The callable's signature needs to be::

            def callback(
                check_name: str,
                exception: Exception,
                status: Literal["xfail", "failed", "skipped", "passed"],
                expected_to_fail: bool,
                expected_to_fail_reason: str,
            )

        ``callback`` cannot be provided together with ``on_fail="raise"``.

    Returns
    -------
    test_results : list
        List of dictionaries with the results of the failing tests, of the form::

            {
                "estimator": estimator,
                "check_name": check_name,
                "exception": exception,
                "status": status (one of "xfail", "failed", "skipped", "passed"),
                "expected_to_fail": expected_to_fail,
                "expected_to_fail_reason": expected_to_fail_reason,
            }

        Generator that yields (estimator, check) tuples. Returned when
        `generate_only=True`.

        ..
            TODO(1.8): remove return value

            ``generate_only`` will be removed in 1.8. Use

    Raises
    ------
    Exception
        If ``on_fail="raise"``, the exception raised by the first failing check is
        raised and running tests are aborted.

    See Also
    --------
    parametrize_with_checks : Pytest specific decorator for parametrizing estimator
        checks.
    estimator_checks_generator : Generator that yields (estimator, check) tuples.

    Examples
    --------
    >>> from sklearn.utils.estimator_checks import check_estimator
    >>> check_estimator(LogisticRegression())
    [...]
    """
        msg = (
        )

    if on_fail == "raise" and callback is not None:
        raise ValueError("callback cannot be provided together with on_fail='raise'")

    # TODO(1.8): remove generate_only
    if generate_only:
            "`generate_only` is deprecated in 1.6 and will be removed in 1.8. "
            "Use :func:`~sklearn.utils.estimator_checks.estimator_checks_generator` "
            "instead.",
        )
        return estimator_checks_generator(
            estimator, legacy=legacy, expected_failed_checks=None, mark="skip"
        )

    test_results = []

    for estimator, check in estimator_checks_generator(
        legacy=legacy,
        expected_failed_checks=expected_failed_checks,
        # Not marking tests to be skipped here, we run and simulate an xfail behavior
        mark=None,
    ):
        test_can_fail, reason = _should_be_skipped_or_marked(
            estimator, check, expected_failed_checks
        )
        try:
            check(estimator)
        except SkipTest as e:
            # We get here if the test raises SkipTest, which is expected in cases where
            # the check cannot run for instance if a required dependency is not
            # installed.
            check_result = {
                "check_name": _check_name(check),
                "exception": e,
                "status": "skipped",
                "expected_to_fail": test_can_fail,
                "expected_to_fail_reason": reason,
            }
            if on_skip == "warn":
                    f"Skipping check {_check_name(check)} for {name} because it raised "
                    f"{type(e).__name__}: {e}",
                    SkipTestWarning,
                )
            if on_fail == "raise" and not test_can_fail:
                raise

            }

            if test_can_fail:
                # This check failed, but could be expected to fail, therefore we mark it
                # as xfail.
                check_result["status"] = "xfail"
            else:
                check_result["status"] = "failed"

            if on_fail == "warn":
                warning = EstimatorCheckFailedWarning(**check_result)
                warnings.warn(warning)
        else:
                "exception": None,
                "status": "passed",
            }

        test_results.append(check_result)

        if callback:
            callback(**check_result)

    return test_results

def _regression_dataset():
    global REGRESSION_DATASET
    if REGRESSION_DATASET is None:
            n_samples=200,
            n_features=10,
            n_informative=1,
            bias=5.0,
            noise=20,
        )
        REGRESSION_DATASET = X, y
    return REGRESSION_DATASET

class _NotAnArray:
    """An object that is convertible to an array.

    data : array-like
        The data.
    """

    def __init__(self, data):
        self.data = np.asarray(data)

    def __array__(self, dtype=None, copy=None):
        return self.data

    def __array_function__(self, func, types, args, kwargs):
        if func.__name__ == "may_share_memory":
        raise TypeError("Don't want to call array_function {}!".format(func.__name__))

def _is_pairwise_metric(estimator):
    """Returns True if estimator accepts pairwise metric.

    Returns
    -------
    out : bool
        True if _pairwise is set to True and False otherwise.
    """
    metric = getattr(estimator, "metric", None)

    return bool(metric == "precomputed")

def _generate_sparse_data(X_csr):
    """Generate sparse matrices or arrays with {32,64}bit indices of diverse format.

    X_csr: scipy.sparse.csr_matrix or scipy.sparse.csr_array
        Input in CSR format.

    Returns
    -------
    out: iter(Matrices) or iter(Arrays)
        In format['dok', 'lil', 'dia', 'bsr', 'csr', 'csc', 'coo',
        'coo_64', 'csc_64', 'csr_64']
    """

    assert X_csr.format == "csr"
    yield "csr", X_csr.copy()
    for sparse_format in ["dok", "lil", "dia", "bsr", "csc", "coo"]:
        yield sparse_format, X_csr.asformat(sparse_format)

    # Generate large indices matrix only if its supported by scipy
    X_coo = X_csr.asformat("coo")
    X_coo.row = X_coo.row.astype("int64")
    X_coo.col = X_coo.col.astype("int64")
    yield "coo_64", X_coo

    for sparse_format in ["csc", "csr"]:
        X = X_csr.asformat(sparse_format)
        X.indices = X.indices.astype("int64")
        X.indptr = X.indptr.astype("int64")
        yield sparse_format + "_64", X

def check_supervised_y_no_nan(name, estimator_orig):
    # Checks that the Estimator targets are not NaN.
    estimator = clone(estimator_orig)
    rng = np.random.RandomState(888)
    X = rng.standard_normal(size=(10, 5))

    for value in [np.nan, np.inf]:
        y = np.full(10, value)
        y = _enforce_estimator_tags_y(estimator, y)

        module_name = estimator.__module__
        if module_name.startswith("sklearn.") and not (
            "test_" in module_name or module_name.endswith("_testing")
        ):
            # In scikit-learn we want the error message to mention the input
            # name and be specific about the kind of unexpected value.
            if np.isinf(value):
                match = (
                    r"Input (y|Y) contains infinity or a value too large for"
                    r" dtype\('float64'\)."
                )
            else:
                match = r"Input (y|Y) contains NaN."
        else:
            # Do not impose a particular error message to third-party libraries.
            match = None
        err_msg = (
            f"Estimator {name} should have raised error on fitting array y with inf"
            " value."
        )
        with raises(ValueError, match=match, err_msg=err_msg):
            estimator.fit(X, y)

def check_array_api_input(
    name,
    estimator_orig,
    array_namespace,
    device=None,
    dtype_name="float64",
    check_values=False,
):
    """Check that the estimator can work consistently with the Array API

    By default, this just checks that the types and shapes of the arrays are
    consistent with calling the same estimator with numpy arrays.

    When check_values is True, it also checks that calling the estimator on the
    array_api Array gives the same results as ndarrays.
    """

    X = _enforce_estimator_tags_X(estimator_orig, X)
    y = _enforce_estimator_tags_y(estimator_orig, y)

    est = clone(estimator_orig)

    array_attributes = {
        key: value for key, value in vars(est).items() if isinstance(value, np.ndarray)
    }

        input_ns = get_namespace(X_xp)[0].__name__

    # Fitted attributes which are arrays must have the same
    # namespace as the one of the training data.
    for key, attribute in array_attributes.items():
        est_xp_param = getattr(est_xp, key)
            attribute_ns = get_namespace(est_xp_param)[0].__name__
        assert attribute_ns == input_ns, (
            f"'{key}' attribute is in wrong namespace, expected {input_ns} "
            f"got {attribute_ns}"
        )

        assert array_device(est_xp_param) == array_device(X_xp)

        est_xp_param_np = _convert_to_numpy(est_xp_param, xp=xp)
        if check_values:
                attribute,
                est_xp_param_np,
                err_msg=f"{key} not the same",
                atol=_atol_for_type(X.dtype),
            )
        else:
            assert attribute.shape == est_xp_param_np.shape
            assert attribute.dtype == est_xp_param_np.dtype

    # Check estimator methods, if supported, give the same results
    methods = (
        "score",
        "score_samples",
        "predict",
    )

    try:
        np.asarray(X_xp)
        np.asarray(y_xp)
        # TODO There are a few errors in SearchCV with array-api-strict because
        # we end up doing X[train_indices] where X is an array-api-strict array
        # and train_indices is a numpy array. array-api-strict insists
        # train_indices should be an array-api-strict array. On the other hand,
        # all the array API libraries (PyTorch, jax, CuPy) accept indexing with a
        # numpy array. This is probably not worth doing anything about for
        # now since array-api-strict seems a bit too strict ...
        numpy_asarray_works = xp.__name__ != "array_api_strict"

        numpy_asarray_works = False

    if numpy_asarray_works:
        # In this case, array_api_dispatch is disabled and we rely on np.asarray
        # being called to convert the non-NumPy inputs to NumPy arrays when needed.
        est_fitted_with_as_array = clone(est).fit(X_xp, y_xp)
        # We only do a smoke test for now, in order to avoid complicating the
        # test function even further.
        for method_name in methods:
            method = getattr(est_fitted_with_as_array, method_name, None)
            if method is None:
                continue

            if method_name == "score":
                method(X_xp, y_xp)
            else:
                method(X_xp)

    for method_name in methods:
        method = getattr(est, method_name, None)
        if method is None:
            continue

        if method_name == "score":
            result = method(X, y)
                result_xp = getattr(est_xp, method_name)(X_xp, y_xp)
            # score typically returns a Python float
            assert isinstance(result, float)
            assert isinstance(result_xp, float)
            if check_values:
                assert abs(result - result_xp) < _atol_for_type(X.dtype)
            continue
        else:
            result = method(X)
                result_xp = getattr(est_xp, method_name)(X_xp)

            result_ns = get_namespace(result_xp)[0].__name__
        assert result_ns == input_ns, (
            f"'{method}' output is in wrong namespace, expected {input_ns}, "
            f"got {result_ns}."
        )

        assert array_device(result_xp) == array_device(X_xp)
        result_xp_np = _convert_to_numpy(result_xp, xp=xp)

                result,
                result_xp_np,
                err_msg=f"{method} did not the return the same result",
            )
        else:
            if hasattr(result, "shape"):
                assert result.shape == result_xp_np.shape
                assert result.dtype == result_xp_np.dtype

        if method_name == "transform" and hasattr(est, "inverse_transform"):
            inverse_result = est.inverse_transform(result)
                invese_result_xp = est_xp.inverse_transform(result_xp)
                inverse_result_ns = get_namespace(invese_result_xp)[0].__name__
            assert inverse_result_ns == input_ns, (
                "'inverse_transform' output is in wrong namespace, expected"
                f" {input_ns}, got {inverse_result_ns}."
            )

            assert array_device(invese_result_xp) == array_device(X_xp)

            invese_result_xp_np = _convert_to_numpy(invese_result_xp, xp=xp)
                assert_allclose(
                    inverse_result,
                    invese_result_xp_np,
                    err_msg="inverse_transform did not the return the same result",
                    atol=_atol_for_type(X.dtype),
                )
            else:
                assert inverse_result.shape == invese_result_xp_np.shape
                assert inverse_result.dtype == invese_result_xp_np.dtype

def check_array_api_input_and_values(
    name,
):
    return check_array_api_input(
        name,
        estimator_orig,
        array_namespace=array_namespace,
        dtype_name=dtype_name,
        check_values=True,
    )

def check_estimator_sparse_tag(name, estimator_orig):
    """Check that estimator tag related with accepting sparse data is properly set."""

    n_samples = 15 if name == "SpectralCoclustering" else 40
    X = rng.uniform(size=(n_samples, 3))
    X[X < 0.6] = 0
    y = rng.randint(0, 3, size=n_samples)
    X = _enforce_estimator_tags_X(estimator, X)
    y = _enforce_estimator_tags_y(estimator, y)
    X = sparse.csr_array(X)

    if tags.input_tags.sparse:
        try:
            estimator.fit(X, y)  # should pass
            err_msg = (
                f"Estimator {name} raised an exception. "
                f"The tag self.input_tags.sparse={tags.input_tags.sparse} "
                "might not be consistent with the estimator's ability to "
                "handle sparse data (i.e. controlled by the parameter `accept_sparse`"
                " in `validate_data` or `check_array` functions)."
            )
            raise AssertionError(err_msg) from e
    else:
        err_msg = (
            f"Estimator {name} raised an exception. "
            "The estimator failed when fitted on sparse data in accordance "
            f"with its tag self.input_tags.sparse={tags.input_tags.sparse} "
            "but didn't raise the appropriate error: error message should "
            "state explicitly that sparse input is not supported if this is "
            "not the case, e.g. by using check_array(X, accept_sparse=False)."
        )
        try:
            estimator.fit(X, y)  # should fail with appropriate error
        except (ValueError, TypeError) as e:
            if re.search("[Ss]parse", str(e)):
                # Got the right error type and mentioning sparse issue
                return
            f"Estimator {name} didn't fail when fitted on sparse data "
            "but should have according to its tag "
            f"self.input_tags.sparse={tags.input_tags.sparse}. "
            f"The tag is inconsistent and must be fixed."
        )

def _check_estimator_sparse_container(name, estimator_orig, sparse_type):
    X = rng.uniform(size=(40, 3))
    y = (4 * rng.uniform(size=X.shape[0])).astype(np.int32)
    # catch deprecation warnings
        estimator = clone(estimator_orig)
    tags = get_tags(estimator_orig)
    for matrix_format, X in _generate_sparse_data(sparse_type(X)):
        # catch deprecation warnings
            estimator = clone(estimator_orig)
            if name in ["Scaler", "StandardScaler"]:
                estimator.set_params(with_mean=False)
        # fit and predict
        if "64" in matrix_format:
            err_msg = (
                f"Estimator {name} doesn't seem to support {matrix_format} "
                "matrix, and is not failing gracefully, e.g. by using "
                "check_array(X, accept_large_sparse=False)."
            )
        else:
            err_msg = (
                f"Estimator {name} doesn't seem to fail gracefully on sparse "
                "data: error message should state explicitly that sparse "
                "input is not supported if this is not the case, e.g. by using "
                "check_array(X, accept_sparse=False)."
            )
        with raises(
            (TypeError, ValueError),
            match=["sparse", "Sparse"],
            may_pass=True,
        ):
            with ignore_warnings(category=FutureWarning):
                estimator.fit(X, y)
            if hasattr(estimator, "predict"):
                pred = estimator.predict(X)
                if tags.target_tags.multi_output and not tags.target_tags.single_output:
                    assert pred.shape == (X.shape[0], 1)
                else:
                    assert pred.shape == (X.shape[0],)
                probs = estimator.predict_proba(X)
                if not tags.classifier_tags.multi_class:
                    expected_probs_shape = (X.shape[0], 2)
                else:
                    expected_probs_shape = (X.shape[0], 4)
                assert probs.shape == expected_probs_shape

def check_estimator_sparse_matrix(name, estimator_orig):
    _check_estimator_sparse_container(name, estimator_orig, sparse.csr_matrix)

def check_estimator_sparse_array(name, estimator_orig):
    _check_estimator_sparse_container(name, estimator_orig, sparse.csr_array)

def check_f_contiguous_array_estimator(name, estimator_orig):
    # https://github.com/scikit-learn/scikit-learn/issues/23988
    # https://github.com/scikit-learn/scikit-learn/issues/24013

    X = 3 * rng.uniform(size=(20, 3))
    y = X[:, 0].astype(int)

        estimator.transform(X)

        estimator.predict(X)

def check_sample_weights_pandas_series(name, estimator_orig):
    # check that estimators will accept a 'sample_weight' parameter of
    # type pandas.Series in the 'fit' function.
    try:

        X = np.array(
            [
                [1, 1],
                [1, 2],
                [1, 3],
                [1, 4],
                [2, 1],
                [2, 2],
                [2, 3],
                [2, 4],
                [3, 1],
                [3, 2],
                [3, 3],
                [3, 4],
            ]
        )
        X = pd.DataFrame(_enforce_estimator_tags_X(estimator_orig, X), copy=False)
        y = pd.Series([1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 2, 2])
        weights = pd.Series([1] * 12)
        if (
            not get_tags(estimator).target_tags.single_output
            and get_tags(estimator).target_tags.multi_output
        ):
            y = pd.DataFrame(y, copy=False)
        try:
            estimator.fit(X, y, sample_weight=weights)
                "Estimator {0} raises error if "
                "'sample_weight' parameter is of "
                "type pandas.Series".format(name)
            )
        raise SkipTest(
            "pandas is not installed: not testing for "
            "input of type pandas.Series to class weight."
        )

def check_sample_weights_not_an_array(name, estimator_orig):
    # type _NotAnArray in the 'fit' function.
        [
            [1, 1],
            [1, 2],
            [1, 3],
            [1, 4],
            [2, 1],
            [2, 2],
            [2, 3],
            [2, 4],
            [3, 1],
            [3, 2],
            [3, 3],
            [3, 4],
        ]
    )
    X = _NotAnArray(_enforce_estimator_tags_X(estimator_orig, X))
    y = _NotAnArray([1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 2, 2])
    weights = _NotAnArray([1] * 12)
    if not tags.target_tags.single_output and tags.target_tags.multi_output:
        y = _NotAnArray(y.data.reshape(-1, 1))
    estimator.fit(X, y, sample_weight=weights)

def check_sample_weights_list(name, estimator_orig):
    # type list in the 'fit' function.
    X = _enforce_estimator_tags_X(estimator_orig, rnd.uniform(size=(n_samples, 3)))
    y = np.arange(n_samples) % 3
    sample_weight = [3] * n_samples
    # Test that estimators don't raise any exception

def check_sample_weights_shape(name, estimator_orig):
    # check that estimators raise an error if sample_weight
    # shape mismatches the input
        [
            [1, 3],
            [1, 3],
            [1, 3],
            [1, 3],
            [2, 1],
            [2, 1],
            [2, 1],
            [2, 1],
            [3, 3],
            [3, 3],
            [3, 3],
            [3, 3],
            [4, 1],
            [4, 1],
            [4, 1],
            [4, 1],
        ]
    )
    y = np.array([1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2])

    estimator.fit(X, y, sample_weight=np.ones(len(y)))

    with raises(ValueError):
        estimator.fit(X, y, sample_weight=np.ones(2 * len(y)))

        estimator.fit(X, y, sample_weight=np.ones((len(y), 2)))

def _check_sample_weight_equivalence(name, estimator_orig, sparse_container):
    # check that setting sample_weight to zero / integer is equivalent
    # to removing / repeating corresponding samples.
    estimator_weighted = clone(estimator_orig)
    estimator_repeated = clone(estimator_orig)
    set_random_state(estimator_weighted, random_state=0)
    set_random_state(estimator_repeated, random_state=0)

    X = rng.rand(n_samples, n_samples * 2)
    # Use random integers (including zero) as weights.
    sw = rng.randint(0, 5, size=n_samples)

    X_weighted = X
    y_weighted = y
    # repeat samples according to weights
    X_repeated = X_weighted.repeat(repeats=sw, axis=0)
    y_repeated = y_weighted.repeat(repeats=sw)

    X_weighted, y_weighted, sw = shuffle(X_weighted, y_weighted, sw, random_state=0)

    # when the estimator has an internal CV scheme
    # we only use weights / repetitions in a specific CV group (here group=0)
    if "cv" in estimator_orig.get_params():
        groups_weighted = np.hstack(
            [np.full_like(y_weighted, 0), np.full_like(y, 1), np.full_like(y, 2)]
        )
        sw = np.hstack([sw, np.ones_like(y), np.ones_like(y)])
        X_weighted = np.vstack([X_weighted, X, X])
        y_weighted = np.hstack([y_weighted, y, y])
        splits_weighted = list(
            LeaveOneGroupOut().split(X_weighted, groups=groups_weighted)
        )
        estimator_weighted.set_params(cv=splits_weighted)

        groups_repeated = np.hstack(
            [np.full_like(y_repeated, 0), np.full_like(y, 1), np.full_like(y, 2)]
        )
        X_repeated = np.vstack([X_repeated, X, X])
        y_repeated = np.hstack([y_repeated, y, y])
        )
        estimator_repeated.set_params(cv=splits_repeated)

    y_weighted = _enforce_estimator_tags_y(estimator_weighted, y_weighted)
    y_repeated = _enforce_estimator_tags_y(estimator_repeated, y_repeated)

    # convert to sparse X if needed
        X_weighted = sparse_container(X_weighted)
        X_repeated = sparse_container(X_repeated)

    estimator_repeated.fit(X_repeated, y=y_repeated, sample_weight=None)
    estimator_weighted.fit(X_weighted, y=y_weighted, sample_weight=sw)

    for method in ["predict_proba", "decision_function", "predict", "transform"]:
        if hasattr(estimator_orig, method):
            X_pred1 = getattr(estimator_repeated, method)(X)
            X_pred2 = getattr(estimator_weighted, method)(X)
            err_msg = (
                f"Comparing the output of {name}.{method} revealed that fitting "
                "with `sample_weight` is not equivalent to fitting with removed "
                "or repeated data points."
            )
            assert_allclose_dense_sparse(X_pred1, X_pred2, err_msg=err_msg)

def check_sample_weight_equivalence_on_dense_data(name, estimator_orig):
    _check_sample_weight_equivalence(name, estimator_orig, sparse_container=None)

def check_sample_weight_equivalence_on_sparse_data(name, estimator_orig):
    _check_sample_weight_equivalence(name, estimator_orig, sparse.csr_array)

def check_sample_weights_not_overwritten(name, estimator_orig):
    # check that estimators don't override the passed sample_weight parameter
    set_random_state(estimator, random_state=0)

        [
            [1, 3],
            [1, 3],
            [1, 3],
            [1, 3],
            [2, 1],
            [2, 1],
            [2, 1],
            [2, 1],
            [3, 3],
            [3, 3],
            [3, 3],
            [3, 3],
            [4, 1],
            [4, 1],
            [4, 1],
            [4, 1],
        ],
    )
    y = np.array([1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2], dtype=int)

    sample_weight_original = np.ones(y.shape[0])
    sample_weight_original[0] = 10.0

    estimator.fit(X, y, sample_weight=sample_weight_fit)

    err_msg = f"{name} overwrote the original `sample_weight` given during fit"
    assert_allclose(sample_weight_fit, sample_weight_original, err_msg=err_msg)

def check_dtype_object(name, estimator_orig):
    # check that estimators treat dtype object as numeric if possible
    X = _enforce_estimator_tags_X(estimator_orig, rng.uniform(size=(40, 10)))
    X = X.astype(object)
    y = (X[:, 0] * 4).astype(int)

    err_msg = (
        "y with unknown label type is passed, but an error with no proper message "
        "is raised. You can use `type_of_target(..., raise_unknown=True)` to check "
        "and raise the right error, or include 'Unknown label type' in the error "
        "message."
    )
    with raises(Exception, match="Unknown label type", may_pass=True, err_msg=err_msg):
        estimator.fit(X, y.astype(object))

    if not tags.input_tags.string:
        X[0, 0] = {"foo": "bar"}
        # This error is raised by:
        # - `np.asarray` in `check_array`
        # - `_unique_python` for encoders
        msg = "argument must be .* string.* number"
        with raises(TypeError, match=msg):
    else:
        # Estimators supporting string will not call np.asarray to convert the
        # data to numeric and therefore, the error will not be raised.
        # Checking for each element dtype in the input array will be costly.
        # Refer to #11401 for full discussion.

def check_complex_data(name, estimator_orig):
    # check that estimators raise an exception on providing complex data
    X = rng.uniform(size=10) + 1j * rng.uniform(size=10)

    # Something both valid for classification and regression
    y = rng.randint(low=0, high=2, size=10) + 1j
    with raises(ValueError, match="Complex data not supported"):

def check_dict_unchanged(name, estimator_orig):
    X = 3 * rnd.uniform(size=(20, 3))

    set_random_state(estimator, 1)

    for method in ["predict", "transform", "decision_function", "predict_proba"]:
        if hasattr(estimator, method):
            dict_before = estimator.__dict__.copy()
            getattr(estimator, method)(X)
            assert estimator.__dict__ == dict_before, (
                "Estimator changes __dict__ during %s" % method
            )

def _is_public_parameter(attr):
    return not (attr.startswith("_") or attr.endswith("_"))

def check_dont_overwrite_parameters(name, estimator_orig):
    # check that fit method only changes or sets private attributes
    if hasattr(estimator_orig.__init__, "deprecated_original"):
        # to not check deprecated classes
        return

    if hasattr(estimator, "n_components"):
        estimator.n_components = 1
    if hasattr(estimator, "n_clusters"):
        estimator.n_clusters = 1

    dict_before_fit = estimator.__dict__.copy()

    dict_after_fit = estimator.__dict__

    public_keys_after_fit = [
        key for key in dict_after_fit.keys() if _is_public_parameter(key)
    ]

    attrs_added_by_fit = [
        key for key in public_keys_after_fit if key not in dict_before_fit.keys()
    ]

    # check that fit doesn't add any public attribute
    assert not attrs_added_by_fit, (
        "Estimator adds public attribute(s) during"
        " the fit method."
        " Estimators are only allowed to add private attributes"
        " either started with _ or ended"
        " with _ but %s added" % ", ".join(attrs_added_by_fit)
    )

    # check that fit doesn't change any public attribute
    attrs_changed_by_fit = [
        key
        for key in public_keys_after_fit
        if (dict_before_fit[key] is not dict_after_fit[key])
    ]

    assert not attrs_changed_by_fit, (
        "Estimator changes public attribute(s) during"
        " the fit method. Estimators are only allowed"
        " to change attributes started"
        " or ended with _, but"
        " %s changed" % ", ".join(attrs_changed_by_fit)
    )

def check_fit2d_predict1d(name, estimator_orig):
    # check by fitting a 2d array and predicting with a 1d array

            with raises(ValueError, match="Reshape your data"):
                getattr(estimator, method)(X[0])

def _apply_on_subsets(func, X):
    # apply function on the whole set and on mini batches
    result_full = func(X)
    result_by_batch = [func(batch.reshape(1, n_features)) for batch in X]

    # func can output tuple (e.g. score_samples)
    if isinstance(result_full, tuple):
        result_full = result_full[0]
        result_by_batch = list(map(lambda x: x[0], result_by_batch))

    if sparse.issparse(result_full):
        result_full = result_full.toarray()
        result_by_batch = [x.toarray() for x in result_by_batch]

    return np.ravel(result_full), np.ravel(result_by_batch)

def check_methods_subset_invariance(name, estimator_orig):
    # check that method gives invariant results if applied
    # on mini batches or the whole set

    ]:
        msg = ("{method} of {name} is not invariant when applied to a subset.").format(
            method=method, name=name
        )

            result_full, result_by_batch = _apply_on_subsets(
                getattr(estimator, method), X
            )
            assert_allclose(result_full, result_by_batch, atol=1e-7, err_msg=msg)

def check_methods_sample_order_invariance(name, estimator_orig):
    # on a subset with different sample order
    y = X[:, 0].astype(np.int64)
    if tags.classifier_tags is not None and not tags.classifier_tags.multi_class:
        y[y == 2] = 1

        estimator.n_clusters = 2

    idx = np.random.permutation(X.shape[0])

    ]:
        msg = (
            "{method} of {name} is not invariant when applied to a dataset"
            "with different sample order."
        ).format(method=method, name=name)

            assert_allclose_dense_sparse(
                _safe_indexing(getattr(estimator, method)(X), idx),
                getattr(estimator, method)(_safe_indexing(X, idx)),
                atol=1e-9,
                err_msg=msg,
            )

def check_fit2d_1sample(name, estimator_orig):
    # Check that fitting a 2d array with only one sample either works or
    # returns an informative message. The error message should either mention
    # the number of samples or the number of classes.
    X = 3 * rnd.uniform(size=(1, 10))

    # min_cluster_size cannot be less than the data size for OPTICS.
    if name == "OPTICS":
        estimator.set_params(min_samples=1.0)

    # perplexity cannot be more than the number of samples for TSNE.
    if name == "TSNE":
        estimator.set_params(perplexity=0.5)

    msgs = [
        "1 sample",
        "n_samples = 1",
        "n_samples=1",
        "one sample",
        "1 class",
        "one class",
    ]

    with raises(ValueError, match=msgs, may_pass=True):

def check_fit2d_1feature(name, estimator_orig):
    # check fitting a 2d array with only 1 feature either works or returns
    # informative message
    X = 3 * rnd.uniform(size=(10, 1))

    # ensure two labels in subsample for RandomizedLogisticRegression
    if name == "RandomizedLogisticRegression":
        estimator.sample_fraction = 1
    # ensure non skipped trials for RANSACRegressor
    if name == "RANSACRegressor":
        estimator.residual_threshold = 0.5

    msgs = [r"1 feature\(s\)", "n_features = 1", "n_features=1"]

def check_fit1d(name, estimator_orig):
    # check fitting 1d X array raises a ValueError
    X = 3 * rnd.uniform(size=(20))
    y = X.astype(int)

def check_transformer_general(name, transformer, readonly_memmap=False):
        centers=[[0, 0, 0], [1, 1, 1]],
    )
    X = _enforce_estimator_tags_X(transformer, X)

        X, y = create_memmap_backed_data([X, y])

    _check_transformer(name, transformer, X, y)

def check_transformer_data_not_an_array(name, transformer):
    )
    this_X = _NotAnArray(X)
    this_y = _NotAnArray(np.asarray(y))
    _check_transformer(name, transformer, this_X, this_y)
    # try the same with some list
    _check_transformer(name, transformer, X.tolist(), y.tolist())

def check_transformers_unfitted(name, transformer):
    X, y = _regression_dataset()

    transformer = clone(transformer)
    with raises(
        (AttributeError, ValueError),
        err_msg=(
            "The unfitted "
            f"transformer {name} does not raise an error when "
            "transform is called. Perhaps use "
            "check_is_fitted in transform."
        ),
    ):
        transformer.transform(X)

def check_transformers_unfitted_stateless(name, transformer):
    """Check that using transform without prior fitting
    doesn't raise a NotFittedError for stateless transformers.
    """
    X = rng.uniform(size=(20, 5))

    X_trans = transformer.transform(X)

    assert X_trans.shape[0] == X.shape[0]

def _check_transformer(name, transformer_orig, X, y):
    n_samples, n_features = np.asarray(X).shape
    transformer = clone(transformer_orig)
    set_random_state(transformer)

    # fit

    if name in CROSS_DECOMPOSITION:
        y_ = np.c_[np.asarray(y), np.asarray(y)]
        y_[::2, 1] *= 2
        if isinstance(X, _NotAnArray):
            y_ = _NotAnArray(y_)
    else:
        y_ = y

    transformer.fit(X, y_)
    # fit_transform method should work on non fitted estimator
    transformer_clone = clone(transformer)
    X_pred = transformer_clone.fit_transform(X, y=y_)

    if isinstance(X_pred, tuple):
        for x_pred in X_pred:
            assert x_pred.shape[0] == n_samples
    else:
        # check for consistent n_samples
        assert X_pred.shape[0] == n_samples

    if hasattr(transformer, "transform"):
        if name in CROSS_DECOMPOSITION:
            X_pred2 = transformer.transform(X, y_)
            X_pred3 = transformer.fit_transform(X, y=y_)
        else:
            X_pred2 = transformer.transform(X)

        if get_tags(transformer_orig).non_deterministic:
            msg = name + " is non deterministic"
            raise SkipTest(msg)
        if isinstance(X_pred, tuple) and isinstance(X_pred2, tuple):
            for x_pred, x_pred2, x_pred3 in zip(X_pred, X_pred2, X_pred3):
                assert_allclose_dense_sparse(
                    x_pred,
                    x_pred2,
                    atol=1e-2,
                    err_msg="fit_transform and transform outcomes not consistent in %s"
                    % transformer,
                )
                    x_pred,
                    x_pred3,
                    err_msg="consecutive fit_transform outcomes not consistent in %s"
                )
        else:
                X_pred,
                X_pred2,
                err_msg="fit_transform and transform outcomes not consistent in %s"
                % transformer,
                atol=1e-2,
            )
                X_pred,
                X_pred3,
                err_msg="consecutive fit_transform outcomes not consistent in %s"
            )
            assert _num_samples(X_pred2) == n_samples
            assert _num_samples(X_pred3) == n_samples

        # raises error on malformed input for transform
        if (
            hasattr(X, "shape")
            and get_tags(transformer).requires_fit
            and X.ndim == 2
            and X.shape[1] > 1
        ):
            # If it's not an array, it does not have a 'T' property
            with raises(
                ValueError,
                err_msg=(
                    f"The transformer {name} does not raise an error "
                    "when the number of features in transform is different from "
                    "the number of features in fit."
                ),
            ):
                transformer.transform(X[:, :-1])

def check_pipeline_consistency(name, estimator_orig):
    if get_tags(estimator_orig).non_deterministic:
        msg = name + " is non deterministic"
        raise SkipTest(msg)

    # check that make_pipeline(est) gives same score as est
    )
    X = _enforce_estimator_tags_X(estimator_orig, X, kernel=rbf_kernel)
    pipeline = make_pipeline(estimator)

    funcs = ["score", "fit_transform"]

    for func_name in funcs:
        func = getattr(estimator, func_name, None)
        if func is not None:
            func_pipeline = getattr(pipeline, func_name)
            result = func(X, y)
            result_pipe = func_pipeline(X, y)
            assert_allclose_dense_sparse(result, result_pipe)

def check_mixin_order(name, estimator_orig):
    """Check that mixins are inherited in the correct order."""
    # We define a list of edges, which in effect define a DAG of mixins and their
    # required order of inheritance.
    # This is of the form (mixin_a_should_be_before, mixin_b_should_be_after)
    dag = [
        (ClassifierMixin, BaseEstimator),
        (RegressorMixin, BaseEstimator),
        (ClusterMixin, BaseEstimator),
        (TransformerMixin, BaseEstimator),
        (BiclusterMixin, BaseEstimator),
        (OneToOneFeatureMixin, BaseEstimator),
        (ClassNamePrefixFeaturesOutMixin, BaseEstimator),
        (DensityMixin, BaseEstimator),
        (OutlierMixin, BaseEstimator),
        (MetaEstimatorMixin, BaseEstimator),
        (MultiOutputMixin, BaseEstimator),
    ]
    violations = []
    mro = type(estimator_orig).mro()
    for mixin_a, mixin_b in dag:
        if (
            mixin_a in mro
            and mixin_b in mro
            and mro.index(mixin_a) > mro.index(mixin_b)
        ):
            violations.append((mixin_a, mixin_b))
    violation_str = "\n".join(
        f"{mixin_a.__name__} comes before/left side of {mixin_b.__name__}"
        for mixin_a, mixin_b in violations
    )
    assert not violations, (
        f"{name} is inheriting from mixins in the wrong order. In general, in mixin "
        "inheritance, more specialized mixins must come before more general ones. "
        "This means, for instance, `BaseEstimator` should be on the right side of most "
        "other mixins. You need to change the order so that:\n"
        f"{violation_str}"
    )

def check_fit_score_takes_y(name, estimator_orig):
    # check that all estimators accept an optional y
    # in fit and score so they can be used in pipelines

    funcs = ["fit", "score", "partial_fit", "fit_predict", "fit_transform"]
            func(X, y)
            args = [p.name for p in signature(func).parameters.values()]
            if args[0] == "self":
                # available_if makes methods into functions
                # with an explicit "self", so need to shift arguments
                args = args[1:]
            assert args[1] in ["y", "Y"], (
                "Expected y or Y as second argument for method "
                "%s of %s. Got arguments: %r."
                % (func_name, type(estimator).__name__, args)
            )

def check_estimators_dtypes(name, estimator_orig):
    X_train_32 = 3 * rnd.uniform(size=(20, 5)).astype(np.float32)
    X_train_32 = _enforce_estimator_tags_X(estimator_orig, X_train_32)
    X_train_64 = X_train_32.astype(np.float64)
    X_train_int_64 = X_train_32.astype(np.int64)
    X_train_int_32 = X_train_32.astype(np.int32)
    y = np.array([1, 2] * 10, dtype=np.int64)

    methods = ["predict", "transform", "decision_function", "predict_proba"]

    for X_train in [X_train_32, X_train_64, X_train_int_64, X_train_int_32]:
        set_random_state(estimator, 1)
        estimator.fit(X_train, y)

            if hasattr(estimator, method):
                getattr(estimator, method)(X_train)

def check_transformer_preserve_dtypes(name, transformer_orig):
    # check that dtype are preserved meaning if input X is of some dtype
    # X_transformed should be from the same dtype.
    if hasattr(transformer, "set_output"):
        transformer.set_output(transform="default")
    )
    X = _enforce_estimator_tags_X(transformer_orig, X)

    for dtype in get_tags(transformer_orig).transformer_tags.preserves_dtype:
        X_cast = X.astype(dtype)
        set_random_state(transformer)
        X_trans1 = transformer.fit_transform(X_cast, y)
        X_trans2 = transformer.fit(X_cast, y).transform(X_cast)

        for Xt, method in zip([X_trans1, X_trans2], ["fit_transform", "transform"]):
            if isinstance(Xt, tuple):
                # cross-decompostion returns a tuple of (x_scores, y_scores)
                # when given y with fit_transform; only check the first element
                Xt = Xt[0]

            # check that the output dtype is preserved
            assert Xt.dtype == dtype, (
                f"{name} (method={method}) does not preserve dtype. "
                f"Original/Expected dtype={dtype}, got dtype={Xt.dtype}."
            )

def check_estimators_empty_data_messages(name, estimator_orig):
    e = clone(estimator_orig)
    set_random_state(e, 1)

    X_zero_samples = np.empty(0).reshape(0, 3)
    # The precise message can change depending on whether X or y is
    # validated first. Let us test the type of exception only:
    err_msg = (
        f"The estimator {name} does not raise a ValueError when an "
        "empty data is used to train. Perhaps use check_array in train."
    )
    with raises(ValueError, err_msg=err_msg):
        e.fit(X_zero_samples, [])

    X_zero_features = np.empty(0).reshape(12, 0)
    # the following y should be accepted by both classifiers and regressors
    # and ignored by unsupervised models
    y = _enforce_estimator_tags_y(e, np.array([1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]))
    msg = r"0 feature\(s\) \(shape=\(\d*, 0\)\) while a minimum of \d* is required."
    with raises(ValueError, match=msg):
        e.fit(X_zero_features, y)

def check_estimators_nan_inf(name, estimator_orig):
    # Checks that Estimator X's do not contain NaN or inf.
    X_train_finite = _enforce_estimator_tags_X(
        estimator_orig, rnd.uniform(size=(10, 3))
    )
    X_train_nan = rnd.uniform(size=(10, 3))
    X_train_nan[0, 0] = np.nan
    X_train_inf = rnd.uniform(size=(10, 3))
    X_train_inf[0, 0] = np.inf
    y[:5] = 0
    error_string_fit = f"Estimator {name} doesn't check for NaN and inf in fit."
    error_string_predict = f"Estimator {name} doesn't check for NaN and inf in predict."
    error_string_transform = (
        f"Estimator {name} doesn't check for NaN and inf in transform."
    )
    for X_train in [X_train_nan, X_train_inf]:
            set_random_state(estimator, 1)
            # try to fit
            with raises(ValueError, match=["inf", "NaN"], err_msg=error_string_fit):
                estimator.fit(X_train, y)
            # actually fit
            estimator.fit(X_train_finite, y)

            # predict
                with raises(
                    ValueError,
                    match=["inf", "NaN"],
                    err_msg=error_string_predict,
                ):
                    estimator.predict(X_train)

            # transform
            if hasattr(estimator, "transform"):
                    err_msg=error_string_transform,
                ):
                    estimator.transform(X_train)

def check_nonsquare_error(name, estimator_orig):
    """Test that error is thrown when non-square data provided."""

    X, y = make_blobs(n_samples=20, n_features=10)

        err_msg=(
            f"The pairwise estimator {name} does not raise an error on non-square data"
        ),
    ):

def check_estimators_pickle(name, estimator_orig, readonly_memmap=False):
    """Test that we can pickle all estimators."""
    check_methods = ["predict", "transform", "decision_function", "predict_proba"]

    )

    # include NaN values when the estimator should deal with them
    if tags.input_tags.allow_nan:
        # set randomly 10 elements to np.nan
        mask = rng.choice(X.size, 10, replace=False)
        X.reshape(-1)[mask] = np.nan

        unpickled_estimator = create_memmap_backed_data(estimator)
    else:
        # No need to touch the file system in that case.
        pickled_estimator = pickle.dumps(estimator)
        ):
            # strict check for sklearn estimators that are not implemented in test
            # modules.
            assert b"_sklearn_version" in pickled_estimator
        unpickled_estimator = pickle.loads(pickled_estimator)

    result = dict()
    for method in check_methods:
            result[method] = getattr(estimator, method)(X)

    for method in result:
        unpickled_result = getattr(unpickled_estimator, method)(X)
        assert_allclose_dense_sparse(result[method], unpickled_result)

def check_estimators_partial_fit_n_features(name, estimator_orig):
    # check if number of features changes between calls to partial_fit.
    if not hasattr(estimator_orig, "partial_fit"):
        return
    X, y = make_blobs(n_samples=50, random_state=1)

    try:
            classes = np.unique(y)
            estimator.partial_fit(X, y, classes=classes)
        else:
            estimator.partial_fit(X, y)
        return

        err_msg=(
            f"The estimator {name} does not raise an error when the "
            "number of features changes between calls to partial_fit."
        ),
    ):
        estimator.partial_fit(X[:, :-1], y)

def check_classifier_multioutput(name, estimator_orig):
    n_samples, n_labels, n_classes = 42, 5, 3
        random_state=42, n_samples=n_samples, n_labels=n_labels, n_classes=n_classes
    )
    y_pred = estimator.predict(X)

    assert y_pred.shape == (n_samples, n_classes), (
        "The shape of the prediction for multioutput data is "
        "incorrect. Expected {}, got {}.".format((n_samples, n_labels), y_pred.shape)
    )
    assert y_pred.dtype.kind == "i"

    if hasattr(estimator, "decision_function"):
        decision = estimator.decision_function(X)
        assert isinstance(decision, np.ndarray)
        assert decision.shape == (n_samples, n_classes), (
            "The shape of the decision function output for "
            "multioutput data is incorrect. Expected {}, got {}.".format(
                (n_samples, n_classes), decision.shape
            )
        )

        dec_pred = (decision > 0).astype(int)
        dec_exp = estimator.classes_[dec_pred]
        assert_array_equal(dec_exp, y_pred)

    if hasattr(estimator, "predict_proba"):
        y_prob = estimator.predict_proba(X)

        if isinstance(y_prob, list) and not tags.classifier_tags.poor_score:
            for i in range(n_classes):
                assert y_prob[i].shape == (n_samples, 2), (
                    "The shape of the probability for multioutput data is"
                    " incorrect. Expected {}, got {}.".format(
                        (n_samples, 2), y_prob[i].shape
                    )
                )
                    np.argmax(y_prob[i], axis=1).astype(int), y_pred[:, i]
                )
        elif not tags.classifier_tags.poor_score:
            assert y_prob.shape == (n_samples, n_classes), (
                "The shape of the probability for multioutput data is"
                " incorrect. Expected {}, got {}.".format(
                    (n_samples, n_classes), y_prob.shape
                )
            )
            assert_array_equal(y_prob.round().astype(int), y_pred)

    if hasattr(estimator, "decision_function") and hasattr(estimator, "predict_proba"):
        for i in range(n_classes):
            y_proba = estimator.predict_proba(X)[:, i]
            y_decision = estimator.decision_function(X)
            assert_array_equal(rankdata(y_proba), rankdata(y_decision[:, i]))

def check_regressor_multioutput(name, estimator):

    if not _is_pairwise_metric(estimator):
        n_samples = n_samples + 1

        random_state=42, n_targets=5, n_samples=n_samples, n_features=n_features
    )

    assert y_pred.dtype == np.dtype("float64"), (
        "Multioutput predictions by a regressor are expected to be"
        f" floating-point precision. Got {y_pred.dtype} instead"
    )
    assert y_pred.shape == y.shape, (
        "The shape of the prediction for multioutput data is incorrect."
        f" Expected {y_pred.shape}, got {y.shape}."
    )

def check_clustering(name, clusterer_orig, readonly_memmap=False):
    clusterer = clone(clusterer_orig)
    X, y = shuffle(X, y, random_state=7)
    rng = np.random.RandomState(7)
    X_noise = np.concatenate([X, rng.uniform(low=-3, high=3, size=(5, 2))])

        X, y, X_noise = create_memmap_backed_data([X, y, X_noise])

    # catch deprecation and neighbors warnings
    if hasattr(clusterer, "n_clusters"):
        clusterer.set_params(n_clusters=3)
    set_random_state(clusterer)
    if name == "AffinityPropagation":
        clusterer.set_params(preference=-100)
        clusterer.set_params(max_iter=100)

    # fit
    clusterer.fit(X)
    # with lists
    clusterer.fit(X.tolist())

    pred = clusterer.labels_
    assert pred.shape == (n_samples,)
    assert adjusted_rand_score(pred, y) > 0.4
    if get_tags(clusterer).non_deterministic:
        return
        pred2 = clusterer.fit_predict(X)

    # fit_predict(X) and labels_ should be of type int
    assert pred.dtype in [np.dtype("int32"), np.dtype("int64")]
    assert pred2.dtype in [np.dtype("int32"), np.dtype("int64")]

    # Add noise to X to test the possible values of the labels
    labels = clusterer.fit_predict(X_noise)

    # There should be at least one sample in every cluster. Equivalently
    # labels_ should contain all the consecutive values between its
    # min and its max.
    labels_sorted = np.unique(labels)
        labels_sorted, np.arange(labels_sorted[0], labels_sorted[-1] + 1)
    )

    # Labels are expected to start at 0 (no noise) or -1 (if noise)
    assert labels_sorted[0] in [0, -1]
    # Labels should be less than n_clusters - 1
        n_clusters = getattr(clusterer, "n_clusters")
        assert n_clusters - 1 >= labels_sorted[-1]

def check_clusterer_compute_labels_predict(name, clusterer_orig):
    """Check that predict is invariant of compute_labels."""
    X, y = make_blobs(n_samples=20, random_state=0)

    if hasattr(clusterer, "compute_labels"):
        # MiniBatchKMeans
        X_pred1 = clusterer.fit(X).predict(X)
        clusterer.set_params(compute_labels=False)
        X_pred2 = clusterer.fit(X).predict(X)
        assert_array_equal(X_pred1, X_pred2)

def check_classifiers_one_label(name, classifier_orig):
    error_string_fit = "Classifier can't train when only one class is present."
    error_string_predict = "Classifier can't predict when only one class is present."
    classifier = clone(classifier_orig)
    X_train = rnd.uniform(size=(10, 3))
    X_test = rnd.uniform(size=(10, 3))
    X_train, X_test = _enforce_estimator_tags_X(classifier, X_train, X_test=X_test)
            ValueError, match="class", may_pass=True, err_msg=error_string_fit
        ) as cm:
            classifier.fit(X_train, y)

        if cm.raised_and_matched:
            # ValueError was raised with proper error message
            return

        assert_array_equal(classifier.predict(X_test), y, err_msg=error_string_predict)

def check_classifiers_one_label_sample_weights(name, classifier_orig):
    """Check that classifiers accepting sample_weight fit or throws a ValueError with
    an explicit message if the problem is reduced to one class.
    """
    error_fit = (
        f"{name} failed when fitted on one label after sample_weight trimming. Error "
        "message is not explicit, it should have 'class'."
    )
    error_predict = f"{name} prediction results should only output the remaining class."
    # X should be square for test on SVC with precomputed kernel
    X_train = rnd.uniform(size=(10, 10))
    X_test = rnd.uniform(size=(10, 10))
    y = np.arange(10) % 2
    sample_weight = y.copy()  # select a single class

    if has_fit_parameter(classifier, "sample_weight"):
        match = [r"\bclass(es)?\b", error_predict]
        err_type, err_msg = (AssertionError, ValueError), error_fit
    else:
        match = r"\bsample_weight\b"
        err_type, err_msg = (TypeError, ValueError), None

    with raises(err_type, match=match, may_pass=True, err_msg=err_msg) as cm:
        classifier.fit(X_train, y, sample_weight=sample_weight)
            # raise the proper error type with the proper error message
            return
        # for estimators that do not fail, they should be able to predict the only
        # class remaining during fit
            classifier.predict(X_test), np.ones(10), err_msg=error_predict
        )

def check_classifiers_train(
    name, classifier_orig, readonly_memmap=False, X_dtype="float64"
):
    X_m, y_m = make_blobs(n_samples=300, random_state=0)
    X_m = X_m.astype(X_dtype)
    X_m, y_m = shuffle(X_m, y_m, random_state=7)
    X_m = StandardScaler().fit_transform(X_m)
    # generate binary problem from multi-class one
    y_b = y_m[y_m != 2]
    X_b = X_m[y_m != 2]

    if name in ["BernoulliNB", "MultinomialNB", "ComplementNB", "CategoricalNB"]:
        X_m -= X_m.min()
        X_b -= X_b.min()

        X_m, y_m, X_b, y_b = create_memmap_backed_data([X_m, y_m, X_b, y_b])

    problems = [(X_b, y_b)]
    tags = get_tags(classifier_orig)
    if tags.classifier_tags.multi_class:
        problems.append((X_m, y_m))

    for X, y in problems:
        classifier = clone(classifier_orig)
        X = _enforce_estimator_tags_X(classifier, X)
        y = _enforce_estimator_tags_y(classifier, y)

        set_random_state(classifier)
        # raises error on malformed input for fit
        if not tags.no_validation:
                err_msg=(
                    f"The classifier {name} does not raise an error when "
                    "incorrect/malformed input data for fit is passed. The number "
                    "of training examples is not the same as the number of "
                    "labels. Perhaps use check_X_y in fit."
                ),
            ):
                classifier.fit(X, y[:-1])

        # fit
        classifier.fit(X, y)
        # with lists
        classifier.fit(X.tolist(), y.tolist())
        assert hasattr(classifier, "classes_")
        y_pred = classifier.predict(X)

        assert y_pred.shape == (n_samples,)
        # training set performance
        if not tags.classifier_tags.poor_score:
            assert accuracy_score(y, y_pred) > 0.83

        # raises error on malformed input for predict
        msg_pairwise = (
            "The classifier {} does not raise an error when shape of X in "
            " {} is not equal to (n_test_samples, n_training_samples)"
        )
        msg = (
            "The classifier {} does not raise an error when the number of "
            "features in {} is different from the number of features in "
            "fit."
        )

            if tags.input_tags.pairwise:
                    err_msg=msg_pairwise.format(name, "predict"),
                ):
                    classifier.predict(X.reshape(-1, 1))
            else:
                with raises(ValueError, err_msg=msg.format(name, "predict")):
                    classifier.predict(X.T)
        if hasattr(classifier, "decision_function"):
            try:
                # decision_function agrees with predict
                decision = classifier.decision_function(X)
                if n_classes == 2:
                    if tags.target_tags.single_output:
                        assert decision.shape == (n_samples,)
                    else:
                        assert decision.shape == (n_samples, 1)
                    dec_pred = (decision.ravel() > 0).astype(int)
                    assert_array_equal(dec_pred, y_pred)
                else:
                    assert decision.shape == (n_samples, n_classes)
                    assert_array_equal(np.argmax(decision, axis=1), y_pred)

                # raises error on malformed input for decision_function
                if not tags.no_validation:
                    if tags.input_tags.pairwise:
                        with raises(
                            ValueError,
                            err_msg=msg_pairwise.format(name, "decision_function"),
                        ):
                            classifier.decision_function(X.reshape(-1, 1))
                    else:
                            err_msg=msg.format(name, "decision_function"),
                        ):
                            classifier.decision_function(X.T)
                pass

        if hasattr(classifier, "predict_proba"):
            # predict_proba agrees with predict
            y_prob = classifier.predict_proba(X)
            assert y_prob.shape == (n_samples, n_classes)
            assert_array_equal(np.argmax(y_prob, axis=1), y_pred)
            # check that probas for all classes sum to one
            assert_array_almost_equal(np.sum(y_prob, axis=1), np.ones(n_samples))
            if not tags.no_validation:
                # raises error on malformed input for predict_proba
                if tags.input_tags.pairwise:
                    with raises(
                        ValueError,
                        err_msg=msg_pairwise.format(name, "predict_proba"),
                    ):
                        classifier.predict_proba(X.reshape(-1, 1))
                else:
                        err_msg=msg.format(name, "predict_proba"),
                    ):
                        classifier.predict_proba(X.T)
            if hasattr(classifier, "predict_log_proba"):
                # predict_log_proba is a transformation of predict_proba
                y_log_prob = classifier.predict_log_proba(X)
                assert_allclose(y_log_prob, np.log(y_prob), 8, atol=1e-9)
                assert_array_equal(np.argsort(y_log_prob), np.argsort(y_prob))

def check_outlier_corruption(num_outliers, expected_outliers, decision):
    # Check for deviation from the precise given contamination level that may
    # be due to ties in the anomaly scores.
    if num_outliers < expected_outliers:
        start = num_outliers
        end = expected_outliers + 1
    else:
        start = expected_outliers
        end = num_outliers + 1

    # ensure that all values in the 'critical area' are tied,
    # leading to the observed discrepancy between provided
    # and actual contamination levels.
    sorted_decision = np.sort(decision)
    msg = (
        "The number of predicted outliers is not equal to the expected "
        "number of outliers and this difference is not explained by the "
        "number of ties in the decision_function values"
    )
    assert len(np.unique(sorted_decision[start:end])) == 1, msg

def check_outliers_train(name, estimator_orig, readonly_memmap=True):
    X, _ = make_blobs(n_samples=n_samples, random_state=0)
    X = shuffle(X, random_state=7)

        X = create_memmap_backed_data(X)

    # fit
    estimator.fit(X.tolist())

    assert y_pred.shape == (n_samples,)
    assert_array_equal(np.unique(y_pred), np.array([-1, 1]))

    decision = estimator.decision_function(X)
    scores = estimator.score_samples(X)
    for output in [decision, scores]:
        assert output.dtype == np.dtype("float")
        assert output.shape == (n_samples,)

    # raises error on malformed input for predict
        estimator.predict(X.T)

    # decision_function agrees with predict
    dec_pred = (decision >= 0).astype(int)
    dec_pred[dec_pred == 0] = -1
    assert_array_equal(dec_pred, y_pred)

    # raises error on malformed input for decision_function
        estimator.decision_function(X.T)

    # decision_function is a translation of score_samples
    y_dec = scores - estimator.offset_
    assert_allclose(y_dec, decision)

    # raises error on malformed input for score_samples
        estimator.score_samples(X.T)

    # contamination parameter (not for OneClassSVM which has the nu parameter)
    if hasattr(estimator, "contamination") and not hasattr(estimator, "novelty"):
        # proportion of outliers equal to contamination parameter when not
        # set to 'auto'. This is true for the training set and cannot thus be
        # checked as follows for estimators with a novelty parameter such as
        # LocalOutlierFactor (tested in check_outliers_fit_predict)
        expected_outliers = 30
        contamination = expected_outliers / n_samples
        estimator.set_params(contamination=contamination)

        num_outliers = np.sum(y_pred != 1)
        # num_outliers should be equal to expected_outliers unless
        # there are ties in the decision_function values. this can
        # only be tested for estimators with a decision_function
        # method, i.e. all estimators except LOF which is already
        # excluded from this if branch.
        if num_outliers != expected_outliers:
            decision = estimator.decision_function(X)
            check_outlier_corruption(num_outliers, expected_outliers, decision)

def check_outlier_contamination(name, estimator_orig):
    # Check that the contamination parameter is in (0.0, 0.5] when it is an
    # interval constraint.

    if not hasattr(estimator_orig, "_parameter_constraints"):
        # Only estimator implementing parameter constraints will be checked
        return

    if "contamination" not in estimator_orig._parameter_constraints:
        return

    contamination_constraints = estimator_orig._parameter_constraints["contamination"]
    if not any([isinstance(c, Interval) for c in contamination_constraints]):
            "contamination constraints should contain a Real Interval constraint."
        )

    for constraint in contamination_constraints:
        if isinstance(constraint, Interval):
            assert (
                constraint.type == Real
                and constraint.left >= 0.0
                and constraint.right <= 0.5
                and (constraint.left > 0 or constraint.closed in {"right", "neither"})
            ), "contamination constraint should be an interval in (0, 0.5]"

def check_classifiers_multilabel_representation_invariance(name, classifier_orig):
    )

    X_train, y_train = X[:80], y[:80]
    X_train, X_test = _enforce_estimator_tags_X(classifier_orig, X_train, X_test=X_test)

    y_train_list_of_lists = y_train.tolist()
    y_train_list_of_arrays = list(y_train)

    y_pred = classifier.fit(X_train, y_train).predict(X_test)

    y_pred_list_of_lists = classifier.fit(X_train, y_train_list_of_lists).predict(
        X_test
    )

    y_pred_list_of_arrays = classifier.fit(X_train, y_train_list_of_arrays).predict(
        X_test
    )

    assert_array_equal(y_pred, y_pred_list_of_arrays)
    assert_array_equal(y_pred, y_pred_list_of_lists)

    assert y_pred.dtype == y_pred_list_of_arrays.dtype
    assert y_pred.dtype == y_pred_list_of_lists.dtype
    assert type(y_pred) == type(y_pred_list_of_arrays)
    assert type(y_pred) == type(y_pred_list_of_lists)

def check_classifiers_multilabel_output_format_predict(name, classifier_orig):
    """Check the output of the `predict` method for classifiers supporting
    multilabel-indicator targets."""

    n_samples, test_size, n_outputs = 100, 25, 5
        n_classes=n_outputs,
    )

    X_train, X_test = X[:-test_size], X[-test_size:]
    y_train, y_test = y[:-test_size], y[-test_size:]

    response_method_name = "predict"
    predict_method = getattr(classifier, response_method_name, None)
    if predict_method is None:
        raise SkipTest(f"{name} does not have a {response_method_name} method.")

    y_pred = predict_method(X_test)

    # y_pred.shape -> y_test.shape with the same dtype
    assert isinstance(y_pred, np.ndarray), (
        f"{name}.predict is expected to output a NumPy array. Got "
        f"{type(y_pred)} instead."
    )
    assert y_pred.shape == y_test.shape, (
        f"{name}.predict outputs a NumPy array of shape {y_pred.shape} "
        f"instead of {y_test.shape}."
    )
    assert y_pred.dtype == y_test.dtype, (
        f"{name}.predict does not output the same dtype than the targets. "
        f"Got {y_pred.dtype} instead of {y_test.dtype}."
    )

def check_classifiers_multilabel_output_format_predict_proba(name, classifier_orig):
    """Check the output of the `predict_proba` method for classifiers supporting

    )

    y_train = y[:-test_size]

    response_method_name = "predict_proba"
    predict_proba_method = getattr(classifier, response_method_name, None)
    if predict_proba_method is None:

    y_pred = predict_proba_method(X_test)

    # y_pred.shape -> 2 possibilities:
    # - list of length n_outputs of shape (n_samples, 2);
    # - ndarray of shape (n_samples, n_outputs).
    # dtype should be floating
    if isinstance(y_pred, list):
        assert len(y_pred) == n_outputs, (
            f"When {name}.predict_proba returns a list, the list should "
            "be of length n_outputs and contain NumPy arrays. Got length "
            f"of {len(y_pred)} instead of {n_outputs}."
        )
        for pred in y_pred:
            assert pred.shape == (test_size, 2), (
                f"When {name}.predict_proba returns a list, this list "
                "should contain NumPy arrays of shape (n_samples, 2). Got "
                f"NumPy arrays of shape {pred.shape} instead of "
                f"{(test_size, 2)}."
            )
            assert pred.dtype.kind == "f", (
                f"When {name}.predict_proba returns a list, it should "
                "contain NumPy arrays with floating dtype. Got "
                f"{pred.dtype} instead."
            )
            # check that we have the correct probabilities
            err_msg = (
                f"When {name}.predict_proba returns a list, each NumPy "
                "array should contain probabilities for each class and "
                "thus each row should sum to 1 (or close to 1 due to "
                "numerical errors)."
            )
            assert_allclose(pred.sum(axis=1), 1, err_msg=err_msg)
    elif isinstance(y_pred, np.ndarray):
        assert y_pred.shape == (test_size, n_outputs), (
            f"When {name}.predict_proba returns a NumPy array, the "
            f"expected shape is (n_samples, n_outputs). Got {y_pred.shape}"
            f" instead of {(test_size, n_outputs)}."
        )
        assert y_pred.dtype.kind == "f", (
            f"expected data type is floating. Got {y_pred.dtype} instead."
        )
        err_msg = (
            f"When {name}.predict_proba returns a NumPy array, this array "
            "is expected to provide probabilities of the positive class "
            "and should therefore contain values between 0 and 1."
        )
        assert_array_less(0, y_pred, err_msg=err_msg)
        assert_array_less(y_pred, 1, err_msg=err_msg)
    else:
            f"Unknown returned type {type(y_pred)} by {name}."
            "predict_proba. A list or a Numpy array is expected."
        )

def check_classifiers_multilabel_output_format_decision_function(name, classifier_orig):
    """Check the output of the `decision_function` method for classifiers supporting

    )

    response_method_name = "decision_function"
    decision_function_method = getattr(classifier, response_method_name, None)
    if decision_function_method is None:

    y_pred = decision_function_method(X_test)

    # y_pred.shape -> y_test.shape with floating dtype
        f"{name}.decision_function is expected to output a NumPy array."
        f" Got {type(y_pred)} instead."
    )
    assert y_pred.shape == (test_size, n_outputs), (
        f"{name}.decision_function is expected to provide a NumPy array "
        f"of shape (n_samples, n_outputs). Got {y_pred.shape} instead of "
        f"{(test_size, n_outputs)}."
    )
    assert y_pred.dtype.kind == "f", (
        f"{name}.decision_function is expected to output a floating dtype."
        f" Got {y_pred.dtype} instead."
    )

def check_get_feature_names_out_error(name, estimator_orig):
    """Check the error raised by get_feature_names_out when called before fit.

    Unfitted estimators with get_feature_names_out should raise a NotFittedError.
    """

    err_msg = (
        f"Estimator {name} should have raised a NotFitted error when fit is called"
        " before get_feature_names_out"
    )
    with raises(NotFittedError, err_msg=err_msg):
        estimator.get_feature_names_out()

def check_estimators_fit_returns_self(name, estimator_orig):
    """Check if self is returned when calling fit."""
    X, y = make_blobs(random_state=0, n_samples=21)

    assert estimator.fit(X, y) is estimator

def check_readonly_memmap_input(name, estimator_orig):
    """Check that the estimator can handle readonly memmap backed data.

    This is particularly needed to support joblib parallelisation.
    """

    # This should not raise an error and should return self

def check_estimators_unfitted(name, estimator_orig):
    """Check that predict raises an exception in an unfitted estimator.

    Unfitted estimators should raise a NotFittedError.
    """
    err_msg = (
        "Estimator should raise a NotFittedError when calling `{method}` before fit. "
        "Either call `check_is_fitted(self)` at the beginning of `{method}` or "
        "set `tags.requires_fit=False` on estimator tags to disable this check.\n"
        "- `check_is_fitted`: https://scikit-learn.org/dev/modules/generated/sklearn."
        "utils.validation.check_is_fitted.html\n"
        "- Estimator Tags: https://scikit-learn.org/dev/developers/develop."
        "html#estimator-tags"
    )
    # Common test for Regressors, Classifiers and Outlier detection estimators

    for method in (
    ):
            with raises(NotFittedError, err_msg=err_msg.format(method=method)):
                getattr(estimator, method)(X)

def check_supervised_y_2d(name, estimator_orig):
    # fit

    # Check that when a 2D y is given, a DataConversionWarning is
    # raised
        warnings.simplefilter("always", DataConversionWarning)
        estimator.fit(X, y[:, np.newaxis])
    y_pred_2d = estimator.predict(X)
    msg = "expected 1 DataConversionWarning, got: %s" % ", ".join(
        [str(w_x) for w_x in w]
    )
    if not tags.target_tags.multi_output:
        # check that we warned if we don't support multi-output
        assert len(w) > 0, msg
        assert (
            "DataConversionWarning('A column-vector y"
            " was passed when a 1d array was expected" in msg
        )
    assert_allclose(y_pred.ravel(), y_pred_2d.ravel())

def check_classifiers_predictions(X, y, name, classifier_orig):
    if name == "BernoulliNB":
        X = X > X.mean()

    if hasattr(classifier, "decision_function"):
        decision = classifier.decision_function(X)
        if len(classes) == 2:
            dec_pred = (decision.ravel() > 0).astype(int)
            dec_exp = classifier.classes_[dec_pred]
                dec_exp,
                y_pred,
                err_msg=(
                    "decision_function does not match "
                    "classifier for %r: expected '%s', got '%s'"
                )
                % (
                    classifier,
                    ", ".join(map(str, dec_exp)),
                    ", ".join(map(str, y_pred)),
                ),
            )
        elif getattr(classifier, "decision_function_shape", "ovr") == "ovr":
            decision_y = np.argmax(decision, axis=1).astype(int)
            y_exp = classifier.classes_[decision_y]
                y_exp,
                y_pred,
                err_msg=(
                )
                % (
                    ", ".join(map(str, y_exp)),
                ),
            )

        classes,
        classifier.classes_,
        err_msg="Unexpected classes_ attribute for %r: expected '%s', got '%s'"
        % (
            ", ".join(map(str, classes)),
            ", ".join(map(str, classifier.classes_)),
        ),
    )

def _choose_check_classifiers_labels(name, y, y_names):
    # Semisupervised classifiers use -1 as the indicator for an unlabeled
    # sample.
    return (
        y
        if name in ["LabelPropagation", "LabelSpreading", "SelfTrainingClassifier"]
        else y_names
    )

def check_classifiers_classes(name, classifier_orig):
    X_multiclass, y_multiclass = make_blobs(
        n_samples=30, random_state=0, cluster_std=0.1
    )
    X_multiclass, y_multiclass = shuffle(X_multiclass, y_multiclass, random_state=7)
    X_multiclass = StandardScaler().fit_transform(X_multiclass)

    X_binary = X_multiclass[y_multiclass != 2]
    y_binary = y_multiclass[y_multiclass != 2]

    X_multiclass = _enforce_estimator_tags_X(classifier_orig, X_multiclass)
    X_binary = _enforce_estimator_tags_X(classifier_orig, X_binary)

    labels_binary = ["one", "two"]

    y_names_multiclass = np.take(labels_multiclass, y_multiclass)
    y_names_binary = np.take(labels_binary, y_binary)

    problems = [(X_binary, y_binary, y_names_binary)]
    if get_tags(classifier_orig).classifier_tags.multi_class:
        problems.append((X_multiclass, y_multiclass, y_names_multiclass))

    for X, y, y_names in problems:
        for y_names_i in [y_names, y_names.astype("O")]:
            y_ = _choose_check_classifiers_labels(name, y, y_names_i)
            check_classifiers_predictions(X, y_, name, classifier_orig)

    labels_binary = [-1, 1]
    y_binary = _choose_check_classifiers_labels(name, y_binary, y_names_binary)
    check_classifiers_predictions(X_binary, y_binary, name, classifier_orig)

def check_regressors_int(name, regressor_orig):
    X, _ = _regression_dataset()
    X = _enforce_estimator_tags_X(regressor_orig, X[:50])
    y = rnd.randint(3, size=X.shape[0])
    y = _enforce_estimator_tags_y(regressor_orig, y)
    # separate estimators to control random seeds
    regressor_1 = clone(regressor_orig)
    regressor_2 = clone(regressor_orig)
    set_random_state(regressor_1)
    set_random_state(regressor_2)

        y_ = np.vstack([y, 2 * y + rnd.randint(2, size=len(y))])
        y_ = y_.T
    else:
        y_ = y

    # fit
    regressor_1.fit(X, y_)
    pred1 = regressor_1.predict(X)
    regressor_2.fit(X, y_.astype(float))
    pred2 = regressor_2.predict(X)
    assert_allclose(pred1, pred2, atol=1e-2, err_msg=name)

def check_regressors_train(
    name, regressor_orig, readonly_memmap=False, X_dtype=np.float64
):
    X = X.astype(X_dtype)
    y = scale(y)  # X is already scaled
    regressor = clone(regressor_orig)
    X = _enforce_estimator_tags_X(regressor, X)
    y = _enforce_estimator_tags_y(regressor, y)
        rnd = np.random.RandomState(0)
        y_ = y_.T
    else:
        y_ = y

        X, y, y_ = create_memmap_backed_data([X, y, y_])

    if not hasattr(regressor, "alphas") and hasattr(regressor, "alpha"):
        # linear regressors need to set alpha, but not generalized CV ones
        regressor.alpha = 0.01
    if name == "PassiveAggressiveRegressor":
        regressor.C = 0.01

    # raises error on malformed input for fit
        err_msg=(
            f"The classifier {name} does not raise an error when "
            "incorrect/malformed input data for fit is passed. The number of "
            "training examples is not the same as the number of labels. Perhaps "
            "use check_X_y in fit."
        ),
    ):
        regressor.fit(X, y[:-1])
    # fit
    regressor.fit(X, y_)
    regressor.fit(X.tolist(), y_.tolist())
    y_pred = regressor.predict(X)
    assert y_pred.shape == y_.shape

    # TODO: find out why PLS and CCA fail. RANSAC is random
    # and furthermore assumes the presence of outliers, hence
    # skipped
    if not get_tags(regressor).regressor_tags.poor_score:
        assert regressor.score(X, y_) > 0.5

def check_regressors_no_decision_function(name, regressor_orig):
    # check that regressors don't have a decision_function, predict_proba, or
    # predict_log_proba method.

    X = rng.normal(size=(10, 4))
    X = _enforce_estimator_tags_X(regressor_orig, X)
    y = _enforce_estimator_tags_y(regressor, X[:, 0])

    funcs = ["decision_function", "predict_proba", "predict_log_proba"]
        assert not hasattr(regressor, func_name)

def check_class_weight_classifiers(name, classifier_orig):
        problems = [2, 3]
    else:  # binary classification only
        problems = [2]

    for n_centers in problems:
        # create a very noisy dataset
        X, y = make_blobs(centers=n_centers, random_state=0, cluster_std=20)
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.5, random_state=0
        )

        # can't use gram_if_pairwise() here, setting up gram matrix manually
        if get_tags(classifier_orig).input_tags.pairwise:
            X_test = rbf_kernel(X_test, X_train)
            X_train = rbf_kernel(X_train, X_train)

        n_centers = len(np.unique(y_train))

        if n_centers == 2:
            class_weight = {0: 1000, 1: 0.0001}
        else:
            class_weight = {0: 1000, 1: 0.0001, 2: 0.0001}

        classifier = clone(classifier_orig).set_params(class_weight=class_weight)
        if hasattr(classifier, "n_iter"):
            classifier.set_params(n_iter=100)
        if hasattr(classifier, "max_iter"):
            classifier.set_params(max_iter=1000)
        if hasattr(classifier, "min_weight_fraction_leaf"):
            classifier.set_params(min_weight_fraction_leaf=0.01)
        if hasattr(classifier, "n_iter_no_change"):
            classifier.set_params(n_iter_no_change=20)

        classifier.fit(X_train, y_train)
        y_pred = classifier.predict(X_test)
        # XXX: Generally can use 0.89 here. On Windows, LinearSVC gets
        #      0.88 (Issue #9111)
        if not get_tags(classifier_orig).classifier_tags.poor_score:
            assert np.mean(y_pred == 0) > 0.87

def check_class_weight_balanced_classifiers(
    name, classifier_orig, X_train, y_train, X_test, y_test, weights
):
    if hasattr(classifier, "n_iter"):
        classifier.set_params(n_iter=100)
    if hasattr(classifier, "max_iter"):
        classifier.set_params(max_iter=1000)

    classifier.set_params(class_weight="balanced")
    y_pred_balanced = classifier.predict(X_test)
    assert f1_score(y_test, y_pred_balanced, average="weighted") > f1_score(
        y_test, y_pred, average="weighted"
    )

def check_class_weight_balanced_linear_classifier(name, estimator_orig):
    """Test class weights with non-contiguous class labels."""
    # this is run on classes, not instances, though this should be changed

    classifier = clone(estimator_orig)

        # This is a very small dataset, default n_iter are likely to prevent
        # convergence
        classifier.set_params(n_iter=1000)
    if hasattr(classifier, "cv"):
        classifier.set_params(cv=3)

    # Let the model compute the class frequencies
    coef_balanced = classifier.fit(X, y).coef_.copy()

    # Count each label occurrence to reweight manually
    n_classes = float(len(np.unique(y)))

    class_weight = {
        1: n_samples / (np.sum(y == 1) * n_classes),
        -1: n_samples / (np.sum(y == -1) * n_classes),
    }
    classifier.set_params(class_weight=class_weight)
    coef_manual = classifier.fit(X, y).coef_.copy()

        coef_balanced,
        coef_manual,
        err_msg="Classifier %s is not computing class_weight=balanced properly." % name,
    )

def check_estimators_overwrite_params(name, estimator_orig):

    # Make a physical copy of the original estimator parameters before fitting.
    params = estimator.get_params()
    original_params = deepcopy(params)

    # Fit the model

    # Compare the state of the model parameters with the original parameters
    new_params = estimator.get_params()
    for param_name, original_value in original_params.items():
        new_value = new_params[param_name]

        # We should never change or mutate the internal state of input
        # parameters by default. To check this we use the joblib.hash function
        # that introspects recursively any subobjects to compute a checksum.
        # The only exception to this rule of immutable constructor parameters
        # is possible RandomState instance but in this check we explicitly
        # fixed the random_state params recursively to be integer seeds.
        assert joblib.hash(new_value) == joblib.hash(original_value), (
            "Estimator %s should not change or mutate "
            " the parameter %s from %s to %s during fit."
            % (name, param_name, original_value, new_value)
        )

def check_no_attributes_set_in_init(name, estimator_orig):
    """Check setting during init."""
    try:
        # Clone fails if the estimator does not store
        # all parameters as an attribute during init
        raise AttributeError(
            f"Estimator {name} should store all parameters as an attribute during init."
        )

    if hasattr(type(estimator).__init__, "deprecated_original"):
        return

    init_params = _get_args(type(estimator).__init__)
    parents_init_params = [
        param
        for params_parent in (_get_args(parent) for parent in type(estimator).__mro__)
        for param in params_parent
    ]

    # Test for no setting apart from parameters during init
    invalid_attr = set(vars(estimator)) - set(init_params) - set(parents_init_params)
    # Ignore private attributes
    invalid_attr = set([attr for attr in invalid_attr if not attr.startswith("_")])
    assert not invalid_attr, (
        "Estimator %s should not set any attribute apart"
        " from parameters during init. Found attributes %s."
        % (name, sorted(invalid_attr))
    )

def check_sparsify_coefficients(name, estimator_orig):
        [
            [-2, -1],
            [-1, -1],
            [-1, -2],
            [1, 1],
            [1, 2],
            [2, 1],
            [-1, -2],
            [2, 2],
            [-2, -2],
        ]
    )
    y = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])

    pred_orig = est.predict(X)

    est.sparsify()
    assert sparse.issparse(est.coef_)
    pred = est.predict(X)
    assert_array_equal(pred, pred_orig)

    est = pickle.loads(pickle.dumps(est))

def check_classifier_data_not_an_array(name, estimator_orig):
        [
            [3, 0],
            [0, 1],
            [0, 2],
            [1, 1],
            [1, 2],
            [2, 1],
            [0, 3],
            [1, 0],
            [2, 0],
            [4, 4],
            [2, 3],
            [3, 2],
        ]
    )
    y = np.array([1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2])
    for obj_type in ["NotAnArray", "PandasDataframe"]:
        check_estimators_data_not_an_array(name, estimator_orig, X, y, obj_type)

def check_regressor_data_not_an_array(name, estimator_orig):

def check_estimators_data_not_an_array(name, estimator_orig, X, y, obj_type):
            "Skipping check_estimators_data_not_an_array "
            "for cross decomposition module as estimators "
            "are not deterministic."
        )
    estimator_1 = clone(estimator_orig)
    estimator_2 = clone(estimator_orig)
    set_random_state(estimator_1)
    set_random_state(estimator_2)

    if obj_type not in ["NotAnArray", "PandasDataframe"]:
        raise ValueError("Data type {0} not supported".format(obj_type))

    if obj_type == "NotAnArray":
        y_ = _NotAnArray(np.asarray(y))
        X_ = _NotAnArray(np.asarray(X))
    else:
        # Here pandas objects (Series and DataFrame) are tested explicitly
        # because some estimators may handle them (especially their indexing)
        # specially.
        try:
            import pandas as pd

            y_ = np.asarray(y)
            if y_.ndim == 1:
                y_ = pd.Series(y_, copy=False)
            else:
                y_ = pd.DataFrame(y_, copy=False)
            X_ = pd.DataFrame(np.asarray(X), copy=False)

                "pandas is not installed: not checking estimators for pandas objects."
            )

    # fit
    estimator_1.fit(X_, y_)
    pred1 = estimator_1.predict(X_)
    estimator_2.fit(X, y)
    pred2 = estimator_2.predict(X)

def check_estimator_cloneable(name, estimator_orig):
    """Checks whether the estimator can be cloned."""
    try:
        clone(estimator_orig)
    except Exception as e:
        raise AssertionError(f"Cloning of {name} failed with error: {e}.") from e

def check_estimator_repr(name, estimator_orig):
    """Check that the estimator has a functioning repr."""
    try:
        repr(estimator)
        raise AssertionError(f"Repr of {name} failed with error: {e}.") from e

def check_parameters_default_constructible(name, estimator_orig):
    # test default-constructibility
    # get rid of deprecation warnings

    Estimator = estimator_orig.__class__

        # test that set_params returns self
        # TODO(devtools): this should be a separate check.
        assert estimator.set_params() is estimator

        # test if init does nothing but set parameters
        # this is important for grid_search etc.
        # We get the default parameters from init and then
        # compare these against the actual values of the attributes.

        # this comes from getattr. Gets rid of deprecation decorator.
        init = getattr(estimator.__init__, "deprecated_original", estimator.__init__)

        try:

            def param_default_value(p):
                """Identify hyper parameters of an estimator."""
                return (
                    p.name != "self"
                    and p.kind != p.VAR_KEYWORD
                    and p.kind != p.VAR_POSITIONAL
                    # and it should have a default value for this test
                    and p.default != p.empty
                )

            def param_required(p):
                return (
                    # technically VAR_POSITIONAL is also required, but we don't have a
                    # nice way to check for it. We assume there's no VAR_POSITIONAL in
                    # the constructor parameters.
                    #
                    # TODO(devtools): separately check that the constructor doesn't
                    # have *args.
                    # these are parameters that don't have a default value and are
                    # required to construct the estimator.
                    and p.default == p.empty
                )

            required_params_names = [
                p.name for p in signature(init).parameters.values() if param_required(p)
            ]

            default_value_params = [
                p for p in signature(init).parameters.values() if param_default_value(p)
            ]

        except (TypeError, ValueError):
            # init is not a python function.
            # true for mixins
            return

        # here we construct an instance of the estimator using only the required
        # parameters.
        old_params = estimator.get_params()
        init_params = {
            param: old_params[param]
            for param in old_params
            if param in required_params_names
        }
        estimator = Estimator(**init_params)
        params = estimator.get_params()

        for init_param in default_value_params:
            allowed_types = {
                str,
                int,
                float,
                bool,
                tuple,
                type(None),
                type,
            }
            # Any numpy numeric such as np.int32.
            allowed_types.update(np.sctypeDict.values())

            allowed_value = (
                type(init_param.default) in allowed_types
                or
                # Although callables are mutable, we accept them as argument
                # default value and trust that neither the implementation of
                # the callable nor of the estimator changes the state of the
                # callable.
                callable(init_param.default)
            )

            assert allowed_value, (
                f"Parameter '{init_param.name}' of estimator "
                f"'{Estimator.__name__}' is of type "
                f"{type(init_param.default).__name__} which is not allowed. "
                f"'{init_param.name}' must be a callable or must be of type "
                f"{set(type.__name__ for type in allowed_types)}."
            )
            if init_param.name not in params.keys():
                # deprecated parameter, not in get_params
                assert init_param.default is None, (
                    f"Estimator parameter '{init_param.name}' of estimator "
                    f"'{Estimator.__name__}' is not returned by get_params. "
                    "If it is deprecated, set its default value to None."
                )
                continue

            param_value = params[init_param.name]
            if isinstance(param_value, np.ndarray):
                assert_array_equal(param_value, init_param.default)
            else:
                failure_text = (
                    f"Parameter {init_param.name} was mutated on init. All "
                    "parameters must be stored unchanged."
                )
                if is_scalar_nan(param_value):
                    # Allows to set default parameters to np.nan
                    assert param_value is init_param.default, failure_text
                else:
                    assert param_value == init_param.default, failure_text

def _enforce_estimator_tags_y(estimator, y):
    # Estimators with a `requires_positive_y` tag only accept strictly positive
    # data
    if tags.target_tags.positive_only:
        # Create strictly positive y. The minimal increment above 0 is 1, as
        # y could be of integer dtype.
        y += 1 + abs(y.min())
    if (
        tags.classifier_tags is not None
        and not tags.classifier_tags.multi_class
        and y.size > 0
    ):
        y = np.where(y == y.min(), y, y.min() + 1)
    # Estimators in mono_output_task_error raise ValueError if y is of 1-D
    # Convert into a 2-D y for those estimators.
    if tags.target_tags.multi_output and not tags.target_tags.single_output:
        return np.reshape(y, (-1, 1))
    return y

def _enforce_estimator_tags_X(estimator, X, X_test=None, kernel=linear_kernel):
    # Estimators with `1darray` in `X_types` tag only accept
    # X of shape (`n_samples`,)
    if get_tags(estimator).input_tags.one_d_array:
        X = X[:, 0]
            X_test = X_test[:, 0]  # pragma: no cover
    # Estimators with a `requires_positive_X` tag only accept
    # strictly positive data
    if get_tags(estimator).input_tags.positive_only:
        X = X - X.min()
            X_test = X_test - X_test.min()  # pragma: no cover
    if get_tags(estimator).input_tags.categorical:
        dtype = np.float64 if get_tags(estimator).input_tags.allow_nan else np.int32
        X = np.round((X - X.min())).astype(dtype)
            X_test = np.round((X_test - X_test.min())).astype(dtype)  # pragma: no cover

    if estimator.__class__.__name__ == "SkewedChi2Sampler":
        # SkewedChi2Sampler requires X > -skewdness in transform

    X_res = X

    # Pairwise estimators only accept
    # X of shape (`n_samples`, `n_samples`)
    if _is_pairwise_metric(estimator):
        X_res = pairwise_distances(X, metric="euclidean")
            X_test = pairwise_distances(
                X_test, X, metric="euclidean"
            )  # pragma: no cover
    elif get_tags(estimator).input_tags.pairwise:
        X_res = kernel(X, X)
            X_test = kernel(X_test, X)  # pragma: no cover
        return X_res, X_test
    return X_res

def check_positive_only_tag_during_fit(name, estimator_orig):
    """Test that the estimator correctly sets the tags.input_tags.positive_only

    If the tag is False, the estimator should accept negative input regardless of the
    tags.input_tags.pairwise flag.
    """

    set_random_state(estimator, 0)
    X -= X.mean()

        with raises(ValueError, match="Negative values in data"):
    else:
        # This should pass
        try:
            err_msg = (
                f"Estimator {name!r} raised {e.__class__.__name__} unexpectedly."
                " This happens when passing negative input values as X."
                " If negative values are not supported for this estimator instance,"
                " then the tags.input_tags.positive_only tag needs to be set to True."
            )

def check_non_transformer_estimators_n_iter(name, estimator_orig):
    # Test that estimators that are not transformers with a parameter
    # max_iter, return the attribute of n_iter_ at least 1.

    if not hasattr(estimator_orig, "max_iter"):
        return

    X, y_ = iris.data, iris.target
    y_ = _enforce_estimator_tags_y(estimator, y_)

    estimator.fit(X, y_)

    assert np.all(np.asarray(estimator.n_iter_) >= 1), (
        "Estimators with a `max_iter` parameter, should expose an `n_iter_` attribute,"
        " indicating the number of iterations that were executed. The values in the "
        "`n_iter_` attribute should be greater or equal to 1."
    )

def check_transformer_n_iter(name, estimator_orig):
    # Test that transformers with a parameter max_iter, return the
    # attribute of n_iter_ at least 1.
    if hasattr(estimator, "max_iter"):
            # Check using default data
            X = [[0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [2.0, 2.0, 2.0], [2.0, 5.0, 4.0]]
            y_ = [[0.1, -0.2], [0.9, 1.1], [0.1, -0.5], [0.3, -0.2]]

        else:
            X, y_ = make_blobs(
                n_samples=30,
                centers=[[0, 0, 0], [1, 1, 1]],
                n_features=2,
                cluster_std=0.1,
            )
            X = _enforce_estimator_tags_X(estimator_orig, X)
        set_random_state(estimator, 0)
        estimator.fit(X, y_)

        # These return a n_iter per component.
            for iter_ in estimator.n_iter_:
                assert iter_ >= 1
        else:
            assert estimator.n_iter_ >= 1

def check_get_params_invariance(name, estimator_orig):
    # Checks if get_params(deep=False) is a subset of get_params(deep=True)

    shallow_params = e.get_params(deep=False)
    deep_params = e.get_params(deep=True)

    assert all(item in deep_params.items() for item in shallow_params.items())

def check_set_params(name, estimator_orig):
    # Check that get_params() returns the same thing
    # before and after set_params() with some fuzz

    orig_params = estimator.get_params(deep=False)
    msg = "get_params result does not match what was passed to set_params"

    estimator.set_params(**orig_params)
    curr_params = estimator.get_params(deep=False)
    assert set(orig_params.keys()) == set(curr_params.keys()), msg
    for k, v in curr_params.items():
        assert orig_params[k] is v, msg

    # some fuzz values
    test_values = [-np.inf, np.inf, None]

    test_params = deepcopy(orig_params)
    for param_name in orig_params.keys():
        default_value = orig_params[param_name]
        for value in test_values:
            test_params[param_name] = value
            try:
                estimator.set_params(**test_params)
            except (TypeError, ValueError) as e:
                e_type = e.__class__.__name__
                # Exception occurred, possibly parameter validation
                    "{0} occurred during set_params of param {1} on "
                    "{2}. It is recommended to delay parameter "
                    "validation until fit.".format(e_type, param_name, name)
                )

                change_warning_msg = (
                    "Estimator's parameters changed after set_params raised {}".format(
                        e_type
                    )
                )
                params_before_exception = curr_params
                curr_params = estimator.get_params(deep=False)
                try:
                    assert set(params_before_exception.keys()) == set(
                        curr_params.keys()
                    )
                    for k, v in curr_params.items():
                        assert params_before_exception[k] is v
                except AssertionError:
                    warnings.warn(change_warning_msg)
            else:
                assert set(test_params.keys()) == set(curr_params.keys()), msg
                for k, v in curr_params.items():
                    assert test_params[k] is v, msg
        test_params[param_name] = default_value

def check_classifiers_regression_target(name, estimator_orig):
    # Check if classifier throws an exception when fed regression targets

    err_msg = (
        "When a classifier is passed a continuous target, it should raise a ValueError"
        " with a message containing 'Unknown label type: ' or a message indicating that"
        " a continuous target is passed and the message should include the word"
        " 'continuous'"
    )
    msg = "Unknown label type: |continuous"
    if not get_tags(e).no_validation:
        with raises(ValueError, match=msg, err_msg=err_msg):
            e.fit(X, y)

def check_decision_proba_consistency(name, estimator_orig):
    # Check whether an estimator having both decision_function and
    # predict_proba methods has outputs with perfect rank correlation.

    centers = [(2, 2), (4, 4)]
        cluster_std=1.0,
    )
        X, y, test_size=0.2, random_state=0
    )

        estimator.fit(X_train, y_train)
        # Since the link function from decision_function() to predict_proba()
        # is sometimes not precise enough (typically expit), we round to the
        # 10th decimal to avoid numerical issues: we compare the rank
        # with deterministic ties rather than get platform specific rank
        # inversions in case of machine level differences.
        a = estimator.predict_proba(X_test)[:, 1].round(decimals=10)
        b = estimator.decision_function(X_test).round(decimals=10)

        rank_proba, rank_score = rankdata(a), rankdata(b)
        try:
            assert_array_almost_equal(rank_proba, rank_score)
            # Sometimes, the rounding applied on the probabilities will have
            # ties that are not present in the scores because it is
            # numerically more precise. In this case, we relax the test by
            # grouping the decision function scores based on the probability
            # rank and check that the score is monotonically increasing.
            grouped_y_score = np.array(
                [b[rank_proba == group].mean() for group in np.unique(rank_proba)]
            )
            sorted_idx = np.argsort(grouped_y_score)
            assert_array_equal(sorted_idx, np.arange(len(sorted_idx)))

def check_outliers_fit_predict(name, estimator_orig):
    # Check fit_predict for outlier detectors.

    y_pred = estimator.fit_predict(X)

    # check fit_predict = fit.predict when the estimator has both a predict and
    # a fit_predict method. recall that it is already assumed here that the
    # estimator has a fit_predict method
        y_pred_2 = estimator.fit(X).predict(X)
        assert_array_equal(y_pred, y_pred_2)

        # set to 'auto'
        contamination = float(expected_outliers) / n_samples
        y_pred = estimator.fit_predict(X)

        # method
        if num_outliers != expected_outliers and hasattr(
            estimator, "decision_function"
        ):

def check_fit_non_negative(name, estimator_orig):
    # Check that proper warning is raised for non-negative X
    # when tag requires_positive_X is present
    X = np.array([[-1.0, 1], [-1.0, 1]])

def check_fit_idempotent(name, estimator_orig):
    # Check that est.fit(X) is the same as est.fit(X).fit(X). Ideally we would
    # check that the estimated parameters during training (e.g. coefs_) are
    # the same, but having a universal comparison function for those
    # attributes is difficult and full of edge cases. So instead we check that
    # predict(), predict_proba(), decision_function() and transform() return

    if "warm_start" in estimator.get_params().keys():
        estimator.set_params(warm_start=False)

    X = rng.normal(loc=100, size=(n_samples, 2))
    if is_regressor(estimator_orig):
    else:
        y = rng.randint(low=0, high=2, size=n_samples)

    train, test = next(ShuffleSplit(test_size=0.2, random_state=rng).split(X))
    X_train, y_train = _safe_split(estimator, X, y, train)
    X_test, y_test = _safe_split(estimator, X, y, test, train)

    # Fit for the first time

    result = {
        method: getattr(estimator, method)(X_test)
        for method in check_methods
        if hasattr(estimator, method)
    }

    # Fit again

            new_result = getattr(estimator, method)(X_test)
            if hasattr(new_result, "dtype") and np.issubdtype(
                new_result.dtype, np.floating
            ):
                tol = 2 * np.finfo(new_result.dtype).eps
            else:
                tol = 2 * np.finfo(np.float64).eps
                result[method],
                new_result,
                atol=max(tol, 1e-9),
                rtol=max(tol, 1e-7),
                err_msg="Idempotency check failed for method {}".format(method),
            )

def check_fit_check_is_fitted(name, estimator_orig):
    # Make sure that estimator doesn't pass check_is_fitted before calling fit
    # and that passes check_is_fitted once it's fit.

    if "warm_start" in estimator.get_params():

    else:

    if get_tags(estimator).requires_fit:
        # stateless estimators (such as FunctionTransformer) are always "fit"!
        try:
            check_is_fitted(estimator)
                f"{estimator.__class__.__name__} passes check_is_fitted before being"
                " fit!"
            )
            pass
    try:
    except NotFittedError as e:
        raise NotFittedError(
            "Estimator fails to pass `check_is_fitted` even though it has been fit."
        ) from e

def check_n_features_in(name, estimator_orig):
    # Make sure that n_features_in_ attribute doesn't exist until fit is
    # called, and that its value is correct.

    else:

    assert hasattr(estimator, "n_features_in_")
    assert estimator.n_features_in_ == X.shape[1]

def check_requires_y_none(name, estimator_orig):
    # Make sure that an estimator with requires_y=True fails gracefully when
    # given y=None

    expected_err_msgs = (
        "requires y to be passed, but the target y is None",
        "Expected array-like (array or non-string sequence), got None",
        "y should be a 1d array",
    )

    try:
        estimator.fit(X, None)
    except ValueError as ve:
        if not any(msg in str(ve) for msg in expected_err_msgs):
            raise ve

def check_n_features_in_after_fitting(name, estimator_orig):
    # Make sure that n_features_in are checked after fitting

    is_supported_X_types = tags.input_tags.two_d_array or tags.input_tags.categorical

    if not is_supported_X_types or tags.no_validation:
        return

    X = rng.normal(size=(n_samples, 4))

    else:

    err_msg = (
        "`{name}.fit()` does not set the `n_features_in_` attribute. "
        "You might want to use `sklearn.utils.validation.validate_data` instead "
        "of `check_array` in `{name}.fit()` which takes care of setting the "
        "attribute.".format(name=name)
    )

    assert hasattr(estimator, "n_features_in_"), err_msg
    assert estimator.n_features_in_ == X.shape[1], err_msg

    # check methods will check n_features_in_
    check_methods = [
        "score",
    ]
    X_bad = X[:, [1]]

    err_msg = """\
        `{name}.{method}()` does not check for consistency between input number
        of features with {name}.fit(), via the `n_features_in_` attribute.
        You might want to use `sklearn.utils.validation.validate_data` instead
        of `check_array` in `{name}.fit()` and {name}.{method}()`. This can be done
        like the following:
        from sklearn.utils.validation import validate_data
        ...
        class MyEstimator(BaseEstimator):
            ...
            def fit(self, X, y):
                X, y = validate_data(self, X, y, ...)
                ...
                return self
            ...
            def {method}(self, X):
                X = validate_data(self, X, ..., reset=False)
                ...
            return X
    """
    err_msg = textwrap.dedent(err_msg)

    msg = f"X has 1 features, but \\w+ is expecting {X.shape[1]} features as input"
        if not hasattr(estimator, method):
            continue

        callable_method = getattr(estimator, method)
        if method == "score":
            callable_method = partial(callable_method, y=y)

            ValueError, match=msg, err_msg=err_msg.format(name=name, method=method)
        ):
            callable_method(X_bad)

    # partial_fit will check in the second call
    if not hasattr(estimator, "partial_fit"):
        return

        estimator.partial_fit(X, y, classes=np.unique(y))
    else:
        estimator.partial_fit(X, y)

        estimator.partial_fit(X_bad, y)

def check_valid_tag_types(name, estimator):
    """Check that estimator tags are valid."""
    assert hasattr(estimator, "__sklearn_tags__"), (
        f"Estimator {name} does not have `__sklearn_tags__` method. This method is"
        " implemented in BaseEstimator and returns a sklearn.utils.Tags instance."
    )
    err_msg = (
        "Tag values need to be of a certain type. "
        "Please refer to the documentation of `sklearn.utils.Tags` for more details."
    )
    assert isinstance(tags.estimator_type, (str, type(None))), err_msg
    assert isinstance(tags.target_tags, TargetTags), err_msg
    assert isinstance(tags.classifier_tags, (ClassifierTags, type(None))), err_msg
    assert isinstance(tags.regressor_tags, (RegressorTags, type(None))), err_msg
    assert isinstance(tags.transformer_tags, (TransformerTags, type(None))), err_msg
    assert isinstance(tags.input_tags, InputTags), err_msg
    assert isinstance(tags.array_api_support, bool), err_msg
    assert isinstance(tags.no_validation, bool), err_msg
    assert isinstance(tags.non_deterministic, bool), err_msg
    assert isinstance(tags.requires_fit, bool), err_msg
    assert isinstance(tags._skip_test, bool), err_msg

    assert isinstance(tags.target_tags.required, bool), err_msg
    assert isinstance(tags.target_tags.one_d_labels, bool), err_msg
    assert isinstance(tags.target_tags.two_d_labels, bool), err_msg
    assert isinstance(tags.target_tags.positive_only, bool), err_msg
    assert isinstance(tags.target_tags.multi_output, bool), err_msg
    assert isinstance(tags.target_tags.single_output, bool), err_msg

    assert isinstance(tags.input_tags.pairwise, bool), err_msg
    assert isinstance(tags.input_tags.allow_nan, bool), err_msg
    assert isinstance(tags.input_tags.sparse, bool), err_msg
    assert isinstance(tags.input_tags.categorical, bool), err_msg
    assert isinstance(tags.input_tags.string, bool), err_msg
    assert isinstance(tags.input_tags.dict, bool), err_msg
    assert isinstance(tags.input_tags.one_d_array, bool), err_msg
    assert isinstance(tags.input_tags.two_d_array, bool), err_msg
    assert isinstance(tags.input_tags.three_d_array, bool), err_msg
    assert isinstance(tags.input_tags.positive_only, bool), err_msg

    if tags.classifier_tags is not None:
        assert isinstance(tags.classifier_tags.poor_score, bool), err_msg
        assert isinstance(tags.classifier_tags.multi_class, bool), err_msg
        assert isinstance(tags.classifier_tags.multi_label, bool), err_msg

    if tags.regressor_tags is not None:
        assert isinstance(tags.regressor_tags.poor_score, bool), err_msg

    if tags.transformer_tags is not None:
        assert isinstance(tags.transformer_tags.preserves_dtype, list), err_msg

def check_estimator_tags_renamed(name, estimator_orig):
    help = """{tags_func}() was removed in 1.6. Please use __sklearn_tags__ instead.
You can implement both __sklearn_tags__() and {tags_func}() to support multiple
scikit-learn versions.
"""

    for klass in type(estimator_orig).mro():
        if (
            # Here we check vars(...) because we want to check if the method is
            # explicitly defined in the class instead of inherited from a parent class.
            ("_more_tags" in vars(klass) or "_get_tags" in vars(klass))
            and "__sklearn_tags__" not in vars(klass)
        ):
                f"Estimator {name} has defined either `_more_tags` or `_get_tags`,"
                " but not `__sklearn_tags__`. If you're customizing tags, and need to"
                " support multiple scikit-learn versions, you can implement both"
                " `__sklearn_tags__` and `_more_tags` or `_get_tags`. This change was"
                " introduced in scikit-learn=1.6"
            )

def check_dataframe_column_names_consistency(name, estimator_orig):
    try:
            "pandas is not installed: not checking column name consistency for pandas"
        )

        return

    X_orig = rng.normal(size=(150, 8))

    X_orig = _enforce_estimator_tags_X(estimator, X_orig)
    n_samples, n_features = X_orig.shape

    names = np.array([f"col_{i}" for i in range(n_features)])
    X = pd.DataFrame(X_orig, columns=names, copy=False)

    else:

    # Check that calling `fit` does not raise any warnings about feature names.
        warnings.filterwarnings(
            "error",
            message="X does not have valid feature names",
            module="sklearn",
        )

    if not hasattr(estimator, "feature_names_in_"):
            "Estimator does not have a feature_names_in_ "
            "attribute after fitting with a dataframe"
        )
    assert isinstance(estimator.feature_names_in_, np.ndarray)
    assert estimator.feature_names_in_.dtype == object
    assert_array_equal(estimator.feature_names_in_, names)

    # Only check sklearn estimators for feature_names_in_ in docstring
    module_name = estimator_orig.__module__
    if (
        module_name.startswith("sklearn.")
        and not ("test_" in module_name or module_name.endswith("_testing"))
        and ("feature_names_in_" not in (estimator_orig.__doc__))
    ):
            f"Estimator {name} does not document its feature_names_in_ attribute"
        )

    check_methods = []
        "score",
    ):
            continue

        check_methods.append((method, callable_method))

    for _, method in check_methods:
                "error",
                message="X does not have valid feature names",
                category=UserWarning,
                module="sklearn",
            )
            method(X)  # works without UserWarning for valid features

    invalid_names = [
        (names[::-1], "Feature names must be in the same order as they were in fit."),
        (
            [f"another_prefix_{i}" for i in range(n_features)],
            (
                "Feature names unseen at fit time:\n- another_prefix_0\n-"
                " another_prefix_1\n"
            ),
        ),
        (
            names[:3],
            f"Feature names seen at fit time, yet now missing:\n- {min(names[3:])}\n",
        ),
    ]
    params = {
        key: value
        for key, value in estimator.get_params().items()
        if "early_stopping" in key
    }
    early_stopping_enabled = any(value is True for value in params.values())

    for invalid_name, additional_message in invalid_names:
        X_bad = pd.DataFrame(X, columns=invalid_name, copy=False)

            "The feature names should match those that were passed during fit.\n"
            f"{additional_message}"
        )
        for name, method in check_methods:
                ValueError, match=expected_msg, err_msg=f"{name} did not raise"
            ):
                method(X_bad)

        # partial_fit checks on second call
        # Do not call partial fit if early_stopping is on
        if not hasattr(estimator, "partial_fit") or early_stopping_enabled:
            continue

        else:

        with raises(ValueError, match=expected_msg):
            estimator.partial_fit(X_bad, y)

def check_transformer_get_feature_names_out(name, transformer_orig):
    tags = get_tags(transformer_orig)
    if not tags.input_tags.two_d_array or tags.no_validation:
        return

    )

    y_ = y

    X_transform = transformer.fit_transform(X, y=y_)
    input_features = [f"feature{i}" for i in range(n_features)]

    # input_features names is not the same length as n_features_in_
    with raises(ValueError, match="input_features should have length equal"):
        transformer.get_feature_names_out(input_features[::2])

    feature_names_out = transformer.get_feature_names_out(input_features)
    assert feature_names_out is not None
    assert isinstance(feature_names_out, np.ndarray)
    assert feature_names_out.dtype == object
    assert all(isinstance(name, str) for name in feature_names_out)

    if isinstance(X_transform, tuple):
        n_features_out = X_transform[0].shape[1]
    else:
        n_features_out = X_transform.shape[1]

    assert len(feature_names_out) == n_features_out, (
        f"Expected {n_features_out} feature names, got {len(feature_names_out)}"
    )

def check_transformer_get_feature_names_out_pandas(name, transformer_orig):
    try:
        )

        return

    )

    y_ = y

    feature_names_in = [f"col{i}" for i in range(n_features)]
    df = pd.DataFrame(X, columns=feature_names_in, copy=False)
    X_transform = transformer.fit_transform(df, y=y_)

    # error is raised when `input_features` do not match feature_names_in
    invalid_feature_names = [f"bad{i}" for i in range(n_features)]
    with raises(ValueError, match="input_features is not equal to feature_names_in_"):
        transformer.get_feature_names_out(invalid_feature_names)

    feature_names_out_default = transformer.get_feature_names_out()
    feature_names_in_explicit_names = transformer.get_feature_names_out(
        feature_names_in
    )
    assert_array_equal(feature_names_out_default, feature_names_in_explicit_names)

    else:

    assert len(feature_names_out_default) == n_features_out, (
        f"Expected {n_features_out} feature names, got {len(feature_names_out_default)}"
    )

def check_param_validation(name, estimator_orig):
    # Check that an informative error is raised when the value of a constructor
    # parameter does not have an appropriate type or value.
    y = rng.randint(0, 2, size=20)

    estimator_params = estimator_orig.get_params(deep=False).keys()

    if estimator_params:
        validation_params = estimator_orig._parameter_constraints.keys()
        unexpected_params = set(validation_params) - set(estimator_params)
        missing_params = set(estimator_params) - set(validation_params)
        err_msg = (
            f"Mismatch between _parameter_constraints and the parameters of {name}."
            f"\nConsider the unexpected parameters {unexpected_params} and expected but"
            f" missing parameters {missing_params}"
        )
        assert validation_params == estimator_params, err_msg

    fit_methods = ["fit", "partial_fit", "fit_transform", "fit_predict"]

    for param_name in estimator_params:
        constraints = estimator_orig._parameter_constraints[param_name]

            continue

        if any(
        ) and any(
        ):
                f"The constraint for parameter {param_name} of {name} can't have a mix"
                " of intervals of Integral and Real types. Use the type RealNotInt"
                " instead of Real."
            )

        match = rf"The '{param_name}' parameter of {name} must be .* Got .* instead."
        err_msg = (
            f"{name} does not raise an informative error message when the "
            f"parameter {param_name} does not have a valid type or value."
        )

        estimator.set_params(**{param_name: param_with_bad_type})

        for method in fit_methods:
            if not hasattr(estimator, method):
                # the method is not accessible with the current set of parameters
                continue

            err_msg = (
                f"{name} does not raise an informative error message when the parameter"
                f" {param_name} does not have a valid type. If any Python type is"
                " valid, the constraint should be 'no_validation'."
            )

            with raises(InvalidParameterError, match=match, err_msg=err_msg):
                if tags.target_tags.one_d_labels or tags.target_tags.two_d_labels:
                    # The estimator is a label transformer and take only `y`
                    getattr(estimator, method)(y)
                else:
                    getattr(estimator, method)(X, y)

            try:
                continue

            estimator.set_params(**{param_name: bad_value})

            for method in fit_methods:
                if not hasattr(estimator, method):
                    # the method is not accessible with the current set of parameters
                    continue

                err_msg = (
                    f"{name} does not raise an informative error message when the "
                    f"parameter {param_name} does not have a valid value.\n"
                    "Constraints should be disjoint. For instance "
                    "[StrOptions({'a_string'}), str] is not a acceptable set of "
                    "constraint because generating an invalid string for the first "
                    "constraint will always produce a valid string for the second "
                    "constraint."
                )

                with raises(InvalidParameterError, match=match, err_msg=err_msg):
                    if tags.target_tags.one_d_labels or tags.target_tags.two_d_labels:
                        # The estimator is a label transformer and take only `y`
                        getattr(estimator, method)(y)
                    else:
                        getattr(estimator, method)(X, y)

def check_set_output_transform(name, transformer_orig):
    # Check transformer.set_output with the default configuration does not
    # change the transform output.
        return

    y = _enforce_estimator_tags_y(transformer_orig, y)

    def fit_then_transform(est):
            return est.fit(X, y).transform(X, y)
        return est.fit(X, y).transform(X)

    def fit_transform(est):
        return est.fit_transform(X, y)

    transform_methods = {
        "transform": fit_then_transform,
        "fit_transform": fit_transform,
    }
    for name, transform_method in transform_methods.items():
        transformer = clone(transformer)
        if not hasattr(transformer, name):
            continue
        X_trans_no_setting = transform_method(transformer)

        # Auto wrapping only wraps the first array
            X_trans_no_setting = X_trans_no_setting[0]

        X_trans_default = transform_method(transformer)

            X_trans_default = X_trans_default[0]

        # Default and no setting -> returns the same transformation
        assert_allclose_dense_sparse(X_trans_no_setting, X_trans_default)

def _output_from_fit_transform(transformer, name, X, df, y):
    """Generate output to test `set_output` for different configuration:

    - calling either `fit.transform` or `fit_transform`;
    - passing either a dataframe or a numpy array to fit;
    - passing either a dataframe or a numpy array to transform.
    """
    outputs = {}

    # fit then transform case:
    cases = [
        ("fit.transform/df/df", df, df),
        ("fit.transform/df/array", df, X),
        ("fit.transform/array/df", X, df),
        ("fit.transform/array/array", X, X),
    ]
    if all(hasattr(transformer, meth) for meth in ["fit", "transform"]):
        for (
            case,
            data_fit,
            data_transform,
        ) in cases:
            transformer.fit(data_fit, y)
            if name in CROSS_DECOMPOSITION:
                X_trans, _ = transformer.transform(data_transform, y)
            else:
                X_trans = transformer.transform(data_transform)
            outputs[case] = (X_trans, transformer.get_feature_names_out())

    # fit_transform case:
    cases = [
        ("fit_transform/df", df),
        ("fit_transform/array", X),
    ]
    if hasattr(transformer, "fit_transform"):
        for case, data in cases:
                X_trans, _ = transformer.fit_transform(data, y)
            else:
                X_trans = transformer.fit_transform(data, y)

    return outputs

def _check_generated_dataframe(
    name,
    case,
    index,
    outputs_default,
    outputs_dataframe_lib,
    is_supported_dataframe,
    create_dataframe,
    assert_frame_equal,
):
    """Check if the generated DataFrame by the transformer is valid.

    The DataFrame implementation is specified through the parameters of this function.

    name : str
        The name of the transformer.
    case : str
        A single case from the cases generated by `_output_from_fit_transform`.
    index : index or None
        The index of the DataFrame. `None` if the library does not implement a DataFrame
        with an index.
    outputs_default : tuple
        A tuple containing the output data and feature names for the default output.
    outputs_dataframe_lib : tuple
        A tuple containing the output data and feature names for the pandas case.
    is_supported_dataframe : callable
        A callable that takes a DataFrame instance as input and return whether or
        E.g. `lambda X: isintance(X, pd.DataFrame)`.
    create_dataframe : callable
        A callable taking as parameters `data`, `columns`, and `index` and returns
        a callable. Be aware that `index` can be ignored. For example, polars dataframes
        would ignore the idnex.
    assert_frame_equal : callable
        A callable taking 2 dataframes to compare if they are equal.
    """
    X_trans, feature_names_default = outputs_default
    df_trans, feature_names_dataframe_lib = outputs_dataframe_lib

    assert is_supported_dataframe(df_trans)
    # We always rely on the output of `get_feature_names_out` of the
    # transformer used to generate the dataframe as a ground-truth of the
    # columns.
    # If a dataframe is passed into transform, then the output should have the same
    # index
    expected_index = index if case.endswith("df") else None
    expected_dataframe = create_dataframe(
        X_trans, columns=feature_names_dataframe_lib, index=expected_index
    )

    try:
        assert_frame_equal(df_trans, expected_dataframe)
    except AssertionError as e:
            f"{name} does not generate a valid dataframe in the {case} "
            "case. The generated dataframe is not equal to the expected "
            f"dataframe. The error message is: {e}"
        ) from e

def _check_set_output_transform_dataframe(
    name,
    transformer_orig,
    *,
    dataframe_lib,
    context,
):
    """Check that a transformer can output a DataFrame when requested.

    name : str
    transformer_orig : estimator
        The original transformer instance.
    dataframe_lib : str
        The name of the library implementing the DataFrame.
        A callable that takes a DataFrame instance as input and returns whether or
        not it is supported by the dataframe library.
        will ignore the index.
    context : {"local", "global"}
        Whether to use a local context by setting `set_output(...)` on the transformer
        or a global context by using the `with config_context(...)`
    """
    # Check transformer.set_output configures the output of transform="pandas".
        return

    feature_names_in = [f"col{i}" for i in range(X.shape[1])]
    index = [f"index{i}" for i in range(X.shape[0])]
    df = create_dataframe(X, columns=feature_names_in, index=index)

    transformer_default = clone(transformer).set_output(transform="default")
    outputs_default = _output_from_fit_transform(transformer_default, name, X, df, y)

    if context == "local":
        transformer_df = clone(transformer).set_output(transform=dataframe_lib)
        context_to_use = nullcontext()
    else:  # global
        transformer_df = clone(transformer)
        context_to_use = config_context(transform_output=dataframe_lib)

    try:
        with context_to_use:
            outputs_df = _output_from_fit_transform(transformer_df, name, X, df, y)
        # transformer does not support sparse data
        capitalized_lib = dataframe_lib.capitalize()
        error_message = str(e)
        assert (
            f"{capitalized_lib} output does not support sparse data." in error_message
            or "The transformer outputs a scipy sparse matrix." in error_message
        ), e
        return

    for case in outputs_default:
        _check_generated_dataframe(
            name,
            case,
            index,
            outputs_default[case],
            outputs_df[case],
            is_supported_dataframe,
            create_dataframe,
            assert_frame_equal,
        )

def _check_set_output_transform_pandas_context(name, transformer_orig, context):
    try:
    except ImportError:  # pragma: no cover
        raise SkipTest("pandas is not installed: not checking set output")

    _check_set_output_transform_dataframe(
        name,
        transformer_orig,
        dataframe_lib="pandas",
        is_supported_dataframe=lambda X: isinstance(X, pd.DataFrame),
        create_dataframe=lambda X, columns, index: pd.DataFrame(
            X, columns=columns, copy=False, index=index
        ),
        assert_frame_equal=pd.testing.assert_frame_equal,
        context=context,
    )

def check_set_output_transform_pandas(name, transformer_orig):
    _check_set_output_transform_pandas_context(name, transformer_orig, "local")

def check_global_output_transform_pandas(name, transformer_orig):
    _check_set_output_transform_pandas_context(name, transformer_orig, "global")

def _check_set_output_transform_polars_context(name, transformer_orig, context):
    try:
        import polars as pl
        from polars.testing import assert_frame_equal
        raise SkipTest("polars is not installed: not checking set output")

    def create_dataframe(X, columns, index):
        if isinstance(columns, np.ndarray):
            columns = columns.tolist()

        return pl.DataFrame(X, schema=columns, orient="row")

        name,
        dataframe_lib="polars",
        is_supported_dataframe=lambda X: isinstance(X, pl.DataFrame),
        create_dataframe=create_dataframe,
        assert_frame_equal=assert_frame_equal,
    )

def check_set_output_transform_polars(name, transformer_orig):
    _check_set_output_transform_polars_context(name, transformer_orig, "local")

def check_global_set_output_transform_polars(name, transformer_orig):
    _check_set_output_transform_polars_context(name, transformer_orig, "global")

def check_inplace_ensure_writeable(name, estimator_orig):
    """Check that estimators able to do inplace operations can work on read-only
    input data even if a copy is not explicitly requested by the user.

    Make sure that a copy is made and consequently that the input array and its
    writeability are not modified by the estimator.
    """

    X, _ = make_blobs(n_samples=n_samples, n_features=3, random_state=rng)

    # These estimators can only work inplace with fortran ordered input
    if name in ("Lasso", "ElasticNet", "MultiTaskElasticNet", "MultiTaskLasso"):

    # Add a missing value for imputers so that transform has to do something
    if hasattr(estimator, "missing_values"):
        X[0, 0] = np.nan

    else:

    # Make X read-only

    assert not X.flags.writeable
    assert_allclose(X, X_copy)

def check_do_not_raise_errors_in_init_or_set_params(name, estimator_orig):
    """Check that init or set_param does not raise errors."""
    Estimator = type(estimator_orig)
    params = signature(Estimator).parameters

    smoke_test_values = [-1, 3.0, "helloworld", np.array([1.0, 4.0]), [1], {}, []]
    for value in smoke_test_values:
        new_params = {key: value for key in params}

        # Does not raise
        est = Estimator(**new_params)

        # Also do does not raise
        est.set_params(**new_params)

def check_classifier_not_supporting_multiclass(name, estimator_orig):
    """Check that if the classifier has tags.classifier_tags.multi_class=False,
    then it should raise a ValueError when calling fit with a multiclass dataset.

    This test is not yielded if the tag is not False.
    """

    )
        The estimator tag `tags.classifier_tags.multi_class` is False for {name}
        which means it does not support multiclass classification. However, it does
        not raise the right `ValueError` when calling fit with a multiclass dataset,
        including the error message 'Only binary classification is supported.' This
        can be achieved by the following pattern:

        y_type = type_of_target(y, input_name='y', raise_unknown=True)
        if y_type != 'binary':
                'Only binary classification is supported. The type of the target '
                f'is {{y_type}}.'
        )
    """.format(name=name)

        ValueError, match="Only binary classification is supported.", err_msg=err_msg
    ):

            )

                return (
                )

                return (
                    #
                )

def _message_with_time(source, message, time):
    """Create one line message for logging purposes.

    source : str
        String indicating the source or the reference of the message.

        Short message.

    time : int
        Time in seconds.
    """
    start_message = "[%s] " % source

    # adapted from joblib.logger.short_format_time without the Windows -.1s
    # adjustment
    if time > 60:
        time_str = "%4.1fmin" % (time / 60)
    else:
        time_str = " %5.1fs" % time
    end_message = " %s, total=%s" % (message, time_str)
    dots_len = 70 - len(start_message) - len(end_message)
    return "%s%s%s" % (start_message, dots_len * ".", end_message)

def _print_elapsed_time(source, message=None):
    """Log elapsed time to stdout when the context is exited.

    message : str, default=None
        Short message. If None, nothing will be printed.

    Returns
    -------
    context_manager
        Prints elapsed time upon exit if verbose.
    """
    if message is None:
        yield
    else:
        start = timeit.default_timer()
        yield
        print(_message_with_time(source, message, timeit.default_timer() - start))

class deprecated:
    """Decorator to mark a function or class as deprecated.

    Issue a warning when the function is called/the class is instantiated and
    adds a warning to the docstring.

    The optional extra argument will be appended to the deprecation message
    and the docstring. Note: to use this with the default value for extra, put
    in an empty of parentheses:

    Examples
    --------
    >>> from sklearn.utils import deprecated
    >>> deprecated()
    <sklearn.utils.deprecation.deprecated object at ...>
    >>> @deprecated()
    ... def some_function(): pass

    extra : str, default=''
          To be added to the deprecation messages.
    """

    # Adapted from https://wiki.python.org/moin/PythonDecoratorLibrary,
    # but with many changes.

    def __init__(self, extra=""):
        self.extra = extra

    def __call__(self, obj):
        """Call method

        obj : object
        """
        if isinstance(obj, type):
            return self._decorate_class(obj)
        elif isinstance(obj, property):
            # Note that this is only triggered properly if the `deprecated`
            # decorator is placed before the `property` decorator, like so:
            #
            # @deprecated(msg)
            # @property
            # def deprecated_attribute_(self):
            #     ...
            return self._decorate_property(obj)
        else:
            return self._decorate_fun(obj)

    def _decorate_class(self, cls):
        msg = "Class %s is deprecated" % cls.__name__
        if self.extra:
            msg += "; %s" % self.extra

        new = cls.__new__
        sig = signature(cls)

        def wrapped(cls, *args, **kwargs):
            warnings.warn(msg, category=FutureWarning)
            if new is object.__new__:
                return object.__new__(cls)

            return new(cls, *args, **kwargs)

        cls.__new__ = wrapped

        wrapped.__name__ = "__new__"
        wrapped.deprecated_original = new
        # Restore the original signature, see PEP 362.
        cls.__signature__ = sig

        return cls

    def _decorate_fun(self, fun):
        """Decorate function fun"""

        msg = "Function %s is deprecated" % fun.__name__

        @functools.wraps(fun)
            return fun(*args, **kwargs)

        # Add a reference to the wrapped function so that we can introspect
        # on function arguments in Python 2 (already works in Python 3)
        wrapped.__wrapped__ = fun

        return wrapped

    def _decorate_property(self, prop):
        msg = self.extra

        @property
        @functools.wraps(prop.fget)
            return prop.fget(*args, **kwargs)

def _is_deprecated(func):
    """Helper to check if func is wrapped by our deprecated decorator"""
    closures = getattr(func, "__closure__", [])
    if closures is None:
        closures = []
    is_deprecated = "deprecated" in "".join(
        [c.cell_contents for c in closures if isinstance(c.cell_contents, str)]
    )
    return is_deprecated

def _deprecate_force_all_finite(force_all_finite, ensure_all_finite):
    """Helper to deprecate force_all_finite in favor of ensure_all_finite."""
    if force_all_finite != "deprecated":
            "'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be "
            "removed in 1.8.",
        )

        if ensure_all_finite is not None:
                "'force_all_finite' and 'ensure_all_finite' cannot be used together. "
                "Pass `ensure_all_finite` only."
            )

        return force_all_finite

    if ensure_all_finite is None:

    return ensure_all_finite

        """
            #
            #     ...
        else:

        return cls

        @property

def _get_dense_mask(X, value_to_mask):
    with suppress(ImportError, AttributeError):
        # We also suppress `AttributeError` because older versions of pandas do
        # not have `NA`.
        import pandas

        if value_to_mask is pandas.NA:
            return pandas.isna(X)

    if is_scalar_nan(value_to_mask):
            Xt = np.isnan(X)
        elif X.dtype.kind in ("i", "u"):
            # can't have NaNs in integer array.
            Xt = np.zeros(X.shape, dtype=bool)
        else:
            # np.isnan does not work on object dtypes.
            Xt = _object_dtype_isnan(X)
    else:
        Xt = X == value_to_mask

    return Xt

def _get_mask(X, value_to_mask):
    """Compute the boolean mask X == value_to_mask.

        Input data, where ``n_samples`` is the number of samples and
        ``n_features`` is the number of features.

    value_to_mask : {int, float}
        The value which is to be masked in X.

    Returns
    -------
    X_mask : {ndarray, sparse matrix} of shape (n_samples, n_features)
        Missing mask.
    """
        # For all cases apart of a sparse input where we need to reconstruct
        # a sparse output
        return _get_dense_mask(X, value_to_mask)

    Xt = _get_dense_mask(X.data, value_to_mask)

    sparse_constructor = sp.csr_matrix if X.format == "csr" else sp.csc_matrix
    Xt_sparse = sparse_constructor(
        (Xt, X.indices.copy(), X.indptr.copy()), shape=X.shape, dtype=bool
    )

    return Xt_sparse

def safe_mask(X, mask):
    """Return a mask which is safe to use on X.

    X : {array-like, sparse matrix}
        Data on which to apply mask.

    mask : array-like
        Mask to be used on X.

    Returns
    -------
    mask : ndarray
        Array that is safe to use on X.

    Examples
    --------
    >>> from sklearn.utils import safe_mask
    >>> data = csr_matrix([[1], [2], [3], [4], [5]])
    >>> condition = [False, True, True, False, True]
    >>> mask = safe_mask(data, condition)
    >>> data[mask].toarray()
    array([[2],
           [3],
           [5]])
    """
    mask = np.asarray(mask)
    if np.issubdtype(mask.dtype, np.signedinteger):

    if hasattr(X, "toarray"):
        ind = np.arange(mask.shape[0])
        mask = ind[mask]
    return mask

def axis0_safe_slice(X, mask, len_mask):
    """Return a mask which is safer to use on X than safe_mask.

    This mask is safer than safe_mask since it returns an
    empty array, when a sparse matrix is sliced with a boolean mask
    with all False, instead of raising an unhelpful error in older
    versions of SciPy.

    See: https://github.com/scipy/scipy/issues/5361

    Also note that we can avoid doing the dot product by checking if
    the len_mask is not zero in _huber_loss_and_gradient but this
    is not going to be the bottleneck, since the number of outliers
    and non_outliers are typically non-zero and it makes the code
    tougher to follow.

    len_mask : int
        The length of the mask.

    Returns
    -------
    """
    if len_mask != 0:
        return X[safe_mask(X, mask), :]
    return np.zeros(shape=(0, X.shape[1]))

def indices_to_mask(indices, mask_length):
    """Convert list of indices to boolean mask.

    indices : list-like
        List of integers treated as indices.
    mask_length : int
        Length of boolean mask to be generated.
        This parameter must be greater than max(indices).

    Returns
    -------
    mask : 1d boolean nd-array
        Boolean array that is True where indices are present, else False.

    Examples
    --------
    >>> from sklearn.utils._mask import indices_to_mask
    >>> indices = [1, 2 , 3, 4]
    >>> indices_to_mask(indices, 5)
    array([False,  True,  True,  True,  True])
    """
    if mask_length <= np.max(indices):
        raise ValueError("mask_length must be greater than max(indices)")

    mask = np.zeros(mask_length, dtype=bool)
    mask[indices] = True

class Bunch(dict):
    """Container object exposing keys as attributes.

    Bunch objects are sometimes used as an output for functions and methods.
    They extend dictionaries by enabling values to be accessed by key,
    `bunch["value_key"]`, or by an attribute, `bunch.value_key`.

    Examples
    --------
    >>> from sklearn.utils import Bunch
    >>> b = Bunch(a=1, b=2)
    >>> b['b']
    2
    >>> b.b
    2
    >>> b.a = 3
    >>> b['a']
    3
    >>> b.c = 6
    >>> b['c']
    6
    """

    def __init__(self, **kwargs):
        super().__init__(kwargs)

        # Map from deprecated key to warning message
        self.__dict__["_deprecated_key_to_warnings"] = {}

    def __getitem__(self, key):
        if key in self.__dict__.get("_deprecated_key_to_warnings", {}):
                self._deprecated_key_to_warnings[key],
            )
        return super().__getitem__(key)

    def _set_deprecated(self, value, *, new_key, deprecated_key, warning_message):
        """Set key in dictionary to be deprecated with its warning message."""
        self.__dict__["_deprecated_key_to_warnings"][deprecated_key] = warning_message
        self[new_key] = self[deprecated_key] = value

    def __setattr__(self, key, value):
        self[key] = value

    def __dir__(self):
        return self.keys()

    def __getattr__(self, key):
        try:
            return self[key]
            raise AttributeError(key)

        # Bunch pickles generated with scikit-learn 0.16.* have an non
        # empty __dict__. This causes a surprising behaviour when
        # loading these pickles scikit-learn 0.17: reading bunch.key
        # uses __dict__ but assigning to bunch.key use __setattr__ and
        # only changes bunch['key']. More details can be found at:
        # https://github.com/scikit-learn/scikit-learn/issues/6196.
        # Overriding __setstate__ to be a noop has the effect of
        # ignoring the pickled __dict__
        pass

            )

        try:

        pass

def _unique(values, *, return_inverse=False, return_counts=False):
    """Helper function to find unique values with support for python objects.

    Uses pure python method for object dtype, and numpy method for
    all other dtypes.

    values : ndarray
        Values to check for unknowns.

    return_inverse : bool, default=False
        If True, also return the indices of the unique values.

    return_counts : bool, default=False
        If True, also return the number of times each unique item appears in
        values.

    Returns
    -------
    unique : ndarray
        The sorted unique values.

    unique_inverse : ndarray
        The indices to reconstruct the original array from the unique array.
        Only provided if `return_inverse` is True.

    unique_counts : ndarray
        The number of times each of the unique values comes up in the original
        array. Only provided if `return_counts` is True.
    """
    if values.dtype == object:
        return _unique_python(
            values, return_inverse=return_inverse, return_counts=return_counts
        )
    # numerical
    return _unique_np(
        values, return_inverse=return_inverse, return_counts=return_counts
    )

def _unique_np(values, return_inverse=False, return_counts=False):
    """Helper function to find unique values for numpy arrays that correctly
    accounts for nans. See `_unique` documentation for details."""
    xp, _ = get_namespace(values)

    inverse, counts = None, None

    if return_inverse and return_counts:
        uniques, _, inverse, counts = xp.unique_all(values)
    elif return_inverse:
        uniques, inverse = xp.unique_inverse(values)
    elif return_counts:
        uniques, counts = xp.unique_counts(values)
    else:
        uniques = xp.unique_values(values)

    # np.unique will have duplicate missing values at the end of `uniques`
    # here we clip the nans and remove it from uniques
    if uniques.size and is_scalar_nan(uniques[-1]):
        nan_idx = _searchsorted(uniques, xp.nan, xp=xp)
        uniques = uniques[: nan_idx + 1]
        if return_inverse:
            inverse[inverse > nan_idx] = nan_idx

        if return_counts:
            counts[nan_idx] = xp.sum(counts[nan_idx:])
            counts = counts[: nan_idx + 1]

    ret = (uniques,)

    if return_inverse:
        ret += (inverse,)

    if return_counts:
        ret += (counts,)

    return ret[0] if len(ret) == 1 else ret

class MissingValues(NamedTuple):
    """Data class for missing data information"""

    nan: bool
    none: bool

    def to_list(self):
        """Convert tuple to a list where None is always first."""
        output = []
        if self.none:
            output.append(None)
        if self.nan:
            output.append(np.nan)

def _extract_missing(values):
    """Extract missing values from `values`.

    values: set
        Set of values to extract missing from.

    Returns
    -------
    output: set
        Set with missing values extracted.

    missing_values: MissingValues
        Object with missing value information.
    """
    missing_values_set = {
        value for value in values if value is None or is_scalar_nan(value)
    }

    if not missing_values_set:
        return values, MissingValues(nan=False, none=False)

    if None in missing_values_set:
        if len(missing_values_set) == 1:
            output_missing_values = MissingValues(nan=False, none=True)
        else:
            # If there is more than one missing value, then it has to be
            # float('nan') or np.nan
            output_missing_values = MissingValues(nan=True, none=True)
    else:
        output_missing_values = MissingValues(nan=True, none=False)

    # create set without the missing values
    output = values - missing_values_set
    return output, output_missing_values

class _nandict(dict):
    """Dictionary with support for nans."""

    def __init__(self, mapping):
        super().__init__(mapping)
        for key, value in mapping.items():
            if is_scalar_nan(key):
                self.nan_value = value
                break

    def __missing__(self, key):
        if hasattr(self, "nan_value") and is_scalar_nan(key):
            return self.nan_value
        raise KeyError(key)

def _map_to_integer(values, uniques):
    """Map values based on its position in uniques."""
    xp, _ = get_namespace(values, uniques)
    table = _nandict({val: i for i, val in enumerate(uniques)})
    return xp.asarray([table[v] for v in values], device=device(values))

def _unique_python(values, *, return_inverse, return_counts):
    # Only used in `_uniques`, see docstring there for details
    try:
        uniques_set = set(values)
        uniques_set, missing_values = _extract_missing(uniques_set)

        uniques = sorted(uniques_set)
        uniques.extend(missing_values.to_list())
        uniques = np.array(uniques, dtype=values.dtype)
        types = sorted(t.__qualname__ for t in set(type(v) for v in values))
            "Encoders require their input argument must be uniformly "
            f"strings or numbers. Got {types}"
        )

        ret += (_map_to_integer(values, uniques),)

        ret += (_get_counts(values, uniques),)

def _encode(values, *, uniques, check_unknown=True):
    """Helper function to encode values into [0, n_uniques - 1].

    The numpy method has the limitation that the `uniques` need to
    be sorted. Importantly, this is not checked but assumed to already be
    the case. The calling method needs to ensure this for all non-object
    values.

        Values to encode.
    uniques : ndarray
        The unique values in `values`. If the dtype is not object, then
        `uniques` needs to be sorted.
    check_unknown : bool, default=True
        If True, check for values in `values` that are not in `unique`
        and raise an error. This is ignored for object dtype, and treated as
        True in this case. This parameter is useful for
        _BaseEncoder._transform() to avoid calling _check_unknown()
        twice.

    Returns
    -------
    encoded : ndarray
        Encoded values
    """
    if not xp.isdtype(values.dtype, "numeric"):
        try:
            return _map_to_integer(values, uniques)
        except KeyError as e:
            raise ValueError(f"y contains previously unseen labels: {e}")
    else:
        if check_unknown:
            diff = _check_unknown(values, uniques)
            if diff:
                raise ValueError(f"y contains previously unseen labels: {diff}")
        return _searchsorted(uniques, values, xp=xp)

def _check_unknown(values, known_values, return_mask=False):
    """
    Helper function to check for unknowns in values to be encoded.

    values : array
    known_values : array
        Known values. Must be unique.
    return_mask : bool, default=False
        If True, return a mask of the same shape as `values` indicating
        the valid values.

    Returns
    -------
    diff : list
        The unique values present in `values` and not in `know_values`.
    valid_mask : boolean array
        Additionally returned if ``return_mask=True``.

    """
    xp, _ = get_namespace(values, known_values)
    valid_mask = None

        values_set = set(values)
        values_set, missing_in_values = _extract_missing(values_set)

        uniques_set = set(known_values)
        uniques_set, missing_in_uniques = _extract_missing(uniques_set)
        diff = values_set - uniques_set

        nan_in_diff = missing_in_values.nan and not missing_in_uniques.nan
        none_in_diff = missing_in_values.none and not missing_in_uniques.none

        def is_valid(value):
            return (
                value in uniques_set
                or (missing_in_uniques.none and value is None)
                or (missing_in_uniques.nan and is_scalar_nan(value))
            )

        if return_mask:
            if diff or nan_in_diff or none_in_diff:
                valid_mask = xp.array([is_valid(value) for value in values])
            else:
                valid_mask = xp.ones(len(values), dtype=xp.bool)

        diff = list(diff)
        if none_in_diff:
            diff.append(None)
        if nan_in_diff:
            diff.append(np.nan)
    else:
        unique_values = xp.unique_values(values)
        diff = xpx.setdiff1d(unique_values, known_values, assume_unique=True, xp=xp)
            if diff.size:
                valid_mask = _isin(values, known_values, xp)
            else:

        # check for nans in the known_values
        if xp.any(xp.isnan(known_values)):
            diff_is_nan = xp.isnan(diff)
            if xp.any(diff_is_nan):
                # removes nan from valid_mask
                if diff.size and return_mask:
                    is_nan = xp.isnan(values)
                    valid_mask[is_nan] = 1

                # remove nan from diff
                diff = diff[~diff_is_nan]

    if return_mask:
        return diff, valid_mask
    return diff

class _NaNCounter(Counter):
    """Counter with support for nan values."""

    def __init__(self, items):
        super().__init__(self._generate_items(items))

    def _generate_items(self, items):
        """Generate items without nans. Stores the nan counts separately."""
            if not is_scalar_nan(item):
                yield item
                continue
            if not hasattr(self, "nan_count"):
                self.nan_count = 0
            self.nan_count += 1

        if hasattr(self, "nan_count") and is_scalar_nan(key):
            return self.nan_count

def _get_counts(values, uniques):
    """Get the count of each of the `uniques` in `values`.

    The counts will use the order passed in by `uniques`. For non-object dtypes,
    `uniques` is assumed to be sorted and `np.nan` is at the end.
    """
    if values.dtype.kind in "OU":
        counter = _NaNCounter(values)
        output = np.zeros(len(uniques), dtype=np.int64)
        for i, item in enumerate(uniques):
            with suppress(KeyError):
                output[i] = counter[item]

    unique_values, counts = _unique_np(values, return_counts=True)

    # Recorder unique_values based on input: `uniques`
    uniques_in_values = np.isin(uniques, unique_values, assume_unique=True)
    if np.isnan(unique_values[-1]) and np.isnan(uniques[-1]):
        uniques_in_values[-1] = True

    unique_valid_indices = np.searchsorted(unique_values, uniques[uniques_in_values])
    output = np.zeros_like(uniques, dtype=np.int64)
    output[uniques_in_values] = counts[unique_valid_indices]
    return output

        output = []

                break

                yield item
                continue

            return (
            )

def check_library_installed(library):
    """Check library is installed."""
    try:
        return importlib.import_module(library)
    except ImportError as exc:
            f"Setting output container to '{library}' requires {library} to be"
            " installed"
        ) from exc

def get_columns(columns):
    if callable(columns):
        try:
            return columns()
    return columns

class ContainerAdapterProtocol(Protocol):
    container_lib: str

    def create_container(self, X_output, X_original, columns, inplace=False):
        """Create container from `X_output` with additional metadata.

        X_output : {ndarray, dataframe}
            Data to wrap.

        X_original : {ndarray, dataframe}
            Original input dataframe. This is used to extract the metadata that should
            be passed to `X_output`, e.g. pandas row index.

        columns : callable, ndarray, or None
            The column names or a callable that returns the column names. The
            callable is useful if the column names require some computation. If `None`,
            then no columns are passed to the container's constructor.

        inplace : bool, default=False
            Whether or not we intend to modify `X_output` in-place. However, it does
            not guarantee that we return the same object if the in-place operation
            is not possible.

        Returns
        -------
        wrapped_output : container_type
            `X_output` wrapped into the container type.
        """

    def is_supported_container(self, X):
        """Return True if X is a supported container.

        Xs: container
            Containers to be checked.

        Returns
        -------
        is_supported_container : bool
            True if X is a supported container.
        """

    def rename_columns(self, X, columns):
        """Rename columns in `X`.

        X : container
            Container which columns is updated.

        columns : ndarray of str
            Columns to update the `X`'s columns with.

        Returns
        -------
        updated_container : container
            Container with new names.
        """

    def hstack(self, Xs):
        """Stack containers horizontally (column-wise).

        Xs : list of containers
            List of containers to stack.

        Returns
        -------
        stacked_Xs : container
            Stacked containers.
        """

class PandasAdapter:
    container_lib = "pandas"

    def create_container(self, X_output, X_original, columns, inplace=True):
        pd = check_library_installed("pandas")
        columns = get_columns(columns)

        if not inplace or not isinstance(X_output, pd.DataFrame):
            # In all these cases, we need to create a new DataFrame

            # Unfortunately, we cannot use `getattr(container, "index")`
            # because `list` exposes an `index` attribute.
            if isinstance(X_output, pd.DataFrame):
                index = X_output.index
            elif isinstance(X_original, pd.DataFrame):
                index = X_original.index
            else:
                index = None

            # We don't pass columns here because it would intend columns selection
            # instead of renaming.
            X_output = pd.DataFrame(X_output, index=index, copy=not inplace)

        if columns is not None:
            return self.rename_columns(X_output, columns)
        return X_output

        return isinstance(X, pd.DataFrame)

        # we cannot use `rename` since it takes a dictionary and at this stage we have
        # potentially duplicate column names in `X`
        X.columns = columns
        return X

        return pd.concat(Xs, axis=1)

class PolarsAdapter:
    container_lib = "polars"

        pl = check_library_installed("polars")
        columns = columns.tolist() if isinstance(columns, np.ndarray) else columns

        if not inplace or not isinstance(X_output, pl.DataFrame):
            return pl.DataFrame(X_output, schema=columns, orient="row")

        return isinstance(X, pl.DataFrame)

        return X

        return pl.concat(Xs, how="horizontal")

class ContainerAdaptersManager:
        self.adapters = {}

    @property
    def supported_outputs(self):
        return {"default"} | set(self.adapters)

    def register(self, adapter):
        self.adapters[adapter.container_lib] = adapter

def _get_adapter_from_container(container):
    """Get the adapter that knows how to handle such container.

    See :class:`sklearn.utils._set_output.ContainerAdapterProtocol` for more
    details.
    """
    module_name = container.__class__.__module__.split(".")[0]
    try:
        return ADAPTERS_MANAGER.adapters[module_name]
    except KeyError as exc:
        available_adapters = list(ADAPTERS_MANAGER.adapters.keys())
            "The container does not have a registered adapter in scikit-learn. "
            f"Available adapters are: {available_adapters} while the container "
            f"provided is: {container!r}."
        ) from exc

def _get_container_adapter(method, estimator=None):
    """Get container adapter."""
    dense_config = _get_output_config(method, estimator)["dense"]
    try:
        return ADAPTERS_MANAGER.adapters[dense_config]
    except KeyError:

def _get_output_config(method, estimator=None):
    """Get output config based on estimator and global configuration.

    method : {"transform"}
        Estimator's method for which the output container is looked up.

    estimator : estimator instance or None
        Estimator to get the output configuration from. If `None`, check global
        configuration is used.

    Returns
    -------
        Dictionary with keys:

        - "dense": specifies the dense container for `method`. This can be
          `"default"` or `"pandas"`.
    """
    est_sklearn_output_config = getattr(estimator, "_sklearn_output_config", {})
    if method in est_sklearn_output_config:
        dense_config = est_sklearn_output_config[method]
    else:
        dense_config = get_config()[f"{method}_output"]

    supported_outputs = ADAPTERS_MANAGER.supported_outputs
    if dense_config not in supported_outputs:
            f"output config must be in {sorted(supported_outputs)}, got {dense_config}"
        )

    return {"dense": dense_config}

def _wrap_data_with_container(method, data_to_wrap, original_input, estimator):
    """Wrap output with container based on an estimator's or global config.

        Estimator's method to get container output for.

    data_to_wrap : {ndarray, dataframe}
        Data to wrap with container.

    original_input : {ndarray, dataframe}
        Original input of function.

        Estimator with to get the output configuration from.

    Returns
    -------
    output : {ndarray, dataframe}
        If the output config is "default" or the estimator is not configured
        for wrapping return `data_to_wrap` unchanged.
        If the output config is "pandas", return `data_to_wrap` as a pandas
        DataFrame.
    """
    output_config = _get_output_config(method, estimator)

    if output_config["dense"] == "default" or not _auto_wrap_is_configured(estimator):
        return data_to_wrap

    dense_config = output_config["dense"]
    if issparse(data_to_wrap):
            "The transformer outputs a scipy sparse matrix. "
            "Try to set the transformer output to a dense array or disable "
            f"{dense_config.capitalize()} output with set_output(transform='default')."
        )

    adapter = ADAPTERS_MANAGER.adapters[dense_config]
    return adapter.create_container(
        data_to_wrap,
        original_input,
        columns=estimator.get_feature_names_out,
    )

def _wrap_method_output(f, method):
    """Wrapper used by `_SetOutputMixin` to automatically wrap methods."""

    @wraps(f)
    def wrapped(self, X, *args, **kwargs):
        data_to_wrap = f(self, X, *args, **kwargs)
        if isinstance(data_to_wrap, tuple):
            # only wrap the first output for cross decomposition
            return_tuple = (
                _wrap_data_with_container(method, data_to_wrap[0], X, self),
                *data_to_wrap[1:],
            )
            # Support for namedtuples `_make` is a documented API for namedtuples:
            # https://docs.python.org/3/library/collections.html#collections.somenamedtuple._make
            if hasattr(type(data_to_wrap), "_make"):
                return type(data_to_wrap)._make(return_tuple)
            return return_tuple

        return _wrap_data_with_container(method, data_to_wrap, X, self)

    return wrapped

def _auto_wrap_is_configured(estimator):
    """Return True if estimator is configured for auto-wrapping the transform method.

    `_SetOutputMixin` sets `_sklearn_auto_wrap_output_keys` to `set()` if auto wrapping
    is manually disabled.
    """
    auto_wrap_output_keys = getattr(estimator, "_sklearn_auto_wrap_output_keys", set())
    return (
        hasattr(estimator, "get_feature_names_out")
        and "transform" in auto_wrap_output_keys
    )

class _SetOutputMixin:
    """Mixin that dynamically wraps methods to return container based on config.

    Currently `_SetOutputMixin` wraps `transform` and `fit_transform` and configures
    it based on `set_output` of the global configuration.

    `set_output` is only defined if `get_feature_names_out` is defined and
    `auto_wrap_output_keys` is the default value.
    """

    def __init_subclass__(cls, auto_wrap_output_keys=("transform",), **kwargs):
        super().__init_subclass__(**kwargs)

        # Dynamically wraps `transform` and `fit_transform` and configure it's
        # output based on `set_output`.
        if not (
            isinstance(auto_wrap_output_keys, tuple) or auto_wrap_output_keys is None
        ):
            raise ValueError("auto_wrap_output_keys must be None or a tuple of keys.")

        if auto_wrap_output_keys is None:
            cls._sklearn_auto_wrap_output_keys = set()
            return

        # Mapping from method to key in configurations
        method_to_key = {
            "transform": "transform",
            "fit_transform": "transform",
        }
        cls._sklearn_auto_wrap_output_keys = set()

        for method, key in method_to_key.items():
            if not hasattr(cls, method) or key not in auto_wrap_output_keys:
                continue
            cls._sklearn_auto_wrap_output_keys.add(key)

            # Only wrap methods defined by cls itself
            if method not in cls.__dict__:
                continue
            wrapped_method = _wrap_method_output(getattr(cls, method), key)
            setattr(cls, method, wrapped_method)

    @available_if(_auto_wrap_is_configured)
        """Set output container.

        Returns
        -------
        """
        if transform is None:

        if not hasattr(self, "_sklearn_output_config"):
            self._sklearn_output_config = {}

        self._sklearn_output_config["transform"] = transform

def _safe_set_output(estimator, *, transform=None):
    """Safely call estimator.set_output and error if it not available.

    This is used by meta-estimators to set the output for child estimators.

        Estimator instance.

    transform : {"default", "pandas", "polars"}, default=None
        Configure output of the following estimator's methods:

        - `"transform"`
        - `"fit_transform"`

        If `None`, this operation is a no-op.

    Returns
    -------
    """
    set_output_for_transform = hasattr(estimator, "transform") or (
        hasattr(estimator, "fit_transform") and transform is not None
    )
    if not set_output_for_transform:
        # If estimator can not transform, then `set_output` does not need to be
        # called.
        return

            f"Unable to configure output for {estimator} because `set_output` "
            "is not available."
        )
    return estimator.set_output(transform=transform)

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

            else:

        return X

            )

        if not (
        ):

            return

        }

                continue

                continue

        Returns
        -------
        """

def _routing_enabled():
    """Return whether metadata routing is enabled.

    Returns
    -------
    enabled : bool
        Whether metadata routing is enabled. If the config is not set, it
        defaults to False.
    """
    return get_config().get("enable_metadata_routing", False)

def _raise_for_params(params, owner, method, allow=None):
    """Raise an error if metadata routing is not enabled and params are passed.

        The metadata passed to a method.

    owner : object
        The object to which the method belongs.

        The name of the method, e.g. "fit".

    allow : list of str, default=None
        A list of parameters which are allowed to be passed even if metadata
        routing is not enabled.

    Raises
    ------
        If metadata routing is not enabled and params are passed.
    """
    caller = (
        f"{owner.__class__.__name__}.{method}" if method else owner.__class__.__name__
    )

    allow = allow if allow is not None else {}

    if not _routing_enabled() and (params.keys() - allow):
            f"Passing extra keyword arguments to {caller} is only supported if"
            " enable_metadata_routing=True, which you can set using"
            " `sklearn.set_config`. See the User Guide"
            " <https://scikit-learn.org/stable/metadata_routing.html> for more"
            f" details. Extra parameters passed are: {set(params)}"
        )

def _raise_for_unsupported_routing(obj, method, **kwargs):
    """Raise when metadata routing is enabled and metadata is passed.

    This is used in meta-estimators which have not implemented metadata routing
    to prevent silent bugs. There is no need to use this function if the
    meta-estimator is not accepting any metadata, especially in `fit`, since
    if a meta-estimator accepts any metadata, they would do that in `fit` as
    well.

    obj : estimator
        The estimator for which we're raising the error.

        The method where the error is raised.

        The metadata passed to the method.
    """
    kwargs = {key: value for key, value in kwargs.items() if value is not None}
    if _routing_enabled() and kwargs:
        cls_name = obj.__class__.__name__
            f"{cls_name}.{method} cannot accept given metadata ({set(kwargs.keys())})"
            f" since metadata routing is not yet implemented for {cls_name}."
        )

class _RoutingNotSupportedMixin:
    """A mixin to be used to remove the default `get_metadata_routing`.

    This is used in meta-estimators where metadata routing is not yet
    implemented.

    This also makes it clear in our rendered documentation that this method
    cannot be used.
    """

        """Raise `NotImplementedError`.

        This estimator does not support metadata routing yet."""
            f"{self.__class__.__name__} has not implemented metadata routing yet."
        )

def request_is_alias(item):
    """Check if an item is a valid alias.

    Values in ``VALID_REQUEST_VALUES`` are not considered aliases in this
    context. Only a string which is a valid identifier is.

    item : object
        The given item to be checked if it can be an alias.

    Returns
    -------
    result : bool
        Whether the given item is a valid alias.
    """
    if item in VALID_REQUEST_VALUES:

    # item is only an alias if it's a valid identifier
    return isinstance(item, str) and item.isidentifier()

def request_is_valid(item):
    """Check if an item is a valid request value (and not an alias).

        The given item to be checked.

    Returns
    -------
        Whether the given item is valid.
    """
    return item in VALID_REQUEST_VALUES

class MethodMetadataRequest:
    """A prescription of how metadata is to be passed to a single method.

    Refer to :class:`MetadataRequest` for how this class is used.

    owner : str
        A display name for the object owning these requests.

        The name of the method to which these requests belong.

    requests : dict of {str: bool, None or str}, default=None
        The initial requests for this method.
    """

    def __init__(self, owner, method, requests=None):
        self._requests = requests or dict()
        self.owner = owner

    @property
    def requests(self):
        """Dictionary of the form: ``{key: alias}``."""
        return self._requests

    def add_request(
        self,
        *,
        param,
        alias,
    ):
        """Add request info for a metadata.

        param : str
            The property for which a request is set.

        alias : str, or {True, False, None}
            Specifies which metadata should be routed to `param`

            - str: the name (or alias) of metadata given to a meta-estimator that
              should be routed to this parameter.

            - True: requested

            - False: not requested

            - None: error if passed
        """
        if not request_is_alias(alias) and not request_is_valid(alias):
                f"The alias you're setting for `{param}` should be either a "
                "valid identifier or one of {None, True, False}, but given "
                f"value is: `{alias}`"
            )

        if alias == param:
            alias = True

        if alias == UNUSED:
            if param in self._requests:
                del self._requests[param]
            else:
                    f"Trying to remove parameter {param} with UNUSED which doesn't"
                    " exist."
                )
        else:
            self._requests[param] = alias

    def _get_param_names(self, return_alias):
        """Get names of all metadata that can be consumed or routed by this method.

        This method returns the names of all metadata, even the ``False``
        ones.

        return_alias : bool
            Controls whether original or aliased names should be returned. If
            ``False``, aliases are ignored and original names are returned.

        Returns
        -------
        names : set of str
            A set of strings with the names of all parameters.
        """
        return set(
            alias if return_alias and not request_is_valid(alias) else prop
            for prop, alias in self._requests.items()
            if not request_is_valid(alias) or alias is not False
        )

    def _check_warnings(self, *, params):
        """Check whether metadata is passed which is marked as WARN.

        If any metadata is passed which is marked as WARN, a warning is raised.

            The metadata passed to a method.
        """
        params = {} if params is None else params
        warn_params = {
            prop
            if alias == WARN and prop in params
        }
        for param in warn_params:
            warn(
                f"Support for {param} has recently been added to this class. "
                "To maintain backward compatibility, it is ignored now. "
                f"Using `set_{self.method}_request({param}={{True, False}})` "
                "on this method of the class, you can set the request value "
                "to False to silence this warning, or to True to consume and "
                "use the metadata."
            )

    def _route_params(self, params, parent, caller):
        """Prepare the given parameters to be passed to the method.

        The output of this method can be used directly as the input to the
        corresponding method as extra props.

            A dictionary of provided metadata.

        parent : object
            Parent class object, that routes the metadata.

        caller : str
            Method from the parent class object, where the metadata is routed from.

        Returns
        -------
        params : Bunch
            A :class:`~sklearn.utils.Bunch` of {prop: value} which can be given to the
            corresponding method.
        """
        self._check_warnings(params=params)
        unrequested = dict()
        args = {arg: value for arg, value in params.items() if value is not None}
        res = Bunch()
        for prop, alias in self._requests.items():
            if alias is False or alias == WARN:
                continue
            elif alias is True and prop in args:
                res[prop] = args[prop]
            elif alias is None and prop in args:
                unrequested[prop] = args[prop]
            elif alias in args:
                res[prop] = args[alias]
        if unrequested:
            if self.method in COMPOSITE_METHODS:
                callee_methods = COMPOSITE_METHODS[self.method]
            else:
                callee_methods = [self.method]
            set_requests_on = "".join(
                [
                    f".set_{method}_request({{metadata}}=True/False)"
                    for method in callee_methods
                ]
            )
            message = (
                f"[{', '.join([key for key in unrequested])}] are passed but are not"
                " explicitly set as requested or not requested for"
                f" {self.owner}.{self.method}, which is used within"
                f" {parent}.{caller}. Call `{self.owner}"
                + set_requests_on
                + "` for each metadata you want to request/ignore. See the"
                " Metadata Routing User guide"
                " <https://scikit-learn.org/stable/metadata_routing.html> for more"
                " information."
            )
            raise UnsetMetadataPassedError(
                message=message,
                unrequested_params=unrequested,
                routed_params=res,
            )
        return res

    def _consumes(self, params):
        """Check whether the given parameters are consumed by this method.

        params : iterable of str
            An iterable of parameters to check.

        Returns
        -------
        consumed : set of str
            A set of parameters which are consumed by this method.
        """
        params = set(params)
        res = set()
            if alias is True and prop in params:
                res.add(prop)
            elif isinstance(alias, str) and alias in params:
                res.add(alias)
        return res

    def _serialize(self):
        """Serialize the object.

        Returns
        -------
        obj : dict
            A serialized version of the instance in the form of a dictionary.
        """

        return str(self._serialize())

        return str(repr(self))

class MetadataRequest:
    """Contains the metadata request info of a consumer.

    Instances of `MethodMetadataRequest` are used in this class for each
    available method under `metadatarequest.{method}`.

    Consumer-only classes such as simple estimators return a serialized
    version of this class as the output of `get_metadata_routing()`.

    owner : str
        The name of the object to which these requests belong.
    """

    # this is here for us to use this attribute's value instead of doing
    # `isinstance` in our checks, so that we avoid issues when people vendor
    # this file instead of using it directly from scikit-learn.
    _type = "metadata_request"

    def __init__(self, owner):
        for method in SIMPLE_METHODS:
            setattr(
                self,
                method,
                MethodMetadataRequest(owner=owner, method=method),
            )

    def consumes(self, method, params):
        """Check whether the given parameters are consumed by the given method.

        method : str
            The name of the method to check.

        Returns
        -------
            A set of parameters which are consumed by the given method.
        """
        return getattr(self, method)._consumes(params=params)

        # Called when the default attribute access fails with an AttributeError
        # (either __getattribute__() raises an AttributeError because name is
        # not an instance attribute or an attribute in the class tree for self;
        # or __get__() of a name property raises AttributeError). This method
        # should either return the (computed) attribute value or raise an
        # AttributeError exception.
        # https://docs.python.org/3/reference/datamodel.html#object.__getattr__
        if name not in COMPOSITE_METHODS:
                f"'{self.__class__.__name__}' object has no attribute '{name}'"
            )

        requests = {}
        for method in COMPOSITE_METHODS[name]:
            mmr = getattr(self, method)
            existing = set(requests.keys())
            upcoming = set(mmr.requests.keys())
            common = existing & upcoming
            conflicts = [key for key in common if requests[key] != mmr._requests[key]]
            if conflicts:
                    f"Conflicting metadata requests for {', '.join(conflicts)} while"
                    f" composing the requests for {name}. Metadata with the same name"
                    f" for methods {', '.join(COMPOSITE_METHODS[name])} should have the"
                    " same request value."
                )
            requests.update(mmr._requests)
        return MethodMetadataRequest(owner=self.owner, method=name, requests=requests)

    def _get_param_names(self, method, return_alias, ignore_self_request=None):
        """Get names of all metadata that can be consumed or routed by specified \
            method.

        ones.

            The name of the method for which metadata names are requested.

        ignore_self_request : bool
            Ignored. Present for API compatibility.

        Returns
        -------
        """
        return getattr(self, method)._get_param_names(return_alias=return_alias)

    def _route_params(self, *, params, method, parent, caller):

        corresponding method as extra keyword arguments to pass metadata.

            The name of the method for which the parameters are requested and
            routed.

        Returns
        -------
        """
        return getattr(self, method)._route_params(
            params=params, parent=parent, caller=caller
        )

    def _check_warnings(self, *, method, params):

            The name of the method for which the warnings should be checked.

        """
        getattr(self, method)._check_warnings(params=params)

        Returns
        -------
        obj : dict
        """
        output = dict()
            if len(mmr.requests):
                output[method] = mmr._serialize()

class MethodMapping:
    """Stores the mapping between caller and callee methods for a router.

    This class is primarily used in a ``get_metadata_routing()`` of a router
    object when defining the mapping between the router's methods and a sub-object (a
    sub-estimator or a scorer).

    Iterating through an instance of this class yields
    ``MethodPair(caller, callee)`` instances.

    """

        self._routes = []

        return iter(self._routes)

    def add(self, *, caller, callee):
        """Add a method mapping.

            Parent estimator's method name in which the ``callee`` is called.

        callee : str
            Child object's method name. This method is called in ``caller``.

        Returns
        -------
        self : MethodMapping
        """
        if caller not in METHODS:
                f"Given caller:{caller} is not a valid method. Valid methods are:"
                f" {METHODS}"
            )
        if callee not in METHODS:
                f"Given callee:{callee} is not a valid method. Valid methods are:"
            )
        self._routes.append(MethodPair(caller=caller, callee=callee))

        Returns
        -------
        obj : list
            A serialized version of the instance in the form of a list.
        """
        result = list()
        for route in self._routes:
            result.append({"caller": route.caller, "callee": route.callee})

class MetadataRouter:
    """Stores and handles metadata routing for a router object.

    This class is used by router objects to store and handle metadata routing.
    Routing information is stored as a dictionary of the form ``{"object_name":
    RouteMappingPair(method_mapping, routing_info)}``, where ``method_mapping``
    is an instance of :class:`~sklearn.utils.metadata_routing.MethodMapping` and
    ``routing_info`` is either a
    :class:`~sklearn.utils.metadata_routing.MetadataRequest` or a
    :class:`~sklearn.utils.metadata_routing.MetadataRouter` instance.

    owner : str
    """

    # `isinstance`` in our checks, so that we avoid issues when people vendor
    _type = "metadata_router"

        self._route_mappings = dict()
        # `_self_request` is used if the router is also a consumer.
        # _self_request, (added using `add_self_request()`) is treated
        # differently from the other objects which are stored in
        # _route_mappings.
        self._self_request = None

    def add_self_request(self, obj):
        """Add `self` (as a consumer) to the routing.

        This method is used if the router is also a consumer, and hence the
        router itself needs to be included in the routing. The passed object
        can be an estimator or a
        :class:`~sklearn.utils.metadata_routing.MetadataRequest`.

        A router should add itself using this method instead of `add` since it
        should be treated differently than the other objects to which metadata
        is routed by the router.

            This is typically the router instance, i.e. `self` in a
            ``get_metadata_routing()`` implementation. It can also be a
            ``MetadataRequest`` instance.

        Returns
        -------
        self : MetadataRouter
            Returns `self`.
        """
        if getattr(obj, "_type", None) == "metadata_request":
            self._self_request = deepcopy(obj)
        elif hasattr(obj, "_get_metadata_request"):
            self._self_request = deepcopy(obj._get_metadata_request())
        else:
                "Given `obj` is neither a `MetadataRequest` nor does it implement the"
                " required API. Inheriting from `BaseEstimator` implements the required"
                " API."
            )

    def add(self, *, method_mapping, **objs):
        """Add named objects with their corresponding method mapping.

        method_mapping : MethodMapping
            The mapping between the child and the parent's methods.

        **objs : dict
            A dictionary of objects from which metadata is extracted by calling
            :func:`~sklearn.utils.metadata_routing.get_routing_for_object` on them.

        Returns
        -------
        """
        method_mapping = deepcopy(method_mapping)

        for name, obj in objs.items():
            self._route_mappings[name] = RouterMappingPair(
                mapping=method_mapping, router=get_routing_for_object(obj)
            )

        Returns
        -------
        """
        res = set()
        if self._self_request:
            res = res | self._self_request.consumes(method=method, params=params)

        for _, route_mapping in self._route_mappings.items():
            for caller, callee in route_mapping.mapping:
                if caller == method:
                    res = res | route_mapping.router.consumes(
                        method=callee, params=params
                    )

        return res

    def _get_param_names(self, *, method, return_alias, ignore_self_request):
            method.

        ones.

            Controls whether original or aliased names should be returned,
            which only applies to the stored `self`. If no `self` routing
            object is stored, this parameter has no effect.

            If `self._self_request` should be ignored. This is used in `_route_params`.
            If ``True``, ``return_alias`` has no effect.

        Returns
        -------
        """
        res = set()
        if self._self_request and not ignore_self_request:
            res = res.union(
                self._self_request._get_param_names(
                    method=method, return_alias=return_alias
                )
            )

        for name, route_mapping in self._route_mappings.items():
                    res = res.union(
                        route_mapping.router._get_param_names(
                            method=callee, return_alias=True, ignore_self_request=False
                        )
                    )
        return res

        This is used when a router is used as a child object of another router.
        The parent router then passes all parameters understood by the child
        object to it and delegates their validation to the child.

            routed.

        Returns
        -------
        """
            res.update(
                self._self_request._route_params(
                    params=params,
                    parent=parent,
                    caller=caller,
                )
            )

        param_names = self._get_param_names(
            method=method, return_alias=True, ignore_self_request=True
        )
        child_params = {
            key: value for key, value in params.items() if key in param_names
        }
        for key in set(res.keys()).intersection(child_params.keys()):
            # conflicts are okay if the passed objects are the same, but it's
            # an issue if they're different objects.
            if child_params[key] is not res[key]:
                    f"In {self.owner}, there is a conflict on {key} between what is"
                    " requested for this estimator and what is requested by its"
                    " children. You can resolve this conflict by using an alias for"
                    " the child estimator(s) requested metadata."
                )

        res.update(child_params)
        return res

    def route_params(self, *, caller, params):
        """Return the input parameters requested by child objects.

        The output of this method is a :class:`~sklearn.utils.Bunch`, which includes the
        metadata for all methods of each child object that is used in the router's
        `caller` method.

        If the router is also a consumer, it also checks for warnings of
        `self`'s/consumer's requested metadata.

            routed. If called inside the :term:`fit` method of a router, it
            would be `"fit"`.

        Returns
        -------
            A :class:`~sklearn.utils.Bunch` of the form
            ``{"object_name": {"method_name": {params: value}}}`` which can be
            used to pass the required metadata to corresponding methods or
            corresponding child objects.
        """
            self._self_request._check_warnings(params=params, method=caller)

            router, mapping = route_mapping.router, route_mapping.mapping

            res[name] = Bunch()
            for _caller, _callee in mapping:
                if _caller == caller:
                    res[name][_callee] = router._route_params(
                        params=params,
                        method=_callee,
                        parent=self.owner,
                        caller=caller,
                    )
        return res

    def validate_metadata(self, *, method, params):
        """Validate given metadata for a method.

        This raises a ``TypeError`` if some of the passed metadata are not
        understood by child objects.

        """
            method=method, return_alias=False, ignore_self_request=False
        )
            self_params = self._self_request._get_param_names(
                method=method, return_alias=False
            )
        else:
            self_params = set()
        extra_keys = set(params.keys()) - param_names - self_params
        if extra_keys:
                f"{self.owner}.{method} got unexpected argument(s) {extra_keys}, which"
                " are not routed to any object."
            )

        Returns
        -------
        obj : dict
        """
        res = dict()
            res["$self_request"] = self._self_request._serialize()
            res[name] = dict()
            res[name]["mapping"] = route_mapping.mapping._serialize()
            res[name]["router"] = route_mapping.router._serialize()

        return res

            for method in METHODS:
                method_mapping.add(caller=method, callee=method)
            yield (
                "$self_request",
                RouterMappingPair(mapping=method_mapping, router=self._self_request),
            )
            yield (name, route_mapping)

def get_routing_for_object(obj=None):
    """Get a ``Metadata{Router, Request}`` instance from the given object.

    This function returns a
    :class:`~sklearn.utils.metadata_routing.MetadataRouter` or a
    :class:`~sklearn.utils.metadata_routing.MetadataRequest` from the given input.

    This function always returns a copy or an instance constructed from the
    input, such that changing the output of this function will not change the
    original object.

    obj : object
        - If the object provides a `get_metadata_routing` method, return a copy
            of the output of that method.
        - If the object is already a
            :class:`~sklearn.utils.metadata_routing.MetadataRequest` or a
            :class:`~sklearn.utils.metadata_routing.MetadataRouter`, return a copy
            of that.
        - Returns an empty :class:`~sklearn.utils.metadata_routing.MetadataRequest`
            otherwise.

    Returns
    -------
    obj : MetadataRequest or MetadataRouting
        A ``MetadataRequest`` or a ``MetadataRouting`` taken or created from
        the given object.
    """
    # doing this instead of a try/except since an AttributeError could be raised
    # for other reasons.
    if hasattr(obj, "get_metadata_routing"):
        return deepcopy(obj.get_metadata_routing())

    elif getattr(obj, "_type", None) in ["metadata_request", "metadata_router"]:
        return deepcopy(obj)

    return MetadataRequest(owner=None)

class RequestMethod:
    """
    A descriptor for request methods.

    name : str
        The name of the method for which the request function should be
        created, e.g. ``"fit"`` would create a ``set_fit_request`` function.

    keys : list of str
        A list of strings which are accepted parameters by the created
        function, e.g. ``["sample_weight"]`` if the corresponding method
        accepts it as a metadata.

    validate_keys : bool, default=True
        Whether to check if the requested parameters fit the actual parameters
        of the method.

    Notes
    -----
    This class is a descriptor [1]_ and uses PEP-362 to set the signature of
    the returned function [2]_.

    .. [1] https://docs.python.org/3/howto/descriptor.html

    .. [2] https://www.python.org/dev/peps/pep-0362/
    """

    def __init__(self, name, keys, validate_keys=True):
        self.keys = keys
        self.validate_keys = validate_keys

    def __get__(self, instance, owner):
        # we would want to have a method which accepts only the expected args
        def func(*args, **kw):
            """Updates the request for provided parameters

            This docstring is overwritten below.
            See REQUESTER_DOC for expected functionality
            """
                    "This method is only available when metadata routing is enabled."
                    " You can enable it using"
                    " sklearn.set_config(enable_metadata_routing=True)."
                )

            if self.validate_keys and (set(kw) - set(self.keys)):
                    f"Unexpected args: {set(kw) - set(self.keys)} in {self.name}. "
                    f"Accepted arguments are: {set(self.keys)}"
                )

            # This makes it possible to use the decorated method as an unbound method,
            # for instance when monkeypatching.
            # https://github.com/scikit-learn/scikit-learn/issues/28632
            if instance is None:
                _instance = args[0]
            else:
                _instance = instance

            # Replicating python's behavior when positional args are given other than
            # `self`, and `self` is only allowed if this method is unbound.
            if args:
                    f"set_{self.name}_request() takes 0 positional argument but"
                    f" {len(args)} were given"
                )

            requests = _instance._get_metadata_request()
            method_metadata_request = getattr(requests, self.name)

            for prop, alias in kw.items():
                if alias is not UNCHANGED:
                    method_metadata_request.add_request(param=prop, alias=alias)
            _instance._metadata_request = requests

            return _instance

        # Now we set the relevant attributes of the function so that it seems
        # like a normal method to the end user, with known expected arguments.
        func.__name__ = f"set_{self.name}_request"
        params = [
            inspect.Parameter(
                name="self",
                kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
                annotation=owner,
            )
        ]
        params.extend(
            [
                inspect.Parameter(
                    k,
                    inspect.Parameter.KEYWORD_ONLY,
                    default=UNCHANGED,
                    annotation=Optional[Union[bool, None, str]],
                )
                for k in self.keys
            ]
        )
        func.__signature__ = inspect.Signature(
            params,
            return_annotation=owner,
        )
        doc = REQUESTER_DOC.format(method=self.name)
        for metadata in self.keys:
            doc += REQUESTER_DOC_PARAM.format(metadata=metadata, method=self.name)
        doc += REQUESTER_DOC_RETURN
        func.__doc__ = doc
        return func

class _MetadataRequester:
    """Mixin class for adding metadata request functionality.

    ``BaseEstimator`` inherits from this Mixin.

    """

    if TYPE_CHECKING:  # pragma: no cover
        # This code is never run in runtime, but it's here for type checking.
        # Type checkers fail to understand that the `set_{method}_request`
        # methods are dynamically generated, and they complain that they are
        # not defined. We define them here to make type checkers happy.
        # During type checking analyzers assume this to be True.
        # The following list of defined methods mirrors the list of methods
        # in SIMPLE_METHODS.
        # fmt: off
        def set_fit_request(self, **kwargs): pass
        def set_partial_fit_request(self, **kwargs): pass
        def set_predict_request(self, **kwargs): pass
        def set_predict_proba_request(self, **kwargs): pass
        def set_predict_log_proba_request(self, **kwargs): pass
        def set_decision_function_request(self, **kwargs): pass
        def set_score_request(self, **kwargs): pass
        def set_split_request(self, **kwargs): pass
        def set_transform_request(self, **kwargs): pass
        def set_inverse_transform_request(self, **kwargs): pass
        # fmt: on

    def __init_subclass__(cls, **kwargs):
        """Set the ``set_{method}_request`` methods.

        This uses PEP-487 [1]_ to set the ``set_{method}_request`` methods. It
        looks for the information available in the set default values which are
        set using ``__metadata_request__*`` class attributes, or inferred
        from method signatures.

        The ``__metadata_request__*`` class attributes are used when a method
        does not explicitly accept a metadata through its arguments or if the
        developer would like to specify a request value for those metadata
        which are different from the default ``None``.

        .. [1] https://www.python.org/dev/peps/pep-0487
        """
        try:
            requests = cls._get_default_requests()
            # if there are any issues in the default values, it will be raised
            # when ``get_metadata_routing`` is called. Here we are going to
            # ignore all the issues such as bad defaults etc.
            super().__init_subclass__(**kwargs)
            return

            mmr = getattr(requests, method)
            # set ``set_{method}_request`` methods
            if not len(mmr.requests):
                continue
            setattr(
                cls,
                f"set_{method}_request",
                RequestMethod(method, sorted(mmr.requests.keys())),
            )

    def _build_request_for_signature(cls, router, method):
        """Build the `MethodMetadataRequest` for a method using its signature.

        This method takes all arguments from the method signature and uses
        ``None`` as their default request value, except ``X``, ``y``, ``Y``,
        ``Xt``, ``yt``, ``*args``, and ``**kwargs``.

        router : MetadataRequest
            The parent object for the created `MethodMetadataRequest`.
            The name of the method.

        Returns
        -------
        method_request : MethodMetadataRequest
            The prepared request using the method's signature.
        """
        mmr = MethodMetadataRequest(owner=cls.__name__, method=method)
        # Here we use `isfunction` instead of `ismethod` because calling `getattr`
        # on a class instead of an instance returns an unbound function.
        if not hasattr(cls, method) or not inspect.isfunction(getattr(cls, method)):
            return mmr
        # ignore the first parameter of the method, which is usually "self"
        params = list(inspect.signature(getattr(cls, method)).parameters.items())[1:]
        for pname, param in params:
            if pname in {"X", "y", "Y", "Xt", "yt"}:
                continue
            if param.kind in {param.VAR_POSITIONAL, param.VAR_KEYWORD}:
                continue
            mmr.add_request(
                param=pname,
                alias=None,
            )
        return mmr

    def _get_default_requests(cls):
        """Collect default request values.

        This method combines the information present in ``__metadata_request__*``
        class attributes, as well as determining request keys from method
        signatures.
        """
        requests = MetadataRequest(owner=cls.__name__)

            setattr(
                requests,
                method,
                cls._build_request_for_signature(router=requests, method=method),
            )

        # Then overwrite those defaults with the ones provided in
        # __metadata_request__* attributes. Defaults set in
        # __metadata_request__* attributes take precedence over signature
        # sniffing.

        # need to go through the MRO since this is a class attribute and
        # ``vars`` doesn't report the parent class attributes. We go through
        # the reverse of the MRO so that child classes have precedence over
        # their parents.
        substr = "__metadata_request__"
        for base_class in reversed(inspect.getmro(cls)):
            for attr, value in vars(base_class).items():
                if substr not in attr:
                    continue
                # we don't check for attr.startswith() since python prefixes attrs
                # starting with __ with the `_ClassName`.
                method = attr[attr.index(substr) + len(substr) :]
                for prop, alias in value.items():
                    # Here we add request values specified via those class attributes
                    # to the `MetadataRequest` object. Adding a request which already
                    # exists will override the previous one. Since we go through the
                    # MRO in reverse order, the one specified by the lowest most classes
                    # in the inheritance tree are the ones which take effect.
                    getattr(requests, method).add_request(param=prop, alias=alias)

        return requests

    def _get_metadata_request(self):

        Returns
        -------
        request : MetadataRequest
            A :class:`~sklearn.utils.metadata_routing.MetadataRequest` instance.
        """
        if hasattr(self, "_metadata_request"):
            requests = get_routing_for_object(self._metadata_request)
        else:
            requests = self._get_default_requests()

        Returns
        -------
        routing : MetadataRequest
            A :class:`~sklearn.utils.metadata_routing.MetadataRequest` encapsulating
        """
        return self._get_metadata_request()

def process_routing(_obj, _method, /, **kwargs):
    """Validate and route input parameters.

    This function is used inside a router's method, e.g. :term:`fit`,
    to validate the metadata and handle the routing.

    Assuming this signature of a router's fit method:
    ``fit(self, X, y, sample_weight=None, **fit_params)``,
    a call to this function would be:
    ``process_routing(self, "fit", sample_weight=sample_weight, **fit_params)``.

    Note that if routing is not enabled and ``kwargs`` is empty, then it
    returns an empty routing where ``process_routing(...).ANYTHING.ANY_METHOD``
    is always an empty dictionary.

    _obj : object
        An object implementing ``get_metadata_routing``. Typically a
        meta-estimator.

    _method : str
        The name of the router's method in which this function is called.

        Metadata to be routed.

    Returns
    -------
    routed_params : Bunch
        A :class:`~utils.Bunch` of the form ``{"object_name": {"method_name":
        {params: value}}}`` which can be used to pass the required metadata to
        A :class:`~sklearn.utils.Bunch` of the form ``{"object_name": {"method_name":
        corresponding methods or corresponding child objects. The object names
        are those defined in `obj.get_metadata_routing()`.
    """
    if not kwargs:
        # If routing is not enabled and kwargs are empty, then we don't have to
        # try doing any routing, we can simply return a structure which returns
        # an empty dict on routed_params.ANYTHING.ANY_METHOD.
        class EmptyRequest:
            def get(self, name, default=None):
                return Bunch(**{method: dict() for method in METHODS})

            def __getitem__(self, name):

            def __getattr__(self, name):

        return EmptyRequest()

    if not (hasattr(_obj, "get_metadata_routing") or isinstance(_obj, MetadataRouter)):
            f"The given object ({_obj.__class__.__name__!r}) needs to either"
            " implement the routing method `get_metadata_routing` or be a"
            " `MetadataRouter` instance."
        )
    if _method not in METHODS:
            f"Can only route and process input on these methods: {METHODS}, "
            f"while the passed method is: {_method}."
        )

    request_routing = get_routing_for_object(_obj)
    request_routing.validate_metadata(params=kwargs, method=_method)
    routed_params = request_routing.route_params(params=kwargs, caller=_method)

    return routed_params

        )

        self,
        *,
        param,
        alias,
    ):

        param : str

        """
            )

            else:
                    " exist."
                )
        else:

        ones.

        Returns
        -------
        """
        )

        """
            prop
        }
            warn(
            )

        Returns
        -------
        """
                continue
            else:
                [
                ]
            )
            message = (
            )
            )
        return res

        Returns
        -------
        """
        res = set()
        return res

        Returns
        -------
        obj : dict
        """

            setattr(
                self,
                method,
            )

        Returns
        -------
        """

            )

                )

            method.

        ones.

        Returns
        -------
        """

            routed.

        Returns
        -------
        """
        )

        """

        Returns
        -------
        obj : dict
        """

        Returns
        -------
        """
            )
            )

        Returns
        -------
        obj : list
        """

        Returns
        -------
        """
        else:
                " API."
            )

        Returns
        -------
        """

            )

        Returns
        -------
        """
        res = set()

                    )

        return res

            method.

        ones.

        Returns
        -------
        """
        res = set()
                )
            )

                        )
                    )
        return res

            routed.

        Returns
        -------
        """
                )
            )

        )
        }
                )

        return res

        Returns
        -------
        """

                    )
        return res

        """
        )
            )
        else:
            )

        Returns
        -------
        obj : dict
        """

        return res

            yield (
            )

            """
                )

                )

            else:

            if args:
                )

        params = [
            )
        ]
            [
                    k,
                )
            ]
        )
            params,
        )

        """
        try:
            return

                continue
            setattr(
                cls,
            )

        Returns
        -------
        """
            return mmr
                continue
                continue
            )
        return mmr

        """

            setattr(
                requests,
                method,
            )

                    continue

        Returns
        -------
        """
        else:

        Returns
        -------
        """

            """
                )

                )

            else:

            if args:
                )

class _IDCounter:
    """Generate sequential ids with a prefix."""

    def __init__(self, prefix):
        self.prefix = prefix

    def get_id(self):
        return f"{self.prefix}-{self.count}"

def _get_css_style():
    return Path(__file__).with_suffix(".css").read_text(encoding="utf-8")

class _VisualBlock:
    """HTML Representation of Estimator

    kind : {'serial', 'parallel', 'single'}
        kind of HTML block

    estimators : list of estimators or `_VisualBlock`s or a single estimator
        If kind != 'single', then `estimators` is a list of
        If kind == 'single', then `estimators` is a single estimator.

    names : list of str, default=None
        If kind != 'single', then `names` corresponds to estimators.
        If kind == 'single', then `names` is a single string corresponding to
        the single estimator.

    name_details : list of str, str, or None, default=None
        If kind != 'single', then `name_details` corresponds to `names`.
        If kind == 'single', then `name_details` is a single string
        corresponding to the single estimator.

    name_caption : str, default=None
        The caption below the name. `None` stands for no caption.
        Only active when kind == 'single'.

    doc_link_label : str, default=None
        The label for the documentation link. If provided, the label would be
        "Documentation for {doc_link_label}". Otherwise it will look for `names`.

    dash_wrapped : bool, default=True
        If true, wrapped HTML element will be wrapped with a dashed border.
        Only active when kind != 'single'.
    """

        self,
        kind,
        *,
        names=None,
        name_details=None,
        name_caption=None,
        doc_link_label=None,
        dash_wrapped=True,
    ):
        self.dash_wrapped = dash_wrapped
        self.name_caption = name_caption
        self.doc_link_label = doc_link_label

        if self.kind in ("parallel", "serial"):
            if names is None:
                names = (None,) * len(estimators)
            if name_details is None:
                name_details = (None,) * len(estimators)

        self.names = names
        self.name_details = name_details

def _write_label_html(
    out,
    name,
    name_details,
    name_caption=None,
    doc_link_label=None,
    outer_class="sk-label-container",
    inner_class="sk-label",
    checked=False,
    doc_link="",
    is_fitted_css_class="",
    is_fitted_icon="",
):
    """Write labeled html with or without a dropdown with named details.

    out : file-like object
        The file to write the HTML representation to.
    name : str
        The label for the estimator. It corresponds either to the estimator class name
        for a simple estimator or in the case of a `Pipeline` and `ColumnTransformer`,
        it corresponds to the name of the step.
    name_details : str
        The details to show as content in the dropdown part of the toggleable label. It
        can contain information such as non-default parameters or column information for
        `ColumnTransformer`.
        The caption below the name. If `None`, no caption will be created.
        "Documentation for {doc_link_label}". Otherwise it will look for `name`.
    outer_class : {"sk-label-container", "sk-item"}, default="sk-label-container"
        The CSS class for the outer container.
    inner_class : {"sk-label", "sk-estimator"}, default="sk-label"
        The CSS class for the inner container.
    checked : bool, default=False
        Whether the dropdown is folded or not. With a single estimator, we intend to
        unfold the content.
    doc_link : str, default=""
        The link to the documentation for the estimator. If an empty string, no link is
        added to the diagram. This can be generated for an estimator if it uses the
        `_HTMLDocumentationLinkMixin`.
    is_fitted_css_class : {"", "fitted"}
        The CSS class to indicate whether or not the estimator is fitted. The
        empty string means that the estimator is not fitted and "fitted" means that the
        estimator is fitted.
    is_fitted_icon : str, default=""
        The HTML representation to show the fitted information in the diagram. An empty
        string means that no information is shown.
    """
    out.write(
        f'<div class="{outer_class}"><div'
        f' class="{inner_class} {is_fitted_css_class} sk-toggleable">'
    )
    name = html.escape(name)

    if name_details is not None:
        name_details = html.escape(str(name_details))
        checked_str = "checked" if checked else ""
        est_id = _ESTIMATOR_ID_COUNTER.get_id()

        if doc_link:
            doc_label = "<span>Online documentation</span>"
            if doc_link_label is not None:
                doc_label = f"<span>Documentation for {doc_link_label}</span>"
            elif name is not None:
                doc_label = f"<span>Documentation for {name}</span>"
            doc_link = (
                f'<a class="sk-estimator-doc-link {is_fitted_css_class}"'
                f' rel="noreferrer" target="_blank" href="{doc_link}">?{doc_label}</a>'
            )

        name_caption_div = (
            ""
            if name_caption is None
            else f'<div class="caption">{html.escape(name_caption)}</div>'
        )
        name_caption_div = f"<div><div>{name}</div>{name_caption_div}</div>"
        links_div = (
            f"<div>{doc_link}{is_fitted_icon}</div>"
            if doc_link or is_fitted_icon
            else ""
        )

        label_html = (
            f'<label for="{est_id}" class="sk-toggleable__label {is_fitted_css_class} '
            f'sk-toggleable__label-arrow">{name_caption_div}{links_div}</label>'
        )

        fmt_str = (
            f'<input class="sk-toggleable__control sk-hidden--visually" id="{est_id}" '
            f'type="checkbox" {checked_str}>{label_html}<div '
            f'class="sk-toggleable__content {is_fitted_css_class}"><pre>{name_details}'
            "</pre></div> "
        )
        out.write(fmt_str)
    else:
        out.write(f"<label>{name}</label>")
    out.write("</div></div>")  # outer_class inner_class

def _get_visual_block(estimator):
    """Generate information about how to display an estimator."""
    if hasattr(estimator, "_sk_visual_block_"):
        try:
            return estimator._sk_visual_block_()
            return _VisualBlock(
                "single",
                names=estimator.__class__.__name__,
                name_details=str(estimator),
            )

    if isinstance(estimator, str):
            "single", estimator, names=estimator, name_details=estimator
        )
    elif estimator is None:
        return _VisualBlock("single", estimator, names="None", name_details="None")

    # check if estimator looks like a meta estimator (wraps estimators)
    if hasattr(estimator, "get_params") and not isclass(estimator):
        estimators = [
            (key, est)
            for key, est in estimator.get_params(deep=False).items()
            if hasattr(est, "get_params") and hasattr(est, "fit") and not isclass(est)
        ]
        if estimators:
                "parallel",
                [est for _, est in estimators],
                names=[f"{key}: {est.__class__.__name__}" for key, est in estimators],
                name_details=[str(est) for _, est in estimators],
            )

    return _VisualBlock(
        "single",
        names=estimator.__class__.__name__,
        name_details=str(estimator),
    )

def _write_estimator_html(
    out,
    estimator_label,
    estimator_label_details,
    is_fitted_css_class,
    first_call=False,
):
    """Write estimator to html in serial, parallel, or by itself (single).

    For multiple estimators, this function is called recursively.

        The estimator to visualize.
    estimator_label : str
        for simple estimator or in the case of `Pipeline` and `ColumnTransformer`, it
        corresponds to the name of the step.
    estimator_label_details : str
        The details to show as content in the dropdown part of the toggleable label.
        It can contain information as non-default parameters or column information for
        The CSS class to indicate whether or not the estimator is fitted or not. The
        string means that no information is shown. If the estimator to be shown is not
        the first estimator (i.e. `first_call=False`), `is_fitted_icon` is always an
        empty string.
    first_call : bool, default=False
        Whether this is the first time this function is called.
    """
    if first_call:
        est_block = _get_visual_block(estimator)
    else:
        is_fitted_icon = ""
        with config_context(print_changed_only=True):
            est_block = _get_visual_block(estimator)
    # `estimator` can also be an instance of `_VisualBlock`
    if hasattr(estimator, "_get_doc_link"):
        doc_link = estimator._get_doc_link()
    else:
        doc_link = ""
    if est_block.kind in ("serial", "parallel"):
        dashed_wrapped = first_call or est_block.dash_wrapped
        dash_cls = " sk-dashed-wrapped" if dashed_wrapped else ""
        out.write(f'<div class="sk-item{dash_cls}">')

        if estimator_label:
            _write_label_html(
                out,
                estimator_label,
                estimator_label_details,
                doc_link=doc_link,
                is_fitted_css_class=is_fitted_css_class,
                is_fitted_icon=is_fitted_icon,
            )

        kind = est_block.kind
        out.write(f'<div class="sk-{kind}">')
        est_infos = zip(est_block.estimators, est_block.names, est_block.name_details)

        for est, name, name_details in est_infos:
            if kind == "serial":
                _write_estimator_html(
                    out,
                    est,
                    name,
                    name_details,
                    is_fitted_css_class=is_fitted_css_class,
                )
            else:  # parallel
                out.write('<div class="sk-parallel-item">')
                # wrap element in a serial visualblock
                serial_block = _VisualBlock("serial", [est], dash_wrapped=False)
                    out,
                    serial_block,
                    name,
                )
                out.write("</div>")  # sk-parallel-item

        out.write("</div></div>")
    elif est_block.kind == "single":
        _write_label_html(
            out,
            est_block.names,
            est_block.name_details,
            est_block.name_caption,
            est_block.doc_link_label,
            outer_class="sk-item",
            inner_class="sk-estimator",
            checked=first_call,
            doc_link=doc_link,
            is_fitted_css_class=is_fitted_css_class,
            is_fitted_icon=is_fitted_icon,
        )

def estimator_html_repr(estimator):
    """Build a HTML representation of an estimator.

    Read more in the :ref:`User Guide <visualizing_composite_estimators>`.

    Returns
    -------
    html: str
        HTML representation of estimator.

    Examples
    --------
    >>> from sklearn.utils._estimator_html_repr import estimator_html_repr
    >>> estimator_html_repr(LogisticRegression())
    '<style>...</div>'
    """
    from sklearn.exceptions import NotFittedError
    from sklearn.utils.validation import check_is_fitted

    if not hasattr(estimator, "fit"):
        status_label = "<span>Not fitted</span>"
        is_fitted_css_class = ""
    else:
        try:
            status_label = "<span>Fitted</span>"
            is_fitted_css_class = "fitted"
            status_label = "<span>Not fitted</span>"
            is_fitted_css_class = ""

    is_fitted_icon = (
        f'<span class="sk-estimator-doc-link {is_fitted_css_class}">'
        f"i{status_label}</span>"
    )
    with closing(StringIO()) as out:
        container_id = _CONTAINER_ID_COUNTER.get_id()
        style_template = Template(_CSS_STYLE)
        style_with_id = style_template.substitute(id=container_id)
        estimator_str = str(estimator)

        # The fallback message is shown by default and loading the CSS sets
        # div.sk-text-repr-fallback to display: none to hide the fallback message.
        #
        # If the notebook is trusted, the CSS is loaded which hides the fallback
        # message. If the notebook is not trusted, then the CSS is not loaded and the
        # fallback message is shown by default.
        #
        # The reverse logic applies to HTML repr div.sk-container.
        # div.sk-container is hidden by default and the loading the CSS displays it.
        fallback_msg = (
            "In a Jupyter environment, please rerun this cell to show the HTML"
            " representation or trust the notebook. <br />On GitHub, the"
            " HTML representation is unable to render, please try loading this page"
            " with nbviewer.org."
        )
        html_template = (
            f"<style>{style_with_id}</style>"
            f'<div id="{container_id}" class="sk-top-container">'
            '<div class="sk-text-repr-fallback">'
            f"<pre>{html.escape(estimator_str)}</pre><b>{fallback_msg}</b>"
            "</div>"
            '<div class="sk-container" hidden>'
        )

        out.write(html_template)

        _write_estimator_html(
            out,
            estimator.__class__.__name__,
            estimator_str,
            first_call=True,
        )

        html_output = out.getvalue()
        return html_output

class _HTMLDocumentationLinkMixin:
    """Mixin class allowing to generate a link to the API documentation.

    This mixin relies on three attributes:
    - `_doc_link_module`: it corresponds to the root module (e.g. `sklearn`). Using this
      mixin, the default value is `sklearn`.
    - `_doc_link_template`: it corresponds to the template used to generate the
      link to the API documentation. Using this mixin, the default value is
      `"https://scikit-learn.org/{version_url}/modules/generated/
      {estimator_module}.{estimator_name}.html"`.
    - `_doc_link_url_param_generator`: it corresponds to a function that generates the
      parameters to be used in the template when the estimator module and name are not
      sufficient.

    The method :meth:`_get_doc_link` generates the link to the API documentation for a
    given estimator.

    This useful provides all the necessary states for
    :func:`sklearn.utils.estimator_html_repr` to generate a link to the API
    documentation for the estimator HTML diagram.

    Examples
    --------
    If the default values for `_doc_link_module`, `_doc_link_template` are not suitable,
    then you can override them and provide a method to generate the URL parameters:
    >>> doc_link_template = "https://address.local/{single_param}.html"
    >>> def url_param_generator(estimator):
    ...     return {"single_param": estimator.__class__.__name__}
    ...     # use "builtins" since it is the associated module when declaring
    ...     # the class in a docstring
    ...     _doc_link_module = "builtins"
    ...     _doc_link_template = doc_link_template
    ...     _doc_link_url_param_generator = url_param_generator
    >>> estimator._get_doc_link()
    'https://address.local/MyEstimator.html'

    If instead of overriding the attributes inside the class definition, you want to
    override a class instance, you can use `types.MethodType` to bind the method to the
    instance:
    >>> import types
    >>> estimator = BaseEstimator()
    >>> estimator._doc_link_template = doc_link_template
    >>> estimator._doc_link_url_param_generator = types.MethodType(
    ...     url_param_generator, estimator)
    'https://address.local/BaseEstimator.html'
    """

    _doc_link_module = "sklearn"
    _doc_link_url_param_generator = None

    @property
    def _doc_link_template(self):
        sklearn_version = parse_version(__version__)
        if sklearn_version.dev is None:
            version_url = f"{sklearn_version.major}.{sklearn_version.minor}"
        else:
            version_url = "dev"
        return getattr(
            self,
            "__doc_link_template",
            (
                f"https://scikit-learn.org/{version_url}/modules/generated/"
                "{estimator_module}.{estimator_name}.html"
            ),
        )

    @_doc_link_template.setter
    def _doc_link_template(self, value):
        setattr(self, "__doc_link_template", value)

    def _get_doc_link(self):
        """Generates a link to the API documentation for a given estimator.

        This method generates the link to the estimator's documentation page
        by using the template defined by the attribute `_doc_link_template`.

        Returns
        -------
        url : str
            The URL to the API documentation for this estimator. If the estimator does
            not belong to module `_doc_link_module`, the empty string (i.e. `""`) is
            returned.
        """
        if self.__class__.__module__.split(".")[0] != self._doc_link_module:
            return ""

        if self._doc_link_url_param_generator is None:
            estimator_name = self.__class__.__name__
            # Construct the estimator's module name, up to the first private submodule.
            # This works because in scikit-learn all public estimators are exposed at
            # that level, even if they actually live in a private sub-module.
            estimator_module = ".".join(
                itertools.takewhile(
                    lambda part: not part.startswith("_"),
                    self.__class__.__module__.split("."),
                )
            )
            return self._doc_link_template.format(
                estimator_module=estimator_module, estimator_name=estimator_name
            )
        return self._doc_link_template.format(**self._doc_link_url_param_generator())

        self,
        kind,
        *,
    ):

        else:
            self,
            (
            ),
        )

        Returns
        -------
        url : str
            returned.
        """
            return ""

                )
            )
            )

class InvalidParameterError(ValueError, TypeError):
    """Custom exception to be raised when the parameter of a class/method/function
    does not have a valid type or value.
    """

def validate_parameter_constraints(parameter_constraints, params, caller_name):
    """Validate types and values of given parameters.

    parameter_constraints : dict or {"no_validation"}
        If "no_validation", validation is skipped for this parameter.

        If a dict, it must be a dictionary `param_name: list of constraints`.
        A parameter is valid if it satisfies one of the constraints from the list.
        Constraints can be:
        - an Interval object, representing a continuous or discrete range of numbers
        - the string "array-like"
        - the string "sparse matrix"
        - the string "random_state"
        - callable
        - None, meaning that None is a valid value for the parameter
        - any type, meaning that any instance of this type is valid
        - an Options object, representing a set of elements of a given type
        - a StrOptions object, representing a set of strings
        - the string "boolean"
        - the string "verbose"
        - the string "cv_object"
        - the string "nan"
        - a MissingValues object representing markers for missing values
        - a HasMethods object, representing method(s) an object must have
        - a Hidden object, representing a constraint not meant to be exposed to the user

        A dictionary `param_name: param_value`. The parameters to validate against the
        constraints.

    caller_name : str
        The name of the estimator or function or method that called this function.
    """
    for param_name, param_val in params.items():
        # We allow parameters to not have a constraint so that third party estimators
        # can inherit from sklearn estimators without having to necessarily use the
        # validation tools.
        if param_name not in parameter_constraints:
            continue

            continue

            if constraint.is_satisfied_by(param_val):
                # this constraint is satisfied, no need to check further.
                break
        else:
            # No constraint is satisfied, raise with an informative message.

            # Ignore constraints that we don't want to expose in the error message,
            # i.e. options that are for internal purpose or not officially supported.
            constraints = [
                constraint for constraint in constraints if not constraint.hidden
            ]

            if len(constraints) == 1:
                constraints_str = f"{constraints[0]}"
            else:
                constraints_str = (
                    f"{', '.join([str(c) for c in constraints[:-1]])} or"
                    f" {constraints[-1]}"
                )

            raise InvalidParameterError(
                f"The {param_name!r} parameter of {caller_name} must be"
                f" {constraints_str}. Got {param_val!r} instead."
            )

def make_constraint(constraint):
    """Convert the constraint into the appropriate Constraint object.

    constraint : object
        The constraint to convert.

    Returns
    -------
    constraint : instance of _Constraint
        The converted constraint.
    """
    if isinstance(constraint, str) and constraint == "array-like":
        return _ArrayLikes()
    if isinstance(constraint, str) and constraint == "sparse matrix":
        return _SparseMatrices()
    if isinstance(constraint, str) and constraint == "random_state":
        return _RandomStates()
    if constraint is callable:
        return _Callables()
    if constraint is None:
        return _NoneConstraint()
    if isinstance(constraint, type):
        return _InstancesOf(constraint)
        constraint, (Interval, StrOptions, Options, HasMethods, MissingValues)
    ):
        return constraint
    if isinstance(constraint, str) and constraint == "boolean":
        return _Booleans()
    if isinstance(constraint, str) and constraint == "verbose":
        return _VerboseHelper()
    if isinstance(constraint, str) and constraint == "cv_object":
        return _CVObjects()
    if isinstance(constraint, Hidden):
        constraint = make_constraint(constraint.constraint)
        constraint.hidden = True
    if (isinstance(constraint, str) and constraint == "nan") or (
        isinstance(constraint, float) and np.isnan(constraint)
    ):
        return _NanConstraint()
    raise ValueError(f"Unknown constraint type: {constraint}")

def validate_params(parameter_constraints, *, prefer_skip_nested_validation):
    """Decorator to validate types and values of functions and methods.

    parameter_constraints : dict
        A dictionary `param_name: list of constraints`. See the docstring of
        `validate_parameter_constraints` for a description of the accepted constraints.

        Note that the *args and **kwargs parameters are not validated and must not be
        present in the parameter_constraints dictionary.

        called by the decorated function will be skipped.

        It should be set to True for most functions, except for those that receive
        non-validated objects as parameters or that are just wrappers around classes
        because they only perform a partial validation.

    Returns
    -------
    decorated_function : function or method
        The decorated function.
    """

    def decorator(func):
        # The dict of parameter constraints is set as an attribute of the function
        # to make it possible to dynamically introspect the constraints for
        # automatic testing.
        setattr(func, "_skl_parameter_constraints", parameter_constraints)

        @functools.wraps(func)
            if global_skip_validation:
                return func(*args, **kwargs)

            func_sig = signature(func)

            # Map *args/**kwargs to the function signature
            params = func_sig.bind(*args, **kwargs)
            params.apply_defaults()

            # ignore self/cls and positional/keyword markers
            to_ignore = [
                p.name
                for p in func_sig.parameters.values()
                if p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
            ]
            to_ignore += ["self", "cls"]
            params = {k: v for k, v in params.arguments.items() if k not in to_ignore}

            validate_parameter_constraints(
                parameter_constraints, params, caller_name=func.__qualname__
            )

            try:
                with config_context(
                    skip_parameter_validation=(
                        prefer_skip_nested_validation or global_skip_validation
                    )
                ):
                    return func(*args, **kwargs)
            except InvalidParameterError as e:
                # When the function is just a wrapper around an estimator, we allow
                # the function to delegate validation to the estimator, but we replace
                # the name of the estimator by the name of the function in the error
                # message to avoid confusion.
                msg = re.sub(
                    r"parameter of \w+ must be",
                    f"parameter of {func.__qualname__} must be",
                    str(e),
                )
                raise InvalidParameterError(msg) from e

class RealNotInt(Real):
    """A type that represents reals that are not instances of int.

    Behaves like float, but also works with values extracted from numpy arrays.
    isintance(1, RealNotInt) -> False
    isinstance(1.0, RealNotInt) -> True
    """

def _type_name(t):
    """Convert type into human readable string."""
    module = t.__module__
    qualname = t.__qualname__
    if module == "builtins":
        return qualname
    elif t == Real:
        return "float"
    elif t == Integral:
        return "int"
    return f"{module}.{qualname}"

class _Constraint(ABC):
    """Base class for the constraint objects."""

        self.hidden = False

    def is_satisfied_by(self, val):
        """Whether or not a value satisfies the constraint.

        val : object
            The value to check.

        Returns
        -------
        is_satisfied : bool
            Whether or not the constraint is satisfied by this value.
        """

        """A human readable representational string of the constraint."""

class _InstancesOf(_Constraint):
    """Constraint representing instances of a given type.

    type : type
        The valid type.
    """

    def __init__(self, type):
        self.type = type

        return isinstance(val, self.type)

        return f"an instance of {_type_name(self.type)!r}"

class _NoneConstraint(_Constraint):
    """Constraint representing the None singleton."""

        return val is None

        return "None"

class _NanConstraint(_Constraint):
    """Constraint representing the indicator `np.nan`."""

        return (
            not isinstance(val, Integral) and isinstance(val, Real) and math.isnan(val)
        )

        return "numpy.nan"

class _PandasNAConstraint(_Constraint):
    """Constraint representing the indicator `pd.NA`."""

        try:

            return isinstance(val, type(pd.NA)) and pd.isna(val)

        return "pandas.NA"

class Options(_Constraint):
    """Constraint representing a finite set of instances of a given type.

    type : type

    options : set
        The set of valid scalars.

    deprecated : set or None, default=None
        A subset of the `options` to mark as deprecated in the string
        representation of the constraint.
    """

    def __init__(self, type, options, *, deprecated=None):
        self.options = options
        self.deprecated = deprecated or set()

        if self.deprecated - self.options:
            raise ValueError("The deprecated options must be a subset of the options.")

        return isinstance(val, self.type) and val in self.options

    def _mark_if_deprecated(self, option):
        """Add a deprecated mark to an option if needed."""
        option_str = f"{option!r}"
        if option in self.deprecated:
            option_str = f"{option_str} (deprecated)"
        return option_str

        options_str = (
            f"{', '.join([self._mark_if_deprecated(o) for o in self.options])}"
        )
        return f"a {_type_name(self.type)} among {{{options_str}}}"

class StrOptions(Options):
    """Constraint representing a finite set of strings.

    options : set of str
        The set of valid strings.

    deprecated : set of str or None, default=None
    """

    def __init__(self, options, *, deprecated=None):
        super().__init__(type=str, options=options, deprecated=deprecated)

class Interval(_Constraint):
    """Constraint representing a typed interval.

    type : {numbers.Integral, numbers.Real, RealNotInt}
        The set of numbers in which to set the interval.

        If RealNotInt, only reals that don't have the integer type
        are allowed. For example 1.0 is allowed but 1 is not.

    left : float or int or None
        The left bound of the interval. None means left bound is -∞.

    right : float, int or None
        The right bound of the interval. None means right bound is +∞.

    closed : {"left", "right", "both", "neither"}
        Whether the interval is open or closed. Possible choices are:

        - `"left"`: the interval is closed on the left and open on the right.
          It is equivalent to the interval `[ left, right )`.
        - `"right"`: the interval is closed on the right and open on the left.
          It is equivalent to the interval `( left, right ]`.
        - `"both"`: the interval is closed.
          It is equivalent to the interval `[ left, right ]`.
        - `"neither"`: the interval is open.
          It is equivalent to the interval `( left, right )`.

    Notes
    -----
    Setting a bound to `None` and setting the interval closed is valid. For instance,
    strictly speaking, `Interval(Real, 0, None, closed="both")` corresponds to
    `[0, +∞) U {+∞}`.
    """

    def __init__(self, type, left, right, *, closed):
        self.left = left
        self.right = right
        self.closed = closed

        if self.type not in (Integral, Real, RealNotInt):
                "type must be either numbers.Integral, numbers.Real or RealNotInt."
                f" Got {self.type} instead."
            )

        if self.closed not in ("left", "right", "both", "neither"):
                "closed must be either 'left', 'right', 'both' or 'neither'. "
                f"Got {self.closed} instead."
            )

        if self.type is Integral:
            suffix = "for an interval over the integers."
            if self.left is not None and not isinstance(self.left, Integral):
                raise TypeError(f"Expecting left to be an int {suffix}")
            if self.right is not None and not isinstance(self.right, Integral):
                raise TypeError(f"Expecting right to be an int {suffix}")
            if self.left is None and self.closed in ("left", "both"):
                    f"left can't be None when closed == {self.closed} {suffix}"
                )
            if self.right is None and self.closed in ("right", "both"):
                    f"right can't be None when closed == {self.closed} {suffix}"
                )
        else:
            if self.left is not None and not isinstance(self.left, Real):
                raise TypeError("Expecting left to be a real number.")
            if self.right is not None and not isinstance(self.right, Real):
                raise TypeError("Expecting right to be a real number.")

        if self.right is not None and self.left is not None and self.right <= self.left:
                f"right can't be less than left. Got left={self.left} and "
                f"right={self.right}"
            )

    def __contains__(self, val):
        if not isinstance(val, Integral) and np.isnan(val):

        left_cmp = operator.lt if self.closed in ("left", "both") else operator.le
        right_cmp = operator.gt if self.closed in ("right", "both") else operator.ge

        left = -np.inf if self.left is None else self.left
        right = np.inf if self.right is None else self.right

        if left_cmp(val, left):
        if right_cmp(val, right):

        if not isinstance(val, self.type):

        return val in self

        type_str = "an int" if self.type is Integral else "a float"
        left_bracket = "[" if self.closed in ("left", "both") else "("
        left_bound = "-inf" if self.left is None else self.left
        right_bound = "inf" if self.right is None else self.right
        right_bracket = "]" if self.closed in ("right", "both") else ")"

        # better repr if the bounds were given as integers
        if not self.type == Integral and isinstance(self.left, Real):
            left_bound = float(left_bound)
        if not self.type == Integral and isinstance(self.right, Real):
            right_bound = float(right_bound)

        return (
            f"{type_str} in the range "
            f"{left_bracket}{left_bound}, {right_bound}{right_bracket}"
        )

class _ArrayLikes(_Constraint):
    """Constraint representing array-likes"""

        return _is_arraylike_not_scalar(val)

        return "an array-like"

class _SparseMatrices(_Constraint):
    """Constraint representing sparse matrices."""

        return issparse(val)

        return "a sparse matrix"

class _Callables(_Constraint):
    """Constraint representing callables."""

        return callable(val)

        return "a callable"

class _RandomStates(_Constraint):
    """Constraint representing random states.

    Convenience class for
    [Interval(Integral, 0, 2**32 - 1, closed="both"), np.random.RandomState, None]
    """

        self._constraints = [
            Interval(Integral, 0, 2**32 - 1, closed="both"),
            _InstancesOf(np.random.RandomState),
            _NoneConstraint(),
        ]

        return any(c.is_satisfied_by(val) for c in self._constraints)

        return (
            f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
            f" {self._constraints[-1]}"
        )

class _Booleans(_Constraint):
    """Constraint representing boolean likes.

    [bool, np.bool_]
    """

            _InstancesOf(bool),
            _InstancesOf(np.bool_),
        ]

        return (
        )

class _VerboseHelper(_Constraint):
    """Helper constraint for the verbose parameter.

    [Interval(Integral, 0, None, closed="left"), bool, numpy.bool_]
    """

        ]

        return (
        )

class MissingValues(_Constraint):
    """Helper constraint for the `missing_values` parameters.

    Convenience for
    [
        Integral,
        Interval(Real, None, None, closed="both"),
        str,   # when numeric_only is False
        None,  # when numeric_only is False
        _NanConstraint(),
        _PandasNAConstraint(),
    ]

    numeric_only : bool, default=False
        Whether to consider only numeric missing value markers.

    """

    def __init__(self, numeric_only=False):

        self.numeric_only = numeric_only

            _InstancesOf(Integral),
            # we use an interval of Real to ignore np.nan that has its own constraint
            Interval(Real, None, None, closed="both"),
            _NanConstraint(),
            _PandasNAConstraint(),
        ]
        if not self.numeric_only:
            self._constraints.extend([_InstancesOf(str), _NoneConstraint()])

        return (
        )

class HasMethods(_Constraint):
    """Constraint representing objects that expose specific methods.

    It is useful for parameters following a protocol and where we don't want to impose
    an affiliation to a specific module or class.

    methods : str or list of str
        The method(s) that the object is expected to expose.
    """

    @validate_params(
        {"methods": [str, list]},
        prefer_skip_nested_validation=True,
    )
    def __init__(self, methods):
        if isinstance(methods, str):
            methods = [methods]
        self.methods = methods

        return all(callable(getattr(val, method, None)) for method in self.methods)

        if len(self.methods) == 1:
            methods = f"{self.methods[0]!r}"
        else:
            methods = (
                f"{', '.join([repr(m) for m in self.methods[:-1]])} and"
                f" {self.methods[-1]!r}"
            )
        return f"an object implementing {methods}"

class _IterablesNotString(_Constraint):
    """Constraint representing iterables that are not strings."""

        return isinstance(val, Iterable) and not isinstance(val, str)

        return "an iterable"

class _CVObjects(_Constraint):
    """Constraint representing cv objects.

    Convenient class for
    [
        Interval(Integral, 2, None, closed="left"),
        HasMethods(["split", "get_n_splits"]),
        _IterablesNotString(),
        None,
    ]
    """

            HasMethods(["split", "get_n_splits"]),
            _IterablesNotString(),
        ]

        return (
        )

class Hidden:
    """Class encapsulating a constraint not meant to be exposed to the user.

    constraint : str or _Constraint instance
        The constraint to be used internally.
    """

    def __init__(self, constraint):
        self.constraint = constraint

def generate_invalid_param_val(constraint):
    """Return a value that does not satisfy the constraint.

    Raises a NotImplementedError if there exists no invalid value for this constraint.

    This is only useful for testing purpose.

    constraint : _Constraint instance
        The constraint to generate a value for.

    Returns
    -------
    val : object
        A value that does not satisfy the constraint.
    """
    if isinstance(constraint, StrOptions):
        return f"not {' or '.join(constraint.options)}"

    if isinstance(constraint, MissingValues):
        return np.array([1, 2, 3])

    if isinstance(constraint, _VerboseHelper):
        return -1

    if isinstance(constraint, HasMethods):
        return type("HasNotMethods", (), {})()

    if isinstance(constraint, _IterablesNotString):
        return "a string"

    if isinstance(constraint, _CVObjects):
        return "not a cv object"

    if isinstance(constraint, Interval) and constraint.type is Integral:
        if constraint.left is not None:
            return constraint.left - 1
        if constraint.right is not None:
            return constraint.right + 1

        # There's no integer outside (-inf, +inf)

    if isinstance(constraint, Interval) and constraint.type in (Real, RealNotInt):
            return constraint.left - 1e-6
            return constraint.right + 1e-6

        # bounds are -inf, +inf
        if constraint.closed in ("right", "neither"):
            return -np.inf
        if constraint.closed in ("left", "neither"):
            return np.inf

        # interval is [-inf, +inf]

    raise NotImplementedError

def generate_valid_param(constraint):
    """Return a value that does satisfy a constraint.

    constraint : Constraint instance

    Returns
    -------
        A value that does satisfy the constraint.
    """
    if isinstance(constraint, _ArrayLikes):

    if isinstance(constraint, _SparseMatrices):
        return csr_matrix([[0, 1], [1, 0]])

    if isinstance(constraint, _RandomStates):
        return np.random.RandomState(42)

    if isinstance(constraint, _Callables):
        return lambda x: x

    if isinstance(constraint, _NoneConstraint):

    if isinstance(constraint, _InstancesOf):
        if constraint.type is np.ndarray:
            # special case for ndarray since it can't be instantiated without arguments
            return np.array([1, 2, 3])

        if constraint.type in (Integral, Real):
            # special case for Integral and Real since they are abstract classes
            return 1

        return constraint.type()

    if isinstance(constraint, _Booleans):

        return 1

    if isinstance(constraint, MissingValues) and constraint.numeric_only:

    if isinstance(constraint, MissingValues) and not constraint.numeric_only:
        return "missing"

        return type(
            "ValidHasMethods", (), {m: lambda self: None for m in constraint.methods}
        )()

        return [1, 2, 3]

        return 5

    if isinstance(constraint, Options):  # includes StrOptions
        for option in constraint.options:
            return option

    if isinstance(constraint, Interval):
        interval = constraint
        if interval.left is None and interval.right is None:
            return 0
        elif interval.left is None:
            return interval.right - 1
        elif interval.right is None:
            return interval.left + 1
        else:
            if interval.type is Real:
                return (interval.left + interval.right) / 2
            else:
                return interval.left + 1

                p.name
            ]

            )

            try:
                    )
                ):
                    str(e),
                )

        Returns
        -------
        """

        return (
        )

        try:

        )

            )

            )

                )
                )
        else:

            )

        return (
        )

        ]

        return (
        )

        ]

        ]

        ]

        else:
            methods = (
            )

        ]

                p.name
            ]

            )

            try:
                    )
                ):
                    str(e),
                )

class ArraySlicingWrapper:
    """
    array
    """

    def __init__(self, array):
        self.array = array

    def __getitem__(self, aslice):
        return MockDataFrame(self.array[aslice])

class MockDataFrame:
    """
    array
    """

    # have shape and length but don't support indexing.

        self.values = array
        self.shape = array.shape
        self.ndim = array.ndim
        # ugly hack to make iloc work.
        self.iloc = ArraySlicingWrapper(array)

        return len(self.array)

    def __array__(self, dtype=None):
        # Pandas data frames also are array-like: we want to make sure that
        # input validation in cross-validation does not try to call that
        # method.
        return self.array

        return MockDataFrame(self.array == other.array)

    def __ne__(self, other):
        return not self == other

    def take(self, indices, axis=0):
        return MockDataFrame(self.array.take(indices, axis=axis))

class CheckingClassifier(ClassifierMixin, BaseEstimator):
    """Dummy classifier to test pipelining and meta-estimators.

    Checks some property of `X` and `y`in fit / predict.
    This allows testing whether pipelines / cross-validation or metaestimators
    changed the input.

    Can also be used to check if `fit_params` are passed correctly, and
    to force a certain score to be returned.

    check_y, check_X : callable, default=None
        The callable used to validate `X` and `y`. These callable should return
        a bool where `False` will trigger an `AssertionError`. If `None`, the
        data is not validated. Default is `None`.

    check_y_params, check_X_params : dict, default=None
        The optional parameters to pass to `check_X` and `check_y`. If `None`,
        then no parameters are passed in.

    methods_to_check : "all" or list of str, default="all"
        The methods in which the checks should be applied. By default,
        all checks will be done on all methods (`fit`, `predict`,
        `predict_proba`, `decision_function` and `score`).

    foo_param : int, default=0
        A `foo` param. When `foo > 1`, the output of :meth:`score` will be 1
        otherwise it is 0.

    expected_sample_weight : bool, default=False
        Whether to check if a valid `sample_weight` was passed to `fit`.

    expected_fit_params : list of str, default=None
        A list of the expected parameters given when calling `fit`.

    classes_ : int
        The classes seen during `fit`.

        The number of features seen during `fit`.

    Examples
    --------
    >>> from sklearn.utils._mocking import CheckingClassifier

    This helper allow to assert to specificities regarding `X` or `y`. In this
    case we expect `check_X` or `check_y` to return a boolean.

    >>> clf = CheckingClassifier(check_X=lambda x: x.shape == (150, 4))
    CheckingClassifier(...)

    We can also provide a check which might raise an error. In this case, we
    expect `check_X` to return `X` and `check_y` to return `y`.

    >>> from sklearn.utils import check_array
    >>> clf = CheckingClassifier(check_X=check_array)
    """

        self,
        *,
        check_y=None,
        check_y_params=None,
        check_X=None,
        check_X_params=None,
        methods_to_check="all",
        foo_param=0,
        expected_sample_weight=None,
        expected_fit_params=None,
    ):
        self.check_y = check_y
        self.check_y_params = check_y_params
        self.check_X = check_X
        self.check_X_params = check_X_params
        self.methods_to_check = methods_to_check
        self.foo_param = foo_param
        self.expected_sample_weight = expected_sample_weight
        self.expected_fit_params = expected_fit_params

    def _check_X_y(self, X, y=None, should_be_fitted=True):
        """Validate X and y and make extra check.

            The data set.
            `X` is checked only if `check_X` is not `None` (default is None).
        y : array-like of shape (n_samples), default=None
            The corresponding target, by default `None`.
            `y` is checked only if `check_y` is not `None` (default is None).
        should_be_fitted : bool, default=True
            Whether or not the classifier should be already fitted.
            By default True.

        Returns
        -------
        X, y
        """
        if should_be_fitted:
        if self.check_X is not None:
            params = {} if self.check_X_params is None else self.check_X_params
            checked_X = self.check_X(X, **params)
            if isinstance(checked_X, (bool, np.bool_)):
                assert checked_X
            else:
                X = checked_X
        if y is not None and self.check_y is not None:
            params = {} if self.check_y_params is None else self.check_y_params
            checked_y = self.check_y(y, **params)
            if isinstance(checked_y, (bool, np.bool_)):
                assert checked_y
            else:
                y = checked_y
        return X, y

        """Fit classifier.

        y : array-like of shape (n_samples, n_outputs) or (n_samples,), \
            Target relative to X for classification or regression;
            None for unsupervised learning.

            Parameters passed to the ``fit`` method of the estimator

        Returns
        -------
        self
        """
        assert _num_samples(X) == _num_samples(y)
        if self.methods_to_check == "all" or "fit" in self.methods_to_check:
            X, y = self._check_X_y(X, y, should_be_fitted=False)
        self.n_features_in_ = np.shape(X)[1]
        self.classes_ = np.unique(check_array(y, ensure_2d=False, allow_nd=True))
        if self.expected_fit_params:
            missing = set(self.expected_fit_params) - set(fit_params)
            if missing:
                raise AssertionError(
                    f"Expected fit parameter(s) {list(missing)} not seen."
                )
            for key, value in fit_params.items():
                if _num_samples(value) != _num_samples(X):
                    raise AssertionError(
                        f"Fit parameter {key} has length {_num_samples(value)}"
                        f"; expected {_num_samples(X)}."
                    )
        if self.expected_sample_weight:
                raise AssertionError("Expected sample_weight to be passed")
            _check_sample_weight(sample_weight, X)

        """Predict the first class seen in `classes_`.

        Returns
        -------
        preds : ndarray of shape (n_samples,)
            Predictions of the first class seen in `classes_`.
        """
        if self.methods_to_check == "all" or "predict" in self.methods_to_check:
            X, y = self._check_X_y(X)
        return rng.choice(self.classes_, size=_num_samples(X))

        """Predict probabilities for each class.

        Here, the dummy classifier will provide a probability of 1 for the
        first class of `classes_` and 0 otherwise.

        Returns
        -------
        proba : ndarray of shape (n_samples, n_classes)
            The probabilities for each sample and class.
        """
        if self.methods_to_check == "all" or "predict_proba" in self.methods_to_check:
        proba = rng.randn(_num_samples(X), len(self.classes_))
        proba = np.abs(proba, out=proba)
        proba /= np.sum(proba, axis=1)[:, np.newaxis]

        """Confidence score.

        Returns
        -------
        decision : ndarray of shape (n_samples,) if n_classes == 2\
                else (n_samples, n_classes)
            Confidence score.
        """
        if (
            self.methods_to_check == "all"
            or "decision_function" in self.methods_to_check
        ):
            # for binary classifier, the confidence score is related to
            # classes_[1] and therefore should be null.
            return rng.randn(_num_samples(X))
        else:
            return rng.randn(_num_samples(X), len(self.classes_))

    def score(self, X=None, Y=None):
        """Fake score.

        Y : array-like of shape (n_samples, n_output) or (n_samples,)

        Returns
        -------
            Either 0 or 1 depending of `foo_param` (i.e. `foo_param > 1 =>
            score=1` otherwise `score=0`).
        """
        if self.methods_to_check == "all" or "score" in self.methods_to_check:
            self._check_X_y(X, Y)
        if self.foo_param > 1:
            score = 1.0
        else:
            score = 0.0

        tags.target_tags.one_d_labels = True

class NoSampleWeightWrapper(BaseEstimator):
    """Wrap estimator which will not expose `sample_weight`.

    est : estimator, default=None
        The estimator to wrap.
    """

    def __init__(self, est=None):
        self.est = est

        return self.est.fit(X, y)

        return self.est.predict(X)

        return self.est.predict_proba(X)

def _check_response(method):
        return self.response_methods is not None and method in self.response_methods

class _MockEstimatorOnOffPrediction(BaseEstimator):
    """Estimator for which we can turn on/off the prediction methods.

    response_methods: list of \
            {"predict", "predict_proba", "decision_function"}, default=None
        List containing the response implemented by the estimator. When, the
        response is in the list, it will return the name of the response method
        when called. Otherwise, an `AttributeError` is raised. It allows to
        use `getattr` as any conventional estimator. By default, no response
        methods are mocked.
    """

    def __init__(self, response_methods=None):
        self.response_methods = response_methods

    @available_if(_check_response("predict"))
        return "predict"

    @available_if(_check_response("predict_proba"))
        return "predict_proba"

    @available_if(_check_response("decision_function"))
        return "decision_function"

        # method.

        self,
        *,
    ):

        Returns
        -------
        X, y
        """
            else:
            else:
        return X, y

        Returns
        -------
        self
        """
                )
                    )

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """
        if (
        ):
        else:

        Returns
        -------
        """
            score = 1.0
        else:
            score = 0.0

def _unique_multiclass(y, xp=None):
    xp, is_array_api_compliant = get_namespace(y, xp=xp)
    if hasattr(y, "__array__") or is_array_api_compliant:
        return cached_unique(xp.asarray(y), xp=xp)
    else:
        return set(y)

def _unique_indicator(y, xp=None):
    xp, _ = get_namespace(y, xp=xp)
    return xp.arange(
        check_array(y, input_name="y", accept_sparse=["csr", "csc", "coo"]).shape[1]
    )

def unique_labels(*ys):
    """Extract an ordered array of unique labels.

    We don't allow:
        - mix of multilabel and multiclass (single label) targets
        - mix of label indicator matrix and anything else,
          because there are no explicit labels)
        - mix of label indicator matrices of different sizes
        - mix of string and integer labels

    At the moment, we also don't allow "multiclass-multioutput" input type.

    *ys : array-likes
        Label values.

    Returns
    -------
    out : ndarray of shape (n_unique_labels,)
        An ordered array of unique labels.

    Examples
    --------
    >>> from sklearn.utils.multiclass import unique_labels
    >>> unique_labels([3, 5, 5, 5, 7, 7])
    array([3, 5, 7])
    >>> unique_labels([1, 2, 3, 4], [2, 2, 3, 4])
    array([1, 2, 3, 4])
    >>> unique_labels([1, 2, 10], [5, 11])
    array([ 1,  2,  5, 10, 11])
    """
    ys = attach_unique(*ys, return_tuple=True)
    xp, is_array_api_compliant = get_namespace(*ys)
    if len(ys) == 0:
        raise ValueError("No argument has been passed.")
    # Check that we don't mix label format

    ys_types = set(type_of_target(x) for x in ys)
    if ys_types == {"binary", "multiclass"}:
        ys_types = {"multiclass"}

    if len(ys_types) > 1:
        raise ValueError("Mix type of y not allowed, got types %s" % ys_types)

    label_type = ys_types.pop()

    # Check consistency for the indicator format
    if (
        label_type == "multilabel-indicator"
        and len(
            set(
                check_array(y, accept_sparse=["csr", "csc", "coo"]).shape[1] for y in ys
            )
        )
        > 1
    ):
            "Multi-label binary indicator input with different numbers of labels"
        )

    # Get the unique set of labels
    _unique_labels = _FN_UNIQUE_LABELS.get(label_type, None)
    if not _unique_labels:
        raise ValueError("Unknown label type: %s" % repr(ys))

        # array_api does not allow for mixed dtypes
        unique_ys = xp.concat([_unique_labels(y, xp=xp) for y in ys])
        return xp.unique_values(unique_ys)

    ys_labels = set(
        chain.from_iterable((i for i in _unique_labels(y, xp=xp)) for y in ys)
    )
    # Check that we don't mix string type with number type
    if len(set(isinstance(label, str) for label in ys_labels)) > 1:
        raise ValueError("Mix of label input types (string and number)")

    return xp.asarray(sorted(ys_labels))

def _is_integral_float(y):
    xp, is_array_api_compliant = get_namespace(y)
    return xp.isdtype(y.dtype, "real floating") and bool(
        xp.all(xp.astype((xp.astype(y, xp.int64)), y.dtype) == y)
    )

def is_multilabel(y):
    """Check if ``y`` is in a multilabel format.

    Returns
    -------
    out : bool
        Return ``True``, if ``y`` is in a multilabel format, else ``False``.

    Examples
    --------
    >>> from sklearn.utils.multiclass import is_multilabel
    >>> is_multilabel([0, 1, 0, 1])
    False
    >>> is_multilabel([[1], [0, 2], []])
    False
    >>> is_multilabel(np.array([[1, 0], [0, 0]]))
    True
    >>> is_multilabel(np.array([[1], [0], [0]]))
    False
    >>> is_multilabel(np.array([[1, 0, 0]]))
    True
    """
    if hasattr(y, "__array__") or isinstance(y, Sequence) or is_array_api_compliant:
        # DeprecationWarning will be replaced by ValueError, see NEP 34
        # https://numpy.org/neps/nep-0034-infer-dtype-is-object.html
        check_y_kwargs = dict(
            ensure_min_samples=0,
            ensure_min_features=0,
        )
            warnings.simplefilter("error", VisibleDeprecationWarning)
            try:
                y = check_array(y, dtype=None, **check_y_kwargs)
            except (VisibleDeprecationWarning, ValueError) as e:
                if str(e).startswith("Complex data not supported"):
                    raise

                # dtype=object should be provided explicitly for ragged arrays,
                # see NEP 34
                y = check_array(y, dtype=object, **check_y_kwargs)

    if not (hasattr(y, "shape") and y.ndim == 2 and y.shape[1] > 1):

    if issparse(y):
        if y.format in ("dok", "lil"):
            y = y.tocsr()
        labels = xp.unique_values(y.data)
        return len(y.data) == 0 or (
            (labels.size == 1 or ((labels.size == 2) and (0 in labels)))
            and (y.dtype.kind in "biu" or _is_integral_float(labels))  # bool, int, uint
        )
    else:
        labels = cached_unique(y, xp=xp)

        return labels.shape[0] < 3 and (
            xp.isdtype(y.dtype, ("bool", "signed integer", "unsigned integer"))
            or _is_integral_float(labels)
        )

def check_classification_targets(y):
    """Ensure that target y is of a non-regression type.

    Only the following target types (as defined in type_of_target) are allowed:
        'binary', 'multiclass', 'multiclass-multioutput',
        'multilabel-indicator', 'multilabel-sequences'

    y : array-like
    """
    y_type = type_of_target(y, input_name="y")
    if y_type not in [
        "binary",
        "multiclass",
        "multilabel-sequences",
    ]:
            f"Unknown label type: {y_type}. Maybe you are trying to fit a "
            "classifier, which expects discrete classes on a "
            "regression target with continuous values."
        )

def type_of_target(y, input_name="", raise_unknown=False):
    """Determine the type of data indicated by the target.

    Note that this type is the most specific type that can be inferred.
    For example:

    * ``binary`` is more specific but compatible with ``multiclass``.
    * ``multiclass`` of integers is more specific but compatible with ``continuous``.
    * ``multilabel-indicator`` is more specific but compatible with
      ``multiclass-multioutput``.

    y : {array-like, sparse matrix}
        Target values. If a sparse matrix, `y` is expected to be a
        CSR/CSC matrix.

    input_name : str, default=""
        The data name used to construct the error message.

        .. versionadded:: 1.1.0

    raise_unknown : bool, default=False
        If `True`, raise an error when the type of target returned by
        :func:`~sklearn.utils.multiclass.type_of_target` is `"unknown"`.

    Returns
    -------
    target_type : str
        One of:

        * 'continuous': `y` is an array-like of floats that are not all
          integers, and is 1d or a column vector.
        * 'continuous-multioutput': `y` is a 2d array of floats that are
          not all integers, and both dimensions are of size > 1.
        * 'binary': `y` contains <= 2 discrete values and is 1d or a column
          vector.
        * 'multiclass': `y` contains more than two discrete values, is not a
          sequence of sequences, and is 1d or a column vector.
        * 'multiclass-multioutput': `y` is a 2d array that contains more
          than two discrete values, is not a sequence of sequences, and both
          dimensions are of size > 1.
        * 'multilabel-indicator': `y` is a label indicator matrix, an array
          of two dimensions with at least two columns, and at most 2 unique
          values.
        * 'unknown': `y` is array-like but none of the above, such as a 3d
          array, sequence of sequences, or an array of non-sequence objects.

    Examples
    --------
    >>> from sklearn.utils.multiclass import type_of_target
    >>> type_of_target([0.1, 0.6])
    'continuous'
    >>> type_of_target([1, -1, -1, 1])
    'binary'
    >>> type_of_target(['a', 'b', 'a'])
    'binary'
    >>> type_of_target([1.0, 2.0])
    'binary'
    >>> type_of_target([1, 0, 2])
    'multiclass'
    >>> type_of_target([1.0, 0.0, 3.0])
    >>> type_of_target(['a', 'b', 'c'])
    >>> type_of_target(np.array([[1, 2], [3, 1]]))
    'multiclass-multioutput'
    >>> type_of_target([[1, 2]])
    'multilabel-indicator'
    >>> type_of_target(np.array([[1.5, 2.0], [3.0, 1.6]]))
    'continuous-multioutput'
    >>> type_of_target(np.array([[0, 1], [1, 1]]))
    """

    def _raise_or_return():
        """Depending on the value of raise_unknown, either raise an error or return
        'unknown'.
        """
        if raise_unknown:
            input = input_name if input_name else "data"
            raise ValueError(f"Unknown label type for {input}: {y!r}")
        else:
            return "unknown"

    valid = (
        (isinstance(y, Sequence) or issparse(y) or hasattr(y, "__array__"))
        and not isinstance(y, str)
    ) or is_array_api_compliant

    if not valid:
            "Expected array-like (array or non-string sequence), got %r" % y
        )

    sparse_pandas = y.__class__.__name__ in ["SparseSeries", "SparseArray"]
    if sparse_pandas:
        raise ValueError("y cannot be class 'SparseSeries' or 'SparseArray'")

    if is_multilabel(y):
        return "multilabel-indicator"

    # DeprecationWarning will be replaced by ValueError, see NEP 34
    # https://numpy.org/neps/nep-0034-infer-dtype-is-object.html
    # We therefore catch both deprecation (NumPy < 1.24) warning and
    # value error (NumPy >= 1.24).
    check_y_kwargs = dict(
        accept_sparse=True,
        allow_nd=True,
        ensure_all_finite=False,
        ensure_min_features=0,
    )

        warnings.simplefilter("error", VisibleDeprecationWarning)
        if not issparse(y):
            try:
                    raise

                # see NEP 34

    try:
        first_row_or_val = y[[0], :] if issparse(y) else y[0]
        # labels in bytes format
        if isinstance(first_row_or_val, bytes):
                "Support for labels represented as bytes is not supported. Convert "
                "the labels to a string or integer format."
            )
        # The old sequence of sequences format
        if (
            not hasattr(first_row_or_val, "__array__")
            and isinstance(first_row_or_val, Sequence)
            and not isinstance(first_row_or_val, str)
        ):
                "You appear to be using a legacy multi-label data"
                " representation. Sequence of sequences are no"
                " longer supported; use a binary array or sparse"
                " matrix instead - the MultiLabelBinarizer"
                " transformer can convert to this format."
            )
    except IndexError:
        pass

    # Invalid inputs
    if y.ndim not in (1, 2):
        # Number of dimension greater than 2: [[[1, 2]]]
        return _raise_or_return()
    if not min(y.shape):
        # Empty ndarray: []/[[]]
            # 1-D empty array: []
            return "binary"  # []
        # 2-D empty array: [[]]
    if not issparse(y) and y.dtype == object and not isinstance(y.flat[0], str):
        # [obj_1] and not ["label_1"]

    # Check if multioutput
    if y.ndim == 2 and y.shape[1] > 1:
        suffix = "-multioutput"  # [[1, 2], [1, 2]]
    else:
        suffix = ""  # [1, 2, 3] or [[1], [2], [3]]

    # Check float and contains non-integer float values
    if xp.isdtype(y.dtype, "real floating"):
        # [.1, .2, 3] or [[.1, .2, 3]] or [[1., .2]] and not [1., 2., 3.]
        data = y.data if issparse(y) else y
        if xp.any(data != xp.astype(data, int)):
            _assert_all_finite(data, input_name=input_name)
            return "continuous" + suffix

    # Check multiclass
    if issparse(first_row_or_val):
        first_row_or_val = first_row_or_val.data
    if cached_unique(y).shape[0] > 2 or (y.ndim == 2 and len(first_row_or_val) > 1):
        # [1, 2, 3] or [[1., 2., 3]] or [[1, 2]]
        return "multiclass" + suffix
    else:
        return "binary"  # [1, 2] or [["a"], ["b"]]

def _check_partial_fit_first_call(clf, classes=None):
    """Private helper function for factorizing common classes param logic.

    Estimators that implement the ``partial_fit`` API need to be provided with
    the list of possible classes at the first call to partial_fit.

    Subsequent calls to partial_fit should check that ``classes`` is still
    consistent with a previous value of ``clf.classes_`` when provided.

    This function returns True if it detects that this was the first call to
    ``partial_fit`` on ``clf``. In that case the ``classes_`` attribute is also
    set on ``clf``.

    """
    if getattr(clf, "classes_", None) is None and classes is None:
        raise ValueError("classes must be passed on the first call to partial_fit.")

    elif classes is not None:
        if getattr(clf, "classes_", None) is not None:
            if not np.array_equal(clf.classes_, unique_labels(classes)):
                    "`classes=%r` is not the same as on last call "
                    "to partial_fit, was: %r" % (classes, clf.classes_)
                )

        else:
            # This is the first call to partial_fit
            clf.classes_ = unique_labels(classes)

    # classes is None and clf.classes_ has already previously been set:
    # nothing to do

def class_distribution(y, sample_weight=None):
    """Compute class priors from multioutput-multiclass target data.

    y : {array-like, sparse matrix} of size (n_samples, n_outputs)
        The labels for each example.

    Returns
    -------
    classes : list of size n_outputs of ndarray of size (n_classes,)
        List of classes for each column.

    n_classes : list of int of size n_outputs
        Number of classes in each column.

    class_prior : list of size n_outputs of ndarray of size (n_classes,)
        Class distribution of each column.
    """
    classes = []
    n_classes = []
    class_prior = []

        y = y.tocsc()
        y_nnz = np.diff(y.indptr)

            col_nonzero = y.indices[y.indptr[k] : y.indptr[k + 1]]
            # separate sample weights for zero and non-zero elements
                nz_samp_weight = sample_weight[col_nonzero]
                zeros_samp_weight_sum = np.sum(sample_weight) - np.sum(nz_samp_weight)
            else:
                nz_samp_weight = None
                zeros_samp_weight_sum = y.shape[0] - y_nnz[k]

            classes_k, y_k = np.unique(
                y.data[y.indptr[k] : y.indptr[k + 1]], return_inverse=True
            )
            class_prior_k = np.bincount(y_k, weights=nz_samp_weight)

            # An explicit zero was found, combine its weight with the weight
            # of the implicit zeros
            if 0 in classes_k:
                class_prior_k[classes_k == 0] += zeros_samp_weight_sum

            # If an there is an implicit zero and it is not in classes and
            # class_prior, make an entry for it
            if 0 not in classes_k and y_nnz[k] < y.shape[0]:
                classes_k = np.insert(classes_k, 0, 0)
                class_prior_k = np.insert(class_prior_k, 0, zeros_samp_weight_sum)

            classes.append(classes_k)
            n_classes.append(classes_k.shape[0])
            class_prior.append(class_prior_k / class_prior_k.sum())
    else:
            classes_k, y_k = np.unique(y[:, k], return_inverse=True)
            class_prior_k = np.bincount(y_k, weights=sample_weight)

    return (classes, n_classes, class_prior)

def _ovr_decision_function(predictions, confidences, n_classes):
    """Compute a continuous, tie-breaking OvR decision function from OvO.

    It is important to include a continuous value, not only votes,
    to make computing AUC or calibration meaningful.

    predictions : array-like of shape (n_samples, n_classifiers)
        Predicted classes for each binary classifier.

    confidences : array-like of shape (n_samples, n_classifiers)
        Decision functions or predicted probabilities for positive class
        for each binary classifier.

    n_classes : int
        Number of classes. n_classifiers must be
        ``n_classes * (n_classes - 1 ) / 2``.
    """
    n_samples = predictions.shape[0]
    sum_of_confidences = np.zeros((n_samples, n_classes))

    k = 0
            sum_of_confidences[:, i] -= confidences[:, k]
            sum_of_confidences[:, j] += confidences[:, k]
            votes[predictions[:, k] == 0, i] += 1
            votes[predictions[:, k] == 1, j] += 1
            k += 1

    # Monotonically transform the sum_of_confidences to (-1/3, 1/3)
    # and add it with votes. The monotonic transformation  is
    # f: x -> x / (3 * (|x| + 1)), it uses 1/3 instead of 1/2
    # to ensure that we won't reach the limits and change vote order.
    # The motivation is to use confidence levels as a way to break ties in
    # the votes without switching any decision made based on a difference
    # of 1 vote.
    transformed_confidences = sum_of_confidences / (
        3 * (np.abs(sum_of_confidences) + 1)
    )
    return votes + transformed_confidences

        """
        else:

class _BinaryClassifierCurveDisplayMixin:
    """Mixin class to be used in Displays requiring a binary classifier.

    The aim of this class is to centralize some validations regarding the estimator and
    the target and gather the response of the estimator.
    """

    def _validate_plot_params(self, *, ax=None, name=None):

        name = self.estimator_name if name is None else name
        return ax, ax.figure, name

    def _validate_and_get_response_values(
        cls, estimator, X, y, *, response_method="auto", pos_label=None, name=None
    ):

        name = estimator.__class__.__name__ if name is None else name

        y_pred, pos_label = _get_response_values_binary(
            X,
        )

        return y_pred, pos_label, name

    def _validate_from_predictions_params(
        cls, y_true, y_pred, *, sample_weight=None, pos_label=None, name=None
    ):

        if type_of_target(y_true) != "binary":
                f"The target y is not binary. Got {type_of_target(y_true)} type of"
                " target."
            )

        check_consistent_length(y_true, y_pred, sample_weight)

        name = name if name is not None else "Classifier"

        return pos_label, name

def _validate_score_name(score_name, scoring, negate_score):
    """Validate the `score_name` parameter.

    If `score_name` is provided, we just return it as-is.
    If `score_name` is `None`, we use `Score` if `negate_score` is `False` and
    `Negative score` otherwise.
    If `score_name` is a string or a callable, we infer the name. We replace `_` by
    spaces and capitalize the first letter. We remove `neg_` and replace it by
    `"Negative"` if `negate_score` is `False` or just remove it otherwise.
    """
    if score_name is not None:
        return score_name
    elif scoring is None:
        return "Negative score" if negate_score else "Score"
    else:
        score_name = scoring.__name__ if callable(scoring) else scoring
        if negate_score:
            if score_name.startswith("neg_"):
                score_name = score_name[4:]
            else:
                score_name = f"Negative {score_name}"
        elif score_name.startswith("neg_"):
            score_name = f"Negative {score_name[4:]}"
        score_name = score_name.replace("_", " ")
        return score_name.capitalize()

def _interval_max_min_ratio(data):
    """Compute the ratio between the largest and smallest inter-point distances.

    A value larger than 5 typically indicates that the parameter range would
    better be displayed with a log scale while a linear scale would be more
    suitable otherwise.
    """
    diff = np.diff(np.sort(data))
    return diff.max() / diff.min()

def _validate_style_kwargs(default_style_kwargs, user_style_kwargs):
    """Create valid style kwargs by avoiding Matplotlib alias errors.

    Matplotlib raises an error when, for example, 'color' and 'c', or 'linestyle' and
    'ls', are specified together. To avoid this, we automatically keep only the one
    specified by the user and raise an error if the user specifies both.

    default_style_kwargs : dict
        The Matplotlib style kwargs used by default in the scikit-learn display.
    user_style_kwargs : dict
        The user-defined Matplotlib style kwargs.

    Returns
    -------
    valid_style_kwargs : dict
        The validated style kwargs taking into account both default and user-defined
        Matplotlib style kwargs.
    """

    invalid_to_valid_kw = {
        "ls": "linestyle",
        "c": "color",
        "ec": "edgecolor",
        "fc": "facecolor",
        "lw": "linewidth",
        "mec": "markeredgecolor",
        "mfcalt": "markerfacecoloralt",
        "ms": "markersize",
        "mew": "markeredgewidth",
        "mfc": "markerfacecolor",
        "aa": "antialiased",
        "ds": "drawstyle",
        "font": "fontproperties",
        "family": "fontfamily",
        "name": "fontname",
        "size": "fontsize",
        "stretch": "fontstretch",
        "style": "fontstyle",
        "variant": "fontvariant",
        "weight": "fontweight",
        "ha": "horizontalalignment",
        "va": "verticalalignment",
        "ma": "multialignment",
    }
    for invalid_key, valid_key in invalid_to_valid_kw.items():
        if invalid_key in user_style_kwargs and valid_key in user_style_kwargs:
                f"Got both {invalid_key} and {valid_key}, which are aliases of one "
                "another"
            )
    valid_style_kwargs = default_style_kwargs.copy()

    for key in user_style_kwargs.keys():
        if key in invalid_to_valid_kw:
            valid_style_kwargs[invalid_to_valid_kw[key]] = user_style_kwargs[key]
        else:
            valid_style_kwargs[key] = user_style_kwargs[key]

    return valid_style_kwargs

def _despine(ax):
    """Remove the top and right spines of the plot.

    ax : matplotlib.axes.Axes
        The axes of the plot to despine.
    """
        ax.spines[s].set_visible(False)
    for s in ["bottom", "left"]:
        ax.spines[s].set_bounds(0, 1)

    ):

            X,
        )

    ):

                " target."
            )

def compute_class_weight(class_weight, *, classes, y, sample_weight=None):
    """Estimate class weights for unbalanced datasets.

    class_weight : dict, "balanced" or None
        If "balanced", class weights will be given by
        `n_samples / (n_classes * np.bincount(y))` or their weighted equivalent if
        `sample_weight` is provided.
        If a dictionary is given, keys are classes and values are corresponding class
        weights.
        If `None` is given, the class weights will be uniform.

    classes : ndarray
        Array of the classes occurring in the data, as given by
        `np.unique(y_org)` with `y_org` the original class labels.

        Array of original class labels per sample.

        Array of weights that are assigned to individual samples. Only used when
        `class_weight='balanced'`.

    Returns
    -------
    class_weight_vect : ndarray of shape (n_classes,)
        Array with `class_weight_vect[i]` the weight for i-th class.

    The "balanced" heuristic is inspired by
    Logistic Regression in Rare Events Data, King, Zen, 2001.

    Examples
    --------
    >>> from sklearn.utils.class_weight import compute_class_weight
    >>> y = [1, 1, 1, 1, 0, 0]
    >>> compute_class_weight(class_weight="balanced", classes=np.unique(y), y=y)
    array([1.5 , 0.75])
    """
    # Import error caused by circular imports.
    from ..preprocessing import LabelEncoder

    if set(y) - set(classes):
        raise ValueError("classes should include all valid labels that can be in y")
    if class_weight is None or len(class_weight) == 0:
        # uniform class weights
        weight = np.ones(classes.shape[0], dtype=np.float64, order="C")
    elif class_weight == "balanced":
        # Find the weight of each class as present in y.
        if not all(np.isin(classes, le.classes_)):
            raise ValueError("classes should have valid labels that are in y")

        sample_weight = _check_sample_weight(sample_weight, y)
        weighted_class_counts = np.bincount(y_ind, weights=sample_weight)
        recip_freq = weighted_class_counts.sum() / (
            len(le.classes_) * weighted_class_counts
        )
        weight = recip_freq[le.transform(classes)]
    else:
        # user-defined dictionary
        unweighted_classes = []
        for i, c in enumerate(classes):
            if c in class_weight:
                weight[i] = class_weight[c]
            else:
                unweighted_classes.append(c)

        n_weighted_classes = len(classes) - len(unweighted_classes)
        if unweighted_classes and n_weighted_classes != len(class_weight):
            unweighted_classes_user_friendly_str = np.array(unweighted_classes).tolist()
                f"The classes, {unweighted_classes_user_friendly_str}, are not in"
                " class_weight"
            )

    return weight

def compute_sample_weight(class_weight, y, *, indices=None):
    """Estimate sample weights by class for unbalanced datasets.

    class_weight : dict, list of dicts, "balanced", or None

        `[{0: 1, 1: 1}, {0: 1, 1: 5}, {0: 1, 1: 1}, {0: 1, 1: 1}]` instead of
        `[{1:1}, {2:5}, {3:1}, {4:1}]`.

        The `"balanced"` mode uses the values of y to automatically adjust
        weights inversely proportional to class frequencies in the input data:
        `n_samples / (n_classes * np.bincount(y))`.

    y : {array-like, sparse matrix} of shape (n_samples,) or (n_samples, n_outputs)

    indices : array-like of shape (n_subsample,), default=None
        Array of indices to be used in a subsample. Can be of length less than
        `n_samples` in the case of a subsample, or equal to `n_samples` in the
        case of a bootstrap subsample with repeated indices. If `None`, the
        sample weight will be calculated over the full sample. Only `"balanced"`
        is supported for `class_weight` if this is provided.

    Returns
    -------
    sample_weight_vect : ndarray of shape (n_samples,)
        Array with sample weights as applied to the original `y`.

    Examples
    --------
    >>> from sklearn.utils.class_weight import compute_sample_weight
    >>> compute_sample_weight(class_weight="balanced", y=y)
    array([0.75, 0.75, 0.75, 0.75, 1.5 , 1.5 ])
    """

    # Ensure y is 2D. Sparse matrices are already 2D.
    if not sparse.issparse(y):

    if indices is not None and class_weight != "balanced":
            "The only valid class_weight for subsampling is 'balanced'. "
            f"Given {class_weight}."
        )
    elif n_outputs > 1:
        if class_weight is None or isinstance(class_weight, dict):
                "For multi-output, class_weight should be a list of dicts, or the "
                "string 'balanced'."
            )
        elif isinstance(class_weight, list) and len(class_weight) != n_outputs:
                "For multi-output, number of elements in class_weight should match "
                f"number of outputs. Got {len(class_weight)} element(s) while having "
                f"{n_outputs} outputs."
            )

    expanded_class_weight = []
        if sparse.issparse(y):
            # Ok to densify a single column at a time
            y_full = y[:, [k]].toarray().flatten()
        else:
            y_full = y[:, k]
        classes_full = np.unique(y_full)
        classes_missing = None

        if class_weight == "balanced" or n_outputs == 1:
            class_weight_k = class_weight
        else:
            class_weight_k = class_weight[k]

        if indices is not None:
            # Get class weights for the subsample, covering all classes in
            # case some labels that were present in the original data are
            # missing from the sample.
            y_subsample = y_full[indices]
            classes_subsample = np.unique(y_subsample)

            weight_k = np.take(
                compute_class_weight(
                    class_weight_k, classes=classes_subsample, y=y_subsample
                ),
                np.searchsorted(classes_subsample, classes_full),
                mode="clip",
            )

            classes_missing = set(classes_full) - set(classes_subsample)
        else:
            weight_k = compute_class_weight(
                class_weight_k, classes=classes_full, y=y_full
            )

        weight_k = weight_k[np.searchsorted(classes_full, y_full)]

        if classes_missing:
            # Make missing classes' weight zero
            weight_k[np.isin(y_full, list(classes_missing))] = 0.0

        expanded_class_weight.append(weight_k)

    expanded_class_weight = np.prod(expanded_class_weight, axis=0, dtype=np.float64)

    return expanded_class_weight

def _attach_unique(y):
    """Attach unique values of y to y and return the result.

    The result is a view of y, and the metadata (unique) is not attached to y.
    """
    if not isinstance(y, np.ndarray):
        return y
    try:
        # avoid recalculating unique in nested calls.
        if "unique" in y.dtype.metadata:
            return y
    except (AttributeError, TypeError):
        pass

    unique = np.unique(y)
    unique_dtype = np.dtype(y.dtype, metadata={"unique": unique})
    return y.view(dtype=unique_dtype)

def attach_unique(*ys, return_tuple=False):
    """Attach unique values of ys to ys and return the results.

    IMPORTANT: The output of this function should NEVER be returned in functions.
    This is to avoid this pattern:

        y = np.array([1, 2, 3])
        y = attach_unique(y)
        y[1] = -1
        # now np.unique(y) will be different from cached_unique(y)

    *ys : sequence of array-like
        Input data arrays.

    return_tuple : bool, default=False
        If True, always return a tuple even if there is only one array.

    Returns
    -------
    ys : tuple of array-like or array-like
        Input data with unique values attached.
    """
    res = tuple(_attach_unique(y) for y in ys)
    if len(res) == 1 and not return_tuple:
    return res

def _cached_unique(y, xp=None):
    """Return the unique values of y.

    Use the cached values from dtype.metadata if present.

    This function does NOT cache the values in y, i.e. it doesn't change y.

    Call `attach_unique` to attach the unique values to y.
    """
    try:
        if y.dtype.metadata is not None and "unique" in y.dtype.metadata:
            return y.dtype.metadata["unique"]
        # in case y is not a numpy array
        pass
    return xp.unique_values(y)

def cached_unique(*ys, xp=None):
    """Return the unique values of ys.

    Returns
    -------
    res : tuple of array-like or array-like
        Unique values of ys.
    """
    res = tuple(_cached_unique(y, xp=xp) for y in ys)
    if len(res) == 1:
    return res

def _init_arpack_v0(size, random_state):
    """Initialize the starting vector for iteration in ARPACK functions.

    Initialize a ndarray with values sampled from the uniform distribution on
    [-1, 1]. This initialization model has been chosen to be consistent with
    the ARPACK one as another initialization can lead to convergence issues.

    size : int
        The size of the eigenvalue vector to be initialized.

        The seed of the pseudo random number generator used to generate a
        uniform distribution. If int, random_state is the seed used by the
        random number generator; If RandomState instance, random_state is the
        random number generator; If None, the random number generator is the
        RandomState instance used by `np.random`.

    Returns
    -------
    v0 : ndarray of shape (size,)
        The initialized vector.
    """
    v0 = random_state.uniform(-1, 1, size)
    return v0

class _BaseComposition(BaseEstimator, metaclass=ABCMeta):
    """Handles parameter management for classifiers composed of named estimators."""

    steps: List[Any]

        pass

    def _get_params(self, attr, deep=True):
        out = super().get_params(deep=deep)
        if not deep:
            return out

        estimators = getattr(self, attr)
        try:
            out.update(estimators)
            # Ignore TypeError for cases where estimators is not a list of
            # (name, estimator) and ignore ValueError when the list is not
            # formatted correctly. This is to prevent errors when calling
            # `set_params`. `BaseEstimator.set_params` calls `get_params` which
            # can error for invalid values for `estimators`.
            return out

        for name, estimator in estimators:
            if hasattr(estimator, "get_params"):
                for key, value in estimator.get_params(deep=True).items():
                    out["%s__%s" % (name, key)] = value
        return out

    def _set_params(self, attr, **params):
        # Ensure strict ordering of parameter setting:
        # 1. All steps
        if attr in params:
            setattr(self, attr, params.pop(attr))
        # 2. Replace items with estimators in params
        items = getattr(self, attr)
        if isinstance(items, list) and items:
            # Get item names used to identify valid names in params
            # `zip` raises a TypeError when `items` does not contains
            # elements of length 2
            with suppress(TypeError):
                item_names, _ = zip(*items)
                for name in list(params.keys()):
                    if "__" not in name and name in item_names:
                        self._replace_estimator(attr, name, params.pop(name))

        # 3. Step parameters and other initialisation arguments
        super().set_params(**params)

    def _replace_estimator(self, attr, name, new_val):
        # assumes `name` is a valid estimator name
        new_estimators = list(getattr(self, attr))
        for i, (estimator_name, _) in enumerate(new_estimators):
            if estimator_name == name:
                new_estimators[i] = (name, new_val)
                break
        setattr(self, attr, new_estimators)

    def _validate_names(self, names):
        if len(set(names)) != len(names):
            raise ValueError("Names provided are not unique: {0!r}".format(list(names)))
        invalid_names = set(names).intersection(self.get_params(deep=False))
        if invalid_names:
                "Estimator names conflict with constructor arguments: {0!r}".format(
                    sorted(invalid_names)
                )
            )
        invalid_names = [name for name in names if "__" in name]
                "Estimator names must not contain __: got {0!r}".format(invalid_names)
            )

def _safe_split(estimator, X, y, indices, train_indices=None):
    """Create subset of dataset and properly handle kernels.

    Slice X, y according to indices for cross-validation, but take care of
    precomputed kernel-matrices or pairwise affinities / distances.

    If ``estimator._pairwise is True``, X needs to be square and
    we slice rows and columns. If ``train_indices`` is not None,
    we slice rows using ``indices`` (assumed the test set) and columns
    using ``train_indices``, indicating the training set.

    Labels y will always be indexed only along the first axis.

        Estimator to determine whether we should slice only rows or rows and
        columns.

    X : array-like, sparse matrix or iterable
        Data to be indexed. If ``estimator._pairwise is True``,
        this needs to be a square array-like or sparse matrix.

    y : array-like, sparse matrix or iterable
        Targets to be indexed.

    indices : array of int
        Rows to select from X and y.
        If ``estimator._pairwise is True`` and ``train_indices is None``
        then ``indices`` will also be used to slice columns.

    train_indices : array of int or None, default=None
        If ``estimator._pairwise is True`` and ``train_indices is not None``,
        then ``train_indices`` will be use to slice the columns of X.

    Returns
    -------
    X_subset : array-like, sparse matrix or list
        Indexed data.

    y_subset : array-like, sparse matrix or list
        Indexed targets.

    """
    if get_tags(estimator).input_tags.pairwise:
        if not hasattr(X, "shape"):
                "Precomputed kernels or affinity matrices have "
                "to be passed as arrays or sparse matrices."
            )
        # X is a precomputed square kernel matrix
            raise ValueError("X should be a square kernel matrix")
        if train_indices is None:
            X_subset = X[np.ix_(indices, indices)]
        else:
            X_subset = X[np.ix_(indices, train_indices)]
    else:
        X_subset = _safe_indexing(X, indices)

    if y is not None:
        y_subset = _safe_indexing(y, indices)
    else:
        y_subset = None

    return X_subset, y_subset

            return out

        try:
            return out

        return out

                break

                )
            )
            )

class _AvailableIfDescriptor:
    """Implements a conditional property using the descriptor protocol.

    Using this class to create a decorator will raise an ``AttributeError``
    if check(self) returns a falsey value. Note that if check raises an error
    this will also result in hasattr returning false.

    See https://docs.python.org/3/howto/descriptor.html for an explanation of
    descriptors.
    """

    def __init__(self, fn, check, attribute_name):
        self.fn = fn
        self.check = check
        self.attribute_name = attribute_name

        # update the docstring of the descriptor
        update_wrapper(self, fn)

    def _check(self, obj, owner):
        attr_err_msg = (
            f"This {owner.__name__!r} has no attribute {self.attribute_name!r}"
        )
        try:
            check_result = self.check(obj)
            raise AttributeError(attr_err_msg) from e

        if not check_result:
            raise AttributeError(attr_err_msg)

    def __get__(self, obj, owner=None):
        if obj is not None:
            # delegate only on instances, not the classes.
            # this is to allow access to the docstrings.
            self._check(obj, owner=owner)
            out = MethodType(self.fn, obj)

        else:
            @wraps(self.fn)
            def out(*args, **kwargs):
                self._check(args[0], owner=owner)
                return self.fn(*args, **kwargs)

        return out

def available_if(check):
    """An attribute that is available only if check returns a truthy value.

    check : callable
        When passed the object with the decorated method, this should return
        a truthy value if the attribute is available, and either return False
        or raise an AttributeError if not available.

    Returns
    -------
    callable
        Callable makes the decorated method available if `check` returns
        a truthy value, otherwise the decorated method is unavailable.

    Examples
    --------
    >>> from sklearn.utils.metaestimators import available_if
    >>> class HelloIfEven:
    ...    def __init__(self, x):
    ...        self.x = x
    ...
    ...    def _x_is_even(self):
    ...        return self.x % 2 == 0
    ...
    ...    @available_if(_x_is_even)
    ...    def say_hello(self):
    ...        print("Hello")
    ...
    >>> obj = HelloIfEven(1)
    >>> hasattr(obj, "say_hello")
    False
    >>> obj.x = 2
    True
    >>> obj.say_hello()
    Hello
    """
    return lambda fn: _AvailableIfDescriptor(fn, check, attribute_name=fn.__name__)

        )
        try:

        else:

        return out

def is_scalar_nan(x):
    """Test if x is NaN.

    This function is meant to overcome the issue that np.isnan does not allow
    non-numerical types as input, and that np.nan is not float('nan').

    x : any type
        Any scalar value.

    Returns
    -------
    bool
        Returns true if x is NaN, and false otherwise.

    Examples
    --------
    >>> from sklearn.utils._missing import is_scalar_nan
    >>> is_scalar_nan(np.nan)
    True
    >>> is_scalar_nan(float("nan"))
    True
    >>> is_scalar_nan(None)
    False
    >>> is_scalar_nan("")
    False
    >>> is_scalar_nan([np.nan])
    False
    """
    return (
        not isinstance(x, numbers.Integral)
        and isinstance(x, numbers.Real)
        and math.isnan(x)
    )

def is_pandas_na(x):
    """Test if x is pandas.NA.

    We intentionally do not use this function to return `True` for `pd.NA` in
    `is_scalar_nan`, because estimators that support `pd.NA` are the exception
    rather than the rule at the moment. When `pd.NA` is more universally
    supported, we may reconsider this decision.

    x : any type

    Returns
    -------
    boolean
    """
    with suppress(ImportError):
        from pandas import NA

        return x is NA

def ignore_warnings(obj=None, category=Warning):
    """Context manager and decorator to ignore warnings.

    Note: Using this (in both variants) will clear all warnings
    from all python modules loaded. In case you need to test
    cross-module-warning-logging, this is not your tool of choice.

    obj : callable, default=None
        callable where you want to ignore the warnings.
    category : warning class, default=Warning
        The category to filter. If Warning, all categories will be muted.

    Examples
    --------
    >>> import warnings
    >>> from sklearn.utils._testing import ignore_warnings
    >>> with ignore_warnings():
    ...     warnings.warn('buhuhuhu')

    >>> def nasty_warn():
    ...     print(42)

    >>> ignore_warnings(nasty_warn)()
    42
    """
    if isinstance(obj, type) and issubclass(obj, Warning):
        # Avoid common pitfall of passing category as the first positional
        # argument which result in the test not being run
        warning_name = obj.__name__
            "'obj' should be a callable where you want to ignore warnings. "
            "You passed a warning class instead: 'obj={warning_name}'. "
            "If you want to pass a warning class to ignore_warnings, "
            "you should use 'category={warning_name}'".format(warning_name=warning_name)
        )
    elif callable(obj):
        return _IgnoreWarnings(category=category)(obj)
    else:
        return _IgnoreWarnings(category=category)

class _IgnoreWarnings:
    """Improved and simplified Python warnings context manager and decorator.

    This class allows the user to ignore the warnings raised by a function.
    Copied from Python 2.7.5 and modified as required.

    category : tuple of warning class, default=Warning
        The category to filter. By default, all the categories will be muted.

    """

    def __init__(self, category):
        self._record = True
        self._module = sys.modules["warnings"]
        self._entered = False
        self.log = []
        self.category = category

    def __call__(self, fn):
        """Decorator to catch and hide warnings without visual nesting."""

        @wraps(fn)
                warnings.simplefilter("ignore", self.category)
                return fn(*args, **kwargs)

        args = []
        if self._record:
            args.append("record=True")
        if self._module is not sys.modules["warnings"]:
            args.append("module=%r" % self._module)
        name = type(self).__name__
        return "%s(%s)" % (name, ", ".join(args))

        if self._entered:
            raise RuntimeError("Cannot enter %r twice" % self)
        self._entered = True
        self._filters = self._module.filters
        self._module.filters = self._filters[:]
        self._showwarning = self._module.showwarning
        warnings.simplefilter("ignore", self.category)

    def __exit__(self, *exc_info):
        if not self._entered:
            raise RuntimeError("Cannot exit %r without entering first" % self)
        self._module.filters = self._filters
        self._module.showwarning = self._showwarning
        self.log[:] = []

def assert_allclose(
    actual, desired, rtol=None, atol=0.0, equal_nan=True, err_msg="", verbose=True
):
    """dtype-aware variant of numpy.testing.assert_allclose

    This variant introspects the least precise floating point dtype
    in the input argument and automatically sets the relative tolerance
    parameter to 1e-4 float32 and use 1e-7 otherwise (typically float64
    in scikit-learn).

    `atol` is always left to 0. by default. It should be adjusted manually
    to an assertion-specific value in case there are null values expected
    in `desired`.

    The aggregate tolerance is `atol + rtol * abs(desired)`.

    actual : array_like
        Array obtained.
    desired : array_like
        Array desired.
    rtol : float, optional, default=None
        Relative tolerance.
        If None, it is set based on the provided arrays' dtypes.
    atol : float, optional, default=0.
        Absolute tolerance.
    equal_nan : bool, optional, default=True
        If True, NaNs will compare equal.
    err_msg : str, optional, default=''
        The error message to be printed in case of failure.
    verbose : bool, optional, default=True
        If True, the conflicting values are appended to the error message.

    Raises
    ------
    AssertionError
        If actual and desired are not equal up to specified precision.

    See Also
    --------
    numpy.testing.assert_allclose

    Examples
    --------
    >>> from sklearn.utils._testing import assert_allclose
    >>> x = [1e-5, 1e-3, 1e-1]
    >>> y = np.arccos(np.cos(x))
    >>> assert_allclose(x, y, rtol=1e-5, atol=0)
    >>> a = np.full(shape=10, fill_value=1e-5, dtype=np.float32)
    >>> assert_allclose(a, 1e-5)
    """
    dtypes = []

    actual, desired = np.asanyarray(actual), np.asanyarray(desired)
    dtypes = [actual.dtype, desired.dtype]

    np_assert_allclose(
        actual,
        desired,
        rtol=rtol,
        equal_nan=equal_nan,
        err_msg=err_msg,
    )

def assert_allclose_dense_sparse(x, y, rtol=1e-07, atol=1e-9, err_msg=""):
    """Assert allclose for sparse and dense data.

    Both x and y need to be either sparse or dense, they
    can't be mixed.

    x : {array-like, sparse matrix}
        First array to compare.

        Second array to compare.

    rtol : float, default=1e-07
        relative tolerance; see numpy.allclose.

    atol : float, default=1e-9
        absolute tolerance; see numpy.allclose. Note that the default here is
        more tolerant than the default for numpy.testing.assert_allclose, where
        atol=0.

    err_msg : str, default=''
        Error message to raise.
    """
    if sp.sparse.issparse(x) and sp.sparse.issparse(y):
        x = x.tocsr()
        y = y.tocsr()
        x.sum_duplicates()
        y.sum_duplicates()
        assert_array_equal(x.indices, y.indices, err_msg=err_msg)
        assert_array_equal(x.indptr, y.indptr, err_msg=err_msg)
        assert_allclose(x.data, y.data, rtol=rtol, atol=atol, err_msg=err_msg)
    elif not sp.sparse.issparse(x) and not sp.sparse.issparse(y):
        # both dense
        assert_allclose(x, y, rtol=rtol, atol=atol, err_msg=err_msg)
    else:
            "Can only compare two sparse matrices, not a sparse matrix and an array."
        )

def set_random_state(estimator, random_state=0):
    """Set random state of an estimator if it has the `random_state` param.

        The estimator.
        Pseudo random number generator state.
    """
    if "random_state" in estimator.get_params():
        estimator.set_params(random_state=random_state)

def _is_numpydoc():
    try:
        import numpydoc  # noqa: F401
    except (ImportError, AssertionError):
    else:

def check_skip_network():
    if int(os.environ.get("SKLEARN_SKIP_NETWORK_TESTS", 0)):
        raise SkipTest("Text tutorial requires large dataset download")

def _delete_folder(folder_path, warn=False):
    """Utility function to cleanup a temporary folder if still existing.

    Copy from joblib.pool (for independence).
    """
    try:
        if os.path.exists(folder_path):
            # This can fail under windows,
            #  but will succeed when called by atexit
            shutil.rmtree(folder_path)
    except OSError:
        if warn:
            warnings.warn("Could not delete temporary folder %s" % folder_path)

class TempMemmap:
    """
    data
    mmap_mode : str, default='r'
    """

    def __init__(self, data, mmap_mode="r"):
        self.mmap_mode = mmap_mode

        data_read_only, self.temp_folder = create_memmap_backed_data(
            self.data, mmap_mode=self.mmap_mode, return_folder=True
        )
        return data_read_only

        _delete_folder(self.temp_folder)

def create_memmap_backed_data(data, mmap_mode="r", return_folder=False):
    """
    data
    return_folder :  bool, default=False
    """
    temp_folder = tempfile.mkdtemp(prefix="sklearn_testing_")
    atexit.register(functools.partial(_delete_folder, temp_folder, warn=True))
    filename = op.join(temp_folder, "data.pkl")
    joblib.dump(data, filename)
    memmap_backed_data = joblib.load(filename, mmap_mode=mmap_mode)
    result = (
        memmap_backed_data if not return_folder else (memmap_backed_data, temp_folder)
    )

def _get_args(function, varargs=False):
    """Helper to get function arguments."""

    try:
        params = signature(function).parameters
        # Error on builtin C function
        return []
    args = [
        key
        for key, param in params.items()
        if param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD)
    ]
    if varargs:
        varargs = [
            param.name
            for param in params.values()
            if param.kind == param.VAR_POSITIONAL
        ]
        if len(varargs) == 0:
            varargs = None
        return args, varargs
    else:
        return args

def _get_func_name(func):
    """Get function full name.

        The function object.

    Returns
    -------
    name : str
        The function name.
    """
    parts = []
    module = inspect.getmodule(func)
    if module:
        parts.append(module.__name__)

    qualname = func.__qualname__
    if qualname != func.__name__:
        parts.append(qualname[: qualname.find(".")])

    parts.append(func.__name__)
    return ".".join(parts)

def check_docstring_parameters(func, doc=None, ignore=None):
    """Helper to check docstring.

        The function object to test.
    doc : str, default=None
        Docstring if it is passed manually to the test.
    ignore : list, default=None
        Parameters to ignore.

    Returns
    -------
    incorrect : list
        A list of string describing the incorrect results.
    """

    ignore = [] if ignore is None else ignore

    func_name = _get_func_name(func)
    if not func_name.startswith("sklearn.") or func_name.startswith(
        "sklearn.externals"
    ):
        return incorrect
    # Don't check docstring for property-functions
    if inspect.isdatadescriptor(func):
    # Don't check docstring for setup / teardown pytest functions
    if func_name.split(".")[-1] in ("setup_module", "teardown_module"):
    # Dont check estimator_checks module
    if func_name.split(".")[2] == "estimator_checks":
    # Get the arguments from the function signature
    param_signature = list(filter(lambda x: x not in ignore, _get_args(func)))
    # drop self
    if len(param_signature) > 0 and param_signature[0] == "self":
        param_signature.remove("self")

    # Analyze function's docstring
    if doc is None:
        records = []
            try:
                doc = docscrape.FunctionDoc(func)
            except UserWarning as exp:
                if "potentially wrong underline length" in str(exp):
                    # Catch warning raised as of numpydoc 1.2 when
                    # the underline length for a section of a docstring
                    # is not consistent.
                    message = str(exp).split("\n")[:3]
                    incorrect += [f"In function: {func_name}"] + message
                    return incorrect
                records.append(str(exp))
            except Exception as exp:
                incorrect += [func_name + " parsing error: " + str(exp)]
                return incorrect
        if len(records):
            raise RuntimeError("Error for %s:\n%s" % (func_name, records[0]))

    param_docs = []
    for name, type_definition, param_doc in doc["Parameters"]:
        # Type hints are empty only if parameter name ended with :
        if not type_definition.strip():
            if ":" in name and name[: name.index(":")][-1:].strip():
                incorrect += [
                    func_name
                    + " There was no space between the param name and colon (%r)" % name
                ]
            elif name.rstrip().endswith(":"):
                    func_name
                    + " Parameter %r has an empty type spec. Remove the colon"
                    % (name.lstrip())
                ]

        # Create a list of parameters to compare with the parameters gotten
        # from the func signature
        if "*" not in name:
            param_docs.append(name.split(":")[0].strip("` "))

    # If one of the docstring's parameters had an error then return that
    # incorrect message

    # Remove the parameters that should be ignored from list
    param_docs = list(filter(lambda x: x not in ignore, param_docs))

    # The following is derived from pytest, Copyright (c) 2004-2017 Holger
    # Krekel and others, Licensed under MIT License. See
    # https://github.com/pytest-dev/pytest

    message = []
    for i in range(min(len(param_docs), len(param_signature))):
        if param_signature[i] != param_docs[i]:
            message += [
                "There's a parameter name mismatch in function"
                " docstring w.r.t. function signature, at index %s"
                " diff: %r != %r" % (i, param_signature[i], param_docs[i])
            ]
            break
    if len(param_signature) > len(param_docs):
        message += [
            "Parameters in function docstring have less items w.r.t."
            " function signature, first missing item: %s"
            % param_signature[len(param_docs)]
        ]

    elif len(param_signature) < len(param_docs):
            "Parameters in function docstring have more items w.r.t."
            " function signature, first extra item: %s"
            % param_docs[len(param_signature)]
        ]

    # If there wasn't any difference in the parameters themselves between
    # docstring and signature including having the same length then return
    # empty list
    if len(message) == 0:
        return []

    import difflib
    import pprint

    param_docs_formatted = pprint.pformat(param_docs).splitlines()
    param_signature_formatted = pprint.pformat(param_signature).splitlines()

    message += ["Full diff:"]

    message.extend(
        line.strip()
        for line in difflib.ndiff(param_signature_formatted, param_docs_formatted)
    )

    incorrect.extend(message)

    # Prepend function name
    incorrect = ["In function: " + func_name] + incorrect

    return incorrect

def _check_item_included(item_name, args):
    """Helper to check if item should be included in checking."""
    if args.include is not True and item_name not in args.include:
    if args.exclude is not None and item_name in args.exclude:

def _diff_key(line):
    """Key for grouping output from `context_diff`."""
    if line.startswith("  "):
        return "  "
    elif line.startswith("- "):
        return "- "
    elif line.startswith("+ "):
        return "+ "
    elif line.startswith("! "):
        return "! "

def _get_diff_msg(docstrings_grouped):
    """Get message showing the difference between type/desc docstrings of all objects.

    `docstrings_grouped` keys should be the type/desc docstrings and values are a list
    of objects with that docstring. Objects with the same type/desc docstring are
    thus grouped together.
    """
    msg_diff = ""
    ref_str = ""
    ref_group = []
    for docstring, group in docstrings_grouped.items():
        if not ref_str and not ref_group:
            ref_str += docstring
            ref_group.extend(group)
        diff = list(
            context_diff(
                ref_str.split(),
                docstring.split(),
                fromfile=str(ref_group),
                tofile=str(group),
                n=8,
            )
        )
        # Add header
        msg_diff += "".join((diff[:3]))
        # Group consecutive 'diff' words to shorten error message
        for start, group in groupby(diff[3:], key=_diff_key):
            if start is None:
                msg_diff += "\n" + "\n".join(group)
            else:
                msg_diff += "\n" + start + " ".join(word[2:] for word in group)
        # Add new lines at end of diff, to separate comparisons
        msg_diff += "\n\n"
    return msg_diff

def _check_consistency_items(
    items_docs,
    type_or_desc,
    section,
    n_objects,
    descr_regex_pattern="",
    ignore_types=tuple(),
):
    """Helper to check docstring consistency of all `items_docs`.

    If item is not present in all objects, checking is skipped and warning raised.
    If `regex` provided, match descriptions to all descriptions.

    items_doc : dict of dict of str
        Dictionary where the key is the string type or description, value is
        a dictionary where the key is "type description" or "description"
        and the value is a list of object names with the same string type or

    type_or_desc : {"type description", "description"}
        Whether to check type description or description between objects.

    section : {"Parameters", "Attributes", "Returns"}
        Name of the section type.

    n_objects : int
        Total number of objects.

    descr_regex_pattern : str, default=""
        Regex pattern to match for description of all objects.
        Ignored when `type_or_desc="type description".

    ignore_types : tuple of str, default=()
        Tuple of parameter/attribute/return names for which type description
        matching is ignored. Ignored when `type_or_desc="description".
    """
    skipped = []
    for item_name, docstrings_grouped in items_docs.items():
        # If item not found in all objects, skip
        if sum([len(objs) for objs in docstrings_grouped.values()]) < n_objects:
            skipped.append(item_name)
        # If regex provided, match to all descriptions
        elif type_or_desc == "description" and descr_regex_pattern:
            not_matched = []
            for docstring, group in docstrings_grouped.items():
                if not re.search(descr_regex_pattern, docstring):
                    not_matched.extend(group)
            if not_matched:
                msg = textwrap.fill(
                    f"The description of {section[:-1]} '{item_name}' in {not_matched}"
                    f" does not match 'descr_regex_pattern': {descr_regex_pattern} "
                )
                raise AssertionError(msg)
        # Skip type checking for items in `ignore_types`
        elif type_or_desc == "type specification" and item_name in ignore_types:
            continue
        # Otherwise, if more than one key, docstrings not consistent between objects
        elif len(docstrings_grouped.keys()) > 1:
            msg_diff = _get_diff_msg(docstrings_grouped)
            obj_groups = " and ".join(
                str(group) for group in docstrings_grouped.values()
            )
            msg = textwrap.fill(
                f"The {type_or_desc} of {section[:-1]} '{item_name}' is inconsistent "
                f"between {obj_groups}:"
            )
            msg += msg_diff
            raise AssertionError(msg)
    if skipped:
            f"Checking was skipped for {section}: {skipped} as they were "
            "not found in all objects."
        )

def assert_docstring_consistency(
    objects,
    include_params=False,
    exclude_params=None,
    include_attrs=False,
    exclude_attrs=None,
    include_returns=False,
    exclude_returns=None,
    descr_regex_pattern=None,
):
    r"""Check consistency between docstring parameters/attributes/returns of objects.

    Checks if parameters/attributes/returns have the same type specification and
    description (ignoring whitespace) across `objects`. Intended to be used for
    related classes/functions/data descriptors.

    Entries that do not appear across all `objects` are ignored.

    objects : list of {classes, functions, data descriptors}
        Objects to check.
        Objects may be classes, functions or data descriptors with docstrings that
        can be parsed by numpydoc.

    include_params : list of str or bool, default=False
        List of parameters to be included. If True, all parameters are included,
        if False, checking is skipped for parameters.
        Can only be set if `exclude_params` is None.

    exclude_params : list of str or None, default=None
        List of parameters to be excluded. If None, no parameters are excluded.
        Can only be set if `include_params` is True.

    include_attrs : list of str or bool, default=False
        List of attributes to be included. If True, all attributes are included,
        if False, checking is skipped for attributes.
        Can only be set if `exclude_attrs` is None.

    exclude_attrs : list of str or None, default=None
        List of attributes to be excluded. If None, no attributes are excluded.
        Can only be set if `include_attrs` is True.

    include_returns : list of str or bool, default=False
        List of returns to be included. If True, all returns are included,
        if False, checking is skipped for returns.
        Can only be set if `exclude_returns` is None.

    exclude_returns : list of str or None, default=None
        List of returns to be excluded. If None, no returns are excluded.
        Can only be set if `include_returns` is True.

    descr_regex_pattern : str, default=None
        Regular expression to match to all descriptions of included
        parameters/attributes/returns. If None, will revert to default behavior
        of comparing descriptions between objects.

    ignore_types : tuple of str, default=tuple()
        Tuple of parameter/attribute/return names to exclude from type description
        matching between objects.

    Examples
    --------
    >>> from sklearn.metrics import (accuracy_score, classification_report,
    ... mean_absolute_error, mean_squared_error, median_absolute_error)
    >>> from sklearn.utils._testing import assert_docstring_consistency
    ... # doctest: +SKIP
    >>> assert_docstring_consistency([mean_absolute_error, mean_squared_error],
    ... include_params=['y_true', 'y_pred', 'sample_weight'])  # doctest: +SKIP
    >>> assert_docstring_consistency([median_absolute_error, mean_squared_error],
    ... include_params=True)  # doctest: +SKIP
    >>> assert_docstring_consistency([accuracy_score, classification_report],
    ... include_params=["y_true"],
    ... descr_regex_pattern=r"Ground truth \(correct\) (labels|target values)")
    """
    from numpydoc.docscrape import NumpyDocString

    Args = namedtuple("args", ["include", "exclude", "arg_name"])

    def _create_args(include, exclude, arg_name, section_name):
        if exclude and include is not True:
                f"The 'exclude_{arg_name}' argument can be set only when the "
                f"'include_{arg_name}' argument is True."
            )
        if include is False:
            return {}
        return {section_name: Args(include, exclude, arg_name)}

    section_args = {
        **_create_args(include_params, exclude_params, "params", "Parameters"),
        **_create_args(include_attrs, exclude_attrs, "attrs", "Attributes"),
        **_create_args(include_returns, exclude_returns, "returns", "Returns"),
    }

    objects_doc = dict()
    for obj in objects:
        if (
            inspect.isdatadescriptor(obj)
            or inspect.isfunction(obj)
            or inspect.isclass(obj)
        ):
            objects_doc[obj.__name__] = NumpyDocString(inspect.getdoc(obj))
        else:
                "All 'objects' must be one of: function, class or descriptor, "
                f"got a: {type(obj)}."
            )

    n_objects = len(objects)
    for section, args in section_args.items():
        type_items = defaultdict(lambda: defaultdict(list))
        desc_items = defaultdict(lambda: defaultdict(list))
        for obj_name, obj_doc in objects_doc.items():
            for item_name, type_def, desc in obj_doc[section]:
                if _check_item_included(item_name, args):
                    # Normalize white space
                    type_def = " ".join(type_def.strip().split())
                    desc = " ".join(chain.from_iterable(line.split() for line in desc))
                    # Use string type/desc as key, to group consistent objs together
                    type_items[item_name][type_def].append(obj_name)
                    desc_items[item_name][desc].append(obj_name)

        _check_consistency_items(
            type_items,
            "type specification",
            section,
            n_objects,
            ignore_types=ignore_types,
        )
            desc_items,
            "description",
            section,
            descr_regex_pattern=descr_regex_pattern,
        )

def assert_run_python_script_without_output(source_code, pattern=".+", timeout=60):
    """Utility to check assertions in an independent Python subprocess.

    The script provided in the source code should return 0 and the stdtout +
    stderr should not match the pattern `pattern`.

    This is a port from cloudpickle https://github.com/cloudpipe/cloudpickle

    source_code : str
        The Python source code to execute.
    pattern : str
        Pattern that the stdout + stderr should not match. By default, unless
        stdout + stderr are both empty, an error will be raised.
    timeout : int, default=60
        Time in seconds before timeout.
    """
    fd, source_file = tempfile.mkstemp(suffix="_src_test_sklearn.py")
    os.close(fd)
    try:
        with open(source_file, "wb") as f:
            f.write(source_code.encode("utf-8"))
        cmd = [sys.executable, source_file]
        cwd = op.normpath(op.join(op.dirname(sklearn.__file__), ".."))
        env = os.environ.copy()
        try:
            env["PYTHONPATH"] = os.pathsep.join([cwd, env["PYTHONPATH"]])
            env["PYTHONPATH"] = cwd
        kwargs = {"cwd": cwd, "stderr": STDOUT, "env": env}
        # If coverage is running, pass the config file to the subprocess
        coverage_rc = os.environ.get("COVERAGE_PROCESS_START")
        if coverage_rc:
            kwargs["env"]["COVERAGE_PROCESS_START"] = coverage_rc

        kwargs["timeout"] = timeout
        try:
            try:
                out = check_output(cmd, **kwargs)
            except CalledProcessError as e:
                    "script errored with output:\n%s" % e.output.decode("utf-8")
                )

            out = out.decode("utf-8")
            if re.search(pattern, out):
                if pattern == ".+":
                    expectation = "Expected no output"
                else:
                    expectation = f"The output was not supposed to match {pattern!r}"

                message = f"{expectation}, got the following output instead: {out!r}"
                raise AssertionError(message)
        except TimeoutExpired as e:
                "script timeout, output so far:\n%s" % e.output.decode("utf-8")
            )
    finally:
        os.unlink(source_file)

def _convert_container(
    container,
    columns_name=None,
    minversion=None,
    categorical_feature_names=None,
):
    """Convert a given container to a specific array-like with a dtype.

    container : array-like
        The container to convert.
    constructor_name : {"list", "tuple", "array", "sparse", "dataframe", \
            "series", "index", "slice", "sparse_csr", "sparse_csc", \
            "sparse_csr_array", "sparse_csc_array", "pyarrow", "polars", \
            "polars_series"}
        The type of the returned container.
    columns_name : index or array-like, default=None
        For pandas container supporting `columns_names`, it will affect
        specific names.
        Force the dtype of the container. Does not apply to `"slice"`
        container.
    minversion : str, default=None
        Minimum version for package to install.
    categorical_feature_names : list of str, default=None
        List of column names to cast to categorical dtype.

    Returns
    -------
    converted_container
    """
    if constructor_name == "list":
            return list(container)
        else:
            return np.asarray(container, dtype=dtype).tolist()
    elif constructor_name == "tuple":
            return tuple(container)
        else:
            return tuple(np.asarray(container, dtype=dtype).tolist())
    elif constructor_name == "array":
        return np.asarray(container, dtype=dtype)
    elif constructor_name in ("pandas", "dataframe"):
        pd = pytest.importorskip("pandas", minversion=minversion)
        result = pd.DataFrame(container, columns=columns_name, dtype=dtype, copy=False)
        if categorical_feature_names is not None:
            for col_name in categorical_feature_names:
                result[col_name] = result[col_name].astype("category")
    elif constructor_name == "pyarrow":
        pa = pytest.importorskip("pyarrow", minversion=minversion)
        array = np.asarray(container)
        if columns_name is None:
            columns_name = [f"col{i}" for i in range(array.shape[1])]
        data = {name: array[:, i] for i, name in enumerate(columns_name)}
        result = pa.Table.from_pydict(data)
            for col_idx, col_name in enumerate(result.column_names):
                if col_name in categorical_feature_names:
                    result = result.set_column(
                        col_idx, col_name, result.column(col_name).dictionary_encode()
                    )
    elif constructor_name == "polars":
        pl = pytest.importorskip("polars", minversion=minversion)
        result = pl.DataFrame(container, schema=columns_name, orient="row")
                result = result.with_columns(pl.col(col_name).cast(pl.Categorical))
    elif constructor_name == "series":
        return pd.Series(container, dtype=dtype)
    elif constructor_name == "polars_series":
        return pl.Series(values=container)
    elif constructor_name == "index":
        return pd.Index(container, dtype=dtype)
    elif constructor_name == "slice":
        return slice(container[0], container[1])
    elif "sparse" in constructor_name:
        if not sp.sparse.issparse(container):
            # For scipy >= 1.13, sparse array constructed from 1d array may be
            # 1d or raise an exception. To avoid this, we make sure that the
            # input container is 2d. For more details, see
            # https://github.com/scipy/scipy/pull/18530#issuecomment-1878005149
            container = np.atleast_2d(container)

        if constructor_name in ("sparse", "sparse_csr"):
            # sparse and sparse_csr are equivalent for legacy reasons
            return sp.sparse.csr_matrix(container, dtype=dtype)
        elif constructor_name == "sparse_csr_array":
            return sp.sparse.csr_array(container, dtype=dtype)
        elif constructor_name == "sparse_csc":
            return sp.sparse.csc_matrix(container, dtype=dtype)
        elif constructor_name == "sparse_csc_array":
            return sp.sparse.csc_array(container, dtype=dtype)

def raises(expected_exc_type, match=None, may_pass=False, err_msg=None):
    """Context manager to ensure exceptions are raised within a code block.

    This is similar to and inspired from pytest.raises, but supports a few
    other cases.

    This is only intended to be used in estimator_checks.py where we don't
    want to use pytest. In the rest of the code base, just use pytest.raises
    instead.

    excepted_exc_type : Exception or list of Exception
        The exception that should be raised by the block. If a list, the block
        should raise one of the exceptions.
    match : str or list of str, default=None
        A regex that the exception message should match. If a list, one of
        the entries must match. If None, match isn't enforced.
    may_pass : bool, default=False
        If True, the block is allowed to not raise an exception. Useful in
        cases where some estimators may support a feature but others must
        fail with an appropriate error message. By default, the context
        manager will raise an exception if the block does not raise an
        exception.
    err_msg : str, default=None
        If the context manager fails (e.g. the block fails to raise the
        proper exception, or fails to match), then an AssertionError is
        raised with this message. By default, an AssertionError is raised
        with a default error message (depends on the kind of failure). Use
        this to indicate how users should fix their estimators to pass the
        checks.

    raised_and_matched : bool
        True if an exception was raised and a match was found, False otherwise.
    """
    return _Raises(expected_exc_type, match, may_pass, err_msg)

class _Raises(contextlib.AbstractContextManager):
    # see raises() for parameters
    def __init__(self, expected_exc_type, match, may_pass, err_msg):
        self.expected_exc_types = (
            expected_exc_type
            if isinstance(expected_exc_type, Iterable)
            else [expected_exc_type]
        )
        self.matches = [match] if isinstance(match, str) else match
        self.may_pass = may_pass
        self.err_msg = err_msg
        self.raised_and_matched = False

    def __exit__(self, exc_type, exc_value, _):
        # see
        # https://docs.python.org/2.5/whatsnew/pep-343.html#SECTION000910000000000000000

        if exc_type is None:  # No exception was raised in the block
            if self.may_pass:
                return True  # CM is happy
            else:
                err_msg = self.err_msg or f"Did not raise: {self.expected_exc_types}"
                raise AssertionError(err_msg)

        if not any(
            issubclass(exc_type, expected_type)
            for expected_type in self.expected_exc_types
        ):
            if self.err_msg is not None:
                raise AssertionError(self.err_msg) from exc_value
            else:
                return False  # will re-raise the original exception

        if self.matches is not None:
            err_msg = self.err_msg or (
                "The error message should contain one of the following "
                "patterns:\n{}\nGot {}".format("\n".join(self.matches), str(exc_value))
            )
            if not any(re.search(match, str(exc_value)) for match in self.matches):
                raise AssertionError(err_msg) from exc_value
            self.raised_and_matched = True

class MinimalClassifier:
    """Minimal classifier implementation without inheriting from BaseEstimator.

    This estimator should be tested with:

    * `check_estimator` in `test_estimator_checks.py`;
    * within a `Pipeline` in `test_pipeline.py`;
    * within a `SearchCV` in `test_search.py`.
    """

    def __init__(self, param=None):
        self.param = param

        return {"param": self.param}

            setattr(self, key, value)

        X, y = check_X_y(X, y)
        self.classes_, counts = np.unique(y, return_counts=True)
        self._most_frequent_class_idx = counts.argmax()

        proba_shape = (X.shape[0], self.classes_.size)
        y_proba = np.zeros(shape=proba_shape, dtype=np.float64)
        y_proba[:, self._most_frequent_class_idx] = 1.0

        y_proba = self.predict_proba(X)
        y_pred = y_proba.argmax(axis=1)
        return self.classes_[y_pred]

        from sklearn.metrics import accuracy_score

        return accuracy_score(y, self.predict(X))

            estimator_type="classifier",
            classifier_tags=ClassifierTags(),
            target_tags=TargetTags(required=True),
        )

class MinimalRegressor:
    """Minimal regressor implementation without inheriting from BaseEstimator.

    """

        self.is_fitted_ = True
        self._mean = np.mean(y)

        return np.ones(shape=(X.shape[0],)) * self._mean

        from sklearn.metrics import r2_score

        return r2_score(y, self.predict(X))

            estimator_type="regressor",
            regressor_tags=RegressorTags(),
        )

class MinimalTransformer:
    """Minimal transformer implementation without inheriting from
    BaseEstimator.

    """

        check_array(X)

        return X

            estimator_type="transformer",
            transformer_tags=TransformerTags(),
        )

def _array_api_for_tests(array_namespace, device):
    try:
        array_mod = importlib.import_module(array_namespace)
    except (ModuleNotFoundError, ImportError):
            f"{array_namespace} is not installed: not checking array_api input"
        )

    if os.environ.get("SCIPY_ARRAY_API") is None:
        raise SkipTest("SCIPY_ARRAY_API is not set: not checking array_api input")

    from sklearn.externals.array_api_compat import get_namespace

    # First create an array using the chosen array module and then get the
    # corresponding (compatibility wrapped) array namespace based on it.
    # This is because `cupy` is not the same as the compatibility wrapped
    # namespace of a CuPy array.
    xp = get_namespace(array_mod.asarray(1))
    if (
        array_namespace == "torch"
        and device == "cuda"
        and not xp.backends.cuda.is_built()
    ):
        raise SkipTest("PyTorch test requires cuda, which is not available")
    elif array_namespace == "torch" and device == "mps":
        if os.getenv("PYTORCH_ENABLE_MPS_FALLBACK") != "1":
            # For now we need PYTORCH_ENABLE_MPS_FALLBACK=1 for all estimators to work
            # when using the MPS device.
                "Skipping MPS device test because PYTORCH_ENABLE_MPS_FALLBACK is not "
                "set."
            )
        if not xp.backends.mps.is_built():
                "MPS is not available because the current PyTorch install was not "
                "built with MPS enabled."
            )
    elif array_namespace == "cupy":  # pragma: nocover
        import cupy

        if cupy.cuda.runtime.getDeviceCount() == 0:
            raise SkipTest("CuPy test requires cuda, which is not available")
    return xp

def _get_warnings_filters_info_list():
    @dataclass
    class WarningInfo:
        action: "warnings._ActionKind"  # type: ignore[annotation-unchecked]
        message: str = ""  # type: ignore[annotation-unchecked]
        category: type[Warning] = Warning  # type: ignore[annotation-unchecked]

        def to_filterwarning_str(self):
            if self.category.__module__ == "builtins":
                category = self.category.__name__
            else:
                category = f"{self.category.__module__}.{self.category.__name__}"

            return f"{self.action}:{self.message}:{category}"

    return [
        WarningInfo("error", category=DeprecationWarning),
        WarningInfo("error", category=FutureWarning),
        WarningInfo("error", category=VisibleDeprecationWarning),
        # TODO: remove when pyamg > 5.0.1
        # Avoid a deprecation warning due pkg_resources usage in pyamg.
        WarningInfo(
            "ignore",
            message="pkg_resources is deprecated as an API",
            category=DeprecationWarning,
        ),
            "ignore",
            message="Deprecated call to `pkg_resources",
        ),
        # pytest-cov issue https://github.com/pytest-dev/pytest-cov/issues/557 not
        # fixed although it has been closed. https://github.com/pytest-dev/pytest-cov/pull/623
        # would probably fix it.
            "ignore",
            message=(
                "The --rsyncdir command line argument and rsyncdirs config variable are"
                " deprecated"
            ),
        ),
        # XXX: Easiest way to ignore pandas Pyarrow DeprecationWarning in the
        # short-term. See https://github.com/pandas-dev/pandas/issues/54466 for
        # more details.
            "ignore",
            message=r"\s*Pyarrow will become a required dependency",
        ),
        # warnings has been fixed from dateutil main but not released yet, see
        # https://github.com/dateutil/dateutil/issues/1314
            "ignore",
            message="datetime.datetime.utcfromtimestamp",
        ),
        # Python 3.12 warnings from joblib fixed in master but not released yet,
        # see https://github.com/joblib/joblib/pull/1518
            "ignore", message="ast.Num is deprecated", category=DeprecationWarning
        ),
            "ignore", message="Attribute n is deprecated", category=DeprecationWarning
        ),
        # Python 3.12 warnings from sphinx-gallery fixed in master but not
        # released yet, see
        # https://github.com/sphinx-gallery/sphinx-gallery/pull/1242
            "ignore", message="ast.Str is deprecated", category=DeprecationWarning
        ),
            "ignore", message="Attribute s is deprecated", category=DeprecationWarning
        ),
        # Plotly deprecated something which we're not using, but internally it's used
        # and needs to be fixed on their side.
        # https://github.com/plotly/plotly.py/issues/4997
            "ignore",
            message=".+scattermapbox.+deprecated.+scattermap.+instead",
        ),
    ]

def get_pytest_filterwarning_lines():
    warning_filters_info_list = _get_warnings_filters_info_list()
    return [
        warning_info.to_filterwarning_str()
        for warning_info in warning_filters_info_list
    ]

def turn_warnings_into_errors():
    warnings_filters_info_list = _get_warnings_filters_info_list()
    for warning_info in warnings_filters_info_list:
            warning_info.action,
            message=warning_info.message,
            category=warning_info.category,
        )

        args = []

        )

            )
            return {}

        )

        # see

            else:

        if not any(
        ):
            else:

            )

        )

        )

        return X

        )

            else:

            else:

def squared_norm(x):
    """Squared Euclidean or Frobenius norm of x.

    Faster than norm(x) ** 2.

        The input array which could be either be a vector or a 2 dimensional array.

    Returns
    -------
    float
        The Euclidean norm when x is a vector, the Frobenius norm when x
        is a matrix (2-d array).
    """
    x = np.ravel(x, order="K")
    if np.issubdtype(x.dtype, np.integer):
            (
                "Array type is integer, np.dot may overflow. "
                "Data should be float type to avoid this issue"
            ),
        )
    return np.dot(x, x)

def row_norms(X, squared=False):
    """Row-wise (squared) Euclidean norm of X.

    Equivalent to np.sqrt((X * X).sum(axis=1)), but also supports sparse
    matrices and does not create an X.shape-sized temporary.

    Performs no input validation.

        The input array.
        If True, return squared norms.

    Returns
    -------
    array-like
        The row-wise (squared) Euclidean norm of X.
    """
        X = X.tocsr()
        norms = csr_row_norms(X)
        if not squared:
            norms = np.sqrt(norms)
    else:
            X = np.asarray(X)
            norms = np.einsum("ij,ij->i", X, X)
            norms = xp.asarray(norms)
        else:
            norms = xp.sum(xp.multiply(X, X), axis=1)
            norms = xp.sqrt(norms)
    return norms

def fast_logdet(A):
    """Compute logarithm of determinant of a square matrix.

    The (natural) logarithm of the determinant of a square matrix
    is returned if det(A) is non-negative and well defined.
    If the determinant is zero or negative returns -Inf.

    Equivalent to : np.log(np.det(A)) but more robust.

    A : array_like of shape (n, n)
        The square matrix.

    Returns
    -------
    logdet : float
        When det(A) is strictly positive, log(det(A)) is returned.
        When det(A) is non-positive or not defined, then -inf is returned.

    See Also
    --------
    numpy.linalg.slogdet : Compute the sign and (natural) logarithm of the determinant
        of an array.

    Examples
    --------
    >>> from sklearn.utils.extmath import fast_logdet
    >>> a = np.array([[5, 1], [2, 8]])
    >>> fast_logdet(a)
    np.float64(3.6375861597263857)
    """
    xp, _ = get_namespace(A)
    sign, ld = xp.linalg.slogdet(A)
    if not sign > 0:
    return ld

def density(w):
    """Compute density of a sparse vector.

    w : {ndarray, sparse matrix}
        The input data can be numpy ndarray or a sparse matrix.

    Returns
    -------
    float
        The density of w, between 0 and 1.

    Examples
    --------
    >>> from sklearn.utils.extmath import density
    >>> X = sparse.random(10, 10, density=0.25, random_state=0)
    >>> density(X)
    0.25
    """
    if hasattr(w, "toarray"):
        d = float(w.nnz) / (w.shape[0] * w.shape[1])
    else:
        d = 0 if w is None else float((w != 0).sum()) / w.size
    return d

def safe_sparse_dot(a, b, *, dense_output=False):
    """Dot product that handle the sparse matrix case correctly.

    a : {ndarray, sparse matrix}
    b : {ndarray, sparse matrix}
        When False, ``a`` and ``b`` both being sparse will yield sparse output.
        When True, output will always be a dense array.

    Returns
    -------
    dot_product : {ndarray, sparse matrix}
        Sparse if ``a`` and ``b`` are sparse and ``dense_output=False``.

    Examples
    --------
    >>> from sklearn.utils.extmath import safe_sparse_dot
    >>> X = csr_matrix([[1, 2], [3, 4], [5, 6]])
    >>> dot_product = safe_sparse_dot(X, X.T)
    >>> dot_product.toarray()
    array([[ 5, 11, 17],
           [11, 25, 39],
           [17, 39, 61]])
    """
    xp, _ = get_namespace(a, b)
    if a.ndim > 2 or b.ndim > 2:
        if sparse.issparse(a):
            # sparse is always 2D. Implies b is 3D+
            # [i, j] @ [k, ..., l, m, n] -> [i, k, ..., l, n]
            b_ = np.rollaxis(b, -2)
            b_2d = b_.reshape((b.shape[-2], -1))
            ret = a @ b_2d
            ret = ret.reshape(a.shape[0], *b_.shape[1:])
        elif sparse.issparse(b):
            # sparse is always 2D. Implies a is 3D+
            # [k, ..., l, m] @ [i, j] -> [k, ..., l, j]
            a_2d = a.reshape(-1, a.shape[-1])
            ret = a_2d @ b
            ret = ret.reshape(*a.shape[:-1], b.shape[1])
        else:
            # Alternative for `np.dot` when dealing with a or b having
            # more than 2 dimensions, that works with the array api.
            # If b is 1-dim then the last axis for b is taken otherwise
            # if b is >= 2-dim then the second to last axis is taken.
            b_axis = -1 if b.ndim == 1 else -2
            ret = xp.tensordot(a, b, axes=[-1, b_axis])
    else:
        ret = a @ b

    if (
        sparse.issparse(a)
        and sparse.issparse(b)
        and dense_output
        and hasattr(ret, "toarray")
    ):
        return ret.toarray()
    return ret

def randomized_range_finder(
    A, *, size, n_iter, power_iteration_normalizer="auto", random_state=None
):
    """Compute an orthonormal matrix whose range approximates the range of A.

    A : {array-like, sparse matrix} of shape (n_samples, n_features)
        The input data matrix.

    size : int
        Size of the return array.

        Number of power iterations used to stabilize the result.

        Whether the power iterations are normalized with step-by-step
        QR factorization (the slowest but most accurate), 'none'
        (the fastest but numerically unstable when `n_iter` is large, e.g.
        typically 5 or larger), or 'LU' factorization (numerically stable
        but can lose slightly in accuracy). The 'auto' mode applies no
        normalization if `n_iter` <= 2 and switches to LU otherwise.

        the data, i.e. getting the random vectors to initialize the algorithm.

    Returns
    -------
    Q : ndarray of shape (size, size)
        A projection matrix, the range of which approximates well the range of the
        input matrix A.

    Notes
    -----

    Follows Algorithm 4.3 of
    :arxiv:`"Finding structure with randomness:
    Halko, et al. (2009)

    An implementation of a randomized algorithm for principal component
    analysis
    A. Szlam et al. 2014

    Examples
    --------
    >>> from sklearn.utils.extmath import randomized_range_finder
    >>> A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    >>> randomized_range_finder(A, size=2, n_iter=2, random_state=42)
    array([[-0.21...,  0.88...],
           [-0.52...,  0.24...],
           [-0.82..., -0.38...]])
    """
    A = check_array(A, accept_sparse=True)

    return _randomized_range_finder(
        A,
        size=size,
        power_iteration_normalizer=power_iteration_normalizer,
    )

def _randomized_range_finder(
):
    """Body of randomized_range_finder without input validation."""
    xp, is_array_api_compliant = get_namespace(A)

    # Generating normal random vectors with shape: (A.shape[1], size)
    # XXX: generate random number directly from xp if it's possible
    # one day.
    Q = xp.asarray(random_state.normal(size=(A.shape[1], size)))
    if hasattr(A, "dtype") and xp.isdtype(A.dtype, kind="real floating"):
        # Use float32 computation and components if A has a float32 dtype.
        Q = xp.astype(Q, A.dtype, copy=False)

    # Move Q to device if needed only after converting to float32 if needed to
    # avoid allocating unnecessary memory on the device.

    # Note: we cannot combine the astype and to_device operations in one go
    # using xp.asarray(..., dtype=dtype, device=device) because downcasting
    # from float64 to float32 in asarray might not always be accepted as only
    # casts following type promotion rules are guarateed to work.
    # https://github.com/data-apis/array-api/issues/647
        Q = xp.asarray(Q, device=device(A))

    # Deal with "auto" mode
    if power_iteration_normalizer == "auto":
        if n_iter <= 2:
            power_iteration_normalizer = "none"
        elif is_array_api_compliant:
            # XXX: https://github.com/data-apis/array-api/issues/627
                "Array API does not support LU factorization, falling back to QR"
                " instead. Set `power_iteration_normalizer='QR'` explicitly to silence"
                " this warning."
            )
            power_iteration_normalizer = "QR"
        else:
            power_iteration_normalizer = "LU"
    elif power_iteration_normalizer == "LU" and is_array_api_compliant:
            "Array API does not support LU factorization. Set "
            "`power_iteration_normalizer='QR'` instead."
        )

        qr_normalizer = partial(xp.linalg.qr, mode="reduced")
    else:
        # Use scipy.linalg instead of numpy.linalg when not explicitly
        # using the Array API.
        qr_normalizer = partial(linalg.qr, mode="economic", check_finite=False)

    if power_iteration_normalizer == "QR":
        normalizer = qr_normalizer
    elif power_iteration_normalizer == "LU":
        normalizer = partial(linalg.lu, permute_l=True, check_finite=False)
    else:
        normalizer = lambda x: (x, None)

    # Perform power iterations with Q to further 'imprint' the top
    # singular vectors of A in Q
        Q, _ = normalizer(A @ Q)
        Q, _ = normalizer(A.T @ Q)

    # Sample the range of A using by linear projection of Q
    # Extract an orthonormal basis
    Q, _ = qr_normalizer(A @ Q)

    return Q

def randomized_svd(
    M,
    *,
    n_oversamples=10,
    n_iter="auto",
    power_iteration_normalizer="auto",
    transpose="auto",
    flip_sign=True,
    svd_lapack_driver="gesdd",
):
    """Compute a truncated randomized SVD.

    This method solves the fixed-rank approximation problem described in [1]_
    (problem (1.5), p5).

    Refer to
    :ref:`sphx_glr_auto_examples_applications_wikipedia_principal_eigenvector.py`
    for a typical example where the power iteration algorithm is used to rank web pages.
    This algorithm is also known to be used as a building block in Google's PageRank
    algorithm.

    M : {array-like, sparse matrix} of shape (n_samples, n_features)
        Matrix to decompose.

        Number of singular values and vectors to extract.

        Additional number of random vectors to sample the range of `M` so as
        to ensure proper conditioning. The total number of random vectors
        used to find the range of `M` is `n_components + n_oversamples`. Smaller
        number can improve speed but can negatively impact the quality of
        approximation of singular vectors and singular values. Users might wish
        to increase this parameter up to `2*k - n_components` where k is the
        effective rank, for large matrices, noisy problems, matrices with
        slowly decaying spectrums, or to increase precision accuracy. See [1]_
        (pages 5, 23 and 26).

    n_iter : int or 'auto', default='auto'
        Number of power iterations. It can be used to deal with very noisy
        problems. When 'auto', it is set to 4, unless `n_components` is small
        (< .1 * min(X.shape)) in which case `n_iter` is set to 7.
        This improves precision with few components. Note that in general
        users should rather increase `n_oversamples` before increasing `n_iter`
        as the principle of the randomized method is to avoid usage of these
        more costly power iterations steps. When `n_components` is equal
        or greater to the effective matrix rank and the spectrum does not
        present a slow decay, `n_iter=0` or `1` should even work fine in theory
        (see [1]_ page 9).

    transpose : bool or 'auto', default='auto'
        Whether the algorithm should be applied to M.T instead of M. The
        result should approximately be the same. The 'auto' mode will
        trigger the transposition if M.shape[1] > M.shape[0] since this
        implementation of randomized SVD tend to be a little faster in that
        case.

    flip_sign : bool, default=True
        The output of a singular value decomposition is only unique up to a
        permutation of the signs of the singular vectors. If `flip_sign` is
        set to `True`, the sign ambiguity is resolved by making the largest
        loadings for each component in the left singular vectors positive.

    random_state : int, RandomState instance or None, default='warn'
        The seed of the pseudo random number generator to use when
        shuffling the data, i.e. getting the random vectors to initialize
        the algorithm. Pass an int for reproducible results across multiple

            The default value changed from 0 to None.

    svd_lapack_driver : {"gesdd", "gesvd"}, default="gesdd"
        Whether to use the more efficient divide-and-conquer approach
        (`"gesdd"`) or more general rectangular approach (`"gesvd"`) to compute
        the SVD of the matrix B, which is the projection of M into a low
        dimensional subspace, as described in [1]_.

    Returns
    -------
    u : ndarray of shape (n_samples, n_components)
        Unitary matrix having left singular vectors with signs flipped as columns.
    s : ndarray of shape (n_components,)
        The singular values, sorted in non-increasing order.
    vh : ndarray of shape (n_components, n_features)
        Unitary matrix having right singular vectors with signs flipped as rows.

    Notes
    -----
    This algorithm finds a (usually very good) approximate truncated
    singular value decomposition using randomization to speed up the
    computations. It is particularly fast on large matrices on which
    you wish to extract only a small number of components. In order to
    obtain further speed up, `n_iter` can be set <=2 (at the cost of
    loss of precision). To increase the precision it is recommended to
    increase `n_oversamples`, up to `2*k-n_components` where k is the
    effective rank. Usually, `n_components` is chosen to be greater than k
    so increasing `n_oversamples` up to `n_components` should be enough.

    .. [1] :arxiv:`"Finding structure with randomness:
      Stochastic algorithms for constructing approximate matrix decompositions"
      <0909.4061>`
      Halko, et al. (2009)

    .. [2] A randomized algorithm for the decomposition of matrices
      Per-Gunnar Martinsson, Vladimir Rokhlin and Mark Tygert

    .. [3] An implementation of a randomized algorithm for principal component
      analysis A. Szlam et al. 2014

    Examples
    --------
    >>> from sklearn.utils.extmath import randomized_svd
    >>> a = np.array([[1, 2, 3, 5],
    ...               [3, 4, 5, 6],
    ...               [7, 8, 9, 10]])
    >>> U, s, Vh = randomized_svd(a, n_components=2, random_state=0)
    >>> U.shape, s.shape, Vh.shape
    ((3, 2), (2,), (2, 4))
    """
    M = check_array(M, accept_sparse=True)
    return _randomized_svd(
        M,
        n_oversamples=n_oversamples,
        transpose=transpose,
        flip_sign=flip_sign,
        svd_lapack_driver=svd_lapack_driver,
    )

def _randomized_svd(
    M,
    *,
):
    """Body of randomized_svd without input validation."""
    xp, is_array_api_compliant = get_namespace(M)

    if sparse.issparse(M) and M.format in ("lil", "dok"):
            "Calculating SVD of a {} is expensive. "
            "csr_matrix is more efficient.".format(type(M).__name__),
            sparse.SparseEfficiencyWarning,
        )

    n_random = n_components + n_oversamples
    n_samples, n_features = M.shape

    if n_iter == "auto":
        # Checks if the number of iterations is explicitly specified
        # Adjust n_iter. 7 was found a good compromise for PCA. See #5299
        n_iter = 7 if n_components < 0.1 * min(M.shape) else 4

    if transpose == "auto":
        transpose = n_samples < n_features
    if transpose:
        # this implementation is a bit faster with smaller shape[1]
        M = M.T

    Q = _randomized_range_finder(
        M,
        size=n_random,
    )

    # project M to the (k + p) dimensional space using the basis vectors
    B = Q.T @ M

    # compute the SVD on the thin matrix: (k + p) wide
        Uhat, s, Vt = xp.linalg.svd(B, full_matrices=False)
    else:
        # When array_api_dispatch is disabled, rely on scipy.linalg
        # instead of numpy.linalg to avoid introducing a behavior change w.r.t.
        # previous versions of scikit-learn.
        Uhat, s, Vt = linalg.svd(
            B, full_matrices=False, lapack_driver=svd_lapack_driver
        )
    del B
    U = Q @ Uhat

    if flip_sign:
        if not transpose:
            U, Vt = svd_flip(U, Vt)
        else:
            # In case of transpose u_based_decision=false
            # to actually flip based on u and not v.

        # transpose back the results according to the input convention
        return Vt[:n_components, :].T, s[:n_components], U[:, :n_components].T
    else:
        return U[:, :n_components], s[:n_components], Vt[:n_components, :]

def _randomized_eigsh(
    M,
    *,
    selection="module",
):
    """Computes a truncated eigendecomposition using randomized methods

    This method solves the fixed-rank approximation problem described in the
    Halko et al paper.

    The choice of which components to select can be tuned with the `selection`

    M : ndarray or sparse matrix
        Matrix to decompose, it should be real symmetric square or complex
        hermitian

        Number of eigenvalues and vectors to extract.

        Additional number of random vectors to sample the range of M so as
        used to find the range of M is n_components + n_oversamples. Smaller
        approximation of eigenvectors and eigenvalues. Users might wish
        slowly decaying spectrums, or to increase precision accuracy. See Halko
        et al (pages 5, 23 and 26).

        (see Halko et al paper, page 9).

    selection : {'value', 'module'}, default='module'
        Strategy used to select the n components. When `selection` is `'value'`
        (not yet implemented, will become the default when implemented), the
        components corresponding to the n largest eigenvalues are returned.
        When `selection` is `'module'`, the components corresponding to the n
        eigenvalues with largest modules are returned.

    Notes
    -----
    eigendecomposition using randomized methods to speed up the computations.

    This method is particularly fast on large matrices on which

    Strategy 'value': not implemented yet.
    Algorithms 5.3, 5.4 and 5.5 in the Halko et al paper should provide good
    candidates for a future implementation.

    Strategy 'module':
    The principle is that for diagonalizable matrices, the singular values and
    eigenvalues are related: if t is an eigenvalue of A, then :math:`|t|` is a
    singular value of A. This method relies on a randomized SVD to find the n
    singular components corresponding to the n singular values with largest
    modules, and then uses the signs of the singular vectors to find the true
    sign of t: if the sign of left and right singular vectors are different
    then the corresponding eigenvalue is negative.

    Returns
    -------
    eigvals : 1D array of shape (n_components,) containing the `n_components`
        eigenvalues selected (see ``selection`` parameter).
    eigvecs : 2D array of shape (M.shape[0], n_components) containing the
        `n_components` eigenvectors corresponding to the `eigvals`, in the
        corresponding order. Note that this follows the `scipy.linalg.eigh`
        convention.

    See Also
    --------
    :func:`randomized_svd`

    * :arxiv:`"Finding structure with randomness:
      (Algorithm 4.3 for strategy 'module') <0909.4061>`
    """
    if selection == "value":  # pragma: no cover
        # to do : an algorithm can be found in the Halko et al reference

    elif selection == "module":
        # Note: no need for deterministic U and Vt (flip_sign=True),
        # as we only use the dot product UVt afterwards
        U, S, Vt = randomized_svd(
            M,
            n_oversamples=n_oversamples,
            power_iteration_normalizer=power_iteration_normalizer,
            flip_sign=False,
        )

        eigvecs = U[:, :n_components]
        eigvals = S[:n_components]

        # Conversion of Singular values into Eigenvalues:
        # For any eigenvalue t, the corresponding singular value is |t|.
        # So if there is a negative eigenvalue t, the corresponding singular
        # value will be -t, and the left (U) and right (V) singular vectors
        # will have opposite signs.
        # Fastest way: see <https://stackoverflow.com/a/61974002/7262247>
        diag_VtU = np.einsum("ji,ij->j", Vt[:n_components, :], U[:, :n_components])
        signs = np.sign(diag_VtU)
        eigvals = eigvals * signs

    else:  # pragma: no cover
        raise ValueError("Invalid `selection`: %r" % selection)

    return eigvals, eigvecs

def weighted_mode(a, w, *, axis=0):
    """Return an array of the weighted modal (most common) value in the passed array.

    If there is more than one such value, only the first is returned.
    The bin-count for the modal bins is also returned.

    This is an extension of the algorithm in scipy.stats.mode.

    a : array-like of shape (n_samples,)
        Array of which values to find mode(s).
    w : array-like of shape (n_samples,)
        Array of weights for each value.
    axis : int, default=0
        Axis along which to operate. Default is 0, i.e. the first axis.

    Returns
    -------
    vals : ndarray
        Array of modal values.
    score : ndarray
        Array of weighted counts for each mode.

    See Also
    --------
    scipy.stats.mode: Calculates the Modal (most common) value of array elements
        along specified axis.

    Examples
    --------
    >>> from sklearn.utils.extmath import weighted_mode
    >>> x = [4, 1, 4, 2, 4, 2]
    >>> weights = [1, 1, 1, 1, 1, 1]
    >>> weighted_mode(x, weights)
    (array([4.]), array([3.]))

    The value 4 appears three times: with uniform weights, the result is
    simply the mode of the distribution.

    >>> weights = [1, 3, 0.5, 1.5, 1, 2]  # deweight the 4's
    (array([2.]), array([3.5]))

    The value 2 has the highest score: it appears twice with weights of
    1.5 and 2: the sum of these is 3.5.
    """
        a = np.ravel(a)
        w = np.ravel(w)
        axis = 0
    else:
        a = np.asarray(a)
        w = np.asarray(w)

    if a.shape != w.shape:
        w = np.full(a.shape, w, dtype=w.dtype)

    scores = np.unique(np.ravel(a))  # get ALL unique values
    testshape = list(a.shape)
    testshape[axis] = 1
    oldmostfreq = np.zeros(testshape)
    oldcounts = np.zeros(testshape)
    for score in scores:
        template = np.zeros(a.shape)
        ind = a == score
        template[ind] = w[ind]
        counts = np.expand_dims(np.sum(template, axis), axis)
        mostfrequent = np.where(counts > oldcounts, score, oldmostfreq)
        oldcounts = np.maximum(counts, oldcounts)
        oldmostfreq = mostfrequent
    return mostfrequent, oldcounts

def cartesian(arrays, out=None):
    """Generate a cartesian product of input arrays.

    arrays : list of array-like
        1-D arrays to form the cartesian product of.
    out : ndarray of shape (M, len(arrays)), default=None
        Array to place the cartesian product in.

    Returns
    -------
    out : ndarray of shape (M, len(arrays))
        Array containing the cartesian products formed of input arrays.
        If not provided, the `dtype` of the output array is set to the most
        permissive `dtype` of the input arrays, according to NumPy type
        promotion.

           Add support for arrays of different types.

    Notes
    -----
    This function may not be used on more than 32 arrays
    because the underlying numpy functions do not support it.

    Examples
    --------
    >>> from sklearn.utils.extmath import cartesian
    >>> cartesian(([1, 2, 3], [4, 5], [6, 7]))
    array([[1, 4, 6],
           [1, 4, 7],
           [1, 5, 6],
           [1, 5, 7],
           [2, 4, 6],
           [2, 4, 7],
           [2, 5, 6],
           [2, 5, 7],
           [3, 4, 6],
           [3, 4, 7],
           [3, 5, 6],
           [3, 5, 7]])
    """
    arrays = [np.asarray(x) for x in arrays]
    shape = (len(x) for x in arrays)

    ix = np.indices(shape)
    ix = ix.reshape(len(arrays), -1).T

        dtype = np.result_type(*arrays)  # find the most permissive dtype
        out = np.empty_like(ix, dtype=dtype)

    for n, arr in enumerate(arrays):
        out[:, n] = arrays[n][ix[:, n]]

    return out

def svd_flip(u, v, u_based_decision=True):
    """Sign correction to ensure deterministic output from SVD.

    Adjusts the columns of u and the rows of v such that the loadings in the
    columns in u that are largest in absolute value are always positive.

    If u_based_decision is False, then the same sign correction is applied to
    so that the rows in v that are largest in absolute value are always
    positive.

    u : ndarray
        Parameters u and v are the output of `linalg.svd` or
        :func:`~sklearn.utils.extmath.randomized_svd`, with matching inner
        dimensions so one can compute `np.dot(u * s, v)`.
        u can be None if `u_based_decision` is False.

    v : ndarray
        dimensions so one can compute `np.dot(u * s, v)`. The input v should
        really be called vt to be consistent with scipy's output.
        v can be None if `u_based_decision` is True.

    u_based_decision : bool, default=True
        If True, use the columns of u as the basis for sign flipping.
        Otherwise, use the rows of v. The choice of which variable to base the
        decision on is generally algorithm dependent.

    Returns
    -------
    u_adjusted : ndarray
        Array u with adjusted columns and the same dimensions as u.

    v_adjusted : ndarray
        Array v with adjusted rows and the same dimensions as v.
    """
    xp, _ = get_namespace(*[a for a in [u, v] if a is not None])

    if u_based_decision:
        # columns of u, rows of v, or equivalently rows of u.T and v
        max_abs_u_cols = xp.argmax(xp.abs(u.T), axis=1)
        shift = xp.arange(u.T.shape[0], device=device(u))
        indices = max_abs_u_cols + shift * u.T.shape[1]
        signs = xp.sign(xp.take(xp.reshape(u.T, (-1,)), indices, axis=0))
        u *= signs[np.newaxis, :]
        if v is not None:
            v *= signs[:, np.newaxis]
    else:
        # rows of v, columns of u
        max_abs_v_rows = xp.argmax(xp.abs(v), axis=1)
        shift = xp.arange(v.shape[0], device=device(v))
        indices = max_abs_v_rows + shift * v.shape[1]
        signs = xp.sign(xp.take(xp.reshape(v, (-1,)), indices, axis=0))
        if u is not None:
            u *= signs[np.newaxis, :]
        v *= signs[:, np.newaxis]
    return u, v

def softmax(X, copy=True):
    """
    Calculate the softmax function.

    The softmax function is calculated by
    np.exp(X) / np.sum(np.exp(X), axis=1)

    This will cause overflow when large values are exponentiated.
    Hence the largest value in each row is subtracted from each data
    point to prevent this.

    X : array-like of float of shape (M, N)
        Argument to the logistic function.

        Copy X or not.

    Returns
    -------
    out : ndarray of shape (M, N)
        Softmax function evaluated at every point in x.
    """
    if copy:
        X = xp.asarray(X, copy=True)
    max_prob = xp.reshape(xp.max(X, axis=1), (-1, 1))
    X -= max_prob

        # optimization for NumPy arrays
        np.exp(X, out=np.asarray(X))
    else:
        # array_api does not have `out=`
        X = xp.exp(X)

    sum_prob = xp.reshape(xp.sum(X, axis=1), (-1, 1))
    X /= sum_prob
    return X

def make_nonnegative(X, min_value=0):
    """Ensure `X.min()` >= `min_value`.

        The matrix to make non-negative.
    min_value : float, default=0
        The threshold value.

    Returns
    -------
        The thresholded array.

    Raises
    ------
        When X is sparse.
    """
    min_ = X.min()
    if min_ < min_value:
                "Cannot make the data matrix"
                " nonnegative because it is sparse."
                " Adding a value to every entry would"
                " make it no longer sparse."
            )
        X = X + (min_value - min_)
    return X

def _safe_accumulator_op(op, x, *args, **kwargs):
    """
    This function provides numpy accumulator functions with a float64 dtype
    when used on a floating point input. This prevents accumulator overflow on
    smaller floating point dtypes.

    op : function
        A numpy accumulator function such as np.mean or np.sum.
    x : ndarray
        A numpy array to apply the accumulator function.
    *args : positional arguments
        Positional arguments passed to the accumulator function after the
        input x.
    **kwargs : keyword arguments
        Keyword arguments passed to the accumulator function.

    Returns
    -------
    result
        The output of the accumulator function passed to this function.
    """
    if np.issubdtype(x.dtype, np.floating) and x.dtype.itemsize < 8:
        result = op(x, *args, **kwargs, dtype=np.float64)
    else:
        result = op(x, *args, **kwargs)

def _incremental_mean_and_var(
    X, last_mean, last_variance, last_sample_count, sample_weight=None
):
    """Calculate mean update and a Youngs and Cramer variance update.

    If sample_weight is given, the weighted mean and variance is computed.

    Update a given mean and (possibly) variance according to new data given
    in X. last_mean is always required to compute the new mean.
    If last_variance is None, no variance is computed and None return for
    updated_variance.

    From the paper "Algorithms for computing the sample variance: analysis and
    recommendations", by Chan, Golub, and LeVeque.

        Data to use for variance update.

    last_mean : array-like of shape (n_features,)

    last_variance : array-like of shape (n_features,)

    last_sample_count : array-like of shape (n_features,)
        The number of samples encountered until now if sample_weight is None.
        If sample_weight is not None, this is the sum of sample_weight
        encountered.

        Sample weights. If None, compute the unweighted mean/variance.

    Returns
    -------
    updated_mean : ndarray of shape (n_features,)

    updated_variance : ndarray of shape (n_features,)
        None if last_variance was None.

    updated_sample_count : ndarray of shape (n_features,)

    Notes
    -----
    NaNs are ignored during the algorithm.

    T. Chan, G. Golub, R. LeVeque. Algorithms for computing the sample
        variance: recommendations, The American Statistician, Vol. 37, No. 3,
        pp. 242-247

    Also, see the sparse implementation of this in
    `utils.sparsefuncs.incr_mean_variance_axis` and
    `utils.sparsefuncs_fast.incr_mean_variance_axis0`
    """
    # old = stats until now
    # new = the current increment
    # updated = the aggregated stats
    last_sum = last_mean * last_sample_count
    X_nan_mask = np.isnan(X)
    if np.any(X_nan_mask):
        sum_op = np.nansum
    else:
        sum_op = np.sum
        # equivalent to np.nansum(X * sample_weight, axis=0)
        # safer because np.float64(X*W) != np.float64(X)*np.float64(W)
        new_sum = _safe_accumulator_op(
            np.matmul, sample_weight, np.where(X_nan_mask, 0, X)
        )
        new_sample_count = _safe_accumulator_op(
            np.sum, sample_weight[:, None] * (~X_nan_mask), axis=0
        )
    else:
        new_sum = _safe_accumulator_op(sum_op, X, axis=0)
        new_sample_count = n_samples - np.sum(X_nan_mask, axis=0)

    updated_sample_count = last_sample_count + new_sample_count

    updated_mean = (last_sum + new_sum) / updated_sample_count

    if last_variance is None:
        updated_variance = None
    else:
        T = new_sum / new_sample_count
        temp = X - T
            # equivalent to np.nansum((X-T)**2 * sample_weight, axis=0)
            # safer because np.float64(X*W) != np.float64(X)*np.float64(W)
            correction = _safe_accumulator_op(
                np.matmul, sample_weight, np.where(X_nan_mask, 0, temp)
            )
            temp **= 2
            new_unnormalized_variance = _safe_accumulator_op(
            )
        else:
            correction = _safe_accumulator_op(sum_op, temp, axis=0)
            temp **= 2
            new_unnormalized_variance = _safe_accumulator_op(sum_op, temp, axis=0)

        # correction term of the corrected 2 pass algorithm.
        # See "Algorithms for computing the sample variance: analysis
        # and recommendations", by Chan, Golub, and LeVeque.
        new_unnormalized_variance -= correction**2 / new_sample_count

        last_unnormalized_variance = last_variance * last_sample_count

        with np.errstate(divide="ignore", invalid="ignore"):
            last_over_new_count = last_sample_count / new_sample_count
            updated_unnormalized_variance = (
                last_unnormalized_variance
                + new_unnormalized_variance
                + last_over_new_count
                / updated_sample_count
                * (last_sum / last_over_new_count - new_sum) ** 2
            )

        zeros = last_sample_count == 0
        updated_unnormalized_variance[zeros] = new_unnormalized_variance[zeros]
        updated_variance = updated_unnormalized_variance / updated_sample_count

    return updated_mean, updated_variance, updated_sample_count

def _deterministic_vector_sign_flip(u):
    """Modify the sign of vectors for reproducibility.

    Flips the sign of elements of all the vectors (rows of u) such that
    the absolute maximum element of each vector is positive.

    u : ndarray
        Array with vectors as its rows.

    Returns
    -------
    u_flipped : ndarray with same shape as u
        Array with the sign flipped vectors as its rows.
    """
    max_abs_rows = np.argmax(np.abs(u), axis=1)
    signs = np.sign(u[range(u.shape[0]), max_abs_rows])
    u *= signs[:, np.newaxis]
    return u

def stable_cumsum(arr, axis=None, rtol=1e-05, atol=1e-08):
    """Use high precision for cumsum and check that final value matches sum.

    Warns if the final cumulative sum does not match the sum (up to the chosen
    tolerance).

    arr : array-like
        To be cumulatively summed as flat.
    axis : int, default=None
        Axis along which the cumulative sum is computed.
        The default (None) is to compute the cumsum over the flattened array.
    rtol : float, default=1e-05
        Relative tolerance, see ``np.allclose``.
    atol : float, default=1e-08
        Absolute tolerance, see ``np.allclose``.

    Returns
    -------
    out : ndarray
        Array with the cumulative sums along the chosen axis.
    """
    out = np.cumsum(arr, axis=axis, dtype=np.float64)
    expected = np.sum(arr, axis=axis, dtype=np.float64)
    if not np.allclose(
        out.take(-1, axis=axis), expected, rtol=rtol, atol=atol, equal_nan=True
    ):
            (
                "cumsum was found to be unstable: "
                "its last element does not correspond to sum"
            ),
        )
    return out

def _nanaverage(a, weights=None):
    """Compute the weighted average, ignoring NaNs.

    a : ndarray
        Array containing data to be averaged.
    weights : array-like, default=None
        An array of weights associated with the values in a. Each value in a
        contributes to the average according to its associated weight. The
        weights array can either be 1-D of the same shape as a. If `weights=None`,
        then all data in a are assumed to have a weight equal to one.

    Returns
    -------
    weighted_average : float
        The weighted average.

    Notes
    -----
    This wrapper to combine :func:`numpy.average` and :func:`numpy.nanmean`, so
    that :func:`np.nan` values are ignored from the average and weights can
    be passed. Note that when possible, we delegate to the prime methods.
    """
    xp, _ = get_namespace(a)
    if a.shape[0] == 0:
        return xp.nan

    mask = xp.isnan(a)
    if xp.all(mask):

        return _nanmean(a, xp=xp)

    weights = xp.asarray(weights)
    a, weights = a[~mask], weights[~mask]
    try:
        return _average(a, weights=weights)
    except ZeroDivisionError:
        # this is when all weights are zero, then ignore them
        return _average(a)

def safe_sqr(X, *, copy=True):
    """Element wise squaring of array-likes and sparse matrices.

    X : {array-like, ndarray, sparse matrix}

        Whether to create a copy of X and operate on it or to perform
        inplace computation (default behaviour).

    Returns
    -------
    X ** 2 : element wise square
         Return the element-wise square of the input.

    Examples
    --------
    >>> from sklearn.utils import safe_sqr
    >>> safe_sqr([1, 2, 3])
    array([1, 4, 9])
    """
    X = check_array(X, accept_sparse=["csr", "csc", "coo"], ensure_2d=False)
        if copy:
            X = X.copy()
        X.data **= 2
    else:
        if copy:
            X = X**2
        else:
            X **= 2
    return X

def _approximate_mode(class_counts, n_draws, rng):
    """Computes approximate mode of multivariate hypergeometric.

    This is an approximation to the mode of the multivariate
    hypergeometric given by class_counts and n_draws.
    It shouldn't be off by more than one.

    It is the mostly likely outcome of drawing n_draws many
    samples from the population given by class_counts.

    class_counts : ndarray of int
        Population per class.
    n_draws : int
        Number of draws (samples to draw) from the overall population.
    rng : random state
        Used to break ties.

    Returns
    -------
    sampled_classes : ndarray of int
        Number of samples drawn from each class.
        np.sum(sampled_classes) == n_draws

    Examples
    --------
    >>> from sklearn.utils.extmath import _approximate_mode
    >>> _approximate_mode(class_counts=np.array([4, 2]), n_draws=3, rng=0)
    array([2, 1])
    >>> _approximate_mode(class_counts=np.array([5, 2]), n_draws=4, rng=0)
    array([3, 1])
    >>> _approximate_mode(class_counts=np.array([2, 2, 2, 1]),
    ...                   n_draws=2, rng=0)
    array([0, 1, 1, 0])
    ...                   n_draws=2, rng=42)
    array([1, 1, 0, 0])
    """
    rng = check_random_state(rng)
    # this computes a bad approximation to the mode of the
    # multivariate hypergeometric given by class_counts and n_draws
    continuous = class_counts / class_counts.sum() * n_draws
    # floored means we don't overshoot n_samples, but probably undershoot
    floored = np.floor(continuous)
    # we add samples according to how much "left over" probability
    # they had, until we arrive at n_samples
    need_to_add = int(n_draws - floored.sum())
    if need_to_add > 0:
        remainder = continuous - floored
        values = np.sort(np.unique(remainder))[::-1]
        # add according to remainder, but break ties
        # randomly to avoid biases
        for value in values:
            (inds,) = np.where(remainder == value)
            # if we need_to_add less than what's in inds
            # we draw randomly from them.
            # if we need to add more, we add them all and
            # go to the next value
            add_now = min(len(inds), need_to_add)
            inds = rng.choice(inds, size=add_now, replace=False)
            floored[inds] += 1
            need_to_add -= add_now
            if need_to_add == 0:
                break
    return floored.astype(int)

def _with_config_and_warning_filters(delayed_func, config, warning_filters):
    """Helper function that intends to attach a config to a delayed function."""
    if hasattr(delayed_func, "with_config_and_warning_filters"):
        return delayed_func.with_config_and_warning_filters(config, warning_filters)
    else:
            (
                "`sklearn.utils.parallel.Parallel` needs to be used in "
                "conjunction with `sklearn.utils.parallel.delayed` instead of "
                "`joblib.delayed` to correctly propagate the scikit-learn "
                "configuration to the joblib workers."
            ),
        )
        return delayed_func

class Parallel(joblib.Parallel):
    """Tweak of :class:`joblib.Parallel` that propagates the scikit-learn configuration.

    This subclass of :class:`joblib.Parallel` ensures that the active configuration
    (thread-local) of scikit-learn is propagated to the parallel workers for the
    duration of the execution of the parallel tasks.

    The API does not change and you can refer to :class:`joblib.Parallel`
    documentation for more details.

    """

    def __call__(self, iterable):
        """Dispatch the tasks and return the results.

        iterable : iterable
            Iterable containing tuples of (delayed_function, args, kwargs) that should
            be consumed.

        Returns
        -------
        results : list
            List of results of the tasks.
        """
        # Capture the thread-local scikit-learn configuration at the time
        # Parallel.__call__ is issued since the tasks can be dispatched
        # in a different thread depending on the backend and on the value of
        # pre_dispatch and n_jobs.
        config = get_config()
        warning_filters = warnings.filters
        iterable_with_config_and_warning_filters = (
            (
                _with_config_and_warning_filters(delayed_func, config, warning_filters),
                args,
                kwargs,
            )
            for delayed_func, args, kwargs in iterable
        )
        return super().__call__(iterable_with_config_and_warning_filters)

def delayed(function):
    """Decorator used to capture the arguments of a function.

    This alternative to `joblib.delayed` is meant to be used in conjunction
    with `sklearn.utils.parallel.Parallel`. The latter captures the scikit-
    learn configuration by calling `sklearn.get_config()` in the current
    thread, prior to dispatching the first task. The captured configuration is
    then propagated and enabled for the duration of the execution of the
    delayed function in the joblib workers.

       `delayed` was moved from `sklearn.utils.fixes` to `sklearn.utils.parallel`
       in scikit-learn 1.3.

    function : callable
        The function to be delayed.

    Returns
    -------
    output: tuple
        Tuple containing the delayed function, the positional arguments, and the
        keyword arguments.
    """

    @functools.wraps(function)
    def delayed_function(*args, **kwargs):
        return _FuncWrapper(function), args, kwargs

    return delayed_function

class _FuncWrapper:
    """Load the global configuration before calling the function."""

    def __init__(self, function):
        self.function = function
        update_wrapper(self, self.function)

    def with_config_and_warning_filters(self, config, warning_filters):
        self.config = config
        self.warning_filters = warning_filters

    def __call__(self, *args, **kwargs):
        config = getattr(self, "config", {})
        warning_filters = getattr(self, "warning_filters", [])
        if not config or not warning_filters:
                (
                    "`sklearn.utils.parallel.delayed` should be used with"
                    " `sklearn.utils.parallel.Parallel` to make it possible to"
                    " propagate the scikit-learn configuration of the current thread to"
                    " the joblib workers."
                ),
            )

        with config_context(**config), warnings.catch_warnings():
            warnings.filters = warning_filters
            return self.function(*args, **kwargs)

def _get_threadpool_controller():
    """Return the global threadpool controller instance."""
    global _threadpool_controller

    if _threadpool_controller is None:
        _threadpool_controller = ThreadpoolController()

    return _threadpool_controller

def _threadpool_controller_decorator(limits=1, user_api="blas"):
    """Decorator to limit the number of threads used at the function level.

    It should be preferred over `threadpoolctl.ThreadpoolController.wrap` because this
    one only loads the shared libraries when the function is called while the latter
    loads them at import time.
    """

            controller = _get_threadpool_controller()
            with controller.limit(limits=limits, user_api=user_api):

        Returns
        -------
        """
            (
                args,
                kwargs,
            )
        )

                (
                ),
            )

def _random_choice_csc(n_samples, classes, class_probability=None, random_state=None):
    """Generate a sparse random matrix given column class distributions

    n_samples : int,
        Number of samples to draw in each column.

    classes : list of size n_outputs of arrays of size (n_classes,)

    class_probability : list of size n_outputs of arrays of \
        shape (n_classes,), default=None
        Class distribution of each column. If None, uniform distribution is
        assumed.

        Controls the randomness of the sampled classes.

    Returns
    -------
    random_matrix : sparse csc matrix of size (n_samples, n_outputs)

    """
    data = array.array("i")
    indices = array.array("i")
    indptr = array.array("i", [0])

    for j in range(len(classes)):
        classes[j] = np.asarray(classes[j])
        if classes[j].dtype.kind != "i":
            raise ValueError("class dtype %s is not supported" % classes[j].dtype)
        classes[j] = classes[j].astype(np.int64, copy=False)

        # use uniform distribution if no class_probability is given
        if class_probability is None:
            class_prob_j = np.empty(shape=classes[j].shape[0])
            class_prob_j.fill(1 / classes[j].shape[0])
        else:
            class_prob_j = np.asarray(class_probability[j])

        if not np.isclose(np.sum(class_prob_j), 1.0):
                "Probability array at index {0} does not sum to one".format(j)
            )

        if class_prob_j.shape[0] != classes[j].shape[0]:
                "classes[{0}] (length {1}) and "
                "class_probability[{0}] (length {2}) have "
                "different length.".format(
                    j, classes[j].shape[0], class_prob_j.shape[0]
                )
            )

        # If 0 is not present in the classes insert it with a probability 0.0
        if 0 not in classes[j]:
            classes[j] = np.insert(classes[j], 0, 0)
            class_prob_j = np.insert(class_prob_j, 0, 0.0)

        # If there are nonzero classes choose randomly using class_probability
        if classes[j].shape[0] > 1:
            index_class_0 = np.flatnonzero(classes[j] == 0).item()
            p_nonzero = 1 - class_prob_j[index_class_0]
            nnz = int(n_samples * p_nonzero)
            ind_sample = sample_without_replacement(
                n_population=n_samples, n_samples=nnz, random_state=random_state
            )
            indices.extend(ind_sample)

            # Normalize probabilities for the nonzero elements
            classes_j_nonzero = classes[j] != 0
            class_probability_nz = class_prob_j[classes_j_nonzero]
            class_probability_nz_norm = class_probability_nz / np.sum(
                class_probability_nz
            )
            classes_ind = np.searchsorted(
                class_probability_nz_norm.cumsum(), rng.uniform(size=nnz)
            )
            data.extend(classes[j][classes_j_nonzero][classes_ind])
        indptr.append(len(indices))

    return sp.csc_matrix((data, indices, indptr), (n_samples, len(classes)), dtype=int)

def single_source_shortest_path_length(graph, source, *, cutoff=None):
    """Return the length of the shortest path from source to all reachable nodes.

    graph : {array-like, sparse matrix} of shape (n_nodes, n_nodes)
        Adjacency matrix of the graph. Sparse matrix of format LIL is
        preferred.

    source : int
       Start node for path.

    cutoff : int, default=None
        Depth to stop the search - only paths of length <= cutoff are returned.

    Returns
    -------
    paths : dict
        Reachable end nodes mapped to length of path from source,
        i.e. `{end: path_length}`.

    Examples
    --------
    >>> from sklearn.utils.graph import single_source_shortest_path_length
    >>> graph = np.array([[ 0, 1, 0, 0],
    ...                   [ 1, 0, 1, 0],
    ...                   [ 0, 1, 0, 0],
    ...                   [ 0, 0, 0, 0]])
    >>> single_source_shortest_path_length(graph, 0)
    {0: 0, 1: 1, 2: 2}
    >>> graph = np.ones((6, 6))
    >>> sorted(single_source_shortest_path_length(graph, 2).items())
    [(0, 1), (1, 1), (2, 0), (3, 1), (4, 1), (5, 1)]
    """
        graph = graph.tolil()
    else:
        graph = sparse.lil_matrix(graph)
    seen = {}  # level (number of hops) when seen in BFS
    level = 0  # the current level
    next_level = [source]  # dict of nodes to check at next level
    while next_level:
        this_level = next_level  # advance to next level
        next_level = set()  # and start a new list (fringe)
        for v in this_level:
            if v not in seen:
                seen[v] = level  # set the level of vertex v
                next_level.update(graph.rows[v])
        if cutoff is not None and cutoff <= level:
            break
        level += 1
    return seen  # return all path lengths as dictionary

def _fix_connected_components(
    X,
    graph,
    component_labels,
    mode="distance",
    **kwargs,
):
    """Add connections to sparse graph to connect unconnected components.

    For each pair of unconnected components, compute all pairwise distances
    from one component to the other, and add a connection on the closest pair
    of samples. This is a hacky way to get a graph with a single connected
    component, which is necessary for example to compute a shortest path
    between all pairs of samples in the graph.

    X : array of shape (n_samples, n_features) or (n_samples, n_samples)
        Features to compute the pairwise distances. If `metric =
        "precomputed"`, X is the matrix of pairwise distances.

        Graph of connection between samples.

        Number of connected components, as computed by
        `scipy.sparse.csgraph.connected_components`.

    component_labels : array of shape (n_samples)
        Labels of connected components, as computed by

    mode : {'connectivity', 'distance'}, default='distance'
        Type of graph matrix: 'connectivity' corresponds to the connectivity
        matrix with ones and zeros, and 'distance' corresponds to the distances

    metric : str
        Metric used in `sklearn.metrics.pairwise.pairwise_distances`.

    kwargs : kwargs
        Keyword arguments passed to
        `sklearn.metrics.pairwise.pairwise_distances`.

    Returns
    -------
        Graph of connection between samples, with a single connected component.
    """
    if metric == "precomputed" and sparse.issparse(X):
            "_fix_connected_components with metric='precomputed' requires the "
            "full distance matrix in X, and does not work with a sparse "
            "neighbors graph."
        )

    for i in range(n_connected_components):
        idx_i = np.flatnonzero(component_labels == i)
        Xi = X[idx_i]
        for j in range(i):
            idx_j = np.flatnonzero(component_labels == j)
            Xj = X[idx_j]

            if metric == "precomputed":
                D = X[np.ix_(idx_i, idx_j)]
            else:
                D = pairwise_distances(Xi, Xj, metric=metric, **kwargs)

            ii, jj = np.unravel_index(D.argmin(axis=None), D.shape)
            if mode == "connectivity":
                graph[idx_i[ii], idx_j[jj]] = 1
                graph[idx_j[jj], idx_i[ii]] = 1
            elif mode == "distance":
                graph[idx_i[ii], idx_j[jj]] = D[ii, jj]
                graph[idx_j[jj], idx_i[ii]] = D[ii, jj]
            else:
                    "Unknown mode=%r, should be one of ['connectivity', 'distance']."
                    % mode
                )

def _weighted_percentile(array, sample_weight, percentile_rank=50, xp=None):
    """Compute the weighted percentile with method 'inverted_cdf'.

    When the percentile lies between two data points of `array`, the function returns
    the lower value.

    If `array` is a 2D array, the `values` are selected along axis 0.

    `NaN` values are ignored by setting their weights to 0. If `array` is 2D, this
    is done in a column-isolated manner: a `NaN` in the second column, does not impact
    the percentile computed for the first column even if `sample_weight` is 1D.

            Accepts 2D `array`.

            Supports handling of `NaN` values.

    array : 1D or 2D array
        Values to take the weighted percentile of.

    sample_weight: 1D or 2D array
        Weights for each value in `array`. Must be same shape as `array` or of shape
        `(array.shape[0],)`.

    percentile_rank: int or float, default=50
        The probability level of the percentile to compute, in percent. Must be between
        0 and 100.

    xp : array_namespace, default=None
        The standard-compatible namespace for `array`. Default: infer.

    Returns
    -------
    percentile : scalar or 0D array if `array` 1D (or 0D), array if `array` 2D
        Weighted percentile at the requested probability level.
    """
    xp, _, device = get_namespace_and_device(array)
    # `sample_weight` should follow `array` for dtypes
    floating_dtype = _find_matching_floating_dtype(array, xp=xp)
    array = xp.asarray(array, dtype=floating_dtype, device=device)
    sample_weight = xp.asarray(sample_weight, dtype=floating_dtype, device=device)

    n_dim = array.ndim
    if n_dim == 0:
        return array
    if array.ndim == 1:
        array = xp.reshape(array, (-1, 1))
    # When sample_weight 1D, repeat for each array.shape[1]
    if array.shape != sample_weight.shape and array.shape[0] == sample_weight.shape[0]:
        sample_weight = xp.tile(sample_weight, (array.shape[1], 1)).T
    # Sort `array` and `sample_weight` along axis=0:
    sorted_idx = xp.argsort(array, axis=0)
    sorted_weights = xp.take_along_axis(sample_weight, sorted_idx, axis=0)

    # Set NaN values in `sample_weight` to 0. Only perform this operation if NaN
    # values present to avoid temporary allocations of size `(n_samples, n_features)`.
    n_features = array.shape[1]
    largest_value_per_column = array[
        sorted_idx[-1, ...], xp.arange(n_features, device=device)
    ]
    # NaN values get sorted to end (largest value)
    if xp.any(xp.isnan(largest_value_per_column)):
        sorted_nan_mask = xp.take_along_axis(xp.isnan(array), sorted_idx, axis=0)
        sorted_weights[sorted_nan_mask] = 0

    # Compute the weighted cumulative distribution function (CDF) based on
    # `sample_weight` and scale `percentile_rank` along it.
    #
    # Note: we call `xp.cumulative_sum` on the transposed `sorted_weights` to
    # ensure that the result is of shape `(n_features, n_samples)` so
    # `xp.searchsorted` calls take contiguous inputs as a result (for
    # performance reasons).
    weight_cdf = xp.cumulative_sum(sorted_weights.T, axis=1)
    adjusted_percentile_rank = percentile_rank / 100 * weight_cdf[..., -1]

    # Ignore leading `sample_weight=0` observations when `percentile_rank=0` (#20528)
    mask = adjusted_percentile_rank == 0
    adjusted_percentile_rank[mask] = xp.nextafter(
        adjusted_percentile_rank[mask], adjusted_percentile_rank[mask] + 1
    )
    # For each feature with index j, find sample index i of the scalar value
    # `adjusted_percentile_rank[j]` in 1D array `weight_cdf[j]`, such that:
    # weight_cdf[j, i-1] < adjusted_percentile_rank[j] <= weight_cdf[j, i].
    percentile_indices = xp.stack(
        [
            xp.searchsorted(
                weight_cdf[feature_idx, ...], adjusted_percentile_rank[feature_idx]
            )
            for feature_idx in range(weight_cdf.shape[0])
        ],
    )
    # In rare cases, `percentile_indices` equals to `sorted_idx.shape[0]`
    max_idx = sorted_idx.shape[0] - 1
    percentile_indices = xp.clip(percentile_indices, 0, max_idx)

    col_indices = xp.arange(array.shape[1], device=device)
    percentile_in_sorted = sorted_idx[percentile_indices, col_indices]

    result = array[percentile_in_sorted, col_indices]

    return result[0] if n_dim == 1 else result

def _averaged_weighted_percentile(array, sample_weight, percentile_rank=50, xp=None):
    return (
        _weighted_percentile(array, sample_weight, percentile_rank, xp=xp)
        - _weighted_percentile(-array, sample_weight, 100 - percentile_rank, xp=xp)
    ) / 2

def check_matplotlib_support(caller_name):
    """Raise ImportError with detailed error message if mpl is not installed.

    Plot utilities like any of the Display's plotting functions should lazily import
    matplotlib and call this helper before any computation.

        The name of the caller that requires matplotlib.
    """
    try:
    except ImportError as e:
            "{} requires matplotlib. You can install matplotlib with "
            "`pip install matplotlib`".format(caller_name)
        ) from e

def check_pandas_support(caller_name):
    """Raise ImportError with detailed error message if pandas is not installed.

    Plot utilities like :func:`fetch_openml` should lazily import
    pandas and call this helper before any computation.

        The name of the caller that requires pandas.

    Returns
    -------
    pandas
        The pandas package.
    """
    try:

        return pandas
        raise ImportError("{} requires pandas.".format(caller_name)) from e

def chunk_generator(gen, chunksize):
    """Chunk generator, ``gen`` into lists of length ``chunksize``. The last
    chunk may have a length less than ``chunksize``."""
        chunk = list(islice(gen, chunksize))
        if chunk:
            yield chunk
        else:
            return

def gen_batches(n, batch_size, *, min_batch_size=0):
    """Generator to create slices containing `batch_size` elements from 0 to `n`.

    The last slice may contain less than `batch_size` elements, when
    `batch_size` does not divide `n`.

    n : int
        Size of the sequence.
    batch_size : int
        Number of elements in each batch.
    min_batch_size : int, default=0
        Minimum number of elements in each batch.

    Yields
    ------
    slice of `batch_size` elements

    See Also
    --------
    gen_even_slices: Generator to create n_packs slices going up to n.

    Examples
    --------
    >>> from sklearn.utils import gen_batches
    >>> list(gen_batches(7, 3))
    [slice(0, 3, None), slice(3, 6, None), slice(6, 7, None)]
    >>> list(gen_batches(6, 3))
    [slice(0, 3, None), slice(3, 6, None)]
    >>> list(gen_batches(2, 3))
    [slice(0, 2, None)]
    >>> list(gen_batches(7, 3, min_batch_size=0))
    >>> list(gen_batches(7, 3, min_batch_size=2))
    [slice(0, 3, None), slice(3, 7, None)]
    """
    start = 0
    for _ in range(int(n // batch_size)):
        end = start + batch_size
        if end + min_batch_size > n:
            continue
        yield slice(start, end)
        start = end
    if start < n:
        yield slice(start, n)

def gen_even_slices(n, n_packs, *, n_samples=None):
    """Generator to create `n_packs` evenly spaced slices going up to `n`.

    If `n_packs` does not divide `n`, except for the first `n % n_packs`
    slices, remaining slices may contain fewer elements.

    n : int
    n_packs : int
        Number of slices to generate.
        Number of samples. Pass `n_samples` when the slices are to be used for
        sparse matrix indexing; slicing off-the-end raises an exception, while
        it works for NumPy arrays.

    Yields
    ------
    `slice` representing a set of indices from 0 to n.

    See Also
    --------
    gen_batches: Generator to create slices containing batch_size elements
        from 0 to n.

    Examples
    --------
    >>> from sklearn.utils import gen_even_slices
    >>> list(gen_even_slices(10, 1))
    [slice(0, 10, None)]
    >>> list(gen_even_slices(10, 10))
    [slice(0, 1, None), slice(1, 2, None), ..., slice(9, 10, None)]
    >>> list(gen_even_slices(10, 5))
    [slice(0, 2, None), slice(2, 4, None), ..., slice(8, 10, None)]
    >>> list(gen_even_slices(10, 3))
    [slice(0, 4, None), slice(4, 7, None), slice(7, 10, None)]
    """
    start = 0
    for pack_num in range(n_packs):
        this_n = n // n_packs
        if pack_num < n % n_packs:
            this_n += 1
        if this_n > 0:
            end = start + this_n
            if n_samples is not None:
                end = min(n_samples, end)
            yield slice(start, end, None)
            start = end

def get_chunk_n_rows(row_bytes, *, max_n_rows=None, working_memory=None):
    """Calculate how many rows can be processed within `working_memory`.

    row_bytes : int
        The expected number of bytes of memory that will be consumed
        during the processing of each row.
    max_n_rows : int, default=None
        The maximum return value.
    working_memory : int or float, default=None
        The number of rows to fit inside this number of MiB will be
        returned. When None (default), the value of

    Returns
    -------
    int
        The number of rows which can be processed within `working_memory`.

    Warns
    -----
    Issues a UserWarning if `row_bytes exceeds `working_memory` MiB.
    """

    if working_memory is None:
        working_memory = get_config()["working_memory"]

    chunk_n_rows = int(working_memory * (2**20) // row_bytes)
    if max_n_rows is not None:
        chunk_n_rows = min(chunk_n_rows, max_n_rows)
    if chunk_n_rows < 1:
            "Could not adhere to working_memory config. "
            "Currently %.0fMiB, %.0fMiB required."
            % (working_memory, np.ceil(row_bytes * 2**-20))
        )
        chunk_n_rows = 1
    return chunk_n_rows

def yield_namespaces(include_numpy_namespaces=True):
    """Yield supported namespace.

    This is meant to be used for testing purposes only.

    include_numpy_namespaces : bool, default=True
        If True, also yield numpy namespaces.

    Returns
    -------
    array_namespace : str
        The name of the Array API namespace.
    """
    for array_namespace in [
        # The following is used to test the array_api_compat wrapper when
        # array_api_dispatch is enabled: in particular, the arrays used in the
        # tests are regular numpy arrays without any "device" attribute.
        "numpy",
        # Stricter NumPy-based Array API implementation. The
        # array_api_strict.Array instances always have a dummy "device" attribute.
        "array_api_strict",
        "cupy",
        "torch",
    ]:
        if not include_numpy_namespaces and array_namespace in _NUMPY_NAMESPACE_NAMES:
            continue
        yield array_namespace

def yield_namespace_device_dtype_combinations(include_numpy_namespaces=True):
    """Yield supported namespace, device, dtype tuples for testing.

    Use this to test that an estimator works with all combinations.
    Use in conjunction with `ids=_get_namespace_device_dtype_ids` to give
    clearer pytest parametrization ID names.

    Returns
    -------

    device : str
        The name of the device on which to allocate the arrays. Can be None to
        indicate that the default value should be used.

    dtype_name : str
        The name of the data type to use for arrays. Can be None to indicate
        that the default value should be used.
    """
    for array_namespace in yield_namespaces(
        include_numpy_namespaces=include_numpy_namespaces
    ):
        if array_namespace == "torch":
            for device, dtype in itertools.product(
                ("cpu", "cuda"), ("float64", "float32")
            ):
                yield array_namespace, device, dtype
            yield array_namespace, "mps", "float32"

        elif array_namespace == "array_api_strict":
            try:
                import array_api_strict

                yield array_namespace, array_api_strict.Device("CPU_DEVICE"), "float64"
                yield array_namespace, array_api_strict.Device("device1"), "float32"
            except ImportError:
                # Those combinations will typically be skipped by pytest if
                # array_api_strict is not installed but we still need to see them in
                # the test output.
                yield array_namespace, "CPU_DEVICE", "float64"
                yield array_namespace, "device1", "float32"
        else:
            yield array_namespace, None, None

def _get_namespace_device_dtype_ids(param):
    """Get pytest parametrization IDs for `yield_namespace_device_dtype_combinations`"""
    # Gives clearer IDs for array-api-strict devices, see #31042 for details
    try:
        import array_api_strict
        # `None` results in the default pytest representation
    else:
        if param == array_api_strict.Device("CPU_DEVICE"):
            return "CPU_DEVICE"
        if param == array_api_strict.Device("device1"):
            return "device1"
        if param == array_api_strict.Device("device2"):
            return "device2"

def _check_array_api_dispatch(array_api_dispatch):
    """Check that array_api_compat is installed and NumPy version is compatible.

    array_api_compat follows NEP29, which has a higher minimum NumPy version than
    scikit-learn.
    """
    if not array_api_dispatch:
        return

    scipy_version = parse_version(scipy.__version__)
    min_scipy_version = "1.14.0"
    if scipy_version < parse_version(min_scipy_version):
            f"SciPy must be {min_scipy_version} or newer"
            " (found {scipy.__version__}) to dispatch array using"
            " the array API specification"
        )

    if os.environ.get("SCIPY_ARRAY_API") != "1":
            "Scikit-learn array API support was enabled but scipy's own support is "
            "not enabled. Please set the SCIPY_ARRAY_API=1 environment variable "
            "before importing sklearn or scipy. More details at: "
            "https://docs.scipy.org/doc/scipy/dev/api-dev/array_api.html"
        )

def _single_array_device(array):
    """Hardware device where the array data resides on."""
    if (
        isinstance(array, (numpy.ndarray, numpy.generic))
        or not hasattr(array, "device")
        # When array API dispatch is disabled, we expect the scikit-learn code
        # to use np.asarray so that the resulting NumPy array will implicitly use the
        # CPU. In this case, scikit-learn should stay as device neutral as possible,
        # hence the use of `device=None` which is accepted by all libraries, before
        # and after the expected conversion to NumPy via np.asarray.
        or not get_config()["array_api_dispatch"]
    ):
    else:
        return array.device

def device(*array_list, remove_none=True, remove_types=(str,)):
    """Hardware device where the array data resides on.

    If the hardware device is not the same for all arrays, an error is raised.

    *array_list : arrays
        List of array instances from NumPy or an array API compatible library.

    remove_none : bool, default=True
        Whether to ignore None objects passed in array_list.

    remove_types : tuple or list, default=(str,)
        Types to ignore in array_list.

    Returns
    -------
    out : device
        `device` object (see the "Device Support" section of the array API spec).
    """
    array_list = _remove_non_arrays(
        *array_list, remove_none=remove_none, remove_types=remove_types
    )

    if not array_list:

    device_ = _single_array_device(array_list[0])

    # Note: here we cannot simply use a Python `set` as it requires
    # hashable members which is not guaranteed for Array API device
    # objects. In particular, CuPy devices are not hashable at the
    # time of writing.
    for array in array_list[1:]:
        device_other = _single_array_device(array)
        if device_ != device_other:
                f"Input arrays use different devices: {device_}, {device_other}"
            )

    return device_

def size(x):
    """Return the total number of elements of x.

    x : array
        Array instance from NumPy or an array API compatible library.

    Returns
    -------
    out : int
        Total number of elements.
    """
    return math.prod(x.shape)

def _is_numpy_namespace(xp):
    """Return True if xp is backed by NumPy."""
    return xp.__name__ in _NUMPY_NAMESPACE_NAMES

def _union1d(a, b, xp):
        # avoid circular import
        from ._unique import cached_unique

        a_unique, b_unique = cached_unique(a, b, xp=xp)
        return xp.asarray(numpy.union1d(a_unique, b_unique))
    assert a.ndim == b.ndim == 1
    return xp.unique_values(xp.concat([xp.unique_values(a), xp.unique_values(b)]))

def isdtype(dtype, kind, *, xp):
    """Returns a boolean indicating whether a provided dtype is of type "kind".

    Included in the v2022.12 of the Array API spec.
    """
    if isinstance(kind, tuple):
        return any(_isdtype_single(dtype, k, xp=xp) for k in kind)
    else:
        return _isdtype_single(dtype, kind, xp=xp)

def _isdtype_single(dtype, kind, *, xp):
    if isinstance(kind, str):
            return dtype == xp.bool
        elif kind == "signed integer":
            return dtype in {xp.int8, xp.int16, xp.int32, xp.int64}
        elif kind == "unsigned integer":
            return dtype in {xp.uint8, xp.uint16, xp.uint32, xp.uint64}
        elif kind == "integral":
            return any(
                _isdtype_single(dtype, k, xp=xp)
                for k in ("signed integer", "unsigned integer")
            )
        elif kind == "real floating":
            return dtype in supported_float_dtypes(xp)
        elif kind == "complex floating":
            # Some name spaces might not have support for complex dtypes.
            complex_dtypes = set()
            if hasattr(xp, "complex64"):
                complex_dtypes.add(xp.complex64)
            if hasattr(xp, "complex128"):
                complex_dtypes.add(xp.complex128)
            return dtype in complex_dtypes
        elif kind == "numeric":
                for k in ("integral", "real floating", "complex floating")
            )
        else:
    else:

def supported_float_dtypes(xp):
    """Supported floating point types for the namespace.

    Note: float16 is not officially part of the Array API spec at the
    time of writing but scikit-learn estimators and functions can choose
    to accept it when xp.float16 is defined.

    https://data-apis.org/array-api/latest/API_specification/data_types.html
    """
    if hasattr(xp, "float16"):
        return (xp.float64, xp.float32, xp.float16)
    else:
        return (xp.float64, xp.float32)

def ensure_common_namespace_device(reference, *arrays):
    """Ensure that all arrays use the same namespace and device as reference.

    If necessary the arrays are moved to the same namespace and device as
    the reference array.

    reference : array
        Reference array.

    *arrays : array
        Arrays to check.

    Returns
    -------
    arrays : list
        Arrays with the same namespace and device as reference.
    """
    xp, is_array_api = get_namespace(reference)

    if is_array_api:
        device_ = device(reference)
        # Move arrays to the same namespace and device as the reference array.
        return [xp.asarray(a, device=device_) for a in arrays]
    else:
        return arrays

def _check_device_cpu(device):
    if device not in {"cpu", None}:

def _accept_device_cpu(func):
    def wrapped_func(*args, **kwargs):
        _check_device_cpu(kwargs.pop("device", None))
        return func(*args, **kwargs)

    return wrapped_func

def _remove_non_arrays(*arrays, remove_none=True, remove_types=(str,)):
    """Filter arrays to exclude None and/or specific types.

    Sparse arrays are always filtered out.

    *arrays : array objects
        Array objects.

        Whether to ignore None objects passed in arrays.

        Types to ignore in the arrays.

    Returns
    -------
    filtered_arrays : list
        List of arrays filtered as requested. An empty list is returned if no input
        passes the filters.
    """
    filtered_arrays = []
    remove_types = tuple(remove_types)
    for array in arrays:
        if remove_none and array is None:
            continue
        if isinstance(array, remove_types):
            continue
        if sp.issparse(array):
            continue
        filtered_arrays.append(array)

    return filtered_arrays

def get_namespace(*arrays, remove_none=True, remove_types=(str,), xp=None):
    """Get namespace of arrays.

    Introspect `arrays` arguments and return their common Array API compatible
    namespace object, if any.

    Note that sparse arrays are filtered by default.

    See: https://numpy.org/neps/nep-0047-array-api-standard.html

    If `arrays` are regular numpy arrays, `array_api_compat.numpy` is returned instead.

    Namespace support is not enabled by default. To enabled it call:

      sklearn.set_config(array_api_dispatch=True)

    or:

      with sklearn.config_context(array_api_dispatch=True):
          # your code here

    Otherwise `array_api_compat.numpy` is
    always returned irrespective of the fact that arrays implement the
    `__array_namespace__` protocol or not.

    Note that if no arrays pass the set filters, ``_NUMPY_API_WRAPPER_INSTANCE, False``
    is returned.

    Returns
    -------
    namespace : module
        Namespace shared by array objects. If any of the `arrays` are not arrays,
        the namespace defaults to the NumPy namespace.

    is_array_api_compliant : bool
        True if the arrays are containers that implement the array API spec (see
        https://data-apis.org/array-api/latest/index.html).
        Always False when array_api_dispatch=False.
    """
    array_api_dispatch = get_config()["array_api_dispatch"]
        if xp is not None:
            return xp, False
        else:
            return np_compat, False

    if xp is not None:
        return xp, True

    arrays = _remove_non_arrays(
        *arrays,
        remove_none=remove_none,
        remove_types=remove_types,
    )

    if not arrays:
        return np_compat, False

    _check_array_api_dispatch(array_api_dispatch)

    namespace, is_array_api_compliant = array_api_compat.get_namespace(*arrays), True

    if namespace.__name__ == "array_api_strict" and hasattr(
        namespace, "set_array_api_strict_flags"
    ):
        namespace.set_array_api_strict_flags(api_version="2024.12")

    return namespace, is_array_api_compliant

def get_namespace_and_device(
    *array_list, remove_none=True, remove_types=(str,), xp=None
):
    """Combination into one single function of `get_namespace` and `device`.

    *array_list : array objects

    Returns
    -------
        the namespace defaults to NumPy.
        True if the arrays are containers that implement the Array API spec.
    device : device
    """
    skip_remove_kwargs = dict(remove_none=False, remove_types=[])

        *array_list,
    )
    arrays_device = device(*array_list, **skip_remove_kwargs)

        xp, is_array_api = get_namespace(*array_list, **skip_remove_kwargs)
    else:
        xp, is_array_api = xp, True

        return xp, is_array_api, arrays_device
    else:
        return xp, False, arrays_device

def _expit(X, xp=None):
        return xp.asarray(special.expit(numpy.asarray(X)))

    return 1.0 / (1.0 + xp.exp(-X))

def _fill_or_add_to_diagonal(array, value, xp, add_value=True, wrap=False):
    """Implementation to facilitate adding or assigning specified values to the
    diagonal of a 2-d array.

    If ``add_value`` is `True` then the values will be added to the diagonal
    elements otherwise the values will be assigned to the diagonal elements.
    By default, ``add_value`` is set to `True. This is currently only
    supported for 2-d arrays.

    The implementation is taken from the `numpy.fill_diagonal` function:
    https://github.com/numpy/numpy/blob/v2.0.0/numpy/lib/_index_tricks_impl.py#L799-L929
    """
    if array.ndim != 2:
            f"array should be 2-d. Got array with shape {tuple(array.shape)}"
        )

    value = xp.asarray(value, dtype=array.dtype, device=device(array))
    end = None
    # Explicit, fast formula for the common case.  For 2-d arrays, we
    # accept rectangular ones.
    step = array.shape[1] + 1
    if not wrap:
        end = array.shape[1] * array.shape[1]

    array_flat = xp.reshape(array, (-1,))
    if add_value:
        array_flat[:end:step] += value
    else:
        array_flat[:end:step] = value

def _is_xp_namespace(xp, name):
    return xp.__name__ in (
        name,
        f"array_api_compat.{name}",
        f"sklearn.externals.array_api_compat.{name}",
    )

def _max_precision_float_dtype(xp, device):
    """Return the float dtype with the highest precision supported by the device."""
    # TODO: Update to use `__array_namespace__info__()` from array-api v2023.12
    # when/if that becomes more widespread.
    if _is_xp_namespace(xp, "torch") and str(device).startswith(
        "mps"
    ):  # pragma: no cover
        return xp.float32
    return xp.float64

def _find_matching_floating_dtype(*arrays, xp):
    """Find a suitable floating point dtype when computing with arrays.

    If any of the arrays are floating point, return the dtype with the highest
    precision by following official type promotion rules:

    https://data-apis.org/array-api/latest/API_specification/type_promotion.html

    If there are no floating point input arrays (all integral inputs for
    instance), return the default floating point dtype for the namespace.
    """
    dtyped_arrays = [xp.asarray(a) for a in arrays if hasattr(a, "dtype")]
    floating_dtypes = [
        a.dtype for a in dtyped_arrays if xp.isdtype(a.dtype, "real floating")
    ]
    if floating_dtypes:
        # Return the floating dtype with the highest precision:
        return xp.result_type(*floating_dtypes)

    # If none of the input arrays have a floating point dtype, they must be all
    # integer arrays or containers of Python scalars: return the default
    # floating point dtype for the namespace (implementation specific).
    return xp.asarray(0.0).dtype

def _average(a, axis=None, weights=None, normalize=True, xp=None):
    """Partial port of np.average to support the Array API.

    It does a best effort at mimicking the return dtype rule described at
    https://numpy.org/doc/stable/reference/generated/numpy.average.html but
    only for the common cases needed in scikit-learn.
    """
    xp, _, device_ = get_namespace_and_device(a, weights)

            return xp.asarray(numpy.average(a, axis=axis, weights=weights))
        elif axis is None and weights is not None:
            return xp.asarray(numpy.dot(a, weights))

    a = xp.asarray(a, device=device_)
        weights = xp.asarray(weights, device=device_)

    if weights is not None and a.shape != weights.shape:
                f"Axis must be specified when the shape of a {tuple(a.shape)} and "
                f"weights {tuple(weights.shape)} differ."
            )

        if tuple(weights.shape) != (a.shape[axis],):
                f"Shape of weights weights.shape={tuple(weights.shape)} must be "
                f"consistent with a.shape={tuple(a.shape)} and {axis=}."
            )

        # If weights are 1D, add singleton dimensions for broadcasting
        shape = [1] * a.ndim
        shape[axis] = a.shape[axis]
        weights = xp.reshape(weights, shape)

    if xp.isdtype(a.dtype, "complex floating"):
            "Complex floating point values are not supported by average."
        )
    if weights is not None and xp.isdtype(weights.dtype, "complex floating"):
        )

    output_dtype = _find_matching_floating_dtype(a, weights, xp=xp)
    a = xp.astype(a, output_dtype)

        return (xp.mean if normalize else xp.sum)(a, axis=axis)

    weights = xp.astype(weights, output_dtype)

    sum_ = xp.sum(xp.multiply(a, weights), axis=axis)

    if not normalize:
        return sum_

    scale = xp.sum(weights, axis=axis)
    if xp.any(scale == 0.0):
        raise ZeroDivisionError("Weights sum to zero, can't be normalized")

    return sum_ / scale

def _xlogy(x, y, xp=None):
    # TODO: Remove this once https://github.com/scipy/scipy/issues/21736 is fixed
    xp, _, device_ = get_namespace_and_device(x, y, xp=xp)

    with numpy.errstate(divide="ignore", invalid="ignore"):
        temp = x * xp.log(y)
    return xp.where(x == 0.0, xp.asarray(0.0, dtype=temp.dtype, device=device_), temp)

def _nanmin(X, axis=None, xp=None):
    # TODO: refactor once nan-aware reductions are standardized:
    # https://github.com/data-apis/array-api/issues/621
    xp, _, device_ = get_namespace_and_device(X, xp=xp)
        return xp.asarray(numpy.nanmin(X, axis=axis))

    else:
        mask = xp.isnan(X)
        X = xp.min(
            xp.where(mask, xp.asarray(+xp.inf, dtype=X.dtype, device=device_), X),
        )
        # Replace Infs from all NaN slices with NaN again
        mask = xp.all(mask, axis=axis)
        if xp.any(mask):
            X = xp.where(mask, xp.asarray(xp.nan, dtype=X.dtype, device=device_), X)
        return X

def _nanmax(X, axis=None, xp=None):
        return xp.asarray(numpy.nanmax(X, axis=axis))

    else:
        X = xp.max(
            xp.where(mask, xp.asarray(-xp.inf, dtype=X.dtype, device=device_), X),
        )
        return X

def _nanmean(X, axis=None, xp=None):
        return xp.asarray(numpy.nanmean(X, axis=axis))
    else:
        total = xp.sum(
            xp.where(mask, xp.asarray(0.0, dtype=X.dtype, device=device_), X), axis=axis
        )
        count = xp.sum(xp.astype(xp.logical_not(mask), X.dtype), axis=axis)
        return total / count

def _asarray_with_order(
    array, dtype=None, order=None, copy=None, *, xp=None, device=None
):
    """Helper to support the order kwarg only for NumPy-backed arrays

    Memory layout parameter `order` is not exposed in the Array API standard,
    however some input validation code in scikit-learn needs to work both
    for classes and functions that will leverage Array API only operations
    and for code that inherently relies on NumPy backed data containers with
    specific memory layout constraints (e.g. our own Cython code). The
    purpose of this helper is to make it possible to share code for data
    container validation without memory copies for both downstream use cases:
    the `order` parameter is only enforced if the input array implementation
    is NumPy based, otherwise `order` is just silently ignored.
    """
    xp, _ = get_namespace(array, xp=xp)
        # Use NumPy API to support order
        if copy is True:
            array = numpy.array(array, order=order, dtype=dtype)
        else:
            array = numpy.asarray(array, order=order, dtype=dtype)

        # At this point array is a NumPy ndarray. We convert it to an array
        # container that is consistent with the input's namespace.
        return xp.asarray(array)
    else:
        return xp.asarray(array, dtype=dtype, copy=copy, device=device)

def _ravel(array, xp=None):
    """Array API compliant version of np.ravel.

    For non numpy namespaces, it just returns a flattened array, that might
    be or not be a copy.
    """
        array = numpy.asarray(array)
        return xp.asarray(numpy.ravel(array, order="C"))

    return xp.reshape(array, shape=(-1,))

def _convert_to_numpy(array, xp):
    """Convert X into a NumPy ndarray on the CPU."""
    if _is_xp_namespace(xp, "torch"):
        return array.cpu().numpy()
    elif _is_xp_namespace(xp, "cupy"):  # pragma: nocover
        return array.get()
    elif _is_xp_namespace(xp, "array_api_strict"):
        return numpy.asarray(xp.asarray(array, device=xp.Device("CPU_DEVICE")))

    return numpy.asarray(array)

def _estimator_with_converted_arrays(estimator, converter):
    """Create new estimator which converting all attributes that are arrays.

    The converter is called on all NumPy arrays and arrays that support the
    `DLPack interface <https://dmlc.github.io/dlpack/latest/>`__.

    estimator : Estimator
        Estimator to convert

    converter : callable
        Callable that takes an array attribute and returns the converted array.

    Returns
    -------
    new_estimator : Estimator
        Convert estimator
    """
    from sklearn.base import clone

    new_estimator = clone(estimator)
    for key, attribute in vars(estimator).items():
        if hasattr(attribute, "__dlpack__") or isinstance(attribute, numpy.ndarray):
            attribute = converter(attribute)
        setattr(new_estimator, key, attribute)
    return new_estimator

def _atol_for_type(dtype_or_dtype_name):
    """Return the absolute tolerance for a given numpy dtype."""
    if dtype_or_dtype_name is None:
        # If no dtype is specified when running tests for a given namespace, we
        # expect the same floating precision level as NumPy's default floating
        # point dtype.
        dtype_or_dtype_name = numpy.float64
    return numpy.finfo(dtype_or_dtype_name).eps * 100

def indexing_dtype(xp):
    """Return a platform-specific integer dtype suitable for indexing.

    On 32-bit platforms, this will typically return int32 and int64 otherwise.

    Note: using dtype is recommended for indexing transient array
    datastructures. For long-lived arrays, such as the fitted attributes of
    estimators, it is instead recommended to use platform-independent int32 if
    we do not expect to index more 2B elements. Using fixed dtypes simplifies
    the handling of serialized models, e.g. to deploy a model fit on a 64-bit
    platform to a target 32-bit platform such as WASM/pyodide.
    """
    # Currently this is implemented with simple hack that assumes that
    # following "may be" statements in the Array API spec always hold:
    # > The default integer data type should be the same across platforms, but
    # > the default may vary depending on whether Python is 32-bit or 64-bit.
    # > The default array index data type may be int32 on 32-bit platforms, but
    # > the default should be int64 otherwise.
    # https://data-apis.org/array-api/latest/API_specification/data_types.html#default-data-types
    # TODO: once sufficiently adopted, we might want to instead rely on the
    # newer inspection API: https://github.com/data-apis/array-api/issues/640
    return xp.asarray(0).dtype

def _searchsorted(a, v, *, side="left", sorter=None, xp=None):
    # Temporary workaround needed as long as searchsorted is not widely
    # adopted by implementers of the Array API spec. This is a quite
    # recent addition to the spec:
    # https://data-apis.org/array-api/latest/API_specification/generated/array_api.searchsorted.html
    xp, _ = get_namespace(a, v, xp=xp)
    if hasattr(xp, "searchsorted"):
        return xp.searchsorted(a, v, side=side, sorter=sorter)

    a_np = _convert_to_numpy(a, xp=xp)
    v_np = _convert_to_numpy(v, xp=xp)
    indices = numpy.searchsorted(a_np, v_np, side=side, sorter=sorter)
    return xp.asarray(indices, device=device(a))

def _isin(element, test_elements, xp, assume_unique=False, invert=False):
    """Calculates ``element in test_elements``, broadcasting over `element`
    only.

    Returns a boolean array of the same shape as `element` that is True
    where an element of `element` is in `test_elements` and False otherwise.
    """
        return xp.asarray(
            numpy.isin(
                element=element,
                test_elements=test_elements,
                assume_unique=assume_unique,
                invert=invert,
            )
        )

    original_element_shape = element.shape
    element = xp.reshape(element, (-1,))
    test_elements = xp.reshape(test_elements, (-1,))
    return xp.reshape(
        _in1d(
            ar1=element,
            ar2=test_elements,
            xp=xp,
            assume_unique=assume_unique,
            invert=invert,
        ),
        original_element_shape,
    )

def _in1d(ar1, ar2, xp, assume_unique=False, invert=False):
    """Checks whether each element of an array is also present in a
    second array.

    Returns a boolean array the same length as `ar1` that is True
    where an element of `ar1` is in `ar2` and False otherwise.

    """
    xp, _ = get_namespace(ar1, ar2, xp=xp)

    if ar2.shape[0] < 10 * ar1.shape[0] ** 0.145:
        if invert:
            mask = xp.ones(ar1.shape[0], dtype=xp.bool, device=device(ar1))
            for a in ar2:
                mask &= ar1 != a
        else:
            mask = xp.zeros(ar1.shape[0], dtype=xp.bool, device=device(ar1))
                mask |= ar1 == a

        ar1, rev_idx = xp.unique_inverse(ar1)
        ar2 = xp.unique_values(ar2)

    ar = xp.concat((ar1, ar2))
    device_ = device(ar)
    if size(sar) >= 1:
    else:
        # indexing undefined in standard when sar is empty

        return ret[: ar1.shape[0]]
    else:
        return xp.take(ret, rev_idx, axis=0)

def _count_nonzero(X, axis=None, sample_weight=None, xp=None, device=None):
    """A variant of `sklearn.utils.sparsefuncs.count_nonzero` for the Array API.

    If the array `X` is sparse, and we are using the numpy namespace then we
    simply call the original function. This function only supports 2D arrays.
    """
    from .sparsefuncs import count_nonzero

    xp, _ = get_namespace(X, sample_weight, xp=xp)
    if _is_numpy_namespace(xp) and sp.issparse(X):
        return count_nonzero(X, axis=axis, sample_weight=sample_weight)

    assert X.ndim == 2

    weights = xp.ones_like(X, device=device)
        sample_weight = xp.reshape(sample_weight, (sample_weight.shape[0], 1))
        weights = xp.astype(weights, sample_weight.dtype) * sample_weight

    zero_scalar = xp.asarray(0, device=device, dtype=weights.dtype)
    return xp.sum(xp.where(X != 0, weights, zero_scalar), axis=axis)

def _modify_in_place_if_numpy(xp, func, *args, out=None, **kwargs):
        func(*args, out=out, **kwargs)
    else:
        out = func(*args, **kwargs)
    return out

def _bincount(array, weights=None, minlength=None, xp=None):
    # TODO: update if bincount is ever adopted in a future version of the standard:
    # https://github.com/data-apis/array-api/issues/812
    if hasattr(xp, "bincount"):
        return xp.bincount(array, weights=weights, minlength=minlength)

    array_np = _convert_to_numpy(array, xp=xp)
        weights_np = _convert_to_numpy(weights, xp=xp)
    else:
        weights_np = None
    bin_out = numpy.bincount(array_np, weights=weights_np, minlength=minlength)
    return xp.asarray(bin_out, device=device(array))

def _tolist(array, xp=None):
        return array.tolist()
    return [element.item() for element in array_np]

def _deprecate_positional_args(func=None, *, version="1.3"):
    """Decorator for methods that issues warnings for positional arguments.

    Using the keyword-only argument syntax in pep 3102, arguments after the
    * will issue a warning when passed as a positional argument.

    func : callable, default=None
        Function to check arguments on.
    version : callable, default="1.3"
        The version when positional arguments will result in error.
    """

    def _inner_deprecate_positional_args(f):
        kwonly_args = []
        all_args = []

        for name, param in sig.parameters.items():
            if param.kind == Parameter.POSITIONAL_OR_KEYWORD:
                all_args.append(name)
            elif param.kind == Parameter.KEYWORD_ONLY:
                kwonly_args.append(name)

        @wraps(f)
        def inner_f(*args, **kwargs):
            extra_args = len(args) - len(all_args)
            if extra_args <= 0:
                return f(*args, **kwargs)

            # extra_args > 0
            args_msg = [
                "{}={}".format(name, arg)
                for name, arg in zip(kwonly_args[:extra_args], args[-extra_args:])
            ]
            args_msg = ", ".join(args_msg)
                (
                    f"Pass {args_msg} as keyword args. From version "
                    f"{version} passing these as positional arguments "
                    "will result in an error"
                ),
            )
            kwargs.update(zip(sig.parameters, args))
            return f(**kwargs)

        return inner_f

    if func is not None:
        return _inner_deprecate_positional_args(func)

    return _inner_deprecate_positional_args

def _assert_all_finite(
    X, allow_nan=False, msg_dtype=None, estimator_name=None, input_name=""
):
    """Like assert_all_finite, but only for ndarray."""

    xp, is_array_api = get_namespace(X)

    if _get_config()["assume_finite"]:
        return

    X = xp.asarray(X)

    # for object dtype data, we only check for NaNs (GH-13254)
    if not is_array_api and X.dtype == np.dtype("object") and not allow_nan:
        if _object_dtype_isnan(X).any():
            raise ValueError("Input contains NaN")

    # We need only consider float arrays, hence can early return for all else.
    if not xp.isdtype(X.dtype, ("real floating", "complex floating")):
        return

    # First try an O(n) time, O(1) space solution for the common case that
    # everything is finite; fall back to O(n) space `np.isinf/isnan` or custom
    # Cython implementation to prevent false positives and provide a detailed
    # error message.
    with np.errstate(over="ignore"):
        first_pass_isfinite = xp.isfinite(xp.sum(X))
    if first_pass_isfinite:
        return

    _assert_all_finite_element_wise(
        X,
        xp=xp,
        allow_nan=allow_nan,
        msg_dtype=msg_dtype,
        input_name=input_name,
    )

def _assert_all_finite_element_wise(
    X, *, xp, allow_nan, msg_dtype=None, estimator_name=None, input_name=""
):
    # Cython implementation doesn't support FP16 or complex numbers
    use_cython = (
        xp is np and X.data.contiguous and X.dtype.type in {np.float32, np.float64}
    )
    if use_cython:
        out = cy_isfinite(X.reshape(-1), allow_nan=allow_nan)
        has_nan_error = False if allow_nan else out == FiniteStatus.has_nan
        has_inf = out == FiniteStatus.has_infinite
    else:
        has_inf = xp.any(xp.isinf(X))
        has_nan_error = False if allow_nan else xp.any(xp.isnan(X))
    if has_inf or has_nan_error:
        if has_nan_error:
            type_err = "NaN"
        else:
            msg_dtype = msg_dtype if msg_dtype is not None else X.dtype
            type_err = f"infinity or a value too large for {msg_dtype!r}"
        padded_input_name = input_name + " " if input_name else ""
        msg_err = f"Input {padded_input_name}contains {type_err}."
        if estimator_name and input_name == "X" and has_nan_error:
            # Improve the error message on how to handle missing values in
            # scikit-learn.
            msg_err += (
                f"\n{estimator_name} does not accept missing values"
                " encoded as NaN natively. For supervised learning, you might want"
                " to consider sklearn.ensemble.HistGradientBoostingClassifier and"
                " Regressor which accept missing values encoded as NaNs natively."
                " Alternatively, it is possible to preprocess the data, for"
                " instance by using an imputer transformer in a pipeline or drop"
                " samples with missing values. See"
                " https://scikit-learn.org/stable/modules/impute.html"
                " You can find a list of all estimators that handle NaN values"
                " at the following page:"
                "#estimators-that-handle-nan-values"
            )
        raise ValueError(msg_err)

def assert_all_finite(
    X,
    *,
    allow_nan=False,
    estimator_name=None,
    input_name="",
):
    """Throw a ValueError if X contains NaN or infinity.

    X : {ndarray, sparse matrix}

        If True, do not throw error when `X` contains NaN.

        The estimator name, used to construct the error message.

        The data name used to construct the error message. In particular
        if `input_name` is "X" and the data has NaN values and
        allow_nan is False, the error message will link to the imputer
        documentation.

    Examples
    --------
    >>> from sklearn.utils import assert_all_finite
    >>> array = np.array([1, np.inf, np.nan, 4])
    >>> try:
    ...     assert_all_finite(array)
    ...     print("Test passed: Array contains only finite values.")
    ... except ValueError:
    ...     print("Test failed: Array contains non-finite values.")
    Test failed: Array contains non-finite values.
    """
    _assert_all_finite(
        X.data if sp.issparse(X) else X,
    )

def as_float_array(
    X, *, copy=True, force_all_finite="deprecated", ensure_all_finite=None
):
    """Convert an array-like to an array of floats.

    The new dtype will be np.float32 or np.float64, depending on the original
    type. The function can create a copy or modify the argument depending
    on the argument copy.

        If True, a copy of X will be created. If False, a copy may still be
        returned if X's dtype is not a floating point type.

        Whether to raise an error on np.inf, np.nan, pd.NA in X. The

        - True: Force all values of X to be finite.
        - False: accepts np.inf, np.nan, pd.NA in X.
        - 'allow-nan': accepts only np.nan and pd.NA values in X. Values cannot
          be infinite.

           Accepts `pd.NA` and converts it into `np.nan`

           in 1.8.

    Returns
    -------
    XT : {ndarray, sparse matrix}
        An array of type float.

    Examples
    --------
    >>> from sklearn.utils import as_float_array
    >>> array = np.array([0, 0, 1, 2, 2], dtype=np.int64)
    >>> as_float_array(array)
    array([0., 0., 1., 2., 2.])
    """

    if isinstance(X, np.matrix) or (
        not isinstance(X, np.ndarray) and not sp.issparse(X)
    ):
        return check_array(
            X,
        )
    elif sp.issparse(X) and X.dtype in [np.float32, np.float64]:
        return X.copy() if copy else X
    elif X.dtype in [np.float32, np.float64]:  # is numpy array
        return X.copy("F" if X.flags["F_CONTIGUOUS"] else "C") if copy else X
    else:
        if X.dtype.kind in "uib" and X.dtype.itemsize <= 4:
            return_dtype = np.float32
        else:
            return_dtype = np.float64
        return X.astype(return_dtype)

def _is_arraylike(x):
    """Returns whether the input is array-like."""
    if sp.issparse(x):

    return hasattr(x, "__len__") or hasattr(x, "shape") or hasattr(x, "__array__")

def _is_arraylike_not_scalar(array):
    """Return True if array is array-like and not a scalar"""
    return _is_arraylike(array) and not np.isscalar(array)

def _use_interchange_protocol(X):
    """Use interchange protocol for non-pandas dataframes that follow the protocol.

    Note: at this point we chose not to use the interchange API on pandas dataframe
    to ensure strict behavioral backward compatibility with older versions of
    """
    return not _is_pandas_df(X) and hasattr(X, "__dataframe__")

def _num_features(X):
    """Return the number of features in an array-like X.

    This helper function tries hard to avoid to materialize an array version
    of X unless necessary. For instance, if X is a list of lists,
    this function will return the length of the first element, assuming
    that subsequent elements are all lists of the same length without
    checking.
        array-like to get the number of features.

    Returns
    -------
    features : int
        Number of features
    """
    type_ = type(X)
    if type_.__module__ == "builtins":
        type_name = type_.__qualname__
    else:
        type_name = f"{type_.__module__}.{type_.__qualname__}"
    message = f"Unable to find the number of features from X of type {type_name}"
    if not hasattr(X, "__len__") and not hasattr(X, "shape"):
        if not hasattr(X, "__array__"):
            raise TypeError(message)
        # Only convert X to a numpy array if there is no cheaper, heuristic
        # option.

    if hasattr(X, "shape"):
        if not hasattr(X.shape, "__len__") or len(X.shape) <= 1:
            message += f" with shape {X.shape}"
        return X.shape[1]

    first_sample = X[0]

    # Do not consider an array-like of strings or dicts to be a 2D array
    if isinstance(first_sample, (str, bytes, dict)):
        message += f" where the samples are of type {type(first_sample).__qualname__}"
        raise TypeError(message)

    try:
        # If X is a list of lists, for instance, we assume that all nested
        # lists have the same length without checking or converting to
        # a numpy array to keep this function call as cheap as possible.
        return len(first_sample)
    except Exception as err:
        raise TypeError(message) from err

def _num_samples(x):
    """Return number of samples in array-like x."""
    message = "Expected sequence or array-like, got %s" % type(x)
    if hasattr(x, "fit") and callable(x.fit):
        # Don't get num_samples from an ensembles length!

    if _use_interchange_protocol(x):
        return x.__dataframe__().num_rows()

    if not hasattr(x, "__len__") and not hasattr(x, "shape"):
        if hasattr(x, "__array__"):
            x = np.asarray(x)
        else:

    if hasattr(x, "shape") and x.shape is not None:
        if len(x.shape) == 0:
                "Input should have at least 1 dimension i.e. satisfy "
                f"`len(x.shape) > 0`, got scalar `{x!r}` instead."
            )
        # Check that shape is returning an integer or default to len
        # Dask dataframes may not return numeric shape[0] value
        if isinstance(x.shape[0], numbers.Integral):
            return x.shape[0]

    try:
        return len(x)
    except TypeError as type_error:
        raise TypeError(message) from type_error

def check_memory(memory):
    """Check that ``memory`` is joblib.Memory-like.

    joblib.Memory-like means that ``memory`` can be converted into a
    joblib.Memory instance (typically a str denoting the ``location``)
    or has the same interface (has a ``cache`` method).

    memory : None, str or object with the joblib.Memory interface
        - If string, the location where to create the `joblib.Memory` interface.
        - If None, no caching is done and the Memory object is completely transparent.

    Returns
    -------
    memory : object with the joblib.Memory interface
        A correct joblib.Memory object.

    Raises
    ------
        If ``memory`` is not joblib.Memory-like.

    Examples
    --------
    >>> from sklearn.utils.validation import check_memory
    >>> check_memory("caching_dir")
    Memory(location=caching_dir/joblib)
    """
    if memory is None or isinstance(memory, str):
        memory = joblib.Memory(location=memory, verbose=0)
    elif not hasattr(memory, "cache"):
            "'memory' should be None, a string or have the same"
            " interface as joblib.Memory."
            " Got memory='{}' instead.".format(memory)
        )
    return memory

def check_consistent_length(*arrays):
    """Check that all arrays have consistent first dimensions.

    Checks whether all objects in arrays have the same shape or length.

    *arrays : list or tuple of input objects.
        Objects that will be checked for consistent length.

    Examples
    --------
    >>> from sklearn.utils.validation import check_consistent_length
    >>> a = [1, 2, 3]
    >>> b = [2, 3, 4]
    >>> check_consistent_length(a, b)
    """
    lengths = [_num_samples(X) for X in arrays if X is not None]
    if len(set(lengths)) > 1:
            "Found input variables with inconsistent numbers of samples: %r"
            % [int(l) for l in lengths]
        )

def _make_indexable(iterable):
    """Ensure iterable supports indexing or convert to an indexable variant.

    Convert sparse matrices to csr and other non-indexable iterable to arrays.
    Let `None` and indexable objects (e.g. pandas dataframes) pass unchanged.

    iterable : {list, dataframe, ndarray, sparse matrix} or None
        Object to be converted to an indexable iterable.
    """
    if sp.issparse(iterable):
        return iterable.tocsr()
    elif hasattr(iterable, "__getitem__") or hasattr(iterable, "iloc"):
        return iterable
    elif iterable is None:
    return np.array(iterable)

def indexable(*iterables):
    """Make arrays indexable for cross-validation.

    Checks consistent length, passes through None, and ensures that everything
    can be indexed by converting sparse matrices to csr and converting
    non-iterable objects to arrays.

    *iterables : {lists, dataframes, ndarrays, sparse matrices}
        List of objects to ensure sliceability.

    Returns
    -------
    result : list of {ndarray, sparse matrix, dataframe} or None
        Returns a list containing indexable arrays (i.e. NumPy array,
        sparse matrix, or dataframe) or `None`.

    Examples
    --------
    >>> from sklearn.utils import indexable
    >>> iterables = [
    ...     [1, 2, 3], np.array([2, 3, 4]), None, csr_matrix([[5], [6], [7]])
    ... ]
    >>> indexable(*iterables)
    [[1, 2, 3], array([2, 3, 4]), None, <...Sparse...dtype 'int64'...shape (3, 1)>]
    """

    result = [_make_indexable(X) for X in iterables]
    check_consistent_length(*result)

def _ensure_sparse_format(
    accept_sparse,
    dtype,
    copy,
    ensure_all_finite,
    accept_large_sparse,
):
    """Convert a sparse container to a given format.

    Checks the sparse format of `sparse_container` and converts if necessary.

    sparse_container : sparse matrix or array
        Input to validate and convert.

    accept_sparse : str, bool or list/tuple of str
        String[s] representing allowed sparse matrix formats ('csc',
        'csr', 'coo', 'dok', 'bsr', 'lil', 'dia'). If the input is sparse but
        not in the allowed format, it will be converted to the first listed
        format. True allows the input to be any format. False means
        that a sparse matrix input will raise an error.

    dtype : str, type or None
        Data type of result. If None, the dtype of the input is preserved.

    copy : bool

    ensure_all_finite : bool or 'allow-nan'

           ``ensure_all_finite`` accepts the string ``'allow-nan'``.

    Returns
    -------
    sparse_container_converted : sparse matrix or array
        Sparse container (matrix/array) that is ensured to have an allowed type.
    """
        dtype = sparse_container.dtype

    changed_format = False
    sparse_container_type_name = type(sparse_container).__name__

    if isinstance(accept_sparse, str):
        accept_sparse = [accept_sparse]

    # Indices dtype validation
    _check_large_sparse(sparse_container, accept_large_sparse)

    if accept_sparse is False:
        padded_input = " for " + input_name if input_name else ""
            f"Sparse data was passed{padded_input}, but dense data is required. "
            "Use '.toarray()' to convert to a dense numpy array."
        )
    elif isinstance(accept_sparse, (list, tuple)):
        if len(accept_sparse) == 0:
                "When providing 'accept_sparse' as a tuple or list, it must contain at "
                "least one string value."
            )
        # ensure correct sparse format
        if sparse_container.format not in accept_sparse:
            # create new with correct sparse
            sparse_container = sparse_container.asformat(accept_sparse[0])
            changed_format = True
    elif accept_sparse is not True:
        # any other type
            "Parameter 'accept_sparse' should be a string, boolean or list of strings."
            f" You provided 'accept_sparse={accept_sparse}'."
        )

    if dtype != sparse_container.dtype:
        # convert dtype
        sparse_container = sparse_container.astype(dtype)
    elif copy and not changed_format:
        # force copy
        sparse_container = sparse_container.copy()

    if ensure_all_finite:
        if not hasattr(sparse_container, "data"):
                f"Can't check {sparse_container.format} sparse matrix for nan or inf.",
            )
        else:
            _assert_all_finite(
                sparse_container.data,
                allow_nan=ensure_all_finite == "allow-nan",
                estimator_name=estimator_name,
                input_name=input_name,
            )

    # TODO: Remove when the minimum version of SciPy supported is 1.12
    # With SciPy sparse arrays, conversion from DIA format to COO, CSR, or BSR
    # triggers the use of `np.int64` indices even if the data is such that it could
    # be more efficiently represented with `np.int32` indices.
    # https://github.com/scipy/scipy/issues/19245 Since not all scikit-learn
    # algorithms support large indices, the following code downcasts to `np.int32`
    # indices when it's safe to do so.
    if changed_format:
        # accept_sparse is specified to a specific format and a conversion occurred
        requested_sparse_format = accept_sparse[0]
        _preserve_dia_indices_dtype(
            sparse_container, sparse_container_type_name, requested_sparse_format
        )

    return sparse_container

def _ensure_no_complex_data(array):
    if (
        hasattr(array, "dtype")
        and array.dtype is not None
        and hasattr(array.dtype, "kind")
        and array.dtype.kind == "c"
    ):
        raise ValueError("Complex data not supported\n{}\n".format(array))

def _check_estimator_name(estimator):
    if estimator is not None:
        if isinstance(estimator, str):
            return estimator
        else:
            return estimator.__class__.__name__

def _pandas_dtype_needs_early_conversion(pd_dtype):
    """Return True if pandas extension pd_dtype need to be converted early."""
    # Check these early for pandas versions without extension dtypes
    from pandas import SparseDtype
    from pandas.api.types import (
        is_bool_dtype,
        is_float_dtype,
        is_integer_dtype,
    )

    if is_bool_dtype(pd_dtype):
        # bool and extension booleans need early conversion because __array__
        # converts mixed dtype dataframes into object dtypes

    if isinstance(pd_dtype, SparseDtype):
        # Sparse arrays will be converted later in `check_array`

    try:
        from pandas.api.types import is_extension_array_dtype

    if isinstance(pd_dtype, SparseDtype) or not is_extension_array_dtype(pd_dtype):
        # Only handle extension arrays for integer and floats
    elif is_float_dtype(pd_dtype):
        # Float ndarrays can normally support nans. They need to be converted
        # first to map pd.NA to np.nan
    elif is_integer_dtype(pd_dtype):
        # XXX: Warn when converting from a high integer to a float

def _is_extension_array_dtype(array):
    # Pandas extension arrays have a dtype with an na_value
    return hasattr(array, "dtype") and hasattr(array.dtype, "na_value")

def check_array(
    array,
    accept_sparse=False,
    *,
    accept_large_sparse=True,
    dtype="numeric",
    order=None,
    force_writeable=False,
    ensure_non_negative=False,
    allow_nd=False,
    ensure_min_samples=1,
    ensure_min_features=1,
):
    """Input validation on an array, list, sparse matrix or similar.

    By default, the input is checked to be a non-empty 2D array containing
    only finite values. If the dtype of the array is object, attempt
    converting to float, raising on failure.

    array : object
        Input object to check / convert.

    accept_sparse : str, bool or list/tuple of str, default=False

    accept_large_sparse : bool, default=True
        If a CSR, CSC, COO or BSR sparse matrix is supplied and accepted by
        accept_sparse, accept_large_sparse=False will cause it to be accepted
        only if its indices are stored with a 32-bit dtype.

    dtype : 'numeric', type, list of type or None, default='numeric'
        If "numeric", dtype is preserved unless array.dtype is object.
        If dtype is a list of types, conversion on the first type is only
        performed if the dtype of the input is not in the list.

    order : {'F', 'C'} or None, default=None
        Whether an array will be forced to be fortran or c-style.
        When order is None (default), then if copy=False, nothing is ensured
        about the memory layout of the output array; otherwise (copy=True)
        the memory layout of the returned array is kept as close as possible
        to the original array.

    force_writeable : bool, default=False
        Whether to force the output array to be writeable. If True, the returned array
        is guaranteed to be writeable, which may require a copy. Otherwise the
        writeability of the input array is preserved.

           in 1.8.

    ensure_non_negative : bool, default=False
        Make sure the array has only non-negative values. If True, an array that
        contains negative values will raise a ValueError.

        Whether to raise a value error if array is not 2D.

    allow_nd : bool, default=False
        Whether to allow array.ndim > 2.

    ensure_min_samples : int, default=1
        Make sure that the array has a minimum number of samples in its first
        axis (rows for a 2D array). Setting to 0 disables this check.

    ensure_min_features : int, default=1
        Make sure that the 2D array has some minimum number of features
        (columns). The default value of 1 rejects empty datasets.
        This check is only enforced when the input data has effectively 2
        dimensions or is originally 1D and ``ensure_2d`` is True. Setting to 0
        disables this check.

    estimator : str or estimator instance, default=None
        If passed, include the name of the estimator in warning messages.

    Returns
    -------
    array_converted : object
        The converted and validated array.

    Examples
    --------
    >>> from sklearn.utils.validation import check_array
    >>> X = [[1, 2, 3], [4, 5, 6]]
    >>> X_checked = check_array(X)
    >>> X_checked
    array([[1, 2, 3], [4, 5, 6]])
    """

    if isinstance(array, np.matrix):
            "np.matrix is not supported. Please convert to a numpy array with "
            "np.asarray. For more information see: "
            "https://numpy.org/doc/stable/reference/generated/numpy.matrix.html"
        )

    xp, is_array_api_compliant = get_namespace(array)

    # store reference to original array to check if copy is needed when
    # function returns
    array_orig = array

    # store whether originally we wanted numeric dtype
    dtype_numeric = isinstance(dtype, str) and dtype == "numeric"

    dtype_orig = getattr(array, "dtype", None)
    if not is_array_api_compliant and not hasattr(dtype_orig, "kind"):
        # not a data type (e.g. a column named dtype in a pandas DataFrame)
        dtype_orig = None

    # check if the object contains several dtypes (typically a pandas
    # DataFrame), and store them. If not, store None.
    dtypes_orig = None
    pandas_requires_conversion = False
    # track if we have a Series-like object to raise a better error message
    type_if_series = None
    if hasattr(array, "dtypes") and hasattr(array.dtypes, "__array__"):
        # throw warning if columns are sparse. If all columns are sparse, then
        # array.sparse exists and sparsity will be preserved (later).
        with suppress(ImportError):
            from pandas import SparseDtype

            def is_sparse(dtype):
                return isinstance(dtype, SparseDtype)

            if not hasattr(array, "sparse") and array.dtypes.apply(is_sparse).any():
                    "pandas.DataFrame with sparse columns found."
                    "It will be converted to a dense numpy array."
                )

        dtypes_orig = list(array.dtypes)
        pandas_requires_conversion = any(
            _pandas_dtype_needs_early_conversion(i) for i in dtypes_orig
        )
        if all(isinstance(dtype_iter, np.dtype) for dtype_iter in dtypes_orig):
            dtype_orig = np.result_type(*dtypes_orig)
        elif pandas_requires_conversion and any(d == object for d in dtypes_orig):
            # Force object if any of the dtypes is an object
            dtype_orig = object

    elif (_is_extension_array_dtype(array) or hasattr(array, "iloc")) and hasattr(
        array, "dtype"
    ):
        # array is a pandas series
        type_if_series = type(array)
        pandas_requires_conversion = _pandas_dtype_needs_early_conversion(array.dtype)
        if isinstance(array.dtype, np.dtype):
            dtype_orig = array.dtype
        else:
            # Set to None to let array.astype work out the best dtype
            dtype_orig = None

    if dtype_numeric:
        if (
            dtype_orig is not None
            and hasattr(dtype_orig, "kind")
            and dtype_orig.kind == "O"
        ):
            # if input is object, convert to float.
            dtype = xp.float64
        else:

    if isinstance(dtype, (list, tuple)):
        if dtype_orig is not None and dtype_orig in dtype:
            # no dtype conversion required
        else:
            # dtype conversion required. Let's select the first element of the
            # list of accepted types.
            dtype = dtype[0]

    if pandas_requires_conversion:
        # pandas dataframe requires conversion earlier to handle extension dtypes with
        # nans
        # Use the original dtype for conversion if dtype is None
        new_dtype = dtype_orig if dtype is None else dtype
        array = array.astype(new_dtype)
        # Since we converted here, we do not need to convert again later
        dtype = None

    if ensure_all_finite not in (True, False, "allow-nan"):
            "ensure_all_finite should be a bool or 'allow-nan'. Got "
            f"{ensure_all_finite!r} instead."
        )

    if dtype is not None and _is_numpy_namespace(xp):
        # convert to dtype object to conform to Array API to be use `xp.isdtype` later
        dtype = np.dtype(dtype)

    estimator_name = _check_estimator_name(estimator)
    context = " by %s" % estimator_name if estimator is not None else ""

    # When all dataframe columns are sparse, convert to a sparse array
    if hasattr(array, "sparse") and array.ndim > 1:

            if array.dtypes.apply(is_sparse).all():
                # DataFrame.sparse only supports `to_coo`
                array = array.sparse.to_coo()
                if array.dtype == np.dtype("object"):
                    unique_dtypes = set([dt.subtype.name for dt in array_orig.dtypes])
                    if len(unique_dtypes) > 1:
                        raise ValueError(
                            "Pandas DataFrame with mixed sparse extension arrays "
                            "generated a sparse matrix with object dtype which "
                            "can not be converted to a scipy sparse matrix."
                            "Sparse extension arrays should all have the same "
                            "numeric type."
                        )

    if sp.issparse(array):
        _ensure_no_complex_data(array)
        array = _ensure_sparse_format(
            array,
            accept_large_sparse=accept_large_sparse,
            estimator_name=estimator_name,
            input_name=input_name,
        )
        if ensure_2d and array.ndim < 2:
                f"Expected 2D input, got input with shape {array.shape}.\n"
                "Reshape your data either using array.reshape(-1, 1) if "
                "your data has a single feature or array.reshape(1, -1) "
                "if it contains a single sample."
            )
    else:
        # If np.array(..) gives ComplexWarning, then we convert the warning
        # to an error. This is needed because specifying a non complex
        # dtype to the function converts complex to real dtype,
        # thereby passing the test made in the lines following the scope
        # of warnings context manager.
            try:
                warnings.simplefilter("error", ComplexWarning)
                if dtype is not None and xp.isdtype(dtype, "integral"):
                    # Conversion float -> int should not contain NaN or
                    # inf (numpy#14412). We cannot use casting='safe' because
                    # then conversion float -> int would be disallowed.
                    array = _asarray_with_order(array, order=order, xp=xp)
                    if xp.isdtype(array.dtype, ("real floating", "complex floating")):
                        _assert_all_finite(
                            array,
                            allow_nan=False,
                            msg_dtype=dtype,
                            estimator_name=estimator_name,
                            input_name=input_name,
                        )
                    array = xp.astype(array, dtype, copy=False)
                else:
                    array = _asarray_with_order(array, order=order, dtype=dtype, xp=xp)
            except ComplexWarning as complex_warning:
                    "Complex data not supported\n{}\n".format(array)
                ) from complex_warning

        # It is possible that the np.array(..) gave no warning. This happens
        # when no dtype conversion happened, for example dtype = None. The
        # result is that np.array(..) produces an array of complex dtype
        # and we need to catch and raise exception for such cases.

        if ensure_2d:
            # If input is scalar raise error
            if array.ndim == 0:
                    "Expected 2D array, got scalar array instead:\narray={}.\n"
                    "Reshape your data either using array.reshape(-1, 1) if "
                    "your data has a single feature or array.reshape(1, -1) "
                    "if it contains a single sample.".format(array)
                )
            # If input is 1D raise error
            if array.ndim == 1:
                # If input is a Series-like object (eg. pandas Series or polars Series)
                if type_if_series is not None:
                    msg = (
                        f"Expected a 2-dimensional container but got {type_if_series} "
                        "instead. Pass a DataFrame containing a single row (i.e. "
                        "single sample) or a single column (i.e. single feature) "
                        "instead."
                    )
                else:
                    msg = (
                        f"Expected 2D array, got 1D array instead:\narray={array}.\n"
                        "Reshape your data either using array.reshape(-1, 1) if "
                        "your data has a single feature or array.reshape(1, -1) "
                        "if it contains a single sample."
                    )
                raise ValueError(msg)

        if dtype_numeric and hasattr(array.dtype, "kind") and array.dtype.kind in "USV":
                "dtype='numeric' is not compatible with arrays of bytes/strings."
                "Convert your data to numeric values explicitly instead."
            )
        if not allow_nd and array.ndim >= 3:
                f"Found array with dim {array.ndim},"
                f" while dim <= 2 is required{context}."
            )

        if ensure_all_finite:
                array,
            )

        if copy:
            if _is_numpy_namespace(xp):
                # only make a copy if `array` and `array_orig` may share memory`
                if np.may_share_memory(array, array_orig):
                    array = _asarray_with_order(
                        array, dtype=dtype, order=order, copy=True, xp=xp
                    )
            else:
                # always make a copy for non-numpy arrays
                array = _asarray_with_order(
                    array, dtype=dtype, order=order, copy=True, xp=xp
                )

    if ensure_min_samples > 0:
        n_samples = _num_samples(array)
        if n_samples < ensure_min_samples:
                "Found array with %d sample(s) (shape=%s) while a"
                " minimum of %d is required%s."
                % (n_samples, array.shape, ensure_min_samples, context)
            )

    if ensure_min_features > 0 and array.ndim == 2:
        n_features = array.shape[1]
        if n_features < ensure_min_features:
                "Found array with %d feature(s) (shape=%s) while"
                " a minimum of %d is required%s."
                % (n_features, array.shape, ensure_min_features, context)
            )

    if ensure_non_negative:
        whom = input_name
        if estimator_name:
            whom += f" in {estimator_name}"
        check_non_negative(array, whom)

    if force_writeable:
        # By default, array.copy() creates a C-ordered copy. We set order=K to
        # preserve the order of the array.
        copy_params = {"order": "K"} if not sp.issparse(array) else {}

        array_data = array.data if sp.issparse(array) else array
        flags = getattr(array_data, "flags", None)
        if not getattr(flags, "writeable", True):
            # This situation can only happen when copy=False, the array is read-only and
            # a writeable output is requested. This is an ambiguous setting so we chose
            # to always (except for one specific setting, see below) make a copy to
            # ensure that the output is writeable, even if avoidable, to not overwrite
            # the user's data by surprise.

            if _is_pandas_df_or_series(array_orig):
                try:
                    # In pandas >= 3, np.asarray(df), called earlier in check_array,
                    # returns a read-only intermediate array. It can be made writeable
                    # safely without copy because if the original DataFrame was backed
                    # by a read-only array, trying to change the flag would raise an
                    # error, in which case we make a copy.
                    array_data.flags.writeable = True
                except ValueError:
                    array = array.copy(**copy_params)
            else:
                array = array.copy(**copy_params)

def _check_large_sparse(X, accept_large_sparse=False):
    """Raise a ValueError if X has 64bit indices and accept_large_sparse=False"""
    if not accept_large_sparse:
        supported_indices = ["int32"]
        if X.format == "coo":
            index_keys = ["col", "row"]
        elif X.format in ["csr", "csc", "bsr"]:
            index_keys = ["indices", "indptr"]
        else:
            return
        for key in index_keys:
            indices_datatype = getattr(X, key).dtype
            if indices_datatype not in supported_indices:
                    "Only sparse matrices with 32-bit integer indices are accepted."
                    f" Got {indices_datatype} indices. Please do report a minimal"
                    " reproducer on scikit-learn issue tracker so that support for"
                    " your use-case can be studied by maintainers. See:"
                    " https://scikit-learn.org/dev/developers/minimal_reproducer.html"
                )

def check_X_y(
    X,
    y,
    *,
    multi_output=False,
    y_numeric=False,
):
    """Input validation for standard estimators.

    Checks X and y for consistent length, enforces X to be 2D and y 1D. By
    default, X is checked to be non-empty and containing only finite values.
    Standard input checks are also applied to y, such as checking that y
    does not have np.nan or np.inf targets. For multi-label y, set
    multi_output=True to allow 2D and sparse y. If the dtype of X is
    object, attempt converting to float, raising on failure.

    X : {ndarray, list, sparse matrix}

    y : {ndarray, list, sparse matrix}
        Labels.

    accept_sparse : str, bool or list of str, default=False

        accept_sparse, accept_large_sparse will cause it to be accepted only
        if its indices are stored with a 32-bit dtype.

    order : {'F', 'C'}, default=None
        Whether an array will be forced to be fortran or c-style. If
        `None`, then the input data's order is preserved when possible.

        Whether to raise an error on np.inf, np.nan, pd.NA in array. This parameter
        does not influence whether y can have np.inf, np.nan, pd.NA values.

        - 'allow-nan': accepts only np.nan or pd.NA values in X. Values cannot

           in 1.8.

        Whether to raise a value error if X is not 2D.

        Whether to allow X.ndim > 2.

        Whether to allow 2D y (array or sparse matrix). If false, y will be
        validated as a vector. y cannot have np.nan or np.inf values if
        multi_output=True.

        Make sure that X has a minimum number of samples in its first
        axis (rows for a 2D array).

        This check is only enforced when X has effectively 2 dimensions or
        is originally 1D and ``ensure_2d`` is True. Setting to 0 disables
        this check.

    y_numeric : bool, default=False
        Whether to ensure that y has a numeric type. If dtype of y is object,
        it is converted to float64. Should only be used for regression
        algorithms.

    Returns
    -------
    X_converted : object
        The converted and validated X.

    y_converted : object
        The converted and validated y.

    Examples
    --------
    >>> from sklearn.utils.validation import check_X_y
    >>> X = [[1, 2], [3, 4], [5, 6]]
    >>> y = [1, 2, 3]
    >>> X, y = check_X_y(X, y)
    >>> X
          [3, 4],
          [5, 6]])
    >>> y
    array([1, 2, 3])
    """
    if y is None:
            estimator_name = "estimator"
        else:
            estimator_name = _check_estimator_name(estimator)
            f"{estimator_name} requires y to be passed, but the target y is None"
        )

    X = check_array(
        X,
        accept_sparse=accept_sparse,
        accept_large_sparse=accept_large_sparse,
        force_writeable=force_writeable,
        ensure_2d=ensure_2d,
        allow_nd=allow_nd,
        ensure_min_samples=ensure_min_samples,
        ensure_min_features=ensure_min_features,
        estimator=estimator,
        input_name="X",
    )

    y = _check_y(y, multi_output=multi_output, y_numeric=y_numeric, estimator=estimator)

    return X, y

def _check_y(y, multi_output=False, y_numeric=False, estimator=None):
    """Isolated part of check_X_y dedicated to y validation"""
    if multi_output:
            y,
        )
    else:
        estimator_name = _check_estimator_name(estimator)
        _assert_all_finite(y, input_name="y", estimator_name=estimator_name)
        _ensure_no_complex_data(y)
    if y_numeric and hasattr(y.dtype, "kind") and y.dtype.kind == "O":
        y = y.astype(np.float64)

    return y

def column_or_1d(y, *, dtype=None, warn=False, device=None):
    """Ravel column or 1d numpy array, else raises an error.

       Input data.

    dtype : data-type, default=None
        Data type for `y`.

    warn : bool, default=False
       To control display of warnings.

    device : device, default=None
        `device` object.
        See the :ref:`Array API User Guide <array_api>` for more details.

    Returns
    -------
    y : ndarray
       Output data.

    Raises
    ------
        If `y` is not a 1D array or a 2D array with a single row or column.

    Examples
    --------
    >>> from sklearn.utils.validation import column_or_1d
    >>> column_or_1d([1, 1])
    array([1, 1])
    """
    xp, _ = get_namespace(y)
    y = check_array(
        y,
        input_name="y",
    )

    shape = y.shape
    if len(shape) == 1:
        return _asarray_with_order(
            xp.reshape(y, (-1,)), order="C", xp=xp, device=device
        )
    if len(shape) == 2 and shape[1] == 1:
        if warn:
                (
                    "(n_samples, ), for example using ravel()."
                ),
            )
        )

        "y should be a 1d array, got an array of shape {} instead.".format(shape)
    )

def check_random_state(seed):
    """Turn seed into a np.random.RandomState instance.

    seed : None, int or instance of RandomState
        If seed is None, return the RandomState singleton used by np.random.
        If seed is an int, return a new RandomState instance seeded with seed.
        If seed is already a RandomState instance, return it.
        Otherwise raise ValueError.

    Returns
    -------
    :class:`numpy:numpy.random.RandomState`
        The random state object based on `seed` parameter.

    Examples
    --------
    >>> from sklearn.utils.validation import check_random_state
    >>> check_random_state(42)
    RandomState(MT19937) at 0x...
    """
    if seed is None or seed is np.random:
        return np.random.mtrand._rand
    if isinstance(seed, numbers.Integral):
        return np.random.RandomState(seed)
    if isinstance(seed, np.random.RandomState):
        return seed
        "%r cannot be used to seed a numpy.random.RandomState instance" % seed
    )

def has_fit_parameter(estimator, parameter):
    """Check whether the estimator's fit method supports the given parameter.

        An estimator to inspect.

    parameter : str
        The searched parameter.

    Returns
    -------
    is_parameter : bool
        Whether the parameter was found to be a named parameter of the
        estimator's fit method.

    Examples
    --------
    >>> from sklearn.utils.validation import has_fit_parameter
    >>> has_fit_parameter(SVC(), "sample_weight")
    True
    """
    return (
        # This is used during test collection in common tests. The
        # hasattr(estimator, "fit") makes it so that we don't fail for an estimator
        # that does not have a `fit` method during collection of checks. The right
        # checks will fail later.
        hasattr(estimator, "fit") and parameter in signature(estimator.fit).parameters
    )

def check_symmetric(array, *, tol=1e-10, raise_warning=True, raise_exception=False):
    """Make sure that array is 2D, square and symmetric.

    If the array is not symmetric, then a symmetrized version is returned.
    Optionally, a warning or exception is raised if the matrix is not
    symmetric.

    array : {ndarray, sparse matrix}
        Input object to check / convert. Must be two-dimensional and square,
        otherwise a ValueError will be raised.

    tol : float, default=1e-10
        Absolute tolerance for equivalence of arrays. Default = 1E-10.

        If True then raise a warning if conversion is required.

    raise_exception : bool, default=False
        If True then raise an exception if array is not symmetric.

    Returns
    -------
    array_sym : {ndarray, sparse matrix}
        Symmetrized version of the input array, i.e. the average of array
        and array.transpose(). If sparse, then duplicate entries are first
        summed and zeros are eliminated.

    Examples
    --------
    >>> from sklearn.utils.validation import check_symmetric
    >>> symmetric_array = np.array([[0, 1, 2], [1, 0, 1], [2, 1, 0]])
    >>> check_symmetric(symmetric_array)
           [1, 0, 1],
           [2, 1, 0]])
    >>> sparse_symmetric_array = csr_matrix(symmetric_array)
    >>> check_symmetric(sparse_symmetric_array)
    <Compressed Sparse Row sparse matrix of dtype 'int64'
        with 6 stored elements and shape (3, 3)>
    """
    if (array.ndim != 2) or (array.shape[0] != array.shape[1]):
            "array must be 2-dimensional and square. shape = {0}".format(array.shape)
        )

        diff = array - array.T
        # only csr, csc, and coo have `data` attribute
        if diff.format not in ["csr", "csc", "coo"]:
            diff = diff.tocsr()
        symmetric = np.all(abs(diff.data) < tol)
    else:
        symmetric = np.allclose(array, array.T, atol=tol)

    if not symmetric:
        if raise_exception:
            raise ValueError("Array must be symmetric")
                (
                    "Array is not symmetric, and will be converted "
                    "to symmetric by average with its transpose."
                ),
            )
            conversion = "to" + array.format
            array = getattr(0.5 * (array + array.T), conversion)()
        else:
            array = 0.5 * (array + array.T)

def _is_fitted(estimator, attributes=None, all_or_any=all):
    """Determine if an estimator is fitted

        Estimator instance for which the check is performed.

    attributes : str, list or tuple of str, default=None
        Attribute name(s) given as string or a list/tuple of strings
        Eg.: ``["coef_", "estimator_", ...], "coef_"``

        If `None`, `estimator` is considered fitted if there exist an
        attribute that ends with a underscore and does not start with double
        underscore.

    all_or_any : callable, {all, any}, default=all
        Specify whether all or any of the given attributes must exist.

    Returns
    -------
    fitted : bool
        Whether the estimator is fitted.
    """
    if attributes is not None:
        if not isinstance(attributes, (list, tuple)):
            attributes = [attributes]
        return all_or_any([hasattr(estimator, attr) for attr in attributes])

    if hasattr(estimator, "__sklearn_is_fitted__"):
        return estimator.__sklearn_is_fitted__()

    fitted_attrs = [
        v for v in vars(estimator) if v.endswith("_") and not v.startswith("__")
    ]
    return len(fitted_attrs) > 0

def check_is_fitted(estimator, attributes=None, *, msg=None, all_or_any=all):
    """Perform is_fitted validation for estimator.

    Checks if the estimator is fitted by verifying the presence of
    fitted attributes (ending with a trailing underscore) and otherwise
    raises a :class:`~sklearn.exceptions.NotFittedError` with the given message.

    If an estimator does not set any attributes with a trailing underscore, it
    can define a ``__sklearn_is_fitted__`` method returning a boolean to
    specify if the estimator is fitted or not. See
    :ref:`sphx_glr_auto_examples_developing_estimators_sklearn_is_fitted.py`
    for an example on how to use the API.

    If no `attributes` are passed, this function will pass if an estimator is stateless.
    An estimator can indicate it's stateless by setting the `requires_fit` tag. See
    :ref:`estimator_tags` for more information. Note that the `requires_fit` tag
    is ignored if `attributes` are passed.

    msg : str, default=None
        The default error message is, "This %(name)s instance is not fitted
        yet. Call 'fit' with appropriate arguments before using this
        estimator."

        For custom messages if "%(name)s" is present in the message string,
        it is substituted for the estimator name.

        Eg. : "Estimator, %(name)s, must be fitted before sparsifying".

    Raises
    ------
    TypeError
        If the estimator is a class or not an estimator instance

    NotFittedError
        If the attributes are not found.

    Examples
    --------
    >>> from sklearn.utils.validation import check_is_fitted
    >>> lr = LogisticRegression()
    >>> try:
    ...     check_is_fitted(lr)
    ... except NotFittedError as exc:
    ...     print(f"Model is not fitted yet.")
    Model is not fitted yet.
    >>> lr.fit([[1, 2], [1, 3]], [1, 0])
    >>> check_is_fitted(lr)
    """
    if isclass(estimator):
        raise TypeError("{} is a class, not an instance.".format(estimator))
    if msg is None:
        msg = (
            "This %(name)s instance is not fitted yet. Call 'fit' with "
            "appropriate arguments before using this estimator."
        )

        raise TypeError("%s is not an estimator instance." % (estimator))

    if not tags.requires_fit and attributes is None:
        return

    if not _is_fitted(estimator, attributes, all_or_any):
        raise NotFittedError(msg % {"name": type(estimator).__name__})

def _estimator_has(attr, *, delegates=("estimator_", "estimator")):
    """Check if we can delegate a method to the underlying estimator.

    We check the `delegates` in the order they are passed. By default, we first check
    the fitted estimator if available, otherwise we check the unfitted estimator.

    attr : str
        Name of the attribute the delegate might or might not have.

    delegates: tuple of str, default=("estimator_", "estimator")
        A tuple of sub-estimator(s) to check if we can delegate the `attr` method.

    Returns
    -------
    check : function
        Function to check if the delegate has the attribute.

    Raises
    ------
        Raised when none of the delegates are present in the object.
    """

        for delegate in delegates:
            # In meta estimators with multiple sub estimators,
            # only the attribute of the first sub estimator is checked,
            # assuming uniformity across all sub estimators.
            if hasattr(self, delegate):
                delegator = getattr(self, delegate)
                if isinstance(delegator, Sequence):
                    return getattr(delegator[0], attr)
                else:
                    return getattr(delegator, attr)

        raise ValueError(f"None of the delegates {delegates} are present in the class.")

def check_non_negative(X, whom):
    """
    Check if there is any negative value in an array.

    whom : str
        Who passed X to this function.
    """
    xp, _ = get_namespace(X)
    # avoid X.min() on sparse matrix since it also sorts the indices
        if X.format in ["lil", "dok"]:
        if X.data.size == 0:
            X_min = 0
        else:
            X_min = X.data.min()
    else:
        X_min = xp.min(X)

    if X_min < 0:
        raise ValueError(f"Negative values in data passed to {whom}.")

def check_scalar(
    x,
    name,
    target_type,
    *,
    min_val=None,
    max_val=None,
    include_boundaries="both",
):
    """Validate scalar parameters type and value.

    x : object
        The scalar parameter to validate.

    name : str
        The name of the parameter to be printed in error messages.

    target_type : type or tuple
        Acceptable data types for the parameter.

    min_val : float or int, default=None
        The minimum valid value the parameter can take. If None (default) it
        is implied that the parameter does not have a lower bound.

    max_val : float or int, default=None
        The maximum valid value the parameter can take. If None (default) it
        is implied that the parameter does not have an upper bound.

    include_boundaries : {"left", "right", "both", "neither"}, default="both"
        Whether the interval defined by `min_val` and `max_val` should include
        the boundaries. Possible choices are:

        - `"left"`: only `min_val` is included in the valid interval.
          It is equivalent to the interval `[ min_val, max_val )`.
        - `"right"`: only `max_val` is included in the valid interval.
          It is equivalent to the interval `( min_val, max_val ]`.
        - `"both"`: `min_val` and `max_val` are included in the valid interval.
          It is equivalent to the interval `[ min_val, max_val ]`.
        - `"neither"`: neither `min_val` nor `max_val` are included in the
          valid interval. It is equivalent to the interval `( min_val, max_val )`.

    Returns
    -------
    x : numbers.Number
        The validated number.

    Raises
    ------
    TypeError
        If the parameter's type does not match the desired type.

        If the parameter's value violates the given bounds.
        If `min_val`, `max_val` and `include_boundaries` are inconsistent.

    Examples
    --------
    >>> from sklearn.utils.validation import check_scalar
    >>> check_scalar(10, "x", int, min_val=1, max_val=20)
    10
    """

    def type_name(t):
        """Convert type into humman readable string."""
        module = t.__module__
        qualname = t.__qualname__
        if module == "builtins":
            return qualname
        elif t == numbers.Real:
            return "float"
        elif t == numbers.Integral:
            return "int"
        return f"{module}.{qualname}"

    if not isinstance(x, target_type):
        if isinstance(target_type, tuple):
            types_str = ", ".join(type_name(t) for t in target_type)
            target_type_str = f"{{{types_str}}}"
        else:
            target_type_str = type_name(target_type)

            f"{name} must be an instance of {target_type_str}, not"
            f" {type(x).__qualname__}."
        )

    expected_include_boundaries = ("left", "right", "both", "neither")
    if include_boundaries not in expected_include_boundaries:
            f"Unknown value for `include_boundaries`: {include_boundaries!r}. "
            f"Possible values are: {expected_include_boundaries}."
        )

    if max_val is None and include_boundaries == "right":
            "`include_boundaries`='right' without specifying explicitly `max_val` "
            "is inconsistent."
        )

    if min_val is None and include_boundaries == "left":
            "`include_boundaries`='left' without specifying explicitly `min_val` "
        )

    comparison_operator = (
        operator.lt if include_boundaries in ("left", "both") else operator.le
    )
    if min_val is not None and comparison_operator(x, min_val):
            f"{name} == {x}, must be"
            f" {'>=' if include_boundaries in ('left', 'both') else '>'} {min_val}."
        )

        operator.gt if include_boundaries in ("right", "both") else operator.ge
    )
    if max_val is not None and comparison_operator(x, max_val):
            f" {'<=' if include_boundaries in ('right', 'both') else '<'} {max_val}."
        )

    return x

def _check_psd_eigenvalues(lambdas, enable_warnings=False):
    """Check the eigenvalues of a positive semidefinite (PSD) matrix.

    Checks the provided array of PSD matrix eigenvalues for numerical or
    conditioning issues and returns a fixed validated version. This method
    should typically be used if the PSD matrix is user-provided (e.g. a
    Gram matrix) or computed using a user-provided dissimilarity metric
    (e.g. kernel function), or if the decomposition process uses approximation
    methods (randomized SVD, etc.).

    It checks for three things:

    - that there are no significant imaginary parts in eigenvalues (more than
      1e-5 times the maximum real part). If this check fails, it raises a
      ``ValueError``. Otherwise all non-significant imaginary parts that may
      remain are set to zero. This operation is traced with a
      ``PositiveSpectrumWarning`` when ``enable_warnings=True``.

    - that eigenvalues are not all negative. If this check fails, it raises a
      ``ValueError``

    - that there are no significant negative eigenvalues with absolute value
      more than 1e-10 (1e-6) and more than 1e-5 (5e-3) times the largest
      positive eigenvalue in double (simple) precision. If this check fails,
      it raises a ``ValueError``. Otherwise all negative eigenvalues that may

    Finally, all the positive eigenvalues that are too small (with a value
    smaller than the maximum eigenvalue multiplied by 1e-12 (2e-7)) are set to
    zero. This operation is traced with a ``PositiveSpectrumWarning`` when
    ``enable_warnings=True``.

    lambdas : array-like of shape (n_eigenvalues,)
        Array of eigenvalues to check / fix.

    enable_warnings : bool, default=False
        When this is set to ``True``, a ``PositiveSpectrumWarning`` will be
        raised when there are imaginary parts, negative eigenvalues, or
        extremely small non-zero eigenvalues. Otherwise no warning will be
        raised. In both cases, imaginary parts, negative eigenvalues, and
        extremely small non-zero eigenvalues will be set to zero.

    Returns
    -------
    lambdas_fixed : ndarray of shape (n_eigenvalues,)
        A fixed validated copy of the array of eigenvalues.

    Examples
    --------
    >>> from sklearn.utils.validation import _check_psd_eigenvalues
    >>> _check_psd_eigenvalues([1, 2])      # nominal case
    array([1, 2])
    >>> _check_psd_eigenvalues([5, 5j])     # significant imag part
        ...
    ValueError: There are significant imaginary parts in eigenvalues (1
        of the maximum real part). Either the matrix is not PSD, or there was
        an issue while computing the eigendecomposition of the matrix.
    >>> _check_psd_eigenvalues([5, 5e-5j])  # insignificant imag part
    array([5., 0.])
    >>> _check_psd_eigenvalues([-5, -1])    # all negative
        ...
    ValueError: All eigenvalues are negative (maximum is -1). Either the
        matrix is not PSD, or there was an issue while computing the
        eigendecomposition of the matrix.
    >>> _check_psd_eigenvalues([5, -1])     # significant negative
        ...
    ValueError: There are significant negative eigenvalues (0.2 of the
        maximum positive). Either the matrix is not PSD, or there was an issue
        while computing the eigendecomposition of the matrix.
    >>> _check_psd_eigenvalues([5, -5e-5])  # insignificant negative
    >>> _check_psd_eigenvalues([5, 4e-12])  # bad conditioning (too small)

    """

    lambdas = np.array(lambdas)
    is_double_precision = lambdas.dtype == np.float64

    # note: the minimum value available is
    #  - single-precision: np.finfo('float32').eps = 1.2e-07
    #  - double-precision: np.finfo('float64').eps = 2.2e-16

    # the various thresholds used for validation
    # we may wish to change the value according to precision.
    significant_imag_ratio = 1e-5
    significant_neg_ratio = 1e-5 if is_double_precision else 5e-3
    significant_neg_value = 1e-10 if is_double_precision else 1e-6
    small_pos_ratio = 1e-12 if is_double_precision else 2e-7

    # Check that there are no significant imaginary parts
    if not np.isreal(lambdas).all():
        max_imag_abs = np.abs(np.imag(lambdas)).max()
        max_real_abs = np.abs(np.real(lambdas)).max()
        if max_imag_abs > significant_imag_ratio * max_real_abs:
                "There are significant imaginary parts in eigenvalues (%g "
                "of the maximum real part). Either the matrix is not PSD, or "
                "there was an issue while computing the eigendecomposition "
                "of the matrix." % (max_imag_abs / max_real_abs)
            )

        # warn about imaginary parts being removed
        if enable_warnings:
                "There are imaginary parts in eigenvalues (%g "
                "of the maximum real part). Either the matrix is not"
                " PSD, or there was an issue while computing the "
                "eigendecomposition of the matrix. Only the real "
                "parts will be kept." % (max_imag_abs / max_real_abs),
                PositiveSpectrumWarning,
            )

    # Remove all imaginary parts (even if zero)
    lambdas = np.real(lambdas)

    # Check that there are no significant negative eigenvalues
    max_eig = lambdas.max()
    if max_eig < 0:
            "All eigenvalues are negative (maximum is %g). "
            "Either the matrix is not PSD, or there was an "
            "issue while computing the eigendecomposition of "
            "the matrix." % max_eig
        )

    else:
        min_eig = lambdas.min()
        if (
            min_eig < -significant_neg_ratio * max_eig
            and min_eig < -significant_neg_value
        ):
                "There are significant negative eigenvalues (%g"
                " of the maximum positive). Either the matrix is "
                "not PSD, or there was an issue while computing "
                "the eigendecomposition of the matrix." % (-min_eig / max_eig)
            )
        elif min_eig < 0:
            # Remove all negative values and warn about it
            if enable_warnings:
                    "There are negative eigenvalues (%g of the "
                    "maximum positive). Either the matrix is not "
                    "PSD, or there was an issue while computing the"
                    " eigendecomposition of the matrix. Negative "
                    "eigenvalues will be replaced with 0." % (-min_eig / max_eig),
                    PositiveSpectrumWarning,
                )
            lambdas[lambdas < 0] = 0

    # Check for conditioning (small positive non-zeros)
    too_small_lambdas = (0 < lambdas) & (lambdas < small_pos_ratio * max_eig)
    if too_small_lambdas.any():
                "Badly conditioned PSD matrix spectrum: the largest "
                "eigenvalue is more than %g times the smallest. "
                "Small eigenvalues will be replaced with 0."
                "" % (1 / small_pos_ratio),
            )
        lambdas[too_small_lambdas] = 0

    return lambdas

def _check_sample_weight(
    sample_weight, X, *, dtype=None, ensure_non_negative=False, copy=False
):
    """Validate sample weights.

    Note that passing sample_weight=None will output an array of ones.
    Therefore, in some cases, you may want to protect the call with:
        sample_weight = _check_sample_weight(...)

    sample_weight : {ndarray, Number or None}, shape (n_samples,)
        Input sample weights.

        dtype of the validated `sample_weight`.
        If None, and `sample_weight` is an array:

            - If `sample_weight.dtype` is one of `{np.float64, np.float32}`,
              then the dtype is preserved.
            - Else the output has NumPy's default dtype: `np.float64`.

        If `dtype` is not `{np.float32, np.float64, None}`, then output will
        be `np.float64`.

    ensure_non_negative : bool, default=False,
        Whether or not the weights are expected to be non-negative.

        If True, a copy of sample_weight will be created.

    Returns
    -------
        Validated sample weight. It is guaranteed to be "C" contiguous.
    """

    if dtype is not None and dtype not in [np.float32, np.float64]:

        sample_weight = np.ones(n_samples, dtype=dtype)
    elif isinstance(sample_weight, numbers.Number):
        sample_weight = np.full(n_samples, sample_weight, dtype=dtype)
    else:
            dtype = [np.float64, np.float32]
        sample_weight = check_array(
            input_name="sample_weight",
        )
        if sample_weight.ndim != 1:
            raise ValueError("Sample weights must be 1D array or scalar")

        if sample_weight.shape != (n_samples,):
                "sample_weight.shape == {}, expected {}!".format(
                    sample_weight.shape, (n_samples,)
                )
            )

        check_non_negative(sample_weight, "`sample_weight`")

    return sample_weight

def _allclose_dense_sparse(x, y, rtol=1e-7, atol=1e-9):
    """Check allclose for sparse and dense data.

    rtol : float, default=1e-7
        Relative tolerance; see numpy.allclose.

        atol=0.
    """
    if sp.issparse(x) and sp.issparse(y):
        return (
            np.array_equal(x.indices, y.indices)
            and np.array_equal(x.indptr, y.indptr)
            and np.allclose(x.data, y.data, rtol=rtol, atol=atol)
        )
    elif not sp.issparse(x) and not sp.issparse(y):
        return np.allclose(x, y, rtol=rtol, atol=atol)
        "Can only compare two sparse matrices, not a sparse matrix and an array"
    )

def _check_response_method(estimator, response_method):
    """Check if `response_method` is available in estimator and return it.

        Classifier or regressor to check.

    response_method : {"predict_proba", "predict_log_proba", "decision_function",

    Returns
    -------
    prediction_method : callable
        Prediction method of estimator.

    Raises
    ------
    AttributeError
        If `response_method` is not available in `estimator`.
    """
    if isinstance(response_method, str):
        list_methods = [response_method]
    else:
        list_methods = response_method

    prediction_method = [getattr(estimator, method, None) for method in list_methods]
    prediction_method = reduce(lambda x, y: x or y, prediction_method)
    if prediction_method is None:
            f"{estimator.__class__.__name__} has none of the following attributes: "
            f"{', '.join(list_methods)}."
        )

def _check_method_params(X, params, indices=None):
    """Check and validate the parameters passed to a specific
    method like `fit`.

        Dictionary containing the parameters passed to the method.

    indices : array-like of shape (n_samples,), default=None
        Indices to be selected if the parameter has the same size as `X`.

    Returns
    -------
    method_params_validated : dict
        Validated parameters. We ensure that the values support indexing.
    """
    from . import _safe_indexing

    method_params_validated = {}
    for param_key, param_value in params.items():
        if (
            not _is_arraylike(param_value) and not sp.issparse(param_value)
        ) or _num_samples(param_value) != _num_samples(X):
            # Non-indexable pass-through (for now for backward-compatibility).
            # https://github.com/scikit-learn/scikit-learn/issues/15805
            method_params_validated[param_key] = param_value
        else:
            # Any other method_params should support indexing
            # (e.g. for cross-validation).
            method_params_validated[param_key] = _make_indexable(param_value)
            method_params_validated[param_key] = _safe_indexing(
                method_params_validated[param_key], indices
            )

    return method_params_validated

def _is_pandas_df_or_series(X):
    """Return True if the X is a pandas dataframe or series."""
    try:
        pd = sys.modules["pandas"]
    return isinstance(X, (pd.DataFrame, pd.Series))

def _is_pandas_df(X):
    """Return True if the X is a pandas dataframe."""
    try:
    return isinstance(X, pd.DataFrame)

def _is_polars_df_or_series(X):
    """Return True if the X is a polars dataframe or series."""
    try:
        pl = sys.modules["polars"]
    return isinstance(X, (pl.DataFrame, pl.Series))

def _is_polars_df(X):
    """Return True if the X is a polars dataframe."""
    try:
    return isinstance(X, pl.DataFrame)

def _get_feature_names(X):
    """Get feature names from X.

    Support for other array containers should place its implementation here.

    X : {ndarray, dataframe} of shape (n_samples, n_features)
        Array container to extract feature names.

        - pandas dataframe : The columns will be considered to be feature
          names. If the dataframe contains non-string feature names, `None` is
          returned.
        - All other array containers will return `None`.

    Returns
    -------
    names: ndarray or None
        Feature names of `X`. Unrecognized array containers will return `None`.
    """
    feature_names = None

    # extract feature names for support array containers
    if _is_pandas_df(X):
        # Make sure we can inspect columns names from pandas, even with
        # versions too old to expose a working implementation of
        # __dataframe__.column_names() and avoid introducing any
        # additional copy.
        # TODO: remove the pandas-specific branch once the minimum supported
        # version of pandas has a working implementation of
        # __dataframe__.column_names() that is guaranteed to not introduce any
        # additional copy of the data without having to impose allow_copy=False
        # that could fail with other libraries. Note: in the longer term, we
        # could decide to instead rely on the __dataframe_namespace__ API once
        # adopted by our minimally supported pandas version.
        feature_names = np.asarray(X.columns, dtype=object)
    elif hasattr(X, "__dataframe__"):
        df_protocol = X.__dataframe__()
        feature_names = np.asarray(list(df_protocol.column_names()), dtype=object)

    if feature_names is None or len(feature_names) == 0:
        return

    types = sorted(t.__qualname__ for t in set(type(v) for v in feature_names))

    # mixed type of string and non-string is not supported
    if len(types) > 1 and "str" in types:
            "Feature names are only supported if all input features have string names, "
            f"but your input has {types} as feature name / column name types. "
            "If you want feature names to be stored and validated, you must convert "
            "them all to strings, by using X.columns = X.columns.astype(str) for "
            "example. Otherwise you can remove feature / column names from your input "
            "data, or convert them all to a non-string data type."
        )

    # Only feature names of all strings are supported
    if len(types) == 1 and types[0] == "str":
        return feature_names

def _check_feature_names_in(estimator, input_features=None, *, generate_names=True):
    """Check `input_features` and generate names if needed.

    Commonly used in :term:`get_feature_names_out`.

    input_features : array-like of str or None, default=None
        Input features.

        - If `input_features` is `None`, then `feature_names_in_` is
          used as feature names in. If `feature_names_in_` is not defined,
          then the following input feature names are generated:
          `["x0", "x1", ..., "x(n_features_in_ - 1)"]`.
        - If `input_features` is an array-like, then `input_features` must
          match `feature_names_in_` if `feature_names_in_` is defined.

    generate_names : bool, default=True
        Whether to generate names when `input_features` is `None` and
        `estimator.feature_names_in_` is not defined. This is useful for transformers
        that validates `input_features` but do not require them in
        :term:`get_feature_names_out` e.g. `PCA`.

    Returns
    -------
    feature_names_in : ndarray of str or `None`
        Feature names in.
    """

    feature_names_in_ = getattr(estimator, "feature_names_in_", None)
    n_features_in_ = getattr(estimator, "n_features_in_", None)

    if input_features is not None:
        input_features = np.asarray(input_features, dtype=object)
        if feature_names_in_ is not None and not np.array_equal(
            feature_names_in_, input_features
        ):
            raise ValueError("input_features is not equal to feature_names_in_")

        if n_features_in_ is not None and len(input_features) != n_features_in_:
                "input_features should have length equal to number of "
                f"features ({n_features_in_}), got {len(input_features)}"
            )

    if feature_names_in_ is not None:
        return feature_names_in_

    if not generate_names:
        return

    # Generates feature names if `n_features_in_` is defined
    if n_features_in_ is None:
        raise ValueError("Unable to generate feature names without n_features_in_")

    return np.asarray([f"x{i}" for i in range(n_features_in_)], dtype=object)

def _generate_get_feature_names_out(estimator, n_features_out, input_features=None):
    """Generate feature names out for estimator using the estimator name as the prefix.

    The input_feature names are validated but not used. This function is useful
    for estimators that generate their own names based on `n_features_out`, i.e. PCA.

        Estimator producing output feature names.

    n_feature_out : int
        Number of feature names out.

        Only used to validate feature names with `estimator.feature_names_in_`.

    Returns
    -------
    """
    _check_feature_names_in(estimator, input_features, generate_names=False)
    return np.asarray(
        [f"{estimator_name}{i}" for i in range(n_features_out)], dtype=object
    )

def _check_monotonic_cst(estimator, monotonic_cst=None):
    """Check the monotonic constraints and return the corresponding array.

    This helper function should be used in the `fit` method of an estimator
    that supports monotonic constraints and called after the estimator has
    introspected input data to set the `n_features_in_` and optionally the
    `feature_names_in_` attributes.

    monotonic_cst : array-like of int, dict of str or None, default=None
        Monotonic constraints for the features.

        - If array-like, then it should contain only -1, 0 or 1. Each value
            will be checked to be in [-1, 0, 1]. If a value is -1, then the
            corresponding feature is required to be monotonically decreasing.
        - If dict, then it the keys should be the feature names occurring in
            `estimator.feature_names_in_` and the values should be -1, 0 or 1.
        - If None, then an array of 0s will be allocated.

    Returns
    -------
    monotonic_cst : ndarray of int
        Monotonic constraints for each feature.
    """
    original_monotonic_cst = monotonic_cst
    if monotonic_cst is None or isinstance(monotonic_cst, dict):
        monotonic_cst = np.full(
            shape=estimator.n_features_in_,
            fill_value=0,
            dtype=np.int8,
        )
        if isinstance(original_monotonic_cst, dict):
            if not hasattr(estimator, "feature_names_in_"):
                    f"{estimator.__class__.__name__} was not fitted on data "
                    "with feature names. Pass monotonic_cst as an integer "
                )
            unexpected_feature_names = list(
                set(original_monotonic_cst) - set(estimator.feature_names_in_)
            )
            unexpected_feature_names.sort()  # deterministic error message
            n_unexpeced = len(unexpected_feature_names)
            if unexpected_feature_names:
                if len(unexpected_feature_names) > 5:
                    unexpected_feature_names = unexpected_feature_names[:5]
                    unexpected_feature_names.append("...")
                    f"monotonic_cst contains {n_unexpeced} unexpected feature "
                    f"names: {unexpected_feature_names}."
                )
            for feature_idx, feature_name in enumerate(estimator.feature_names_in_):
                if feature_name in original_monotonic_cst:
                    cst = original_monotonic_cst[feature_name]
                    if cst not in [-1, 0, 1]:
                            f"monotonic_cst['{feature_name}'] must be either "
                            f"-1, 0 or 1. Got {cst!r}."
                        )
                    monotonic_cst[feature_idx] = cst
    else:
        unexpected_cst = np.setdiff1d(monotonic_cst, [-1, 0, 1])
        if unexpected_cst.shape[0]:
                "monotonic_cst must be an array-like of -1, 0 or 1. Observed "
                f"values: {unexpected_cst.tolist()}."
            )

        monotonic_cst = np.asarray(monotonic_cst, dtype=np.int8)
        if monotonic_cst.shape[0] != estimator.n_features_in_:
                f"monotonic_cst has shape {monotonic_cst.shape} but the input data "
                f"X has {estimator.n_features_in_} features."
            )
    return monotonic_cst

def _check_pos_label_consistency(pos_label, y_true):
    """Check if `pos_label` need to be specified or not.

    In binary classification, we fix `pos_label=1` if the labels are in the set
    {-1, 1} or {0, 1}. Otherwise, we raise an error asking to specify the
    `pos_label` parameters.

        The positive label.
        The target vector.

    Returns
    -------
        If `pos_label` can be inferred, it will be returned.

    Raises
    ------
        In the case that `y_true` does not have label in {-1, 1} or {0, 1},
        it will raise a `ValueError`.
    """
    # ensure binary classification if pos_label is not specified
    # classes.dtype.kind in ('O', 'U', 'S') is required to avoid
    # triggering a FutureWarning by calling np.array_equal(a, b)
    # when elements in the two arrays are not comparable.
        # Compute classes only if pos_label is not specified:
        if classes.dtype.kind in "OUS" or not (
            np.array_equal(classes, [0, 1])
            or np.array_equal(classes, [-1, 1])
            or np.array_equal(classes, [0])
            or np.array_equal(classes, [-1])
            or np.array_equal(classes, [1])
        ):
            classes_repr = ", ".join([repr(c) for c in classes.tolist()])
                f"y_true takes value in {{{classes_repr}}} and pos_label is not "
                "specified: either make y_true take value in {0, 1} or "
                "{-1, 1} or pass pos_label explicitly."
            )
        pos_label = 1

    return pos_label

def _to_object_array(sequence):
    """Convert sequence to a 1-D NumPy array of object dtype.

    numpy.array constructor has a similar use but it's output
    is ambiguous. It can be 1-D NumPy array of object dtype if
    the input is a ragged array, but if the input is a list of
    equal length arrays, then the output is a 2D numpy.array.
    _to_object_array solves this ambiguity by guarantying that
    the output is a 1-D NumPy array of objects for any input.

    sequence : array-like of shape (n_elements,)
        The sequence to be converted.

    Returns
    -------
    out : ndarray of shape (n_elements,), dtype=object
        The converted sequence into a 1-D NumPy array of object dtype.

    Examples
    --------
    >>> from sklearn.utils.validation import _to_object_array
    >>> _to_object_array([np.array([0]), np.array([1])])
    array([array([0]), array([1])], dtype=object)
    >>> _to_object_array([np.array([0]), np.array([1, 2])])
    array([array([0]), array([1, 2])], dtype=object)
    """
    out = np.empty(len(sequence), dtype=object)
    out[:] = sequence
    return out

def _check_feature_names(estimator, X, *, reset):
    """Set or check the `feature_names_in_` attribute of an estimator.

        Moved from :class:`~sklearn.base.BaseEstimator` to
        :mod:`sklearn.utils.validation`.

        The estimator to validate the input for.

    reset : bool
        Whether to reset the `feature_names_in_` attribute.
        If False, the input will be checked for consistency with
        feature names of data provided when reset was last True.
        .. note::
           It is recommended to call `reset=True` in `fit` and in the first
           call to `partial_fit`. All other methods that validate `X`
           should set `reset=False`.
    """

    if reset:
        feature_names_in = _get_feature_names(X)
        if feature_names_in is not None:
            estimator.feature_names_in_ = feature_names_in
        elif hasattr(estimator, "feature_names_in_"):
            # Delete the attribute when the estimator is fitted on a new dataset
            # that has no feature names.
            delattr(estimator, "feature_names_in_")
        return

    fitted_feature_names = getattr(estimator, "feature_names_in_", None)
    X_feature_names = _get_feature_names(X)

    if fitted_feature_names is None and X_feature_names is None:
        # no feature names seen in fit and in X
        return

    if X_feature_names is not None and fitted_feature_names is None:
            f"X has feature names, but {estimator.__class__.__name__} was fitted "
            "without feature names"
        )
        return

    if X_feature_names is None and fitted_feature_names is not None:
            "X does not have valid feature names, but"
            f" {estimator.__class__.__name__} was fitted with feature names"
        )
        return

    # validate the feature names against the `feature_names_in_` attribute
    if len(fitted_feature_names) != len(X_feature_names) or np.any(
        fitted_feature_names != X_feature_names
    ):
        message = "The feature names should match those that were passed during fit.\n"
        fitted_feature_names_set = set(fitted_feature_names)
        X_feature_names_set = set(X_feature_names)

        unexpected_names = sorted(X_feature_names_set - fitted_feature_names_set)
        missing_names = sorted(fitted_feature_names_set - X_feature_names_set)

        def add_names(names):
            output = ""
            max_n_names = 5
            for i, name in enumerate(names):
                if i >= max_n_names:
                    output += "- ...\n"
                    break
                output += f"- {name}\n"
            return output

        if unexpected_names:
            message += "Feature names unseen at fit time:\n"
            message += add_names(unexpected_names)

        if missing_names:
            message += "Feature names seen at fit time, yet now missing:\n"
            message += add_names(missing_names)

        if not missing_names and not unexpected_names:
            message += "Feature names must be in the same order as they were in fit.\n"

        raise ValueError(message)

def _check_n_features(estimator, X, reset):
    """Set the `n_features_in_` attribute, or check against it on an estimator.

        :mod:`~sklearn.utils.validation`.

        If True, the `n_features_in_` attribute is set to `X.shape[1]`.
        If False and the attribute exists, then check that it is equal to
        `X.shape[1]`. If False and the attribute does *not* exist, then
        the check is skipped.
        .. note::
           It is recommended to call reset=True in `fit` and in the first
    """
    try:
        n_features = _num_features(X)
    except TypeError as e:
        if not reset and hasattr(estimator, "n_features_in_"):
                "X does not contain any features, but "
                f"{estimator.__class__.__name__} is expecting "
                f"{estimator.n_features_in_} features"
            ) from e
        # If the number of features is not defined and reset=True,
        # then we skip this check
        return

    if reset:
        estimator.n_features_in_ = n_features
        return

    if not hasattr(estimator, "n_features_in_"):
        # Skip this check if the expected number of expected input features
        # was not recorded by calling fit first. This is typically the case
        # for stateless transformers.
        return

    if n_features != estimator.n_features_in_:
            f"X has {n_features} features, but {estimator.__class__.__name__} "
            f"is expecting {estimator.n_features_in_} features as input."
        )

def validate_data(
    _estimator,
    /,
    X="no_validation",
    y="no_validation",
    reset=True,
    validate_separately=False,
    skip_check_array=False,
    **check_params,
):
    """Validate input data and set or check feature names and counts of the input.

    This helper function should be used in an estimator that requires input
    validation. This mutates the estimator and sets the `n_features_in_` and
    `feature_names_in_` attributes if `reset=True`.

    _estimator : estimator instance

    X : {array-like, sparse matrix, dataframe} of shape \
            (n_samples, n_features), default='no validation'
        If `'no_validation'`, no validation is performed on `X`. This is
        useful for meta-estimator which can delegate input validation to
        their underlying estimator(s). In that case `y` must be passed and
        the only accepted `check_params` are `multi_output` and
        `y_numeric`.

    y : array-like of shape (n_samples,), default='no_validation'

        - If `None`, :func:`~sklearn.utils.check_array` is called on `X`. If
          the estimator's `requires_y` tag is True, then an error will be raised.
        - If `'no_validation'`, :func:`~sklearn.utils.check_array` is called
          on `X` and the estimator's `requires_y` tag is ignored. This is a default
          placeholder and is never meant to be explicitly set. In that case `X` must be
          passed.
        - Otherwise, only `y` with `_check_y` or both `X` and `y` are checked with
          either :func:`~sklearn.utils.check_array` or
          :func:`~sklearn.utils.check_X_y` depending on `validate_separately`.

    reset : bool, default=True
        Whether to reset the `n_features_in_` attribute.
        If False, the input will be checked for consistency with data
        provided when reset was last True.

        .. note::

    validate_separately : False or tuple of dicts, default=False
        Only used if `y` is not `None`.
        If `False`, call :func:`~sklearn.utils.check_X_y`. Else, it must be a tuple of
        kwargs to be used for calling :func:`~sklearn.utils.check_array` on `X` and `y`
        respectively.

        `estimator=self` is automatically added to these dicts to generate
        more informative error message in case of invalid input data.

    skip_check_array : bool, default=False
        If `True`, `X` and `y` are unchanged and only `feature_names_in_` and
        `n_features_in_` are checked. Otherwise, :func:`~sklearn.utils.check_array`
        is called on `X` and `y`.

    **check_params : kwargs
        Parameters passed to :func:`~sklearn.utils.check_array` or
        :func:`~sklearn.utils.check_X_y`. Ignored if validate_separately
        is not False.

        `estimator=self` is automatically added to these params to generate

    Returns
    -------
    out : {ndarray, sparse matrix} or tuple of these
        The validated input. A tuple is returned if both `X` and `y` are
        validated.
    """
    _check_feature_names(_estimator, X, reset=reset)
    tags = get_tags(_estimator)
    if y is None and tags.target_tags.required:
            f"This {_estimator.__class__.__name__} estimator "
            "requires y to be passed, but the target y is None."
        )

    no_val_X = isinstance(X, str) and X == "no_validation"
    no_val_y = y is None or (isinstance(y, str) and y == "no_validation")

    if no_val_X and no_val_y:
        raise ValueError("Validation should be done on X, y or both.")

    default_check_params = {"estimator": _estimator}
    check_params = {**default_check_params, **check_params}

    if skip_check_array:
        if not no_val_X and no_val_y:
            out = X
        elif no_val_X and not no_val_y:
            out = y
        else:
            out = X, y
    elif not no_val_X and no_val_y:
        out = check_array(X, input_name="X", **check_params)
    elif no_val_X and not no_val_y:
        out = _check_y(y, **check_params)
    else:
        if validate_separately:
            # We need this because some estimators validate X and y
            # separately, and in general, separately calling check_array()
            # on X and y isn't equivalent to just calling check_X_y()
            # :(
            check_X_params, check_y_params = validate_separately
            if "estimator" not in check_X_params:
                check_X_params = {**default_check_params, **check_X_params}
            X = check_array(X, input_name="X", **check_X_params)
            if "estimator" not in check_y_params:
                check_y_params = {**default_check_params, **check_y_params}
            y = check_array(y, input_name="y", **check_y_params)
        else:
            X, y = check_X_y(X, y, **check_params)
        out = X, y

    if not no_val_X and check_params.get("ensure_2d", True):
        _check_n_features(_estimator, X, reset=reset)

    return out

        @wraps(f)

            ]
                (
                ),
            )

                else:

            ]
                (
                ),
            )

            output = ""
                    break

class KeyValTuple(tuple):
    """Dummy class for correctly rendering key-value tuples from dicts."""

        # needed for _dispatch[tuple.__repr__] not to be overridden
        return super().__repr__()

class KeyValTupleParam(KeyValTuple):
    """Dummy class for correctly rendering key-value tuples from parameters."""

    pass

def _changed_params(estimator):
    """Return dict (param_name: value) of parameters that were given to
    estimator with non-default values."""

    params = estimator.get_params(deep=False)
    init_func = getattr(estimator.__init__, "deprecated_original", estimator.__init__)
    init_params = inspect.signature(init_func).parameters
    init_params = {name: param.default for name, param in init_params.items()}

    def has_changed(k, v):
        if k not in init_params:  # happens if k is part of a **kwargs
        if init_params[k] == inspect._empty:  # k has no default value
        # try to avoid calling repr on nested estimators
        if isinstance(v, BaseEstimator) and v.__class__ != init_params[k].__class__:
        # Use repr as a last resort. It may be expensive.
        if repr(v) != repr(init_params[k]) and not (
            is_scalar_nan(init_params[k]) and is_scalar_nan(v)
        ):

    return {k: v for k, v in params.items() if has_changed(k, v)}

class _EstimatorPrettyPrinter(pprint.PrettyPrinter):
    """Pretty Printer class for estimator objects.

    This extends the pprint.PrettyPrinter class, because:
    - we need estimators to be printed with their parameters, e.g.
      Estimator(param1=value1, ...) which is not supported by default.
    - the 'compact' parameter of PrettyPrinter is ignored for dicts, which
      may lead to very long representations that we want to avoid.

    Quick overview of pprint.PrettyPrinter (see also
    https://stackoverflow.com/questions/49565047/pprint-with-hex-numbers):

    - the entry point is the _format() method which calls format() (overridden
      here)
    - format() directly calls _safe_repr() for a first try at rendering the
      object
    - _safe_repr formats the whole object recursively, only calling itself,
      not caring about line length or anything
    - back to _format(), if the output string is too long, _format() then calls
      the appropriate _pprint_TYPE() method (e.g. _pprint_list()) depending on
      the type of the object. This where the line length and the compact
      parameters are taken into account.
    - those _pprint_TYPE() methods will internally use the format() method for
      rendering the nested objects of an object (e.g. the elements of a list)

    In the end, everything has to be implemented twice: in _safe_repr and in
    the custom _pprint_TYPE methods. Unfortunately PrettyPrinter is really not
    straightforward to extend (especially when we want a compact output), so
    the code is a bit convoluted.

    This class overrides:
    - format() to support the changed_only parameter
    - _safe_repr to support printing of estimators (for when they fit on a
      single line)
    - _format_dict_items so that dict are correctly 'compacted'
    - _format_items so that ellipsis is used on long lists and tuples

    When estimators cannot be printed on a single line, the builtin _format()
    will call _pprint_estimator() because it was registered to do so (see
    _dispatch[BaseEstimator.__repr__] = _pprint_estimator).

    both _format_dict_items() and _pprint_estimator() use the
    _format_params_or_dict_items() method that will format parameters and
    key-value pairs respecting the compact parameter. This method needs another
    subroutine _pprint_key_val_tuple() used when a parameter or a key-value
    pair is too long to fit on a single line. This subroutine is called in
    _format() and is registered as well in the _dispatch dict (just like
    _pprint_estimator). We had to create the two classes KeyValTuple and
    KeyValTupleParam for this.
    """

        self,
        indent=1,
        width=80,
        depth=None,
        stream=None,
        *,
        compact=False,
        indent_at_name=True,
        n_max_elements_to_show=None,
    ):
        super().__init__(indent, width, depth, stream, compact=compact)
        self._indent_at_name = indent_at_name
        if self._indent_at_name:
            self._indent_per_level = 1  # ignore indent param
        self._changed_only = get_config()["print_changed_only"]
        # Max number of elements in a list, dict, tuple until we start using
        # ellipsis. This also affects the number of arguments of an estimators
        # (they are treated as dicts)
        self.n_max_elements_to_show = n_max_elements_to_show

    def format(self, object, context, maxlevels, level):
        return _safe_repr(
            object, context, maxlevels, level, changed_only=self._changed_only
        )

    def _pprint_estimator(self, object, stream, indent, allowance, context, level):
        stream.write(object.__class__.__name__ + "(")
            indent += len(object.__class__.__name__)

        if self._changed_only:
            params = _changed_params(object)
        else:
            params = object.get_params(deep=False)

        self._format_params(
            sorted(params.items()), stream, indent, allowance + 1, context, level
        )
        stream.write(")")

    def _format_dict_items(self, items, stream, indent, allowance, context, level):
        return self._format_params_or_dict_items(
            items, stream, indent, allowance, context, level, is_dict=True
        )

    def _format_params(self, items, stream, indent, allowance, context, level):
            items, stream, indent, allowance, context, level, is_dict=False
        )

    def _format_params_or_dict_items(
        self, object, stream, indent, allowance, context, level, is_dict
    ):
        """Format dict items or parameters respecting the compact=True
        parameter. For some reason, the builtin rendering of dict items doesn't
        respect compact=True and will use one line per key-value if all cannot
        fit in a single line.
        Dict items will be rendered as <'key': value> while params will be
        rendered as <key=value>. The implementation is mostly copy/pasting from
        the builtin _format_items().
        This also adds ellipsis if the number of items is greater than
        self.n_max_elements_to_show.
        """
        write = stream.write
        indent += self._indent_per_level
        delimnl = ",\n" + " " * indent
        delim = ""
        width = max_width = self._width - indent + 1
        it = iter(object)
        try:
            next_ent = next(it)
        except StopIteration:
            return
        last = False
        n_items = 0
        while not last:
            if n_items == self.n_max_elements_to_show:
                write(", ...")
                break
            n_items += 1
            ent = next_ent
            try:
                next_ent = next(it)
            except StopIteration:
                last = True
                max_width -= allowance
                width -= allowance
            if self._compact:
                k, v = ent
                krepr = self._repr(k, context, level)
                vrepr = self._repr(v, context, level)
                if not is_dict:
                    krepr = krepr.strip("'")
                middle = ": " if is_dict else "="
                rep = krepr + middle + vrepr
                w = len(rep) + 2
                if width < w:
                    width = max_width
                    if delim:
                        delim = delimnl
                if width >= w:
                    width -= w
                    write(delim)
                    delim = ", "
                    write(rep)
                    continue
            write(delim)
            delim = delimnl
            class_ = KeyValTuple if is_dict else KeyValTupleParam
            self._format(
                class_(ent), stream, indent, allowance if last else 1, context, level
            )

    def _format_items(self, items, stream, indent, allowance, context, level):
        """Format the items of an iterable (list, tuple...). Same as the
        built-in _format_items, with support for ellipsis if the number of
        elements is greater than self.n_max_elements_to_show.
        """
        if self._indent_per_level > 1:
            write((self._indent_per_level - 1) * " ")
        delim = ""
        it = iter(items)
        try:
            return
        n_items = 0
                break
            try:
                last = True
                rep = self._repr(ent, context, level)
                    if delim:
                    width -= w
                    delim = ", "
                    continue
            self._format(ent, stream, indent, allowance if last else 1, context, level)

    def _pprint_key_val_tuple(self, object, stream, indent, allowance, context, level):
        """Pretty printing for key-value tuples from dict or parameters."""
        k, v = object
        rep = self._repr(k, context, level)
        if isinstance(object, KeyValTupleParam):
            rep = rep.strip("'")
            middle = "="
        else:
            middle = ": "
        stream.write(rep)
        stream.write(middle)
        self._format(
            v, stream, indent + len(rep) + len(middle), allowance, context, level
        )

    # Note: need to copy _dispatch to prevent instances of the builtin
    # PrettyPrinter class to call methods of _EstimatorPrettyPrinter (see issue
    # 12906)
    # mypy error: "Type[PrettyPrinter]" has no attribute "_dispatch"
    _dispatch = pprint.PrettyPrinter._dispatch.copy()  # type: ignore[attr-defined]
    _dispatch[BaseEstimator.__repr__] = _pprint_estimator
    _dispatch[KeyValTuple.__repr__] = _pprint_key_val_tuple

def _safe_repr(object, context, maxlevels, level, changed_only=False):
    """Same as the builtin _safe_repr, with added support for Estimator
    objects."""
    typ = type(object)

    if typ in pprint._builtin_scalars:
        return repr(object), True, False

    r = getattr(typ, "__repr__", None)
    if issubclass(typ, dict) and r is dict.__repr__:
        if not object:
            return "{}", True, False
        objid = id(object)
        if maxlevels and level >= maxlevels:
            return "{...}", False, objid in context
        if objid in context:
            return pprint._recursion(object), False, True
        context[objid] = 1
        readable = True
        recursive = False
        components = []
        append = components.append
        level += 1
        saferepr = _safe_repr
        items = sorted(object.items(), key=pprint._safe_tuple)
        for k, v in items:
            krepr, kreadable, krecur = saferepr(
                k, context, maxlevels, level, changed_only=changed_only
            )
            vrepr, vreadable, vrecur = saferepr(
                v, context, maxlevels, level, changed_only=changed_only
            )
            append("%s: %s" % (krepr, vrepr))
            readable = readable and kreadable and vreadable
            if krecur or vrecur:
                recursive = True
        del context[objid]
        return "{%s}" % ", ".join(components), readable, recursive

    if (issubclass(typ, list) and r is list.__repr__) or (
        issubclass(typ, tuple) and r is tuple.__repr__
    ):
        if issubclass(typ, list):
            if not object:
                return "[]", True, False
            format = "[%s]"
        elif len(object) == 1:
            format = "(%s,)"
        else:
                return "()", True, False
            format = "(%s)"
            return format % "...", False, objid in context
        level += 1
        for o in object:
            orepr, oreadable, orecur = _safe_repr(
                o, context, maxlevels, level, changed_only=changed_only
            )
            append(orepr)
            if not oreadable:
                readable = False
            if orecur:
        return format % ", ".join(components), readable, recursive

    if issubclass(typ, BaseEstimator):
            return f"{typ.__name__}(...)", False, objid in context
        if changed_only:
        else:
        level += 1
        items = sorted(params.items(), key=pprint._safe_tuple)
            )
            )
            append("%s=%s" % (krepr.strip("'"), vrepr))
        return ("%s(%s)" % (typ.__name__, ", ".join(components)), readable, recursive)

    rep = repr(object)
    return rep, (rep and not rep.startswith("<")), False

        ):

        self,
        indent=1,
        width=80,
        *,
    ):

        )

        else:

        )

        )

        )

    ):
        """
        delim = ""
        try:
            return
        n_items = 0
                break
            try:
                last = True
                k, v = ent
                    if delim:
                    width -= w
                    delim = ", "
                    continue
            )

        """
        delim = ""
        try:
            return
        n_items = 0
                break
            try:
                last = True
                    if delim:
                    width -= w
                    delim = ", "
                    continue

        else:
        )

def _array_indexing(array, key, key_dtype, axis):
    """Index an array or scipy.sparse consistently across NumPy version."""
    xp, is_array_api = get_namespace(array)
        return xp.take(array, key, axis=axis)
    if issparse(array) and key_dtype == "bool":
        key = np.asarray(key)
    if isinstance(key, tuple):
        key = list(key)
    return array[key, ...] if axis == 0 else array[:, key]

def _pandas_indexing(X, key, key_dtype, axis):
    """Index a pandas dataframe or a series."""
    if _is_arraylike_not_scalar(key):

    if key_dtype == "int" and not (isinstance(key, slice) or np.isscalar(key)):
        # using take() instead of iloc[] ensures the return value is a "proper"
        # copy that will not raise SettingWithCopyWarning
        return X.take(key, axis=axis)
    else:
        # check whether we should index with loc or iloc
        indexer = X.iloc if key_dtype == "int" else X.loc
        return indexer[:, key] if axis else indexer[key]

def _list_indexing(X, key, key_dtype):
    """Index a Python list."""
    if np.isscalar(key) or isinstance(key, slice):
        # key is a slice or a scalar
        return X[key]
    if key_dtype == "bool":
        # key is a boolean array-like
        return list(compress(X, key))
    # key is a integer array-like of key
    return [X[idx] for idx in key]

def _polars_indexing(X, key, key_dtype, axis):
    """Indexing X with polars interchange protocol."""
    # Polars behavior is more consistent with lists
    if isinstance(key, np.ndarray):
        # Convert each element of the array to a Python scalar
        key = key.tolist()
    elif not (np.isscalar(key) or isinstance(key, slice)):

        # Here we are certain to have a polars DataFrame; which can be indexed with
        # integer and string scalar, and list of integer, string and boolean
        return X[:, key]

        # Boolean mask can be indexed in the same way for Series and DataFrame (axis=0)
        return X.filter(key)

    # Integer scalar and list of integer can be indexed in the same way for Series and
    # DataFrame (axis=0)
    X_indexed = X[key]
    if np.isscalar(key) and len(X.shape) == 2:
        # `X_indexed` is a DataFrame with a single row; we return a Series to be
        # consistent with pandas
        return pl.Series(X_indexed.row(0))
    return X_indexed

def _determine_key_type(key, accept_slice=True):
    """Determine the data type of key.

    key : scalar, slice or array-like
        The key from which we want to infer the data type.

    accept_slice : bool, default=True
        Whether or not to raise an error if the key is a slice.

    Returns
    -------
    dtype : {'int', 'str', 'bool', None}
        Returns the data type of key.
    """
    err_msg = (
        "No valid specification of the columns. Only a scalar, list or "
        "slice of all integers or all strings, or boolean mask is "
        "allowed"
    )

    dtype_to_str = {int: "int", str: "str", bool: "bool", np.bool_: "bool"}
    array_dtype_to_str = {
        "i": "int",
        "u": "int",
        "b": "bool",
        "O": "str",
        "U": "str",
        "S": "str",
    }

    if key is None:
    if isinstance(key, tuple(dtype_to_str.keys())):
        try:
            return dtype_to_str[type(key)]
    if isinstance(key, slice):
        if not accept_slice:
                "Only array-like or scalar are supported. A Python slice was given."
            )
        if key.start is None and key.stop is None:
        key_start_type = _determine_key_type(key.start)
        key_stop_type = _determine_key_type(key.stop)
        if key_start_type is not None and key_stop_type is not None:
            if key_start_type != key_stop_type:
        if key_start_type is not None:
            return key_start_type
        return key_stop_type
    # TODO(1.9) remove UserList when the force_int_remainder_cols param
    # of ColumnTransformer is removed
    if isinstance(key, (list, tuple, UserList)):
        unique_key = set(key)
        key_type = {_determine_key_type(elt) for elt in unique_key}
        if not key_type:
        if len(key_type) != 1:
        return key_type.pop()
    if hasattr(key, "dtype"):
        xp, is_array_api = get_namespace(key)
        # NumPy arrays are special-cased in their own branch because the Array API
        # cannot handle object/string-based dtypes that are often used to index
        # columns of dataframes by names.
        if is_array_api and not _is_numpy_namespace(xp):
            if xp.isdtype(key.dtype, "bool"):
                return "bool"
            elif xp.isdtype(key.dtype, "integral"):
                return "int"
            else:
        else:
            try:
                return array_dtype_to_str[key.dtype.kind]
            except KeyError:
    raise ValueError(err_msg)

def _safe_indexing(X, indices, *, axis=0):
    """Return rows, items or columns of X using indices.

        This utility is documented, but **private**. This means that
        backward compatibility might be broken without any deprecation
        cycle.

    X : array-like, sparse-matrix, list, pandas.DataFrame, pandas.Series
        Data from which to sample rows, items or columns. `list` are only
        supported when `axis=0`.
    indices : bool, int, str, slice, array-like
        - If `axis=0`, boolean and integer array-like, integer slice,
          and scalar integer are supported.
        - If `axis=1`:
            - to select a single column, `indices` can be of `int` type for
              all `X` types and `str` only for dataframe. The selected subset
              will be 1D, unless `X` is a sparse matrix in which case it will
              be 2D.
            - to select multiples columns, `indices` can be one of the
              following: `list`, `array`, `slice`. The type used in
              these containers can be one of the following: `int`, 'bool' and
              `str`. However, `str` is only supported when `X` is a dataframe.
              The selected subset will be 2D.
        The axis along which `X` will be subsampled. `axis=0` will select
        rows while `axis=1` will select columns.

    Returns
    -------
    subset
        Subset of X on axis 0 or 1.

    Notes
    -----
    CSR, CSC, and LIL sparse matrices are supported. COO sparse matrices are
    not supported.

    Examples
    --------
    >>> from sklearn.utils import _safe_indexing
    >>> data = np.array([[1, 2], [3, 4], [5, 6]])
    >>> _safe_indexing(data, 0, axis=0)  # select the first row
    >>> _safe_indexing(data, 0, axis=1)  # select the first column
    array([1, 3, 5])
    """
    if indices is None:
        return X

            "'axis' should be either 0 (to index rows) or 1 (to index "
            " column). Got {} instead.".format(axis)
        )

    indices_dtype = _determine_key_type(indices)

    if axis == 0 and indices_dtype == "str":
        raise ValueError("String indexing is not supported with 'axis=0'")

    if axis == 1 and isinstance(X, list):
        raise ValueError("axis=1 is not supported for lists")

    if axis == 1 and hasattr(X, "shape") and len(X.shape) != 2:
            "'X' should be a 2D NumPy array, 2D sparse matrix or "
            "dataframe when indexing the columns (i.e. 'axis=1'). "
            "Got {} instead with {} dimension(s).".format(type(X), len(X.shape))
        )

    if (
        axis == 1
        and indices_dtype == "str"
        and not (_is_pandas_df(X) or _use_interchange_protocol(X))
    ):
            "Specifying the columns using strings is only supported for dataframes."
        )

    if hasattr(X, "iloc"):
        # TODO: we should probably use _is_pandas_df_or_series(X) instead but this
        # would require updating some tests such as test_train_test_split_mock_pandas.
        return _pandas_indexing(X, indices, indices_dtype, axis=axis)
    elif _is_polars_df_or_series(X):
        return _polars_indexing(X, indices, indices_dtype, axis=axis)
    elif hasattr(X, "shape"):
        return _array_indexing(X, indices, indices_dtype, axis=axis)
    else:
        return _list_indexing(X, indices, indices_dtype)

def _safe_assign(X, values, *, row_indexer=None, column_indexer=None):
    """Safe assignment to a numpy array, sparse matrix, or pandas dataframe.

    X : {ndarray, sparse-matrix, dataframe}
        Array to be modified. It is expected to be 2-dimensional.

        The values to be assigned to `X`.

    row_indexer : array-like, dtype={int, bool}, default=None
        A 1-dimensional array to select the rows of interest. If `None`, all
        rows are selected.

    column_indexer : array-like, dtype={int, bool}, default=None
        A 1-dimensional array to select the columns of interest. If `None`, all
        columns are selected.
    """
    row_indexer = slice(None, None, None) if row_indexer is None else row_indexer
    column_indexer = (
        slice(None, None, None) if column_indexer is None else column_indexer
    )

    if hasattr(X, "iloc"):  # pandas dataframe
            # pandas >= 1.5 raises a warning when using iloc to set values in a column
            # that does not have the same type as the column being set. It happens
            # for instance when setting a categorical column with a string.
            # In the future the behavior won't change and the warning should disappear.
            # TODO(1.3): check if the warning is still raised or remove the filter.
            warnings.simplefilter("ignore", FutureWarning)
            X.iloc[row_indexer, column_indexer] = values
    else:  # numpy array or sparse matrix
        X[row_indexer, column_indexer] = values

def _get_column_indices_for_bool_or_int(key, n_columns):
    # Convert key into list of positive integer indexes
    try:
        idx = _safe_indexing(np.arange(n_columns), key)
    except IndexError as e:
            f"all features must be in [0, {n_columns - 1}] or [-{n_columns}, 0]"
        ) from e
    return np.atleast_1d(idx).tolist()

def _get_column_indices(X, key):
    """Get feature column indices for input data X and key.

    For accepted values of `key`, see the docstring of
    :func:`_safe_indexing`.
    """
    key_dtype = _determine_key_type(key)
    if _use_interchange_protocol(X):
        return _get_column_indices_interchange(X.__dataframe__(), key, key_dtype)

    n_columns = X.shape[1]
    if isinstance(key, (list, tuple)) and not key:
        # we get an empty list
        return []
    elif key_dtype in ("bool", "int"):
        return _get_column_indices_for_bool_or_int(key, n_columns)
    else:
        try:
            all_columns = X.columns
                "Specifying the columns using strings is only supported for dataframes."
            )
        if isinstance(key, str):
            columns = [key]
        elif isinstance(key, slice):
            start, stop = key.start, key.stop
            if start is not None:
                start = all_columns.get_loc(start)
            if stop is not None:
                # pandas indexing with strings is endpoint included
                stop = all_columns.get_loc(stop) + 1
            else:
                stop = n_columns + 1
            return list(islice(range(n_columns), start, stop))
        else:
            columns = list(key)

        try:
            column_indices = []
            for col in columns:
                col_idx = all_columns.get_loc(col)
                if not isinstance(col_idx, numbers.Integral):
                        f"Selected columns, {columns}, are not unique in dataframe"
                    )
                column_indices.append(col_idx)

            raise ValueError("A given column is not a column of the dataframe") from e

        return column_indices

def _get_column_indices_interchange(X_interchange, key, key_dtype):
    """Same as _get_column_indices but for X with __dataframe__ protocol."""

    n_columns = X_interchange.num_columns()

        return []
    else:
        column_names = list(X_interchange.column_names())

        if isinstance(key, slice):
            if key.step not in [1, None]:
                raise NotImplementedError("key.step must be 1 or None")
                start = column_names.index(start)

                stop = column_names.index(stop) + 1
            else:

        selected_columns = [key] if np.isscalar(key) else key

        try:
            return [column_names.index(col) for col in selected_columns]

def resample(
    *arrays,
    replace=True,
    stratify=None,
):
    """Resample arrays or sparse matrices in a consistent way.

    The default strategy implements one step of the bootstrapping
    procedure.

    *arrays : sequence of array-like of shape (n_samples,) or \
        Indexable data-structures can be arrays, lists, dataframes or scipy
        sparse matrices with consistent first dimension.

    replace : bool, default=True
        Implements resampling with replacement. It must be set to True
        whenever sampling with non-uniform weights: a few data points with very large
        weights are expected to be sampled several times with probability to preserve
        the distribution induced by the weights. If False, this will implement
        (sliced) random permutations.

        Number of samples to generate. If left to None this is
        automatically set to the first dimension of the arrays.
        If replace is False it should not be larger than the length of
        arrays.

        Determines random number generation for shuffling
        the data.

    stratify : {array-like, sparse matrix} of shape (n_samples,) or \
            (n_samples, n_outputs), default=None
        If not None, data is split in a stratified fashion, using this as
        the class labels.

        Contains weight values to be associated with each sample. Values are
        normalized to sum to one and interpreted as probability for sampling
        each data point.

    Returns
    -------
    resampled_arrays : sequence of array-like of shape (n_samples,) or \
        Sequence of resampled copies of the collections. The original arrays
        are not impacted.

    See Also
    --------
    shuffle : Shuffle arrays or sparse matrices in a consistent way.

    Examples
    --------
    It is possible to mix sparse and dense arrays in the same run::

      >>> import numpy as np
      >>> X = np.array([[1., 0.], [2., 1.], [0., 0.]])
      >>> y = np.array([0, 1, 2])

      >>> from scipy.sparse import coo_matrix
      >>> X_sparse = coo_matrix(X)

      >>> from sklearn.utils import resample
      >>> X, X_sparse, y = resample(X, X_sparse, y, random_state=0)
      >>> X
      array([[1., 0.],
             [2., 1.],
             [1., 0.]])

      >>> X_sparse
      <Compressed Sparse Row sparse matrix of dtype 'float64'
          with 4 stored elements and shape (3, 2)>

      >>> X_sparse.toarray()
             [2., 1.],
             [1., 0.]])

      >>> y
      array([0, 1, 0])

      >>> resample(y, n_samples=2, random_state=0)
      array([0, 1])

    Example using stratification::

      >>> y = [0, 0, 1, 1, 1, 1, 1, 1, 1]
      >>> resample(y, n_samples=5, replace=False, stratify=y,
      ...          random_state=0)
      [1, 1, 1, 0, 1]
    """
    max_n_samples = n_samples

    if len(arrays) == 0:

    first = arrays[0]
    n_samples = first.shape[0] if hasattr(first, "shape") else len(first)

    if max_n_samples is None:
        max_n_samples = n_samples
    elif (max_n_samples > n_samples) and (not replace):
            "Cannot sample %d out of arrays with dim %d when replace is False"
            % (max_n_samples, n_samples)
        )

    check_consistent_length(*arrays)

    if sample_weight is not None and not replace:
            "Resampling with sample_weight is only implemented for replace=True."
        )
    if sample_weight is not None and stratify is not None:
            "Resampling with sample_weight is only implemented for stratify=None."
        )
    if stratify is None:
        if replace:
                    sample_weight, first, dtype=np.float64
                )
                p = sample_weight / sample_weight.sum()
            else:
                p = None
                size=max_n_samples,
                p=p,
                replace=True,
            )
        else:
            indices = np.arange(n_samples)
            random_state.shuffle(indices)
            indices = indices[:max_n_samples]
    else:
        # Code adapted from StratifiedShuffleSplit()
        y = check_array(stratify, ensure_2d=False, dtype=None)
        if y.ndim == 2:
            # for multi-label y, map each distinct row to a string repr
            # using join because str(row) uses an ellipsis if len(row) > 1000
            y = np.array([" ".join(row.astype("str")) for row in y])

        classes, y_indices = np.unique(y, return_inverse=True)
        n_classes = classes.shape[0]

        class_counts = np.bincount(y_indices)

        # Find the sorted list of instances for each class:
        # (np.unique above performs a sort, so code is O(n logn) already)
        class_indices = np.split(
            np.argsort(y_indices, kind="mergesort"), np.cumsum(class_counts)[:-1]
        )

        n_i = _approximate_mode(class_counts, max_n_samples, random_state)

            indices_i = random_state.choice(class_indices[i], n_i[i], replace=replace)
            indices.extend(indices_i)

        indices = random_state.permutation(indices)

    # convert sparse matrices to CSR for row-based indexing
    arrays = [a.tocsr() if issparse(a) else a for a in arrays]
    resampled_arrays = [_safe_indexing(a, indices) for a in arrays]
    if len(resampled_arrays) == 1:
        # syntactic sugar for the unit argument case
        return resampled_arrays[0]
    else:
        return resampled_arrays

def shuffle(*arrays, random_state=None, n_samples=None):
    """Shuffle arrays or sparse matrices in a consistent way.

    This is a convenience alias to ``resample(*arrays, replace=False)`` to do
    random permutations of the collections.

    *arrays : sequence of indexable data-structures

        the data.

        automatically set to the first dimension of the arrays.  It should
        not be larger than the length of arrays.

    Returns
    -------
    shuffled_arrays : sequence of indexable data-structures
        Sequence of shuffled copies of the collections. The original arrays

    See Also
    --------
    resample : Resample arrays or sparse matrices in a consistent way.

    Examples
    --------

      >>> from sklearn.utils import shuffle
      >>> X, X_sparse, y = shuffle(X, X_sparse, y, random_state=0)
      >>> X
      array([[0., 0.],
             [2., 1.],
             [1., 0.]])

          with 3 stored elements and shape (3, 2)>

             [2., 1.],
             [1., 0.]])

      >>> y
      array([2, 1, 0])

      >>> shuffle(y, n_samples=2, random_state=0)
    """
    return resample(
        *arrays, replace=False, n_samples=n_samples, random_state=random_state
    )

def get_working_memory():
    return get_config()["working_memory"]

def test_configuration_passes_through_to_joblib(n_jobs, backend):
    # Tests that the global global configuration is passed to joblib jobs

    with config_context(working_memory=123):
        results = Parallel(n_jobs=n_jobs, backend=backend)(
            delayed(get_working_memory)() for _ in range(2)
        )

    assert_array_equal(results, [123] * 2)

def test_parallel_delayed_warnings():
    """Informative warnings should be raised when mixing sklearn and joblib API"""
    # We should issue a warning when one wants to use sklearn.utils.fixes.Parallel
    # with joblib.delayed. The config will not be propagated to the workers.
    warn_msg = "`sklearn.utils.parallel.Parallel` needs to be used in conjunction"
    with pytest.warns(UserWarning, match=warn_msg) as records:
        Parallel()(joblib.delayed(time.sleep)(0) for _ in range(10))
    assert len(records) == 10

    # We should issue a warning if one wants to use sklearn.utils.fixes.delayed with
    # joblib.Parallel
        "`sklearn.utils.parallel.delayed` should be used with "
        "`sklearn.utils.parallel.Parallel` to make it possible to propagate"
    )
        joblib.Parallel()(delayed(time.sleep)(0) for _ in range(10))

def test_dispatch_config_parallel(n_jobs):
    """Check that we properly dispatch the configuration in parallel processing.

    https://github.com/scikit-learn/scikit-learn/issues/25239
    """
    iris = load_iris(as_frame=True)

    class TransformerRequiredDataFrame(StandardScaler):
            assert isinstance(X, pd.DataFrame), "X should be a DataFrame"
            return super().fit(X, y)

            return super().transform(X, y)

    dropper = make_column_transformer(
        ("drop", [0]),
        remainder="passthrough",
    )
    param_grid = {"randomforestclassifier__max_depth": [1, 2, 3]}
    search_cv = GridSearchCV(
        make_pipeline(
            dropper,
            TransformerRequiredDataFrame(),
            RandomForestClassifier(n_estimators=5, n_jobs=n_jobs),
        ),
        param_grid,
        cv=5,
        error_score="raise",  # this search should not fail
    )

    # make sure that `fit` would fail in case we don't request dataframe
    with pytest.raises(AssertionError, match="X should be a DataFrame"):
        search_cv.fit(iris.data, iris.target)

        # we expect each intermediate steps to output a DataFrame

    assert not np.isnan(search_cv.cv_results_["mean_test_score"]).any()

def raise_warning():
    warnings.warn("Convergence warning", ConvergenceWarning)

def test_filter_warning_propagates(n_jobs, backend):
    """Check warning propagates to the job."""
        warnings.simplefilter("error", category=ConvergenceWarning)

        with pytest.raises(ConvergenceWarning):
            Parallel(n_jobs=n_jobs, backend=backend)(
                delayed(raise_warning)() for _ in range(2)
            )

def test_check_warnings_threading():
    """Check that warnings filters are set correctly in the threading backend."""

        filters = warnings.filters
        assert ("error", None, ConvergenceWarning, None, 0) in filters

        all_warnings = Parallel(n_jobs=2, backend="threading")(
            delayed(get_warnings)() for _ in range(2)
        )

        assert all(w == filters for w in all_warnings)

def test_filter_warning_propagates_no_side_effect_with_loky_backend():

        Parallel(n_jobs=2, backend="loky")(delayed(time.sleep)(0) for _ in range(10))

        # Since loky workers are reused, make sure that inside the loky workers,
        # warnings filters have been reset to their original value. Using joblib
        # directly should not turn ConvergenceWarning into an error.
        joblib.Parallel(n_jobs=2, backend="loky")(
            joblib.delayed(warnings.warn)("Convergence warning", ConvergenceWarning)
            for _ in range(10)
        )

def test_get_namespace_ndarray_default(X):
    """Check that get_namespace returns NumPy wrapper"""
    xp_out, is_array_api_compliant = get_namespace(X)
    assert xp_out is np_compat
    assert not is_array_api_compliant

def test_get_namespace_ndarray_creation_device():
    """Check expected behavior with device and creation functions."""
    X = numpy.asarray([1, 2, 3])
    xp_out, _ = get_namespace(X)

    full_array = xp_out.full(10, fill_value=2.0, device="cpu")
    assert_allclose(full_array, [2.0] * 10)

    with pytest.raises(ValueError, match="Unsupported device"):
        xp_out.zeros(10, device="cuda")

def test_get_namespace_ndarray_with_dispatch():
    """Test get_namespace on NumPy ndarrays."""

    X_np = numpy.asarray([[1, 2, 3]])

        xp_out, is_array_api_compliant = get_namespace(X_np)
        assert is_array_api_compliant

        # In the future, NumPy should become API compliant library and we should have
        # assert xp_out is numpy
        assert xp_out is np_compat

def test_get_namespace_array_api(monkeypatch):
    """Test get_namespace for ArrayAPI arrays."""

    X_xp = xp.asarray(X_np)
        xp_out, is_array_api_compliant = get_namespace(X_xp)

        with pytest.raises(TypeError):
            xp_out, is_array_api_compliant = get_namespace(X_xp, X_np)

        def mock_getenv(key):
            if key == "SCIPY_ARRAY_API":
                return "0"

        monkeypatch.setattr("os.environ.get", mock_getenv)
        assert os.environ.get("SCIPY_ARRAY_API") != "1"
            RuntimeError,
            match="scipy's own support is not enabled.",
        ):
            get_namespace(X_xp)

def test_asarray_with_order(array_api):
    """Test _asarray_with_order passes along order for NumPy arrays."""
    xp = pytest.importorskip(array_api)

    X = xp.asarray([1.2, 3.4, 5.1])
    X_new = _asarray_with_order(X, order="F", xp=xp)

    X_new_np = numpy.asarray(X_new)
    assert X_new_np.flags["F_CONTIGUOUS"]

def test_average(
    array_namespace, device_, dtype_name, weights, axis, normalize, expected
):
    xp = _array_api_for_tests(array_namespace, device_)
    array_in = numpy.asarray([[1, 2, 3], [4, 5, 6]], dtype=dtype_name)
    array_in = xp.asarray(array_in, device=device_)
        weights = numpy.asarray(weights, dtype=dtype_name)

        result = _average(array_in, axis=axis, weights=weights, normalize=normalize)

    if np_version < parse_version("2.0.0") or np_version >= parse_version("2.1.0"):
        # NumPy 2.0 has a problem with the device attribute of scalar arrays:
        # https://github.com/numpy/numpy/issues/26850
        assert device(array_in) == device(result)

    result = _convert_to_numpy(result, xp)
    assert_allclose(result, expected, atol=_atol_for_type(dtype_name))

def test_average_raises_with_wrong_dtype(array_namespace, device, dtype_name):

    array_in = numpy.asarray([2, 0], dtype=dtype_name) + 1j * numpy.asarray(
        [4, 3], dtype=dtype_name
    )
    complex_type_name = array_in.dtype.name
    if not hasattr(xp, complex_type_name):
        # This is the case for cupy as of March 2024 for instance.
        pytest.skip(f"{array_namespace} does not support {complex_type_name}")

    array_in = xp.asarray(array_in, device=device)

    err_msg = "Complex floating point values are not supported by average."
    with (
        config_context(array_api_dispatch=True),
        pytest.raises(NotImplementedError, match=err_msg),
    ):
        _average(array_in)

def test_average_raises_with_invalid_parameters(
    array_namespace, device, dtype_name, axis, weights, error, error_msg
):

    weights = numpy.asarray(weights, dtype=dtype_name)
    weights = xp.asarray(weights, device=device)

    with config_context(array_api_dispatch=True), pytest.raises(error, match=error_msg):
        _average(array_in, axis=axis, weights=weights)

def test_device_none_if_no_input():
    assert device() is None

    assert device(None, "name") is None

def test_device_inspection():
    class Device:
        def __init__(self, name):
            self.name = name

        def __eq__(self, device):
            return self.name == device.name

        def __hash__(self):
            raise TypeError("Device object is not hashable")

        def __str__(self):
            return self.name

    class Array:
        def __init__(self, device_name):
            self.device = Device(device_name)

    # Sanity check: ensure our Device mock class is non hashable, to
    # accurately account for non-hashable device objects in some array
    # libraries, because of which the `device` inspection function shouldn't
    # make use of hash lookup tables (in particular, not use `set`)
        hash(Array("device").device)

    # If array API dispatch is disabled the device should be ignored. Erroring
    # early for different devices would prevent the np.asarray conversion to
    # happen. For example, `r2_score(np.ones(5), torch.ones(5))` should work
    # fine with array API disabled.
    assert device(Array("cpu"), Array("mygpu")) is None

    # Test that ValueError is raised if on different devices and array API dispatch is
    # enabled.
    err_msg = "Input arrays use different devices: cpu, mygpu"
            device(Array("cpu"), Array("mygpu"))

        # Test expected value is returned otherwise
        array1 = Array("device")
        array2 = Array("device")

        assert array1.device == device(array1)
        assert array1.device == device(array1, array2)
        assert array1.device == device(array1, array1, array2)

def test_nan_reductions(library, X, reduction, expected):
    """Check NaN reductions like _nanmin and _nanmax"""
    xp = pytest.importorskip(library)

        result = reduction(xp.asarray(X))

    assert_allclose(result, expected)

def test_ravel(namespace, _device, _dtype):
    xp = _array_api_for_tests(namespace, _device)

    array = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
    array_xp = xp.asarray(array, device=_device)
        result = _ravel(array_xp)

    expected = numpy.ravel(array, order="C")

    assert_allclose(expected, result)

        assert numpy.asarray(result).flags["C_CONTIGUOUS"]

def test_convert_to_numpy_gpu(library):  # pragma: nocover
    """Check convert_to_numpy for GPU backed libraries."""

    if library == "torch":
        if not xp.backends.cuda.is_built():
            pytest.skip("test requires cuda")
        X_gpu = xp.asarray([1.0, 2.0, 3.0], device="cuda")
    else:
        X_gpu = xp.asarray([1.0, 2.0, 3.0])

    X_cpu = _convert_to_numpy(X_gpu, xp=xp)
    expected_output = numpy.asarray([1.0, 2.0, 3.0])
    assert_allclose(X_cpu, expected_output)

def test_convert_to_numpy_cpu():
    """Check convert_to_numpy for PyTorch CPU arrays."""
    torch = pytest.importorskip("torch")
    X_torch = torch.asarray([1.0, 2.0, 3.0], device="cpu")

    X_cpu = _convert_to_numpy(X_torch, xp=torch)

        self.X_ = X
        self.n_features_ = X.shape[0]

def test_convert_estimator_to_ndarray(array_namespace, converter):
    """Convert estimator attributes to ndarray."""
    xp = pytest.importorskip(array_namespace)

    X = xp.asarray([[1.3, 4.5]])
    est = SimpleEstimator().fit(X)

    new_est = _estimator_with_converted_arrays(est, converter)
    assert isinstance(new_est.X_, numpy.ndarray)

def test_convert_estimator_to_array_api():
    """Convert estimator attributes to ArrayAPI arrays."""

    X_np = numpy.asarray([[1.3, 4.5]])
    est = SimpleEstimator().fit(X_np)

    new_est = _estimator_with_converted_arrays(est, lambda array: xp.asarray(array))
    assert hasattr(new_est.X_, "__array_namespace__")

def test_indexing_dtype(namespace, _device, _dtype):

        assert indexing_dtype(xp) == xp.int32
    else:
        assert indexing_dtype(xp) == xp.int64

def test_max_precision_float_dtype(namespace, _device, _dtype):
    expected_dtype = xp.float32 if _device == "mps" else xp.float64
    assert _max_precision_float_dtype(xp, _device) == expected_dtype

def test_isin(
    array_namespace, device, _, invert, assume_unique, element_size, int_dtype
):
    r = element_size // 2
    element = 2 * numpy.arange(element_size).reshape((r, 2)).astype(int_dtype)
    test_elements = numpy.array(numpy.arange(14), dtype=int_dtype)
    element_xp = xp.asarray(element, device=device)
    test_elements_xp = xp.asarray(test_elements, device=device)
    expected = numpy.isin(
        element=element,
        test_elements=test_elements,
        assume_unique=assume_unique,
        invert=invert,
    )
        result = _isin(
            element=element_xp,
            test_elements=test_elements_xp,
            xp=xp,
        )

    assert_array_equal(_convert_to_numpy(result, xp=xp), expected)

def test_get_namespace_and_device():
    # Use torch as a library with custom Device objects:

    from sklearn.externals.array_api_compat import torch as torch_compat

    some_torch_tensor = torch.arange(3, device="cpu")
    some_numpy_array = numpy.arange(3)

    # When dispatch is disabled, get_namespace_and_device should return the
    # default NumPy wrapper namespace and "cpu" device. Our code will handle such
    # inputs via the usual __array__ interface without attempting to dispatch
    # via the array API.
    namespace, is_array_api, device = get_namespace_and_device(some_torch_tensor)
    assert namespace is get_namespace(some_numpy_array)[0]
    assert not is_array_api
    assert device is None

    # Otherwise, expose the torch namespace and device via array API compat
    # wrapper.
        namespace, is_array_api, device = get_namespace_and_device(some_torch_tensor)
        assert namespace is torch_compat
        assert is_array_api
        assert device == some_torch_tensor.device

def test_count_nonzero(
    array_namespace, device_, dtype_name, csr_container, axis, sample_weight_type
):
    from sklearn.utils.sparsefuncs import count_nonzero as sparse_count_nonzero

    array = numpy.array([[0, 3, 0], [2, -1, 0], [0, 0, 0], [9, 8, 7], [4, 0, 5]])
    if sample_weight_type == "int":
        sample_weight = numpy.asarray([1, 2, 2, 3, 1])
    elif sample_weight_type == "float":
        sample_weight = numpy.asarray([0.5, 1.5, 0.8, 3.2, 2.4], dtype=dtype_name)
    else:
    expected = sparse_count_nonzero(
        csr_container(array), axis=axis, sample_weight=sample_weight
    )
    array_xp = xp.asarray(array, device=device_)

        result = _count_nonzero(
            array_xp, axis=axis, sample_weight=sample_weight, xp=xp, device=device_
        )

    assert_allclose(_convert_to_numpy(result, xp=xp), expected)

        assert device(array_xp) == device(result)

def test_fill_or_add_to_diagonal(array_namespace, device_, dtype_name, wrap):
    array_np = numpy.zeros((5, 4), dtype=numpy.int64)
    array_xp = xp.asarray(array_np)
    _fill_or_add_to_diagonal(array_xp, value=1, xp=xp, add_value=False, wrap=wrap)
    numpy.fill_diagonal(array_np, val=1, wrap=wrap)
    assert_array_equal(_convert_to_numpy(array_xp, xp=xp), array_np)

def test_sparse_device(csr_container, dispatch):
    a, b = csr_container(numpy.array([[1]])), csr_container(numpy.array([[2]]))
    if dispatch and os.environ.get("SCIPY_ARRAY_API") is None:
    with config_context(array_api_dispatch=dispatch):
        assert device(a, b) is None
        assert device(a, numpy.array([1])) is None
        assert get_namespace_and_device(a, b)[2] is None
        assert get_namespace_and_device(a, numpy.array([1]))[2] is None

        self.X_ = X

                return "0"

def test_validate_score_name(score_name, scoring, negate_score, expected_score_name):
    """Check that we return the right score name."""
    assert (
        _validate_score_name(score_name, scoring, negate_score) == expected_score_name
    )

def test_inverval_max_min_ratio(data, lower_bound, upper_bound):
    assert lower_bound < _interval_max_min_ratio(data) < upper_bound

def test_validate_style_kwargs(default_kwargs, user_kwargs, expected):
    """Check the behaviour of `validate_style_kwargs` with various type of entries."""
    result = _validate_style_kwargs(default_kwargs, user_kwargs)
    assert result == expected, (
        "The validation of style keywords does not provide the expected results: "
        f"Got {result} instead of {expected}."
    )

def test_validate_style_kwargs_error(default_kwargs, user_kwargs):
    """Check that `validate_style_kwargs` raises TypeError"""
        _validate_style_kwargs(default_kwargs, user_kwargs)

def test_despine(pyplot):
    _despine(ax)
    assert ax.spines["top"].get_visible() is False
    assert ax.spines["right"].get_visible() is False
    assert ax.spines["bottom"].get_bounds() == (0, 1)
    assert ax.spines["left"].get_bounds() == (0, 1)

def test_write_label_html(checked):
    # Test checking logic and labeling
    name = "LogisticRegression"
    tool_tip = "hello-world"

        _write_label_html(out, name, tool_tip, checked=checked)
        html_label = out.getvalue()

        p = (
            r'<label for="sk-estimator-id-[0-9]*"'
            r' class="sk-toggleable__label (fitted)? sk-toggleable__label-arrow">'
            r"<div><div>LogisticRegression</div></div>"
        )
        re_compiled = re.compile(p)
        assert re_compiled.search(html_label)

        assert html_label.startswith('<div class="sk-label-container">')
        assert "<pre>hello-world</pre>" in html_label
        if checked:
            assert "checked>" in html_label

def test_get_visual_block_single_str_none(est):
    # Test estimators that are represented by strings
    est_html_info = _get_visual_block(est)
    assert est_html_info.kind == "single"
    assert est_html_info.estimators == est
    assert est_html_info.names == str(est)
    assert est_html_info.name_details == str(est)

def test_get_visual_block_single_estimator():
    est = LogisticRegression(C=10.0)
    assert est_html_info.names == est.__class__.__name__

def test_get_visual_block_pipeline():
        [
            ("imputer", SimpleImputer()),
            ("do_nothing", "passthrough"),
            ("do_nothing_more", None),
            ("classifier", LogisticRegression()),
        ]
    )
    est_html_info = _get_visual_block(pipe)
    assert est_html_info.kind == "serial"
    assert est_html_info.estimators == tuple(step[1] for step in pipe.steps)
    assert est_html_info.names == [
        "imputer: SimpleImputer",
        "do_nothing: passthrough",
        "do_nothing_more: passthrough",
        "classifier: LogisticRegression",
    ]
    assert est_html_info.name_details == [str(est) for _, est in pipe.steps]

def test_get_visual_block_feature_union():
    f_union = FeatureUnion([("pca", PCA()), ("svd", TruncatedSVD())])
    est_html_info = _get_visual_block(f_union)
    assert est_html_info.kind == "parallel"
    assert est_html_info.names == ("pca", "svd")
    assert est_html_info.estimators == tuple(
        trans[1] for trans in f_union.transformer_list
    )
    assert est_html_info.name_details == (None, None)

def test_get_visual_block_voting():
    clf = VotingClassifier(
        [("log_reg", LogisticRegression()), ("mlp", MLPClassifier())]
    )
    est_html_info = _get_visual_block(clf)
    assert est_html_info.estimators == tuple(trans[1] for trans in clf.estimators)
    assert est_html_info.names == ("log_reg", "mlp")

def test_get_visual_block_column_transformer():
    ct = ColumnTransformer(
        [("pca", PCA(), ["num1", "num2"]), ("svd", TruncatedSVD, [0, 3])]
    )
    est_html_info = _get_visual_block(ct)
    assert est_html_info.estimators == tuple(trans[1] for trans in ct.transformers)
    assert est_html_info.name_details == (["num1", "num2"], [0, 3])

def test_estimator_html_repr_an_empty_pipeline():
    """Check that the representation of an empty Pipeline does not fail.

    """
    empty_pipeline = Pipeline([])
    estimator_html_repr(empty_pipeline)

def test_estimator_html_repr_pipeline():
    num_trans = Pipeline(
        steps=[("pass", "passthrough"), ("imputer", SimpleImputer(strategy="median"))]
    )

    cat_trans = Pipeline(
        steps=[
            ("imputer", SimpleImputer(strategy="constant", missing_values="empty")),
            ("one-hot", OneHotEncoder(drop="first")),
        ]
    )

        [
            ("num", num_trans, ["a", "b", "c", "d", "e"]),
            ("cat", cat_trans, [0, 1, 2, 3]),
        ]
    )

    feat_u = FeatureUnion(
        [
            ("pca", PCA(n_components=1)),
            (
                "tsvd",
                Pipeline(
                    [
                        ("first", TruncatedSVD(n_components=3)),
                        ("select", SelectPercentile()),
                    ]
                ),
            ),
        ]
    )

        [
            ("lr", LogisticRegression(solver="lbfgs", random_state=1)),
            ("mlp", MLPClassifier(alpha=0.001)),
        ]
    )

        [("preprocessor", preprocess), ("feat_u", feat_u), ("classifier", clf)]
    )
    html_output = estimator_html_repr(pipe)

    # top level estimators show estimator with changes
    assert html.escape(str(pipe)) in html_output
    for _, est in pipe.steps:
        assert (
            '<div class="sk-toggleable__content "><pre>' + html.escape(str(est))
        ) in html_output

    # low level estimators do not show changes
    with config_context(print_changed_only=True):
        assert html.escape(str(num_trans["pass"])) in html_output
        assert "<div><div>passthrough</div></div></label>" in html_output
        assert html.escape(str(num_trans["imputer"])) in html_output

        for _, _, cols in preprocess.transformers:
            assert f"<pre>{html.escape(str(cols))}</pre>" in html_output

        # feature union
        for name, _ in feat_u.transformer_list:
            assert f"<label>{html.escape(name)}</label>" in html_output

        pca = feat_u.transformer_list[0][1]
        assert f"<pre>{html.escape(str(pca))}</pre>" in html_output

        tsvd = feat_u.transformer_list[1][1]
        first = tsvd["first"]
        select = tsvd["select"]
        assert f"<pre>{html.escape(str(first))}</pre>" in html_output
        assert f"<pre>{html.escape(str(select))}</pre>" in html_output

        # voting classifier
        for name, est in clf.estimators:
            assert f"<pre>{html.escape(str(est))}</pre>" in html_output

    # verify that prefers-color-scheme is implemented
    assert "prefers-color-scheme" in html_output

def test_stacking_classifier(final_estimator):
        ("mlp", MLPClassifier(alpha=0.001)),
        ("tree", DecisionTreeClassifier()),
    ]
    clf = StackingClassifier(estimators=estimators, final_estimator=final_estimator)

    html_output = estimator_html_repr(clf)

    assert html.escape(str(clf)) in html_output
    # If final_estimator's default changes from LogisticRegression
    # this should be updated
        assert "LogisticRegression(" in html_output
    else:
        assert final_estimator.__class__.__name__ in html_output

def test_stacking_regressor(final_estimator):
        estimators=[("svr", LinearSVR())], final_estimator=final_estimator
    )
    html_output = estimator_html_repr(reg)

    assert html.escape(str(reg.estimators[0][0])) in html_output
    p = (
        r'<label for="sk-estimator-id-[0-9]*"'
        r' class="sk-toggleable__label (fitted)? sk-toggleable__label-arrow">'
        r"<div><div>LinearSVR</div></div>"
    )
    re_compiled = re.compile(p)
    assert re_compiled.search(html_output)

        p = (
            r"<div><div>RidgeCV</div></div>"
        )
        assert re_compiled.search(html_output)
    else:
        assert html.escape(final_estimator.__class__.__name__) in html_output

def test_birch_duck_typing_meta():
    # Test duck typing meta estimators with Birch
    birch = Birch(n_clusters=AgglomerativeClustering(n_clusters=3))
    html_output = estimator_html_repr(birch)

    # inner estimators do not show changes
        assert f"<pre>{html.escape(str(birch.n_clusters))}" in html_output

        p = r"<div><div>AgglomerativeClustering</div></div><div>.+</div></label>"

    # outer estimator contains all changes
    assert f"<pre>{html.escape(str(birch))}" in html_output

def test_ovo_classifier_duck_typing_meta():
    # Test duck typing metaestimators with OVO
    ovo = OneVsOneClassifier(LinearSVC(penalty="l1"))
    html_output = estimator_html_repr(ovo)

        assert f"<pre>{html.escape(str(ovo.estimator))}" in html_output
        # regex to match the start of the tag
        p = (
            r'<label for="sk-estimator-id-[0-9]*" '
            r'class="sk-toggleable__label  sk-toggleable__label-arrow">'
            r"<div><div>LinearSVC</div></div>"
        )

    # outer estimator
    assert f"<pre>{html.escape(str(ovo))}" in html_output

def test_duck_typing_nested_estimator():
    # Test duck typing metaestimators with random search
    kernel_ridge = KernelRidge(kernel=ExpSineSquared())
    param_distributions = {"alpha": [1, 2]}

    kernel_ridge_tuned = RandomizedSearchCV(
        kernel_ridge,
        param_distributions=param_distributions,
    )
    html_output = estimator_html_repr(kernel_ridge_tuned)
    assert "<div><div>estimator: KernelRidge</div></div></label>" in html_output

def test_one_estimator_print_change_only(print_changed_only):
    pca = PCA(n_components=10)

    with config_context(print_changed_only=print_changed_only):
        pca_repr = html.escape(str(pca))
        html_output = estimator_html_repr(pca)
        assert pca_repr in html_output

def test_fallback_exists():
    """Check that repr fallback is in the HTML."""
    html_output = estimator_html_repr(pca)

    assert (
        f'<div class="sk-text-repr-fallback"><pre>{html.escape(str(pca))}'
        in html_output
    )

def test_show_arrow_pipeline():
    """Show arrow in pipeline for top level in pipeline"""
    pipe = Pipeline([("scale", StandardScaler()), ("log_Reg", LogisticRegression())])

    assert (
        'class="sk-toggleable__label  sk-toggleable__label-arrow">'
        "<div><div>Pipeline</div></div>" in html_output
    )

def test_invalid_parameters_in_stacking():
    """Invalidate stacking configuration uses default repr.

    Non-regression test for #24009.
    """
    stacker = StackingClassifier(estimators=[])

    html_output = estimator_html_repr(stacker)
    assert html.escape(str(stacker)) in html_output

def test_estimator_get_params_return_cls():
    """Check HTML repr works where a value in get_params is a class."""

    class MyEstimator:
        def get_params(self, deep=False):
            return {"inner_cls": LogisticRegression}

    assert "MyEstimator" in estimator_html_repr(est)

def test_estimator_html_repr_unfitted_vs_fitted():
    """Check that we have the information that the estimator is fitted or not in the
    HTML representation.
    """

    estimator = MyEstimator()
    assert "<span>Not fitted</span>" in estimator_html_repr(estimator)
    assert "<span>Fitted</span>" in estimator_html_repr(estimator)

def test_estimator_html_repr_fitted_icon(estimator):
    """Check that we are showing the fitted status icon only once."""
    pattern = '<span class="sk-estimator-doc-link ">i<span>Not fitted</span></span>'
    assert estimator_html_repr(estimator).count(pattern) == 1
    pattern = '<span class="sk-estimator-doc-link fitted">i<span>Fitted</span></span>'

def test_html_documentation_link_mixin_sklearn(mock_version):
    """Check the behaviour of the `_HTMLDocumentationLinkMixin` class for scikit-learn
    default.
    """

    # mock the `__version__` where the mixin is located
    with patch("sklearn.utils._estimator_html_repr.__version__", mock_version):
        mixin = _HTMLDocumentationLinkMixin()

        assert mixin._doc_link_module == "sklearn"
        sklearn_version = parse_version(mock_version)
        # we need to parse the version manually to be sure that this test is passing in
        # other branches than `main` (that is "dev").
            version = f"{sklearn_version.major}.{sklearn_version.minor}"
        else:
            version = "dev"
        assert (
            mixin._doc_link_template
            == f"https://scikit-learn.org/{version}/modules/generated/"
            "{estimator_module}.{estimator_name}.html"
        )
        assert (
            mixin._get_doc_link()
            "sklearn.utils._HTMLDocumentationLinkMixin.html"
        )

def test_html_documentation_link_mixin_get_doc_link_instance(
    module_path, expected_module
):
    """Check the behaviour of the `_get_doc_link` with various parameter."""

    class FooBar(_HTMLDocumentationLinkMixin):
        pass

    FooBar.__module__ = module_path
    est = FooBar()
    # if we set `_doc_link`, then we expect to infer a module and name for the estimator
    est._doc_link_module = "prefix"
    est._doc_link_template = (
        "https://website.com/{estimator_module}.{estimator_name}.html"
    )
    assert est._get_doc_link() == f"https://website.com/{expected_module}.FooBar.html"

def test_html_documentation_link_mixin_get_doc_link_class(module_path, expected_module):
    """Check the behaviour of the `_get_doc_link` when `_doc_link_module` and
    `_doc_link_template` are defined at the class level and not at the instance
    level."""

        _doc_link_module = "prefix"
        _doc_link_template = (
            "https://website.com/{estimator_module}.{estimator_name}.html"
        )

def test_html_documentation_link_mixin_get_doc_link_out_of_library():
    mixin = _HTMLDocumentationLinkMixin()

    # if the `_doc_link_module` does not refer to the root module of the estimator
    # (here the mixin), then we should return an empty string.
    mixin._doc_link_module = "xxx"
    assert mixin._get_doc_link() == ""

def test_html_documentation_link_mixin_doc_link_url_param_generator_instance():
    # we can bypass the generation by providing our own callable
    mixin._doc_link_template = (
        "https://website.com/{my_own_variable}.{another_variable}.html"
    )

    def url_param_generator(estimator):
        return {
            "my_own_variable": "value_1",
            "another_variable": "value_2",
        }

    mixin._doc_link_url_param_generator = types.MethodType(url_param_generator, mixin)

    assert mixin._get_doc_link() == "https://website.com/value_1.value_2.html"

def test_html_documentation_link_mixin_doc_link_url_param_generator_class():

        return {
        }

            "https://website.com/{my_own_variable}.{another_variable}.html"
        )
        _doc_link_url_param_generator = url_param_generator

    estimator = FooBar()
    assert estimator._get_doc_link() == "https://website.com/value_1.value_2.html"

def set_non_utf8_locale():
    """Pytest fixture to set non utf-8 locale during the test.

    The locale is set to the original one after the test has run.
    """
    try:
        locale.setlocale(locale.LC_CTYPE, "C")
    except locale.Error:
        pytest.skip("'C' locale is not available on this OS")

    yield

    # Resets the locale to the original one. Python calls setlocale(LC_TYPE, "")
    # at startup according to
    # https://docs.python.org/3/library/locale.html#background-details-hints-tips-and-caveats.
    # This assumes that no other locale changes have been made. For some reason,
    # on some platforms, trying to restore locale with something like
    # locale.setlocale(locale.LC_CTYPE, locale.getlocale()) raises a
    # locale.Error: unsupported locale setting
    locale.setlocale(locale.LC_CTYPE, "")

def test_non_utf8_locale(set_non_utf8_locale):
    """Checks that utf8 encoding is used when reading the CSS file.

    Non-regression test for https://github.com/scikit-learn/scikit-learn/issues/27725
    """
    _get_css_style()

def test_function_transformer_show_caption(func, expected_name):
    # Test that function name is shown as the name and "FunctionTransformer" is shown
    # in the caption
    ft = FunctionTransformer(func)
    html_output = estimator_html_repr(ft)

    p = (
        r'<label for="sk-estimator-id-[0-9]*" class="sk-toggleable__label fitted '
        rf'sk-toggleable__label-arrow"><div><div>{expected_name}</div>'
        r'<div class="caption">FunctionTransformer</div></div>'
    )

        )

        return {
        }

        )

def test_bunch_attribute_deprecation():
    """Check that bunch raises deprecation message with `__getattr__`."""
    bunch = Bunch()
    values = np.asarray([1, 2, 3])
    msg = (
        "Key: 'values', is deprecated in 1.3 and will be "
        "removed in 1.5. Please use 'grid_values' instead"
    )
    bunch._set_deprecated(
        values, new_key="grid_values", deprecated_key="values", warning_message=msg
    )

        # Does not warn for "grid_values"
        v = bunch["grid_values"]

    assert v is values

        # Warns for "values"
        v = bunch["values"]

def test_types(type_t, value, expected_dtype):
    """Check that the types defined in _typedefs correspond to the expected
    numpy dtypes.
    """
    assert testing_make_array_from_typed_val[type_t](value).dtype == expected_dtype

    keys = np.unique(rng.randint(100, size=10).astype(np.intp))

    assert len(d) == len(keys)

    d.append(120, 3.0)
    assert d[120] == 3.0
    assert len(d) == len(keys) + 1
    for i in range(2000):
        d.append(i + 1000, 4.0)
    assert d[1100] == 4.0

def test_int_float_dict_argmin():
    # Test the argmin implementation on the IntFloatDict
    keys = np.arange(100, dtype=np.intp)
    values = np.arange(100, dtype=np.float64)
    assert argmin(d) == (0, 0)

def test_to_arrays():
    # Test that an IntFloatDict is converted into arrays
    # of keys and values correctly
    keys_in = np.array([1, 2, 3], dtype=np.intp)
    values_in = np.array([4, 5, 6], dtype=np.float64)

    d = IntFloatDict(keys_in, values_in)
    keys_out, values_out = d.to_arrays()

    assert keys_out.dtype == keys_in.dtype
    assert values_in.dtype == values_out.dtype
    assert_array_equal(keys_out, keys_in)
    assert_allclose(values_out, values_in)

class ClassifierEstimator:
    # This is to test whether not inheriting from mixins works.

class EmptyTransformer(TransformerMixin, BaseEstimator):
    pass

def test_requires_y(estimator, value):
    assert get_tags(estimator).target_tags.required == value

def test_no___sklearn_tags__with_more_tags():
    """Test that calling `get_tags` on a class that defines `_more_tags` but not
    `__sklearn_tags__` raises an error.
    """

    class MoreTagsEstimator(BaseEstimator):
        def _more_tags(self):
            return {"requires_y": True}  # pragma: no cover

        TypeError, match="has defined either `_more_tags` or `_get_tags`"
    ):
        check_estimator_tags_renamed("MoreTagsEstimator", MoreTagsEstimator())

def test_tag_test_passes_with_inheritance():
    class MyTags(Tags):
        my_tag: bool = True  # type: ignore[annotation-unchecked]

            tags_orig = super().__sklearn_tags__()
            as_dict = {
                field.name: getattr(tags_orig, field.name)
                for field in fields(tags_orig)
            }
            tags = MyTags(**as_dict)
            tags.my_tag = True

    check_valid_tag_types("MyEstimator", MyEstimator())

def test_tags_no_sklearn_tags_concrete_implementation():
    """Non-regression test for:
    https://github.com/scikit-learn/scikit-learn/issues/30479

    Either the estimator doesn't implement `__sklearn_tags` or there is no class
    implementing `__sklearn_tags__` without calling `super().__sklearn_tags__()` in
    its mro. Thus, we raise a warning and request to inherit from
    `BaseEstimator` that implements `__sklearn_tags__`.
    """

    X = np.array([[1, 2], [2, 3], [3, 4]])
    y = np.array([1, 0, 1])

    # 1st case, the estimator inherits from a class that only implements
    # `__sklearn_tags__` by calling `super().__sklearn_tags__()`.
    class MyEstimator(ClassifierMixin):
        def __init__(self, *, param=1):

            self.is_fitted_ = True

            return np.full(shape=X.shape[0], fill_value=self.param)

    my_pipeline = Pipeline([("estimator", MyEstimator(param=1))])
    with pytest.warns(DeprecationWarning, match="The following error was raised"):
        my_pipeline.fit(X, y).predict(X)

    # 2nd case, the estimator doesn't implement `__sklearn_tags__` at all.
    class MyEstimator2:

    my_pipeline = Pipeline([("estimator", MyEstimator2(param=1))])

    # check that we still raise an error if it is not a AttributeError or related to
    # __sklearn_tags__
    class MyEstimator3(MyEstimator, BaseEstimator):
        def __init__(self, *, param=1, error_type=AttributeError):
            self.error_type = error_type

            super().__sklearn_tags__()
            raise self.error_type("test")

    for error_type in (AttributeError, TypeError, ValueError):
        estimator = MyEstimator3(param=1, error_type=error_type)
        with pytest.raises(error_type):
            get_tags(estimator)

            as_dict = {
            }

            as_dict = {
            }

def test_set_random_state():
    lda = LinearDiscriminantAnalysis()
    set_random_state(lda, 3)
    set_random_state(tree, 3)
    assert tree.random_state == 3

def test_assert_allclose_dense_sparse(csr_container):
    x = np.arange(9).reshape(3, 3)
    msg = "Not equal to tolerance "
    y = csr_container(x)
    for X in [x, y]:
        # basic compare
            assert_allclose_dense_sparse(X, X * 2)
        assert_allclose_dense_sparse(X, X)

    with pytest.raises(ValueError, match="Can only compare two sparse"):
        assert_allclose_dense_sparse(x, y)

    A = sparse.diags(np.ones(5), offsets=0).tocsr()
    B = csr_container(np.ones((1, 5)))
    with pytest.raises(AssertionError, match="Arrays are not equal"):
        assert_allclose_dense_sparse(B, A)

def test_ignore_warning():
    # This check that ignore_warning decorator and context manager are working
    # as expected
    def _warning_function():
        warnings.warn("deprecation warning", DeprecationWarning)

    def _multiple_warning_function():
        warnings.warn("deprecation warning")

    # Check the function directly

        ignore_warnings(_warning_function)
        ignore_warnings(_warning_function, category=DeprecationWarning)

        ignore_warnings(_warning_function, category=UserWarning)()

    with pytest.warns() as record:
        ignore_warnings(_multiple_warning_function, category=FutureWarning)()
    assert len(record) == 2
    assert isinstance(record[0].message, DeprecationWarning)
    assert isinstance(record[1].message, UserWarning)

        ignore_warnings(_multiple_warning_function, category=UserWarning)()

        ignore_warnings(_warning_function, category=(DeprecationWarning, UserWarning))

    # Check the decorator
    @ignore_warnings
    def decorator_no_warning():
        _warning_function()
        _multiple_warning_function()

    @ignore_warnings(category=(DeprecationWarning, UserWarning))
    def decorator_no_warning_multiple():

    @ignore_warnings(category=DeprecationWarning)
    def decorator_no_deprecation_warning():

    @ignore_warnings(category=UserWarning)
    def decorator_no_user_warning():

    def decorator_no_deprecation_multiple_warning():

    def decorator_no_user_multiple_warning():

        decorator_no_warning()
        decorator_no_warning_multiple()
        decorator_no_deprecation_warning()

        decorator_no_user_warning()
        decorator_no_deprecation_multiple_warning()
        decorator_no_user_multiple_warning()

    # Check the context manager
    def context_manager_no_warning():
            _warning_function()

    def context_manager_no_warning_multiple():
        with ignore_warnings(category=(DeprecationWarning, UserWarning)):
            _multiple_warning_function()

    def context_manager_no_deprecation_warning():

    def context_manager_no_user_warning():
        with ignore_warnings(category=UserWarning):

    def context_manager_no_deprecation_multiple_warning():

    def context_manager_no_user_multiple_warning():

        context_manager_no_warning()
        context_manager_no_warning_multiple()
        context_manager_no_deprecation_warning()

        context_manager_no_user_warning()
        context_manager_no_deprecation_multiple_warning()
        context_manager_no_user_multiple_warning()

    # Check that passing warning class as first positional argument
    warning_class = UserWarning
    match = "'obj' should be a callable.+you should use 'category=UserWarning'"

        silence_warnings_func = ignore_warnings(warning_class)(_warning_function)
        silence_warnings_func()

        @ignore_warnings(warning_class)
        def test():
            pass

def f_ok(a, b):
    """Function f

    a : int
        Parameter a
    b : float
        Parameter b

    Returns
    -------
    c : list
        Parameter c
    """
    c = a + b
    return c

def f_bad_sections(a, b):

    a : int
    b : float

    Results
    -------
    c : list
    """
    c = a + b
    return c

def f_bad_order(b, a):

    a : int
    b : float

    Returns
    -------
    c : list
    """
    c = a + b
    return c

def f_too_many_param_docstring(a, b):

    a : int
    b : int
    c : int

    Returns
    -------
    d : list
    """
    d = a + b
    return d

def f_missing(a, b):

    a : int

    Returns
    -------
    c : list
    """
    c = a + b
    return c

def f_check_param_definition(a, b, c, d, e):

    a: int
    b:
    c :
        This is parsed correctly in numpydoc 1.2
    d:int
        Parameter d
    e
        No typespec is allowed without colon
    """
    return a + b + c + d

class Klass:
    def f_missing(self, X, y):
        pass

    def f_bad_sections(self, X, y):
        """Function f

        Parameter
        ---------
        a : int
            Parameter a
        b : float
            Parameter b

        Results
        -------
        c : list
            Parameter c
        """
        pass

class MockEst:
        """MockEstimator"""

        return X

        return X

        return X

    def score(self, X):
        return 1.0

class MockMetaEstimator:
    def __init__(self, delegate):
        """MetaEstimator to check if doctest on delegated methods work.

        ---------
        delegate : estimator
            Delegated estimator.
        """
        self.delegate = delegate

    @available_if(lambda self: hasattr(self.delegate, "predict"))
        """This is available only if delegate has predict.

        y : ndarray
            Parameter y
        """
        return self.delegate.predict(X)

    @available_if(lambda self: hasattr(self.delegate, "score"))
    @deprecated("Testing a deprecated delegated method")
        """This is available only if delegate has score.

        ---------
        y : ndarray
        """

    @available_if(lambda self: hasattr(self.delegate, "predict_proba"))
        """This is available only if delegate has predict_proba.

        ---------
        X : ndarray
            Parameter X
        """
        return X

    @deprecated("Testing deprecated function with wrong params")
        """Incorrect docstring but should not be tested"""

def test_check_docstring_parameters():
    incorrect = check_docstring_parameters(f_ok)
    assert incorrect == []
    incorrect = check_docstring_parameters(f_ok, ignore=["b"])
    incorrect = check_docstring_parameters(f_missing, ignore=["b"])
    with pytest.raises(RuntimeError, match="Unknown section Results"):
        check_docstring_parameters(f_bad_sections)
    with pytest.raises(RuntimeError, match="Unknown section Parameter"):
        check_docstring_parameters(Klass.f_bad_sections)

    incorrect = check_docstring_parameters(f_check_param_definition)
    mock_meta = MockMetaEstimator(delegate=MockEst())
    mock_meta_name = mock_meta.__class__.__name__
    assert incorrect == [
        (
            "sklearn.utils.tests.test_testing.f_check_param_definition There "
            "was no space between the param name and colon ('a: int')"
        ),
        (
            "was no space between the param name and colon ('b:')"
        ),
        (
            "was no space between the param name and colon ('d:int')"
        ),
    ]

    messages = [
        [
            "In function: sklearn.utils.tests.test_testing.f_bad_order",
            (
                "There's a parameter name mismatch in function docstring w.r.t."
                " function signature, at index 0 diff: 'b' != 'a'"
            ),
            "Full diff:",
            "- ['b', 'a']",
            "+ ['a', 'b']",
        ],
        [
            "In function: sklearn.utils.tests.test_testing.f_too_many_param_docstring",
            (
                "Parameters in function docstring have more items w.r.t. function"
                " signature, first extra item: c"
            ),
            "- ['a', 'b']",
            "+ ['a', 'b', 'c']",
            "?          +++++",
        ],
        [
            "In function: sklearn.utils.tests.test_testing.f_missing",
            (
                "Parameters in function docstring have less items w.r.t. function"
                " signature, first missing item: b"
            ),
            "+ ['a']",
        ],
        [
            "In function: sklearn.utils.tests.test_testing.Klass.f_missing",
            (
                " signature, first missing item: X"
            ),
            "- ['X', 'y']",
            "+ []",
        ],
        [
            f"In function: sklearn.utils.tests.test_testing.{mock_meta_name}.predict",
            (
                " function signature, at index 0 diff: 'X' != 'y'"
            ),
            "- ['X']",
            "?   ^",
            "+ ['y']",
            "?   ^",
        ],
        [
            "In function: "
            f"sklearn.utils.tests.test_testing.{mock_meta_name}."
            "predict_proba",
            "potentially wrong underline length... ",
            "Parameters ",
            "--------- in ",
        ],
        [
            f"In function: sklearn.utils.tests.test_testing.{mock_meta_name}.score",
        ],
        [
            f"In function: sklearn.utils.tests.test_testing.{mock_meta_name}.fit",
            (
            ),
            "+ []",
        ],
    ]

    for msg, f in zip(
        messages,
        [
            f_bad_order,
            f_too_many_param_docstring,
            f_missing,
            Klass.f_missing,
            mock_meta.predict,
            mock_meta.predict_proba,
            mock_meta.score,
            mock_meta.fit,
        ],
    ):
        incorrect = check_docstring_parameters(f)
        assert msg == incorrect, '\n"%s"\n not in \n"%s"' % (msg, incorrect)

def f_one(a, b):  # pragma: no cover
    """Function one.

    a : int,   float
        Parameter a.
        Second    line.

    b : str
        Parameter b.

    Returns
    -------
    c : int
       Returning

    d : int
       Returning
    """
    pass

def f_two(a, b):  # pragma: no cover
    """Function two.

    a :   int, float
          Second line.

    b : str
        Parameter bb.

    e : int
        Extra parameter.

    Returns
    -------
    c : int
       Returning

    d : int
       Returning
    """
    pass

def f_three(a, b):  # pragma: no cover

    b : str
        Parameter B!

    e :

    Returns
    -------
    c : int
       Returning.

    d : int
       Returning
    """
    pass

def test_assert_docstring_consistency_object_type():
    """Check error raised when `objects` incorrect type."""
    with pytest.raises(TypeError, match="All 'objects' must be one of"):
        assert_docstring_consistency(["string", f_one])

def test_assert_docstring_consistency_arg_checks(objects, kwargs, error):
    """Check `assert_docstring_consistency` argument checking correct."""
    with pytest.raises(TypeError, match=error):
        assert_docstring_consistency(objects, **kwargs)

def test_assert_docstring_consistency(objects, kwargs, error, warn):
    """Check `assert_docstring_consistency` gives correct results."""
    if error:
        with pytest.raises(AssertionError, match=error):
            assert_docstring_consistency(objects, **kwargs)
    elif warn:
        with pytest.warns(UserWarning, match=warn):
    else:

def f_four(labels):  # pragma: no cover
    """Function four.

        order if `average is None`. Labels present in the data can be excluded.
    """
    pass

def f_five(labels):  # pragma: no cover
    """Function five.

        order if `average is None`. This is an extra line. Labels present in the
        data can be excluded.
    """
    pass

def f_six(labels):  # pragma: no cover
    """Function six.

        The group of labels to add when `average != 'binary'`, and the
        order if `average is None`. Labels present on them datas can be excluded.
    """
    pass

def test_assert_docstring_consistency_error_msg():
    """Check `assert_docstring_consistency` difference message."""
    msg = r"""The description of Parameter 'labels' is inconsistent between
\['f_four'\] and \['f_five'\] and \['f_six'\]:

\*\*\* \['f_four'\]
--- \['f_five'\]
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*

\*\*\* 10,25 \*\*\*\*

--- 10,30 ----

  'binary'`, and their order if `average is None`.
\+ This is an extra line.
  Labels present in the data can be excluded.

--- \['f_six'\]

\*\*\* 1,25 \*\*\*\*

  The
! set
  of labels to
! include
  when `average != 'binary'`, and
! their
  order if `average is None`. Labels present
! in the data
  can be excluded.
--- 1,25 ----

  The
! group
! add
! the
! on them datas
  can be excluded."""

        assert_docstring_consistency([f_four, f_five, f_six], include_params=True)

def test_assert_docstring_consistency_descr_regex_pattern():
    """Check `assert_docstring_consistency` `descr_regex_pattern` works."""
    # Check regex that matches full parameter descriptions
    regex_full = (
        r"The (set|group) "  # match 'set' or 'group'
        r"of labels to (include|add) "  # match 'include' or 'add'
        r"when `average \!\= 'binary'`, and (their|the) "  #  match 'their' or 'the'
        r"order if `average is None`\."
        r"[\s\w]*\.* "  # optionally match additional sentence
        r"Labels present (on|in) "  # match 'on' or 'in'
        r"(them|the) "  # match 'them' or 'the'
        r"datas? can be excluded\."  # match 'data' or 'datas'
    )

    assert_docstring_consistency(
        [f_four, f_five, f_six],
        include_params=True,
        descr_regex_pattern=" ".join(regex_full.split()),
    )
    # Check we can just match a few alternate words
    regex_words = r"(labels|average|binary)"  # match any of these 3 words
        descr_regex_pattern=" ".join(regex_words.split()),
    )
    # Check error raised when regex doesn't match
    regex_error = r"The set of labels to include when.+"
    msg = r"The description of Parameter 'labels' in \['f_six'\] does not match"
        assert_docstring_consistency(
            [f_four, f_five, f_six],
            include_params=True,
            descr_regex_pattern=" ".join(regex_error.split()),
        )

class RegistrationCounter:
        self.nb_calls = 0

    def __call__(self, to_register_func):
        self.nb_calls += 1
        assert to_register_func.func is _delete_folder

def check_memmap(input_array, mmap_data, mmap_mode="r"):
    assert isinstance(mmap_data, np.memmap)
    writeable = mmap_mode != "r"
    assert mmap_data.flags.writeable is writeable
    np.testing.assert_array_equal(input_array, mmap_data)

def test_tempmemmap(monkeypatch):
    registration_counter = RegistrationCounter()
    monkeypatch.setattr(atexit, "register", registration_counter)

    input_array = np.ones(3)
    with TempMemmap(input_array) as data:
        check_memmap(input_array, data)
        temp_folder = os.path.dirname(data.filename)
    if os.name != "nt":
        assert not os.path.exists(temp_folder)
    assert registration_counter.nb_calls == 1

    mmap_mode = "r+"
    with TempMemmap(input_array, mmap_mode=mmap_mode) as data:
        check_memmap(input_array, data, mmap_mode=mmap_mode)
    assert registration_counter.nb_calls == 2

def test_create_memmap_backed_data(monkeypatch):

    data = create_memmap_backed_data(input_array)
    check_memmap(input_array, data)

    data, folder = create_memmap_backed_data(input_array, return_folder=True)
    assert folder == os.path.dirname(data.filename)

    data = create_memmap_backed_data(input_array, mmap_mode=mmap_mode)
    check_memmap(input_array, data, mmap_mode)
    assert registration_counter.nb_calls == 3

    input_list = [input_array, input_array + 1, input_array + 2]
    mmap_data_list = create_memmap_backed_data(input_list)
    for input_array, data in zip(input_list, mmap_data_list):
    assert registration_counter.nb_calls == 4

    output_data, other = create_memmap_backed_data([input_array, "not-an-array"])
    check_memmap(input_array, output_data)
    assert other == "not-an-array"

def test_convert_container(
    container_type,
    dtype,
    superdtype,
):
    """Check that we convert the container to the right type of array with the
    right data type."""
    if constructor_name in ("dataframe", "polars", "series", "polars_series", "index"):
        # delay the import of pandas/polars within the function to only skip this test
        # instead of the whole file
        container_type = container_type()
    container = [0, 1]

    container_converted = _convert_container(
        container,
        constructor_name,
    )
    assert isinstance(container_converted, container_type)

    if constructor_name in ("list", "tuple", "index"):
        # list and tuple will use Python class dtype: int, float
        # pandas index will always use high precision: np.int64 and np.float64
        assert np.issubdtype(type(container_converted[0]), superdtype)
    elif hasattr(container_converted, "dtype"):
        assert container_converted.dtype == dtype
    elif hasattr(container_converted, "dtypes"):
        assert container_converted.dtypes[0] == dtype

def test_convert_container_categories_pandas():
        [["x"]], "dataframe", ["A"], categorical_feature_names=["A"]
    )
    assert df.dtypes.iloc[0] == "category"

def test_convert_container_categories_polars():
    pl = pytest.importorskip("polars")
    df = _convert_container([["x"]], "polars", ["A"], categorical_feature_names=["A"])
    assert df.schema["A"] == pl.Categorical()

def test_convert_container_categories_pyarrow():
    pa = pytest.importorskip("pyarrow")
    df = _convert_container([["x"]], "pyarrow", ["A"], categorical_feature_names=["A"])
    assert type(df.schema[0].type) is pa.DictionaryType

def test_raises():
    # Tests for the raises context manager

    # Proper type, no match
    with raises(TypeError):
        raise TypeError()

    # Proper type, proper match
    with raises(TypeError, match="how are you") as cm:
        raise TypeError("hello how are you")
    assert cm.raised_and_matched

    # Proper type, proper match with multiple patterns
    with raises(TypeError, match=["not this one", "how are you"]) as cm:

    # bad type, no match
    with pytest.raises(ValueError, match="this will be raised"):
        with raises(TypeError) as cm:
            raise ValueError("this will be raised")
    assert not cm.raised_and_matched

    # Bad type, no match, with a err_msg
    with pytest.raises(AssertionError, match="the failure message"):
        with raises(TypeError, err_msg="the failure message") as cm:

    # bad type, with match (is ignored anyway)
        with raises(TypeError, match="this is ignored") as cm:

    # proper type but bad match
        AssertionError, match="should contain one of the following patterns"
    ):
        with raises(TypeError, match="hello") as cm:
            raise TypeError("Bad message")

    # proper type but bad match, with err_msg
        with raises(TypeError, match="hello", err_msg="the failure message") as cm:

    # no raise with default may_pass=False
    with pytest.raises(AssertionError, match="Did not raise"):
            pass

    # no raise with may_pass=True
    with raises(TypeError, match="hello", may_pass=True) as cm:
        pass  # still OK

    # Multiple exception types:
    with raises((TypeError, ValueError)):
        raise ValueError()
        with raises((TypeError, ValueError)):
            pass

def test_float32_aware_assert_allclose():
    # The relative tolerance for float32 inputs is 1e-4
    assert_allclose(np.array([1.0 + 2e-5], dtype=np.float32), 1.0)
        assert_allclose(np.array([1.0 + 2e-4], dtype=np.float32), 1.0)

    # The relative tolerance for other inputs is left to 1e-7 as in
    # the original numpy version.
    assert_allclose(np.array([1.0 + 2e-8], dtype=np.float64), 1.0)
        assert_allclose(np.array([1.0 + 2e-7], dtype=np.float64), 1.0)

    # atol is left to 0.0 by default, even for float32
        assert_allclose(np.array([1e-5], dtype=np.float32), 0.0)
    assert_allclose(np.array([1e-5], dtype=np.float32), 0.0, atol=2e-5)

def test_assert_run_python_script_without_output():
    code = "x = 1"
    assert_run_python_script_without_output(code)

    code = "print('something to stdout')"
    with pytest.raises(AssertionError, match="Expected no output"):
        assert_run_python_script_without_output(code)

        AssertionError,
        match="output was not supposed to match.+got.+something to stdout",
    ):
        assert_run_python_script_without_output(code, pattern="to.+stdout")

    code = "\n".join(["import sys", "print('something to stderr', file=sys.stderr)"])
        match="output was not supposed to match.+got.+something to stderr",
    ):
        assert_run_python_script_without_output(code, pattern="to.+stderr")

def test_convert_container_sparse_to_sparse(constructor_name):
    """Non-regression test to check that we can still convert a sparse container
    from a given format to another format.
    """
    X_sparse = sparse.random(10, 10, density=0.1, format="csr")
    _convert_container(X_sparse, constructor_name)

def check_warnings_as_errors(warning_info, warnings_as_errors):
    if warning_info.action == "error" and warnings_as_errors:
        with pytest.raises(warning_info.category, match=warning_info.message):
                message=warning_info.message,
                category=warning_info.category,
            )
    if warning_info.action == "ignore":
            message = warning_info.message
            # Special treatment when regex is used
            if "Pyarrow" in message:
                message = "\nPyarrow will become a required dependency"

            )
            assert len(record) == 0 if warnings_as_errors else 1
            if record:
                assert str(record[0].message) == message
                assert record[0].category == warning_info.category

def test_sklearn_warnings_as_errors(warning_info):
    warnings_as_errors = os.environ.get("SKLEARN_WARNINGS_AS_ERRORS", "0") != "0"
    check_warnings_as_errors(warning_info, warnings_as_errors=warnings_as_errors)

def test_turn_warnings_into_errors(warning_info):
        turn_warnings_into_errors()
        check_warnings_as_errors(warning_info, warnings_as_errors=True)

        Parameter
        ---------
        a : int
        b : float

        Results
        -------
        c : list
        """
        pass

        ---------
        """

        y : ndarray
        """

        ---------
        y : ndarray
        """

        ---------
        X : ndarray
        """
        return X

def test_density(sparse_container):
    X = rng.randint(10, size=(10, 5))
    X[1, 2] = 0
    X[5, 3] = 0

    assert density(sparse_container(X)) == density(X)

def test_uniform_weights():
    # with uniform weights, results should be identical to stats.mode
    x = rng.randint(10, size=(10, 5))
    weights = np.ones(x.shape)

    for axis in (None, 0, 1):
        mode, score = _mode(x, axis)
        mode2, score2 = weighted_mode(x, weights, axis=axis)

        assert_array_equal(mode, mode2)
        assert_array_equal(score, score2)

def test_random_weights():
    # set this up so that each row should have a weighted mode of 6,
    # with a score that is easily reproduced
    mode_result = 6

    x = rng.randint(mode_result, size=(100, 10))
    w = rng.random_sample(x.shape)

    x[:, :5] = mode_result
    w[:, :5] += 1

    mode, score = weighted_mode(x, w, axis=1)

    assert_array_equal(mode, mode_result)
    assert_array_almost_equal(score.ravel(), w[:, :5].sum(1))

def test_randomized_svd_low_rank_all_dtypes(dtype):
    # Check that extmath.randomized_svd is consistent with linalg.svd
    rank = 5
    k = 10
    decimal = 5 if dtype == np.float32 else 7
    dtype = np.dtype(dtype)

    # generate a matrix X of approximate effective rank `rank` and no noise
    # component (very structured signal):
        effective_rank=rank,
        tail_strength=0.0,
    ).astype(dtype, copy=False)

    # compute the singular values of X using the slow exact method

    # Convert the singular values to the specific dtype
    U = U.astype(dtype, copy=False)
    s = s.astype(dtype, copy=False)
    Vt = Vt.astype(dtype, copy=False)

    for normalizer in ["auto", "LU", "QR"]:  # 'none' would not be stable
        # compute the singular values of X using the fast approximate method
        Ua, sa, Va = randomized_svd(
            X, k, power_iteration_normalizer=normalizer, random_state=0
        )

        # If the input dtype is float, then the output dtype is float of the
        # same bit size (f32 is not upcast to f64)
        # But if the input dtype is int, the output dtype is float64
        if dtype.kind == "f":
            assert Ua.dtype == dtype
            assert sa.dtype == dtype
            assert Va.dtype == dtype
        else:
            assert Ua.dtype == np.float64
            assert sa.dtype == np.float64
            assert Va.dtype == np.float64

        assert Ua.shape == (n_samples, k)
        assert sa.shape == (k,)
        assert Va.shape == (k, n_features)

        # ensure that the singular values of both methods are equal up to the
        # real rank of the matrix
        assert_almost_equal(s[:k], sa, decimal=decimal)

        # check the singular vectors too (while not checking the sign)
            np.dot(U[:, :k], Vt[:k, :]), np.dot(Ua, Va), decimal=decimal
        )

        # check the sparse matrix representation
            X = csr_container(X)

            # compute the singular values of X using the fast approximate method
            Ua, sa, Va = randomized_svd(
                X, k, power_iteration_normalizer=normalizer, random_state=0
            )
            if dtype.kind == "f":
                assert Ua.dtype == dtype
                assert sa.dtype == dtype
                assert Va.dtype == dtype
            else:
                assert Ua.dtype.kind == "f"
                assert sa.dtype.kind == "f"
                assert Va.dtype.kind == "f"

            assert_almost_equal(s[:rank], sa[:rank], decimal=decimal)

def test_randomized_eigsh(dtype):
    """Test that `_randomized_eigsh` returns the appropriate components"""

    X = np.diag(np.array([1.0, -2.0, 0.0, 3.0], dtype=dtype))
    # random rotation that preserves the eigenvalues of X
    rand_rot = np.linalg.qr(rng.normal(size=X.shape))[0]
    X = rand_rot @ X @ rand_rot.T

    # with 'module' selection method, the negative eigenvalue shows up
    eigvals, eigvecs = _randomized_eigsh(X, n_components=2, selection="module")
    # eigenvalues
    assert eigvals.shape == (2,)
    assert_array_almost_equal(eigvals, [3.0, -2.0])  # negative eigenvalue here
    # eigenvectors
    assert eigvecs.shape == (4, 2)

    # with 'value' selection method, the negative eigenvalue does not show up
        _randomized_eigsh(X, n_components=2, selection="value")

def test_randomized_eigsh_compared_to_others(k):
    """Check that `_randomized_eigsh` is similar to other `eigsh`

    Tests that for a random PSD matrix, `_randomized_eigsh` provides results
    comparable to LAPACK (scipy.linalg.eigh) and ARPACK
    (scipy.sparse.linalg.eigsh).

    Note: some versions of ARPACK do not support k=n_features.
    """

    # make a random PSD matrix
    n_features = 200
    X = make_sparse_spd_matrix(n_features, random_state=0)

    # compare two versions of randomized
    # rough and fast
    eigvals, eigvecs = _randomized_eigsh(
        X, n_components=k, selection="module", n_iter=25, random_state=0
    )
    # more accurate but slow (TODO find realistic settings here)
    eigvals_qr, eigvecs_qr = _randomized_eigsh(
        X,
        n_components=k,
        n_iter=25,
        n_oversamples=20,
        power_iteration_normalizer="QR",
        selection="module",
    )

    # with LAPACK
    eigvals_lapack, eigvecs_lapack = eigh(
        X, subset_by_index=(n_features - k, n_features - 1)
    )
    indices = eigvals_lapack.argsort()[::-1]
    eigvals_lapack = eigvals_lapack[indices]
    eigvecs_lapack = eigvecs_lapack[:, indices]

    # -- eigenvalues comparison
    assert eigvals_lapack.shape == (k,)
    # comparison precision
    assert_array_almost_equal(eigvals, eigvals_lapack, decimal=6)
    assert_array_almost_equal(eigvals_qr, eigvals_lapack, decimal=6)

    # -- eigenvectors comparison
    assert eigvecs_lapack.shape == (n_features, k)
    # flip eigenvectors' sign to enforce deterministic output
    dummy_vecs = np.zeros_like(eigvecs).T
    eigvecs, _ = svd_flip(eigvecs, dummy_vecs)
    eigvecs_qr, _ = svd_flip(eigvecs_qr, dummy_vecs)
    eigvecs_lapack, _ = svd_flip(eigvecs_lapack, dummy_vecs)
    assert_array_almost_equal(eigvecs, eigvecs_lapack, decimal=4)
    assert_array_almost_equal(eigvecs_qr, eigvecs_lapack, decimal=6)

    # comparison ARPACK ~ LAPACK (some ARPACK implems do not support k=n)
    if k < n_features:
        v0 = _init_arpack_v0(n_features, random_state=0)
        # "LA" largest algebraic <=> selection="value" in randomized_eigsh
        eigvals_arpack, eigvecs_arpack = eigsh(
            X, k, which="LA", tol=0, maxiter=None, v0=v0
        )
        indices = eigvals_arpack.argsort()[::-1]
        # eigenvalues
        eigvals_arpack = eigvals_arpack[indices]
        assert_array_almost_equal(eigvals_lapack, eigvals_arpack, decimal=10)
        eigvecs_arpack = eigvecs_arpack[:, indices]
        eigvecs_arpack, _ = svd_flip(eigvecs_arpack, dummy_vecs)
        assert_array_almost_equal(eigvecs_arpack, eigvecs_lapack, decimal=8)

def test_randomized_eigsh_reconst_low_rank(n, rank):
    """Check that randomized_eigsh is able to reconstruct a low rank psd matrix

    Tests that the decomposition provided by `_randomized_eigsh` leads to
    orthonormal eigenvectors, and that a low rank PSD matrix can be effectively
    reconstructed with good accuracy using it.
    """
    assert rank < n

    # create a low rank PSD
    rng = np.random.RandomState(69)
    X = rng.randn(n, rank)
    A = X @ X.T

    # approximate A with the "right" number of components
    S, V = _randomized_eigsh(A, n_components=rank, random_state=rng)
    # orthonormality checks
    assert_array_almost_equal(np.linalg.norm(V, axis=0), np.ones(S.shape))
    assert_array_almost_equal(V.T @ V, np.diag(np.ones(S.shape)))
    # reconstruction
    A_reconstruct = V @ np.diag(S) @ V.T

    # test that the approximation is good
    assert_array_almost_equal(A_reconstruct, A, decimal=6)

def test_row_norms(dtype, csr_container):
    X = np.random.RandomState(42).randn(100, 100)
    if dtype is np.float32:
        precision = 4
    else:
        precision = 5

    sq_norm = (X**2).sum(axis=1)

    assert_array_almost_equal(sq_norm, row_norms(X, squared=True), precision)
    assert_array_almost_equal(np.sqrt(sq_norm), row_norms(X), precision)

    for csr_index_dtype in [np.int32, np.int64]:
        Xcsr = csr_container(X, dtype=dtype)
        # csr_matrix will use int32 indices by default,
        # up-casting those to int64 when necessary
        if csr_index_dtype is np.int64:
            Xcsr.indptr = Xcsr.indptr.astype(csr_index_dtype, copy=False)
            Xcsr.indices = Xcsr.indices.astype(csr_index_dtype, copy=False)
        assert Xcsr.indices.dtype == csr_index_dtype
        assert Xcsr.indptr.dtype == csr_index_dtype
        assert_array_almost_equal(sq_norm, row_norms(Xcsr, squared=True), precision)
        assert_array_almost_equal(np.sqrt(sq_norm), row_norms(Xcsr), precision)

def test_randomized_svd_low_rank_with_noise():
    # Check that extmath.randomized_svd can handle noisy matrices
    rank = 5
    k = 10

    # generate a matrix X wity structure approximate rank `rank` and an
    # important noisy component
    )

    _, s, _ = linalg.svd(X, full_matrices=False)

    for normalizer in ["auto", "none", "LU", "QR"]:
        # compute the singular values of X using the fast approximate
        # method without the iterated power method
        _, sa, _ = randomized_svd(
            X, k, n_iter=0, power_iteration_normalizer=normalizer, random_state=0
        )

        # the approximation does not tolerate the noise:
        assert np.abs(s[:k] - sa).max() > 0.01

        # method with iterated power method
        _, sap, _ = randomized_svd(
        )

        # the iterated power method is helping getting rid of the noise:
        assert_almost_equal(s[:k], sap, decimal=3)

def test_randomized_svd_infinite_rank():
    rank = 5
    k = 10

    # let us try again without 'low_rank component': just regularly but slowly
    # decreasing singular values: the rank of the data matrix is infinite
        tail_strength=1.0,
    )

        # without the iterated power method
        )

        assert np.abs(s[:k] - sa).max() > 0.1

        # with iterated power method
            X, k, n_iter=5, power_iteration_normalizer=normalizer, random_state=0
        )

        # the iterated power method is still managing to get most of the
        # structure at the requested rank

def test_randomized_svd_transpose_consistency():
    # Check that transposing the design matrix has limited impact
    rank = 4
    k = 10

        tail_strength=0.5,
    )

    U1, s1, V1 = randomized_svd(X, k, n_iter=3, transpose=False, random_state=0)
    U2, s2, V2 = randomized_svd(X, k, n_iter=3, transpose=True, random_state=0)
    U3, s3, V3 = randomized_svd(X, k, n_iter=3, transpose="auto", random_state=0)
    U4, s4, V4 = linalg.svd(X, full_matrices=False)

    assert_almost_equal(s1, s4[:k], decimal=3)
    assert_almost_equal(s2, s4[:k], decimal=3)
    assert_almost_equal(s3, s4[:k], decimal=3)

    assert_almost_equal(np.dot(U1, V1), np.dot(U4[:, :k], V4[:k, :]), decimal=2)
    assert_almost_equal(np.dot(U2, V2), np.dot(U4[:, :k], V4[:k, :]), decimal=2)

    # in this case 'auto' is equivalent to transpose
    assert_almost_equal(s2, s3)

def test_randomized_svd_power_iteration_normalizer():
    # randomized_svd with power_iteration_normalized='none' diverges for
    # large number of power iterations on this dataset
    X = make_low_rank_matrix(100, 500, effective_rank=50, random_state=rng)
    X += 3 * rng.randint(0, 2, size=X.shape)
    n_components = 50

    # Check that it diverges with many (non-normalized) power iterations
    U, s, Vt = randomized_svd(
        X, n_components, n_iter=2, power_iteration_normalizer="none", random_state=0
    )
    A = X - U.dot(np.diag(s).dot(Vt))
    error_2 = linalg.norm(A, ord="fro")
        X, n_components, n_iter=20, power_iteration_normalizer="none", random_state=0
    )
    error_20 = linalg.norm(A, ord="fro")
    assert np.abs(error_2 - error_20) > 100

    for normalizer in ["LU", "QR", "auto"]:
        U, s, Vt = randomized_svd(
            X,
            n_iter=2,
            power_iteration_normalizer=normalizer,
        )
        A = X - U.dot(np.diag(s).dot(Vt))
        error_2 = linalg.norm(A, ord="fro")

        for i in [5, 10, 50]:
            U, s, Vt = randomized_svd(
                X,
                n_components,
                n_iter=i,
                power_iteration_normalizer=normalizer,
            )
            A = X - U.dot(np.diag(s).dot(Vt))
            error = linalg.norm(A, ord="fro")
            assert 15 > np.abs(error_2 - error)

def test_randomized_svd_sparse_warnings(sparse_container):
    # randomized_svd throws a warning for lil and dok matrix
    X = make_low_rank_matrix(50, 20, effective_rank=10, random_state=rng)

        "Calculating SVD of a {} is expensive. csr_matrix is more efficient.".format(
            sparse_container.__name__
        )
    )
    with pytest.warns(sparse.SparseEfficiencyWarning, match=warn_msg):
        randomized_svd(X, n_components, n_iter=1, power_iteration_normalizer="none")

def test_svd_flip():
    # Check that svd_flip works in both situations, and reconstructs input.
    rs = np.random.RandomState(1999)
    X = rs.randn(n_samples, n_features)

    # Check matrix reconstruction
    U, S, Vt = linalg.svd(X, full_matrices=False)
    U1, V1 = svd_flip(U, Vt, u_based_decision=False)
    assert_almost_equal(np.dot(U1 * S, V1), X, decimal=6)

    # Check transposed matrix reconstruction
    XT = X.T
    U, S, Vt = linalg.svd(XT, full_matrices=False)
    U2, V2 = svd_flip(U, Vt, u_based_decision=True)
    assert_almost_equal(np.dot(U2 * S, V2), XT, decimal=6)

    # Check that different flip methods are equivalent under reconstruction
    U_flip1, V_flip1 = svd_flip(U, Vt, u_based_decision=True)
    assert_almost_equal(np.dot(U_flip1 * S, V_flip1), XT, decimal=6)
    U_flip2, V_flip2 = svd_flip(U, Vt, u_based_decision=False)
    assert_almost_equal(np.dot(U_flip2 * S, V_flip2), XT, decimal=6)

def test_svd_flip_max_abs_cols(n_samples, n_features, global_random_seed):
    rs = np.random.RandomState(global_random_seed)
    U, _, Vt = linalg.svd(X, full_matrices=False)

    U1, _ = svd_flip(U, Vt, u_based_decision=True)
    max_abs_U1_row_idx_for_col = np.argmax(np.abs(U1), axis=0)
    assert (U1[max_abs_U1_row_idx_for_col, np.arange(U1.shape[1])] >= 0).all()

    _, V2 = svd_flip(U, Vt, u_based_decision=False)
    max_abs_V2_col_idx_for_row = np.argmax(np.abs(V2), axis=1)
    assert (V2[np.arange(V2.shape[0]), max_abs_V2_col_idx_for_row] >= 0).all()

def test_randomized_svd_sign_flip():
    a = np.array([[2.0, 0.0], [0.0, 1.0]])
    u1, s1, v1 = randomized_svd(a, 2, flip_sign=True, random_state=41)
        u2, s2, v2 = randomized_svd(a, 2, flip_sign=True, random_state=seed)
        assert_almost_equal(u1, u2)
        assert_almost_equal(v1, v2)
        assert_almost_equal(np.dot(u2 * s2, v2), a)
        assert_almost_equal(np.dot(u2.T, u2), np.eye(2))
        assert_almost_equal(np.dot(v2.T, v2), np.eye(2))

def test_randomized_svd_sign_flip_with_transpose():
    # Check if the randomized_svd sign flipping is always done based on u
    # irrespective of transpose.
    # See https://github.com/scikit-learn/scikit-learn/issues/5608
    def max_loading_is_positive(u, v):
        """
        returns bool tuple indicating if the values maximising np.abs
        are positive across all rows for u and across all columns for v.
        """
        u_based = (np.abs(u).max(axis=0) == u.max(axis=0)).all()
        v_based = (np.abs(v).max(axis=1) == v.max(axis=1)).all()
        return u_based, v_based

    mat = np.arange(10 * 8).reshape(10, -1)

    # Without transpose
    u_flipped, _, v_flipped = randomized_svd(mat, 3, flip_sign=True, random_state=0)
    u_based, v_based = max_loading_is_positive(u_flipped, v_flipped)
    assert u_based
    assert not v_based

    # With transpose
    u_flipped_with_transpose, _, v_flipped_with_transpose = randomized_svd(
        mat, 3, flip_sign=True, transpose=True, random_state=0
    )
    u_based, v_based = max_loading_is_positive(
        u_flipped_with_transpose, v_flipped_with_transpose
    )

def test_randomized_svd_lapack_driver(n, m, k, seed):
    # Check that different SVD drivers provide consistent results

    # Matrix being compressed
    X = rng.rand(n, m)

    # Number of components
    u1, s1, vt1 = randomized_svd(X, k, svd_lapack_driver="gesdd", random_state=0)
    u2, s2, vt2 = randomized_svd(X, k, svd_lapack_driver="gesvd", random_state=0)

    # Check shape and contents
    assert u1.shape == u2.shape
    assert_allclose(u1, u2, atol=0, rtol=1e-3)

    assert s1.shape == s2.shape
    assert_allclose(s1, s2, atol=0, rtol=1e-3)

    assert vt1.shape == vt2.shape
    assert_allclose(vt1, vt2, atol=0, rtol=1e-3)

def test_cartesian():
    # Check if cartesian product delivers the right results

    axes = (np.array([1, 2, 3]), np.array([4, 5]), np.array([6, 7]))

    true_out = np.array(
        [
            [1, 4, 6],
            [1, 4, 7],
            [1, 5, 6],
            [1, 5, 7],
            [2, 4, 6],
            [2, 4, 7],
            [2, 5, 6],
            [2, 5, 7],
            [3, 4, 6],
            [3, 4, 7],
            [3, 5, 6],
            [3, 5, 7],
        ]
    )

    out = cartesian(axes)
    assert_array_equal(true_out, out)

    # check single axis
    x = np.arange(3)
    assert_array_equal(x[:, np.newaxis], cartesian((x,)))

def test_cartesian_mix_types(arrays, output_dtype):
    """Check that the cartesian product works with mixed types."""
    output = cartesian(arrays)

    assert output.dtype == output_dtype

def test_incremental_weighted_mean_and_variance_simple(rng, dtype):
    mult = 10
    X = rng.rand(1000, 20).astype(dtype) * mult
    sample_weight = rng.rand(X.shape[0]) * mult
    mean, var, _ = _incremental_mean_and_var(X, 0, 0, 0, sample_weight=sample_weight)

    expected_mean = np.average(X, weights=sample_weight, axis=0)
    expected_var = np.average(X**2, weights=sample_weight, axis=0) - expected_mean**2
    assert_almost_equal(mean, expected_mean)
    assert_almost_equal(var, expected_var)

def test_incremental_weighted_mean_and_variance(
    mean, var, weight_loc, weight_scale, rng
):
    # Testing of correctness and numerical stability
    def _assert(X, sample_weight, expected_mean, expected_var):
        for chunk_size in [1, n // 10 + 1, n // 4 + 1, n // 2 + 1, n]:
            last_mean, last_weight_sum, last_var = 0, 0, 0
            for batch in gen_batches(n, chunk_size):
                last_mean, last_var, last_weight_sum = _incremental_mean_and_var(
                    X[batch],
                    last_mean,
                    last_var,
                    last_weight_sum,
                    sample_weight=sample_weight[batch],
                )
            assert_allclose(last_mean, expected_mean)
            assert_allclose(last_var, expected_var, atol=1e-6)

    size = (100, 20)
    weight = rng.normal(loc=weight_loc, scale=weight_scale, size=size[0])

    # Compare to weighted average: np.average
    X = rng.normal(loc=mean, scale=var, size=size)
    expected_mean = _safe_accumulator_op(np.average, X, weights=weight, axis=0)
    expected_var = _safe_accumulator_op(
        np.average, (X - expected_mean) ** 2, weights=weight, axis=0
    )
    _assert(X, weight, expected_mean, expected_var)

    # Compare to unweighted mean: np.mean
    ones_weight = np.ones(size[0])
    expected_mean = _safe_accumulator_op(np.mean, X, axis=0)
    expected_var = _safe_accumulator_op(np.var, X, axis=0)
    _assert(X, ones_weight, expected_mean, expected_var)

def test_incremental_weighted_mean_and_variance_ignore_nan(dtype):
    old_means = np.array([535.0, 535.0, 535.0, 535.0])
    old_variances = np.array([4225.0, 4225.0, 4225.0, 4225.0])
    old_weight_sum = np.array([2, 2, 2, 2], dtype=np.int32)
    sample_weights_X = np.ones(3)
    sample_weights_X_nan = np.ones(4)

        [[170, 170, 170, 170], [430, 430, 430, 430], [300, 300, 300, 300]]
    ).astype(dtype)

        [
            [170, np.nan, 170, 170],
            [np.nan, 170, 430, 430],
            [430, 430, np.nan, 300],
            [300, 300, 300, np.nan],
        ]

    X_means, X_variances, X_count = _incremental_mean_and_var(
        X, old_means, old_variances, old_weight_sum, sample_weight=sample_weights_X
    )
    X_nan_means, X_nan_variances, X_nan_count = _incremental_mean_and_var(
        X_nan,
        old_means,
        old_variances,
        old_weight_sum,
        sample_weight=sample_weights_X_nan,
    )

    assert_allclose(X_nan_means, X_means)
    assert_allclose(X_nan_variances, X_variances)
    assert_allclose(X_nan_count, X_count)

def test_incremental_variance_update_formulas():
    # Test Youngs and Cramer incremental variance formulas.
    # Doggie data from https://www.mathsisfun.com/data/standard-deviation.html
    A = np.array(
        [
            [600, 470, 170, 430, 300],
        ]
    ).T
    idx = 2
    X1 = A[:idx, :]
    X2 = A[idx:, :]

    old_means = X1.mean(axis=0)
    old_variances = X1.var(axis=0)
    old_sample_count = np.full(X1.shape[1], X1.shape[0], dtype=np.int32)
    final_means, final_variances, final_count = _incremental_mean_and_var(
        X2, old_means, old_variances, old_sample_count
    )
    assert_almost_equal(final_means, A.mean(axis=0), 6)
    assert_almost_equal(final_variances, A.var(axis=0), 6)
    assert_almost_equal(final_count, A.shape[0])

def test_incremental_mean_and_variance_ignore_nan():
    old_sample_count = np.array([2, 2, 2, 2], dtype=np.int32)

    X = np.array([[170, 170, 170, 170], [430, 430, 430, 430], [300, 300, 300, 300]])

        [
        ]
    )

        X, old_means, old_variances, old_sample_count
    )
        X_nan, old_means, old_variances, old_sample_count
    )

def test_incremental_variance_numerical_stability():

    def np_var(A):
        return A.var(axis=0)

    # Naive one pass variance computation - not numerically stable
    # https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
    def one_pass_var(X):
        exp_x2 = (X**2).sum(axis=0) / n
        expx_2 = (X.sum(axis=0) / n) ** 2
        return exp_x2 - expx_2

    # Two-pass algorithm, stable.
    # We use it as a benchmark. It is not an online algorithm
    # https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Two-pass_algorithm
    def two_pass_var(X):
        mean = X.mean(axis=0)
        Y = X.copy()
        return np.mean((Y - mean) ** 2, axis=0)

    # Naive online implementation
    # https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm
    # This works only for chunks for size 1
    def naive_mean_variance_update(x, last_mean, last_variance, last_sample_count):
        updated_sample_count = last_sample_count + 1
        samples_ratio = last_sample_count / float(updated_sample_count)
        updated_mean = x / updated_sample_count + last_mean * samples_ratio
        updated_variance = (
            last_variance * samples_ratio
            + (x - last_mean) * (x - updated_mean) / updated_sample_count
        )
        return updated_mean, updated_variance, updated_sample_count

    # We want to show a case when one_pass_var has error > 1e-3 while
    # _batch_mean_variance_update has less.
    tol = 200
    x1 = np.array(1e8, dtype=np.float64)
    x2 = np.log(1e-5, dtype=np.float64)
    A0 = np.full((n_samples // 2, n_features), x1, dtype=np.float64)
    A1 = np.full((n_samples // 2, n_features), x2, dtype=np.float64)
    A = np.vstack((A0, A1))

    # Naive one pass var: >tol (=1063)
    assert np.abs(np_var(A) - one_pass_var(A)).max() > tol

    # Starting point for online algorithms: after A0

    # Naive implementation: >tol (436)
    mean, var, n = A0[0, :], np.zeros(n_features), n_samples // 2
    for i in range(A1.shape[0]):
        mean, var, n = naive_mean_variance_update(A1[i, :], mean, var, n)
    assert n == A.shape[0]
    # the mean is also slightly unstable
    assert np.abs(A.mean(axis=0) - mean).max() > 1e-6
    assert np.abs(np_var(A) - var).max() > tol

    # Robust implementation: <tol (177)
    mean, var = A0[0, :], np.zeros(n_features)
    n = np.full(n_features, n_samples // 2, dtype=np.int32)
        mean, var, n = _incremental_mean_and_var(
            A1[i, :].reshape((1, A1.shape[1])), mean, var, n
        )
    assert_array_equal(n, A.shape[0])
    assert_array_almost_equal(A.mean(axis=0), mean)
    assert tol > np.abs(np_var(A) - var).max()

def test_incremental_variance_ddof():
    # Test that degrees of freedom parameter for calculations are correct.
    X = rng.randn(50, 10)
    for batch_size in [11, 20, 37]:
        steps = np.arange(0, X.shape[0], batch_size)
        if steps[-1] != X.shape[0]:
            steps = np.hstack([steps, n_samples])

        for i, j in itertools.pairwise(steps):
            batch = X[i:j, :]
            if i == 0:
                incremental_means = batch.mean(axis=0)
                incremental_variances = batch.var(axis=0)
                # Assign this twice so that the test logic is consistent
                incremental_count = batch.shape[0]
                sample_count = np.full(batch.shape[1], batch.shape[0], dtype=np.int32)
            else:
                result = _incremental_mean_and_var(
                    batch, incremental_means, incremental_variances, sample_count
                )
                (incremental_means, incremental_variances, incremental_count) = result
                sample_count += batch.shape[0]

            calculated_means = np.mean(X[:j], axis=0)
            calculated_variances = np.var(X[:j], axis=0)
            assert_almost_equal(incremental_means, calculated_means, 6)
            assert_almost_equal(incremental_variances, calculated_variances, 6)
            assert_array_equal(incremental_count, sample_count)

def test_vector_sign_flip():
    # Testing that sign flip is working & largest value has positive sign
    data = np.random.RandomState(36).randn(5, 5)
    max_abs_rows = np.argmax(np.abs(data), axis=1)
    data_flipped = _deterministic_vector_sign_flip(data)
    max_rows = np.argmax(data_flipped, axis=1)
    assert_array_equal(max_abs_rows, max_rows)
    signs = np.sign(data[range(data.shape[0]), max_abs_rows])
    assert_array_equal(data, data_flipped * signs[:, np.newaxis])

def test_softmax():
    X = rng.randn(3, 5)
    exp_X = np.exp(X)
    sum_exp_X = np.sum(exp_X, axis=1).reshape((-1, 1))
    assert_array_almost_equal(softmax(X), exp_X / sum_exp_X)

def test_stable_cumsum():
    assert_array_equal(stable_cumsum([1, 2, 3]), np.cumsum([1, 2, 3]))
    r = np.random.RandomState(0).rand(100000)
    with pytest.warns(RuntimeWarning):
        stable_cumsum(r, rtol=0, atol=0)

    # test axis parameter
    A = np.random.RandomState(36).randint(1000, size=(5, 5, 5))
    assert_array_equal(stable_cumsum(A, axis=0), np.cumsum(A, axis=0))
    assert_array_equal(stable_cumsum(A, axis=1), np.cumsum(A, axis=1))
    assert_array_equal(stable_cumsum(A, axis=2), np.cumsum(A, axis=2))

def test_safe_sparse_dot_2d(A_container, B_container):

    A = rng.random_sample((30, 10))
    B = rng.random_sample((10, 20))
    expected = np.dot(A, B)

    A = A_container(A)
    B = B_container(B)
    actual = safe_sparse_dot(A, B, dense_output=True)

    assert_allclose(actual, expected)

def test_safe_sparse_dot_nd(csr_container):

    # dense ND / sparse
    A = rng.random_sample((2, 3, 4, 5, 6))
    B = rng.random_sample((6, 7))
    B = csr_container(B)
    actual = safe_sparse_dot(A, B)

    # sparse / dense ND
    A = rng.random_sample((2, 3))
    B = rng.random_sample((4, 5, 3, 6))
    A = csr_container(A)

def test_safe_sparse_dot_2d_1d(container):
    B = rng.random_sample((10))

    # 2D @ 1D
    actual = safe_sparse_dot(container(A), B)

    # 1D @ 2D
    A = rng.random_sample((10, 30))
    expected = np.dot(B, A)
    actual = safe_sparse_dot(B, container(A))

def test_safe_sparse_dot_dense_output(dense_output):

    A = sparse.random(30, 10, density=0.1, random_state=rng)
    B = sparse.random(10, 20, density=0.1, random_state=rng)

    expected = A.dot(B)
    actual = safe_sparse_dot(A, B, dense_output=dense_output)

    assert sparse.issparse(actual) == (not dense_output)

    if dense_output:
        expected = expected.toarray()
    assert_allclose_dense_sparse(actual, expected)

def test_approximate_mode():
    """Make sure sklearn.utils.extmath._approximate_mode returns valid
    results for cases where "class_counts * n_draws" is enough
    to overflow 32-bit signed integer.

    https://github.com/scikit-learn/scikit-learn/issues/20774
    """
    X = np.array([99000, 1000], dtype=np.int32)
    ret = _approximate_mode(class_counts=X, n_draws=25000, rng=0)

    # Draws 25% of the total population, so in this case a fair draw means:
    # 25% * 99.000 = 24.750
    # 25% *  1.000 =    250
    assert_array_equal(ret, [24750, 250])

def test_randomized_svd_array_api_compliance(array_namespace, device, dtype):

    X = rng.normal(size=(30, 10)).astype(dtype)
    atol = 1e-5 if dtype == "float32" else 0

        u_np, s_np, vt_np = randomized_svd(X, n_components, random_state=0)
        u_xp, s_xp, vt_xp = randomized_svd(X_xp, n_components, random_state=0)

        assert get_namespace(u_xp)[0].__name__ == xp.__name__
        assert get_namespace(s_xp)[0].__name__ == xp.__name__
        assert get_namespace(vt_xp)[0].__name__ == xp.__name__

        assert_allclose(_convert_to_numpy(u_xp, xp), u_np, atol=atol)
        assert_allclose(_convert_to_numpy(s_xp, xp), s_np, atol=atol)
        assert_allclose(_convert_to_numpy(vt_xp, xp), vt_np, atol=atol)

def test_randomized_range_finder_array_api_compliance(array_namespace, device, dtype):

    size = 5
    n_iter = 10

        Q_np = randomized_range_finder(X, size=size, n_iter=n_iter, random_state=0)
        Q_xp = randomized_range_finder(X_xp, size=size, n_iter=n_iter, random_state=0)

        assert get_namespace(Q_xp)[0].__name__ == xp.__name__
        assert_allclose(_convert_to_numpy(Q_xp, xp), Q_np, atol=atol)

        """
        """

                    X[batch],
                    last_var,
                )

        )

class CorrectNotFittedError(ValueError):

    Like NotFittedError, it inherits from ValueError, but not from
    AttributeError. Used for testing only.
    """

class BaseBadClassifier(ClassifierMixin, BaseEstimator):

class ChangesDict(BaseEstimator):
    def __init__(self, key=0):
        self.key = key

        self.key = 1000

class SetsWrongAttribute(BaseEstimator):
    def __init__(self, acceptable_key=0):
        self.acceptable_key = acceptable_key

        self.wrong_attribute = 0

class ChangesWrongAttribute(BaseEstimator):
    def __init__(self, wrong_attribute=0):
        self.wrong_attribute = wrong_attribute

        self.wrong_attribute = 1

class ChangesUnderscoreAttribute(BaseEstimator):
        self._good_attribute = 1

class RaisesErrorInSetParams(BaseEstimator):
    def __init__(self, p=0):
        self.p = p

        if "p" in kwargs:
            p = kwargs.pop("p")
            if p < 0:
                raise ValueError("p can't be less than 0")
            self.p = p
        return super().set_params(**kwargs)

class HasMutableParameters(BaseEstimator):
    def __init__(self, p=object()):
        self.p = p

class HasImmutableParameters(BaseEstimator):
    # Note that object is an uninitialized class, thus immutable.
    def __init__(self, p=42, q=np.int32(42), r=object):
        self.p = p
        self.q = q
        self.r = r

class ModifiesValueInsteadOfRaisingError(BaseEstimator):
        self.p = p

            if p < 0:
                p = 0
            self.p = p

class ModifiesAnotherValue(BaseEstimator):
    def __init__(self, a=0, b="method1"):
        self.a = a
        self.b = b

        if "a" in kwargs:
            a = kwargs.pop("a")
            self.a = a
            if a is None:
                kwargs.pop("b")
                self.b = "method2"

class NoCheckinPredict(BaseBadClassifier):

class NoSparseClassifier(BaseBadClassifier):
    def __init__(self, raise_for_type=None):
        # raise_for_type : str, expects "sparse_array" or "sparse_matrix"
        self.raise_for_type = raise_for_type

        X, y = validate_data(self, X, y, accept_sparse=["csr", "csc"])
        if self.raise_for_type == "sparse_array":
            correct_type = isinstance(X, sp.sparray)
        elif self.raise_for_type == "sparse_matrix":
            correct_type = isinstance(X, sp.spmatrix)
        if correct_type:
            raise ValueError("Nonsensical Error")

class CorrectNotFittedErrorClassifier(BaseBadClassifier):
        self.coef_ = np.ones(X.shape[1])

class NoSampleWeightPandasSeriesType(BaseEstimator):
        )
        # Function is only called after we verify that pandas is installed
        from pandas import Series

        if isinstance(sample_weight, Series):
                "Estimator does not accept 'sample_weight'of type pandas.Series"
            )

class BadBalancedWeightsClassifier(BaseBadClassifier):
    def __init__(self, class_weight=None):

        from sklearn.preprocessing import LabelEncoder
        from sklearn.utils import compute_class_weight

        classes = label_encoder.classes_
        class_weight = compute_class_weight(self.class_weight, classes=classes, y=y)

        # Intentionally modify the balanced class_weight
        # to simulate a bug and raise an exception
        if self.class_weight == "balanced":
            class_weight += 1.0

        # Simply assigning coef_ to the class_weight
        self.coef_ = class_weight

class BadTransformerWithoutMixin(BaseEstimator):

        return X

class NotInvariantPredict(BaseEstimator):
        )

        # return 1 if X has more than one element else return 0
        if X.shape[0] > 1:
        return np.zeros(X.shape[0])

class NotInvariantSampleOrder(BaseEstimator):
        )
        # store the original X to check for sample order later
        self._X = X

        # if the input contains the same elements but different sample order,
        # then just return zeros.
        if (
            np.array_equiv(np.sort(X, axis=0), np.sort(self._X, axis=0))
            and (X != self._X).any()
        ):

class OneClassSampleErrorClassifier(BaseBadClassifier):
    """Classifier allowing to trigger different behaviors when `sample_weight` reduces
    the number of classes to 1."""

    def __init__(self, raise_when_single_class=False):
        self.raise_when_single_class = raise_when_single_class

        X, y = check_X_y(
            X, y, accept_sparse=("csr", "csc"), multi_output=True, y_numeric=True
        )

        self.has_single_class_ = False
        n_classes_ = self.classes_.shape[0]
        if n_classes_ < 2 and self.raise_when_single_class:
            self.has_single_class_ = True
            raise ValueError("normal class error")

        # find the number of class after trimming
            if isinstance(sample_weight, np.ndarray) and len(sample_weight) > 0:
                n_classes_ = np.count_nonzero(np.bincount(y, sample_weight))
            if n_classes_ < 2:
                self.has_single_class_ = True
                raise ValueError("Nonsensical Error")

        if self.has_single_class_:

class LargeSparseNotSupportedClassifier(BaseEstimator):
    """Estimator that claims to support large sparse data
    (accept_large_sparse=True), but doesn't"""

            self,
            X,
            y,
            accept_sparse=("csr", "csc", "coo"),
            accept_large_sparse=True,
        )
            if X.format == "coo":
                if X.row.dtype == "int64" or X.col.dtype == "int64":
                    raise ValueError("Estimator doesn't support 64-bit indices")
            elif X.format in ["csc", "csr"]:
                assert "int64" not in (
                    X.indices.dtype,
                    X.indptr.dtype,
                ), "Estimator doesn't support 64-bit indices"

class SparseTransformer(TransformerMixin, BaseEstimator):
    def __init__(self, sparse_container=None):
        self.sparse_container = sparse_container

        validate_data(self, X)

        return self.sparse_container(X)

class EstimatorInconsistentForPandas(BaseEstimator):
        try:
            from pandas import DataFrame

            if isinstance(X, DataFrame):
                self.value_ = X.iloc[0, 0]
            else:
                X = check_array(X)
                self.value_ = X[1, 0]

            self.value_ = X[1, 0]

        return np.array([self.value_] * X.shape[0])

class UntaggedBinaryClassifier(SGDClassifier):
    # Toy classifier that only supports binary classification, will fail tests.
        super().fit(X, y, coef_init, intercept_init, sample_weight)
        if len(self.classes_) > 2:
            raise ValueError("Only 2 classes are supported")

        super().partial_fit(X=X, y=y, classes=classes, sample_weight=sample_weight)

class TaggedBinaryClassifier(UntaggedBinaryClassifier):
        y_type = type_of_target(y, input_name="y", raise_unknown=True)
        if y_type != "binary":
                "Only binary classification is supported. The type of the target "
                f"is {y_type}."
            )

    # Toy classifier that only supports binary classification.
        tags.classifier_tags.multi_class = False

class RequiresPositiveXRegressor(LinearRegression):
        # reject sparse X to be able to call (X < 0).any()
        X, y = validate_data(self, X, y, accept_sparse=False, multi_output=True)
        if (X < 0).any():
            raise ValueError("Negative values in data passed to X.")

class RequiresPositiveYRegressor(LinearRegression):
        X, y = validate_data(self, X, y, accept_sparse=True, multi_output=True)
        if (y <= 0).any():
            raise ValueError("negative y values not supported!")

        tags.target_tags.positive_only = True

class PoorScoreLogisticRegression(LogisticRegression):
        return super().decision_function(X) + 1

class PartialFitChecksName(BaseEstimator):
        validate_data(self, X, y)

        reset = not hasattr(self, "_fitted")
        validate_data(self, X, y, reset=reset)
        self._fitted = True

class BrokenArrayAPI(BaseEstimator):
    """Make different predictions when using Numpy and the Array API"""

        enabled = get_config()["array_api_dispatch"]
        xp, _ = _array_api.get_namespace(X)
        if enabled:
            return xp.asarray([1, 2, 3])
        else:
            return np.array([3, 2, 1])

def test_check_array_api_input():
    try:
        importlib.import_module("array_api_strict")
    except ModuleNotFoundError:  # pragma: nocover
        raise SkipTest("array-api-strict is required to run this test")

    with raises(AssertionError, match="Not equal to tolerance"):
        check_array_api_input(
            "BrokenArrayAPI",
            BrokenArrayAPI(),
            array_namespace="array_api_strict",
            check_values=True,
        )

def test_not_an_array_array_function():
    not_array = _NotAnArray(np.ones(10))
    msg = "Don't want to call array_function sum!"
    with raises(TypeError, match=msg):
        np.sum(not_array)
    # always returns True
    assert np.may_share_memory(not_array, None)

def test_check_fit_score_takes_y_works_on_deprecated_fit():
    # Tests that check_fit_score_takes_y works on a class with
    # a deprecated fit method

    class TestEstimatorWithDeprecatedFitMethod(BaseEstimator):
        @deprecated("Deprecated for the purpose of testing check_fit_score_takes_y")

    check_fit_score_takes_y("test", TestEstimatorWithDeprecatedFitMethod())

def test_check_estimator_with_class_removed():
    """Test that passing a class instead of an instance fails."""
    msg = "Passing a class was deprecated"

def test_mutable_default_params():
    """Test that constructor cannot have mutable default parameters."""
    msg = (
        "Parameter 'p' of estimator 'HasMutableParameters' is of type "
        "object which is not allowed"
    )
    # check that the "default_constructible" test checks for mutable parameters
    check_parameters_default_constructible(
        "Immutable", HasImmutableParameters()
    )  # should pass
    with raises(AssertionError, match=msg):
        check_parameters_default_constructible("Mutable", HasMutableParameters())

def test_check_set_params():
    """Check set_params doesn't fail and sets the right values."""
    # check that values returned by get_params match set_params
        check_set_params("test", ModifiesValueInsteadOfRaisingError())

    with warnings.catch_warnings(record=True) as records:
        check_set_params("test", RaisesErrorInSetParams())
    assert UserWarning in [rec.category for rec in records]

        check_set_params("test", ModifiesAnotherValue())

def test_check_estimators_nan_inf():
    # check that predict does input validation (doesn't accept dicts in input)
    msg = "Estimator NoCheckinPredict doesn't check for NaN and inf in predict"
        check_estimators_nan_inf("NoCheckinPredict", NoCheckinPredict())

def test_check_dict_unchanged():
    # check that estimator state does not change
    # at transform/predict/predict_proba time
    msg = "Estimator changes __dict__ during predict"
        check_dict_unchanged("test", ChangesDict())

def test_check_sample_weights_pandas_series():
    # check that sample_weights in fit accepts pandas.Series type
    try:
        from pandas import Series  # noqa: F401

        msg = (
            "Estimator NoSampleWeightPandasSeriesType raises error if "
            "'sample_weight' parameter is of type pandas.Series"
        )
        with raises(ValueError, match=msg):
            check_sample_weights_pandas_series(
                "NoSampleWeightPandasSeriesType", NoSampleWeightPandasSeriesType()
            )
        pass

def test_check_estimators_overwrite_params():
    # check that `fit` only changes attributes that
    # are private (start with an _ or end with a _).
    msg = (
        "Estimator ChangesWrongAttribute should not change or mutate  "
        "the parameter wrong_attribute from 0 to 1 during fit."
    )
        check_estimators_overwrite_params(
            "ChangesWrongAttribute", ChangesWrongAttribute()
        )
    check_estimators_overwrite_params("test", ChangesUnderscoreAttribute())

def test_check_dont_overwrite_parameters():
    # check that `fit` doesn't add any public attribute
    msg = (
        r"Estimator adds public attribute\(s\) during the fit method."
        " with _ but wrong_attribute added"
    )
        check_dont_overwrite_parameters("test", SetsWrongAttribute())

def test_check_methods_sample_order_invariance():
    # check for sample order invariance
    name = NotInvariantSampleOrder.__name__
    method = "predict"
    msg = (
        "{method} of {name} is not invariant when applied to a dataset"
        "with different sample order."
    ).format(method=method, name=name)
        check_methods_sample_order_invariance(
            "NotInvariantSampleOrder", NotInvariantSampleOrder()
        )

def test_check_methods_subset_invariance():
    # check for invariant method
    name = NotInvariantPredict.__name__
    msg = ("{method} of {name} is not invariant when applied to a subset.").format(
        method=method, name=name
    )
        check_methods_subset_invariance("NotInvariantPredict", NotInvariantPredict())

def test_check_estimator_sparse_data():
    # check for sparse data input handling
    name = NoSparseClassifier.__name__
    msg = "Estimator %s doesn't seem to fail gracefully on sparse data" % name
        check_estimator_sparse_matrix(name, NoSparseClassifier("sparse_matrix"))

    if SPARRAY_PRESENT:
        with raises(AssertionError, match=msg):
            check_estimator_sparse_array(name, NoSparseClassifier("sparse_array"))

    # Large indices test on bad estimator
    msg = (
        "Estimator LargeSparseNotSupportedClassifier doesn't seem to "
        r"support \S{3}_64 matrix, and is not failing gracefully.*"
    )
        check_estimator_sparse_matrix(
            "LargeSparseNotSupportedClassifier",
            LargeSparseNotSupportedClassifier("sparse_matrix"),
        )

            check_estimator_sparse_array(
                "LargeSparseNotSupportedClassifier",
                LargeSparseNotSupportedClassifier("sparse_array"),
            )

def test_check_classifiers_one_label_sample_weights():
    # check for classifiers reducing to less than two classes via sample weights
    name = OneClassSampleErrorClassifier.__name__
    msg = (
        f"{name} failed when fitted on one label after sample_weight "
        "trimming. Error message is not explicit, it should have "
        "'class'."
    )
        check_classifiers_one_label_sample_weights(
            "OneClassSampleErrorClassifier", OneClassSampleErrorClassifier()
        )

def test_check_estimator_not_fail_fast():
    """Check the contents of the results returned with on_fail!="raise".

    This results should contain details about the observed failures, expected
    or not.
    """
    check_results = check_estimator(BaseEstimator(), on_fail=None)
    assert isinstance(check_results, list)
    assert len(check_results) > 0
        isinstance(item, dict)
        and set(item.keys())
        == {
            "estimator",
            "check_name",
            "exception",
            "status",
            "expected_to_fail",
            "expected_to_fail_reason",
        }
        for item in check_results
    )
    # Some tests are expected to fail, some are expected to pass.
    assert any(item["status"] == "failed" for item in check_results)
    assert any(item["status"] == "passed" for item in check_results)

def test_check_estimator():
    # tests that the estimator actually fails on "bad" estimators.
    # not a complete test of all checks, which are very extensive.

    # check that we have a fit method
    msg = "object has no attribute 'fit'"
    with raises(AttributeError, match=msg):
        check_estimator(BaseEstimator())

    # does error on binary_only untagged estimator
    msg = "Only 2 classes are supported"
        check_estimator(UntaggedBinaryClassifier())

        # non-regression test for estimators transforming to sparse data
        check_estimator(SparseTransformer(sparse_container=csr_container))

    # doesn't error on actual estimator
    check_estimator(LogisticRegression())
    check_estimator(LogisticRegression(C=0.01))
    check_estimator(MultiTaskElasticNet())

    # doesn't error on binary_only tagged estimator
    check_estimator(TaggedBinaryClassifier())
    check_estimator(RequiresPositiveXRegressor())

    # Check regressor with requires_positive_y estimator tag
    msg = "negative y values not supported!"
        check_estimator(RequiresPositiveYRegressor())

    # Does not raise error on classifier with poor_score tag
    check_estimator(PoorScoreLogisticRegression())

def test_check_outlier_corruption():
    # should raise AssertionError
    decision = np.array([0.0, 1.0, 1.5, 2.0])
    with raises(AssertionError):
        check_outlier_corruption(1, 2, decision)
    # should pass
    decision = np.array([0.0, 1.0, 1.0, 2.0])
    check_outlier_corruption(1, 2, decision)

def test_check_estimator_sparse_tag():
    """Test that check_estimator_sparse_tag raises error when sparse tag is
    misaligned."""

    class EstimatorWithSparseConfig(BaseEstimator):
        def __init__(self, tag_sparse, accept_sparse, fit_error=None):
            self.tag_sparse = tag_sparse
            self.accept_sparse = accept_sparse
            self.fit_error = fit_error

            if self.fit_error:
                raise self.fit_error
            validate_data(self, X, y, accept_sparse=self.accept_sparse)

            tags.input_tags.sparse = self.tag_sparse

    test_cases = [
        {"tag_sparse": True, "accept_sparse": True, "error_type": None},
        {"tag_sparse": False, "accept_sparse": False, "error_type": None},
        {"tag_sparse": False, "accept_sparse": True, "error_type": AssertionError},
        {"tag_sparse": True, "accept_sparse": False, "error_type": AssertionError},
    ]

    for test_case in test_cases:
        estimator = EstimatorWithSparseConfig(
            test_case["tag_sparse"],
            test_case["accept_sparse"],
        )
        if test_case["error_type"] is None:
            check_estimator_sparse_tag(estimator.__class__.__name__, estimator)
        else:
            with raises(test_case["error_type"]):
                check_estimator_sparse_tag(estimator.__class__.__name__, estimator)

    # estimator `tag_sparse=accept_sparse=False` fails on sparse data
    # but does not raise the appropriate error
    for fit_error in [TypeError("unexpected error"), KeyError("other error")]:
        estimator = EstimatorWithSparseConfig(False, False, fit_error)
        with raises(AssertionError):

def test_check_estimator_transformer_no_mixin():
    # check that TransformerMixin is not required for transformer tests to run
    # but it fails since the tag is not set
    with raises(RuntimeError, "the `transformer_tags` tag is not set"):
        check_estimator(BadTransformerWithoutMixin())

def test_check_estimator_clones():
    # check that check_estimator doesn't modify the estimator it receives

    for Estimator in [
        GaussianMixture,
        LinearRegression,
        SGDClassifier,
        PCA,
        MiniBatchKMeans,
    ]:
        # without fitting
            est = Estimator()
            set_random_state(est)
            old_hash = joblib.hash(est)
            check_estimator(
                est, expected_failed_checks=_get_expected_failed_checks(est)
            )
        assert old_hash == joblib.hash(est)

        # with fitting
            est.fit(iris.data, iris.target)
            )

def test_check_estimators_unfitted():
    # check that a ValueError/AttributeError is raised when calling predict
    # on an unfitted estimator
    msg = "Estimator should raise a NotFittedError when calling"
        check_estimators_unfitted("estimator", NoSparseClassifier())

    # check that CorrectNotFittedError inherit from either ValueError
    # or AttributeError
    check_estimators_unfitted("estimator", CorrectNotFittedErrorClassifier())

def test_check_no_attributes_set_in_init():
    class NonConformantEstimatorPrivateSet(BaseEstimator):
            self.you_should_not_set_this_ = None

    class NonConformantEstimatorNoParamSet(BaseEstimator):
        def __init__(self, you_should_set_this_=None):
            pass

    class ConformantEstimatorClassAttribute(BaseEstimator):
        # making sure our __metadata_request__* class attributes are okay!
        __metadata_request__fit = {"foo": True}

    msg = (
        "Estimator estimator_name should not set any"
        " attribute apart from parameters during init."
        r" Found attributes \['you_should_not_set_this_'\]."
    )
        check_no_attributes_set_in_init(
            "estimator_name", NonConformantEstimatorPrivateSet()
        )

    msg = (
        "Estimator estimator_name should store all parameters as an attribute"
        " during init"
    )
            "estimator_name", NonConformantEstimatorNoParamSet()
        )

    # a private class attribute is okay!
    check_no_attributes_set_in_init(
        "estimator_name", ConformantEstimatorClassAttribute()
    )
    # also check if cloning an estimator which has non-default set requests is
    # fine. Setting a non-default value via `set_{method}_request` sets the
    # private _metadata_request instance attribute which is copied in `clone`.
            "estimator_name",
            ConformantEstimatorClassAttribute().set_fit_request(foo=True),
        )

def test_check_estimator_pairwise():
    # check that check_estimator() works on estimator with _pairwise
    # kernel or metric

    # test precomputed kernel
    est = SVC(kernel="precomputed")
    check_estimator(est)

    # test precomputed metric
    est = KNeighborsRegressor(metric="precomputed")
    check_estimator(est, expected_failed_checks=_get_expected_failed_checks(est))

def test_check_classifier_data_not_an_array():
        check_classifier_data_not_an_array(
            "estimator_name", EstimatorInconsistentForPandas()
        )

def test_check_regressor_data_not_an_array():
        check_regressor_data_not_an_array(
        )

def test_check_dataframe_column_names_consistency():
    err_msg = "Estimator does not have a feature_names_in_"
    with raises(ValueError, match=err_msg):
        check_dataframe_column_names_consistency("estimator_name", BaseBadClassifier())
    check_dataframe_column_names_consistency("estimator_name", PartialFitChecksName())

    check_dataframe_column_names_consistency(lr.__class__.__name__, lr)
    lr.__doc__ = "Docstring that does not document the estimator's attributes"
    err_msg = (
        "Estimator LogisticRegression does not document its feature_names_in_ attribute"
    )
        check_dataframe_column_names_consistency(lr.__class__.__name__, lr)

class _BaseMultiLabelClassifierMock(ClassifierMixin, BaseEstimator):
    def __init__(self, response_output):
        self.response_output = response_output

def test_check_classifiers_multilabel_output_format_predict():
    )
    y_test = y[-test_size:]

    class MultiLabelClassifierPredict(_BaseMultiLabelClassifierMock):
            return self.response_output

    # 1. inconsistent array type
    clf = MultiLabelClassifierPredict(response_output=y_test.tolist())
    err_msg = (
        r"MultiLabelClassifierPredict.predict is expected to output a "
        r"NumPy array. Got <class 'list'> instead."
    )
    with raises(AssertionError, match=err_msg):
        check_classifiers_multilabel_output_format_predict(clf.__class__.__name__, clf)
    # 2. inconsistent shape
    clf = MultiLabelClassifierPredict(response_output=y_test[:, :-1])
    err_msg = (
        r"MultiLabelClassifierPredict.predict outputs a NumPy array of "
        r"shape \(25, 4\) instead of \(25, 5\)."
    )
    # 3. inconsistent dtype
    clf = MultiLabelClassifierPredict(response_output=y_test.astype(np.float64))
    err_msg = (
        r"MultiLabelClassifierPredict.predict does not output the same "
        r"dtype than the targets."
    )

def test_check_classifiers_multilabel_output_format_predict_proba():
    )

    class MultiLabelClassifierPredictProba(_BaseMultiLabelClassifierMock):

        # 1. unknown output type
        clf = MultiLabelClassifierPredictProba(response_output=csr_container(y_test))
        err_msg = (
            f"Unknown returned type .*{csr_container.__name__}.* by "
            r"MultiLabelClassifierPredictProba.predict_proba. A list or a Numpy "
            r"array is expected."
        )
        with raises(ValueError, match=err_msg):
            check_classifiers_multilabel_output_format_predict_proba(
                clf.__class__.__name__,
                clf,
            )
    # 2. for list output
    # 2.1. inconsistent length
    clf = MultiLabelClassifierPredictProba(response_output=y_test.tolist())
    err_msg = (
        "When MultiLabelClassifierPredictProba.predict_proba returns a list, "
        "the list should be of length n_outputs and contain NumPy arrays. Got "
        f"length of {test_size} instead of {n_outputs}."
    )
        check_classifiers_multilabel_output_format_predict_proba(
            clf.__class__.__name__,
            clf,
        )
    # 2.2. array of inconsistent shape
    response_output = [np.ones_like(y_test) for _ in range(n_outputs)]
    clf = MultiLabelClassifierPredictProba(response_output=response_output)
    err_msg = (
        r"When MultiLabelClassifierPredictProba.predict_proba returns a list, "
        r"this list should contain NumPy arrays of shape \(n_samples, 2\). Got "
        r"NumPy arrays of shape \(25, 5\) instead of \(25, 2\)."
    )
            clf,
        )
    # 2.3. array of inconsistent dtype
    response_output = [
        np.ones(shape=(y_test.shape[0], 2), dtype=np.int64) for _ in range(n_outputs)
    ]
    err_msg = (
        "it should contain NumPy arrays with floating dtype."
    )
            clf,
        )
    # 2.4. array does not contain probability (each row should sum to 1)
        np.ones(shape=(y_test.shape[0], 2), dtype=np.float64) for _ in range(n_outputs)
    ]
    err_msg = (
        r"each NumPy array should contain probabilities for each class and "
        r"thus each row should sum to 1"
    )
            clf,
        )
    # 3 for array output
    # 3.1. array of inconsistent shape
    clf = MultiLabelClassifierPredictProba(response_output=y_test[:, :-1])
    err_msg = (
        r"When MultiLabelClassifierPredictProba.predict_proba returns a NumPy "
        r"array, the expected shape is \(n_samples, n_outputs\). Got \(25, 4\)"
        r" instead of \(25, 5\)."
    )
            clf,
        )
    # 3.2. array of inconsistent dtype
    response_output = np.zeros_like(y_test, dtype=np.int64)
    err_msg = (
        r"array, the expected data type is floating."
    )
            clf,
        )
    # 4. array does not contain probabilities
    clf = MultiLabelClassifierPredictProba(response_output=y_test * 2.0)
    err_msg = (
        r"array, this array is expected to provide probabilities of the "
        r"positive class and should therefore contain values between 0 and 1."
    )
            clf,
        )

def test_check_classifiers_multilabel_output_format_decision_function():
    )

    class MultiLabelClassifierDecisionFunction(_BaseMultiLabelClassifierMock):

    clf = MultiLabelClassifierDecisionFunction(response_output=y_test.tolist())
    err_msg = (
        r"MultiLabelClassifierDecisionFunction.decision_function is expected "
        r"to output a NumPy array. Got <class 'list'> instead."
    )
        check_classifiers_multilabel_output_format_decision_function(
            clf,
        )
    clf = MultiLabelClassifierDecisionFunction(response_output=y_test[:, :-1])
    err_msg = (
        r"to provide a NumPy array of shape \(n_samples, n_outputs\). Got "
        r"\(25, 4\) instead of \(25, 5\)"
    )
            clf,
        )
    clf = MultiLabelClassifierDecisionFunction(response_output=y_test)
    err_msg = (
        r"to output a floating dtype."
    )
            clf,
        )

def run_tests_without_pytest():
    """Runs the tests in this file without using pytest."""
    main_module = sys.modules["__main__"]
    test_functions = [
        getattr(main_module, name)
        for name in dir(main_module)
        if name.startswith("test_")
    ]
    test_cases = [unittest.FunctionTestCase(fn) for fn in test_functions]
    suite = unittest.TestSuite()
    suite.addTests(test_cases)
    runner = unittest.TextTestRunner()
    runner.run(suite)

def test_check_class_weight_balanced_linear_classifier():
    # check that ill-computed balanced weights raises an exception
    msg = "Classifier estimator_name is not computing class_weight=balanced properly"
        check_class_weight_balanced_linear_classifier(
            "estimator_name", BadBalancedWeightsClassifier()
        )

def test_all_estimators_all_public():
    # all_estimator should not fail when pytest is not installed and return
    # only public estimators
        estimators = all_estimators()
    # no warnings are raised
    assert not record
    for est in estimators:
        assert not est.__class__.__name__.startswith("_")

def test_estimator_checks_generator_skipping_tests():
    # Make sure the checks generator skips tests that are expected to fail
    est = next(_construct_instances(NuSVC))
    expected_to_fail = _get_expected_failed_checks(est)
    checks = estimator_checks_generator(
        est, legacy=True, expected_failed_checks=expected_to_fail, mark="skip"
    )
    # making sure we use a class that has expected failures
    assert len(expected_to_fail) > 0
    skipped_checks = []
    for estimator, check in checks:
        try:
        except SkipTest:
            skipped_checks.append(_check_name(check))
    # all checks expected to fail are skipped
    # some others might also be skipped, if their dependencies are not installed.
    assert set(expected_to_fail.keys()) <= set(skipped_checks)

def test_xfail_count_with_no_fast_fail():
    """Test that the right number of xfail warnings are raised when on_fail is "warn".

    It also checks the number of raised EstimatorCheckFailedWarning, and checks the
    output of check_estimator.
    """
    est = NuSVC()
    expected_failed_checks = _get_expected_failed_checks(est)
    # This is to make sure we test a class that has some expected failures
    assert len(expected_failed_checks) > 0
        logs = check_estimator(
            est,
            expected_failed_checks=expected_failed_checks,
            on_fail="warn",
        )
    xfail_warns = [w for w in records if w.category != SkipTestWarning]
    assert all([rec.category == EstimatorCheckFailedWarning for rec in xfail_warns])
    assert len(xfail_warns) == len(expected_failed_checks)

    xfailed = [log for log in logs if log["status"] == "xfail"]
    assert len(xfailed) == len(expected_failed_checks)

def test_check_estimator_callback():
    """Test that the callback is called with the right arguments."""
    call_count = {"xfail": 0, "skipped": 0, "passed": 0, "failed": 0}

    def callback(
        *,
        check_name,
        exception,
        status,
        expected_to_fail,
        expected_to_fail_reason,
    ):
        assert status in ("xfail", "skipped", "passed", "failed")
        nonlocal call_count
        call_count[status] += 1

        check_estimator(
            est,
            on_fail=None,
            callback=callback,
        )
    all_checks_count = len(list(estimator_checks_generator(est, legacy=True)))
    assert call_count["xfail"] == len(expected_failed_checks)
    assert call_count["passed"] > 0
    assert call_count["failed"] == 0
    assert call_count["skipped"] == (
        all_checks_count - call_count["xfail"] - call_count["passed"]
    )

def test_minimal_class_implementation_checks():
    # Check that third-party library can run tests without inheriting from
    # BaseEstimator.
    # FIXME
    raise SkipTest
    minimal_estimators = [MinimalTransformer(), MinimalRegressor(), MinimalClassifier()]
    for estimator in minimal_estimators:
        check_estimator(estimator)

def test_check_fit_check_is_fitted():
        def __init__(self, behavior="attribute"):
            self.behavior = behavior

            if self.behavior == "attribute":
                self.is_fitted_ = True
            elif self.behavior == "method":
                self._is_fitted = True

        @available_if(lambda self: self.behavior in {"method", "always-true"})
        def __sklearn_is_fitted__(self):
            if self.behavior == "always-true":
            return hasattr(self, "_is_fitted")

    with raises(Exception, match="passes check_is_fitted before being fit"):
        check_fit_check_is_fitted("estimator", Estimator(behavior="always-true"))

    check_fit_check_is_fitted("estimator", Estimator(behavior="method"))
    check_fit_check_is_fitted("estimator", Estimator(behavior="attribute"))

def test_check_requires_y_none():
            X, y = check_X_y(X, y)

        check_requires_y_none("estimator", Estimator())

    assert not [r.message for r in record]

def test_non_deterministic_estimator_skip_tests():
    # check estimators with non_deterministic tag set to True
    # will skip certain tests, refer to issue #22313 for details
    for Estimator in [MinimalTransformer, MinimalRegressor, MinimalClassifier]:
        all_tests = list(_yield_all_checks(Estimator(), legacy=True))
        assert check_methods_sample_order_invariance in all_tests
        assert check_methods_subset_invariance in all_tests

        class MyEstimator(Estimator):
            def __sklearn_tags__(self):
                tags = super().__sklearn_tags__()
                tags.non_deterministic = True
                return tags

        all_tests = list(_yield_all_checks(MyEstimator(), legacy=True))
        assert check_methods_sample_order_invariance not in all_tests
        assert check_methods_subset_invariance not in all_tests

def test_check_outlier_contamination():
    """Check the test for the contamination parameter in the outlier detectors."""

    # Without any parameter constraints, the estimator will early exit the test by
    # returning None.
    class OutlierDetectorWithoutConstraint(OutlierMixin, BaseEstimator):
        """Outlier detector without parameter validation."""

        def __init__(self, contamination=0.1):
            self.contamination = contamination

        def fit(self, X, y=None, sample_weight=None):

        def predict(self, X, y=None):

    detector = OutlierDetectorWithoutConstraint()
    assert check_outlier_contamination(detector.__class__.__name__, detector) is None

    # Now, we check that with the parameter constraints, the test should only be valid
    # if an Interval constraint with bound in [0, 1] is provided.
    class OutlierDetectorWithConstraint(OutlierDetectorWithoutConstraint):
        _parameter_constraints = {"contamination": [StrOptions({"auto"})]}

    detector = OutlierDetectorWithConstraint()
    err_msg = "contamination constraints should contain a Real Interval constraint."
        check_outlier_contamination(detector.__class__.__name__, detector)

    # Add a correct interval constraint and check that the test passes.
    OutlierDetectorWithConstraint._parameter_constraints["contamination"] = [
        Interval(Real, 0, 0.5, closed="right")
    ]
    check_outlier_contamination(detector.__class__.__name__, detector)

    incorrect_intervals = [
        Interval(Integral, 0, 1, closed="right"),  # not an integral interval
        Interval(Real, -1, 1, closed="right"),  # lower bound is negative
        Interval(Real, 0, 2, closed="right"),  # upper bound is greater than 1
        Interval(Real, 0, 0.5, closed="left"),  # lower bound include 0
    ]

    err_msg = r"contamination constraint should be an interval in \(0, 0.5\]"
    for interval in incorrect_intervals:
        OutlierDetectorWithConstraint._parameter_constraints["contamination"] = [
            interval
        ]
        detector = OutlierDetectorWithConstraint()
        with raises(AssertionError, match=err_msg):
            check_outlier_contamination(detector.__class__.__name__, detector)

def test_decision_proba_tie_ranking():
    """Check that in case with some probabilities ties, we relax the
    ranking comparison with the decision function.
    https://github.com/scikit-learn/scikit-learn/issues/24025
    """
    estimator = SGDClassifier(loss="log_loss")
    check_decision_proba_consistency("SGDClassifier", estimator)

def test_yield_all_checks_legacy():
    # Test that _yield_all_checks with legacy=True returns more checks.
    estimator = MinimalClassifier()

    legacy_checks = list(_yield_all_checks(estimator, legacy=True))
    non_legacy_checks = list(_yield_all_checks(estimator, legacy=False))

    assert len(legacy_checks) > len(non_legacy_checks)

    def get_check_name(check):
        try:
            return check.__name__
            return check.func.__name__

    # Check that all non-legacy checks are included in legacy checks
    non_legacy_check_names = {get_check_name(check) for check in non_legacy_checks}
    legacy_check_names = {get_check_name(check) for check in legacy_checks}
    assert non_legacy_check_names.issubset(legacy_check_names)

def test_check_estimator_cloneable_error():
    """Check that the right error is raised when the estimator is not cloneable."""

    class NotCloneable(BaseEstimator):
            raise NotImplementedError("This estimator is not cloneable.")

    estimator = NotCloneable()
    msg = "Cloning of .* failed with error"
        check_estimator_cloneable("NotCloneable", estimator)

def test_estimator_repr_error():
    """Check that the right error is raised when the estimator does not have a repr."""

    class NotRepr(BaseEstimator):
        def __repr__(self):
            raise NotImplementedError("This estimator does not have a repr.")

    estimator = NotRepr()
    msg = "Repr of .* failed with error"
        check_estimator_repr("NotRepr", estimator)

def test_check_estimator_tags_renamed():
    class BadEstimator1:
            return None  # pragma: no cover

    class BadEstimator2:
        def _get_tags(self):

    class OkayEstimator:

    msg = "has defined either `_more_tags` or `_get_tags`"
        check_estimator_tags_renamed("BadEstimator1", BadEstimator1())
        check_estimator_tags_renamed("BadEstimator2", BadEstimator2())

    # This shouldn't fail since we allow both __sklearn_tags__ and _more_tags
    # to exist so that third party estimators can easily support multiple sklearn
    # versions.
    check_estimator_tags_renamed("OkayEstimator", OkayEstimator())

def test_check_classifier_not_supporting_multiclass():
    """Check that when the estimator has the wrong tags.classifier_tags.multi_class
    set, the test fails."""

    class BadEstimator(BaseEstimator):
        # we don't actually need to define the tag here since we're running the test
        # manually, and BaseEstimator defaults to multi_output=False.

    msg = "The estimator tag `tags.classifier_tags.multi_class` is False"
        check_classifier_not_supporting_multiclass("BadEstimator", BadEstimator())

def test_estimator_with_set_output():
    # Doing this since pytest is not available for this file.
    for lib in ["pandas", "polars"]:
        try:
            importlib.__import__(lib)
            raise SkipTest(f"Library {lib} is not installed")

        estimator = StandardScaler().set_output(transform=lib)

def test_estimator_checks_generator():
    """Check that checks_generator returns a generator."""
    all_instance_gen_checks = estimator_checks_generator(LogisticRegression())

def test_check_estimator_callback_with_fast_fail_error():
    """Check that check_estimator fails correctly with on_fail='raise' and callback."""
        ValueError, match="callback cannot be provided together with on_fail='raise'"
    ):
        check_estimator(LogisticRegression(), on_fail="raise", callback=lambda: None)

def test_check_mixin_order():
    """Test that the check raises an error when the mixin order is incorrect."""

    class BadEstimator(BaseEstimator, TransformerMixin):

    msg = "TransformerMixin comes before/left side of BaseEstimator"
    with raises(AssertionError, match=re.escape(msg)):
        check_mixin_order("BadEstimator", BadEstimator())

def test_check_positive_only_tag_during_fit():
    class RequiresPositiveXBadTag(RequiresPositiveXRegressor):
            tags.input_tags.positive_only = False

        AssertionError, match="This happens when passing negative input values as X."
    ):
        check_positive_only_tag_during_fit(
            "RequiresPositiveXBadTag", RequiresPositiveXBadTag()
        )

            if p < 0:
            self.p = p

        self.p = p
        self.q = q
        self.r = r

            if p < 0:
                p = 0
            self.p = p

        self.a = a
        self.b = b

            self.a = a

        )

            )

        return X

        )

        )
        self._X = X

        if (
        ):

        )

            self,
            X,
            y,
        )

        try:

            else:

            )

        else:

            pass

        *,
        status,
    ):

        try:

            pass

def _numpy_to_cython(dtype):
    cython = pytest.importorskip("cython")
        return cython.float
    elif dtype == np.float64:
        return cython.double

def test_dot(dtype):
    dot = _dot_memview[_numpy_to_cython(dtype)]

    x = rng.random_sample(10).astype(dtype, copy=False)
    y = rng.random_sample(10).astype(dtype, copy=False)

    expected = x.dot(y)
    actual = dot(x, y)

    assert_allclose(actual, expected, rtol=RTOL[dtype])

def test_asum(dtype):
    asum = _asum_memview[_numpy_to_cython(dtype)]

    expected = np.abs(x).sum()
    actual = asum(x)

def test_axpy(dtype):
    axpy = _axpy_memview[_numpy_to_cython(dtype)]

    alpha = 2.5

    expected = alpha * x + y
    axpy(alpha, x, y)

    assert_allclose(y, expected, rtol=RTOL[dtype])

def test_nrm2(dtype):
    nrm2 = _nrm2_memview[_numpy_to_cython(dtype)]

    expected = np.linalg.norm(x)
    actual = nrm2(x)

def test_copy(dtype):
    copy = _copy_memview[_numpy_to_cython(dtype)]

    y = np.empty_like(x)

    expected = x.copy()
    copy(x, y)

def test_scal(dtype):
    scal = _scal_memview[_numpy_to_cython(dtype)]

    alpha = 2.5

    expected = alpha * x
    scal(alpha, x)

    assert_allclose(x, expected, rtol=RTOL[dtype])

def test_rotg(dtype):
    rotg = _rotg_memview[_numpy_to_cython(dtype)]

    a = dtype(rng.randn())
    b = dtype(rng.randn())
    c, s = 0.0, 0.0

    def expected_rotg(a, b):
        roe = a if abs(a) > abs(b) else b
        if a == 0 and b == 0:
            c, s, r, z = (1, 0, 0, 0)
        else:
            r = np.sqrt(a**2 + b**2) * (1 if roe >= 0 else -1)
            c, s = a / r, b / r
            z = s if roe == a else (1 if c == 0 else 1 / c)
        return r, z, c, s

    expected = expected_rotg(a, b)
    actual = rotg(a, b, c, s)

def test_rot(dtype):
    rot = _rot_memview[_numpy_to_cython(dtype)]

    c = dtype(rng.randn())
    s = dtype(rng.randn())

    expected_x = c * x + s * y
    expected_y = c * y - s * x

    rot(x, y, c, s)

    assert_allclose(x, expected_x)
    assert_allclose(y, expected_y)

def test_gemv(dtype, opA, transA, order):
    gemv = _gemv_memview[_numpy_to_cython(dtype)]

    A = np.asarray(
        opA(rng.random_sample((20, 10)).astype(dtype, copy=False)), order=ORDER[order]
    )
    y = rng.random_sample(20).astype(dtype, copy=False)
    alpha, beta = 2.5, -0.5

    expected = alpha * opA(A).dot(x) + beta * y
    gemv(transA, alpha, A, x, beta, y)

def test_ger(dtype, order):
    ger = _ger_memview[_numpy_to_cython(dtype)]

        rng.random_sample((10, 20)).astype(dtype, copy=False), order=ORDER[order]
    )
    alpha = 2.5

    expected = alpha * np.outer(x, y) + A
    ger(alpha, x, y, A)

    assert_allclose(A, expected, rtol=RTOL[dtype])

def test_gemm(dtype, opA, transA, opB, transB, order):
    gemm = _gemm_memview[_numpy_to_cython(dtype)]

        opA(rng.random_sample((30, 10)).astype(dtype, copy=False)), order=ORDER[order]
    )
    B = np.asarray(
        opB(rng.random_sample((10, 20)).astype(dtype, copy=False)), order=ORDER[order]
    )
    C = np.asarray(
        rng.random_sample((30, 20)).astype(dtype, copy=False), order=ORDER[order]
    )

    expected = alpha * opA(A).dot(opB(B)) + beta * C
    gemm(transA, transB, alpha, A, B, beta, C)

    assert_allclose(C, expected, rtol=RTOL[dtype])

        else:

def test_min_pos():
    # Check that min_pos returns a positive value and that it's consistent
    # between float and double
    X = np.random.RandomState(0).randn(100)

    min_double = min_pos(X)
    min_float = min_pos(X.astype(np.float32))

    assert_allclose(min_double, min_float)
    assert min_double >= 0

def test_min_pos_no_positive(dtype):
    # Check that the return value of min_pos is the maximum representable
    # value of the input dtype when all input elements are <= 0 (#19328)
    X = np.full(100, -1.0).astype(dtype, copy=False)

    assert min_pos(X) == np.finfo(dtype).max

def test_all_with_any_reduction_axis_1(dtype, value):
    # Check that return value is False when there is no row equal to `value`
    X = np.arange(12, dtype=dtype).reshape(3, 4)
    assert not _all_with_any_reduction_axis_1(X, value=value)

    # Make a row equal to `value`
    X[1, :] = value
    assert _all_with_any_reduction_axis_1(X, value=value)

def test_get_sys_info():

    assert "python" in sys_info
    assert "executable" in sys_info
    assert "machine" in sys_info

def test_get_deps_info():
        deps_info = _get_deps_info()

    assert "pip" in deps_info
    assert "setuptools" in deps_info
    assert "sklearn" in deps_info
    assert "numpy" in deps_info
    assert "scipy" in deps_info
    assert "Cython" in deps_info
    assert "pandas" in deps_info
    assert "matplotlib" in deps_info
    assert "joblib" in deps_info

def test_show_versions(capsys):
        show_versions()
        out, err = capsys.readouterr()

    assert "python" in out
    assert "numpy" in out

    info = threadpool_info()
    if info:
        assert "threadpoolctl info:" in out

def test_safe_mask(csr_container):
    X = random_state.rand(5, 4)
    mask = [False, False, True, True, True]

    mask = safe_mask(X, mask)
    assert X[mask].shape[0] == 3

    mask = safe_mask(X_csr, mask)
    assert X_csr[mask].shape[0] == 3

def test_fix_connected_components():
    # Test that _fix_connected_components reduces the number of component to 1.
    graph = kneighbors_graph(X, n_neighbors=2, mode="distance")

    n_connected_components, labels = connected_components(graph)
    assert n_connected_components > 1

    graph = _fix_connected_components(X, graph, n_connected_components, labels)

    assert n_connected_components == 1

def test_fix_connected_components_precomputed():
    # Test that _fix_connected_components accepts precomputed distance matrix.

    distances = pairwise_distances(X)
    graph = _fix_connected_components(
        distances, graph, n_connected_components, labels, metric="precomputed"
    )

    # but it does not work with precomputed neighbors graph
    with pytest.raises(RuntimeError, match="does not work with a sparse"):
        _fix_connected_components(
            graph, graph, n_connected_components, labels, metric="precomputed"
        )

def test_fix_connected_components_wrong_mode():
    # Test that the an error is raised if the mode string is incorrect.

    with pytest.raises(ValueError, match="Unknown mode"):
        graph = _fix_connected_components(
            X, graph, n_connected_components, labels, mode="foo"
        )

def test_fix_connected_components_connectivity_mode():
    # Test that the connectivity mode fill new connections with ones.
    X = np.array([0, 1, 6, 7])[:, None]
    graph = kneighbors_graph(X, n_neighbors=1, mode="connectivity")
        X, graph, n_connected_components, labels, mode="connectivity"
    )
    assert np.all(graph.data == 1)

def test_fix_connected_components_distance_mode():
    # Test that the distance mode does not fill new connections with ones.
    graph = kneighbors_graph(X, n_neighbors=1, mode="distance")

        X, graph, n_connected_components, labels, mode="distance"
    )
    assert not np.all(graph.data == 1)

def test_make_rng():
    # Check the check_random_state utility function behavior
    assert check_random_state(None) is np.random.mtrand._rand
    assert check_random_state(np.random) is np.random.mtrand._rand

    rng_42 = np.random.RandomState(42)
    assert check_random_state(42).randint(100) == rng_42.randint(100)

    assert check_random_state(rng_42) is rng_42

    assert check_random_state(43).randint(100) != rng_42.randint(100)

        check_random_state("some invalid seed")

def test_as_float_array():
    # Test function for as_float_array
    X = np.ones((3, 10), dtype=np.int32)
    X = X + np.arange(10, dtype=np.int32)
    X2 = as_float_array(X, copy=False)
    assert X2.dtype == np.float32
    # Another test
    X = X.astype(np.int64)
    X2 = as_float_array(X, copy=True)
    # Checking that the array wasn't overwritten
    assert as_float_array(X, copy=False) is not X
    assert X2.dtype == np.float64
    # Test int dtypes <= 32bit
    tested_dtypes = [bool, np.int8, np.int16, np.int32, np.uint8, np.uint16, np.uint32]
    for dtype in tested_dtypes:
        X = X.astype(dtype)
        X2 = as_float_array(X)
        assert X2.dtype == np.float32

    # Test object dtype

    # Here, X is of the right type, it shouldn't be modified
    X = np.ones((3, 2), dtype=np.float32)
    assert as_float_array(X, copy=False) is X
    # Test that if X is fortran ordered it stays
    assert np.isfortran(as_float_array(X, copy=True))

    # Test the copy parameter with some matrices
    matrices = [
        sp.csc_matrix(np.arange(5)).toarray(),
        _sparse_random_matrix(10, 10, density=0.10).toarray(),
    ]
    for M in matrices:
        N = as_float_array(M, copy=True)
        N[0, 0] = np.nan
        assert not np.isnan(M).any()

def test_as_float_array_nan(X):
    X[5, 0] = np.nan
    X[6, 1] = np.nan
    X_converted = as_float_array(X, ensure_all_finite="allow-nan")
    assert_allclose_dense_sparse(X_converted, X)

def test_np_matrix():
    # Confirm that input validation code does not return np.matrix
    X = np.arange(12).reshape(3, 4)

    assert not isinstance(as_float_array(X), np.matrix)
    assert not isinstance(as_float_array(sp.csc_matrix(X)), np.matrix)

def test_memmap():
    # Confirm that input validation code doesn't copy memory mapped arrays

    asflt = lambda x: as_float_array(x, copy=False)

    with NamedTemporaryFile(prefix="sklearn-test") as tmp:
        M = np.memmap(tmp, shape=(10, 10), dtype=np.float32)
        M[:] = 0

        for f in (check_array, np.asarray, asflt):
            X = f(M)
            X[:] = 1
            assert_array_equal(X.ravel(), M.ravel())
            X[:] = 0

def test_ordering():
    # Check that ordering is enforced correctly by validation utilities.
    # We need to check each validation utility, because a 'copy' without
    # 'order=K' will kill the ordering.
    X = np.ones((10, 5))
    for A in X, X.T:
        for copy in (True, False):
            B = check_array(A, order="C", copy=copy)
            assert B.flags["C_CONTIGUOUS"]
            B = check_array(A, order="F", copy=copy)
            assert B.flags["F_CONTIGUOUS"]
            if copy:
                assert A is not B

    X = sp.csr_matrix(X)
    X.data = X.data[::-1]
    assert not X.data.flags["C_CONTIGUOUS"]

def test_check_array_ensure_all_finite_valid(value, ensure_all_finite, retype):
    X = retype(np.arange(4).reshape(2, 2).astype(float))
    X[0, 0] = value
    X_checked = check_array(X, ensure_all_finite=ensure_all_finite, accept_sparse=True)
    assert_allclose_dense_sparse(X, X_checked)

def test_check_array_ensure_all_finite_invalid(
    value, input_name, ensure_all_finite, match_msg, retype
):
    X = retype(np.arange(4).reshape(2, 2).astype(np.float64))
    with pytest.raises(ValueError, match=match_msg):
        check_array(
            X,
        )

def test_check_array_links_to_imputer_doc_only_for_X(input_name, retype):
    data = retype(np.arange(4).reshape(2, 2).astype(np.float64))
    data[0, 0] = np.nan
    estimator = SVR()
    extended_msg = (
        f"\n{estimator.__class__.__name__} does not accept missing values"
        " encoded as NaN natively. For supervised learning, you might want"
        " to consider sklearn.ensemble.HistGradientBoostingClassifier and Regressor"
        " which accept missing values encoded as NaNs natively."
        " Alternatively, it is possible to preprocess the"
        " data, for instance by using an imputer transformer in a pipeline"
        " or drop samples with missing values. See"
        " https://scikit-learn.org/stable/modules/impute.html"
        " You can find a list of all estimators that handle NaN values"
        " at the following page:"
        "#estimators-that-handle-nan-values"
    )

    with pytest.raises(ValueError, match=f"Input {input_name} contains NaN") as ctx:
            data,
        )

    if input_name == "X":
        assert extended_msg in ctx.value.args[0]
    else:
        assert extended_msg not in ctx.value.args[0]

        # Veriy that _validate_data is automatically called with the right argument
        # to generate the same exception:
        with pytest.raises(ValueError, match=f"Input {input_name} contains NaN") as ctx:
            SVR().fit(data, np.ones(data.shape[0]))

def test_check_array_ensure_all_finite_object():
    X = np.array([["a", "b", np.nan]], dtype=object).T

    X_checked = check_array(X, dtype=None, ensure_all_finite="allow-nan")
    assert X is X_checked

    X_checked = check_array(X, dtype=None, ensure_all_finite=False)

    with pytest.raises(ValueError, match="Input contains NaN"):
        check_array(X, dtype=None, ensure_all_finite=True)

def test_check_array_ensure_all_finite_object_unsafe_casting(
    X, err_msg, ensure_all_finite
):
    # casting a float array containing NaN or inf to int dtype should
    # raise an error irrespective of the ensure_all_finite parameter.
        check_array(X, dtype=int, ensure_all_finite=ensure_all_finite)

def test_check_array_series_err_msg():
    """
    Check that we raise a proper error message when passing a Series and we expect a
    2-dimensional container.

    https://github.com/scikit-learn/scikit-learn/issues/27498
    """
    ser = pd.Series([1, 2, 3])
    msg = f"Expected a 2-dimensional container but got {type(ser)} instead."
        check_array(ser, ensure_2d=True)

def test_check_array():
    # accept_sparse == False
    # raise error on sparse inputs
    X_csr = sp.csr_matrix(X)
        check_array(X_csr)

    # ensure_2d=False
    X_array = check_array([0, 1, 2], ensure_2d=False)
    assert X_array.ndim == 1
    # ensure_2d=True with 1d array
    with pytest.raises(ValueError, match="Expected 2D array, got 1D array instead"):
        check_array([0, 1, 2], ensure_2d=True)

    # ensure_2d=True with scalar array
    with pytest.raises(ValueError, match="Expected 2D array, got scalar array instead"):
        check_array(10, ensure_2d=True)

    # ensure_2d=True with 1d sparse array
    if hasattr(sp, "csr_array"):
        sparse_row = next(iter(sp.csr_array(X)))
        if sparse_row.ndim == 1:
            # In scipy 1.14 and later, sparse row is 1D while it was 2D before.
            with pytest.raises(ValueError, match="Expected 2D input, got"):
                check_array(sparse_row, accept_sparse=True, ensure_2d=True)

    # don't allow ndim > 3
    X_ndim = np.arange(8).reshape(2, 2, 2)
        check_array(X_ndim)
    check_array(X_ndim, allow_nd=True)  # doesn't raise

    # dtype and order enforcement.
    X_C = np.arange(4).reshape(2, 2).copy("C")
    X_F = X_C.copy("F")
    X_int = X_C.astype(int)
    X_float = X_C.astype(float)
    Xs = [X_C, X_F, X_int, X_float]
    dtypes = [np.int32, int, float, np.float32, None, bool, object]
    orders = ["C", "F", None]
    copys = [True, False]

    for X, dtype, order, copy in product(Xs, dtypes, orders, copys):
        X_checked = check_array(X, dtype=dtype, order=order, copy=copy)
        if dtype is not None:
            assert X_checked.dtype == dtype
        else:
            assert X_checked.dtype == X.dtype
        if order == "C":
            assert X_checked.flags["C_CONTIGUOUS"]
            assert not X_checked.flags["F_CONTIGUOUS"]
        elif order == "F":
            assert X_checked.flags["F_CONTIGUOUS"]
            assert not X_checked.flags["C_CONTIGUOUS"]
        if copy:
            assert X is not X_checked
        else:
            # doesn't copy if it was already good
            if (
                X.dtype == X_checked.dtype
                and X_checked.flags["C_CONTIGUOUS"] == X.flags["C_CONTIGUOUS"]
                and X_checked.flags["F_CONTIGUOUS"] == X.flags["F_CONTIGUOUS"]
            ):
                assert X is X_checked

    # allowed sparse != None

    # try different type of sparse format
    Xs = []
    Xs.extend(
        [
            sparse_container(X_C)
            for sparse_container in CSR_CONTAINERS
            + CSC_CONTAINERS
            + COO_CONTAINERS
            + DOK_CONTAINERS
        ]
    )
    Xs.extend([Xs[0].astype(np.int64), Xs[0].astype(np.float64)])

    accept_sparses = [["csr", "coo"], ["coo", "dok"]]
    # scipy sparse matrices do not support the object dtype so
    # this dtype is skipped in this loop
    non_object_dtypes = [dt for dt in dtypes if dt is not object]
    for X, dtype, accept_sparse, copy in product(
        Xs, non_object_dtypes, accept_sparses, copys
    ):
        X_checked = check_array(X, dtype=dtype, accept_sparse=accept_sparse, copy=copy)
        else:
        if X.format in accept_sparse:
            # no change if allowed
            assert X.format == X_checked.format
        else:
            # got converted
            assert X_checked.format == accept_sparse[0]
        if copy:
        else:
            if X.dtype == X_checked.dtype and X.format == X_checked.format:

    # other input formats
    # convert lists to arrays
    X_dense = check_array([[1, 2], [3, 4]])
    assert isinstance(X_dense, np.ndarray)
    # raise on too deep lists
        check_array(X_ndim.tolist())
    check_array(X_ndim.tolist(), allow_nd=True)  # doesn't raise

    # convert weird stuff to arrays
    X_no_array = _NotAnArray(X_dense)
    result = check_array(X_no_array)
    assert isinstance(result, np.ndarray)

    # check negative values when ensure_non_negative=True
    X_neg = check_array([[1, 2], [-3, 4]])
    err_msg = "Negative values in data passed to X in RandomForestRegressor"
            X_neg,
            input_name="X",
            estimator=RandomForestRegressor(),
        )

def test_check_array_numeric_error(X):
    """Test that check_array errors when it receives an array of bytes/string
    while a numeric dtype is required."""
    expected_msg = r"dtype='numeric' is not compatible with arrays of bytes/strings"
        check_array(X, dtype="numeric")

def test_check_array_pandas_na_support(pd_dtype, dtype, expected_dtype):
    # Test pandas numerical extension arrays with pd.NA

    if pd_dtype in {"Float32", "Float64"}:
        # Extension dtypes with Floats was added in 1.2
        pd = pytest.importorskip("pandas", minversion="1.2")

    X_np = np.array(
        [[1, 2, 3, np.nan, np.nan], [np.nan, np.nan, 8, 4, 6], [1, 2, 3, 4, 5]]
    ).T

    # Creates dataframe with numerical extension arrays with pd.NA
    X = pd.DataFrame(X_np, dtype=pd_dtype, columns=["a", "b", "c"])
    # column c has no nans
    X["c"] = X["c"].astype("float")
    X_checked = check_array(X, ensure_all_finite="allow-nan", dtype=dtype)
    assert_allclose(X_checked, X_np)
    assert X_checked.dtype == expected_dtype

    X_checked = check_array(X, ensure_all_finite=False, dtype=dtype)

    msg = "Input contains NaN"
        check_array(X, ensure_all_finite=True)

def test_check_array_panadas_na_support_series():
    """Check check_array is correct with pd.NA in a series."""

    X_int64 = pd.Series([1, 2, pd.NA], dtype="Int64")

        check_array(X_int64, ensure_all_finite=True, ensure_2d=False)

    X_out = check_array(X_int64, ensure_all_finite=False, ensure_2d=False)
    assert_allclose(X_out, [1, 2, np.nan])
    assert X_out.dtype == np.float64

    X_out = check_array(
        X_int64, ensure_all_finite=False, ensure_2d=False, dtype=np.float32
    )
    assert X_out.dtype == np.float32

def test_check_array_pandas_dtype_casting():
    # test that data-frames with homogeneous dtype are not upcast
    X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.float32)
    assert check_array(X_df).dtype == np.float32
    assert check_array(X_df, dtype=FLOAT_DTYPES).dtype == np.float32

    X_df = X_df.astype({0: np.float16})
    assert_array_equal(X_df.dtypes, (np.float16, np.float32, np.float32))

    X_df = X_df.astype({0: np.int16})
    # float16, int16, float32 casts to float32

    X_df = X_df.astype({2: np.float16})
    # float16, int16, float16 casts to float32

    X_df = X_df.astype(np.int16)
    assert check_array(X_df).dtype == np.int16
    # we're not using upcasting rules for determining
    # the target type yet, so we cast to the default of float64
    assert check_array(X_df, dtype=FLOAT_DTYPES).dtype == np.float64

    # check that we handle pandas dtypes in a semi-reasonable way
    # this is actually tricky because we can't really know that this
    # should be integer ahead of converting it.
    cat_df = pd.DataFrame({"cat_col": pd.Categorical([1, 2, 3])})
    assert check_array(cat_df).dtype == np.int64
    assert check_array(cat_df, dtype=FLOAT_DTYPES).dtype == np.float64

def test_check_array_on_mock_dataframe():
    arr = np.array([[0.2, 0.7], [0.6, 0.5], [0.4, 0.1], [0.7, 0.2]])
    mock_df = MockDataFrame(arr)
    checked_arr = check_array(mock_df)
    assert checked_arr.dtype == arr.dtype
    checked_arr = check_array(mock_df, dtype=np.float32)
    assert checked_arr.dtype == np.dtype(np.float32)

def test_check_array_dtype_stability():
    # test that lists with ints don't get converted to floats
    X = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    assert check_array(X).dtype.kind == "i"
    assert check_array(X, ensure_2d=False).dtype.kind == "i"

def test_check_array_dtype_warning():
    X_int_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    X_float32 = np.asarray(X_int_list, dtype=np.float32)
    X_int64 = np.asarray(X_int_list, dtype=np.int64)
    X_csr_float32 = sp.csr_matrix(X_float32)
    X_csc_float32 = sp.csc_matrix(X_float32)
    X_csc_int32 = sp.csc_matrix(X_int64, dtype=np.int32)
    integer_data = [X_int64, X_csc_int32]
    float32_data = [X_float32, X_csr_float32, X_csc_float32]

        for X in integer_data:
            X_checked = check_array(X, dtype=np.float64, accept_sparse=True)
            assert X_checked.dtype == np.float64

        for X in float32_data:
            X_checked = check_array(
                X, dtype=[np.float64, np.float32], accept_sparse=True
            )
            assert X_checked.dtype == np.float32
            assert X_checked is X

                X,
                accept_sparse=["csr", "dok"],
                copy=True,
            )
            assert X_checked is not X

        X_checked = check_array(
            X_csc_float32,
            accept_sparse=["csr", "dok"],
        )
        assert X_checked.dtype == np.float32
        assert X_checked is not X_csc_float32
        assert X_checked.format == "csr"

def test_check_array_accept_sparse_type_exception():
    invalid_type = SVR()

    msg = (
        "Sparse data was passed, but dense data is required. "
        r"Use '.toarray\(\)' to convert to a dense numpy array."
    )
        check_array(X_csr, accept_sparse=False)

    msg = (
        "Parameter 'accept_sparse' should be a string, "
        "boolean or list of strings. You provided 'accept_sparse=.*'."
    )
        check_array(X_csr, accept_sparse=invalid_type)

    msg = (
        "When providing 'accept_sparse' as a tuple or list, "
        "it must contain at least one string value."
    )
        check_array(X_csr, accept_sparse=[])
        check_array(X_csr, accept_sparse=())
    with pytest.raises(TypeError, match="SVR"):
        check_array(X_csr, accept_sparse=[invalid_type])

def test_check_array_accept_sparse_no_exception():

    check_array(X_csr, accept_sparse=True)
    check_array(X_csr, accept_sparse="csr")
    check_array(X_csr, accept_sparse=["csr"])
    check_array(X_csr, accept_sparse=("csr",))

def X_64bit(request):
    X = sp.random(20, 10, format=request.param)

    if request.param == "coo":
        if hasattr(X, "coords"):
            # for scipy >= 1.13 .coords is a new attribute and is a tuple. The
            # .col and .row attributes do not seem to be able to change the
            # dtype, for more details see https://github.com/scipy/scipy/pull/18530/
            # and https://github.com/scipy/scipy/pull/20003 where .indices was
            # renamed to .coords
            X.coords = tuple(v.astype("int64") for v in X.coords)
        else:
            # scipy < 1.13
            X.row = X.row.astype("int64")
            X.col = X.col.astype("int64")
    else:

    yield X

def test_check_array_accept_large_sparse_no_exception(X_64bit):
    # When large sparse are allowed
    check_array(X_64bit, accept_large_sparse=True, accept_sparse=True)

def test_check_array_accept_large_sparse_raise_exception(X_64bit):
    # When large sparse are not allowed
    msg = (
        "Only sparse matrices with 32-bit integer indices "
        "are accepted. Got int64 indices. Please do report"
    )
        check_array(X_64bit, accept_sparse=True, accept_large_sparse=False)

def test_check_array_min_samples_and_features_messages():
    # empty list is considered 2D by default:
    msg = r"0 feature\(s\) \(shape=\(1, 0\)\) while a minimum of 1 is required."
        check_array([[]])

    # If considered a 1D collection when ensure_2d=False, then the minimum
    # number of samples will break:
    msg = r"0 sample\(s\) \(shape=\(0,\)\) while a minimum of 1 is required."
        check_array([], ensure_2d=False)

    # Invalid edge case when checking the default minimum sample of a scalar
        (
            "`len(x.shape) > 0`, got scalar `array(42)` instead."
        )
    )
        check_array(42, ensure_2d=False)

    # Simulate a model that would need at least 2 samples to be well defined
    X = np.ones((1, 10))
    y = np.ones(1)
    msg = r"1 sample\(s\) \(shape=\(1, 10\)\) while a minimum of 2 is required."
        check_X_y(X, y, ensure_min_samples=2)

    # The same message is raised if the data has 2 dimensions even if this is
    # not mandatory
        check_X_y(X, y, ensure_min_samples=2, ensure_2d=False)

    # Simulate a model that would require at least 3 features (e.g. SelectKBest
    # with k=3)
    y = np.ones(2)
    msg = r"2 feature\(s\) \(shape=\(10, 2\)\) while a minimum of 3 is required."
        check_X_y(X, y, ensure_min_features=3)

    # Only the feature check is enabled whenever the number of dimensions is 2
    # even if allow_nd is enabled:
        check_X_y(X, y, ensure_min_features=3, allow_nd=True)

    # Simulate a case where a pipeline stage as trimmed all the features of a
    # 2D dataset.
    X = np.empty(0).reshape(10, 0)
    msg = r"0 feature\(s\) \(shape=\(10, 0\)\) while a minimum of 1 is required."
        check_X_y(X, y)

    # nd-data is not checked for any minimum number of features by default:
    X = np.ones((10, 0, 28, 28))
    X_checked, y_checked = check_X_y(X, y, allow_nd=True)
    assert_array_equal(X, X_checked)
    assert_array_equal(y, y_checked)

def test_check_array_complex_data_error():
    X = np.array([[1 + 2j, 3 + 4j, 5 + 7j], [2 + 3j, 4 + 5j, 6 + 7j]])
    with pytest.raises(ValueError, match="Complex data not supported"):

    # list of lists
    X = [[1 + 2j, 3 + 4j, 5 + 7j], [2 + 3j, 4 + 5j, 6 + 7j]]

    # tuple of tuples
    X = ((1 + 2j, 3 + 4j, 5 + 7j), (2 + 3j, 4 + 5j, 6 + 7j))

    # list of np arrays
    X = [np.array([1 + 2j, 3 + 4j, 5 + 7j]), np.array([2 + 3j, 4 + 5j, 6 + 7j])]

    # tuple of np arrays
    X = (np.array([1 + 2j, 3 + 4j, 5 + 7j]), np.array([2 + 3j, 4 + 5j, 6 + 7j]))

    # dataframe
    X = MockDataFrame(np.array([[1 + 2j, 3 + 4j, 5 + 7j], [2 + 3j, 4 + 5j, 6 + 7j]]))

    # sparse matrix
    X = sp.coo_matrix([[0, 1 + 2j], [0, 0]])

    # target variable does not always go through check_array but should
    # never accept complex data either.
    y = np.array([1 + 2j, 3 + 4j, 5 + 7j, 2 + 3j, 4 + 5j, 6 + 7j])
        _check_y(y)

def test_has_fit_parameter():
    assert not has_fit_parameter(KNeighborsClassifier, "sample_weight")
    assert has_fit_parameter(RandomForestRegressor, "sample_weight")
    assert has_fit_parameter(SVR, "sample_weight")
    assert has_fit_parameter(SVR(), "sample_weight")

    class TestClassWithDeprecatedFitMethod:
        @deprecated("Deprecated for the purpose of testing has_fit_parameter")
            pass

    assert has_fit_parameter(TestClassWithDeprecatedFitMethod, "sample_weight"), (
        "has_fit_parameter fails for class with deprecated fit method."
    )

def test_check_symmetric():
    arr_sym = np.array([[0, 1], [1, 2]])
    arr_bad = np.ones(2)
    arr_asym = np.array([[0, 2], [0, 2]])

    test_arrays = {
        "dense": arr_asym,
        "dok": sp.dok_matrix(arr_asym),
        "csr": sp.csr_matrix(arr_asym),
        "csc": sp.csc_matrix(arr_asym),
        "coo": sp.coo_matrix(arr_asym),
        "lil": sp.lil_matrix(arr_asym),
        "bsr": sp.bsr_matrix(arr_asym),
    }

    # check error for bad inputs
        check_symmetric(arr_bad)

    # check that asymmetric arrays are properly symmetrized
    for arr_format, arr in test_arrays.items():
        # Check for warnings and errors
        with pytest.warns(UserWarning):
            check_symmetric(arr)
            check_symmetric(arr, raise_exception=True)

        output = check_symmetric(arr, raise_warning=False)
        if sp.issparse(output):
            assert output.format == arr_format
            assert_array_equal(output.toarray(), arr_sym)
        else:
            assert_array_equal(output, arr_sym)

def test_check_is_fitted_with_is_fitted():
        def fit(self, **kwargs):
            self._is_fitted = True

            return hasattr(self, "_is_fitted") and self._is_fitted

        check_is_fitted(Estimator())
    check_is_fitted(Estimator().fit())

def test_check_is_fitted_stateless():
    """Check that check_is_fitted passes for stateless estimators."""

            tags.requires_fit = False

    check_is_fitted(StatelessEstimator())

def test_check_is_fitted():
    # Check is TypeError raised when non estimator instance passed
        check_is_fitted(ARDRegression)
        check_is_fitted("SVR")

    ard = ARDRegression()
    svr = SVR()

    try:
            check_is_fitted(ard)
            check_is_fitted(svr)
        assert False, "check_is_fitted failed with ValueError"

    # NotFittedError is a subclass of both ValueError and AttributeError
    msg = "Random message %(name)s, %(name)s"
    match = "Random message ARDRegression, ARDRegression"
        check_is_fitted(ard, msg=msg)

    msg = "Another message %(name)s, %(name)s"
    match = "Another message SVR, SVR"
    with pytest.raises(AttributeError, match=match):
        check_is_fitted(svr, msg=msg)

    ard.fit(*make_blobs())
    svr.fit(*make_blobs())

    assert check_is_fitted(ard) is None
    assert check_is_fitted(svr) is None

def test_check_is_fitted_attributes():

    msg = "not fitted"

    assert not _is_fitted(est, attributes=["a_", "b_"])
        check_is_fitted(est, attributes=["a_", "b_"])
    assert not _is_fitted(est, attributes=["a_", "b_"], all_or_any=all)
        check_is_fitted(est, attributes=["a_", "b_"], all_or_any=all)
    assert not _is_fitted(est, attributes=["a_", "b_"], all_or_any=any)
        check_is_fitted(est, attributes=["a_", "b_"], all_or_any=any)

    est.a_ = "a"
    assert _is_fitted(est, attributes=["a_", "b_"], all_or_any=any)
    check_is_fitted(est, attributes=["a_", "b_"], all_or_any=any)

    est.b_ = "b"
    assert _is_fitted(est, attributes=["a_", "b_"])
    check_is_fitted(est, attributes=["a_", "b_"])
    assert _is_fitted(est, attributes=["a_", "b_"], all_or_any=all)
    check_is_fitted(est, attributes=["a_", "b_"], all_or_any=all)

def test_check_is_fitted_with_attributes(wrap):
        check_is_fitted(ard, wrap(["coef_"]))

    check_is_fitted(ard, wrap(["coef_"]))

    # Raises when using attribute that is not defined
        check_is_fitted(ard, wrap(["coef_bad_"]))

def test_check_consistent_length():
    """Test that `check_consistent_length` raises on inconsistent lengths and wrong
    input types trigger TypeErrors."""
    check_consistent_length([1], [2], [3], [4], [5])
    check_consistent_length([[1, 2], [[1, 2]]], [1, 2], ["a", "b"])
    check_consistent_length([1], (2,), np.array([3]), sp.csr_matrix((1, 2)))
        check_consistent_length([1, 2], [1])
    with pytest.raises(TypeError, match=r"got <\w+ 'int'>"):
        check_consistent_length([1, 2], 1)
    with pytest.raises(TypeError, match=r"got <\w+ 'object'>"):
        check_consistent_length([1, 2], object())
        check_consistent_length([1, 2], np.array(1))
    # Despite ensembles having __len__ they must raise TypeError
    with pytest.raises(TypeError, match="Expected sequence or array-like"):
        check_consistent_length([1, 2], RandomForestRegressor())

def test_check_consistent_length_array_api(array_namespace, device, _):
    """Test that check_consistent_length works with different array types."""

        check_consistent_length(
            xp.asarray([1, 2, 3], device=device),
            xp.asarray([[1, 1], [2, 2], [3, 3]], device=device),
            [1, 2, 3],
            ["a", "b", "c"],
            np.asarray(("a", "b", "c"), dtype=object),
            sp.csr_array([[0, 1], [1, 0], [0, 0]]),
        )

        with pytest.raises(ValueError, match="inconsistent numbers of samples"):
            check_consistent_length(
                xp.asarray([1, 2], device=device), xp.asarray([1], device=device)
            )

def test_check_dataframe_fit_attribute():
    # check pandas dataframe with 'fit' column does not raise error
    # https://github.com/scikit-learn/scikit-learn/issues/8415
    try:

        X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
        X_df = pd.DataFrame(X, columns=["a", "b", "fit"])
        check_consistent_length(X_df)
        raise SkipTest("Pandas not found")

def test_suppress_validation():
    X = np.array([0, np.inf])
        assert_all_finite(X)
    sklearn.set_config(assume_finite=True)
    sklearn.set_config(assume_finite=False)

def test_check_array_series():
    # regression test that check_array works on pandas Series
    pd = importorskip("pandas")
    res = check_array(pd.Series([1, 2, 3]), ensure_2d=False)
    assert_array_equal(res, np.array([1, 2, 3]))

    # with categorical dtype (not a numpy dtype) (GH12699)
    s = pd.Series(["a", "b", "c"]).astype("category")
    res = check_array(s, dtype=None, ensure_2d=False)
    assert_array_equal(res, np.array(["a", "b", "c"], dtype=object))

def test_check_dataframe_mixed_float_dtypes(dtype, bool_dtype):
    # pandas dataframe will coerce a boolean into a object, this is a mismatch
    # with np.result_type which will return a float
    # check_array needs to explicitly check for bool dtype in a dataframe for
    # this situation
    # https://github.com/scikit-learn/scikit-learn/issues/15787

    df = pd.DataFrame(
        {
            "int": [1, 2, 3],
            "float": [0, 0.1, 2.1],
            "bool": pd.Series([True, False, True], dtype=bool_dtype),
        },
        columns=["int", "float", "bool"],
    )

    array = check_array(df, dtype=dtype)
    assert array.dtype == np.float64
    expected_array = np.array(
        [[1.0, 0.0, 1.0], [2.0, 0.1, 0.0], [3.0, 2.1, 1.0]], dtype=float
    )
    assert_allclose_dense_sparse(array, expected_array)

def test_check_dataframe_with_only_bool():
    """Check that dataframe with bool return a boolean arrays."""
    df = pd.DataFrame({"bool": [True, False, True]})

    array = check_array(df, dtype=None)
    assert array.dtype == np.bool_
    assert_array_equal(array, [[True], [False], [True]])

    # common dtype is int for bool + int
        {"bool": [True, False, True], "int": [1, 2, 3]},
        columns=["bool", "int"],
    )
    array = check_array(df, dtype="numeric")
    assert array.dtype == np.int64
    assert_array_equal(array, [[1, 1], [0, 2], [1, 3]])

def test_check_dataframe_with_only_boolean():
    """Check that dataframe with boolean return a float array with dtype=None"""
    df = pd.DataFrame({"bool": pd.Series([True, False, True], dtype="boolean")})

class DummyMemory:
    def cache(self, func):

def test_check_memory():
    memory = check_memory("cache_directory")
    assert memory.location == "cache_directory"

    memory = check_memory(None)
    assert memory.location is None

    dummy = DummyMemory()
    memory = check_memory(dummy)
    assert memory is dummy

    msg = (
        "'memory' should be None, a string or have the same interface as"
        " joblib.Memory. Got memory='1' instead."
    )
        check_memory(1)
    dummy = WrongDummyMemory()
    msg = (
        " joblib.Memory. Got memory='{}' instead.".format(dummy)
    )
        check_memory(dummy)

def test_check_array_memmap(copy):
    X = np.ones((4, 4))
    with TempMemmap(X, mmap_mode="r") as X_memmap:
        X_checked = check_array(X_memmap, copy=copy)
        assert np.may_share_memory(X_memmap, X_checked) == (not copy)
        assert X_checked.flags["WRITEABLE"] == copy

def test_estimator_has(
    estimator_name, estimator_value, delegates, expected_result, expected_exception
):
    """
    Tests the _estimator_has function by verifying:
    - Functionality with default and custom delegates.
    - Raises ValueError if delegates are missing.
    - Raises AttributeError if the specified attribute is missing.
    """

    # always checks for attribute - "attribute_present"
    # ["estimator_", "estimator"] is default value for delegates
    if delegates is None:
        check = _estimator_has("attribute_present")
    else:
        check = _estimator_has("attribute_present", delegates=delegates)

        pass

    a = MockEstimator()
    setattr(a, estimator_name, estimator_value)

    if expected_exception:
        with pytest.raises(expected_exception):
            check(a)
    else:
        assert check(a) == expected_result

def test_check_non_negative(retype):
    A = np.array([[1, 1, 0, 0], [1, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
    X = retype(A)
    check_non_negative(X, "")
    X = retype([[0, 0], [0, 0]])

    A[0, 0] = -1
    with pytest.raises(ValueError, match="Negative "):
        check_non_negative(X, "")

def test_check_X_y_informative_error():
    X = np.ones((2, 2))
    y = None
    msg = "estimator requires y to be passed, but the target y is None"

    msg = "RandomForestRegressor requires y to be passed, but the target y is None"
        check_X_y(X, y, estimator=RandomForestRegressor())

def test_retrieve_samples_from_non_standard_shape():
    class TestNonNumericShape:
            self.shape = ("not numeric",)

        def __len__(self):
            return len([1, 2, 3])

    X = TestNonNumericShape()
    assert _num_samples(X) == len(X)

    # check that it gives a good error if there's no __len__
    class TestNoLenWeirdShape:

        _num_samples(TestNoLenWeirdShape())

def test_check_scalar_valid(x):
    """Test that check_scalar returns no error/warning if valid inputs are
    provided"""
        scalar = check_scalar(
            x,
            "test_name",
            min_val=2,
            max_val=5,
            include_boundaries="both",
        )
    assert scalar == x

def test_check_scalar_invalid(
    x, target_name, target_type, min_val, max_val, include_boundaries, err_msg
):
    """Test that check_scalar returns the right error if a wrong input is
    given"""
    with pytest.raises(Exception) as raised_error:
            x,
            target_name,
            target_type=target_type,
            min_val=min_val,
            max_val=max_val,
            include_boundaries=include_boundaries,
        )
    assert str(raised_error.value) == str(err_msg)
    assert isinstance(raised_error.value, type(err_msg))

def test_check_psd_eigenvalues_valid(
    lambdas, expected_lambdas, w_type, w_msg, enable_warnings
):
    # Test that ``_check_psd_eigenvalues`` returns the right output for valid
    # input, possibly raising the right warning

    if not enable_warnings:
        w_type = None

    if w_type is None:
            warnings.simplefilter("error", PositiveSpectrumWarning)
            lambdas_fixed = _check_psd_eigenvalues(
                lambdas, enable_warnings=enable_warnings
            )
    else:
        with pytest.warns(w_type, match=w_msg):
            )

    assert_allclose(expected_lambdas, lambdas_fixed)

def test_check_psd_eigenvalues_invalid(lambdas, err_type, err_msg):
    # Test that ``_check_psd_eigenvalues`` raises the right error for invalid
    # input

        _check_psd_eigenvalues(lambdas)

def test_check_sample_weight():
    # check array order
    sample_weight = np.ones(10)[::2]
    assert not sample_weight.flags["C_CONTIGUOUS"]
    sample_weight = _check_sample_weight(sample_weight, X=np.ones((5, 1)))
    assert sample_weight.flags["C_CONTIGUOUS"]

    # check None input
    sample_weight = _check_sample_weight(None, X=np.ones((5, 2)))
    assert_allclose(sample_weight, np.ones(5))

    # check numbers input
    sample_weight = _check_sample_weight(2.0, X=np.ones((5, 2)))
    assert_allclose(sample_weight, 2 * np.ones(5))

    # check wrong number of dimensions
    with pytest.raises(ValueError, match="Sample weights must be 1D array or scalar"):
        _check_sample_weight(np.ones((2, 4)), X=np.ones((2, 2)))

    # check incorrect n_samples
    msg = r"sample_weight.shape == \(4,\), expected \(2,\)!"
        _check_sample_weight(np.ones(4), X=np.ones((2, 2)))

    # float32 dtype is preserved
    sample_weight = np.ones(5, dtype=np.float32)
    sample_weight = _check_sample_weight(sample_weight, X)
    assert sample_weight.dtype == np.float32

    # int dtype will be converted to float64 instead
    X = np.ones((5, 2), dtype=int)
    sample_weight = _check_sample_weight(None, X, dtype=X.dtype)
    assert sample_weight.dtype == np.float64

    # check negative weight when ensure_non_negative=True
    sample_weight = np.ones(_num_samples(X))
        _check_sample_weight(sample_weight, X, ensure_non_negative=True)

def test_allclose_dense_sparse_equals(toarray):
    base = np.arange(9).reshape(3, 3)
    x, y = toarray(base), toarray(base)
    assert _allclose_dense_sparse(x, y)

def test_allclose_dense_sparse_not_equals(toarray):
    x, y = toarray(base), toarray(base + 1)
    assert not _allclose_dense_sparse(x, y)

def test_allclose_dense_sparse_raise(toarray):
    y = toarray(x + 1)

    msg = "Can only compare two sparse matrices, not a sparse matrix and an array"
        _allclose_dense_sparse(x, y)

def test_deprecate_positional_args_warns_for_function():
    @_deprecate_positional_args
    def f1(a, b, *, c=1, d=1):
        pass

    with pytest.warns(FutureWarning, match=r"Pass c=3 as keyword args"):
        f1(1, 2, 3)

    with pytest.warns(FutureWarning, match=r"Pass c=3, d=4 as keyword args"):
        f1(1, 2, 3, 4)

    def f2(a=1, *, b=1, c=1, d=1):
        pass

    with pytest.warns(FutureWarning, match=r"Pass b=2 as keyword args"):
        f2(1, 2)

    # The * is place before a keyword only argument without a default value
    def f3(a, *, b, c=1, d=1):
        pass

        f3(1, 2)

def test_deprecate_positional_args_warns_for_function_version():
    @_deprecate_positional_args(version="1.1")
    def f1(a, *, b):
        pass

        FutureWarning, match=r"From version 1.1 passing these as positional"
    ):
        f1(1, 2)

def test_deprecate_positional_args_warns_for_class():
    class A1:
        @_deprecate_positional_args
        def __init__(self, a, b, *, c=1, d=1):
            pass

        A1(1, 2, 3)

        A1(1, 2, 3, 4)

    class A2:
        def __init__(self, a=1, b=1, *, c=1, d=1):
            pass

        A2(1, 2, 3)

        A2(1, 2, 3, 4)

def test_check_method_params(indices):
    X = np.random.randn(4, 2)
    _params = {
        "list": [1, 2, 3, 4],
        "array": np.array([1, 2, 3, 4]),
        "sparse-col": sp.csc_matrix([1, 2, 3, 4]).T,
        "sparse-row": sp.csc_matrix([1, 2, 3, 4]),
        "scalar-int": 1,
        "scalar-str": "xxx",
        "None": None,
    }
    result = _check_method_params(X, params=_params, indices=indices)
    indices_ = indices if indices is not None else list(range(X.shape[0]))

    for key in ["sparse-row", "scalar-int", "scalar-str", "None"]:
        assert result[key] is _params[key]

    assert result["list"] == _safe_indexing(_params["list"], indices_)
    assert_array_equal(result["array"], _safe_indexing(_params["array"], indices_))
    assert_allclose_dense_sparse(
        result["sparse-col"], _safe_indexing(_params["sparse-col"], indices_)
    )

def test_check_sparse_pandas_sp_format(sp_format):
    # check_array converts pandas dataframe with only sparse arrays into
    sp_mat = _sparse_random_matrix(10, 3)

    sdf = pd.DataFrame.sparse.from_spmatrix(sp_mat)
    result = check_array(sdf, accept_sparse=sp_format)

    if sp_format is True:
        # by default pandas converts to coo when accept_sparse is True
        sp_format = "coo"

    assert sp.issparse(result)
    assert result.format == sp_format
    assert_allclose_dense_sparse(sp_mat, result)

def test_check_pandas_sparse_mixed_dtypes(ntype1, ntype2):
    """Check that pandas dataframes having sparse extension arrays with mixed dtypes
    works."""
        {
            "col1": pd.arrays.SparseArray([0, 1, 0], dtype=ntype1, fill_value=0),
            "col2": pd.arrays.SparseArray([1, 0, 1], dtype=ntype2, fill_value=0),
        }
    )
    check_array(df, accept_sparse=["csr", "csc"])

def test_check_pandas_sparse_valid(ntype1, ntype2, expected_subtype):
    # check that we support the conversion of sparse dataframe with mixed
    # type which can be converted safely.
        {
        }
    )
    arr = check_array(df, accept_sparse=["csr", "csc"])
    assert np.issubdtype(arr.dtype, expected_subtype)

def test_num_features(constructor_name):
    """Check _num_features for array-likes."""
    assert _num_features(X) == 3

def test_num_features_errors_1d_containers(X, constructor_name):
    if constructor_name == "array":
        expected_type_name = "numpy.ndarray"
        expected_type_name = "pandas.*Series"
    else:
        expected_type_name = constructor_name
    message = (
        f"Unable to find the number of features from X of type {expected_type_name}"
    )
        message += re.escape(" with shape (3,)")
    elif isinstance(X[0], str):
        message += " where the samples are of type str"
    elif isinstance(X[0], dict):
        message += " where the samples are of type dict"
        _num_features(X)

def test_num_features_errors_scalars(X):
    msg = f"Unable to find the number of features from X of type {type(X).__qualname__}"

def test_get_feature_names_pandas_with_ints_no_warning(names):
    """Get feature names with pandas dataframes without warning.

    Column names with consistent dtypes will not warn, such as int or MultiIndex.
    """
    X = pd.DataFrame([[1, 2], [4, 5], [5, 6]], columns=names)

        names = _get_feature_names(X)
    assert names is None

def test_get_feature_names_pandas():
    """Get feature names with pandas dataframes."""
    columns = [f"col_{i}" for i in range(3)]
    X = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=columns)
    feature_names = _get_feature_names(X)

    assert_array_equal(feature_names, columns)

def test_get_feature_names_dataframe_protocol(constructor_name, minversion):
    )
    feature_names = _get_feature_names(df)

def test_is_pandas_df_other_libraries(constructor_name):
    df = _convert_container([[1, 4, 2], [3, 3, 6]], constructor_name)
    if constructor_name in ("pyarrow", "polars"):
        assert not _is_pandas_df(df)
    else:
        assert _is_pandas_df(df)

def test_is_pandas_df():
    """Check behavior of is_pandas_df when pandas is installed."""
    df = pd.DataFrame([[1, 2, 3]])
    assert _is_pandas_df(df)
    assert not _is_pandas_df(np.asarray([1, 2, 3]))
    assert not _is_pandas_df(1)

def test_is_pandas_df_pandas_not_installed(hide_available_pandas):
    """Check _is_pandas_df when pandas is not installed."""

def test_is_polars_df_other_libraries(constructor_name, minversion):
        [[1, 4, 2], [3, 3, 6]],
        minversion=minversion,
    )
    if constructor_name in ("pyarrow", "dataframe"):
        assert not _is_polars_df(df)
    else:
        assert _is_polars_df(df)

def test_is_polars_df_for_duck_typed_polars_dataframe():
    """Check _is_polars_df for object that looks like a polars dataframe"""

    class NotAPolarsDataFrame:
            self.columns = [1, 2, 3]
            self.schema = "my_schema"

    not_a_polars_df = NotAPolarsDataFrame()
    assert not _is_polars_df(not_a_polars_df)

def test_get_feature_names_numpy():
    """Get feature names return None for numpy arrays."""
    X = np.array([[1, 2, 3], [4, 5, 6]])
    names = _get_feature_names(X)

def test_get_feature_names_invalid_dtypes(names, dtypes):
    """Get feature names errors when the feature names have mixed dtypes"""

        f"but your input has {dtypes} as feature name / column name types. "
    )

class PassthroughTransformer(BaseEstimator):
        validate_data(self, X, reset=True)

        return X

def test_check_feature_names_in():
    """Check behavior of check_feature_names_in for arrays."""
    X = np.array([[0.0, 1.0, 2.0]])
    est = PassthroughTransformer().fit(X)

    names = est.get_feature_names_out()
    assert_array_equal(names, ["x0", "x1", "x2"])

    incorrect_len_names = ["x10", "x1"]
    with pytest.raises(ValueError, match="input_features should have length equal to"):
        est.get_feature_names_out(incorrect_len_names)

    # remove n_feature_in_
    del est.n_features_in_
    with pytest.raises(ValueError, match="Unable to generate feature names"):
        est.get_feature_names_out()

def test_check_feature_names_in_pandas():
    """Check behavior of check_feature_names_in for pandas dataframes."""
    names = ["a", "b", "c"]
    df = pd.DataFrame([[0.0, 1.0, 2.0]], columns=names)
    est = PassthroughTransformer().fit(df)

    assert_array_equal(names, ["a", "b", "c"])

    with pytest.raises(ValueError, match="input_features is not equal to"):
        est.get_feature_names_out(["x1", "x2", "x3"])

def test_check_response_method_unknown_method():
    """Check the error message when passing an unknown response method."""
    err_msg = (
        "RandomForestRegressor has none of the following attributes: unknown_method."
    )
        _check_response_method(RandomForestRegressor(), "unknown_method")

def test_check_response_method_not_supported_response_method(response_method):
    """Check the error message when a response method is not supported by the
    estimator."""
    err_msg = (
        f"EstimatorWithFit has none of the following attributes: {response_method}."
    )
        _check_response_method(EstimatorWithFit(), response_method)

def test_check_response_method_list_str():
    """Check that we can pass a list of ordered method."""
    method_implemented = ["predict_proba"]
    my_estimator = _MockEstimatorOnOffPrediction(method_implemented)

    X = "mocking_data"

    # raise an error when no methods are defined
    response_method = ["decision_function", "predict"]
    err_msg = (
        "_MockEstimatorOnOffPrediction has none of the following attributes: "
        f"{', '.join(response_method)}."
    )
        _check_response_method(my_estimator, response_method)(X)

    # check that we don't get issue when one of the method is defined
    response_method = ["decision_function", "predict_proba"]
    method_name_predicting = _check_response_method(my_estimator, response_method)(X)
    assert method_name_predicting == "predict_proba"

    # check the order of the methods returned
    method_implemented = ["predict_proba", "predict"]
    response_method = ["decision_function", "predict", "predict_proba"]
    assert method_name_predicting == "predict"

def test_boolean_series_remains_boolean():
    """Regression test for gh-25145"""
    res = check_array(pd.Series([True, False]), ensure_2d=False)
    expected = np.array([True, False])

    assert res.dtype == expected.dtype
    assert_array_equal(res, expected)

def test_pandas_array_returns_ndarray(input_values):
    """Check pandas array with extensions dtypes returns a numeric ndarray.

    Non-regression test for gh-25637.
    """
    input_series = pd.array(input_values, dtype="Int32")
    result = check_array(
        input_series,
        allow_nd=False,
    )
    assert np.issubdtype(result.dtype.kind, np.floating)
    assert_allclose(result, input_values)

def test_check_array_array_api_has_non_finite():
    """Checks that Array API arrays checks non-finite correctly."""

    X_nan = xp.asarray([[xp.nan, 1, 0], [0, xp.nan, 3]], dtype=xp.float32)
        with pytest.raises(ValueError, match="Input contains NaN."):
            check_array(X_nan)

    X_inf = xp.asarray([[xp.inf, 1, 0], [0, xp.inf, 3]], dtype=xp.float32)
        with pytest.raises(ValueError, match="infinity or a value too large"):
            check_array(X_inf)

def test_check_array_multiple_extensions(
    extension_dtype, regular_dtype, include_object
):
    """Check pandas extension arrays give the same result as non-extension arrays."""
    X_regular = pd.DataFrame(
        {
            "a": pd.Series([1, 0, 1, 0], dtype=regular_dtype),
            "c": pd.Series([9, 8, 7, 6], dtype="int64"),
        }
    )
    if include_object:
        X_regular["b"] = pd.Series(["a", "b", "c", "d"], dtype="object")

    X_extension = X_regular.assign(a=X_regular["a"].astype(extension_dtype))

    X_regular_checked = check_array(X_regular, dtype=None)
    X_extension_checked = check_array(X_extension, dtype=None)
    assert_array_equal(X_regular_checked, X_extension_checked)

def test_num_samples_dataframe_protocol():
    """Use the DataFrame interchange protocol to get n_samples from polars."""

    df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    assert _num_samples(df) == 3

def test_check_array_dia_to_int32_indexed_csr_csc_coo(sparse_container, output_format):
    """Check the consistency of the indices dtype with sparse matrices/arrays."""
    X = sparse_container([[0, 1], [1, 0]], dtype=np.float64)

    # Explicitly set the dtype of the indexing arrays
    if hasattr(X, "offsets"):  # DIA matrix
        X.offsets = X.offsets.astype(np.int32)
    elif hasattr(X, "row") and hasattr(X, "col"):  # COO matrix
        X.row = X.row.astype(np.int32)
    elif hasattr(X, "indices") and hasattr(X, "indptr"):  # CSR or CSC matrix
        X.indices = X.indices.astype(np.int32)
        X.indptr = X.indptr.astype(np.int32)

    X_checked = check_array(X, accept_sparse=output_format)
    if output_format == "coo":
        assert X_checked.row.dtype == np.int32
        assert X_checked.col.dtype == np.int32
    else:  # output_format in ["csr", "csc"]
        assert X_checked.indices.dtype == np.int32
        assert X_checked.indptr.dtype == np.int32

def test_to_object_array(sequence):
    out = _to_object_array(sequence)
    assert isinstance(out, np.ndarray)
    assert out.dtype.kind == "O"
    assert out.ndim == 1

def test_column_or_1d():
        ("binary", ["spam", "egg", "spam"]),
        ("binary", [0, 1, 0, 1]),
        ("continuous", np.arange(10) / 20.0),
        ("multiclass", [1, 2, 3]),
        ("multiclass", [0, 1, 2, 2, 0]),
        ("multiclass", [[1], [2], [3]]),
        ("multilabel-indicator", [[0, 1, 0], [0, 0, 1]]),
        ("multiclass-multioutput", [[1, 2, 3]]),
        ("multiclass-multioutput", [[1, 1], [2, 2], [3, 1]]),
        ("multiclass-multioutput", [[5, 1], [4, 2], [3, 1]]),
        ("continuous-multioutput", np.arange(30).reshape((-1, 3))),
    ]

    for y_type, y in EXAMPLES:
        if y_type in ["binary", "multiclass", "continuous"]:
            assert_array_equal(column_or_1d(y), np.ravel(y))
        else:
                column_or_1d(y)

def test__is_polars_df():
    """Check that _is_polars_df return False for non-dataframe objects."""

    class LooksLikePolars:
            self.columns = ["a", "b"]
            self.schema = ["a", "b"]

    assert not _is_polars_df(LooksLikePolars())

def test_check_array_writeable_np():
    """Check the behavior of check_array when a writeable array is requested
    without copy if possible, on numpy arrays.
    """

    out = check_array(X, copy=False, force_writeable=True)
    # X is already writeable, no copy is needed
    assert np.may_share_memory(out, X)
    assert out.flags.writeable

    # X is not writeable, a copy is made
    assert not np.may_share_memory(out, X)

def test_check_array_writeable_mmap():
    without copy if possible, on a memory-map.

    A common situation is when a meta-estimators run in parallel using multiprocessing
    with joblib, which creates read-only memory-maps of large arrays.
    """

    mmap = create_memmap_backed_data(X, mmap_mode="w+")
    out = check_array(mmap, copy=False, force_writeable=True)
    # mmap is already writeable, no copy is needed
    assert np.may_share_memory(out, mmap)

    mmap = create_memmap_backed_data(X, mmap_mode="r")
    # mmap is read-only, a copy is made
    assert not np.may_share_memory(out, mmap)

def test_check_array_writeable_df():
    without copy if possible, on a dataframe.
    """

    df = pd.DataFrame(X, copy=False)

    out = check_array(df, copy=False, force_writeable=True)
    # df is backed by a writeable array, no copy is needed
    assert np.may_share_memory(out, df)

    # df is backed by a read-only array, a copy is made
    assert not np.may_share_memory(out, df)

def test_check_array_on_sparse_inputs_with_array_api_enabled():
    X_sp = sp.csr_array([[0, 1, 0], [1, 0, 1]])
        assert sp.issparse(check_array(X_sp, accept_sparse=True))

            check_array(X_sp)

    y = np.random.randint(1, size=(10,))

        check_array(X, force_all_finite=True)

        check_X_y(X, y, force_all_finite=True)

        as_float_array(X, force_all_finite=True)

def test_check_array_allow_nd_errors(X, estimator, expected_error_message):
    with pytest.raises(ValueError, match=expected_error_message):
        check_array(X, estimator=estimator)

            pass

    class A1:
            pass

    class A2:
            pass

            pass

            pass

def test_mean_variance_axis0(csc_container, csr_container, lil_container):
    X, _ = make_classification(5, 4, random_state=0)
    # Sparsify the array a little bit
    X[0, 0] = 0
    X[2, 1] = 0
    X[4, 3] = 0
    X_lil = lil_container(X)
    X_lil[1, 0] = 0
    X[1, 0] = 0

        mean_variance_axis(X_lil, axis=0)

    X_csr = csr_container(X_lil)
    X_csc = csc_container(X_lil)

    expected_dtypes = [
        (np.float32, np.float32),
        (np.float64, np.float64),
        (np.int32, np.float64),
        (np.int64, np.float64),
    ]

    for input_dtype, output_dtype in expected_dtypes:
        X_test = X.astype(input_dtype)
        for X_sparse in (X_csr, X_csc):
            X_sparse = X_sparse.astype(input_dtype)
            X_means, X_vars = mean_variance_axis(X_sparse, axis=0)
            assert X_means.dtype == output_dtype
            assert X_vars.dtype == output_dtype
            assert_array_almost_equal(X_means, np.mean(X_test, axis=0))
            assert_array_almost_equal(X_vars, np.var(X_test, axis=0))

def test_mean_variance_axis0_precision(dtype, sparse_constructor):
    # Check that there's no big loss of precision when the real variance is
    # exactly 0. (#19766)
    X = np.full(fill_value=100.0, shape=(1000, 1), dtype=dtype)
    # Add some missing records which should be ignored:
    missing_indices = rng.choice(np.arange(X.shape[0]), 10, replace=False)
    X[missing_indices, 0] = np.nan
    X = sparse_constructor(X)

    # Random positive weights:
    sample_weight = rng.rand(X.shape[0]).astype(dtype)

    _, var = mean_variance_axis(X, weights=sample_weight, axis=0)

    assert var < np.finfo(dtype).eps

def test_mean_variance_axis1(csc_container, csr_container, lil_container):
    X[0, 0] = 0
    X[2, 1] = 0
    X[4, 3] = 0
    X[1, 0] = 0

        mean_variance_axis(X_lil, axis=1)

    ]

def test_incr_mean_variance_axis_weighted_axis1(
    Xw, X, weights, sparse_constructor, dtype
):
    axis = 1
    Xw_sparse = sparse_constructor(Xw).astype(dtype)
    X_sparse = sparse_constructor(X).astype(dtype)

    last_mean = np.zeros(np.shape(Xw)[0], dtype=dtype)
    last_var = np.zeros_like(last_mean, dtype=dtype)
    last_n = np.zeros_like(last_mean, dtype=np.int64)
    means0, vars0, n_incr0 = incr_mean_variance_axis(
        X=X_sparse,
        last_mean=last_mean,
        last_var=last_var,
        last_n=last_n,
    )

    means_w0, vars_w0, n_incr_w0 = incr_mean_variance_axis(
        X=Xw_sparse,
    )

    assert means_w0.dtype == dtype
    assert vars_w0.dtype == dtype
    assert n_incr_w0.dtype == dtype

    means_simple, vars_simple = mean_variance_axis(X=X_sparse, axis=axis)

    assert_array_almost_equal(means0, means_w0)
    assert_array_almost_equal(means0, means_simple)
    assert_array_almost_equal(vars0, vars_w0)
    assert_array_almost_equal(vars0, vars_simple)
    assert_array_almost_equal(n_incr0, n_incr_w0)

    # check second round for incremental
    means1, vars1, n_incr1 = incr_mean_variance_axis(
        last_mean=means0,
        last_var=vars0,
        last_n=n_incr0,
    )

    means_w1, vars_w1, n_incr_w1 = incr_mean_variance_axis(
        last_mean=means_w0,
        last_var=vars_w0,
        last_n=n_incr_w0,
    )

    assert_array_almost_equal(means1, means_w1)
    assert_array_almost_equal(vars1, vars_w1)
    assert_array_almost_equal(n_incr1, n_incr_w1)

    assert means_w1.dtype == dtype
    assert vars_w1.dtype == dtype
    assert n_incr_w1.dtype == dtype

def test_incr_mean_variance_axis_weighted_axis0(
):
    axis = 0

    last_mean = np.zeros(np.size(Xw, 1), dtype=dtype)
    last_var = np.zeros_like(last_mean)
    )

    )

    )

    )

def test_incr_mean_variance_axis(csc_container, csr_container, lil_container):
    for axis in [0, 1]:
        n_features = 50
        n_samples = 10
            data_chunks = [rng.randint(0, 2, size=n_features) for i in range(n_samples)]
        else:
            data_chunks = [rng.randint(0, 2, size=n_samples) for i in range(n_features)]

        # default params for incr_mean_variance
        last_mean = np.zeros(n_features) if axis == 0 else np.zeros(n_samples)
        last_var = np.zeros_like(last_mean)
        last_n = np.zeros_like(last_mean, dtype=np.int64)

        # Test errors
        X = np.array(data_chunks[0])
        X = X.T if axis == 1 else X
        X_lil = lil_container(X)
        X_csr = csr_container(X_lil)

            incr_mean_variance_axis(
                X=axis, axis=last_mean, last_mean=last_var, last_var=last_n
            )
                X_lil, axis=axis, last_mean=last_mean, last_var=last_var, last_n=last_n
            )

        # Test _incr_mean_and_var with a 1 row input
        X_means, X_vars = mean_variance_axis(X_csr, axis)
        X_means_incr, X_vars_incr, n_incr = incr_mean_variance_axis(
            X_csr, axis=axis, last_mean=last_mean, last_var=last_var, last_n=last_n
        )
        assert_array_almost_equal(X_means, X_means_incr)
        assert_array_almost_equal(X_vars, X_vars_incr)
        # X.shape[axis] picks # samples
        assert_array_equal(X.shape[axis], n_incr)

        X_csc = csc_container(X_lil)
        X_means, X_vars = mean_variance_axis(X_csc, axis)

        # Test _incremental_mean_and_var with whole data
        X = np.vstack(data_chunks)

        expected_dtypes = [
            (np.float32, np.float32),
            (np.float64, np.float64),
            (np.int32, np.float64),
            (np.int64, np.float64),
        ]

        for input_dtype, output_dtype in expected_dtypes:
            for X_sparse in (X_csr, X_csc):
                X_sparse = X_sparse.astype(input_dtype)
                last_mean = last_mean.astype(output_dtype)
                last_var = last_var.astype(output_dtype)
                X_means, X_vars = mean_variance_axis(X_sparse, axis)
                X_means_incr, X_vars_incr, n_incr = incr_mean_variance_axis(
                    X_sparse,
                    axis=axis,
                    last_mean=last_mean,
                    last_var=last_var,
                    last_n=last_n,
                )
                assert X_means_incr.dtype == output_dtype
                assert X_vars_incr.dtype == output_dtype
                assert_array_almost_equal(X_means, X_means_incr)
                assert_array_almost_equal(X_vars, X_vars_incr)
                assert_array_equal(X.shape[axis], n_incr)

def test_incr_mean_variance_axis_dim_mismatch(sparse_constructor):
    """Check that we raise proper error when axis=1 and the dimension mismatch.
    https://github.com/scikit-learn/scikit-learn/pull/18655
    """
    n_samples, n_features = 60, 4
    X = sparse_constructor(rng.rand(n_samples, n_features))

    last_mean = np.zeros(n_features)
    last_n = np.zeros(last_mean.shape, dtype=np.int64)

    kwargs = dict(last_mean=last_mean, last_var=last_var, last_n=last_n)
    mean0, var0, _ = incr_mean_variance_axis(X, axis=0, **kwargs)
    assert_allclose(np.mean(X.toarray(), axis=0), mean0)
    assert_allclose(np.var(X.toarray(), axis=0), var0)

    # test ValueError if axis=1 and last_mean.size == n_features
        incr_mean_variance_axis(X, axis=1, **kwargs)

    # test inconsistent shapes of last_mean, last_var, last_n
    kwargs = dict(last_mean=last_mean[:-1], last_var=last_var, last_n=last_n)
        incr_mean_variance_axis(X, axis=0, **kwargs)

def test_incr_mean_variance_axis_equivalence_mean_variance(X1, X2, csr_container):
    # https://github.com/scikit-learn/scikit-learn/issues/16448
    # check that computing the incremental mean and variance is equivalent to
    # computing the mean and variance on the stacked dataset.
    X1 = csr_container(X1)
    X2 = csr_container(X2)
    axis = 0
    last_mean, last_var = np.zeros(X1.shape[1]), np.zeros(X1.shape[1])
    last_n = np.zeros(X1.shape[1], dtype=np.int64)
    updated_mean, updated_var, updated_n = incr_mean_variance_axis(
        X1, axis=axis, last_mean=last_mean, last_var=last_var, last_n=last_n
    )
        X2, axis=axis, last_mean=updated_mean, last_var=updated_var, last_n=updated_n
    )
    X = sp.vstack([X1, X2])
    assert_allclose(updated_mean, np.nanmean(X.toarray(), axis=axis))
    assert_allclose(updated_var, np.nanvar(X.toarray(), axis=axis))
    assert_allclose(updated_n, np.count_nonzero(~np.isnan(X.toarray()), axis=0))

def test_incr_mean_variance_no_new_n():
    # check the behaviour when we update the variance with an empty matrix
    axis = 0
    X1 = sp.random(5, 1, density=0.8, random_state=0).tocsr()
    X2 = sp.random(0, 1, density=0.8, random_state=0).tocsr()
    last_mean, last_var, last_n = incr_mean_variance_axis(
    )
    # update statistic with a column which should ignored
        X2, axis=axis, last_mean=last_mean, last_var=last_var, last_n=last_n
    )
    assert_allclose(updated_mean, last_mean)
    assert_allclose(updated_var, last_var)
    assert_allclose(updated_n, last_n)

def test_incr_mean_variance_n_float():
    # check the behaviour when last_n is just a number
    axis = 0
    X = sp.random(5, 2, density=0.8, random_state=0).tocsr()
    last_mean, last_var = np.zeros(X.shape[1]), np.zeros(X.shape[1])
    last_n = 0
    _, _, new_n = incr_mean_variance_axis(
        X, axis=axis, last_mean=last_mean, last_var=last_var, last_n=last_n
    )
    assert_allclose(new_n, np.full(X.shape[1], X.shape[0]))

def test_incr_mean_variance_axis_ignore_nan(axis, sparse_constructor):
    old_sample_count = np.array([2, 2, 2, 2], dtype=np.int64)

    X = sparse_constructor(
        np.array([[170, 170, 170, 170], [430, 430, 430, 430], [300, 300, 300, 300]])
    )

    X_nan = sparse_constructor(
        np.array(
            [
                [170, np.nan, 170, 170],
                [np.nan, 170, 430, 430],
                [430, 430, np.nan, 300],
                [300, 300, 300, np.nan],
            ]
        )
    )

    # we avoid creating specific data for axis 0 and 1: translating the data is
    # enough.
    if axis:
        X = X.T
        X_nan = X_nan.T

    # take a copy of the old statistics since they are modified in place.
    X_means, X_vars, X_sample_count = incr_mean_variance_axis(
        X,
        last_mean=old_means.copy(),
        last_var=old_variances.copy(),
        last_n=old_sample_count.copy(),
    )
    X_nan_means, X_nan_vars, X_nan_sample_count = incr_mean_variance_axis(
        X_nan,
    )

    assert_allclose(X_nan_vars, X_vars)
    assert_allclose(X_nan_sample_count, X_sample_count)

def test_mean_variance_illegal_axis(csr_container):
    X[0, 0] = 0
    X[2, 1] = 0
    X[4, 3] = 0
        mean_variance_axis(X_csr, axis=-3)
        mean_variance_axis(X_csr, axis=2)
        mean_variance_axis(X_csr, axis=-1)

        incr_mean_variance_axis(
            X_csr, axis=-3, last_mean=None, last_var=None, last_n=None
        )

            X_csr, axis=2, last_mean=None, last_var=None, last_n=None
        )

            X_csr, axis=-1, last_mean=None, last_var=None, last_n=None
        )

def test_densify_rows(csr_container):
        X = csr_container(
            [[0, 3, 0], [2, 4, 0], [0, 0, 0], [9, 8, 7], [4, 0, 5]], dtype=dtype
        )
        X_rows = np.array([0, 2, 3], dtype=np.intp)
        out = np.ones((6, X.shape[1]), dtype=dtype)
        out_rows = np.array([1, 3, 4], dtype=np.intp)

        expect = np.ones_like(out)
        expect[out_rows] = X[X_rows, :].toarray()

        assign_rows_csr(X, X_rows, out_rows, out)
        assert_array_equal(out, expect)

def test_inplace_column_scale():
    X = sp.random(100, 200, density=0.05)
    Xr = X.tocsr()
    Xc = X.tocsc()
    scale = rng.rand(200)
    XA *= scale

    inplace_column_scale(Xc, scale)
    inplace_column_scale(Xr, scale)
    assert_array_almost_equal(Xr.toarray(), Xc.toarray())
    assert_array_almost_equal(XA, Xc.toarray())
    assert_array_almost_equal(XA, Xr.toarray())
        inplace_column_scale(X.tolil(), scale)

    scale = scale.astype(np.float32)
    XA *= scale

def test_inplace_row_scale():
    scale = rng.rand(100)
    XA *= scale.reshape(-1, 1)

    inplace_row_scale(Xc, scale)
    inplace_row_scale(Xr, scale)

def test_inplace_swap_row(csc_container, csr_container):
        [[0, 3, 0], [2, 4, 0], [0, 0, 0], [9, 8, 7], [4, 0, 5]], dtype=np.float64
    )

    swap = linalg.get_blas_funcs(("swap",), (X,))
    swap = swap[0]
    X[0], X[-1] = swap(X[0], X[-1])
    inplace_swap_row(X_csr, 0, -1)
    inplace_swap_row(X_csc, 0, -1)
    assert_array_equal(X_csr.toarray(), X_csc.toarray())
    assert_array_equal(X, X_csc.toarray())
    assert_array_equal(X, X_csr.toarray())

    X[2], X[3] = swap(X[2], X[3])
    inplace_swap_row(X_csr, 2, 3)
    inplace_swap_row(X_csc, 2, 3)
        inplace_swap_row(X_csr.tolil())

        [[0, 3, 0], [2, 4, 0], [0, 0, 0], [9, 8, 7], [4, 0, 5]], dtype=np.float32
    )

def test_inplace_swap_column(csc_container, csr_container):
    )

    X[:, 0], X[:, -1] = swap(X[:, 0], X[:, -1])
    inplace_swap_column(X_csr, 0, -1)
    inplace_swap_column(X_csc, 0, -1)

    X[:, 0], X[:, 1] = swap(X[:, 0], X[:, 1])
    inplace_swap_column(X_csr, 0, 1)
    inplace_swap_column(X_csc, 0, 1)
        inplace_swap_column(X_csr.tolil())

    )

def test_min_max(
    dtype,
    axis,
    sparse_format,
    missing_values,
    min_func,
    max_func,
    ignore_nan,
    large_indices,
):
        [
            [0, 3, 0],
            [2, -1, missing_values],
            [0, 0, 0],
            [9, missing_values, 7],
            [4, 0, 5],
        ],
    )
    X_sparse = sparse_format(X)

    if large_indices:
        X_sparse.indices = X_sparse.indices.astype("int64")
        X_sparse.indptr = X_sparse.indptr.astype("int64")

    mins_sparse, maxs_sparse = min_max_axis(X_sparse, axis=axis, ignore_nan=ignore_nan)
    assert_array_equal(mins_sparse, min_func(X, axis=axis))
    assert_array_equal(maxs_sparse, max_func(X, axis=axis))

def test_min_max_axis_errors(csc_container, csr_container):
        [[0, 3, 0], [2, -1, 0], [0, 0, 0], [9, 8, 7], [4, 0, 5]], dtype=np.float64
    )
        min_max_axis(X_csr.tolil(), axis=0)
        min_max_axis(X_csr, axis=2)
        min_max_axis(X_csc, axis=-3)

def test_count_nonzero(csc_container, csr_container):
    )
    X_nonzero = X != 0
    sample_weight = [0.5, 0.2, 0.3, 0.1, 0.1]
    X_nonzero_weighted = X_nonzero * np.array(sample_weight)[:, None]

    for axis in [0, 1, -1, -2, None]:
            count_nonzero(X_csr, axis=axis), X_nonzero.sum(axis=axis)
        )
            count_nonzero(X_csr, axis=axis, sample_weight=sample_weight),
            X_nonzero_weighted.sum(axis=axis),
        )

        count_nonzero(X_csc)
        count_nonzero(X_csr, axis=2)

    assert count_nonzero(X_csr, axis=0).dtype == count_nonzero(X_csr, axis=1).dtype
    assert (
        count_nonzero(X_csr, axis=0, sample_weight=sample_weight).dtype
        == count_nonzero(X_csr, axis=1, sample_weight=sample_weight).dtype
    )

    # Check dtypes with large sparse matrices too
    # XXX: test fails on 32bit (Windows/Linux)
    try:
        X_csr.indices = X_csr.indices.astype(np.int64)
        X_csr.indptr = X_csr.indptr.astype(np.int64)
        assert count_nonzero(X_csr, axis=0).dtype == count_nonzero(X_csr, axis=1).dtype
        assert (
            count_nonzero(X_csr, axis=0, sample_weight=sample_weight).dtype
            == count_nonzero(X_csr, axis=1, sample_weight=sample_weight).dtype
        )
        assert "according to the rule 'safe'" in e.args[0] and np.intp().nbytes < 8, e

def test_csc_row_median(csc_container, csr_container):
    # Test csc_row_median actually calculates the median.

    # Test that it gives the same output when X is dense.
    X = rng.rand(100, 50)
    dense_median = np.median(X, axis=0)
    csc = csc_container(X)
    sparse_median = csc_median_axis_0(csc)
    assert_array_equal(sparse_median, dense_median)

    # Test that it gives the same output when X is sparse
    X = rng.rand(51, 100)
    X[X < 0.7] = 0.0
    ind = rng.randint(0, 50, 10)
    X[ind] = -X[ind]

    # Test for toy data.
    X = [[0, -2], [-1, -1], [1, 0], [2, 1]]
    assert_array_equal(csc_median_axis_0(csc), np.array([0.5, -0.5]))
    X = [[0, -2], [-1, -5], [1, -3]]
    assert_array_equal(csc_median_axis_0(csc), np.array([0.0, -3]))

    # Test that it raises an Error for non-csc matrices.
        csc_median_axis_0(csr_container(X))

def test_inplace_normalize(csr_container, inplace_csr_row_normalize):
    if csr_container is sp.csr_matrix:
        ones = np.ones((10, 1))
    else:
        ones = np.ones(10)
    rs = RandomState(10)

    for dtype in (np.float64, np.float32):
        X = rs.randn(10, 5).astype(dtype)
        X_csr = csr_container(X)
        for index_dtype in [np.int32, np.int64]:
            # csr_matrix will use int32 indices by default,
            # up-casting those to int64 when necessary
            if index_dtype is np.int64:
                X_csr.indptr = X_csr.indptr.astype(index_dtype)
                X_csr.indices = X_csr.indices.astype(index_dtype)
            assert X_csr.indices.dtype == index_dtype
            assert X_csr.indptr.dtype == index_dtype
            inplace_csr_row_normalize(X_csr)
            assert X_csr.dtype == dtype
            if inplace_csr_row_normalize is inplace_csr_row_normalize_l2:
                X_csr.data **= 2
            assert_array_almost_equal(np.abs(X_csr).sum(axis=1), ones)

def test_csr_row_norms(dtype):
    # checks that csr_row_norms returns the same output as
    # scipy.sparse.linalg.norm, and that the dype is the same as X.dtype.
    X = sp.random(100, 10, format="csr", dtype=dtype, random_state=42)

    scipy_norms = sp.linalg.norm(X, axis=1) ** 2
    norms = csr_row_norms(X)

    assert norms.dtype == dtype
    rtol = 1e-6 if dtype == np.float32 else 1e-7
    assert_allclose(norms, scipy_norms, rtol=rtol)

def centered_matrices(request):
    """Returns equivalent tuple[sp.linalg.LinearOperator, np.ndarray]."""
    sparse_container = request.param

    random_state = np.random.default_rng(42)

    X_sparse = sparse_container(
        sp.random(500, 100, density=0.1, format="csr", random_state=random_state)
    )
    mu = np.asarray(X_sparse.mean(axis=0)).ravel()

    X_sparse_centered = _implicit_column_offset(X_sparse, mu)
    X_dense_centered = X_dense - mu

    return X_sparse_centered, X_dense_centered

def test_implicit_center_matmat(global_random_seed, centered_matrices):
    X_sparse_centered, X_dense_centered = centered_matrices
    Y = rng.standard_normal((X_dense_centered.shape[1], 50))
    assert_allclose(X_dense_centered @ Y, X_sparse_centered.matmat(Y))
    assert_allclose(X_dense_centered @ Y, X_sparse_centered @ Y)

def test_implicit_center_matvec(global_random_seed, centered_matrices):
    y = rng.standard_normal(X_dense_centered.shape[1])
    assert_allclose(X_dense_centered @ y, X_sparse_centered.matvec(y))
    assert_allclose(X_dense_centered @ y, X_sparse_centered @ y)

def test_implicit_center_rmatmat(global_random_seed, centered_matrices):
    Y = rng.standard_normal((X_dense_centered.shape[0], 50))
    assert_allclose(X_dense_centered.T @ Y, X_sparse_centered.rmatmat(Y))
    assert_allclose(X_dense_centered.T @ Y, X_sparse_centered.T @ Y)

def test_implit_center_rmatvec(global_random_seed, centered_matrices):
    y = rng.standard_normal(X_dense_centered.shape[0])
    assert_allclose(X_dense_centered.T @ y, X_sparse_centered.rmatvec(y))
    assert_allclose(X_dense_centered.T @ y, X_sparse_centered.T @ y)

def test_mmhash3_int():
    assert murmurhash3_32(3) == 847579505
    assert murmurhash3_32(3, seed=0) == 847579505
    assert murmurhash3_32(3, seed=42) == -1823081949

    assert murmurhash3_32(3, positive=False) == 847579505
    assert murmurhash3_32(3, seed=0, positive=False) == 847579505
    assert murmurhash3_32(3, seed=42, positive=False) == -1823081949

    assert murmurhash3_32(3, positive=True) == 847579505
    assert murmurhash3_32(3, seed=0, positive=True) == 847579505
    assert murmurhash3_32(3, seed=42, positive=True) == 2471885347

def test_mmhash3_int_array():
    keys = rng.randint(-5342534, 345345, size=3 * 2 * 1).astype(np.int32)
    keys = keys.reshape((3, 2, 1))

    for seed in [0, 42]:
        expected = np.array([murmurhash3_32(int(k), seed) for k in keys.flat])
        expected = expected.reshape(keys.shape)
        assert_array_equal(murmurhash3_32(keys, seed), expected)

        expected = np.array([murmurhash3_32(k, seed, positive=True) for k in keys.flat])
        assert_array_equal(murmurhash3_32(keys, seed, positive=True), expected)

def test_mmhash3_bytes():
    assert murmurhash3_32(b"foo", 0) == -156908512
    assert murmurhash3_32(b"foo", 42) == -1322301282

    assert murmurhash3_32(b"foo", 0, positive=True) == 4138058784
    assert murmurhash3_32(b"foo", 42, positive=True) == 2972666014

def test_mmhash3_unicode():
    assert murmurhash3_32("foo", 0) == -156908512
    assert murmurhash3_32("foo", 42) == -1322301282

    assert murmurhash3_32("foo", 0, positive=True) == 4138058784
    assert murmurhash3_32("foo", 42, positive=True) == 2972666014

def test_no_collision_on_byte_range():
    previous_hashes = set()
        h = murmurhash3_32(" " * i, 0)
        assert h not in previous_hashes, "Found collision on growing empty string"

def test_uniform_distribution():
    n_bins, n_samples = 10, 100000
    bins = np.zeros(n_bins, dtype=np.float64)

        bins[murmurhash3_32(i, positive=True) % n_bins] += 1

    means = bins / n_samples
    expected = np.full(n_bins, 1.0 / n_bins)

    assert_array_almost_equal(means / expected, np.ones(n_bins), 2)

def test_message_with_time(source, message, is_long, time, time_str):
    out = _message_with_time(source, message, time)
    if is_long:
        assert len(out) > 70
    else:
        assert len(out) == 70

    assert out.startswith("[" + source + "] ")
    out = out[len(source) + 3 :]

    assert out.endswith(time_str)
    out = out[: -len(time_str)]
    assert out.endswith(", total=")
    out = out[: -len(", total=")]
    assert out.endswith(message)
    out = out[: -len(message)]
    assert out.endswith(" ")
    out = out[:-1]

        assert not out
    else:
        assert list(set(out)) == ["."]

def test_print_elapsed_time(message, expected, capsys, monkeypatch):
    monkeypatch.setattr(timeit, "default_timer", lambda: 0)
    with _print_elapsed_time("ABC", message):
        monkeypatch.setattr(timeit, "default_timer", lambda: 0.1)
    assert capsys.readouterr().out == expected

def test_get_response_values_regressor_error(response_method):
    """Check the error message with regressor an not supported response
    method."""
    my_estimator = _MockEstimatorOnOffPrediction(response_methods=[response_method])
    X = "mocking_data", "mocking_target"
    err_msg = f"{my_estimator.__class__.__name__} should either be a classifier"
        _get_response_values(my_estimator, X, response_method=response_method)

def test_get_response_values_regressor(return_response_method_used):
    """Check the behaviour of `_get_response_values` with regressor."""
    X, y = make_regression(n_samples=10, random_state=0)
    regressor = LinearRegression().fit(X, y)
    results = _get_response_values(
        regressor,
        X,
    )
    assert_array_equal(results[0], regressor.predict(X))
    assert results[1] is None
        assert results[2] == "predict"

def test_get_response_values_outlier_detection(
    response_method, return_response_method_used
):
    """Check the behaviour of `_get_response_values` with outlier detector."""
    X, y = make_classification(n_samples=50, random_state=0)
        X,
    )
    chosen_response_method = (
        response_method[0] if isinstance(response_method, list) else response_method
    )
    prediction_method = getattr(outlier_detector, chosen_response_method)
    assert_array_equal(results[0], prediction_method(X))
        assert results[2] == chosen_response_method

def test_get_response_values_classifier_unknown_pos_label(response_method):
    """Check that `_get_response_values` raises the proper error message with
    classifier."""
    X, y = make_classification(n_samples=10, n_classes=2, random_state=0)

    # provide a `pos_label` which is not in `y`
    err_msg = r"pos_label=whatever is not a valid label: It should be one of \[0 1\]"
        _get_response_values(
            X,
            pos_label="whatever",
        )

def test_get_response_values_classifier_inconsistent_y_pred_for_binary_proba(
):
    """Check that `_get_response_values` will raise an error when `y_pred` has a
    single class with `predict_proba`."""
    X, y_two_class = make_classification(n_samples=10, n_classes=2, random_state=0)
    y_single_class = np.zeros_like(y_two_class)
    classifier = DecisionTreeClassifier().fit(X, y_single_class)

    err_msg = (
        r"Got predict_proba of shape \(10, 1\), but need classifier with "
        r"two classes"
    )
        _get_response_values(classifier, X, response_method=response_method)

def test_get_response_values_binary_classifier_decision_function(
    return_response_method_used,
):
    """Check the behaviour of `_get_response_values` with `decision_function`
    and binary classifier."""
        n_samples=10,
        weights=[0.3, 0.7],
    )
    response_method = "decision_function"

    # default `pos_label`
        classifier,
        X,
    )
    assert_allclose(results[0], classifier.decision_function(X))
    assert results[1] == 1
        assert results[2] == "decision_function"

    # when forcing `pos_label=classifier.classes_[0]`
        X,
        pos_label=classifier.classes_[0],
    )
    assert_allclose(results[0], classifier.decision_function(X) * -1)
    assert results[1] == 0

def test_get_response_values_binary_classifier_predict_proba(
    return_response_method_used, response_method
):
    """Check that `_get_response_values` with `predict_proba` and binary
    )

        X,
    )
    assert_allclose(results[0], getattr(classifier, response_method)(X)[:, 1])
        assert len(results) == 3
        assert results[2] == response_method
    else:
        assert len(results) == 2

    y_pred, pos_label, *_ = _get_response_values(
        X,
    )
    assert_allclose(y_pred, getattr(classifier, response_method)(X)[:, 0])
    assert pos_label == 0

def test_get_response_error(estimator, X, y, err_msg, params):
    """Check that we raise the proper error messages in _get_response_values_binary."""

        _get_response_values_binary(estimator, X, **params)

def test_get_response_predict_proba(return_response_method_used):
    """Check the behaviour of `_get_response_values_binary` using `predict_proba`."""
    classifier = DecisionTreeClassifier().fit(X_binary, y_binary)
    results = _get_response_values_binary(
        X_binary,
    )
    assert_allclose(results[0], classifier.predict_proba(X_binary)[:, 1])
        assert results[2] == "predict_proba"

        X_binary,
        pos_label=0,
    )
    assert_allclose(results[0], classifier.predict_proba(X_binary)[:, 0])

def test_get_response_decision_function(return_response_method_used):
    """Check the behaviour of `_get_response_values_binary` using decision_function."""
    classifier = LogisticRegression().fit(X_binary, y_binary)
        X_binary,
    )
    assert_allclose(results[0], classifier.decision_function(X_binary))

        X_binary,
    )
    assert_allclose(results[0], classifier.decision_function(X_binary) * -1)

def test_get_response_values_multiclass(estimator, response_method):
    """Check that we can call `_get_response_values` with a multiclass estimator.
    It should return the predictions untouched.
    """
    predictions, pos_label = _get_response_values(
        estimator, X, response_method=response_method
    )

    assert pos_label is None
    assert predictions.shape == (X.shape[0], len(estimator.classes_))
        assert np.logical_and(predictions >= 0, predictions <= 1).all()
    elif response_method == "predict_log_proba":
        assert (predictions <= 0.0).all()

def test_get_response_values_with_response_list():
    """Check the behaviour of passing a list of responses to `_get_response_values`."""

    # it should use `predict_proba`
    y_pred, pos_label, response_method = _get_response_values(
        X_binary,
        response_method=["predict_proba", "decision_function"],
        return_response_method_used=True,
    )
    assert_allclose(y_pred, classifier.predict_proba(X_binary)[:, 1])
    assert pos_label == 1
    assert response_method == "predict_proba"

    # it should use `decision_function`
        X_binary,
    )
    assert_allclose(y_pred, classifier.decision_function(X_binary))
    assert response_method == "decision_function"

def test_get_response_values_multilabel_indicator(response_method):
    X, Y = make_multilabel_classification(random_state=0)
    estimator = ClassifierChain(LogisticRegression()).fit(X, Y)

    y_pred, pos_label = _get_response_values(
    )
    assert y_pred.shape == Y.shape

        assert np.logical_and(y_pred >= 0, y_pred <= 1).all()
    elif response_method == "decision_function":
        # values returned by `decision_function` are not bounded in [0, 1]
        assert (y_pred < 0).sum() > 0
        assert (y_pred > 1).sum() > 0
    else:  # response_method == "predict"
        assert np.logical_or(y_pred == 0, y_pred == 1).all()

def test_is_scalar_nan(value, result):
    assert is_scalar_nan(value) is result
    # make sure that we are returning a Python bool
    assert isinstance(is_scalar_nan(value), bool)

def _func(a, b=0, *args, c, d=0, **kwargs):
    """A function to test the validation of functions."""

class _Class:
    """A class to test the _InstancesOf constraint and the validation of methods."""

    @validate_params({"a": [Real]}, prefer_skip_nested_validation=True)
    def _method(self, a):
        """A validated method"""

    @deprecated()
    def _deprecated_method(self, a):
        """A deprecated validated method"""

class _Estimator(BaseEstimator):
    """An estimator to test the validation of estimator parameters."""

    _parameter_constraints: dict = {"a": [Real]}

    def __init__(self, a):
        self.a = a

        pass

def test_interval_range(interval_type):
    """Check the range of values depending on closed."""
    interval = Interval(interval_type, -2, 2, closed="left")
    assert -2 in interval
    assert 2 not in interval

    interval = Interval(interval_type, -2, 2, closed="right")
    assert -2 not in interval
    assert 2 in interval

    interval = Interval(interval_type, -2, 2, closed="both")

    interval = Interval(interval_type, -2, 2, closed="neither")

def test_interval_large_integers(interval_type):
    """Check that Interval constraint work with large integers.

    non-regression test for #26648.
    """
    interval = Interval(interval_type, 0, 2, closed="neither")
    assert 2**65 not in interval
    assert 2**128 not in interval
    assert float(2**65) not in interval
    assert float(2**128) not in interval

    interval = Interval(interval_type, 0, 2**128, closed="neither")
    assert 2**65 in interval
    assert float(2**65) in interval

    assert 2**1024 not in interval

def test_interval_inf_in_bounds():
    """Check that inf is included iff a bound is closed and set to None.

    Only valid for real intervals.
    """
    interval = Interval(Real, 0, None, closed="right")
    assert np.inf in interval

    interval = Interval(Real, None, 0, closed="left")
    assert -np.inf in interval

    interval = Interval(Real, None, None, closed="neither")
    assert np.inf not in interval
    assert -np.inf not in interval

def test_nan_not_in_interval(interval):
    """Check that np.nan is not in any interval."""
    assert np.nan not in interval

def test_interval_errors(params, error, match):
    """Check that informative errors are raised for invalid combination of parameters"""
    with pytest.raises(error, match=match):
        Interval(**params)

def test_stroptions():
    """Sanity check for the StrOptions constraint"""
    options = StrOptions({"a", "b", "c"}, deprecated={"c"})
    assert options.is_satisfied_by("a")
    assert options.is_satisfied_by("c")
    assert not options.is_satisfied_by("d")

    assert "'c' (deprecated)" in str(options)

def test_options():
    """Sanity check for the Options constraint"""
    options = Options(Real, {-0.5, 0.5, np.inf}, deprecated={-0.5})
    assert options.is_satisfied_by(-0.5)
    assert options.is_satisfied_by(np.inf)
    assert not options.is_satisfied_by(1.23)

    assert "-0.5 (deprecated)" in str(options)

def test_instances_of_type_human_readable(type, expected_type_name):
    """Check the string representation of the _InstancesOf constraint."""
    constraint = _InstancesOf(type)
    assert str(constraint) == f"an instance of '{expected_type_name}'"

def test_hasmethods():
    """Check the HasMethods constraint."""
    constraint = HasMethods(["a", "b"])

    class _Good:
        def a(self):

        def b(self):

    class _Bad:

    assert constraint.is_satisfied_by(_Good())
    assert not constraint.is_satisfied_by(_Bad())
    assert str(constraint) == "an object implementing 'a' and 'b'"

def test_generate_invalid_param_val(constraint):
    """Check that the value generated does not satisfy the constraint"""
    bad_value = generate_invalid_param_val(constraint)
    assert not constraint.is_satisfied_by(bad_value)

def test_generate_invalid_param_val_2_intervals(integer_interval, real_interval):
    """Check that the value generated for an interval constraint does not satisfy any of
    the interval constraints.
    """
    bad_value = generate_invalid_param_val(constraint=real_interval)
    assert not real_interval.is_satisfied_by(bad_value)
    assert not integer_interval.is_satisfied_by(bad_value)

    bad_value = generate_invalid_param_val(constraint=integer_interval)

def test_generate_invalid_param_val_all_valid(constraint):
    """Check that the function raises NotImplementedError when there's no invalid value
    for the constraint.
    """
        generate_invalid_param_val(constraint)

def test_generate_valid_param(constraint):
    """Check that the value generated does satisfy the constraint."""
    value = generate_valid_param(constraint)
    assert constraint.is_satisfied_by(value)

def test_is_satisfied_by(constraint_declaration, value):
    """Sanity check for the is_satisfied_by method"""
    constraint = make_constraint(constraint_declaration)

def test_make_constraint(constraint_declaration, expected_constraint_class):
    """Check that make_constraint dispatches to the appropriate constraint class"""
    assert constraint.__class__ is expected_constraint_class

def test_make_constraint_unknown():
    """Check that an informative error is raised when an unknown constraint is passed"""
    with pytest.raises(ValueError, match="Unknown constraint"):
        make_constraint("not a valid constraint")

def test_validate_params():
    """Check that validate_params works no matter how the arguments are passed"""
        InvalidParameterError, match="The 'a' parameter of _func must be"
    ):
        _func("wrong", c=1)

        InvalidParameterError, match="The 'b' parameter of _func must be"
    ):
        _func(*[1, "wrong"], c=1)

        InvalidParameterError, match="The 'c' parameter of _func must be"
    ):
        _func(1, **{"c": "wrong"})

        InvalidParameterError, match="The 'd' parameter of _func must be"
    ):
        _func(1, c=1, d="wrong")

    # check in the presence of extra positional and keyword args
    ):
        _func(0, *["wrong", 2, 3], c=4, **{"e": 5})

    ):
        _func(0, *[1, 2, 3], c="four", **{"e": 5})

def test_validate_params_missing_params():
    """Check that no error is raised when there are parameters without
    constraints
    """

    @validate_params({"a": [int]}, prefer_skip_nested_validation=True)
    def func(a, b):
        pass

    func(1, 2)

def test_decorate_validated_function():
    """Check that validate_params functions can be decorated"""
    decorated_function = deprecated()(_func)

    with pytest.warns(FutureWarning, match="Function _func is deprecated"):
        decorated_function(1, 2, c=3)

    # outer decorator does not interfere with validation
            InvalidParameterError, match=r"The 'c' parameter of _func must be"
        ):
            decorated_function(1, 2, c="wrong")

def test_validate_params_method():
    """Check that validate_params works with methods"""
        InvalidParameterError, match="The 'a' parameter of _Class._method must be"
    ):
        _Class()._method("wrong")

    # validated method can be decorated
    with pytest.warns(FutureWarning, match="Function _deprecated_method is deprecated"):
            InvalidParameterError,
            match="The 'a' parameter of _Class._deprecated_method must be",
        ):
            _Class()._deprecated_method("wrong")

def test_validate_params_estimator():
    """Check that validate_params works with Estimator instances"""
    # no validation in init
    est = _Estimator("wrong")

        InvalidParameterError, match="The 'a' parameter of _Estimator must be"
    ):
        est.fit()

def test_stroptions_deprecated_subset():
    """Check that the deprecated parameter must be a subset of options."""
    with pytest.raises(ValueError, match="deprecated options must be a subset"):
        StrOptions({"a", "b", "c"}, deprecated={"a", "d"})

def test_hidden_constraint():
    """Check that internal constraints are not exposed in the error message."""

        {"param": [Hidden(list), dict]}, prefer_skip_nested_validation=True
    )
    def f(param):
        pass

    # list and dict are valid params
    f({"a": 1, "b": 2, "c": 3})
    f([1, 2, 3])

        InvalidParameterError, match="The 'param' parameter"
    ) as exc_info:
        f(param="bad")

    # the list option is not exposed in the error message
    err_msg = str(exc_info.value)
    assert "an instance of 'dict'" in err_msg
    assert "an instance of 'list'" not in err_msg

def test_hidden_stroptions():
    """Check that we can have 2 StrOptions constraints, one being hidden."""

        {"param": [StrOptions({"auto"}), Hidden(StrOptions({"warn"}))]},
    )
        pass

    # "auto" and "warn" are valid params
    f("auto")
    f("warn")

    # the "warn" option is not exposed in the error message
    assert "auto" in err_msg
    assert "warn" not in err_msg

def test_validate_params_set_param_constraints_attribute():
    """Check that the validate_params decorator properly sets the parameter constraints
    as attribute of the decorated function/method.
    """
    assert hasattr(_func, "_skl_parameter_constraints")
    assert hasattr(_Class()._method, "_skl_parameter_constraints")

def test_boolean_constraint_deprecated_int():
    """Check that validate_params raise a deprecation message but still passes
    validation when using an int for a parameter accepting a boolean.
    """

    @validate_params({"param": ["boolean"]}, prefer_skip_nested_validation=True)
        pass

    # True/False and np.bool_(True/False) are valid params
    f(True)
    f(np.bool_(False))

def test_no_validation():
    """Check that validation can be skipped for a parameter."""

        {"param1": [int, None], "param2": "no_validation"},
    )
    def f(param1=None, param2=None):
        pass

    # param1 is validated
    with pytest.raises(InvalidParameterError, match="The 'param1' parameter"):
        f(param1="wrong")

    # param2 is not validated: any type is valid.
    class SomeType:
        pass

    f(param2=SomeType)
    f(param2=SomeType())

def test_pandas_na_constraint_with_pd_na():
    """Add a specific test for checking support for `pandas.NA`."""

    na_constraint = _PandasNAConstraint()
    assert na_constraint.is_satisfied_by(pd.NA)
    assert not na_constraint.is_satisfied_by(np.array([1, 2, 3]))

def test_iterable_not_string():
    """Check that a string does not satisfy the _IterableNotString constraint."""
    constraint = _IterablesNotString()
    assert constraint.is_satisfied_by([1, 2, 3])
    assert constraint.is_satisfied_by(range(10))
    assert not constraint.is_satisfied_by("some string")

def test_cv_objects():
    """Check that the _CVObjects constraint accepts all current ways
    to pass cv objects."""
    constraint = _CVObjects()
    assert constraint.is_satisfied_by(5)
    assert constraint.is_satisfied_by(LeaveOneOut())
    assert constraint.is_satisfied_by([([1, 2], [3, 4]), ([3, 4], [1, 2])])
    assert constraint.is_satisfied_by(None)
    assert not constraint.is_satisfied_by("not a CV object")

def test_third_party_estimator():
    """Check that the validation from a scikit-learn estimator inherited by a third
    party estimator does not impose a match between the dict of constraints and the
    parameters of the estimator.
    """

    class ThirdPartyEstimator(_Estimator):
        def __init__(self, b):
            self.b = b
            super().__init__(a=0)

        def fit(self, X=None, y=None):

    # does not raise, even though "b" is not in the constraints dict and "a" is not
    # a parameter of the estimator.
    ThirdPartyEstimator(b=0).fit()

def test_interval_real_not_int():
    """Check for the type RealNotInt in the Interval constraint."""
    constraint = Interval(RealNotInt, 0, 1, closed="both")
    assert constraint.is_satisfied_by(1.0)
    assert not constraint.is_satisfied_by(1)

def test_real_not_int():
    """Check for the RealNotInt type."""
    assert isinstance(1.0, RealNotInt)
    assert not isinstance(1, RealNotInt)
    assert isinstance(np.float64(1), RealNotInt)
    assert not isinstance(np.int64(1), RealNotInt)

def test_skip_param_validation():
    """Check that param validation can be skipped using config_context."""

    def f(a):
        pass

    with pytest.raises(InvalidParameterError, match="The 'a' parameter"):
        f(a="1")

    with config_context(skip_parameter_validation=True):
        f(a="1")

def test_skip_nested_validation(prefer_skip_nested_validation):
    """Check that nested validation can be skipped."""

    def f(a):
        pass

        {"b": [int]},
        prefer_skip_nested_validation=prefer_skip_nested_validation,
    )
    def g(b):
        # calls f with a bad parameter type
        return f(a="invalid_param_value")

    # Validation for g is never skipped.
    with pytest.raises(InvalidParameterError, match="The 'b' parameter"):
        g(b="invalid_param_value")

    if prefer_skip_nested_validation:
        g(b=1)  # does not raise because inner f is not validated
    else:
        with pytest.raises(InvalidParameterError, match="The 'a' parameter"):
            g(b=1)

def test_skip_nested_validation_and_config_context(
    skip_parameter_validation, prefer_skip_nested_validation, expected_skipped
):
    """Check interaction between global skip and local skip."""

        {"a": [int]}, prefer_skip_nested_validation=prefer_skip_nested_validation
    )
    def g(a):
        return get_config()["skip_parameter_validation"]

    with config_context(skip_parameter_validation=skip_parameter_validation):
        actual_skipped = g(1)

    assert actual_skipped == expected_skipped

            self.b = b

    def g(b):

    def g(a):

            self.b = b

def test_polars_indexing():
    """Check _safe_indexing for polars as expected."""
    pl = pytest.importorskip("polars", minversion="0.18.2")
    df = pl.DataFrame(
        {"a": [1, 2, 3, 4], "b": [4, 5, 6, 8], "c": [1, 4, 1, 10]}, orient="row"
    )

    from polars.testing import assert_frame_equal

    str_keys = [["b"], ["a", "b"], ["b", "a", "c"], ["c"], ["a"]]

    for key in str_keys:
        out = _safe_indexing(df, key, axis=1)
        assert_frame_equal(df[key], out)

    bool_keys = [([True, False, True], ["a", "c"]), ([False, False, True], ["c"])]

    for bool_key, str_key in bool_keys:
        out = _safe_indexing(df, bool_key, axis=1)
        assert_frame_equal(df[:, str_key], out)

    int_keys = [([0, 1], ["a", "b"]), ([2], ["c"])]

    for int_key, str_key in int_keys:
        out = _safe_indexing(df, int_key, axis=1)

    axis_0_keys = [[0, 1], [1, 3], [3, 2]]
    for key in axis_0_keys:
        out = _safe_indexing(df, key, axis=0)

def test_determine_key_type(key, dtype):
    assert _determine_key_type(key) == dtype

def test_determine_key_type_error():
    with pytest.raises(ValueError, match="No valid specification of the"):
        _determine_key_type(1.0)

def test_determine_key_type_slice_error():
    with pytest.raises(TypeError, match="Only array-like or scalar are"):
        _determine_key_type(slice(0, 2, 1), accept_slice=False)

def test_determine_key_type_array_api(array_namespace, device, dtype_name):

    with sklearn.config_context(array_api_dispatch=True):
        int_array_key = xp.asarray([1, 2, 3])
        assert _determine_key_type(int_array_key) == "int"

        bool_array_key = xp.asarray([True, False, True])
        assert _determine_key_type(bool_array_key) == "bool"

        try:
            complex_array_key = xp.asarray([1 + 1j, 2 + 2j, 3 + 3j])
            # Complex numbers are not supported by all Array API libraries.
            complex_array_key = None

        if complex_array_key is not None:
            with pytest.raises(ValueError, match="No valid specification of the"):
                _determine_key_type(complex_array_key)

def test_safe_indexing_2d_container_axis_0(array_type, indices_type):
    indices = [1, 2]
    if indices_type == "slice" and isinstance(indices[1], int):
        indices[1] += 1
    array = _convert_container([[1, 2, 3], [4, 5, 6], [7, 8, 9]], array_type)
    indices = _convert_container(indices, indices_type)
    subset = _safe_indexing(array, indices, axis=0)
        subset, _convert_container([[4, 5, 6], [7, 8, 9]], array_type)
    )

def test_safe_indexing_1d_container(array_type, indices_type):
    array = _convert_container([1, 2, 3, 4, 5, 6, 7, 8, 9], array_type)
    assert_allclose_dense_sparse(subset, _convert_container([2, 3], array_type))

def test_safe_indexing_2d_container_axis_1(array_type, indices_type, indices):
    # validation of the indices
    # we make a copy because indices is mutable and shared between tests
    indices_converted = copy(indices)
        indices_converted[1] += 1

    columns_name = ["col_0", "col_1", "col_2"]
    array = _convert_container(
        [[1, 2, 3], [4, 5, 6], [7, 8, 9]], array_type, columns_name
    )
    indices_converted = _convert_container(indices_converted, indices_type)

    if isinstance(indices[0], str) and array_type not in ("dataframe", "polars"):
        err_msg = (
            "Specifying the columns using strings is only supported for dataframes"
        )
            _safe_indexing(array, indices_converted, axis=1)
    else:
        subset = _safe_indexing(array, indices_converted, axis=1)
            subset, _convert_container([[2, 3], [5, 6], [8, 9]], array_type)
        )

def test_safe_indexing_2d_read_only_axis_1(
    array_read_only, indices_read_only, array_type, indices_type, axis, expected_array
):
    array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    if array_read_only:
        array.setflags(write=False)
    array = _convert_container(array, array_type)
    indices = np.array([1, 2])
    if indices_read_only:
        indices.setflags(write=False)
    subset = _safe_indexing(array, indices, axis=axis)
    assert_allclose_dense_sparse(subset, _convert_container(expected_array, array_type))

def test_safe_indexing_1d_container_mask(array_type, indices_type):
    indices = [False] + [True] * 2 + [False] * 6

def test_safe_indexing_2d_mask(array_type, indices_type, axis, expected_subset):
    )
    indices = [False, True, True]

        subset, _convert_container(expected_subset, array_type)
    )

def test_safe_indexing_2d_scalar_axis_0(array_type, expected_output_type):
    indices = 2
    expected_array = _convert_container([7, 8, 9], expected_output_type)
    assert_allclose_dense_sparse(subset, expected_array)

def test_safe_indexing_1d_scalar(array_type):
    indices = 2
    assert subset == 3

def test_safe_indexing_2d_scalar_axis_1(array_type, expected_output_type, indices):
    )

    if isinstance(indices, str) and array_type not in ("dataframe", "polars"):
        err_msg = (
        )
            _safe_indexing(array, indices, axis=1)
    else:
        subset = _safe_indexing(array, indices, axis=1)
        expected_output = [3, 6, 9]
        if expected_output_type == "sparse":
            # sparse matrix are keeping the 2D shape
            expected_output = [[3], [6], [9]]
        expected_array = _convert_container(expected_output, expected_output_type)
        assert_allclose_dense_sparse(subset, expected_array)

def test_safe_indexing_None_axis_0(array_type):
    X = _convert_container([[1, 2, 3], [4, 5, 6], [7, 8, 9]], array_type)
    X_subset = _safe_indexing(X, None, axis=0)
    assert_allclose_dense_sparse(X_subset, X)

def test_safe_indexing_pandas_no_matching_cols_error():
    err_msg = "No valid specification of the columns."
    X = pd.DataFrame(X_toy)
        _safe_indexing(X, [1.0], axis=1)

def test_safe_indexing_error_axis(axis):
    with pytest.raises(ValueError, match="'axis' should be either 0"):
        _safe_indexing(X_toy, [0, 1], axis=axis)

def test_safe_indexing_1d_array_error(X_constructor):
    # check that we are raising an error if the array-like passed is 1D and
    # we try to index on the 2nd dimension
    X = list(range(5))
    if X_constructor == "array":
        X_constructor = np.asarray(X)
    elif X_constructor == "series":
        X_constructor = pd.Series(X)
    elif X_constructor == "polars_series":
        pl = pytest.importorskip("polars")
        X_constructor = pl.Series(values=X)

    err_msg = "'X' should be a 2D NumPy array, 2D sparse matrix or dataframe"
        _safe_indexing(X_constructor, [0, 1], axis=1)

def test_safe_indexing_container_axis_0_unsupported_type():
    indices = ["col_1", "col_2"]
    array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    err_msg = "String indexing is not supported with 'axis=0'"
        _safe_indexing(array, indices, axis=0)

def test_safe_indexing_pandas_no_settingwithcopy_warning():
    # Using safe_indexing with an array-like indexer gives a copy of the
    # DataFrame -> ensure it doesn't raise a warning if modified

    pd_version = parse_version(pd.__version__)
    pd_base_version = parse_version(pd_version.base_version)

    if pd_base_version >= parse_version("3"):
        raise SkipTest("SettingWithCopyWarning has been removed in pandas 3.0.0.dev")

    X = pd.DataFrame({"a": [1, 2, 3], "b": [3, 4, 5]})
    subset = _safe_indexing(X, [0, 1], axis=0)
    if hasattr(pd.errors, "SettingWithCopyWarning"):
        SettingWithCopyWarning = pd.errors.SettingWithCopyWarning
    else:
        # backward compatibility for pandas < 1.5
        SettingWithCopyWarning = pd.core.common.SettingWithCopyWarning
        warnings.simplefilter("error", SettingWithCopyWarning)
        subset.iloc[0, 0] = 10
    # The original dataframe is unaffected by the assignment on the subset:
    assert X.iloc[0, 0] == 1

def test_safe_indexing_list_axis_1_unsupported(indices):
    """Check that we raise a ValueError when axis=1 with input as list."""
    X = [[1, 2], [4, 5], [7, 8]]
    err_msg = "axis=1 is not supported for lists"
        _safe_indexing(X, indices, axis=1)

def test_safe_assign(array_type):
    """Check that `_safe_assign` works as expected."""
    X_array = rng.randn(10, 5)

    row_indexer = [1, 2]
    values = rng.randn(len(row_indexer), X_array.shape[1])
    X = _convert_container(X_array, array_type)
    _safe_assign(X, values, row_indexer=row_indexer)

    assigned_portion = _safe_indexing(X, row_indexer, axis=0)
        assigned_portion, _convert_container(values, array_type)
    )

    column_indexer = [1, 2]
    values = rng.randn(X_array.shape[0], len(column_indexer))
    _safe_assign(X, values, column_indexer=column_indexer)

    assigned_portion = _safe_indexing(X, column_indexer, axis=1)
    )

    row_indexer, column_indexer = None, None
    values = rng.randn(*X.shape)

    assert_allclose_dense_sparse(X, _convert_container(values, array_type))

def test_get_column_indices_error(key, err_msg):
    X_df = pd.DataFrame(X_toy, columns=["col_0", "col_1", "col_2"])

        _get_column_indices(X_df, key)

def test_get_column_indices_pandas_nonunique_columns_error(key):
    toy = np.zeros((1, 5), dtype=int)
    columns = ["col1", "col1", "col2", "col3", "col2"]
    X = pd.DataFrame(toy, columns=columns)

    err_msg = "Selected columns, {}, are not unique in dataframe".format(key)
    with pytest.raises(ValueError) as exc_info:
        _get_column_indices(X, key)
    assert str(exc_info.value) == err_msg

def test_get_column_indices_interchange():
    """Check _get_column_indices for edge cases with the interchange"""

    # Polars dataframes go down the interchange path.
    df = pl.DataFrame([[1, 2, 3], [4, 5, 6]], schema=["a", "b", "c"])

    key_results = [
        (slice(1, None), [1, 2]),
        (slice(None, 2), [0, 1]),
        (slice(1, 2), [1]),
        (["b", "c"], [1, 2]),
        (slice("a", "b"), [0, 1]),
        (slice("a", None), [0, 1, 2]),
        (slice(None, "a"), [0]),
        (["c", "a"], [2, 0]),
        ([], []),
    ]
    for key, result in key_results:
        assert _get_column_indices(df, key) == result

    msg = "A given column is not a column of the dataframe"
        _get_column_indices(df, ["not_a_column"])

    msg = "key.step must be 1 or None"
        _get_column_indices(df, slice("a", None, 2))

def test_resample():
    # Border case not worth mentioning in doctests
    assert resample() is None

    # Check that invalid arguments yield ValueError
        resample([0], [0, 1])
        resample([0, 1], [0, 1], replace=False, n_samples=3)

    # Issue:6581, n_samples can be more when replace is True (default).
    assert len(resample([1, 2], n_samples=5)) == 5

def test_resample_weighted():
    # Check that sampling with replacement with integer weights yields the
    # samples from the same distribution as sampling uniformly with
    # repeated data points.
    data = np.array([-1, 0, 1])
    sample_weight = np.asarray([0, 100, 1])

    mean_repeated = []
    mean_reweighted = []

    for seed in range(100):
        mean_repeated.append(
            resample(
                data.repeat(sample_weight),
                n_samples=data.shape[0],
            ).mean()
        )
        mean_reweighted.append(
            resample(
                data,
            ).mean()
        )

    mean_repeated = np.asarray(mean_repeated)
    mean_reweighted = np.asarray(mean_reweighted)

    test_result = kstest(mean_repeated, mean_reweighted)
    # Should never be negative because -1 has a 0 weight.
    assert np.all(mean_reweighted >= 0)
    # The null-hypothesis (the computed means are identically distributed)
    # cannot be rejected.
    assert test_result.pvalue > 0.05

def test_resample_stratified():
    # Make sure resample can stratify
    p = 0.9
    X = rng.normal(size=(n_samples, 1))
    y = rng.binomial(1, p, size=n_samples)

    _, y_not_stratified = resample(X, y, n_samples=10, random_state=0, stratify=None)
    assert np.all(y_not_stratified == 1)

    _, y_stratified = resample(X, y, n_samples=10, random_state=0, stratify=y)
    assert not np.all(y_stratified == 1)
    assert np.sum(y_stratified) == 9  # all 1s, one 0

def test_resample_stratified_replace():
    # Make sure stratified resampling supports the replace parameter

    X_replace, _ = resample(
        X, y, replace=True, n_samples=50, random_state=rng, stratify=y
    )
    X_no_replace, _ = resample(
        X, y, replace=False, n_samples=50, random_state=rng, stratify=y
    )
    assert np.unique(X_replace).shape[0] < 50
    assert np.unique(X_no_replace).shape[0] == 50

    # make sure n_samples can be greater than X.shape[0] if we sample with
    # replacement
        X, y, replace=True, n_samples=1000, random_state=rng, stratify=y
    )
    assert X_replace.shape[0] == 1000
    assert np.unique(X_replace).shape[0] == 100

def test_resample_stratify_2dy():
    # Make sure y can be 2d when stratifying
    y = rng.randint(0, 2, size=(n_samples, 2))
    X, y = resample(X, y, n_samples=50, random_state=rng, stratify=y)
    assert y.ndim == 2

def test_notimplementederror():
        NotImplementedError,
        match="Resampling with sample_weight is only implemented for replace=True.",
    ):
        resample([0, 1], [0, 1], sample_weight=[1, 1], replace=False)

        match="Resampling with sample_weight is only implemented for stratify=None",
    ):
        resample([0, 1], [0, 1], sample_weight=[1, 1], stratify=[0, 1])

def test_resample_stratify_sparse_error(csr_container):
    # resample must be ndarray
    X = rng.normal(size=(n_samples, 2))
    stratify = csr_container(y.reshape(-1, 1))
        X, y = resample(X, y, n_samples=50, random_state=rng, stratify=stratify)

def test_shuffle_on_ndim_equals_three():
    def to_tuple(A):  # to make the inner arrays hashable
        return tuple(tuple(tuple(C) for C in B) for B in A)

    A = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])  # A.shape = (2,2,2)
    S = set(to_tuple(A))
    shuffle(A)  # shouldn't raise a ValueError for dim = 3
    assert set(to_tuple(A)) == S

def test_shuffle_dont_convert_to_array(csc_container):
    # Check that shuffle does not try to convert to numpy arrays with float
    # dtypes can let any indexable datastructure pass-through.
    a = ["a", "b", "c"]
    b = np.array(["a", "b", "c"], dtype=object)
    c = [1, 2, 3]
    d = MockDataFrame(np.array([["a", 0], ["b", 1], ["c", 2]], dtype=object))
    e = csc_container(np.arange(6).reshape(3, 2))
    a_s, b_s, c_s, d_s, e_s = shuffle(a, b, c, d, e, random_state=0)

    assert a_s == ["c", "b", "a"]
    assert type(a_s) == list

    assert_array_equal(b_s, ["c", "b", "a"])
    assert b_s.dtype == object

    assert c_s == [3, 2, 1]
    assert type(c_s) == list

    assert_array_equal(d_s, np.array([["c", 2], ["b", 1], ["a", 0]], dtype=object))
    assert type(d_s) == MockDataFrame

    assert_array_equal(e_s.toarray(), np.array([[4, 5], [2, 3], [0, 1]]))

def assert_csr_equal_values(current, expected):
    current.eliminate_zeros()
    expected.eliminate_zeros()
    expected = expected.astype(current.dtype)
    assert current.shape[0] == expected.shape[0]
    assert current.shape[1] == expected.shape[1]
    assert_array_equal(current.data, expected.data)
    assert_array_equal(current.indices, expected.indices)
    assert_array_equal(current.indptr, expected.indptr)

def _make_dense_dataset(float_dtype):
    if float_dtype == np.float32:
        return ArrayDataset32(X32, y32, sample_weight32, seed=42)
    return ArrayDataset64(X64, y64, sample_weight64, seed=42)

def _make_sparse_dataset(csr_container, float_dtype):
        X, y, sample_weight, csr_dataset = X32, y32, sample_weight32, CSRDataset32
    else:
        X, y, sample_weight, csr_dataset = X64, y64, sample_weight64, CSRDataset64
    return csr_dataset(X.data, X.indptr, X.indices, y, sample_weight, seed=42)

def _make_dense_datasets():
    return [_make_dense_dataset(float_dtype) for float_dtype in floating]

def _make_sparse_datasets():
    return [
        _make_sparse_dataset(csr_container, float_dtype)
        for csr_container, float_dtype in product(CSR_CONTAINERS, floating)
    ]

def _make_fused_types_datasets():
    all_datasets = _make_dense_datasets() + _make_sparse_datasets()
    # group dataset by array types to get a tuple (float32, float64)
    return (all_datasets[idx : idx + 2] for idx in range(0, len(all_datasets), 2))

def test_seq_dataset_basic_iteration(dataset, csr_container):
    NUMBER_OF_RUNS = 5
    X_csr64 = csr_container(X64)
    for _ in range(NUMBER_OF_RUNS):
        # next sample
        xi_, yi, swi, idx = dataset._next_py()
        xi = csr_container(xi_, shape=(1, X64.shape[1]))

        assert_csr_equal_values(xi, X_csr64[[idx]])
        assert yi == y64[idx]
        assert swi == sample_weight64[idx]

        # random sample
        xi_, yi, swi, idx = dataset._random_py()

def test_seq_dataset_shuffle(dense_dataset, sparse_dataset):
    # not shuffled
        _, _, _, idx1 = dense_dataset._next_py()
        _, _, _, idx2 = sparse_dataset._next_py()
        assert idx1 == i
        assert idx2 == i

    for i in [132, 50, 9, 18, 58]:
        _, _, _, idx1 = dense_dataset._random_py()
        _, _, _, idx2 = sparse_dataset._random_py()

    seed = 77
    dense_dataset._shuffle_py(seed)
    sparse_dataset._shuffle_py(seed)

    idx_next = [63, 91, 148, 87, 29]
    idx_shuffle = [137, 125, 56, 121, 127]
    for i, j in zip(idx_next, idx_shuffle):

        assert idx1 == j
        assert idx2 == j

def test_fused_types_consistency(dataset_32, dataset_64):
        (xi_data32, _, _), yi32, _, _ = dataset_32._next_py()
        (xi_data64, _, _), yi64, _, _ = dataset_64._next_py()

        assert xi_data32.dtype == np.float32
        assert xi_data64.dtype == np.float64

        assert_allclose(xi_data64, xi_data32, rtol=1e-5)
        assert_allclose(yi64, yi32, rtol=1e-5)

def test_buffer_dtype_mismatch_error():
    with pytest.raises(ValueError, match="Buffer dtype mismatch"):
        ArrayDataset64(X32, y32, sample_weight32, seed=42)

        ArrayDataset32(X64, y64, sample_weight64, seed=42)

        X_csr32 = csr_container(X32)
        X_csr64 = csr_container(X64)
        with pytest.raises(ValueError, match="Buffer dtype mismatch"):
            CSRDataset64(
                X_csr32.data,
                X_csr32.indptr,
                X_csr32.indices,
                y32,
                sample_weight32,
                seed=42,
            )

            CSRDataset32(
                X_csr64.data,
                X_csr64.indptr,
                X_csr64.indices,
                y64,
                sample_weight64,
                seed=42,
            )

def test_gen_even_slices():
    # check that gen_even_slices contains all samples
    some_range = range(10)
    joined_range = list(chain(*[some_range[slice] for slice in gen_even_slices(10, 3)]))
    assert_array_equal(some_range, joined_range)

def test_get_chunk_n_rows(row_bytes, max_n_rows, working_memory, expected):
        actual = get_chunk_n_rows(
            row_bytes=row_bytes,
            max_n_rows=max_n_rows,
        )

    assert actual == expected
    assert type(actual) is type(expected)
            actual = get_chunk_n_rows(row_bytes=row_bytes, max_n_rows=max_n_rows)
        assert actual == expected
        assert type(actual) is type(expected)

def test_get_chunk_n_rows_warns():
    """Check that warning is raised when working_memory is too low."""
    row_bytes = 1024 * 1024 + 1
    max_n_rows = None
    working_memory = 1
    expected = 1

        "Could not adhere to working_memory config. Currently 1MiB, 2MiB required."
    )
        )

def test_attach_unique_attaches_unique_to_array():
    arr = np.array([1, 2, 2, 3, 4, 4, 5])
    arr_ = attach_unique(arr)
    assert_array_equal(arr_.dtype.metadata["unique"], np.array([1, 2, 3, 4, 5]))
    assert_array_equal(arr_, arr)

def test_cached_unique_returns_cached_unique():
    my_dtype = np.dtype(np.float64, metadata={"unique": np.array([1, 2])})
    arr = np.array([1, 2, 2, 3, 4, 4, 5], dtype=my_dtype)
    assert_array_equal(cached_unique(arr), np.array([1, 2]))

def test_attach_unique_not_ndarray():
    """Test that when not np.ndarray, we don't touch the array."""
    arr = [1, 2, 2, 3, 4, 4, 5]
    assert arr_ is arr

def test_attach_unique_returns_view():
    """Test that attach_unique returns a view of the array."""
    assert arr_.base is arr

def test_attach_unique_return_tuple():
    """Test return_tuple argument of the function."""
    arr_tuple = attach_unique(arr, return_tuple=True)
    assert isinstance(arr_tuple, tuple)
    assert len(arr_tuple) == 1
    assert_array_equal(arr_tuple[0], arr)

    arr_single = attach_unique(arr, return_tuple=False)
    assert isinstance(arr_single, np.ndarray)
    assert_array_equal(arr_single, arr)

def test_check_array_keeps_unique():
    """Test that check_array keeps the unique metadata."""
    arr = np.array([[1, 2, 2, 3, 4, 4, 5]])
    arr_ = check_array(arr_)

def test_type_invariance(dtype, WeightVector):
    """Check the `dtype` consistency of `WeightVector`."""
    weights = np.random.rand(100).astype(dtype)
    average_weights = np.random.rand(100).astype(dtype)

    weight_vector = WeightVector(weights, average_weights)

    assert np.asarray(weight_vector.w).dtype is np.dtype(dtype)
    assert np.asarray(weight_vector.aw).dtype is np.dtype(dtype)

def test_averaged_weighted_median():
    y = np.array([0, 1, 2, 3, 4, 5])
    sw = np.array([1, 1, 1, 1, 1, 1])

    score = _averaged_weighted_percentile(y, sw, 50)

    assert score == np.median(y)

def test_averaged_weighted_percentile(global_random_seed):
    y = rng.randint(20, size=10)

    sw = np.ones(10)

    score = _averaged_weighted_percentile(y, sw, 20)

    assert score == np.percentile(y, 20, method="averaged_inverted_cdf")

def test_averaged_and_weighted_percentile():
    y = np.array([0, 1, 2])
    sw = np.array([5, 1, 5])
    q = 50

    score_averaged = _averaged_weighted_percentile(y, sw, q)
    score = _weighted_percentile(y, sw, q)

    assert score_averaged == score

def test_weighted_percentile():
    """Check `weighted_percentile` on artificial data with obvious median."""
    y = np.empty(102, dtype=np.float64)
    y[:50] = 0
    y[-51:] = 2
    y[-1] = 100000
    y[50] = 1
    sw = np.ones(102, dtype=np.float64)
    sw[-1] = 0.0
    value = _weighted_percentile(y, sw, 50)
    assert approx(value) == 1

def test_weighted_percentile_equal():
    """Check `weighted_percentile` with all weights equal to 1."""
    y.fill(0.0)
    score = _weighted_percentile(y, sw, 50)
    assert approx(score) == 0

def test_weighted_percentile_zero_weight():
    """Check `weighted_percentile` with all weights equal to 0."""
    y.fill(1.0)
    sw.fill(0.0)
    assert approx(value) == 1.0

def test_weighted_percentile_zero_weight_zero_percentile():
    """Check `weighted_percentile(percentile_rank=0)` behaves correctly.

    Ensures that (leading)zero-weight observations ignored when `percentile_rank=0`.
    See #20528 for details.
    """
    sw = np.array([0, 0, 1, 1, 1, 0])
    value = _weighted_percentile(y, sw, 0)
    assert approx(value) == 2

    assert approx(value) == 3

    value = _weighted_percentile(y, sw, 100)
    assert approx(value) == 4

def test_weighted_median_equal_weights(global_random_seed):
    """Checks `_weighted_percentile(percentile_rank=50)` is the same as `np.median`.

    `sample_weights` are all 1s and the number of samples is odd.
    When number of samples is odd, `_weighted_percentile` always falls on a single
    observation (not between 2 values, in which case the lower value would be taken)
    and is thus equal to `np.median`.
    For an even number of samples, this check will not always hold as (note that
    for some other percentile methods it will always hold). See #17370 for details.
    """
    x = rng.randint(10, size=11)
    median = np.median(x)
    w_median = _weighted_percentile(x, weights)
    assert median == approx(w_median)

def test_weighted_median_integer_weights(global_random_seed):
    # Checks average weighted percentile_rank=0.5 is same as median when manually weight
    # data
    x = rng.randint(20, size=10)
    weights = rng.choice(5, size=10)
    x_manual = np.repeat(x, weights)
    median = np.median(x_manual)
    w_median = _averaged_weighted_percentile(x, weights)

def test_weighted_percentile_2d(global_random_seed):
    # Check for when array 2D and sample_weight 1D
    x1 = rng.randint(10, size=10)
    w1 = rng.choice(5, size=10)

    x2 = rng.randint(20, size=10)
    x_2d = np.vstack((x1, x2)).T

    w_median = _weighted_percentile(x_2d, w1)
    p_axis_0 = [_weighted_percentile(x_2d[:, i], w1) for i in range(x_2d.shape[1])]
    assert_allclose(w_median, p_axis_0)
    # Check when array and sample_weight both 2D
    w2 = rng.choice(5, size=10)
    w_2d = np.vstack((w1, w2)).T

    w_median = _weighted_percentile(x_2d, w_2d)
    p_axis_0 = [
        _weighted_percentile(x_2d[:, i], w_2d[:, i]) for i in range(x_2d.shape[1])
    ]

def test_weighted_percentile_array_api_consistency(
    global_random_seed, array_namespace, device, dtype_name, data, weights, percentile
):
    """Check `_weighted_percentile` gives consistent results with array API."""
    if array_namespace == "array_api_strict":
        try:
            import array_api_strict
            pass
        else:
            if device == array_api_strict.Device("device1"):
                # See https://github.com/data-apis/array-api-strict/issues/134
                pytest.xfail(
                    "array_api_strict has bug when indexing with tuple of arrays "
                    "on non-'CPU_DEVICE' devices."
                )

    # Skip test for percentile=0 edge case (#20528) on namespace/device where
    # xp.nextafter is broken. This is the case for torch with MPS device:
    # https://github.com/pytorch/pytorch/issues/150027
    zero = xp.zeros(1, device=device)
    one = xp.ones(1, device=device)
    if percentile == 0 and xp.all(xp.nextafter(zero, one) == zero):
        pytest.xfail(f"xp.nextafter is broken on {device}")

    X_np = data(rng) if callable(data) else data
    weights_np = weights(rng) if callable(weights) else weights
    # Ensure `data` of correct dtype
    X_np = X_np.astype(dtype_name)

    result_np = _weighted_percentile(X_np, weights_np, percentile)
    # Convert to Array API arrays
    X_xp = xp.asarray(X_np, device=device)
    weights_xp = xp.asarray(weights_np, device=device)

        result_xp = _weighted_percentile(X_xp, weights_xp, percentile)
        assert get_namespace(result_xp)[0] == get_namespace(X_xp)[0]

    assert result_xp_np.dtype == result_np.dtype
    assert result_xp_np.shape == result_np.shape
    assert_allclose(result_np, result_xp_np)

    # Check dtype correct (`sample_weight` should follow `array`)
    if dtype_name == "float32":
        assert result_xp_np.dtype == result_np.dtype == np.float32
    else:
        assert result_xp_np.dtype == np.float64

def test_weighted_percentile_nan_filtered(sample_weight_ndim, global_random_seed):
    """Test that calling _weighted_percentile on an array with nan values returns
    the same results as calling _weighted_percentile on a filtered version of the data.
    We test both with sample_weight of the same shape as the data and with
    one-dimensional sample_weight."""

    array_with_nans = rng.rand(100, 10)
    array_with_nans[rng.rand(*array_with_nans.shape) < 0.5] = np.nan
    nan_mask = np.isnan(array_with_nans)

    if sample_weight_ndim == 2:
        sample_weight = rng.randint(1, 6, size=(100, 10))
    else:
        sample_weight = rng.randint(1, 6, size=(100,))

    # Find the weighted percentile on the array with nans:
    results = _weighted_percentile(array_with_nans, sample_weight, 30)

    # Find the weighted percentile on the filtered array:
    filtered_array = [
        array_with_nans[~nan_mask[:, col], col]
        for col in range(array_with_nans.shape[1])
    ]
    if sample_weight.ndim == 1:
        sample_weight = np.repeat(sample_weight, array_with_nans.shape[1]).reshape(
            array_with_nans.shape[0], array_with_nans.shape[1]
        )
    filtered_weights = [
        sample_weight[~nan_mask[:, col], col] for col in range(array_with_nans.shape[1])
    ]

    expected_results = np.array(
        [
            _weighted_percentile(filtered_array[col], filtered_weights[col], 30)
            for col in range(array_with_nans.shape[1])
        ]
    )

    assert_array_equal(expected_results, results)

def test_weighted_percentile_all_nan_column():
    """Check that nans are ignored in general, except for all NaN columns."""

    array = np.array(
        [
            [np.nan, 5],
            [np.nan, 1],
            [np.nan, np.nan],
        ]
    )
    weights = np.ones_like(array)
    percentile_rank = 90

    values = _weighted_percentile(array, weights, percentile_rank)

    # The percentile of the second column should be `5` even though there are many nan
    # values present; the percentile of the first column can only be nan, since there
    # are no other possible values:
    assert np.array_equal(values, np.array([np.nan, 5]), equal_nan=True)

def test_weighted_percentile_like_numpy_quantile(percentile, global_random_seed):
    """Check that _weighted_percentile delivers equivalent results as np.quantile
    with weights."""

    array = rng.rand(10, 100)
    sample_weight = rng.randint(1, 6, size=(10, 100))

    percentile_weighted_percentile = _weighted_percentile(
        array, sample_weight, percentile
    )
    percentile_numpy_quantile = np.quantile(
        array, percentile / 100, weights=sample_weight, axis=0, method="inverted_cdf"
    )

    assert_array_equal(percentile_weighted_percentile, percentile_numpy_quantile)

def test_weighted_percentile_like_numpy_nanquantile(percentile, global_random_seed):
    """Check that _weighted_percentile delivers equivalent results as np.nanquantile

    array_with_nans = rng.rand(10, 100)

        array_with_nans, sample_weight, percentile
    )
    percentile_numpy_nanquantile = np.nanquantile(
        array_with_nans,
        percentile / 100,
        axis=0,
        method="inverted_cdf",
    )

    assert_array_equal(percentile_weighted_percentile, percentile_numpy_nanquantile)

def _generate_sparse(
    data,
    sparse_containers=tuple(
        COO_CONTAINERS
        + DOK_CONTAINERS
    ),
    dtypes=(bool, int, np.int8, np.uint8, float, np.float32),
):
    return [
        sparse_container(data, dtype=dtype)
        for sparse_container in sparse_containers
        for dtype in dtypes
    ]

def test_unique_labels():
    # Empty iterable
        unique_labels()

    # Multiclass problem
    assert_array_equal(unique_labels(range(10)), np.arange(10))
    assert_array_equal(unique_labels(np.arange(10)), np.arange(10))
    assert_array_equal(unique_labels([4, 0, 2]), np.array([0, 2, 4]))

    # Multilabel indicator
        unique_labels(np.array([[0, 0, 1], [1, 0, 1], [0, 0, 0]])), np.arange(3)
    )

    assert_array_equal(unique_labels(np.array([[0, 0, 1], [0, 0, 0]])), np.arange(3))

    # Several arrays passed
    assert_array_equal(unique_labels([4, 0, 2], range(5)), np.arange(5))
    assert_array_equal(unique_labels((0, 1, 2), (0,), (2, 1)), np.arange(3))

    # Border line case with binary indicator matrix
        unique_labels([4, 0, 2], np.ones((5, 5)))
        unique_labels(np.ones((5, 4)), np.ones((5, 5)))

    assert_array_equal(unique_labels(np.ones((4, 5)), np.ones((5, 5))), np.arange(5))

def test_unique_labels_non_specific():
    # Test unique_labels with a variety of collected examples

    # Smoke test for all supported format
    for format in ["binary", "multiclass", "multilabel-indicator"]:
        for y in EXAMPLES[format]:
            unique_labels(y)

    # We don't support those format at the moment
    for example in NON_ARRAY_LIKE_EXAMPLES:
            unique_labels(example)

    for y_type in [
        "unknown",
        "continuous",
    ]:
        for example in EXAMPLES[y_type]:
                unique_labels(example)

def test_unique_labels_mixed_types():
    # Mix with binary or multiclass and multilabel
    mix_clf_format = product(
        EXAMPLES["multilabel-indicator"], EXAMPLES["multiclass"] + EXAMPLES["binary"]
    )

    for y_multilabel, y_multiclass in mix_clf_format:
            unique_labels(y_multiclass, y_multilabel)
            unique_labels(y_multilabel, y_multiclass)

        unique_labels([[1, 2]], [["a", "d"]])

        unique_labels(["1", 2])

        unique_labels([["1", 2], [1, 3]])

        unique_labels([["1", "2"], [2, 3]])

def test_is_multilabel():
    for group, group_examples in EXAMPLES.items():
        dense_exp = group == "multilabel-indicator"

        for example in group_examples:
            # Only mark explicitly defined sparse examples as valid sparse
            # multilabel-indicators
            sparse_exp = dense_exp and issparse(example)

            if issparse(example) or (
                hasattr(example, "__array__")
                and np.asarray(example).ndim == 2
                and np.asarray(example).dtype.kind in "biuf"
                and np.asarray(example).shape[1] > 0
            ):
                examples_sparse = [
                    sparse_container(example)
                    for sparse_container in (
                        COO_CONTAINERS
                        + CSC_CONTAINERS
                        + CSR_CONTAINERS
                        + DOK_CONTAINERS
                        + LIL_CONTAINERS
                    )
                ]
                for exmpl_sparse in examples_sparse:
                    assert sparse_exp == is_multilabel(exmpl_sparse), (
                        f"is_multilabel({exmpl_sparse!r}) should be {sparse_exp}"
                    )

            # Densify sparse examples before testing
            if issparse(example):
                example = example.toarray()

            assert dense_exp == is_multilabel(example), (
                f"is_multilabel({example!r}) should be {dense_exp}"
            )

def test_is_multilabel_array_api_compliance(array_namespace, device, dtype_name):

    for group, group_examples in ARRAY_API_EXAMPLES.items():
            if np.asarray(example).dtype.kind == "f":
                example = np.asarray(example, dtype=dtype_name)
            else:
                example = np.asarray(example)
            example = xp.asarray(example, device=device)

                assert dense_exp == is_multilabel(example), (
                    f"is_multilabel({example!r}) should be {dense_exp}"
                )

    for y_type in EXAMPLES.keys():
        if y_type in ["unknown", "continuous", "continuous-multioutput"]:
            for example in EXAMPLES[y_type]:
                msg = "Unknown label type: "
                with pytest.raises(ValueError, match=msg):
                    check_classification_targets(example)
        else:
                check_classification_targets(example)

def test_type_of_target():
            assert type_of_target(example) == group, (
                "type_of_target(%r) should be %r, got %r"
                % (
                    example,
                    group,
                    type_of_target(example),
                )
            )

        msg_regex = r"Expected array-like \(array or non-string sequence\).*"
        with pytest.raises(ValueError, match=msg_regex):
            type_of_target(example)

    for example in MULTILABEL_SEQUENCES:
        msg = (
            "You appear to be using a legacy multi-label data "
            "representation. Sequence of sequences are no longer supported;"
            " use a binary array or sparse matrix instead."
        )

def test_type_of_target_pandas_sparse():

    y = pd.arrays.SparseArray([1, np.nan, np.nan, 1, np.nan])
    msg = "y cannot be class 'SparseSeries' or 'SparseArray'"
        type_of_target(y)

def test_type_of_target_pandas_nullable():
    """Check that type_of_target works with pandas nullable dtypes."""

    for dtype in ["Int32", "Float32"]:
        y_true = pd.Series([1, 0, 2, 3, 4], dtype=dtype)
        assert type_of_target(y_true) == "multiclass"

        y_true = pd.Series([1, 0, 1, 0], dtype=dtype)
        assert type_of_target(y_true) == "binary"

    y_true = pd.DataFrame([[1.4, 3.1], [3.1, 1.4]], dtype="Float32")
    assert type_of_target(y_true) == "continuous-multioutput"

    y_true = pd.DataFrame([[0, 1], [1, 1]], dtype="Int32")
    assert type_of_target(y_true) == "multilabel-indicator"

    y_true = pd.DataFrame([[1, 2], [3, 1]], dtype="Int32")
    assert type_of_target(y_true) == "multiclass-multioutput"

def test_unique_labels_pandas_nullable(dtype):
    """Checks that unique_labels work with pandas nullable dtypes.

    Non-regression test for gh-25634.
    """

    y_true = pd.Series([1, 0, 0, 1, 0, 1, 1, 0, 1], dtype=dtype)

    labels = unique_labels(y_true, y_predicted)
    assert_array_equal(labels, [0, 1])

def test_class_distribution(csc_container):
        [
            [2, 2, 0, 1],
            [1, 3, 0, 1],
            [4, 2, 0, 1],
            [2, 0, 0, 1],
        ]
    )
    # Define the sparse matrix with a mix of implicit and explicit zeros
    data = np.array([1, 2, 1, 4, 2, 1, 0, 2, 3, 2, 3, 1, 1, 1, 1, 1, 1])
    indices = np.array([0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 5, 0, 1, 2, 3, 4, 5])
    indptr = np.array([0, 6, 11, 11, 17])
    y_sp = csc_container((data, indices, indptr), shape=(6, 4))

    classes, n_classes, class_prior = class_distribution(y)
    classes_sp, n_classes_sp, class_prior_sp = class_distribution(y_sp)
    classes_expected = [[1, 2, 4], [0, 2, 3], [0], [1]]
    n_classes_expected = [3, 3, 1, 1]
    class_prior_expected = [[3 / 6, 2 / 6, 1 / 6], [1 / 3, 1 / 3, 1 / 3], [1.0], [1.0]]

        assert_array_almost_equal(classes[k], classes_expected[k])
        assert_array_almost_equal(n_classes[k], n_classes_expected[k])
        assert_array_almost_equal(class_prior[k], class_prior_expected[k])

        assert_array_almost_equal(classes_sp[k], classes_expected[k])
        assert_array_almost_equal(n_classes_sp[k], n_classes_expected[k])
        assert_array_almost_equal(class_prior_sp[k], class_prior_expected[k])

    # Test again with explicit sample weights
    (classes, n_classes, class_prior) = class_distribution(
        y, [1.0, 2.0, 1.0, 2.0, 1.0, 2.0]
    )
    (classes_sp, n_classes_sp, class_prior_sp) = class_distribution(
    )
    class_prior_expected = [[4 / 9, 3 / 9, 2 / 9], [2 / 9, 4 / 9, 3 / 9], [1.0], [1.0]]

def test_safe_split_with_precomputed_kernel():
    clf = SVC()
    clfp = SVC(kernel="precomputed")

    cv = ShuffleSplit(test_size=0.25, random_state=0)
    train, test = next(iter(cv.split(X)))

    X_train, y_train = _safe_split(clf, X, y, train)
    K_train, y_train2 = _safe_split(clfp, K, y, train)
    assert_array_almost_equal(K_train, np.dot(X_train, X_train.T))
    assert_array_almost_equal(y_train, y_train2)

    X_test, y_test = _safe_split(clf, X, y, test, train)
    K_test, y_test2 = _safe_split(clfp, K, y, test, train)
    assert_array_almost_equal(K_test, np.dot(X_test, X_train.T))
    assert_array_almost_equal(y_test, y_test2)

    # test properties for ovr decision function

    predictions = np.array([[0, 1, 1], [0, 1, 0], [0, 1, 1], [0, 1, 1]])

    confidences = np.array(
        [[-1e16, 0, -1e16], [1.0, 2.0, -3.0], [-5.0, 2.0, 5.0], [-0.5, 0.2, 0.5]]
    )

    dec_values = _ovr_decision_function(predictions, confidences, n_classes)

    # check that the decision values are within 0.5 range of the votes
    votes = np.array([[1, 0, 2], [1, 1, 1], [1, 0, 2], [1, 0, 2]])

    assert_allclose(votes, dec_values, atol=0.5)

    # check that the prediction are what we expect
    # highest vote or highest confidence if there is a tie.
    # for the second sample we have a tie (should be won by 1)
    expected_prediction = np.array([2, 1, 2, 2])
    assert_array_equal(np.argmax(dec_values, axis=1), expected_prediction)

    # third and fourth sample have the same vote but third sample
    # has higher confidence, this should reflect on the decision values
    assert dec_values[2, 2] > dec_values[3, 2]

    # assert subset invariance.
    dec_values_one = [
        _ovr_decision_function(
            np.array([predictions[i]]), np.array([confidences[i]]), n_classes
        )[0]
        for i in range(4)
    ]

    assert_allclose(dec_values, dec_values_one, atol=1e-6)

def test_labels_in_bytes_format_error(input_type):
    # check that we raise an error with bytes encoded labels
    # https://github.com/scikit-learn/scikit-learn/issues/16980
    target = _convert_container([b"a", b"b"], input_type)
    err_msg = "Support for labels represented as bytes is not supported"
        type_of_target(target)

def test_pandas_adapter():
    """Check pandas adapter has expected behavior."""
    X_np = np.asarray([[1, 0, 3], [0, 0, 1]])
    columns = np.asarray(["f0", "f1", "f2"], dtype=object)
    index = np.asarray([0, 1])
    X_df_orig = pd.DataFrame([[1, 2], [1, 3]], index=index)

    adapter = ADAPTERS_MANAGER.adapters["pandas"]
    X_container = adapter.create_container(X_np, X_df_orig, columns=lambda: columns)
    assert isinstance(X_container, pd.DataFrame)
    assert_array_equal(X_container.columns, columns)
    assert_array_equal(X_container.index, index)

    # Input dataframe's index does not change
    new_columns = np.asarray(["f0", "f1"], dtype=object)
    X_df = pd.DataFrame([[1, 2], [1, 3]], index=[10, 12])
    new_df = adapter.create_container(X_df, X_df_orig, columns=new_columns)
    assert_array_equal(new_df.columns, new_columns)
    assert_array_equal(new_df.index, X_df.index)

    assert adapter.is_supported_container(X_df)
    assert not adapter.is_supported_container(X_np)

    # adapter.update_columns updates the columns
    new_columns = np.array(["a", "c"], dtype=object)
    new_df = adapter.rename_columns(X_df, new_columns)

    # adapter.hstack stacks the dataframes horizontally.
    X_df_1 = pd.DataFrame([[1, 2, 5], [3, 4, 6]], columns=["a", "b", "e"])
    X_df_2 = pd.DataFrame([[4], [5]], columns=["c"])
    X_stacked = adapter.hstack([X_df_1, X_df_2])

    expected_df = pd.DataFrame(
        [[1, 2, 5, 4], [3, 4, 6, 5]], columns=["a", "b", "e", "c"]
    )
    pd.testing.assert_frame_equal(X_stacked, expected_df)

    # check that we update properly the columns even with duplicate column names
    # this use-case potentially happen when using ColumnTransformer
    # non-regression test for gh-28260
    X_df = pd.DataFrame([[1, 2], [1, 3]], columns=["a", "a"])
    new_columns = np.array(["x__a", "y__a"], dtype=object)

    # check the behavior of the inplace parameter in `create_container`
    # we should trigger a copy
    X_df = pd.DataFrame([[1, 2], [1, 3]], index=index)
    X_output = adapter.create_container(X_df, X_df, columns=["a", "b"], inplace=False)
    assert X_output is not X_df
    assert list(X_df.columns) == [0, 1]
    assert list(X_output.columns) == ["a", "b"]

    # the operation is inplace
    X_output = adapter.create_container(X_df, X_df, columns=["a", "b"], inplace=True)
    assert X_output is X_df
    assert list(X_df.columns) == ["a", "b"]

def test_polars_adapter():
    """Check Polars adapter has expected behavior."""
    X_np = np.array([[1, 0, 3], [0, 0, 1]])
    columns = ["f1", "f2", "f3"]
    X_df_orig = pl.DataFrame(X_np, schema=columns, orient="row")

    adapter = ADAPTERS_MANAGER.adapters["polars"]

    assert isinstance(X_container, pl.DataFrame)

    # Update columns with create_container
    new_columns = np.asarray(["a", "b", "c"], dtype=object)
    new_df = adapter.create_container(X_df_orig, X_df_orig, columns=new_columns)

    assert adapter.is_supported_container(X_df_orig)

    new_columns = np.array(["a", "c", "g"], dtype=object)
    new_df = adapter.rename_columns(X_df_orig, new_columns)

    X_df_1 = pl.DataFrame([[1, 2, 5], [3, 4, 6]], schema=["a", "b", "e"], orient="row")
    X_df_2 = pl.DataFrame([[4], [5]], schema=["c"], orient="row")

    expected_df = pl.DataFrame(
        [[1, 2, 5, 4], [3, 4, 6, 5]], schema=["a", "b", "e", "c"], orient="row"
    )

    assert_frame_equal(X_stacked, expected_df)

    X_df = pl.DataFrame([[1, 2], [1, 3]], schema=["a", "b"], orient="row")
    X_output = adapter.create_container(X_df, X_df, columns=["c", "d"], inplace=False)
    assert list(X_output.columns) == ["c", "d"]

    X_output = adapter.create_container(X_df, X_df, columns=["c", "d"], inplace=True)
    assert list(X_df.columns) == ["c", "d"]

def test__container_error_validation(csr_container):
    """Check errors in _wrap_data_with_container."""
    X = np.asarray([[1, 0, 3], [0, 0, 1]])
    match = "The transformer outputs a scipy sparse matrix."
        with pytest.raises(ValueError, match=match):
            _wrap_data_with_container("transform", X_csr, X, StandardScaler())

class EstimatorNoSetOutputWithTransform:
        return X  # pragma: no cover

class EstimatorWithSetOutput(_SetOutputMixin):
        self.n_features_in_ = X.shape[1]

        return X

        return np.asarray([f"X{i}" for i in range(self.n_features_in_)], dtype=object)

def test__safe_set_output():
    """Check _safe_set_output works as expected."""

    # Estimator without transform will not raise when setting set_output for transform.
    est = EstimatorWithoutSetOutputAndWithoutTransform()
    _safe_set_output(est, transform="pandas")

    # Estimator with transform but without set_output will raise
    est = EstimatorNoSetOutputWithTransform()
    with pytest.raises(ValueError, match="Unable to configure output"):
        _safe_set_output(est, transform="pandas")

    est = EstimatorWithSetOutput().fit(np.asarray([[1, 2, 3]]))
    config = _get_output_config("transform", est)
    assert config["dense"] == "pandas"

    _safe_set_output(est, transform="default")
    assert config["dense"] == "default"

    # transform is None is a no-op, so the config remains "default"
    _safe_set_output(est, transform=None)

class EstimatorNoSetOutputWithTransformNoFeatureNamesOut(_SetOutputMixin):

def test_set_output_mixin():
    """Estimator without get_feature_names_out does not define `set_output`."""
    est = EstimatorNoSetOutputWithTransformNoFeatureNamesOut()

def test__safe_set_output_error():
    """Check transform with invalid config."""

    est = EstimatorWithSetOutput()
    _safe_set_output(est, transform="bad")

    msg = "output config must be in"
        est.transform(X)

def test_set_output_method(dataframe_lib):
    """Check that the output is a dataframe."""
    lib = pytest.importorskip(dataframe_lib)

    est = EstimatorWithSetOutput().fit(X)

    # transform=None is a no-op
    est2 = est.set_output(transform=None)
    assert est2 is est
    X_trans_np = est2.transform(X)
    assert isinstance(X_trans_np, np.ndarray)

    est.set_output(transform=dataframe_lib)

    X_trans_pd = est.transform(X)

    assert isinstance(X_trans_pd, lib.DataFrame)

def test_set_output_method_error():
    """Check transform fails with invalid transform."""

    est.set_output(transform="bad")

def test__get_output_config(transform_output):
    """Check _get_output_config works as expected."""

    # Without a configuration set, the global config is used
    global_config = get_config()["transform_output"]
    config = _get_output_config("transform")
    assert config["dense"] == global_config

    with config_context(transform_output=transform_output):
        # with estimator=None, the global config is used
        config = _get_output_config("transform")
        assert config["dense"] == transform_output

        est = EstimatorNoSetOutputWithTransform()
        config = _get_output_config("transform", est)

        est = EstimatorWithSetOutput()
        # If estimator has not config, use global config

        # If estimator has a config, use local config
        assert config["dense"] == "default"

    est.set_output(transform=transform_output)
    assert config["dense"] == transform_output

class EstimatorWithSetOutputNoAutoWrap(_SetOutputMixin, auto_wrap_output_keys=None):
        return X

def test_get_output_auto_wrap_false():
    """Check that auto_wrap_output_keys=None does not wrap."""
    est = EstimatorWithSetOutputNoAutoWrap()

    assert X is est.transform(X)

def test_auto_wrap_output_keys_errors_with_incorrect_input():
    msg = "auto_wrap_output_keys must be None or a tuple of keys."

        class BadEstimator(_SetOutputMixin, auto_wrap_output_keys="bad_parameter"):
            pass

class AnotherMixin:
    def __init_subclass__(cls, custom_parameter, **kwargs):
        cls.custom_parameter = custom_parameter

def test_set_output_mixin_custom_mixin():
    """Check that multiple init_subclasses passes parameters up."""

    class BothMixinEstimator(_SetOutputMixin, AnotherMixin, custom_parameter=123):
            return X

            return input_features

    est = BothMixinEstimator()
    assert est.custom_parameter == 123
    assert hasattr(est, "set_output")

def test_set_output_mro():
    """Check that multi-inheritance resolves to the correct class method.

    Non-regression test gh-25293.
    """

    class Base(_SetOutputMixin):
            return "Base"

    class A(Base):
        pass

    class B(Base):
            return "B"

    class C(A, B):
        pass

    assert C().transform(None) == "B"

class EstimatorWithSetOutputIndex(_SetOutputMixin):

        # transform by giving output a new index.
        return pd.DataFrame(X.to_numpy(), index=[f"s{i}" for i in range(X.shape[0])])

def test_set_output_pandas_keep_index():
    """Check that set_output does not override index.

    Non-regression test for gh-25730.
    """

    X = pd.DataFrame([[1, 2, 3], [4, 5, 6]], index=[0, 1])
    est = EstimatorWithSetOutputIndex().set_output(transform="pandas")

    assert_array_equal(X_trans.index, ["s0", "s1"])

class EstimatorReturnTuple(_SetOutputMixin):
    def __init__(self, OutputTuple):
        self.OutputTuple = OutputTuple

        return self.OutputTuple(X, 2 * X)

def test_set_output_named_tuple_out():
    """Check that namedtuples are kept by default."""
    Output = namedtuple("Output", "X, Y")
    X = np.asarray([[1, 2, 3]])
    est = EstimatorReturnTuple(OutputTuple=Output)

    assert isinstance(X_trans, Output)
    assert_array_equal(X_trans.X, X)
    assert_array_equal(X_trans.Y, 2 * X)

class EstimatorWithListInput(_SetOutputMixin):
        assert isinstance(X, list)
        self.n_features_in_ = len(X[0])

        return X

def test_set_output_list_input(dataframe_lib):
    """Check set_output for list input.

    Non-regression test for #27037.
    """

    X = [[0, 1, 2, 3], [4, 5, 6, 7]]
    est = EstimatorWithListInput()

    X_out = est.fit(X).transform(X)
    assert isinstance(X_out, lib.DataFrame)
    assert_array_equal(X_out.columns, ["X0", "X1", "X2", "X3"])

def test_adapter_class_has_interface(name):
    """Check adapters have the correct interface."""
    assert isinstance(ADAPTERS_MANAGER.adapters[name], ContainerAdapterProtocol)

def test_check_library_installed(monkeypatch):
    """Check import error changed."""
    orig_import_module = importlib.import_module

    def patched_import_module(name):
        orig_import_module(name, package=None)

    monkeypatch.setattr(importlib, "import_module", patched_import_module)

    msg = "Setting output container to 'pandas' requires"
        check_library_installed("pandas")

def test_get_adapter_from_container():
    """Check the behavior fo `_get_adapter_from_container`."""
    X = pd.DataFrame({"a": [1, 2, 3], "b": [10, 20, 100]})
    adapter = _get_adapter_from_container(X)
    assert adapter.container_lib == "pandas"
    err_msg = "The container does not have a registered adapter in scikit-learn."
        _get_adapter_from_container(X.to_numpy())

            return X

            return "B"

            pass

def test_compute_class_weight():
    # Test (and demo) compute_class_weight.
    y = np.asarray([2, 2, 2, 3, 3, 4])

    cw = compute_class_weight("balanced", classes=classes, y=y)
    # total effect of samples is preserved
    class_counts = np.bincount(y)[2:]
    assert_almost_equal(np.dot(cw, class_counts), y.shape[0])
    assert cw[0] < cw[1] < cw[2]

def test_compute_class_weight_not_present(y_type, class_weight, classes, err_msg):
    # Raise error when y does not contain all class labels
    y = (
        np.asarray([0, 0, 0, 1, 1, 2])
        if y_type == "numeric"
        else np.asarray(["dog", "cat", "dog"])
    )

    print(y)
        compute_class_weight(class_weight, classes=classes, y=y)

def test_compute_class_weight_dict():
    classes = np.arange(3)
    class_weights = {0: 1.0, 1: 2.0, 2: 3.0}
    y = np.asarray([0, 0, 1, 2])
    cw = compute_class_weight(class_weights, classes=classes, y=y)

    # When the user specifies class weights, compute_class_weights should just
    # return them.
    assert_array_almost_equal(np.asarray([1.0, 2.0, 3.0]), cw)

    # When a class weight is specified that isn't in classes, the weight is ignored
    class_weights = {0: 1.0, 1: 2.0, 2: 3.0, 4: 1.5}
    assert_allclose([1.0, 2.0, 3.0], cw)

    class_weights = {-1: 5.0, 0: 4.0, 1: 2.0, 2: 3.0}
    assert_allclose([4.0, 2.0, 3.0], cw)

def test_compute_class_weight_invariance():
    # Test that results with class_weight="balanced" is invariant wrt
    # class imbalance if the number of samples is identical.
    # The test uses a balanced two class dataset with 100 datapoints.
    # It creates three versions, one where class 1 is duplicated
    # resulting in 150 points of class 1 and 50 of class 0,
    # one where there are 50 points in class 1 and 150 in class 0,
    # and one where there are 100 points of each class (this one is balanced
    # again).
    # With balancing class weights, all three should give the same model.
    X, y = make_blobs(centers=2, random_state=0)
    # create dataset where class 1 is duplicated twice
    X_1 = np.vstack([X] + [X[y == 1]] * 2)
    y_1 = np.hstack([y] + [y[y == 1]] * 2)
    # create dataset where class 0 is duplicated twice
    X_0 = np.vstack([X] + [X[y == 0]] * 2)
    y_0 = np.hstack([y] + [y[y == 0]] * 2)
    # duplicate everything
    X_ = np.vstack([X] * 2)
    y_ = np.hstack([y] * 2)
    # results should be identical
    logreg1 = LogisticRegression(class_weight="balanced").fit(X_1, y_1)
    logreg0 = LogisticRegression(class_weight="balanced").fit(X_0, y_0)
    logreg = LogisticRegression(class_weight="balanced").fit(X_, y_)
    assert_array_almost_equal(logreg1.coef_, logreg0.coef_)
    assert_array_almost_equal(logreg.coef_, logreg0.coef_)

def test_compute_class_weight_balanced_negative():
    # Test compute_class_weight when labels are negative
    # Test with balanced class labels.
    classes = np.array([-2, -1, 0])
    y = np.asarray([-1, -1, 0, 0, -2, -2])

    assert len(cw) == len(classes)
    assert_array_almost_equal(cw, np.array([1.0, 1.0, 1.0]))

def test_compute_class_weight_balanced_sample_weight_equivalence():
    # Test with unbalanced and negative class labels for
    # equivalence between repeated and weighted samples

    sw = np.asarray([1, 0, 1, 1, 1, 2])

    y_rep = np.repeat(y, sw, axis=0)

    class_weights_weighted = compute_class_weight(
        "balanced", classes=classes, y=y, sample_weight=sw
    )
    class_weights_repeated = compute_class_weight("balanced", classes=classes, y=y_rep)
    assert len(class_weights_weighted) == len(classes)
    assert len(class_weights_repeated) == len(classes)

    class_counts_weighted = np.bincount(y + 2, weights=sw)
    class_counts_repeated = np.bincount(y_rep + 2)

    assert np.dot(class_weights_weighted, class_counts_weighted) == pytest.approx(
        np.dot(class_weights_repeated, class_counts_repeated)
    )

    assert_allclose(class_weights_weighted, class_weights_repeated)

def test_compute_class_weight_balanced_unordered():
    # Test compute_class_weight when classes are unordered
    classes = np.array([1, 0, 3])
    y = np.asarray([1, 0, 0, 3, 3, 3])

    class_counts = np.bincount(y)[classes]
    assert_array_almost_equal(cw, [2.0, 1.0, 2.0 / 3])

def test_compute_class_weight_default():
    # Test for the case where no weight is given for a present class.
    # Current behaviour is to assign the unweighted classes a weight of 1.
    classes_len = len(classes)

    # Test for non specified weights
    cw = compute_class_weight(None, classes=classes, y=y)
    assert len(cw) == classes_len
    assert_array_almost_equal(cw, np.ones(3))

    # Tests for partly specified weights
    cw = compute_class_weight({2: 1.5}, classes=classes, y=y)
    assert_array_almost_equal(cw, [1.5, 1.0, 1.0])

    cw = compute_class_weight({2: 1.5, 4: 0.5}, classes=classes, y=y)
    assert_array_almost_equal(cw, [1.5, 1.0, 0.5])

def test_compute_sample_weight():
    # Test (and demo) compute_sample_weight.
    # Test with balanced classes
    y = np.asarray([1, 1, 1, 2, 2, 2])
    sample_weight = compute_sample_weight("balanced", y)
    assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0])

    # Test with user-defined weights
    sample_weight = compute_sample_weight({1: 2, 2: 1}, y)
    assert_array_almost_equal(sample_weight, [2.0, 2.0, 2.0, 1.0, 1.0, 1.0])

    # Test with column vector of balanced classes
    y = np.asarray([[1], [1], [1], [2], [2], [2]])

    # Test with unbalanced classes
    y = np.asarray([1, 1, 1, 2, 2, 2, 3])
    expected_balanced = np.array(
        [0.7777, 0.7777, 0.7777, 0.7777, 0.7777, 0.7777, 2.3333]
    )
    assert_array_almost_equal(sample_weight, expected_balanced, decimal=4)

    # Test with `None` weights
    sample_weight = compute_sample_weight(None, y)
    assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0])

    # Test with multi-output of balanced classes
    y = np.asarray([[1, 0], [1, 0], [1, 0], [2, 1], [2, 1], [2, 1]])

    # Test with multi-output with user-defined weights
    sample_weight = compute_sample_weight([{1: 2, 2: 1}, {0: 1, 1: 2}], y)
    assert_array_almost_equal(sample_weight, [2.0, 2.0, 2.0, 2.0, 2.0, 2.0])

    # Test with multi-output of unbalanced classes
    y = np.asarray([[1, 0], [1, 0], [1, 0], [2, 1], [2, 1], [2, 1], [3, -1]])
    assert_array_almost_equal(sample_weight, expected_balanced**2, decimal=3)

def test_compute_sample_weight_with_subsample():
    # Test compute_sample_weight with subsamples specified.
    # Test with balanced classes and all samples present
    sample_weight = compute_sample_weight("balanced", y, indices=range(6))

    # Test with column vector of balanced classes and all samples present

    # Test with a subsample
    sample_weight = compute_sample_weight("balanced", y, indices=range(4))
    assert_array_almost_equal(sample_weight, [2.0 / 3, 2.0 / 3, 2.0 / 3, 2.0, 2.0, 2.0])

    # Test with a bootstrap subsample
    sample_weight = compute_sample_weight("balanced", y, indices=[0, 1, 1, 2, 2, 3])
    expected_balanced = np.asarray([0.6, 0.6, 0.6, 3.0, 3.0, 3.0])
    assert_array_almost_equal(sample_weight, expected_balanced)

    # Test with a bootstrap subsample for multi-output
    assert_array_almost_equal(sample_weight, expected_balanced**2)

    # Test with a missing class
    assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0])

    # Test with a missing class for multi-output
    y = np.asarray([[1, 0], [1, 0], [1, 0], [2, 1], [2, 1], [2, 1], [2, 2]])

def test_compute_sample_weight_errors(y_type, class_weight, indices, err_msg):
    # Test compute_sample_weight raises errors expected.
    # Invalid preset string
    y_single_output = np.asarray([1, 1, 1, 2, 2, 2])
    y_multi_output = np.asarray([[1, 0], [1, 0], [1, 0], [2, 1], [2, 1], [2, 1]])

    y = y_single_output if y_type == "single-output" else y_multi_output
        compute_sample_weight(class_weight, y, indices=indices)

def test_compute_sample_weight_more_than_32():
    # Non-regression smoke test for #12146
    y = np.arange(50)  # more than 32 distinct classes
    indices = np.arange(50)  # use subsampling
    weight = compute_sample_weight("balanced", y, indices=indices)
    assert_array_almost_equal(weight, np.ones(y.shape[0]))

def test_class_weight_does_not_contains_more_classes():
    """Check that class_weight can contain more labels than in y.

    Non-regression test for #22413
    """
    tree = DecisionTreeClassifier(class_weight={0: 1, 1: 10, 2: 20})

    tree.fit([[0, 0, 1], [1, 0, 1], [1, 2, 0]], [0, 0, 1])

def test_compute_sample_weight_sparse(csc_container):
    """Check that we can compute weight for sparse `y`."""
    y = csc_container(np.asarray([[0], [1], [1]]))
    assert_allclose(sample_weight, [1.5, 0.75, 0.75])

def floyd_warshall_slow(graph, directed=False):
    N = graph.shape[0]

    # set nonzero entries to infinity
    graph[np.where(graph == 0)] = np.inf

    # set diagonal to zero
    graph.flat[:: N + 1] = 0

    if not directed:
        graph = np.minimum(graph, graph.T)

    for k in range(N):
            for j in range(N):
                graph[i, j] = min(graph[i, j], graph[i, k] + graph[k, j])

    graph[np.where(np.isinf(graph))] = 0

def generate_graph(N=20):
    # sparse grid of distances
    dist_matrix = rng.random_sample((N, N))

    # make symmetric: distances are not direction-dependent
    dist_matrix = dist_matrix + dist_matrix.T

    # make graph sparse
    i = (rng.randint(N, size=N * N // 2), rng.randint(N, size=N * N // 2))
    dist_matrix[i] = 0

    dist_matrix.flat[:: N + 1] = 0

    return dist_matrix

def test_shortest_path():
    dist_matrix = generate_graph(20)
    # We compare path length and not costs (-> set distances to 0 or 1)
    dist_matrix[dist_matrix != 0] = 1

    for directed in (True, False):
        if not directed:
            dist_matrix = np.minimum(dist_matrix, dist_matrix.T)

        graph_py = floyd_warshall_slow(dist_matrix.copy(), directed)
        for i in range(dist_matrix.shape[0]):
            # Non-reachable nodes have distance 0 in graph_py
            dist_dict = defaultdict(int)
            dist_dict.update(single_source_shortest_path_length(dist_matrix, i))

            for j in range(graph_py[i].shape[0]):
                assert_array_almost_equal(dist_dict[j], graph_py[i, j])

def test_check_on_fit_success(iris, kwargs):
    X, y = iris
    CheckingClassifier(**kwargs).fit(X, y)

def test_check_on_fit_fail(iris, kwargs):
    X, y = iris
    clf = CheckingClassifier(**kwargs)

def test_check_X_on_predict_success(iris, pred_func):
    X, y = iris
    clf = CheckingClassifier(check_X=_success).fit(X, y)
    getattr(clf, pred_func)(X)

def test_check_X_on_predict_fail(iris, pred_func):
    X, y = iris
    clf.set_params(check_X=_fail)
        getattr(clf, pred_func)(X)

def test_checking_classifier(iris, input_type):
    # Check that the CheckingClassifier outputs what we expect
    X, y = iris
    clf = CheckingClassifier()

    assert len(clf.classes_) == 3
    assert clf.n_features_in_ == 4

    assert all(pred in clf.classes_ for pred in y_pred)

    assert clf.score(X) == pytest.approx(0)
    clf.set_params(foo_param=10)
    assert clf.fit(X, y).score(X) == pytest.approx(1)

    assert y_proba.shape == (150, 3)
    assert np.logical_and(y_proba >= 0, y_proba <= 1).all()

    y_decision = clf.decision_function(X)
    assert y_decision.shape == (150, 3)

    # check the shape in case of binary classification
    first_2_classes = np.logical_or(y == 0, y == 1)
    X = _safe_indexing(X, first_2_classes)
    y = _safe_indexing(y, first_2_classes)

    assert y_proba.shape == (100, 2)

    assert y_decision.shape == (100,)

def test_checking_classifier_with_params(iris, csr_container):
    X, y = iris

    clf = CheckingClassifier(check_X=sparse.issparse)
    clf.fit(X_sparse, y)

    clf = CheckingClassifier(
        check_X=check_array, check_X_params={"accept_sparse": False}
    )
        clf.fit(X_sparse, y)

def test_checking_classifier_fit_params(iris):
    # check the error raised when the number of samples is not the one expected
    X, y = iris
    sample_weight = np.ones(len(X) // 2)

    msg = f"sample_weight.shape == ({len(X) // 2},), expected ({len(X)},)!"
    assert exc.value.args[0] == msg

def test_checking_classifier_missing_fit_params(iris):
    X, y = iris
    err_msg = "Expected sample_weight to be passed"
    with pytest.raises(AssertionError, match=err_msg):

def test_checking_classifier_methods_to_check(iris, methods_to_check, predict_method):
    # check that methods_to_check allows to bypass checks
    X, y = iris

        check_X=sparse.issparse,
        methods_to_check=methods_to_check,
    )

    if predict_method in methods_to_check:
            getattr(clf, predict_method)(X)
    else:
        getattr(clf, predict_method)(X)

def test_mock_estimator_on_off_prediction(iris, response_methods):
    X, y = iris
    estimator = _MockEstimatorOnOffPrediction(response_methods=response_methods)

    assert hasattr(estimator, "classes_")
    assert_array_equal(estimator.classes_, np.unique(y))

    possible_responses = ["predict", "predict_proba", "decision_function"]
    for response in possible_responses:
        if response in response_methods:
            assert hasattr(estimator, response)
            assert getattr(estimator, response)(X) == response
        else:
            assert not hasattr(estimator, response)

def test_invalid_sample_without_replacement_algorithm():
        sample_without_replacement(5, 4, "unknown")

def test_sample_without_replacement_algorithms():
    methods = ("auto", "tracking_selection", "reservoir_sampling", "pool")

    for m in methods:

        def sample_without_replacement_method(
            n_population, n_samples, random_state=None
        ):
            return sample_without_replacement(
                n_population, n_samples, method=m, random_state=random_state
            )

        check_edge_case_of_sample_int(sample_without_replacement_method)
        check_sample_int(sample_without_replacement_method)
        check_sample_int_distribution(sample_without_replacement_method)

def check_edge_case_of_sample_int(sample_without_replacement):
    # n_population < n_sample
        sample_without_replacement(0, 1)
        sample_without_replacement(1, 2)

    # n_population == n_samples
    assert sample_without_replacement(0, 0).shape == (0,)

    assert sample_without_replacement(1, 1).shape == (1,)

    # n_population >= n_samples
    assert sample_without_replacement(5, 0).shape == (0,)
    assert sample_without_replacement(5, 1).shape == (1,)

    # n_population < 0 or n_samples < 0
        sample_without_replacement(-1, 5)
        sample_without_replacement(5, -1)

def check_sample_int(sample_without_replacement):
    # This test is heavily inspired from test_random.py of python-core.
    #
    # For the entire allowable range of 0 <= k <= N, validate that
    # the sample is of the correct length and contains only unique items
    n_population = 100

    for n_samples in range(n_population + 1):
        s = sample_without_replacement(n_population, n_samples)
        assert len(s) == n_samples
        unique = np.unique(s)
        assert np.size(unique) == n_samples
        assert np.all(unique < n_population)

    # test edge case n_population == n_samples == 0
    assert np.size(sample_without_replacement(0, 0)) == 0

def check_sample_int_distribution(sample_without_replacement):
    #
    # sample generates all possible permutations
    n_population = 10

    # a large number of trials prevents false negatives without slowing normal
    # case
    n_trials = 10000

    for n_samples in range(n_population):
        # Counting the number of combinations is not as good as counting the
        # the number of permutations. However, it works with sampling algorithm
        # that does not provide a random permutation of the subset of integer.
        n_expected = comb(n_population, n_samples, exact=True)

        output = {}
        for i in range(n_trials):
            output[frozenset(sample_without_replacement(n_population, n_samples))] = (
                None
            )

            if len(output) == n_expected:
                break
        else:
                "number of combinations != number of expected (%s != %s)"
                % (len(output), n_expected)
            )

def test_random_choice_csc(n_samples=10000, random_state=24):
    # Explicit class probabilities
    classes = [np.array([0, 1]), np.array([0, 1, 2])]
    class_probabilities = [np.array([0.5, 0.5]), np.array([0.6, 0.1, 0.3])]

    got = _random_choice_csc(n_samples, classes, class_probabilities, random_state)
    assert sp.issparse(got)

    for k in range(len(classes)):
        p = np.bincount(got[:, [k]].toarray().ravel()) / float(n_samples)
        assert_array_almost_equal(class_probabilities[k], p, decimal=1)

    # Implicit class probabilities
    classes = [[0, 1], [1, 2]]  # test for array-like support
    class_probabilities = [np.array([0.5, 0.5]), np.array([0, 1 / 2, 1 / 2])]

    got = _random_choice_csc(
        n_samples=n_samples, classes=classes, random_state=random_state
    )

    # Edge case probabilities 1.0 and 0.0
    class_probabilities = [np.array([0.0, 1.0]), np.array([0.0, 1.0, 0.0])]

        p = (
            np.bincount(
                got[:, [k]].toarray().ravel(), minlength=len(class_probabilities[k])
            )
            / n_samples
        )

    # One class target data
    classes = [[1], [0]]  # test for array-like support
    class_probabilities = [np.array([0.0, 1.0]), np.array([1.0])]

    )

        p = np.bincount(got[:, [k]].toarray().ravel()) / n_samples

def test_random_choice_csc_errors():
    # the length of an array in classes and class_probabilities is mismatched
    classes = [np.array([0, 1]), np.array([0, 1, 2, 3])]
        _random_choice_csc(4, classes, class_probabilities, 1)

    # the class dtype is not supported
    classes = [np.array(["a", "1"]), np.array(["z", "1", "2"])]

    classes = [np.array([4.2, 0.1]), np.array([0.1, 0.2, 9.4])]

    # Given probabilities don't sum to 1
    class_probabilities = [np.array([0.5, 0.6]), np.array([0.6, 0.1, 0.3])]

def test_our_rand_r():
    assert 131541053 == _our_rand_r_py(1273642419)
    assert 270369 == _our_rand_r_py(0)

        ):
            )

class LogisticRegression(BaseEstimator):
        self,
        tol=1e-4,
        C=1.0,
        solver="warn",
        multi_class="warn",
    ):
        self.C = C

class StandardScaler(TransformerMixin, BaseEstimator):
    def __init__(self, copy=True, with_mean=True, with_std=True):
        self.with_mean = with_mean
        self.with_std = with_std

    def transform(self, X, copy=None):

class RFE(BaseEstimator):
    def __init__(self, estimator, n_features_to_select=None, step=1, verbose=0):
        self.n_features_to_select = n_features_to_select
        self.step = step

class GridSearchCV(BaseEstimator):
        self,
        iid="warn",
        cv="warn",
        pre_dispatch="2*n_jobs",
        error_score="raise-deprecating",
        return_train_score=False,
    ):
        self.param_grid = param_grid
        self.iid = iid
        self.pre_dispatch = pre_dispatch
        self.error_score = error_score
        self.return_train_score = return_train_score

class CountVectorizer(BaseEstimator):
        self,
        min_df=1,
    ):

class Pipeline(BaseEstimator):
    def __init__(self, steps, memory=None):

class SVC(BaseEstimator):
        self,
        C=1.0,
        degree=3,
        gamma="auto_deprecated",
        tol=1e-3,
    ):
        self.C = C

class PCA(BaseEstimator):
        self,
        tol=0.0,
    ):

class NMF(BaseEstimator):
        self,
        tol=1e-4,
    ):

class SimpleImputer(BaseEstimator):
        self,
    ):

def test_basic(print_changed_only_false):
    # Basic pprint test
    expected = """
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
                   intercept_scaling=1, l1_ratio=None, max_iter=100,
                   multi_class='warn', n_jobs=None, penalty='l2',
                   random_state=None, solver='warn', tol=0.0001, verbose=0,
                   warm_start=False)"""

    expected = expected[1:]  # remove first \n
    assert lr.__repr__() == expected

def test_changed_only():
    # Make sure the changed_only param is correctly used when True (default)
    lr = LogisticRegression(C=99)
    expected = """LogisticRegression(C=99)"""

    # Check with a repr that doesn't fit on a single line
        C=99, class_weight=0.4, fit_intercept=False, tol=1234, verbose=True
    )
LogisticRegression(C=99, class_weight=0.4, fit_intercept=False, tol=1234,
                   verbose=True)"""

    imputer = SimpleImputer(missing_values=0)
    expected = """SimpleImputer(missing_values=0)"""
    assert imputer.__repr__() == expected

    # Defaults to np.nan, trying with float('NaN')
    imputer = SimpleImputer(missing_values=float("NaN"))
    expected = """SimpleImputer()"""

    # make sure array parameters don't throw error (see #13583)
    repr(LogisticRegressionCV(Cs=np.array([0.1, 1])))

def test_pipeline(print_changed_only_false):
    # Render a pipeline object
    pipeline = make_pipeline(StandardScaler(), LogisticRegression(C=999))
Pipeline(memory=None,
         steps=[('standardscaler',
                 StandardScaler(copy=True, with_mean=True, with_std=True)),
                ('logisticregression',
                 LogisticRegression(C=999, class_weight=None, dual=False,
                                    fit_intercept=True, intercept_scaling=1,
                                    l1_ratio=None, max_iter=100,
                                    multi_class='warn', n_jobs=None,
                                    penalty='l2', random_state=None,
                                    solver='warn', tol=0.0001, verbose=0,
                                    warm_start=False))],
         transform_input=None, verbose=False)"""

    assert pipeline.__repr__() == expected

def test_deeply_nested(print_changed_only_false):
    # Render a deeply nested estimator
    rfe = RFE(RFE(RFE(RFE(RFE(RFE(RFE(LogisticRegression())))))))
RFE(estimator=RFE(estimator=RFE(estimator=RFE(estimator=RFE(estimator=RFE(estimator=RFE(estimator=LogisticRegression(C=1.0,
                                                                                                                     class_weight=None,
                                                                                                                     dual=False,
                                                                                                                     fit_intercept=True,
                                                                                                                     intercept_scaling=1,
                                                                                                                     l1_ratio=None,
                                                                                                                     max_iter=100,
                                                                                                                     multi_class='warn',
                                                                                                                     n_jobs=None,
                                                                                                                     penalty='l2',
                                                                                                                     random_state=None,
                                                                                                                     solver='warn',
                                                                                                                     tol=0.0001,
                                                                                                                     verbose=0,
                                                                                                                     warm_start=False),
                                                                                        n_features_to_select=None,
                                                                                        step=1,
                                                                                        verbose=0),
                                                                          n_features_to_select=None,
                                                                          step=1,
                                                                          verbose=0),
                                                            n_features_to_select=None,
                                                            step=1, verbose=0),
                                              n_features_to_select=None, step=1,
                                              verbose=0),
                                n_features_to_select=None, step=1, verbose=0),
                  n_features_to_select=None, step=1, verbose=0),
    n_features_to_select=None, step=1, verbose=0)"""

    assert rfe.__repr__() == expected

def test_print_estimator_max_depth(print_changed_only, expected):
        pp = _EstimatorPrettyPrinter(depth=1)

        rfe = RFE(RFE(RFE(RFE(RFE(LogisticRegression())))))
        assert pp.pformat(rfe) == expected

def test_gridsearch(print_changed_only_false):
    # render a gridsearch
    param_grid = [
        {"kernel": ["rbf"], "gamma": [1e-3, 1e-4], "C": [1, 10, 100, 1000]},
        {"kernel": ["linear"], "C": [1, 10, 100, 1000]},
    ]
    gs = GridSearchCV(SVC(), param_grid, cv=5)

GridSearchCV(cv=5, error_score='raise-deprecating',
             estimator=SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
                           decision_function_shape='ovr', degree=3,
                           gamma='auto_deprecated', kernel='rbf', max_iter=-1,
                           probability=False, random_state=None, shrinking=True,
                           tol=0.001, verbose=False),
             iid='warn', n_jobs=None,
             param_grid=[{'C': [1, 10, 100, 1000], 'gamma': [0.001, 0.0001],
                          'kernel': ['rbf']},
                         {'C': [1, 10, 100, 1000], 'kernel': ['linear']}],
             pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
             scoring=None, verbose=0)"""

    assert gs.__repr__() == expected

def test_gridsearch_pipeline(print_changed_only_false):
    # render a pipeline inside a gridsearch
    pp = _EstimatorPrettyPrinter(compact=True, indent=1, indent_at_name=True)

    pipeline = Pipeline([("reduce_dim", PCA()), ("classify", SVC())])
    N_FEATURES_OPTIONS = [2, 4, 8]
    C_OPTIONS = [1, 10, 100, 1000]
        {
            "reduce_dim": [PCA(iterated_power=7), NMF()],
            "reduce_dim__n_components": N_FEATURES_OPTIONS,
            "classify__C": C_OPTIONS,
        },
        {
            "reduce_dim": [SelectKBest(chi2)],
            "reduce_dim__k": N_FEATURES_OPTIONS,
        },
    ]
    gspipline = GridSearchCV(pipeline, cv=3, n_jobs=1, param_grid=param_grid)
GridSearchCV(cv=3, error_score='raise-deprecating',
             estimator=Pipeline(memory=None,
                                steps=[('reduce_dim',
                                        PCA(copy=True, iterated_power='auto',
                                            n_components=None,
                                            random_state=None,
                                            svd_solver='auto', tol=0.0,
                                            whiten=False)),
                                       ('classify',
                                        SVC(C=1.0, cache_size=200,
                                            class_weight=None, coef0=0.0,
                                            decision_function_shape='ovr',
                                            degree=3, gamma='auto_deprecated',
                                            kernel='rbf', max_iter=-1,
                                            probability=False,
                                            random_state=None, shrinking=True,
                                            tol=0.001, verbose=False))]),
             iid='warn', n_jobs=1,
             param_grid=[{'classify__C': [1, 10, 100, 1000],
                          'reduce_dim': [PCA(copy=True, iterated_power=7,
                                             n_components=None,
                                             random_state=None,
                                             svd_solver='auto', tol=0.0,
                                             whiten=False),
                                         NMF(alpha=0.0, beta_loss='frobenius',
                                             init=None, l1_ratio=0.0,
                                             max_iter=200, n_components=None,
                                             random_state=None, shuffle=False,
                                             solver='cd', tol=0.0001,
                                             verbose=0)],
                          'reduce_dim__n_components': [2, 4, 8]},
                         {'classify__C': [1, 10, 100, 1000],
                          'reduce_dim': [SelectKBest(k=10,
                                                     score_func=<function chi2 at some_address>)],
                          'reduce_dim__k': [2, 4, 8]}],
             scoring=None, verbose=0)"""  # noqa: E501

    repr_ = pp.pformat(gspipline)
    # Remove address of '<function chi2 at 0x.....>' for reproducibility
    repr_ = re.sub("function chi2 at 0x.*>", "function chi2 at some_address>", repr_)
    assert repr_ == expected

def test_n_max_elements_to_show(print_changed_only_false):
    n_max_elements_to_show = 30
    pp = _EstimatorPrettyPrinter(
        compact=True,
        indent=1,
        n_max_elements_to_show=n_max_elements_to_show,
    )

    # No ellipsis
    vocabulary = {i: i for i in range(n_max_elements_to_show)}
    vectorizer = CountVectorizer(vocabulary=vocabulary)

    expected = r"""
CountVectorizer(analyzer='word', binary=False, decode_error='strict',
                dtype=<class 'numpy.int64'>, encoding='utf-8', input='content',
                lowercase=True, max_df=1.0, max_features=None, min_df=1,
                ngram_range=(1, 1), preprocessor=None, stop_words=None,
                strip_accents=None, token_pattern='(?u)\\b\\w\\w+\\b',
                tokenizer=None,
                vocabulary={0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7,
                            8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14,
                            15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20,
                            21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26,
                            27: 27, 28: 28, 29: 29})"""

    assert pp.pformat(vectorizer) == expected

    # Now with ellipsis
    vocabulary = {i: i for i in range(n_max_elements_to_show + 1)}

                            27: 27, 28: 28, 29: 29, ...})"""

    # Also test with lists
    param_grid = {"C": list(range(n_max_elements_to_show))}
    gs = GridSearchCV(SVC(), param_grid)
GridSearchCV(cv='warn', error_score='raise-deprecating',
             param_grid={'C': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
                               15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
                               27, 28, 29]},

    assert pp.pformat(gs) == expected

    param_grid = {"C": list(range(n_max_elements_to_show + 1))}
                               27, 28, 29, ...]},

def test_bruteforce_ellipsis(print_changed_only_false):
    # Check that the bruteforce ellipsis (used when the number of non-blank
    # characters exceeds N_CHAR_MAX) renders correctly.

    # test when the left and right side of the ellipsis aren't on the same
    # line.
                   in...

    assert expected == lr.__repr__(N_CHAR_MAX=150)

    # test with very small N_CHAR_MAX
    # Note that N_CHAR_MAX is not strictly enforced, but it's normal: to avoid
    # weird reprs we still keep the whole line of the right part (after the
    # ellipsis).
Lo...

    assert expected == lr.__repr__(N_CHAR_MAX=4)

    # test with N_CHAR_MAX == number of non-blank characters: In this case we
    # don't want ellipsis
    full_repr = lr.__repr__(N_CHAR_MAX=float("inf"))
    n_nonblank = len("".join(full_repr.split()))
    assert lr.__repr__(N_CHAR_MAX=n_nonblank) == full_repr
    assert "..." not in full_repr

    # test with N_CHAR_MAX == number of non-blank characters - 10: the left and
    # right side of the ellispsis are on different lines. In this case we
    # want to expend the whole line of the right side
                   intercept_scaling=1, l1_ratio=None, max_i...
    assert expected == lr.__repr__(N_CHAR_MAX=n_nonblank - 10)

    # right side of the ellispsis are on the same line. In this case we don't
    # want to expend the whole line of the right side, just add the ellispsis
    # between the 2 sides.
                   intercept_scaling=1, l1_ratio=None, max_iter...,
    assert expected == lr.__repr__(N_CHAR_MAX=n_nonblank - 4)

    # test with N_CHAR_MAX == number of non-blank characters - 2: the left and
    # right side of the ellispsis are on the same line, but adding the ellipsis
    # would actually make the repr longer. So we don't add the ellipsis.
    assert expected == lr.__repr__(N_CHAR_MAX=n_nonblank - 2)

def test_builtin_prettyprinter():
    # non regression test than ensures we can still use the builtin
    # PrettyPrinter class for estimators (as done e.g. by joblib).
    # Used to be a bug

    PrettyPrinter().pprint(LogisticRegression())

def test_kwargs_in_init():
    # Make sure the changed_only=True mode is OK when an argument is passed as
    # kwargs.
    # https://github.com/scikit-learn/scikit-learn/issues/17206

    class WithKWargs(BaseEstimator):
        # Estimator with a kwargs argument. These need to hack around
        # set_params and get_params. Here we mimic what LightGBM does.
        def __init__(self, a="willchange", b="unchanged", **kwargs):
            self.a = a
            self.b = b
            self._other_params = {}
            self.set_params(**kwargs)

        def get_params(self, deep=True):
            params = super().get_params(deep=deep)
            params.update(self._other_params)
            return params

        def set_params(self, **params):
            for key, value in params.items():
                self._other_params[key] = value

    est = WithKWargs(a="something", c="abcd", d=None)

    expected = "WithKWargs(a='something', c='abcd', d=None)"
    assert expected == est.__repr__()

    with config_context(print_changed_only=False):
        expected = "WithKWargs(a='something', b='unchanged', c='abcd', d=None)"
        assert expected == est.__repr__()

def test_complexity_print_changed_only():
    # Make sure `__repr__` is called the same amount of times
    # whether `print_changed_only` is True or False
    # https://github.com/scikit-learn/scikit-learn/issues/18490

        nb_times_repr_called = 0

        def __init__(self, estimator=None):

            DummyEstimator.nb_times_repr_called += 1
            return super().__repr__()

        def transform(self, X, copy=None):  # pragma: no cover
            return X

    estimator = DummyEstimator(
        make_pipeline(DummyEstimator(DummyEstimator()), DummyEstimator(), "passthrough")
    )
        nb_repr_print_changed_only_false = DummyEstimator.nb_times_repr_called

    DummyEstimator.nb_times_repr_called = 0
        nb_repr_print_changed_only_true = DummyEstimator.nb_times_repr_called

    assert nb_repr_print_changed_only_false == nb_repr_print_changed_only_true

        self,
        tol=1e-4,
        C=1.0,
    ):
        self.C = C

        self,
    ):

        self,
        min_df=1,
    ):

        self,
        C=1.0,
        degree=3,
        tol=1e-3,
    ):
        self.C = C

        self,
        tol=0.0,
    ):

        self,
        tol=1e-4,
    ):

        self,
    ):

            self.a = a
            self.b = b

            return X

            self.a = a
            self.b = b

            return X

def test_object_dtype_isnan(dtype, val):
    X = np.array([[val, np.nan], [np.nan, val]], dtype=dtype)

    expected_mask = np.array([[False, True], [True, False]])

    mask = _object_dtype_isnan(X)

    assert_array_equal(mask, expected_mask)

def test_smallest_admissible_index_dtype_max_val(params, expected_dtype):
    """Check the behaviour of `smallest_admissible_index_dtype` depending only on the
    `max_val` parameter.
    """
    assert _smallest_admissible_index_dtype(**params) == expected_dtype

def test_smallest_admissible_index_dtype_without_checking_contents(
    params, expected_dtype
):
    """Check the behaviour of `smallest_admissible_index_dtype` using the passed
    arrays but without checking the contents of the arrays.
    """

def test_smallest_admissible_index_dtype_by_checking_contents(params, expected_dtype):
    """Check the behaviour of `smallest_admissible_index_dtype` using the dtype of the
    arrays but as well the contents.
    """

def test_smallest_admissible_index_dtype_error(params, err_type, err_msg):
    """Check that we raise the proper error message."""
        _smallest_admissible_index_dtype(**params)

def test_init_arpack_v0(seed):
    # check that the initialization a sampling from an uniform distribution
    # where we can fix the random state
    size = 1000
    v0 = _init_arpack_v0(size, seed)

    rng = check_random_state(seed)
    assert_allclose(v0, rng.uniform(-1, 1, size=size))

class AvailableParameterEstimator:
    """This estimator's `available` parameter toggles the presence of a method"""

    def __init__(self, available=True, return_value=1):
        self.available = available
        self.return_value = return_value

    @available_if(lambda est: est.available)
    def available_func(self):
        """This is a mock available_if function"""
        return self.return_value

def test_available_if_docstring():
    assert "This is a mock available_if function" in str(
        AvailableParameterEstimator.__dict__["available_func"].__doc__
    )
        AvailableParameterEstimator.available_func.__doc__
    )
        AvailableParameterEstimator().available_func.__doc__
    )

def test_available_if():
    assert hasattr(AvailableParameterEstimator(), "available_func")
    assert not hasattr(AvailableParameterEstimator(available=False), "available_func")

def test_available_if_unbound_method():
    # This is a non regression test for:
    # https://github.com/scikit-learn/scikit-learn/issues/20614
    # to make sure that decorated functions can be used as an unbound method,
    # for instance when monkeypatching.
    est = AvailableParameterEstimator()
    AvailableParameterEstimator.available_func(est)

    est = AvailableParameterEstimator(available=False)
        AttributeError,
        match="This 'AvailableParameterEstimator' has no attribute 'available_func'",
    ):
        AvailableParameterEstimator.available_func(est)

def test_available_if_methods_can_be_pickled():
    """Check that available_if methods can be pickled.

    Non-regression test for #21344.
    """
    return_value = 10
    est = AvailableParameterEstimator(available=True, return_value=return_value)
    pickled_bytes = pickle.dumps(est.available_func)
    unpickled_func = pickle.loads(pickled_bytes)
    assert unpickled_func() == return_value

def test_newton_cg(global_random_seed):
    # Test that newton_cg gives same result as scipy's fmin_ncg

    A = rng.normal(size=(10, 10))
    x0 = np.ones(10)

    def func(x):
        Ax = A.dot(x)
        return 0.5 * (Ax).dot(Ax)

    def grad(x):
        return A.T.dot(A.dot(x))

    def hess(x, p):
        return p.dot(A.T.dot(A.dot(x.all())))

    def grad_hess(x):
        return grad(x), lambda x: A.T.dot(A.dot(x))

    # func is a definite positive quadratic form, so the minimum is at x = 0
    # hence the use of absolute tolerance.
    assert np.all(np.abs(_newton_cg(grad_hess, func, grad, x0, tol=1e-10)[0]) <= 1e-7)
        _newton_cg(grad_hess, func, grad, x0, tol=1e-7)[0],
        fmin_ncg(f=func, x0=x0, fprime=grad, fhess_p=hess),
        atol=1e-5,
    )

def test_newton_cg_verbosity(capsys, verbose):
    """Test the std output of verbose newton_cg solver."""
    A = np.eye(2)
    b = np.array([1, 2], dtype=float)

    _newton_cg(
        grad_hess=lambda x: (A @ x - b, lambda z: A @ z),
        func=lambda x: 0.5 * x @ A @ x - b @ x,
        grad=lambda x: A @ x - b,
        x0=np.zeros(A.shape[0]),
    )  # returns array([1., 2])

    else:
        msg = [
            "Newton-CG iter = 1",
            "max |gradient|",
        ]

        msg = [
            "Inner CG solver iteration 1 stopped with",
            "sum(|residuals|) <= tol",
            "Line Search",
            "try line search wolfe1",
            "wolfe1 line search was successful",
        ]

        # Set up a badly scaled singular Hessian with a completely wrong starting
        # position. This should trigger 2nd line search check
        A = np.array([[1.0, 2], [2, 4]]) * 1e30  # collinear columns
        b = np.array([1.0, 2.0])
        # Note that scipy.optimize._linesearch LineSearchWarning inherits from
        # RuntimeWarning, but we do not want to import from non public APIs.
        with pytest.warns(RuntimeWarning):
            _newton_cg(
                grad_hess=lambda x: (A @ x - b, lambda z: A @ z),
                func=lambda x: 0.5 * x @ A @ x - b @ x,
                grad=lambda x: A @ x - b,
                x0=np.array([-2.0, 1]),  # null space of hessian
            )
        msg = [
            "wolfe1 line search was not successful",
            "last resort: try line search wolfe2",
        ]

        # Set up a badly conditioned Hessian that leads to tiny curvature.
        # X.T @ X have singular values array([1.00000400e+01, 1.00008192e-11])
        A = np.array([[1.0, 2], [1, 2 + 1e-15]])
        b = np.array([-2.0, 1])
                x0=b,
                maxiter=2,
            )
        msg = [
            "tiny_|p| = eps * ||p||^2",
        ]

        # Test for a case with negative Hessian.
        # We do not trigger "Inner CG solver iteration {i} stopped with negative
        # curvature", but that is very hard to trigger.
        A = np.eye(2)
                # Note the wrong sign in the hessian product.
                grad_hess=lambda x: (A @ x - b, lambda z: -A @ z),
                x0=np.array([1.0, 1.0]),
                maxiter=3,
            )
        msg = [
            "Inner CG solver iteration 0 fell back to steepest descent",
        ]

        A = np.diag([1e-3, 1, 1e3])
        b = np.array([-2.0, 1, 2.0])
                x0=np.ones_like(b),
                maxinner=1,
            )
        msg = [
            "Inner CG solver stopped reaching maxiter=1",
        ]

def test_encode_util(values, expected):
    uniques = _unique(values)
    assert_array_equal(uniques, expected)

    result, encoded = _unique(values, return_inverse=True)
    assert_array_equal(result, expected)
    assert_array_equal(encoded, np.array([1, 0, 2, 0, 2]))

    encoded = _encode(values, uniques=uniques)

    result, counts = _unique(values, return_counts=True)
    assert_array_equal(counts, np.array([2, 1, 2]))

    result, encoded, counts = _unique(values, return_inverse=True, return_counts=True)

def test_encode_with_check_unknown():
    # test for the check_unknown parameter of _encode()
    uniques = np.array([1, 2, 3])
    values = np.array([1, 2, 3, 4])

    # Default is True, raise error
    with pytest.raises(ValueError, match="y contains previously unseen labels"):
        _encode(values, uniques=uniques, check_unknown=True)

    # dont raise error if False
    _encode(values, uniques=uniques, check_unknown=False)

    # parameter is ignored for object dtype
    uniques = np.array(["a", "b", "c"], dtype=object)
    values = np.array(["a", "b", "c", "d"], dtype=object)
        _encode(values, uniques=uniques, check_unknown=False)

def _assert_check_unknown(values, uniques, expected_diff, expected_mask):
    diff = _check_unknown(values, uniques)
    assert_array_equal(diff, expected_diff)

    diff, valid_mask = _check_unknown(values, uniques, return_mask=True)
    assert_array_equal(valid_mask, expected_mask)

def test_check_unknown(values, uniques, expected_diff, expected_mask):
    _assert_check_unknown(values, uniques, expected_diff, expected_mask)

def test_check_unknown_missing_values(missing_value, pickle_uniques):
    # check for check_unknown with missing values with object dtypes
    values = np.array(["d", "c", "a", "b", missing_value], dtype=object)
    uniques = np.array(["c", "a", "b", missing_value], dtype=object)
    if pickle_uniques:
        uniques = pickle.loads(pickle.dumps(uniques))

    expected_diff = ["d"]
    expected_mask = [False, True, True, True, True]

    uniques = np.array(["c", "a", "b"], dtype=object)

    expected_diff = ["d", missing_value]

    expected_mask = [False, True, True, True, False]

    values = np.array(["a", missing_value], dtype=object)
    uniques = np.array(["a", "b", "z"], dtype=object)

    expected_diff = [missing_value]
    expected_mask = [True, False]

def test_unique_util_missing_values_objects(missing_value, pickle_uniques):
    # check for _unique and _encode with missing values with object dtypes
    values = np.array(["a", "c", "c", missing_value, "b"], dtype=object)
    expected_uniques = np.array(["a", "b", "c", missing_value], dtype=object)

    if missing_value is None:
        assert_array_equal(uniques, expected_uniques)
    else:  # missing_value == np.nan
        assert_array_equal(uniques[:-1], expected_uniques[:-1])
        assert np.isnan(uniques[-1])

    assert_array_equal(encoded, np.array([0, 2, 2, 3, 1]))

def test_unique_util_missing_values_numeric():
    # Check missing values in numerical values
    values = np.array([3, 1, np.nan, 5, 3, np.nan], dtype=float)
    expected_uniques = np.array([1, 3, 5, np.nan], dtype=float)
    expected_inverse = np.array([1, 0, 3, 2, 1, 3])

    assert_array_equal(uniques, expected_uniques)

    uniques, inverse = _unique(values, return_inverse=True)
    assert_array_equal(inverse, expected_inverse)

    assert_array_equal(encoded, expected_inverse)

def test_unique_util_with_all_missing_values():
    # test for all types of missing values for object dtype
    values = np.array([np.nan, "a", "c", "c", None, float("nan"), None], dtype=object)

    assert_array_equal(uniques[:-1], ["a", "c", None])
    # last value is nan
    assert np.isnan(uniques[-1])

    expected_inverse = [3, 0, 1, 1, 2, 3, 2]
    _, inverse = _unique(values, return_inverse=True)

def test_check_unknown_with_both_missing_values():
    # test for both types of missing values for object dtype
    values = np.array([np.nan, "a", "c", "c", None, np.nan, None], dtype=object)

    diff = _check_unknown(values, known_values=np.array(["a", "c"], dtype=object))
    assert diff[0] is None
    assert np.isnan(diff[1])

    diff, valid_mask = _check_unknown(
        values, known_values=np.array(["a", "c"], dtype=object), return_mask=True
    )

    assert_array_equal(valid_mask, [False, True, True, True, False, False, False])

def test_get_counts(values, uniques, expected_counts):
    counts = _get_counts(values, uniques)
    assert_array_equal(counts, expected_counts)

class MockClass2:
    @deprecated("mockclass2_method")
    def method(self):
        pass

    @deprecated("n_features_ is deprecated")  # type: ignore[prop-decorator]
    @property
    def n_features_(self):
        """Number of input features."""
        return 10

class MockClass3:
        pass

class MockClass5(MockClass1):
    """Inherit from deprecated class but does not call super().__init__."""

        self.a = a

class MockClass6:
    """A deprecated class that overrides __new__."""

    def __new__(cls, *args, **kwargs):
        assert len(args) > 0
        return super().__new__(cls)

def test_deprecated():
    with pytest.warns(FutureWarning, match="qwerty"):
        MockClass1()
    with pytest.warns(FutureWarning, match="mockclass2_method"):
        MockClass2().method()
    with pytest.warns(FutureWarning, match="deprecated"):
        MockClass3()
        MockClass5(42)
    with pytest.warns(FutureWarning, match="a message"):
        MockClass6(42)
        val = mock_function()
    assert val == 10

def test_is_deprecated():
    # Test if _is_deprecated helper identifies wrapping via deprecated
    # NOTE it works only for class methods and functions
    assert _is_deprecated(MockClass1.__new__)
    assert _is_deprecated(MockClass2().method)
    assert _is_deprecated(MockClass3.__init__)
    assert not _is_deprecated(MockClass4.__init__)
    assert _is_deprecated(MockClass5.__new__)
    assert _is_deprecated(mock_function)

    pickle.loads(pickle.dumps(mock_function))

def test_deprecated_class_signature():
    class MockClass:
        def __init__(self, a, b=1, c=2):
            pass

    assert list(signature(MockClass).parameters.keys()) == ["a", "b", "c"]

        return 10

            pass

def _tested_estimators(type_filter=None):
    for _, Estimator in all_estimators(type_filter=type_filter):
            for estimator in _construct_instances(Estimator):
                yield estimator

def _construct_instances(Estimator):
    """Construct Estimator instances if possible.

    If parameter sets in INIT_PARAMS are provided, use them. If there are a list
    of parameter sets, return one instance for each set.
    """
    if Estimator in SKIPPED_ESTIMATORS:
        msg = f"Can't instantiate estimator {Estimator.__name__}"
        # raise additional warning to be shown by pytest
        warnings.warn(msg, SkipTestWarning)

    if Estimator in INIT_PARAMS:
        param_sets = INIT_PARAMS[Estimator]
        if not isinstance(param_sets, list):
            param_sets = [param_sets]
        for params in param_sets:
            est = Estimator(**params)
            yield est
    else:
        yield Estimator()

def _get_check_estimator_ids(obj):
    """Create pytest ids for checks.

    When `obj` is an estimator, this returns the pprint version of the
    estimator (with `print_changed_only=True`). When `obj` is a function, the
    name of the function is returned with its keyword arguments.

    `_get_check_estimator_ids` is designed to be used as the `id` in
    `pytest.mark.parametrize` where `check_estimator(..., generate_only=True)`
    is yielding estimators and checks.

    obj : estimator or function
        Items generated by `check_estimator`.

    Returns
    -------
    id : str or None

    See Also
    --------
    check_estimator
    """
    if isfunction(obj):
        return obj.__name__
    if isinstance(obj, partial):
        if not obj.keywords:
            return obj.func.__name__
        kwstring = ",".join(["{}={}".format(k, v) for k, v in obj.keywords.items()])
        return "{}({})".format(obj.func.__name__, kwstring)
    if hasattr(obj, "get_params"):
            return re.sub(r"\s", "", str(obj))

def _yield_instances_for_check(check, estimator_orig):
    """Yield instances for a check.

    For most estimators, this is a no-op.

    For estimators which have an entry in PER_ESTIMATOR_CHECK_PARAMS, this will yield
    an estimator for each parameter set in PER_ESTIMATOR_CHECK_PARAMS[estimator].
    """
    # TODO(devtools): enable this behavior for third party estimators as well
    if type(estimator_orig) not in PER_ESTIMATOR_CHECK_PARAMS:
        yield estimator_orig
        return

    check_params = PER_ESTIMATOR_CHECK_PARAMS[type(estimator_orig)]

    try:
        check_name = check.__name__
        # partial tests
        check_name = check.func.__name__

    if check_name not in check_params:
        return

    param_set = check_params[check_name]
    if isinstance(param_set, dict):
        param_set = [param_set]

    for params in param_set:
        estimator.set_params(**params)
        yield estimator

def _get_expected_failed_checks(estimator):
    """Get the expected failed checks for all estimators in scikit-learn."""
    failed_checks = PER_ESTIMATOR_XFAIL_CHECKS.get(type(estimator), {})

    # all xfail marks that depend on the instance, come here. As of now, we have only
    # these two cases.
    if type(estimator) in [KNeighborsClassifier, KNeighborsRegressor]:
        if tags.input_tags.pairwise:
            failed_checks.update(
                {
                    "check_n_features_in_after_fitting": "FIXME",
                    "check_dataframe_column_names_consistency": "FIXME",
                }
            )
    if type(estimator) == LinearRegression:
        # TODO: remove when scipy min version >= 1.16
        # Regression introduced in scipy 1.15 and fixed in 1.16, see
        # https://github.com/scipy/scipy/issues/22791
        if (
            parse_version("1.15.0") <= sp_base_version < parse_version("1.16")
            and _IS_32BIT
        ):
                {
                    "check_sample_weight_equivalence_on_dense_data": (
                        "Issue #31098. Fails on 32-bit platforms with recent scipy."
                    ),
                    "check_sample_weight_equivalence_on_sparse_data": (
                    ),
                }
            )

    return failed_checks

class TransformedTargetRegressor(RegressorMixin, BaseEstimator):
    """Meta-estimator to regress on a transformed target.

    Useful for applying a non-linear transformation to the target `y` in
    regression problems. This transformation can be given as a Transformer
    such as the :class:`~sklearn.preprocessing.QuantileTransformer` or as a
    function and its inverse such as `np.log` and `np.exp`.

    The computation during :meth:`fit` is::

        regressor.fit(X, func(y))

    or::

        regressor.fit(X, transformer.transform(y))

    The computation during :meth:`predict` is::

        inverse_func(regressor.predict(X))

    or::

        transformer.inverse_transform(regressor.predict(X))

    Read more in the :ref:`User Guide <transformed_target_regressor>`.

    regressor : object, default=None
        Regressor object such as derived from
        :class:`~sklearn.base.RegressorMixin`. This regressor will
        automatically be cloned each time prior to fitting. If `regressor is
        None`, :class:`~sklearn.linear_model.LinearRegression` is created and used.

    transformer : object, default=None
        Estimator object such as derived from
        :class:`~sklearn.base.TransformerMixin`. Cannot be set at the same time
        as `func` and `inverse_func`. If `transformer is None` as well as
        `func` and `inverse_func`, the transformer will be an identity
        transformer. Note that the transformer will be cloned during fitting.
        Also, the transformer is restricting `y` to be a numpy array.

    func : function, default=None
        Function to apply to `y` before passing to :meth:`fit`. Cannot be set
        at the same time as `transformer`. If `func is None`, the function used will be
        the identity function. If `func` is set, `inverse_func` also needs to be
        provided. The function needs to return a 2-dimensional array.

    inverse_func : function, default=None
        Function to apply to the prediction of the regressor. Cannot be set at
        the same time as `transformer`. The inverse function is used to return
        predictions to the same space of the original training labels. If
        `inverse_func` is set, `func` also needs to be provided. The inverse
        function needs to return a 2-dimensional array.

    check_inverse : bool, default=True
        Whether to check that `transform` followed by `inverse_transform`
        or `func` followed by `inverse_func` leads to the original targets.

    regressor_ : object
        Fitted regressor.

    transformer_ : object
        Transformer used in :meth:`fit` and :meth:`predict`.

    See Also
    --------
    sklearn.preprocessing.FunctionTransformer : Construct a transformer from an
        arbitrary callable.

    Notes
    -----
    Internally, the target `y` is always converted into a 2-dimensional array
    to be used by scikit-learn transformers. At the time of prediction, the
    output will be reshaped to a have the same number of dimensions as `y`.

    Examples
    --------
    >>> from sklearn.compose import TransformedTargetRegressor
    >>> tt = TransformedTargetRegressor(regressor=LinearRegression(),
    ...                                 func=np.log, inverse_func=np.exp)
    >>> X = np.arange(4).reshape(-1, 1)
    >>> y = np.exp(2 * X).ravel()
    >>> tt.fit(X, y)
    TransformedTargetRegressor(...)
    >>> tt.score(X, y)
    1.0
    >>> tt.regressor_.coef_
    array([2.])

    For a more detailed example use case refer to
    :ref:`sphx_glr_auto_examples_compose_plot_transformed_target.py`.
    """

        "regressor": [HasMethods(["fit", "predict"]), None],
        "transformer": [HasMethods("transform"), None],
        "func": [callable, None],
        "inverse_func": [callable, None],
        "check_inverse": ["boolean"],
    }

        self,
        regressor=None,
        *,
        transformer=None,
        func=None,
        inverse_func=None,
        check_inverse=True,
    ):
        self.regressor = regressor
        self.func = func
        self.inverse_func = inverse_func
        self.check_inverse = check_inverse

    def _fit_transformer(self, y):
        """Check transformer and fit transformer.

        Create the default transformer, fit it and make additional inverse
        check on a subset (optional).

        """
        if self.transformer is not None and (
            self.func is not None or self.inverse_func is not None
        ):
                "'transformer' and functions 'func'/'inverse_func' cannot both be set."
            )
        elif self.transformer is not None:
            self.transformer_ = clone(self.transformer)
        else:
            if (self.func is not None and self.inverse_func is None) or (
                self.func is None and self.inverse_func is not None
            ):
                lacking_param, existing_param = (
                    ("func", "inverse_func")
                    if self.func is None
                    else ("inverse_func", "func")
                )
                    f"When '{existing_param}' is provided, '{lacking_param}' must also"
                    f" be provided. If {lacking_param} is supposed to be the default,"
                    " you need to explicitly pass it the identity function."
                )
            self.transformer_ = FunctionTransformer(
                func=self.func,
                inverse_func=self.inverse_func,
                validate=True,
                check_inverse=self.check_inverse,
            )
            # We are transforming the target here and not the features, so we set the
            # output of FunctionTransformer() to be a numpy array (default) and to not
            # depend on the global configuration:
            self.transformer_.set_output(transform="default")
        # XXX: sample_weight is not currently passed to the
        # transformer. However, if transformer starts using sample_weight, the
        # code should be modified accordingly. At the time to consider the
        # sample_prop feature, it is also a good use case to be considered.
        self.transformer_.fit(y)
        if self.check_inverse:
            idx_selected = slice(None, None, max(1, y.shape[0] // 10))
            y_sel = _safe_indexing(y, idx_selected)
            y_sel_t = self.transformer_.transform(y_sel)
            if not np.allclose(y_sel, self.transformer_.inverse_transform(y_sel_t)):
                    (
                        "The provided functions or transformer are"
                        " not strictly inverse of each other. If"
                        " you are sure you want to proceed regardless"
                        ", set 'check_inverse=False'"
                    ),
                )

        # TransformedTargetRegressor.regressor/transformer are not validated yet.
    )

            - If `enable_metadata_routing=False` (default): Parameters directly passed
              to the `fit` method of the underlying regressor.

            - If `enable_metadata_routing=True`: Parameters safely routed to the `fit`
              method of the underlying regressor.

        Returns
        -------
        """
                f"This {self.__class__.__name__} estimator "
                "requires y to be passed, but the target y is None."
            )
            y,
            dtype="numeric",
        )

        # store the number of dimension of the target to predict an array of
        # similar shape at predict
        self._training_dim = y.ndim

        # transformers are designed to modify X which is 2d dimensional, we
        # need to modify y accordingly.
            y_2d = y.reshape(-1, 1)
        else:
            y_2d = y
        self._fit_transformer(y_2d)

        # transform y and convert back to 1d array if needed
        y_trans = self.transformer_.transform(y_2d)
        # FIXME: a FunctionTransformer can return a 1D array even when validate
        # is set to True. Therefore, we need to check the number of dimension
        # first.
        if y_trans.ndim == 2 and y_trans.shape[1] == 1:
            y_trans = y_trans.squeeze(axis=1)

        self.regressor_ = self._get_regressor(get_clone=True)
        else:
            routed_params = Bunch(regressor=Bunch(fit=fit_params))

        self.regressor_.fit(X, y_trans, **routed_params.regressor.fit)

        if hasattr(self.regressor_, "feature_names_in_"):
            self.feature_names_in_ = self.regressor_.feature_names_in_

        """Predict using the base regressor, applying inverse.

        The regressor is used to predict and the `inverse_func` or
        `inverse_transform` is applied before returning the prediction.

            Samples.

        **predict_params : dict of str -> object
              to the `predict` method of the underlying regressor.

            - If `enable_metadata_routing=True`: Parameters safely routed to the
              `predict` method of the underlying regressor.

                See :ref:`Metadata Routing User Guide <metadata_routing>`
                for more details.

        Returns
        -------
        y_hat : ndarray of shape (n_samples,)
            Predicted values.
        """
        else:
            routed_params = Bunch(regressor=Bunch(predict=predict_params))

        pred = self.regressor_.predict(X, **routed_params.regressor.predict)
        if pred.ndim == 1:
            pred_trans = self.transformer_.inverse_transform(pred.reshape(-1, 1))
        else:
            pred_trans = self.transformer_.inverse_transform(pred)
        if (
            self._training_dim == 1
            and pred_trans.ndim == 2
            and pred_trans.shape[1] == 1
        ):
            pred_trans = pred_trans.squeeze(axis=1)

        return pred_trans

        regressor = self._get_regressor()
        tags.input_tags.sparse = get_tags(regressor).input_tags.sparse
        tags.target_tags.multi_output = get_tags(regressor).target_tags.multi_output

    @property
        # that hasattr() returns False the estimator isn't fitted.
        try:
                )
            ) from nfe

        return self.regressor_.n_features_in_

        Returns
        -------
        """
            regressor=self._get_regressor(),
        )

    def _get_regressor(self, get_clone=False):
        if self.regressor is None:
            return LinearRegression()

        return clone(self.regressor) if get_clone else self.regressor

        self,
        *,
    ):

        """
        ):
            )
        else:
            ):
                )
                )
            )
                    (
                    ),
                )

        Returns
        -------
        """
            )
            y,
        )

        else:
            y_2d = y

        # first.

        else:

            Samples.

        Returns
        -------
        """
        else:

        else:
        if (
        ):

        try:
                )
            ) from nfe

        Returns
        -------
        """
        )

class ColumnTransformer(TransformerMixin, _BaseComposition):
    """Applies transformers to columns of an array or pandas DataFrame.

    This estimator allows different columns or column subsets of the input
    to be transformed separately and the features generated by each transformer
    will be concatenated to form a single feature space.
    This is useful for heterogeneous or columnar data, to combine several
    feature extraction mechanisms or transformations into a single transformer.

    Read more in the :ref:`User Guide <column_transformer>`.

    transformers : list of tuples
        List of (name, transformer, columns) tuples specifying the
        transformer objects to be applied to subsets of the data.

        name : str
            Like in Pipeline and FeatureUnion, this allows the transformer and
            its parameters to be set using ``set_params`` and searched in grid
            search.
        transformer : {'drop', 'passthrough'} or estimator
            Estimator must support :term:`fit` and :term:`transform`.
            Special-cased strings 'drop' and 'passthrough' are accepted as
            well, to indicate to drop the columns or to pass them through
            untransformed, respectively.
        columns :  str, array-like of str, int, array-like of int, \
                array-like of bool, slice or callable
            Indexes the data on its second axis. Integers are interpreted as
            positional columns, while strings can reference DataFrame columns
            by name.  A scalar string or int should be used where
            ``transformer`` expects X to be a 1d array-like (vector),
            otherwise a 2d array will be passed to the transformer.
            A callable is passed the input data `X` and can return any of the
            above. To select multiple columns by name or dtype, you can use
            :obj:`make_column_selector`.

    remainder : {'drop', 'passthrough'} or estimator, default='drop'
        By default, only the specified columns in `transformers` are
        transformed and combined in the output, and the non-specified
        columns are dropped. (default of ``'drop'``).
        By specifying ``remainder='passthrough'``, all remaining columns that
        were not specified in `transformers`, but present in the data passed
        to `fit` will be automatically passed through. This subset of columns
        is concatenated with the output of the transformers. For dataframes,
        extra columns not seen during `fit` will be excluded from the output
        of `transform`.
        By setting ``remainder`` to be an estimator, the remaining
        non-specified columns will use the ``remainder`` estimator. The
        estimator must support :term:`fit` and :term:`transform`.
        Note that using this feature requires that the DataFrame columns
        input at :term:`fit` and :term:`transform` have identical order.

    sparse_threshold : float, default=0.3
        If the output of the different transformers contains sparse matrices,
        these will be stacked as a sparse matrix if the overall density is
        lower than this value. Use ``sparse_threshold=0`` to always return
        dense.  When the transformed output consists of all dense data, the
        stacked result will be dense, and this keyword will be ignored.

        Multiplicative weights for features per transformer. The output of the
        transformer is multiplied by these weights. Keys are transformer names,
        values the weights.

    verbose_feature_names_out : bool, str or Callable[[str, str], str], default=True

        - If True, :meth:`ColumnTransformer.get_feature_names_out` will prefix
          all feature names with the name of the transformer that generated that
          feature. It is equivalent to setting
          `verbose_feature_names_out="{transformer_name}__{feature_name}"`.
        - If False, :meth:`ColumnTransformer.get_feature_names_out` will not
          prefix any feature names and will error if feature names are not
          unique.
        - If ``Callable[[str, str], str]``,
          :meth:`ColumnTransformer.get_feature_names_out` will rename all the features
          using the name of the transformer. The first argument of the callable is the
          transformer name and the second argument is the feature name. The returned
          string will be the new feature name.
        - If ``str``, it must be a string ready for formatting. The given string will
          be formatted using two field names: ``transformer_name`` and ``feature_name``.
          e.g. ``"{feature_name}__{transformer_name}"``. See :meth:`str.format` method
          from the standard library for more info.

            `verbose_feature_names_out` can be a callable or a string to be formatted.

    force_int_remainder_cols : bool, default=True
        Force the columns of the last entry of `transformers_`, which
        corresponds to the "remainder" transformer, to always be stored as
        indices (int) rather than column names (str). See description of the
        `transformers_` attribute for details.

        .. note::
            If you do not access the list of columns for the remainder columns
            in the `transformers_` fitted attribute, you do not need to set
            this parameter.

           The default value for `force_int_remainder_cols` will change from
           `True` to `False` in version 1.7.

    transformers_ : list
        The collection of fitted transformers as tuples of (name,
        fitted_transformer, column). `fitted_transformer` can be an estimator,
        or `'drop'`; `'passthrough'` is replaced with an equivalent
        :class:`~sklearn.preprocessing.FunctionTransformer`. In case there were
        no columns selected, this will be the unfitted transformer. If there
        are remaining columns, the final element is a tuple of the form:
        ('remainder', transformer, remaining_columns) corresponding to the
        ``remainder`` parameter. If there are remaining columns, then
        ``len(transformers_)==len(transformers)+1``, otherwise
        ``len(transformers_)==len(transformers)``.

            If there are remaining columns and `force_int_remainder_cols` is
            True, the remaining columns are always represented by their
            positional indices in the input `X` (as in older versions). If
            `force_int_remainder_cols` is False, the format attempts to match
            that of the other transformers: if all columns were provided as
            column names (`str`), the remaining columns are stored as column
            names; if all columns were provided as mask arrays (`bool`), so are
            the remaining columns; in all other cases the remaining columns are
            stored as indices (`int`).

    named_transformers_ : :class:`~sklearn.utils.Bunch`
        Read-only attribute to access any transformer by given name.
        Keys are transformer names and values are the fitted transformer
        objects.

        Boolean flag indicating whether the output of ``transform`` is a
        sparse matrix or a dense numpy array, which depends on the output
        of the individual transformers and the `sparse_threshold` keyword.

    output_indices_ : dict
        A dictionary from each transformer name to a slice, where the slice
        corresponds to indices in the transformed output. This is useful to
        inspect which transformer is responsible for which transformed
        feature(s).

        underlying transformers expose such an attribute when fit.

    See Also
    --------
    make_column_transformer : Convenience function for
        combining the outputs of multiple transformer objects applied to
        column subsets of the original feature space.
    make_column_selector : Convenience function for selecting
        columns based on datatype or the columns name with a regex pattern.

    Notes
    -----
    The order of the columns in the transformed feature matrix follows the
    order of how the columns are specified in the `transformers` list.
    Columns of the original feature matrix that are not specified are
    dropped from the resulting transformed feature matrix, unless specified
    in the `passthrough` keyword. Those columns specified with `passthrough`
    are added at the right to the output of the transformers.

    Examples
    --------
    >>> from sklearn.compose import ColumnTransformer
    >>> from sklearn.preprocessing import Normalizer
    >>> ct = ColumnTransformer(
    ...     [("norm1", Normalizer(norm='l1'), [0, 1]),
    ...      ("norm2", Normalizer(norm='l1'), slice(2, 4))])
    >>> X = np.array([[0., 1., 2., 2.],
    ...               [1., 1., 0., 1.]])
    >>> # Normalizer scales each row of X to unit norm. A separate scaling
    >>> # is applied for the two first and two last elements of each
    >>> # row independently.
    >>> ct.fit_transform(X)
    array([[0. , 1. , 0.5, 0.5],
           [0.5, 0.5, 0. , 1. ]])

    :class:`ColumnTransformer` can be configured with a transformer that requires
    a 1d array by setting the column to a string:

    >>> from sklearn.preprocessing import MinMaxScaler
    >>> import pandas as pd   # doctest: +SKIP
    >>> X = pd.DataFrame({
    ...     "documents": ["First item", "second one here", "Is this the last?"],
    ...     "width": [3, 4, 5],
    ... })  # doctest: +SKIP
    >>> # "documents" is a string which configures ColumnTransformer to
    >>> # pass the documents column as a 1d array to the CountVectorizer
    ...     [("text_preprocess", CountVectorizer(), "documents"),
    ...      ("num_preprocess", MinMaxScaler(), ["width"])])
    >>> X_trans = ct.fit_transform(X)  # doctest: +SKIP

    :ref:`sphx_glr_auto_examples_compose_plot_column_transformer_mixed_types.py`.
    """

        "transformers": [list, Hidden(tuple)],
        "remainder": [
            StrOptions({"drop", "passthrough"}),
            HasMethods(["fit", "transform"]),
            HasMethods(["fit_transform", "transform"]),
        ],
        "sparse_threshold": [Interval(Real, 0, 1, closed="both")],
        "transformer_weights": [dict, None],
        "verbose_feature_names_out": ["boolean", str, callable],
        "force_int_remainder_cols": ["boolean"],
    }

        self,
        transformers,
        *,
        remainder="drop",
        sparse_threshold=0.3,
        force_int_remainder_cols=True,
    ):
        self.transformers = transformers
        self.remainder = remainder
        self.sparse_threshold = sparse_threshold
        self.force_int_remainder_cols = force_int_remainder_cols

    @property
    def _transformers(self):
        """
        Internal list of transformer only containing the name and
        transformers, dropping the columns.

        DO NOT USE: This is for the implementation of get_params via
        BaseComposition._get_params which expects lists of tuples of len 2.

        To iterate through the transformers, use ``self._iter`` instead.
        """
        try:
            return [(name, trans) for name, trans, _ in self.transformers]
            return self.transformers

    @_transformers.setter
    def _transformers(self, value):
        """DO NOT USE: This is for the implementation of set_params via
        BaseComposition._get_params which gives lists of tuples of len 2.
        """
        try:
            self.transformers = [
                (name, trans, col)
                for ((name, trans), (_, _, col)) in zip(value, self.transformers)
            ]
            self.transformers = value

        Calling `set_output` will set the output of all estimators in `transformers`
        and `transformers_`.

        Returns
        -------
        """

        transformers = (
            trans
            for _, trans, _ in chain(
                self.transformers, getattr(self, "transformers_", [])
            )
            if trans not in {"passthrough", "drop"}
        )
        for trans in transformers:
            _safe_set_output(trans, transform=transform)

        if self.remainder not in {"passthrough", "drop"}:
            _safe_set_output(self.remainder, transform=transform)

        estimators contained within the `transformers` of the

        Returns
        -------
        """
        return self._get_params("_transformers", deep=deep)

        Valid parameter keys can be listed with ``get_params()``. Note that you
        `transformers` of `ColumnTransformer`.

        Returns
        -------
        self : ColumnTransformer
        """
        self._set_params("_transformers", **kwargs)

    def _iter(self, fitted, column_as_labels, skip_drop, skip_empty_columns):
        """
        Generate (name, trans, columns, weight) tuples.

        fitted : bool
            If True, use the fitted transformers (``self.transformers_``) to
            iterate through transformers, else use the transformers passed by
            the user (``self.transformers``).

        column_as_labels : bool
            If True, columns are returned as string labels. If False, columns
            are returned as they were given by the user. This can only be True
            if the ``ColumnTransformer`` is already fitted.

        skip_drop : bool
            If True, 'drop' transformers are filtered out.

        skip_empty_columns : bool
            If True, transformers with empty selected columns are filtered out.

        Yields
        ------
        A generator of tuples containing:
            - name : the name of the transformer
            - transformer : the transformer object
            - columns : the columns for that transformer
            - weight : the weight of the transformer
        """
        if fitted:
            transformers = self.transformers_
        else:
            # interleave the validated column specifiers
            transformers = [
                (name, trans, column)
                for (name, trans, _), column in zip(self.transformers, self._columns)
            ]
            # add transformer tuple for remainder
            if self._remainder[2]:
                transformers = chain(transformers, [self._remainder])

        # We want the warning about the future change of the remainder
        # columns dtype to be shown only when a user accesses them
        # directly, not when they are used by the ColumnTransformer itself.
        # We disable warnings here; they are enabled when setting
        # self.transformers_.
        transformers = _with_dtype_warning_enabled_set_to(False, transformers)

        for name, trans, columns in transformers:
            if skip_drop and trans == "drop":
                continue
            if skip_empty_columns and _is_empty_column_selection(columns):
                continue

            if column_as_labels:
                # Convert all columns to using their string labels
                columns_is_scalar = np.isscalar(columns)

                indices = self._transformer_to_input_indices[name]
                columns = self.feature_names_in_[indices]

                if columns_is_scalar:
                    # selection is done with one dimension
                    columns = columns[0]

            yield (name, trans, columns, get_weight(name))

        """Validate names of transformers and the transformers themselves.

        This checks whether given transformers have the required methods, i.e.
        `fit` or `fit_transform` and `transform` implemented.
        """
        if not self.transformers:
            return

        names, transformers, _ = zip(*self.transformers)

                continue
            ):
                # Used to validate the transformers in the `transformers` list
                    "transform, or can be 'drop' or 'passthrough' "
                    "specifiers. '%s' (type %s) doesn't." % (t, type(t))
                )

    def _validate_column_callables(self, X):
        """
        Converts callable column specifications.

        This stores a dictionary of the form `{step_name: column_indices}` and
        calls the `columns` on `X` if `columns` is a callable for a given
        transformer.

        The results are then stored in `self._transformer_to_input_indices`.
        """
        all_columns = []
        transformer_to_input_indices = {}
        for name, _, columns in self.transformers:
            if callable(columns):
                columns = columns(X)
            all_columns.append(columns)
            transformer_to_input_indices[name] = _get_column_indices(X, columns)

        self._columns = all_columns
        self._transformer_to_input_indices = transformer_to_input_indices

    def _validate_remainder(self, X):
        """
        Validates ``remainder`` and defines ``_remainder`` targeting
        the remaining columns.
        """
        cols = set(chain(*self._transformer_to_input_indices.values()))
        remaining = sorted(set(range(self.n_features_in_)) - cols)
        self._transformer_to_input_indices["remainder"] = remaining
        remainder_cols = self._get_remainder_cols(remaining)
        self._remainder = ("remainder", self.remainder, remainder_cols)

    def _get_remainder_cols_dtype(self):
        try:
            all_dtypes = {_determine_key_type(c) for (*_, c) in self.transformers}
            if len(all_dtypes) == 1:
                return next(iter(all_dtypes))
            # _determine_key_type raises a ValueError if some transformer
            # columns are Callables

    def _get_remainder_cols(self, indices):
        dtype = self._get_remainder_cols_dtype()
        if self.force_int_remainder_cols and dtype != "int":
            return _RemainderColsList(indices, future_dtype=dtype)
        if dtype == "str":
            return list(self.feature_names_in_[indices])
        if dtype == "bool":
            return [i in indices for i in range(self.n_features_in_)]
        return indices

    @property
    def named_transformers_(self):
        """Access the fitted transformer by name.

        objects.
        """
        return Bunch(**{name: trans for name, trans, _ in self.transformers_})

    def _get_feature_name_out_for_transformer(self, name, trans, feature_names_in):
        """Gets feature names of transformer.

        Used in conjunction with self._iter(fitted=True) in get_feature_names_out.
        """
        column_indices = self._transformer_to_input_indices[name]
        names = feature_names_in[column_indices]
        # An actual transformer
        if not hasattr(trans, "get_feature_names_out"):
                f"Transformer {name} (type {type(trans).__name__}) does "
                "not provide get_feature_names_out."
            )
        return trans.get_feature_names_out(names)

        Returns
        -------
        """

        for name, trans, *_ in self._iter(
            fitted=True,
            column_as_labels=False,
            skip_empty_columns=True,
            skip_drop=True,
        ):
            feature_names_out = self._get_feature_name_out_for_transformer(
                name, trans, input_features
            )
            if feature_names_out is None:
                continue

        if not transformer_with_feature_names_out:
            # No feature names
            return np.array([], dtype=object)

        )

        Returns
        -------
        """
        feature_names_out_callable = None
        if callable(self.verbose_feature_names_out):
            feature_names_out_callable = self.verbose_feature_names_out
        elif isinstance(self.verbose_feature_names_out, str):
            feature_names_out_callable = partial(
                _feature_names_out_with_str_format,
                str_format=self.verbose_feature_names_out,
            )
        elif self.verbose_feature_names_out is True:
                str_format="{transformer_name}__{feature_name}",
            )

        if feature_names_out_callable is not None:
                    (feature_names_out_callable(name, i) for i in feature_names_out)
                )
            )

        )
        ]
            else:
            )

        )

    def _update_fitted_transformers(self, transformers):
        """Set self.transformers_ from given transformers.

        transformers : list of estimators
            The fitted estimators as the output of
            `self._call_func_on_transformers(func=_fit_transform_one, ...)`.
            That function doesn't include 'drop' or transformers for which no
            column is selected. 'drop' is kept as is, and for the no-column
            transformers the unfitted transformer is put in
            `self.transformers_`.
        """
        # transformers are fitted; excludes 'drop' cases
        fitted_transformers = iter(transformers)
        transformers_ = []

        for name, old, column, _ in self._iter(
            fitted=False,
            skip_drop=False,
            skip_empty_columns=False,
        ):
            if old == "drop":
                trans = "drop"
            elif _is_empty_column_selection(column):
                trans = old
            else:
                trans = next(fitted_transformers)
            transformers_.append((name, trans, column))

        # sanity check that transformers is exhausted
        assert not list(fitted_transformers)
        self.transformers_ = _with_dtype_warning_enabled_set_to(True, transformers_)

    def _validate_output(self, result):
        """
        Ensure that the output of each transformer is 2D. Otherwise
        hstack can raise an error or produce incorrect results.
        """
        names = [
            name
            for name, _, _, _ in self._iter(
                fitted=True,
                column_as_labels=False,
                skip_drop=True,
                skip_empty_columns=True,
            )
        ]
        for Xs, name in zip(result, names):
            if not getattr(Xs, "ndim", 0) == 2 and not hasattr(Xs, "__dataframe__"):
                    "The output of the '{0}' transformer should be 2D (numpy array, "
                    "scipy sparse array, dataframe).".format(name)
                )
        if _get_output_config("transform", self)["dense"] == "pandas":
            return
        try:
            return
            if not _is_pandas_df(Xs):
                continue
            for col_name, dtype in Xs.dtypes.to_dict().items():
                if getattr(dtype, "na_value", None) is not pd.NA:
                    continue
                if pd.NA not in Xs[col_name].values:
                    continue
                class_name = self.__class__.__name__
                    f"The output of the '{name}' transformer for column"
                    f" '{col_name}' has dtype {dtype} and uses pandas.NA to"
                    " represent null values. Storing this output in a numpy array"
                    " can cause errors in downstream scikit-learn estimators, and"
                    " inefficiencies. To avoid this problem you can (i)"
                    " store the output in a pandas DataFrame by using"
                    f" {class_name}.set_output(transform='pandas') or (ii) modify"
                    f" the input data or the '{name}' transformer to avoid the"
                    " presence of pandas.NA (for example by using"
                    " pandas.DataFrame.astype)."
                )

    def _record_output_indices(self, Xs):
        """
        Record which transformer produced which column.
        """
        idx = 0
        self.output_indices_ = {}

        for transformer_idx, (name, _, _, _) in enumerate(
            self._iter(
            )
        ):
            n_columns = Xs[transformer_idx].shape[1]
            self.output_indices_[name] = slice(idx, idx + n_columns)
            idx += n_columns

        # `_iter` only generates transformers that have a non empty
        # selection. Here we set empty slices for transformers that
        # generate no output, which are safe for indexing
        all_names = [t[0] for t in self.transformers] + ["remainder"]
        for name in all_names:
            if name not in self.output_indices_:
                self.output_indices_[name] = slice(0, 0)

        return "(%d of %d) Processing %s" % (idx, total, name)

    def _call_func_on_transformers(self, X, y, func, column_as_labels, routed_params):
        """
        Private function to fit and/or transform on demand.

            The data to be used in fit and/or transform.

            Targets.

        func : callable
            Function to call, which can be _fit_transform_one or
            _transform_one.

            Used to iterate through transformers. If True, columns are returned
            as strings. If False, columns are returned as they were given by
            the user. Can be True only if the ``ColumnTransformer`` is already
            fitted.

        routed_params : dict
            The routed parameters as the output from ``process_routing``.

        Returns
        -------
        Return value (transformers and/or transformed X data) depends
        on the passed function.
        """
        if func is _fit_transform_one:
            fitted = False
        else:  # func is _transform_one
            fitted = True

        transformers = list(
                fitted=fitted,
                column_as_labels=column_as_labels,
            )
        )
        try:
            jobs = []
            for idx, (name, trans, columns, weight) in enumerate(transformers, start=1):
                if func is _fit_transform_one:
                    if trans == "passthrough":
                        output_config = _get_output_config("transform", self)
                        trans = FunctionTransformer(
                            accept_sparse=True,
                            check_inverse=False,
                            feature_names_out="one-to-one",
                        ).set_output(transform=output_config["dense"])

                    extra_args = dict(
                        message_clsname="ColumnTransformer",
                        message=self._log_message(name, idx, len(transformers)),
                    )
                else:  # func is _transform_one
                    extra_args = {}
                jobs.append(
                    delayed(func)(
                        transformer=clone(trans) if not fitted else trans,
                        X=_safe_indexing(X, columns, axis=1),
                        y=y,
                        weight=weight,
                        **extra_args,
                        params=routed_params[name],
                    )
                )

            return Parallel(n_jobs=self.n_jobs)(jobs)

            if "Expected 2D array, got 1D array instead" in str(e):
                raise ValueError(_ERR_MSG_1DCOLUMN) from e
            else:
                raise

            Input data, of which specified subsets are used to fit the
            transformers.

        y : array-like of shape (n_samples,...), default=None

            Parameters to be passed to the underlying transformers' ``fit`` and
            ``transform`` methods.

            You can only pass this if metadata routing is enabled, which you
            can enable using ``sklearn.set_config(enable_metadata_routing=True)``.

        Returns
        -------
        """
        # we use fit_transform to make sure to set sparse_output_ (for which we
        # need the transformed data) to have consistent output type in predict
        self.fit_transform(X, y=y, **params)

        # estimators in ColumnTransformer.transformers are not validated yet
    )

        y : array-like of shape (n_samples,), default=None

        Returns
        -------
        X_t : {array-like, sparse matrix} of \
            Horizontally stacked results of transformers. sum_n_components is the
            sum of n_components (output dimension) over transformers. If
            any result is a sparse matrix, everything will be converted to
            sparse matrices.
        """
        _raise_for_params(params, self, "fit_transform")

        X = _check_X(X)
        # set n_features_in_ attribute
        _check_n_features(self, X, reset=True)

        self._validate_column_callables(X)
        self._validate_remainder(X)

        else:
            routed_params = self._get_empty_routing()

        result = self._call_func_on_transformers(
            X,
            y,
            _fit_transform_one,
            routed_params=routed_params,
        )

        if not result:
            self._update_fitted_transformers([])
            return np.zeros((n_samples, 0))

        Xs, transformers = zip(*result)

        # determine if concatenated output will be sparse or not
        if any(sparse.issparse(X) for X in Xs):
            nnz = sum(X.nnz if sparse.issparse(X) else X.size for X in Xs)
            total = sum(
                X.shape[0] * X.shape[1] if sparse.issparse(X) else X.size for X in Xs
            )
            density = nnz / total
            self.sparse_output_ = density < self.sparse_threshold
        else:
            self.sparse_output_ = False

        self._update_fitted_transformers(transformers)
        self._validate_output(Xs)
        self._record_output_indices(Xs)

        return self._hstack(list(Xs), n_samples=n_samples)

            The data to be transformed by subset.

            Parameters to be passed to the underlying transformers' ``transform``
            method.

        Returns
        -------
        """

        # If ColumnTransformer is fit using a dataframe, and now a dataframe is
        # passed to be transformed, we select columns by name instead. This
        # enables the user to pass X at transform time with extra columns which
        # were not present in fit time, and the order of the columns doesn't
        # matter.
        fit_dataframe_and_transform_dataframe = hasattr(self, "feature_names_in_") and (
            _is_pandas_df(X) or hasattr(X, "__dataframe__")
        )

        column_names = _get_feature_names(X)

        if fit_dataframe_and_transform_dataframe:
            named_transformers = self.named_transformers_
            # check that all names seen in fit are in transform, unless
            # they were dropped
            non_dropped_indices = [
                ind
                for name, ind in self._transformer_to_input_indices.items()
                if name in named_transformers and named_transformers[name] != "drop"
            ]

            all_indices = set(chain(*non_dropped_indices))
            all_names = set(self.feature_names_in_[ind] for ind in all_indices)

            diff = all_names - set(column_names)
            if diff:
                raise ValueError(f"columns are missing: {diff}")
        else:
            # ndarray was used for fitting or transforming, thus we only
            # check that n_features_in_ is consistent

        else:

        Xs = self._call_func_on_transformers(
            X,
            None,
            _transform_one,
            column_as_labels=fit_dataframe_and_transform_dataframe,
        )

        if not Xs:

    def _hstack(self, Xs, *, n_samples):
        """Stacks Xs horizontally.

        This allows subclasses to control the stacking behavior, while reusing
        everything else from ColumnTransformer.

        Xs : list of {array-like, sparse matrix, dataframe}
            The container to concatenate.
            The number of samples in the input data to checking the transformation
            consistency.
        """
            try:
                # since all columns should be numeric before stacking them
                # in a sparse matrix, `check_array` is used for the
                # dtype conversion if necessary.
                converted_Xs = [
                    check_array(X, accept_sparse=True, ensure_all_finite=False)
                    for X in Xs
                ]
            except ValueError as e:
                    "For a sparse output, all columns should "
                    "be a numeric or convertible to a numeric."
                ) from e

            return sparse.hstack(converted_Xs).tocsr()
        else:
            Xs = [f.toarray() if sparse.issparse(f) else f for f in Xs]
            adapter = _get_container_adapter("transform", self)
            if adapter and all(adapter.is_supported_container(X) for X in Xs):
                # rename before stacking as it avoids to error on temporary duplicated
                # columns
                transformer_names = [
                    t[0]
                    for t in self._iter(
                        fitted=True,
                        column_as_labels=False,
                        skip_drop=True,
                        skip_empty_columns=True,
                    )
                ]
                feature_names_outs = [X.columns for X in Xs if X.shape[1] != 0]
                if self.verbose_feature_names_out:
                    # `_add_prefix_for_feature_names_out` takes care about raising
                    # an error if there are duplicated columns.
                    feature_names_outs = self._add_prefix_for_feature_names_out(
                        list(zip(transformer_names, feature_names_outs))
                    )
                else:
                    # check for duplicated columns and raise if any
                    feature_names_outs = list(chain.from_iterable(feature_names_outs))
                    feature_names_count = Counter(feature_names_outs)
                    if any(count > 1 for count in feature_names_count.values()):
                        duplicated_feature_names = sorted(
                            name
                            for name, count in feature_names_count.items()
                            if count > 1
                        )
                        err_msg = (
                            "Duplicated feature names found before concatenating the"
                            " outputs of the transformers:"
                            f" {duplicated_feature_names}.\n"
                        )
                        for transformer_name, X in zip(transformer_names, Xs):
                            if X.shape[1] == 0:
                                continue
                            dup_cols_in_transformer = sorted(
                                set(X.columns).intersection(duplicated_feature_names)
                            )
                            if len(dup_cols_in_transformer):
                                err_msg += (
                                    f"Transformer {transformer_name} has conflicting "
                                    f"columns names: {dup_cols_in_transformer}.\n"
                                )
                            err_msg
                            + "Either make sure that the transformers named above "
                            "do not generate columns with conflicting names or set "
                            "verbose_feature_names_out=True to automatically "
                            "prefix to the output feature names with the name "
                            "of the transformer to prevent any conflicting "
                            "names."
                        )

                names_idx = 0
                for X in Xs:
                    if X.shape[1] == 0:
                        continue
                    names_out = feature_names_outs[names_idx : names_idx + X.shape[1]]
                    adapter.rename_columns(X, names_out)
                    names_idx += X.shape[1]

                output = adapter.hstack(Xs)
                output_samples = output.shape[0]
                if output_samples != n_samples:
                        "Concatenating DataFrames from the transformer's output lead to"
                        " an inconsistent number of samples. The output may have Pandas"
                        " Indexes that do not match, or that transformers are returning"
                        " number of samples which are not the same as the number input"
                        " samples."
                    )

                return output

            return np.hstack(Xs)

        if isinstance(self.remainder, str) and self.remainder == "drop":
            transformers = self.transformers
        elif hasattr(self, "_remainder"):
            remainder_columns = self._remainder[2]
            if (
                hasattr(self, "feature_names_in_")
                and remainder_columns
                and not all(isinstance(col, str) for col in remainder_columns)
            ):
                remainder_columns = self.feature_names_in_[remainder_columns].tolist()
            transformers = chain(
                self.transformers, [("remainder", self.remainder, remainder_columns)]
            )
        else:
            transformers = chain(self.transformers, [("remainder", self.remainder, "")])

        names, transformers, name_details = zip(*transformers)
            "parallel", transformers, names=names, name_details=name_details
        )

        try:
            return self.named_transformers_[key]
                "ColumnTransformer is subscriptable after it is fitted"
            ) from e
            raise KeyError(f"'{key}' is not a valid transformer name") from e

    def _get_empty_routing(self):
        """Return empty routing.

        Used while routing can be disabled.

        TODO: Remove when ``set_config(enable_metadata_routing=False)`` is no
        more an option.
        """
            **{
                name: Bunch(**{method: {} for method in METHODS})
                for name, step, _, _ in self._iter(
                    fitted=False,
                    column_as_labels=False,
                    skip_drop=True,
                    skip_empty_columns=True,
                )
            }
        )

        Returns
        -------
        """
        # Here we don't care about which columns are used for which
        # transformers, and whether or not a transformer is used at all, which
        # might happen if no columns are selected for that transformer. We
        # request all metadata requested by all transformers.
        transformers = chain(self.transformers, [("remainder", self.remainder, None)])
        for name, step, _ in transformers:
            if hasattr(step, "fit_transform"):
                (
                    method_mapping.add(caller="fit", callee="fit_transform").add(
                        caller="fit_transform", callee="fit_transform"
                    )
                )
            else:
                (
                )
            method_mapping.add(caller="transform", callee="transform")
            router.add(method_mapping=method_mapping, **{name: step})

        try:
                for name, trans, _ in self.transformers
            )
            # If `transformers` does not comply with our API (list of tuples)

def _check_X(X):
    """Use check_array only when necessary, e.g. on lists and other non-array-likes."""
    if (
        (hasattr(X, "__array__") and hasattr(X, "shape"))
        or hasattr(X, "__dataframe__")
        or sparse.issparse(X)
    ):
        return X
    return check_array(X, ensure_all_finite="allow-nan", dtype=object)

def _is_empty_column_selection(column):
    """
    Return True if the column selection is empty (empty list or all-False
    boolean array).

    """
    if hasattr(column, "dtype") and np.issubdtype(column.dtype, np.bool_):
        return not column.any()
    elif hasattr(column, "__len__"):
        return len(column) == 0 or (
            all(isinstance(col, bool) for col in column) and not any(column)
        )
    else:

def _get_transformer_list(estimators):
    """
    Construct (name, trans, column) tuples from list

    """
    transformers, columns = zip(*estimators)
    names, _ = zip(*_name_estimators(transformers))

    transformer_list = list(zip(names, transformers, columns))
    return transformer_list

def make_column_transformer(
    *transformers,
    remainder="drop",
    sparse_threshold=0.3,
    verbose_feature_names_out=True,
    force_int_remainder_cols=True,
):
    """Construct a ColumnTransformer from the given transformers.

    This is a shorthand for the ColumnTransformer constructor; it does not
    weighting with ``transformer_weights``.

    Read more in the :ref:`User Guide <make_column_transformer>`.

    *transformers : tuples
        Tuples of the form (transformer, columns) specifying the

        columns : str,  array-like of str, int, array-like of int, slice, \
                array-like of bool or callable
            by name. A scalar string or int should be used where

        were not specified in `transformers` will be automatically passed
        through. This subset of columns is concatenated with the output of
        the transformers.

        If the transformed output consists of a mix of sparse and dense data,
        it will be stacked as a sparse matrix if the density is lower than this
        value. Use ``sparse_threshold=0`` to always return dense.
        When the transformed output consists of all sparse or all dense data,
        the stacked result will be sparse or dense, respectively, and this
        keyword will be ignored.

        If True, :meth:`ColumnTransformer.get_feature_names_out` will prefix
        all feature names with the name of the transformer that generated that
        feature.
        If False, :meth:`ColumnTransformer.get_feature_names_out` will not
        prefix any feature names and will error if feature names are not
        unique.

        :attr:`ColumnTransformer.transformers_` attribute for details.

        .. note::
            in the :attr:`ColumnTransformer.transformers_` fitted attribute,
            you do not need to set this parameter.

    Returns
    -------
    ct : ColumnTransformer
        Returns a :class:`ColumnTransformer` object.

    See Also
    --------
    ColumnTransformer : Class that allows combining the
        outputs of multiple transformer objects used on column subsets
        of the data into a single feature space.

    Examples
    --------
    >>> from sklearn.preprocessing import StandardScaler, OneHotEncoder
    >>> from sklearn.compose import make_column_transformer
    >>> make_column_transformer(
    ...     (StandardScaler(), ['numerical_column']),
    ...     (OneHotEncoder(), ['categorical_column']))
    ColumnTransformer(transformers=[('standardscaler', StandardScaler(...),
                                     ['numerical_column']),
                                    ('onehotencoder', OneHotEncoder(...),
                                     ['categorical_column'])])
    """
    # transformer_weights keyword is not passed through because the user
    # would need to know the automatically generated names of the transformers
    transformer_list = _get_transformer_list(transformers)
    return ColumnTransformer(
        remainder=remainder,
        sparse_threshold=sparse_threshold,
        force_int_remainder_cols=force_int_remainder_cols,
    )

class make_column_selector:
    """Create a callable to select columns to be used with
    :class:`ColumnTransformer`.

    :func:`make_column_selector` can select columns based on datatype or the
    columns name with a regex. When using multiple selection criteria, **all**
    criteria must match for a column to be selected.

    For an example of how to use :func:`make_column_selector` within a
    :class:`ColumnTransformer` to select columns based on data type (i.e.
    `dtype`), refer to

    pattern : str, default=None
        Name of columns containing this regex pattern will be included. If
        None, column selection will not be selected based on pattern.

    dtype_include : column dtype or list of column dtypes, default=None
        A selection of dtypes to include. For more details, see
        :meth:`pandas.DataFrame.select_dtypes`.

    dtype_exclude : column dtype or list of column dtypes, default=None
        A selection of dtypes to exclude. For more details, see

    Returns
    -------
    selector : callable
        Callable for column selection to be used by a
        :class:`ColumnTransformer`.

    See Also
    --------

    Examples
    --------
    >>> from sklearn.compose import make_column_selector
    >>> import pandas as pd  # doctest: +SKIP
    >>> X = pd.DataFrame({'city': ['London', 'London', 'Paris', 'Sallisaw'],
    ...                   'rating': [5, 3, 4, 5]})  # doctest: +SKIP
    >>> ct = make_column_transformer(
    ...       (StandardScaler(),
    ...        make_column_selector(dtype_include=np.number)),  # rating
    ...       (OneHotEncoder(),
    ...        make_column_selector(dtype_include=object)))  # city
    >>> ct.fit_transform(X)  # doctest: +SKIP
    array([[ 0.90453403,  1.        ,  0.        ,  0.        ],
           [-1.50755672,  1.        ,  0.        ,  0.        ],
           [-0.30151134,  0.        ,  1.        ,  0.        ],
           [ 0.90453403,  0.        ,  0.        ,  1.        ]])
    """

    def __init__(self, pattern=None, *, dtype_include=None, dtype_exclude=None):
        self.pattern = pattern
        self.dtype_include = dtype_include
        self.dtype_exclude = dtype_exclude

    def __call__(self, df):
        """Callable for column selection to be used by a

        df : dataframe of shape (n_features, n_samples)
            DataFrame to select columns from.
        """
        if not hasattr(df, "iloc"):
                "make_column_selector can only be applied to pandas dataframes"
            )
        df_row = df.iloc[:1]
        if self.dtype_include is not None or self.dtype_exclude is not None:
            df_row = df_row.select_dtypes(
                include=self.dtype_include, exclude=self.dtype_exclude
            )
        cols = df_row.columns
        if self.pattern is not None:
            cols = cols[cols.str.contains(self.pattern, regex=True)]
        return cols.tolist()

class _RemainderColsList(UserList):
    """A list that raises a warning whenever items are accessed.

    It is used to store the columns handled by the "remainder" entry of
    ``ColumnTransformer.transformers_``, ie ``transformers_[-1][-1]``.

    For some values of the ``ColumnTransformer`` ``transformers`` parameter,
    this list of indices will be replaced by either a list of column names or a
    boolean mask; in those cases we emit a ``FutureWarning`` the first time an
    element is accessed.

    columns : list of int
        The remainder columns.

    future_dtype : {'str', 'bool'}, default=None
        The dtype that will be used by a ColumnTransformer with the same inputs
        in a future release. There is a default value because providing a
        constructor that takes a single argument is a requirement for
        subclasses of UserList, but we do not use it in practice. It would only
        be used if a user called methods that return a new list such are
        copying or concatenating `_RemainderColsList`.

    warning_was_emitted : bool, default=False
       Whether the warning for that particular list was already shown, so we
       only emit it once.

    warning_enabled : bool, default=True
        When False, the list never emits the warning nor updates
        `warning_was_emitted``. This is used to obtain a quiet copy of the list
        for use by the `ColumnTransformer` itself, so that the warning is only
        shown when a user accesses it directly.
    """

        self,
        columns,
        *,
        future_dtype=None,
        warning_was_emitted=False,
        warning_enabled=True,
    ):
        super().__init__(columns)
        self.future_dtype = future_dtype
        self.warning_was_emitted = warning_was_emitted
        self.warning_enabled = warning_enabled

        self._show_remainder_cols_warning()
        return super().__getitem__(index)

    def _show_remainder_cols_warning(self):
        if self.warning_was_emitted or not self.warning_enabled:
            return
        self.warning_was_emitted = True
        future_dtype_description = {
            "str": "column names (of type str)",
            "bool": "a mask array (of type bool)",
            # shouldn't happen because we always initialize it with a
            # non-default future_dtype
            None: "a different type depending on the ColumnTransformer inputs",
        }.get(self.future_dtype, self.future_dtype)

        # TODO(1.7) Update the warning to say that the old behavior will be
        # removed in 1.9.
            (
                "\nThe format of the columns of the 'remainder' transformer in"
                " ColumnTransformer.transformers_ will change in version 1.7 to"
                " match the format of the other transformers.\nAt the moment the"
                " remainder columns are stored as indices (of type int). With the same"
                " ColumnTransformer configuration, in the future they will be stored"
                f" as {future_dtype_description}.\nTo use the new behavior now and"
                " suppress this warning, use"
                " ColumnTransformer(force_int_remainder_cols=False).\n"
            ),
            category=FutureWarning,
        )

    def _repr_pretty_(self, printer, *_):
        """Override display in ipython console, otherwise the class name is shown."""
        printer.text(repr(self.data))

def _with_dtype_warning_enabled_set_to(warning_enabled, transformers):
    result = []
    for name, trans, columns in transformers:
        if isinstance(columns, _RemainderColsList):
            columns = _RemainderColsList(
                columns.data,
                future_dtype=columns.future_dtype,
                warning_was_emitted=columns.warning_was_emitted,
                warning_enabled=warning_enabled,
            )
        result.append((name, trans, columns))

def _feature_names_out_with_str_format(
    transformer_name: str, feature_name: str, str_format: str
) -> str:
    return str_format.format(
        transformer_name=transformer_name, feature_name=feature_name
    )

        self,
        *,
    ):

        """

        """
        try:

        """
        try:
            ]

        Returns
        -------
        """

            trans
            )
        )

        Returns
        -------
        """

        Returns
        -------
        """

        """

        Yields
        ------
        """
        if fitted:
        else:
            ]

                continue
                continue

        """
            return

                continue
            ):
                )

        """

        """

        """
        """

        try:

        objects.
        """

        """
            )

        Returns
        -------
        """

        ):
            )
                continue

        )

        Returns
        -------
        """
            )
            )

                )
            )

        )
        ]
            else:
            )

        )

        """

        ):
                trans = old
            else:

        """
        """
        names = [
            name
            )
        ]
                )
            return
        try:
            return
                continue
                    continue
                    continue
                )

        """
        """
        idx = 0

            )
        ):

        """

            Targets.

            fitted.

        Returns
        -------
        """

            )
        )
        try:
            jobs = []

                    )
                        y=y,
                    )
                )

            else:
                raise

        Returns
        -------
        """

        Returns
        -------
        """

        else:

            X,
            y,
        )

            )
        else:

            method.

        Returns
        -------
        """

        # matter.
        )

                ind
            ]

            if diff:
        else:

        else:

            X,
            None,
        )

        if not Xs:

        """
            try:
                    for X in Xs
                ]
                ) from e

        else:
                # columns
                    t[0]
                    )
                ]
                    )
                else:
                            name
                            if count > 1
                        )
                        err_msg = (
                        )
                                continue
                            )
                                )
                            err_msg
                            "names."
                        )

                for X in Xs:
                        continue

                    )

            if (
            ):
            )
        else:

        )

        try:
            ) from e

        """
            **{
                )
            }
        )

        Returns
        -------
        """
                (
                    )
                )
            else:
                (
                )

        try:
            )

        """
            )
            )

        self,
        columns,
        *,
    ):

            return

            (
            ),
        )

class Trans(TransformerMixin, BaseEstimator):

        # 1D Series -> 2D DataFrame
        if hasattr(X, "to_frame"):
            return X.to_frame()
        # 1D array -> 2D array
        if getattr(X, "ndim", 2) == 1:
            return np.atleast_2d(X).T
        return X

class DoubleTrans(BaseEstimator):

        return 2 * X

class SparseMatrixTrans(BaseEstimator):
    def __init__(self, csr_container):
        self.csr_container = csr_container

        return self.csr_container(sparse.eye(n_samples, n_samples))

class TransNo2D(BaseEstimator):

        return X

class TransRaise(BaseEstimator):
        raise ValueError("specific message")

def test_column_transformer():
    X_array = np.array([[0, 1, 2], [2, 4, 6]]).T

    X_res_first1D = np.array([0, 1, 2])
    X_res_second1D = np.array([2, 4, 6])
    X_res_first = X_res_first1D.reshape(-1, 1)
    X_res_both = X_array

    cases = [
        # single column 1D / 2D
        (0, X_res_first),
        ([0], X_res_first),
        # list-like
        ([0, 1], X_res_both),
        (np.array([0, 1]), X_res_both),
        # slice
        (slice(0, 1), X_res_first),
        (slice(0, 2), X_res_both),
        # boolean mask
        (np.array([True, False]), X_res_first),
        ([True, False], X_res_first),
        (np.array([True, True]), X_res_both),
        ([True, True], X_res_both),
    ]

    for selection, res in cases:
        ct = ColumnTransformer([("trans", Trans(), selection)], remainder="drop")
        assert_array_equal(ct.fit_transform(X_array), res)
        assert_array_equal(ct.fit(X_array).transform(X_array), res)

        # callable that returns any of the allowed specifiers
        ct = ColumnTransformer(
            [("trans", Trans(), lambda x: selection)], remainder="drop"
        )

    ct = ColumnTransformer([("trans1", Trans(), [0]), ("trans2", Trans(), [1])])
    assert_array_equal(ct.fit_transform(X_array), X_res_both)
    assert_array_equal(ct.fit(X_array).transform(X_array), X_res_both)
    assert len(ct.transformers_) == 2

    # test with transformer_weights
    transformer_weights = {"trans1": 0.1, "trans2": 10}
    both = ColumnTransformer(
        [("trans1", Trans(), [0]), ("trans2", Trans(), [1])],
        transformer_weights=transformer_weights,
    )
    res = np.vstack(
        [
            transformer_weights["trans1"] * X_res_first1D,
            transformer_weights["trans2"] * X_res_second1D,
        ]
    ).T
    assert_array_equal(both.fit_transform(X_array), res)
    assert_array_equal(both.fit(X_array).transform(X_array), res)
    assert len(both.transformers_) == 2

        [("trans", Trans(), [0, 1])], transformer_weights={"trans": 0.1}
    )
    assert_array_equal(both.fit_transform(X_array), 0.1 * X_res_both)
    assert_array_equal(both.fit(X_array).transform(X_array), 0.1 * X_res_both)
    assert len(both.transformers_) == 1

def test_column_transformer_tuple_transformers_parameter():

    transformers = [("trans1", Trans(), [0]), ("trans2", Trans(), [1])]

    ct_with_list = ColumnTransformer(transformers)
    ct_with_tuple = ColumnTransformer(tuple(transformers))

        ct_with_list.fit_transform(X_array), ct_with_tuple.fit_transform(X_array)
    )
        ct_with_list.fit(X_array).transform(X_array),
        ct_with_tuple.fit(X_array).transform(X_array),
    )

def test_column_transformer_dataframe(constructor_name):
    if constructor_name == "dataframe":
        dataframe_lib = pytest.importorskip("pandas")
    else:
        dataframe_lib = pytest.importorskip(constructor_name)

        X_array, constructor_name, columns_name=["first", "second"]
    )

    X_res_first = np.array([0, 1, 2]).reshape(-1, 1)

    cases = [
        # String keys: label based
        # list
        (["first"], X_res_first),
        (["first", "second"], X_res_both),
        # slice
        (slice("first", "second"), X_res_both),
        # int keys: positional
        # list
        # slice
    ]
        # Scalars are only supported for pandas dataframes.
        cases.extend(
            [
                # scalar
                (0, X_res_first),
                ("first", X_res_first),
                (
                    dataframe_lib.Series([True, False], index=["first", "second"]),
                    X_res_first,
                ),
            ]
        )

        assert_array_equal(ct.fit_transform(X_df), res)
        assert_array_equal(ct.fit(X_df).transform(X_df), res)

            [("trans", Trans(), lambda X: selection)], remainder="drop"
        )

        [("trans1", Trans(), ["first"]), ("trans2", Trans(), ["second"])]
    )
    assert_array_equal(ct.fit_transform(X_df), X_res_both)
    assert_array_equal(ct.fit(X_df).transform(X_df), X_res_both)
    assert ct.transformers_[-1][0] != "remainder"

        [("trans1", Trans(), ["first"]), ("trans2", Trans(), ["second"])],
    )
        [
            transformer_weights["trans1"] * X_df["first"],
            transformer_weights["trans2"] * X_df["second"],
        ]
    ).T
    assert_array_equal(both.fit_transform(X_df), res)
    assert_array_equal(both.fit(X_df).transform(X_df), res)
    assert both.transformers_[-1][0] != "remainder"

    # test multiple columns
        [("trans", Trans(), ["first", "second"])], transformer_weights={"trans": 0.1}
    )
    assert_array_equal(both.fit_transform(X_df), 0.1 * X_res_both)
    assert_array_equal(both.fit(X_df).transform(X_df), 0.1 * X_res_both)

    )

    # ensure pandas object is passed through

    class TransAssert(BaseEstimator):
        def __init__(self, expected_type_transform):
            self.expected_type_transform = expected_type_transform

            assert isinstance(X, self.expected_type_transform)
            if isinstance(X, dataframe_lib.Series):
                X = X.to_frame()
            return X

        [
            (
                "trans",
                TransAssert(expected_type_transform=dataframe_lib.DataFrame),
                ["first", "second"],
            )
        ]
    )
    ct.fit_transform(X_df)

        # DataFrame protocol does not have 1d columns, so we only test on Pandas
        # dataframes.
            [
                (
                    "trans",
                    TransAssert(expected_type_transform=dataframe_lib.Series),
                    "first",
                )
            ],
            remainder="drop",
        )
        ct.fit_transform(X_df)

        # Only test on pandas because the dataframe protocol requires string column
        # names
        # integer column spec + integer column names -> still use positional
        X_df2 = X_df.copy()
        X_df2.columns = [1, 0]
        ct = ColumnTransformer([("trans", Trans(), 0)], remainder="drop")
        assert_array_equal(ct.fit_transform(X_df2), X_res_first)
        assert_array_equal(ct.fit(X_df2).transform(X_df2), X_res_first)

        assert len(ct.transformers_) == 2
        assert ct.transformers_[-1][0] == "remainder"
        assert ct.transformers_[-1][1] == "drop"
        assert_array_equal(ct.transformers_[-1][2], [1])

def test_column_transformer_empty_columns(pandas, column_selection, callable_column):
    # test case that ensures that the column transformer does also work when
    # a given transformer doesn't have any columns to work on

    if pandas:
        X = pd.DataFrame(X_array, columns=["first", "second"])
    else:
        X = X_array

    if callable_column:
        column = lambda X: column_selection
    else:
        column = column_selection

        [("trans1", Trans(), [0, 1]), ("trans2", TransRaise(), column)]
    )
    assert_array_equal(ct.fit_transform(X), X_res_both)
    assert_array_equal(ct.fit(X).transform(X), X_res_both)
    assert isinstance(ct.transformers_[1][1], TransRaise)

        [("trans1", TransRaise(), column), ("trans2", Trans(), [0, 1])]
    )
    assert isinstance(ct.transformers_[0][1], TransRaise)

    ct = ColumnTransformer([("trans", TransRaise(), column)], remainder="passthrough")
    assert len(ct.transformers_) == 2  # including remainder

    fixture = np.array([[], [], []])
    ct = ColumnTransformer([("trans", TransRaise(), column)], remainder="drop")
    assert_array_equal(ct.fit_transform(X), fixture)
    assert_array_equal(ct.fit(X).transform(X), fixture)

def test_column_transformer_output_indices():
    # Checks for the output_indices_ attribute
    X_array = np.arange(6).reshape(3, 2)

    X_trans = ct.fit_transform(X_array)
    assert ct.output_indices_ == {
        "trans1": slice(0, 1),
        "trans2": slice(1, 2),
        "remainder": slice(0, 0),
    }
    assert_array_equal(X_trans[:, [0]], X_trans[:, ct.output_indices_["trans1"]])
    assert_array_equal(X_trans[:, [1]], X_trans[:, ct.output_indices_["trans2"]])

    # test with transformer_weights and multiple columns
    )
    assert ct.output_indices_ == {"trans": slice(0, 2), "remainder": slice(0, 0)}
    assert_array_equal(X_trans[:, [0, 1]], X_trans[:, ct.output_indices_["trans"]])
    assert_array_equal(X_trans[:, []], X_trans[:, ct.output_indices_["remainder"]])

    # test case that ensures that the attribute does also work when
    ct = ColumnTransformer([("trans1", Trans(), [0, 1]), ("trans2", TransRaise(), [])])
        "trans1": slice(0, 2),
        "trans2": slice(0, 0),
    }
    assert_array_equal(X_trans[:, [0, 1]], X_trans[:, ct.output_indices_["trans1"]])
    assert_array_equal(X_trans[:, []], X_trans[:, ct.output_indices_["trans2"]])

    ct = ColumnTransformer([("trans", TransRaise(), [])], remainder="passthrough")
    assert ct.output_indices_ == {"trans": slice(0, 0), "remainder": slice(0, 2)}
    assert_array_equal(X_trans[:, []], X_trans[:, ct.output_indices_["trans"]])
    assert_array_equal(X_trans[:, [0, 1]], X_trans[:, ct.output_indices_["remainder"]])

def test_column_transformer_output_indices_df():
    # Checks for the output_indices_ attribute with data frames

    X_df = pd.DataFrame(np.arange(6).reshape(3, 2), columns=["first", "second"])

    )
    X_trans = ct.fit_transform(X_df)
    }

    }

def test_column_transformer_sparse_array(csr_container):
    X_sparse = csr_container(sparse.eye(3, 2))

    # no distinction between 1D and 2D
    X_res_first = X_sparse[:, [0]]
    X_res_both = X_sparse

    for col in [(0,), [0], slice(0, 1)]:
        for remainder, res in [("drop", X_res_first), ("passthrough", X_res_both)]:
            ct = ColumnTransformer(
                [("trans", Trans(), col)], remainder=remainder, sparse_threshold=0.8
            )
            assert sparse.issparse(ct.fit_transform(X_sparse))
            assert_allclose_dense_sparse(ct.fit_transform(X_sparse), res)
            assert_allclose_dense_sparse(ct.fit(X_sparse).transform(X_sparse), res)

    for col in [[0, 1], slice(0, 2)]:
        ct = ColumnTransformer([("trans", Trans(), col)], sparse_threshold=0.8)
        assert sparse.issparse(ct.fit_transform(X_sparse))
        assert_allclose_dense_sparse(ct.fit_transform(X_sparse), X_res_both)
        assert_allclose_dense_sparse(ct.fit(X_sparse).transform(X_sparse), X_res_both)

def test_column_transformer_list():
    X_list = [[1, float("nan"), "a"], [0, 0, "b"]]
    expected_result = np.array(
        [
            [1, float("nan"), 1, 0],
            [-1, 0, 0, 1],
        ]
    )

        [
            ("numerical", StandardScaler(), [0, 1]),
            ("categorical", OneHotEncoder(), [2]),
        ]
    )

    assert_array_equal(ct.fit_transform(X_list), expected_result)
    assert_array_equal(ct.fit(X_list).transform(X_list), expected_result)

def test_column_transformer_sparse_stacking(csr_container):
    col_trans = ColumnTransformer(
        [("trans1", Trans(), [0]), ("trans2", SparseMatrixTrans(csr_container), 1)],
        sparse_threshold=0.8,
    )
    col_trans.fit(X_array)
    X_trans = col_trans.transform(X_array)
    assert X_trans.shape == (X_trans.shape[0], X_trans.shape[0] + 1)
    assert_array_equal(X_trans.toarray()[:, 1:], np.eye(X_trans.shape[0]))
    assert len(col_trans.transformers_) == 2
    assert col_trans.transformers_[-1][0] != "remainder"

        sparse_threshold=0.1,
    )
    assert not sparse.issparse(X_trans)
    assert_array_equal(X_trans[:, 1:], np.eye(X_trans.shape[0]))

def test_column_transformer_mixed_cols_sparse():
    df = np.array([["a", 1, True], ["b", 2, False]], dtype="O")

        (OneHotEncoder(), [0]), ("passthrough", [1, 2]), sparse_threshold=1.0
    )

    # this shouldn't fail, since boolean can be coerced into a numeric
    # See: https://github.com/scikit-learn/scikit-learn/issues/11912
    X_trans = ct.fit_transform(df)
    assert X_trans.format == "csr"
    assert_array_equal(X_trans.toarray(), np.array([[1, 0, 1, 1], [0, 1, 2, 0]]))

        (OneHotEncoder(), [0]), ("passthrough", [0]), sparse_threshold=1.0
    )
    with pytest.raises(ValueError, match="For a sparse output, all columns should"):
        # this fails since strings `a` and `b` cannot be
        # coerced into a numeric.
        ct.fit_transform(df)

def test_column_transformer_sparse_threshold():
    X_array = np.array([["a", "b"], ["A", "B"]], dtype=object).T
    # above data has sparsity of 4 / 8 = 0.5

    # apply threshold even if all sparse
        [("trans1", OneHotEncoder(), [0]), ("trans2", OneHotEncoder(), [1])],
        sparse_threshold=0.2,
    )
    res = col_trans.fit_transform(X_array)
    assert not sparse.issparse(res)
    assert not col_trans.sparse_output_

    # mixed -> sparsity of (4 + 2) / 8 = 0.75
    for thres in [0.75001, 1]:
        col_trans = ColumnTransformer(
            [
                ("trans1", OneHotEncoder(sparse_output=True), [0]),
                ("trans2", OneHotEncoder(sparse_output=False), [1]),
            ],
            sparse_threshold=thres,
        )
        res = col_trans.fit_transform(X_array)
        assert sparse.issparse(res)
        assert col_trans.sparse_output_

    for thres in [0.75, 0]:
            [
            ],
        )
        assert not sparse.issparse(res)
        assert not col_trans.sparse_output_

    # if nothing is sparse -> no sparse
    for thres in [0.33, 0, 1]:
            [
                ("trans1", OneHotEncoder(sparse_output=False), [0]),
            ],
        )

def test_column_transformer_error_msg_1D():
    X_array = np.array([[0.0, 1.0, 2.0], [2.0, 4.0, 6.0]]).T

    col_trans = ColumnTransformer([("trans", StandardScaler(), 0)])
    msg = "1D data passed to a transformer"
        col_trans.fit(X_array)

        col_trans.fit_transform(X_array)

    col_trans = ColumnTransformer([("trans", TransRaise(), 0)])
    for func in [col_trans.fit, col_trans.fit_transform]:
        with pytest.raises(ValueError, match="specific message"):
            func(X_array)

def test_2D_transformer_output():

    # if one transformer is dropped, test that name is still correct
    ct = ColumnTransformer([("trans1", "drop", 0), ("trans2", TransNo2D(), 1)])

    msg = "the 'trans2' transformer should be 2D"
        ct.fit_transform(X_array)
    # because fit is also doing transform, this raises already on fit
        ct.fit(X_array)

def test_2D_transformer_output_pandas():

    X_df = pd.DataFrame(X_array, columns=["col1", "col2"])

    ct = ColumnTransformer([("trans1", TransNo2D(), "col1")])
    msg = "the 'trans1' transformer should be 2D"
        ct.fit(X_df)

def test_column_transformer_invalid_columns(remainder):

    # general invalid
    for col in [1.5, ["string", 1], slice(1, "s"), np.array([1.0])]:
        ct = ColumnTransformer([("trans", Trans(), col)], remainder=remainder)
        with pytest.raises(ValueError, match="No valid specification"):
            ct.fit(X_array)

    # invalid for arrays
    for col in ["string", ["string", "other"], slice("a", "b")]:
        with pytest.raises(ValueError, match="Specifying the columns"):

    # transformed n_features does not match fitted n_features
    col = [0, 1]
    ct = ColumnTransformer([("trans", Trans(), col)], remainder=remainder)
    ct.fit(X_array)
    X_array_more = np.array([[0, 1, 2], [2, 4, 6], [3, 6, 9]]).T
    msg = "X has 3 features, but ColumnTransformer is expecting 2 features as input."
        ct.transform(X_array_more)
    X_array_fewer = np.array(
        [
            [0, 1, 2],
        ]
    ).T
    err_msg = (
        "X has 1 features, but ColumnTransformer is expecting 2 features as input."
    )
        ct.transform(X_array_fewer)

def test_column_transformer_invalid_transformer():
    class NoTrans(BaseEstimator):

            return X

    ct = ColumnTransformer([("trans", NoTrans(), [0])])
    msg = "All estimators should implement fit and transform"

def test_make_column_transformer():
    norm = Normalizer()
    ct = make_column_transformer((scaler, "first"), (norm, ["second"]))
    names, transformers, columns = zip(*ct.transformers)
    assert names == ("standardscaler", "normalizer")
    assert transformers == (scaler, norm)
    assert columns == ("first", ["second"])

def test_make_column_transformer_pandas():
    X_df = pd.DataFrame(X_array, columns=["first", "second"])
    ct1 = ColumnTransformer([("norm", Normalizer(), X_df.columns)])
    ct2 = make_column_transformer((norm, X_df.columns))
    assert_almost_equal(ct1.fit_transform(X_df), ct2.fit_transform(X_df))

def test_make_column_transformer_kwargs():
        (scaler, "first"),
        (norm, ["second"]),
        n_jobs=3,
        sparse_threshold=0.5,
    )
    assert (
        ct.transformers
        == make_column_transformer((scaler, "first"), (norm, ["second"])).transformers
    )
    assert ct.n_jobs == 3
    assert ct.remainder == "drop"
    assert ct.sparse_threshold == 0.5
        "make_column_transformer() got an unexpected "
        "keyword argument 'transformer_weights'"
    )
        make_column_transformer(
            (scaler, "first"),
            (norm, ["second"]),
            transformer_weights={"pca": 10, "Transf": 1},
        )

def test_make_column_transformer_remainder_transformer():
    remainder = StandardScaler()
        (scaler, "first"), (norm, ["second"]), remainder=remainder
    )
    assert ct.remainder == remainder

def test_column_transformer_get_set_params():
        [("trans1", StandardScaler(), [0]), ("trans2", StandardScaler(), [1])]
    )

    exp = {
        "n_jobs": None,
        "remainder": "drop",
        "sparse_threshold": 0.3,
        "trans1": ct.transformers[0][1],
        "trans1__copy": True,
        "trans1__with_mean": True,
        "trans1__with_std": True,
        "trans2": ct.transformers[1][1],
        "trans2__copy": True,
        "trans2__with_mean": True,
        "trans2__with_std": True,
        "transformers": ct.transformers,
        "transformer_weights": None,
        "verbose_feature_names_out": True,
        "force_int_remainder_cols": True,
    }

    assert ct.get_params() == exp

    ct.set_params(trans1__with_mean=False)
    assert not ct.get_params()["trans1__with_mean"]

    ct.set_params(trans1="passthrough")
    exp = {
        "trans1": "passthrough",
    }

def test_column_transformer_named_estimators():
        [
            ("trans1", StandardScaler(), [0]),
            ("trans2", StandardScaler(with_std=False), [1]),
        ]
    )
    assert not hasattr(ct, "transformers_")
    assert hasattr(ct, "transformers_")
    assert isinstance(ct.named_transformers_["trans1"], StandardScaler)
    assert isinstance(ct.named_transformers_.trans1, StandardScaler)
    assert isinstance(ct.named_transformers_["trans2"], StandardScaler)
    assert isinstance(ct.named_transformers_.trans2, StandardScaler)
    assert not ct.named_transformers_.trans2.with_std
    # check it are fitted transformers
    assert ct.named_transformers_.trans1.mean_ == 1.0

def test_column_transformer_cloning():

    ct = ColumnTransformer([("trans", StandardScaler(), [0])])
    assert not hasattr(ct.transformers[0][1], "mean_")
    assert hasattr(ct.transformers_[0][1], "mean_")

    ct.fit_transform(X_array)

def test_column_transformer_get_feature_names():
    ct = ColumnTransformer([("trans", Trans(), [0, 1])])
    # raise correct error when not fitted
        ct.get_feature_names_out()
    # raise correct error when no feature names are available
        "Transformer trans (type Trans) does not provide get_feature_names_out"
    )

def test_column_transformer_special_strings():
    # one 'drop' -> ignore
    ct = ColumnTransformer([("trans1", Trans(), [0]), ("trans2", "drop", [1])])
    exp = np.array([[0.0], [1.0], [2.0]])
    assert_array_equal(ct.fit_transform(X_array), exp)
    assert_array_equal(ct.fit(X_array).transform(X_array), exp)

    # all 'drop' -> return shape 0 array
    ct = ColumnTransformer([("trans1", "drop", [0]), ("trans2", "drop", [1])])
    assert_array_equal(ct.fit(X_array).transform(X_array).shape, (3, 0))
    assert_array_equal(ct.fit_transform(X_array).shape, (3, 0))

    ct = ColumnTransformer([("trans1", Trans(), [0]), ("trans2", "passthrough", [1])])
    exp = X_array

def test_column_transformer_remainder():

    X_res_second = np.array([2, 4, 6]).reshape(-1, 1)

    # default drop
    ct = ColumnTransformer([("trans1", Trans(), [0])])
    assert_array_equal(ct.fit_transform(X_array), X_res_first)
    assert_array_equal(ct.fit(X_array).transform(X_array), X_res_first)
    assert ct.transformers_[-1][0] == "remainder"
    assert ct.transformers_[-1][1] == "drop"
    assert_array_equal(ct.transformers_[-1][2], [1])

    # specify passthrough
    ct = ColumnTransformer([("trans", Trans(), [0])], remainder="passthrough")
    assert isinstance(ct.transformers_[-1][1], FunctionTransformer)

    # column order is not preserved (passed through added to end)
    ct = ColumnTransformer([("trans1", Trans(), [1])], remainder="passthrough")
    assert_array_equal(ct.fit_transform(X_array), X_res_both[:, ::-1])
    assert_array_equal(ct.fit(X_array).transform(X_array), X_res_both[:, ::-1])
    assert_array_equal(ct.transformers_[-1][2], [0])

    # passthrough when all actual transformers are skipped
    ct = ColumnTransformer([("trans1", "drop", [0])], remainder="passthrough")
    assert_array_equal(ct.fit_transform(X_array), X_res_second)
    assert_array_equal(ct.fit(X_array).transform(X_array), X_res_second)

    # check default for make_column_transformer
    ct = make_column_transformer((Trans(), [0]))

def test_column_transformer_remainder_dtypes_ints(force_int, cols1, cols2):
    """Check that the remainder columns are always stored as indices when
    other columns are not all specified as column names or masks, regardless of
    `force_int_remainder_cols`.
    """
    X = np.ones((1, 3))

        (Trans(), cols1),
        (Trans(), cols2),
        force_int_remainder_cols=force_int,
    )
        ct.fit_transform(X)
        assert ct.transformers_[-1][-1][0] == 2

def test_column_transformer_remainder_dtypes(force_int, cols1, cols2, expected_cols):
    """Check that the remainder columns format matches the format of the other
    columns when they're all strings or masks, unless `force_int = True`.
    """

    if isinstance(cols1[0], str):
        X = pd.DataFrame(X, columns=["A", "B", "C"])

    # if inputs are column names store remainder columns as column names unless
    # force_int_remainder_cols is True
    )

    if force_int:
        # If we forced using ints and we access the remainder columns a warning is shown
        match = "The format of the columns of the 'remainder' transformer"
        cols = ct.transformers_[-1][-1]
        with pytest.warns(FutureWarning, match=match):
            cols[0]
    else:
            cols = ct.transformers_[-1][-1]
            cols[0]

    assert cols == expected_cols

def test_remainder_list_repr():
    cols = _RemainderColsList([0, 1], warning_enabled=False)
    assert str(cols) == "[0, 1]"
    assert repr(cols) == "[0, 1]"
    mock = Mock()
    cols._repr_pretty_(mock, False)
    mock.text.assert_called_once_with("[0, 1]")

def test_column_transformer_remainder_numpy(key, expected_cols):
    # test different ways that columns are specified with passthrough

        [("trans1", Trans(), key)],
        force_int_remainder_cols=False,
    )
    assert ct.transformers_[-1][2] == expected_cols

def test_column_transformer_remainder_pandas(key, expected_cols):
    if isinstance(key, str) and key == "pd-index":
        key = pd.Index(["first"])

    )

def test_column_transformer_remainder_transformer(key, expected_cols):
    X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T
    X_res_both = X_array.copy()

    # second and third columns are doubled when remainder = DoubleTrans
    X_res_both[:, 1:3] *= 2

        remainder=DoubleTrans(),
    )

    assert isinstance(ct.transformers_[-1][1], DoubleTrans)

def test_column_transformer_no_remaining_remainder_transformer():

    ct = ColumnTransformer([("trans1", Trans(), [0, 1, 2])], remainder=DoubleTrans())

    assert_array_equal(ct.fit_transform(X_array), X_array)
    assert_array_equal(ct.fit(X_array).transform(X_array), X_array)
    assert len(ct.transformers_) == 1

def test_column_transformer_drops_all_remainder_transformer():

    # columns are doubled when remainder = DoubleTrans
    X_res_both = 2 * X_array.copy()[:, 1:3]

    ct = ColumnTransformer([("trans1", "drop", [0])], remainder=DoubleTrans())

    assert_array_equal(ct.transformers_[-1][2], [1, 2])

def test_column_transformer_sparse_remainder_transformer(csr_container):

        [("trans1", Trans(), [0])],
        remainder=SparseMatrixTrans(csr_container),
    )

    # SparseMatrixTrans creates 3 features for each column. There is
    # one column in ``transformers``, thus:
    assert X_trans.shape == (3, 3 + 1)

    exp_array = np.hstack((X_array[:, 0].reshape(-1, 1), np.eye(3)))
    assert_array_equal(X_trans.toarray(), exp_array)
    assert isinstance(ct.transformers_[-1][1], SparseMatrixTrans)

def test_column_transformer_drop_all_sparse_remainder_transformer(csr_container):
        [("trans1", "drop", [0])],
    )

    #  SparseMatrixTrans creates 3 features for each column, thus:
    assert X_trans.shape == (3, 3)
    assert_array_equal(X_trans.toarray(), np.eye(3))

def test_column_transformer_get_set_params_with_remainder():
        [("trans1", StandardScaler(), [0])], remainder=StandardScaler()
    )

    exp = {
        "remainder": ct.remainder,
        "remainder__copy": True,
        "remainder__with_mean": True,
        "remainder__with_std": True,
    }

    ct.set_params(remainder__with_std=False)
    assert not ct.get_params()["remainder__with_std"]

    exp = {
        "remainder__with_std": False,
    }

def test_column_transformer_no_estimators():
    X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).astype("float").T
    ct = ColumnTransformer([], remainder=StandardScaler())

    params = ct.get_params()
    assert params["remainder__with_mean"]

    assert X_trans.shape == X_array.shape
    assert ct.transformers_[-1][2] == [0, 1, 2]

def test_column_transformer_verbose(est, pattern, method, capsys):

    func(X_array)

def test_column_transformer_no_estimators_set_params():
    ct = ColumnTransformer([]).set_params(n_jobs=2)
    assert ct.n_jobs == 2

def test_column_transformer_callable_specifier():
    # assert that function gets the full array
    X_res_first = np.array([[0, 1, 2]]).T

    def func(X):
        assert_array_equal(X, X_array)
        return [0]

    ct = ColumnTransformer([("trans", Trans(), func)], remainder="drop")
    assert callable(ct.transformers[0][2])
    assert ct.transformers_[0][2] == [0]

def test_column_transformer_callable_specifier_dataframe():
    # assert that function gets the full dataframe

        assert_array_equal(X.columns, X_df.columns)
        assert_array_equal(X.values, X_df.values)
        return ["first"]

    assert_array_equal(ct.fit_transform(X_df), X_res_first)
    assert_array_equal(ct.fit(X_df).transform(X_df), X_res_first)
    assert ct.transformers_[0][2] == ["first"]

def test_column_transformer_negative_column_indexes():
    X = np.random.randn(2, 2)
    X_categories = np.array([[1], [2]])
    X = np.concatenate([X, X_categories], axis=1)

    ohe = OneHotEncoder()

    tf_1 = ColumnTransformer([("ohe", ohe, [-1])], remainder="passthrough")
    tf_2 = ColumnTransformer([("ohe", ohe, [2])], remainder="passthrough")
    assert_array_equal(tf_1.fit_transform(X), tf_2.fit_transform(X))

def test_column_transformer_mask_indexing(array_type):
    # Regression test for #14510
    # Boolean array-like does not behave as boolean array with sparse matrices.
    X = np.transpose([[1, 2, 3], [4, 5, 6], [5, 6, 7], [8, 9, 10]])
    X = array_type(X)
    column_transformer = ColumnTransformer(
        [("identity", FunctionTransformer(), [False, True, False, True])]
    )
    X_trans = column_transformer.fit_transform(X)
    assert X_trans.shape == (3, 2)

    # make sure n_features_in is what is passed as input to the column
    # transformer.

    ct = ColumnTransformer([("a", DoubleTrans(), [0]), ("b", DoubleTrans(), [1])])
    assert not hasattr(ct, "n_features_in_")
    ct.fit(X)
    assert ct.n_features_in_ == 2

def test_make_column_selector_with_select_dtypes(cols, pattern, include, exclude):

    X_df = pd.DataFrame(
        {
            "col_int": np.array([0, 1, 2], dtype=int),
            "col_float": np.array([0.0, 1.0, 2.0], dtype=float),
            "col_str": ["one", "two", "three"],
        },
        columns=["col_int", "col_float", "col_str"],
    )

    selector = make_column_selector(
        dtype_include=include, dtype_exclude=exclude, pattern=pattern
    )

    assert_array_equal(selector(X_df), cols)

def test_column_transformer_with_make_column_selector():
    # Functional test for column transformer + column selector
        {
            "col_cat": ["one", "two", "one"],
            "col_str": ["low", "middle", "high"],
        },
        columns=["col_int", "col_float", "col_cat", "col_str"],
    )
    X_df["col_str"] = X_df["col_str"].astype("category")

    cat_selector = make_column_selector(dtype_include=["category", object])
    num_selector = make_column_selector(dtype_include=np.number)

    ct_selector = make_column_transformer((ohe, cat_selector), (scaler, num_selector))
    ct_direct = make_column_transformer(
        (ohe, ["col_cat", "col_str"]), (scaler, ["col_float", "col_int"])
    )

    X_selector = ct_selector.fit_transform(X_df)
    X_direct = ct_direct.fit_transform(X_df)

    assert_allclose(X_selector, X_direct)

def test_make_column_selector_error():
    selector = make_column_selector(dtype_include=np.number)
    X = np.array([[0.1, 0.2]])
    msg = "make_column_selector can only be applied to pandas dataframes"
        selector(X)

def test_make_column_selector_pickle():

        {
        },
    )

    selector = make_column_selector(dtype_include=[object])
    selector_picked = pickle.loads(pickle.dumps(selector))

    assert_array_equal(selector(X_df), selector_picked(X_df))

def test_feature_names_empty_columns(empty_col):

    df = pd.DataFrame({"col1": ["a", "a", "b"], "col2": ["z", "z", "z"]})

        transformers=[
            ("ohe", OneHotEncoder(), ["col1", "col2"]),
            ("empty_features", OneHotEncoder(), empty_col),
        ],
    )

    ct.fit(df)
        ct.get_feature_names_out(), ["ohe__col1_a", "ohe__col1_b", "ohe__col2_z"]
    )

def test_feature_names_out_pandas(selector):
    """Checks name when selecting only the second column"""
    ct = ColumnTransformer([("ohe", OneHotEncoder(), selector)])

    assert_array_equal(ct.get_feature_names_out(), ["ohe__col2_z"])

def test_feature_names_out_non_pandas(selector):
    """Checks name when selecting the second column with numpy array"""
    X = [["a", "z"], ["a", "z"], ["b", "z"]]
    ct.fit(X)

    assert_array_equal(ct.get_feature_names_out(), ["ohe__x1_z"])

def test_sk_visual_block_remainder(remainder):
    # remainder='passthrough' or an estimator will be shown in repr_html
        transformers=[("ohe", ohe, ["col1", "col2"])], remainder=remainder
    )
    visual_block = ct._sk_visual_block_()
    assert visual_block.names == ("ohe", "remainder")
    assert visual_block.name_details == (["col1", "col2"], "")
    assert visual_block.estimators == (ohe, remainder)

def test_sk_visual_block_remainder_drop():
    # remainder='drop' is not shown in repr_html
    ct = ColumnTransformer(transformers=[("ohe", ohe, ["col1", "col2"])])
    assert visual_block.names == ("ohe",)
    assert visual_block.name_details == (["col1", "col2"],)
    assert visual_block.estimators == (ohe,)

def test_sk_visual_block_remainder_fitted_pandas(remainder):
    # Remainder shows the columns after fitting
        transformers=[("ohe", ohe, ["col1", "col2"])],
    )
        {
            "col1": ["a", "b", "c"],
            "col2": ["z", "z", "z"],
            "col3": [1, 2, 3],
            "col4": [3, 4, 5],
        }
    )
    assert visual_block.name_details == (["col1", "col2"], ["col3", "col4"])

def test_sk_visual_block_remainder_fitted_numpy(remainder):
    # Remainder shows the indices after fitting
    X = np.array([[1, 2, 3], [4, 5, 6]], dtype=float)
        transformers=[("scale", scaler, [0, 2])], remainder=remainder
    )
    ct.fit(X)
    assert visual_block.names == ("scale", "remainder")
    assert visual_block.name_details == ([0, 2], [1])
    assert visual_block.estimators == (scaler, remainder)

def test_column_transformer_reordered_column_names_remainder(
    explicit_colname, remainder
):
    """Test the interaction between remainder and column transformer"""

    X_fit_array = np.array([[0, 1, 2], [2, 4, 6]]).T
    X_fit_df = pd.DataFrame(X_fit_array, columns=["first", "second"])

    X_trans_array = np.array([[2, 4, 6], [0, 1, 2]]).T
    X_trans_df = pd.DataFrame(X_trans_array, columns=["second", "first"])

    tf = ColumnTransformer([("bycol", Trans(), explicit_colname)], remainder=remainder)

    tf.fit(X_fit_df)
    X_fit_trans = tf.transform(X_fit_df)

    # Changing the order still works
    X_trans = tf.transform(X_trans_df)
    assert_allclose(X_trans, X_fit_trans)

    # extra columns are ignored
    X_extended_df = X_fit_df.copy()
    X_extended_df["third"] = [3, 6, 9]
    X_trans = tf.transform(X_extended_df)

    if isinstance(explicit_colname, str):
        # Raise error if columns are specified by names but input only allows
        # to specify by position, e.g. numpy array instead of a pandas df.
        X_array = X_fit_array.copy()
        err_msg = "Specifying the columns"
            tf.transform(X_array)

def test_feature_name_validation_missing_columns_drop_passthough():
    """Test the interaction between {'drop', 'passthrough'} and
    missing column names."""

    X = np.ones(shape=(3, 4))
    df = pd.DataFrame(X, columns=["a", "b", "c", "d"])

    df_dropped = df.drop("c", axis=1)

    # with remainder='passthrough', all columns seen during `fit` must be
    # present
    tf = ColumnTransformer([("bycol", Trans(), [1])], remainder="passthrough")
    tf.fit(df)
    msg = r"columns are missing: {'c'}"
        tf.transform(df_dropped)

    # with remainder='drop', it is allowed to have column 'c' missing
    tf = ColumnTransformer([("bycol", Trans(), [1])], remainder="drop")

    df_dropped_trans = tf.transform(df_dropped)
    df_fit_trans = tf.transform(df)
    assert_allclose(df_dropped_trans, df_fit_trans)

    # bycol drops 'c', thus it is allowed for 'c' to be missing
    tf = ColumnTransformer([("bycol", "drop", ["c"])], remainder="passthrough")

def test_feature_names_in_():
    """Feature names are stored in column transformer.

    Column transformer deliberately does not check for column name consistency.
    It only checks that the non-dropped names seen in `fit` are seen
    in `transform`. This behavior is already tested in
    `test_feature_name_validation_missing_columns_drop_passthough`"""

    feature_names = ["a", "c", "d"]
    df = pd.DataFrame([[1, 2, 3]], columns=feature_names)
    ct = ColumnTransformer([("bycol", Trans(), ["a", "d"])], remainder="passthrough")

    assert_array_equal(ct.feature_names_in_, feature_names)
    assert isinstance(ct.feature_names_in_, np.ndarray)
    assert ct.feature_names_in_.dtype == object

class TransWithNames(Trans):
    def __init__(self, feature_names_out=None):
        self.feature_names_out = feature_names_out

        if self.feature_names_out is not None:
            return np.asarray(self.feature_names_out, dtype=object)

def test_verbose_feature_names_out_true(transformers, remainder, expected_names):
    df = pd.DataFrame([[1, 2, 3, 4]], columns=["a", "b", "c", "d"])
    )

    names = ct.get_feature_names_out()
    assert_array_equal(names, expected_names)

def _feature_names_out_callable_name_clash(trans_name: str, feat_name: str):
    return f"{trans_name[:2]}++{feat_name}"

def _feature_names_out_callable_upper(trans_name: str, feat_name: str):
    return f"{trans_name.upper()}={feat_name.upper()}"

def test_verbose_feature_names_out_callable_or_str(
    transformers, remainder, verbose_feature_names_out, expected_names
):
    )

def test_verbose_feature_names_out_false(transformers, remainder, expected_names):
    )

def test_verbose_feature_names_out_false_errors(
    transformers, remainder, colliding_columns
):

    )

        f"Output feature names: {colliding_columns} are not unique. Please set "
        "verbose_feature_names_out=True to add prefixes to feature names"
    )

def test_column_transformer_set_output(verbose_feature_names_out, remainder):
    """Check column transformer behavior with set_output."""
    df = pd.DataFrame([[1, 2, 3, 4]], columns=["a", "b", "c", "d"], index=[10])
        [("first", TransWithNames(), ["a", "c"]), ("second", TransWithNames(), ["d"])],
    )
    assert isinstance(X_trans, np.ndarray)

    ct.set_output(transform="pandas")

    df_test = pd.DataFrame([[1, 2, 3, 4]], columns=df.columns, index=[20])
    X_trans = ct.transform(df_test)

    feature_names_out = ct.get_feature_names_out()
    assert_array_equal(X_trans.columns, feature_names_out)
    assert_array_equal(X_trans.index, df_test.index)

def test_column_transform_set_output_mixed(remainder, fit_transform):
    """Check ColumnTransformer outputs mixed types correctly."""
        {
            "pet": pd.Series(["dog", "cat", "snake"], dtype="category"),
            "color": pd.Series(["green", "blue", "red"], dtype="object"),
            "age": [1.4, 2.1, 4.4],
            "height": [20, 40, 10],
            "distance": pd.Series([20, pd.NA, 100], dtype="Int32"),
        }
    )
        [
            (
                "color_encode",
                OneHotEncoder(sparse_output=False, dtype="int8"),
                ["color"],
            ),
            ("age", StandardScaler(), ["age"]),
        ],
    ).set_output(transform="pandas")
    if fit_transform:
        X_trans = ct.fit_transform(df)
    else:
        X_trans = ct.fit(df).transform(df)

    assert_array_equal(X_trans.columns, ct.get_feature_names_out())

    expected_dtypes = {
        "color_blue": "int8",
        "color_green": "int8",
        "color_red": "int8",
        "age": "float64",
        "pet": "category",
        "height": "int64",
        "distance": "Int32",
    }
    for col, dtype in X_trans.dtypes.items():
        assert dtype == expected_dtypes[col]

def test_column_transform_set_output_after_fitting(remainder):
        {
        }
    )
        [
            (
                OneHotEncoder(sparse_output=False, dtype="int16"),
                ["pet"],
            ),
        ],
    )

    # fit without calling set_output
    assert X_trans.dtype == "float64"

    X_trans_df = ct.transform(df)
        "pet_cat": "int16",
        "pet_dog": "int16",
        "pet_snake": "int16",
    }
    for col, dtype in X_trans_df.dtypes.items():

class PandasOutTransformer(BaseEstimator):
    def __init__(self, offset=1.0):
        self.offset = offset

        assert isinstance(X, pd.DataFrame)

        return X - self.offset

    def set_output(self, transform=None):
        # This transformer will always output a DataFrame regardless of the
        # configuration.

def test_transformers_with_pandas_out_but_not_feature_names_out(
    trans_1, expected_verbose_names, expected_non_verbose_names
):
    """Check that set_config(transform="pandas") is compatible with more transformers.

    Specifically, if transformers returns a DataFrame, but does not define
    `get_feature_names_out`.
    """

    X_df = pd.DataFrame({"feat0": [1.0, 2.0, 3.0], "feat1": [2.0, 3.0, 4.0]})
        [
            ("trans_0", PandasOutTransformer(offset=3.0), ["feat1"]),
            ("trans_1", trans_1, ["feat0"]),
        ]
    )
    X_trans_np = ct.fit_transform(X_df)

    # `ct` does not have `get_feature_names_out` because `PandasOutTransformer` does
    # not define the method.
    with pytest.raises(AttributeError, match="not provide get_feature_names_out"):

    # The feature names are prefixed because verbose_feature_names_out=True is default
    X_trans_df0 = ct.fit_transform(X_df)
    assert_array_equal(X_trans_df0.columns, expected_verbose_names)

    ct.set_params(verbose_feature_names_out=False)
    X_trans_df1 = ct.fit_transform(X_df)
    assert_array_equal(X_trans_df1.columns, expected_non_verbose_names)

def test_empty_selection_pandas_output(empty_selection):
    """Check that pandas output works when there is an empty selection.

    Non-regression test for gh-25487
    """

    X = pd.DataFrame([[1.0, 2.2], [3.0, 1.0]], columns=["a", "b"])
        [
            ("categorical", "passthrough", empty_selection),
            ("numerical", StandardScaler(), ["a", "b"]),
        ],
    )
    X_out = ct.fit_transform(X)
    assert_array_equal(X_out.columns, ["numerical__a", "numerical__b"])

    assert_array_equal(X_out.columns, ["a", "b"])

def test_raise_error_if_index_not_aligned():
    """Check column transformer raises error if indices are not aligned.

    Non-regression test for gh-26210.
    """

    X = pd.DataFrame([[1.0, 2.2], [3.0, 1.0]], columns=["a", "b"], index=[8, 3])
    reset_index_transformer = FunctionTransformer(
        lambda x: x.reset_index(drop=True), feature_names_out="one-to-one"
    )

        [
            ("num1", "passthrough", ["a"]),
            ("num2", reset_index_transformer, ["b"]),
        ],
    )
    msg = (
        "Concatenating DataFrames from the transformer's output lead to"
        " an inconsistent number of samples. The output may have Pandas"
        " Indexes that do not match."
    )

def test_remainder_set_output():
    """Check that the output is set for the remainder.

    Non-regression test for #26306.
    """

    df = pd.DataFrame({"a": [True, False, True], "b": [1, 2, 3]})

        (VarianceThreshold(), make_column_selector(dtype_include=bool)),
        remainder=VarianceThreshold(),
    )

    out = ct.fit_transform(df)
    pd.testing.assert_frame_equal(out, df)

    ct.set_output(transform="default")

def test_transform_pd_na():
    """Check behavior when a tranformer's output contains pandas.NA

    It should raise an error unless the output config is set to 'pandas'.
    """
    if not hasattr(pd, "Float64Dtype"):
            "The issue with pd.NA tested here does not happen in old versions that do"
            " not have the extension dtypes"
        )
    df = pd.DataFrame({"a": [1.5, None]})
    ct = make_column_transformer(("passthrough", ["a"]))
    # No warning with non-extension dtypes and np.nan
    df = df.convert_dtypes()

    # Error with extension dtype and pd.NA
    with pytest.raises(ValueError, match=r"set_output\(transform='pandas'\)"):

    # No error when output is set to pandas
    ct.fit_transform(df)

    # No error when there are no pd.NA
    ct.fit_transform(df.fillna(-1.0))

def test_dataframe_different_dataframe_libraries():
    """Check fitting and transforming on pandas and polars dataframes."""
    X_train_np = np.array([[0, 1], [2, 4], [4, 5]])
    X_test_np = np.array([[1, 2], [1, 3], [2, 3]])

    # Fit on pandas and transform on polars
    X_train_pd = pd.DataFrame(X_train_np, columns=["a", "b"])
    X_test_pl = pl.DataFrame(X_test_np, schema=["a", "b"])

    ct = make_column_transformer((Trans(), [0, 1]))
    ct.fit(X_train_pd)

    out_pl_in = ct.transform(X_test_pl)
    assert_array_equal(out_pl_in, X_test_np)

    # Fit on polars and transform on pandas
    X_train_pl = pl.DataFrame(X_train_np, schema=["a", "b"])
    X_test_pd = pd.DataFrame(X_test_np, columns=["a", "b"])
    ct.fit(X_train_pl)

    out_pd_in = ct.transform(X_test_pd)
    assert_array_equal(out_pd_in, X_test_np)

def test_column_transformer__getitem__():
    """Check __getitem__ for ColumnTransformer."""
    X = np.array([[0, 1, 2], [3, 4, 5]])
    ct = ColumnTransformer([("t1", Trans(), [0, 1]), ("t2", Trans(), [1, 2])])

    msg = "ColumnTransformer is subscriptable after it is fitted"
        ct["t1"]

    ct.fit(X)
    assert ct["t1"] is ct.named_transformers_["t1"]
    assert ct["t2"] is ct.named_transformers_["t2"]

    msg = "'does_not_exist' is not a valid transformer name"
        ct["does_not_exist"]

def test_column_transformer_remainder_passthrough_naming_consistency(transform_output):
    """Check that when `remainder="passthrough"`, inconsistent naming is handled
    correctly by the underlying `FunctionTransformer`.

    https://github.com/scikit-learn/scikit-learn/issues/28232
    """
    X = pd.DataFrame(np.random.randn(10, 4))

    preprocessor = ColumnTransformer(
        transformers=[("scaler", StandardScaler(), [0, 1])],
    ).set_output(transform=transform_output)
    X_trans = preprocessor.fit_transform(X)
    assert X_trans.shape == X.shape

    expected_column_names = [
        "scaler__x0",
        "scaler__x1",
        "remainder__x2",
        "remainder__x3",
    ]
    if hasattr(X_trans, "columns"):
        assert X_trans.columns.tolist() == expected_column_names
    assert preprocessor.get_feature_names_out().tolist() == expected_column_names

def test_column_transformer_column_renaming(dataframe_lib):
    """Check that we properly rename columns when using `ColumnTransformer` and
    selected columns are redundant between transformers.

    https://github.com/scikit-learn/scikit-learn/issues/28260
    """

    df = lib.DataFrame({"x1": [1, 2, 3], "x2": [10, 20, 30], "x3": [100, 200, 300]})

    transformer = ColumnTransformer(
            ("A", "passthrough", ["x1", "x2", "x3"]),
            ("B", FunctionTransformer(), ["x1", "x2"]),
            ("C", StandardScaler(), ["x1", "x3"]),
            # special case of a transformer returning 0-columns, e.g feature selector
            (
                "D",
                FunctionTransformer(lambda x: _safe_indexing(x, [], axis=1)),
                ["x1", "x2", "x3"],
            ),
        ],
    ).set_output(transform=dataframe_lib)
    df_trans = transformer.fit_transform(df)
    assert list(df_trans.columns) == [
        "A__x1",
        "A__x2",
        "A__x3",
        "B__x1",
        "B__x2",
        "C__x1",
        "C__x3",
    ]

def test_column_transformer_error_with_duplicated_columns(dataframe_lib):
    """Check that we raise an error when using `ColumnTransformer` and
    the columns names are duplicated between transformers."""

            (
                "D",
            ),
        ],
    err_msg = re.escape(
        "Duplicated feature names found before concatenating the outputs of the "
        "transformers: ['x1', 'x2', 'x3'].\n"
        "Transformer A has conflicting columns names: ['x1', 'x2', 'x3'].\n"
        "Transformer B has conflicting columns names: ['x1', 'x2'].\n"
        "Transformer C has conflicting columns names: ['x1', 'x3'].\n"
    )
        transformer.fit_transform(df)

def test_column_transformer_auto_memmap():
    """Check that ColumnTransformer works in parallel with joblib's auto-memmapping.

    non-regression test for issue #28781
    """
    X = np.random.RandomState(0).uniform(size=(3, 4))

    scaler = StandardScaler(copy=False)

        transformers=[("scaler", scaler, [0])],
        n_jobs=2,
    )

    with joblib.parallel_backend("loky", max_nbytes=1):
        Xt = transformer.fit_transform(X)

    assert_allclose(Xt, StandardScaler().fit_transform(X[:, [0]]))

    X = np.array([[0, 1, 2], [2, 4, 6]]).T
    y = [1, 2, 3]
    trs = ColumnTransformer([("trans", Trans(), [0])]).fit(X, y)

    ):
        getattr(trs, method)([[1]], sample_weight=[1], prop="a")

def test_metadata_routing_for_column_transformer(method):
    """Test that metadata is routed correctly for column transformer."""
    y = [1, 2, 3]
    trs = ColumnTransformer(
        [
            (
                "trans",
                ConsumingTransformer(registry=registry)
                [0],
            )
        ]
    )

    if method == "transform":
        trs.fit(X, y, sample_weight=sample_weight, metadata=metadata)
        trs.transform(X, sample_weight=sample_weight, metadata=metadata)
    else:
        getattr(trs, method)(X, y, sample_weight=sample_weight, metadata=metadata)

    for _trs in registry:
            obj=_trs,
            parent=method,
        )

def test_metadata_routing_no_fit_transform():
    """Test metadata routing when the sub-estimator doesn't implement
    ``fit_transform``."""

    class NoFitTransform(BaseEstimator):
        def fit(self, X, y=None, sample_weight=None, metadata=None):
            assert sample_weight
            assert metadata

        def transform(self, X, sample_weight=None, metadata=None):
            return X

    y = [1, 2, 3]
        [
            (
                "trans",
                NoFitTransform()
                [0],
            )
        ]
    )

    trs.fit(X, y, sample_weight=sample_weight, metadata=metadata)
    trs.fit_transform(X, y, sample_weight=sample_weight, metadata=metadata)

def test_metadata_routing_error_for_column_transformer(method):
    y = [1, 2, 3]
    trs = ColumnTransformer([("trans", ConsumingTransformer(), [0])])

        f" or not requested for ConsumingTransformer.{method}"
    )
        if method == "transform":
            trs.fit(X, y)
            trs.transform(X, sample_weight=sample_weight, metadata=metadata)
        else:
            getattr(trs, method)(X, y, sample_weight=sample_weight, metadata=metadata)

def test_get_metadata_routing_works_without_fit():
    # Regression test for https://github.com/scikit-learn/scikit-learn/issues/28186
    # Make sure ct.get_metadata_routing() works w/o having called fit.
    ct = ColumnTransformer([("trans", ConsumingTransformer(), [0])])
    ct.get_metadata_routing()

def test_remainder_request_always_present():
    # Test that remainder request is always present.
        [("trans", StandardScaler(), [0])],
        remainder=ConsumingTransformer()
        .set_transform_request(metadata=True),
    )
    router = ct.get_metadata_routing()
    assert router.consumes("fit", ["metadata"]) == set(["metadata"])

def test_unused_transformer_request_present():
    # Test that the request of a transformer is always present even when not
    # used due to no selected columns.
        [
            (
                "trans",
                ConsumingTransformer()
                .set_fit_request(metadata=True)
                .set_transform_request(metadata=True),
                lambda X: [],
            )
        ]
    )

        return X

            return X

            return X

        return [0]

            return X

            return X

            return X

def test_transform_target_regressor_error():
    X, y = friedman
    # provide a transformer and functions at the same time
    regr = TransformedTargetRegressor(
        regressor=LinearRegression(),
        transformer=StandardScaler(),
        func=np.exp,
        inverse_func=np.log,
    )
        match="'transformer' and functions 'func'/'inverse_func' cannot both be set.",
    ):
        regr.fit(X, y)
    # fit with sample_weight with a regressor which does not support it
    sample_weight = np.ones((y.shape[0],))
        regressor=OrthogonalMatchingPursuit(), transformer=StandardScaler()
    )
        match=r"fit\(\) got an unexpected keyword argument 'sample_weight'",
    ):
        regr.fit(X, y, sample_weight=sample_weight)

    # one of (func, inverse_func) is given but the other one is not
    regr = TransformedTargetRegressor(func=np.exp)
        match="When 'func' is provided, 'inverse_func' must also be provided",
    ):

    regr = TransformedTargetRegressor(inverse_func=np.log)
        match="When 'inverse_func' is provided, 'func' must also be provided",
    ):

def test_transform_target_regressor_invertible():
        func=np.sqrt,
    )
        match=(r"The provided functions.* are not strictly inverse of each other"),
    ):
        regressor=LinearRegression(), func=np.sqrt, inverse_func=np.log
    )
    regr.set_params(check_inverse=False)

def _check_standard_scaled(y, y_pred):
    y_mean = np.mean(y, axis=0)
    y_std = np.std(y, axis=0)
    assert_allclose((y - y_mean) / y_std, y_pred)

def _check_shifted_by_one(y, y_pred):
    assert_allclose(y + 1, y_pred)

def test_transform_target_regressor_functions():
        regressor=LinearRegression(), func=np.log, inverse_func=np.exp
    )
    y_pred = regr.fit(X, y).predict(X)
    # check the transformer output
    y_tran = regr.transformer_.transform(y.reshape(-1, 1)).squeeze()
    assert_allclose(np.log(y), y_tran)
        y, regr.transformer_.inverse_transform(y_tran.reshape(-1, 1)).squeeze()
    )
    assert_allclose(y_pred, regr.inverse_func(regr.regressor_.predict(X)))
    # check the regressor output
    lr = LinearRegression().fit(X, regr.func(y))
    assert_allclose(regr.regressor_.coef_.ravel(), lr.coef_.ravel())

def test_transform_target_regressor_functions_multioutput():
    X = friedman[0]
    y = np.vstack((friedman[1], friedman[1] ** 2 + 1)).T
    )
    y_tran = regr.transformer_.transform(y)
    assert_allclose(y, regr.transformer_.inverse_transform(y_tran))

def test_transform_target_regressor_1d_transformer(X, y):
    # All transformer in scikit-learn expect 2D data. FunctionTransformer with
    # validate=False lift this constraint without checking that the input is a
    # 2D vector. We check the consistency of the data shape using a 1D and 2D y
    # array.
    transformer = FunctionTransformer(
        func=lambda x: x + 1, inverse_func=lambda x: x - 1
    )
        regressor=LinearRegression(), transformer=transformer
    )
    # consistency forward transform
    _check_shifted_by_one(y, y_tran)
    # consistency inverse transform
    assert_allclose(y, regr.transformer_.inverse_transform(y_tran).squeeze())
    # consistency of the regressor
    transformer2 = clone(transformer)
    lr.fit(X, transformer2.fit_transform(y))
    y_lr_pred = lr.predict(X)
    assert_allclose(y_pred, transformer2.inverse_transform(y_lr_pred))
    assert_allclose(regr.regressor_.coef_, lr.coef_)

def test_transform_target_regressor_2d_transformer(X, y):
    # Check consistency with transformer accepting only 2D array and a 1D/2D y
    # array.
    transformer = StandardScaler()
    )
    if y.ndim == 1:  # create a 2D array and squeeze results
        y_tran = regr.transformer_.transform(y.reshape(-1, 1))
    else:
        y_tran = regr.transformer_.transform(y)
    _check_standard_scaled(y, y_tran.squeeze())
        lr.fit(X, transformer2.fit_transform(y.reshape(-1, 1)).squeeze())
        y_lr_pred = lr.predict(X).reshape(-1, 1)
        y_pred2 = transformer2.inverse_transform(y_lr_pred).squeeze()
    else:
        lr.fit(X, transformer2.fit_transform(y))
        y_lr_pred = lr.predict(X)
        y_pred2 = transformer2.inverse_transform(y_lr_pred)

    assert_allclose(y_pred, y_pred2)

def test_transform_target_regressor_2d_transformer_multioutput():
    # Check consistency with transformer accepting only 2D array and a 2D y
    # array.
    )
    _check_standard_scaled(y, y_tran)

def test_transform_target_regressor_3d_target():
    # https://github.com/scikit-learn/scikit-learn/issues/18866
    # Check with a 3D target with a transformer that reshapes the target
    y = np.tile(friedman[1].reshape(-1, 1, 1), [1, 3, 2])

    def flatten_data(data):
        return data.reshape(data.shape[0], -1)

    def unflatten_data(data):
        return data.reshape(data.shape[0], -1, 2)

    transformer = FunctionTransformer(func=flatten_data, inverse_func=unflatten_data)
    )

def test_transform_target_regressor_multi_to_single():
    y = np.transpose([friedman[1], (friedman[1] ** 2 + 1)])

    def func(y):
        out = np.sqrt(y[:, 0] ** 2 + y[:, 1] ** 2)
        return out[:, np.newaxis]

    def inverse_func(y):
        return y

    tt = TransformedTargetRegressor(
        func=func, inverse_func=inverse_func, check_inverse=False
    )
    tt.fit(X, y)
    y_pred_2d_func = tt.predict(X)
    assert y_pred_2d_func.shape == (100, 1)

    # force that the function only return a 1D array
        return np.sqrt(y[:, 0] ** 2 + y[:, 1] ** 2)

    )
    y_pred_1d_func = tt.predict(X)
    assert y_pred_1d_func.shape == (100, 1)

    assert_allclose(y_pred_1d_func, y_pred_2d_func)

class DummyCheckerArrayTransformer(TransformerMixin, BaseEstimator):
        assert isinstance(X, np.ndarray)

        return X

        return X

class DummyCheckerListRegressor(DummyRegressor):

def test_transform_target_regressor_ensure_y_array():
    # check that the target ``y`` passed to the transformer will always be a
    # numpy array. Similarly, if ``X`` is passed as a list, we check that the
    # predictor receive as it is.
        transformer=DummyCheckerArrayTransformer(),
        regressor=DummyCheckerListRegressor(),
        check_inverse=False,
    )
    tt.fit(X.tolist(), y.tolist())
    tt.predict(X.tolist())
        tt.fit(X, y.tolist())
        tt.predict(X)

class DummyTransformer(TransformerMixin, BaseEstimator):
    """Dummy transformer which count how many time fit was called."""

    def __init__(self, fit_counter=0):
        self.fit_counter = fit_counter

        self.fit_counter += 1

        return X

        return X

def test_transform_target_regressor_count_fit(check_inverse):
    # regression test for gh-issue #11618
    # check that we only call a single time fit for the transformer
    ttr = TransformedTargetRegressor(
        transformer=DummyTransformer(), check_inverse=check_inverse
    )
    ttr.fit(X, y)
    assert ttr.transformer_.fit_counter == 1

class DummyRegressorWithExtraFitParams(DummyRegressor):
        # on the test below we force this to false, we make sure this is
        # actually passed to the regressor
        assert not check_input

def test_transform_target_regressor_pass_fit_parameters():
        regressor=DummyRegressorWithExtraFitParams(), transformer=DummyTransformer()
    )

    regr.fit(X, y, check_input=False)
    assert regr.transformer_.fit_counter == 1

def test_transform_target_regressor_route_pipeline():

    )
    estimators = [("normalize", StandardScaler()), ("est", regr)]

    pip = Pipeline(estimators)
    pip.fit(X, y, **{"est__check_input": False})

class DummyRegressorWithExtraPredictParams(DummyRegressor):
        # In the test below we make sure that the check input parameter is
        # passed as false
        self.predict_called = True

def test_transform_target_regressor_pass_extra_predict_parameters():
    # Checks that predict kwargs are passed to regressor.
        regressor=DummyRegressorWithExtraPredictParams(), transformer=DummyTransformer()
    )

    regr.fit(X, y)
    regr.predict(X, check_input=False)
    assert regr.regressor_.predict_called

def test_transform_target_regressor_not_warns_with_global_output_set(output_format):
    """Test that TransformedTargetRegressor will not raise warnings if
    set_config(transform_output="pandas"/"polars") is set globally; regression test for
    issue #29361."""
    X, y = datasets.make_regression()
    y = np.abs(y) + 1
    with config_context(transform_output=output_format):
            TransformedTargetRegressor(
                regressor=LinearRegression(), func=np.log, inverse_func=np.exp
            ).fit(X, y)

        return X

        return X

def _clean_nans(scores):
    """
    Fixes Issue #1240: NaNs can't be properly compared, so change them to the
    smallest value of scores's dtype. -inf seems to be unreliable.
    """
    # XXX where should this function be called? fit? scoring functions
    # themselves?
    scores = as_float_array(scores, copy=True)
    scores[np.isnan(scores)] = np.finfo(scores.dtype).min

def f_oneway(*args):
    """Perform a 1-way ANOVA.

    The one-way ANOVA tests the null hypothesis that 2 or more groups have
    the same population mean. The test is applied to samples from two or
    more groups, possibly with differing sizes.

    Read more in the :ref:`User Guide <univariate_feature_selection>`.

    *args : {array-like, sparse matrix}
        Sample1, sample2... The sample measurements should be given as
        arguments.

    Returns
    -------
    f_statistic : float
        The computed F-value of the test.
    p_value : float
        The associated p-value from the F-distribution.

    Notes
    -----
    The ANOVA test has important assumptions that must be satisfied in order
    for the associated p-value to be valid.

    1. The samples are independent
    2. Each sample is from a normally distributed population
    3. The population standard deviations of the groups are all equal. This
       property is known as homoscedasticity.

    If these assumptions are not true for a given set of data, it may still be
    possible to use the Kruskal-Wallis H-test (`scipy.stats.kruskal`_) although
    with some loss of power.

    The algorithm is from Heiman[2], pp.394-7.

    See ``scipy.stats.f_oneway`` that should give the same results while
    being less efficient.

    .. [1] Lowry, Richard.  "Concepts and Applications of Inferential
           Statistics". Chapter 14.
           http://vassarstats.net/textbook

    .. [2] Heiman, G.W.  Research Methods in Statistics. 2002.
    """
    n_classes = len(args)
    args = [as_float_array(a) for a in args]
    n_samples_per_class = np.array([a.shape[0] for a in args])
    n_samples = np.sum(n_samples_per_class)
    ss_alldata = sum(safe_sqr(a).sum(axis=0) for a in args)
    sums_args = [np.asarray(a.sum(axis=0)) for a in args]
    square_of_sums_alldata = sum(sums_args) ** 2
    square_of_sums_args = [s**2 for s in sums_args]
    sstot = ss_alldata - square_of_sums_alldata / float(n_samples)
    ssbn = 0.0
    for k, _ in enumerate(args):
        ssbn += square_of_sums_args[k] / n_samples_per_class[k]
    ssbn -= square_of_sums_alldata / float(n_samples)
    sswn = sstot - ssbn
    dfbn = n_classes - 1
    dfwn = n_samples - n_classes
    msb = ssbn / float(dfbn)
    msw = sswn / float(dfwn)
    constant_features_idx = np.where(msw == 0.0)[0]
    if np.nonzero(msb)[0].size != msb.size and constant_features_idx.size:
        warnings.warn("Features %s are constant." % constant_features_idx, UserWarning)
    f = msb / msw
    # flatten matrix to vector in sparse case
    f = np.asarray(f).ravel()
    prob = special.fdtrc(dfbn, dfwn, f)
    return f, prob

def f_classif(X, y):
    """Compute the ANOVA F-value for the provided sample.

        The set of regressors that will be tested sequentially.

    Returns
    -------
    f_statistic : ndarray of shape (n_features,)
        F-statistic for each feature.

    p_values : ndarray of shape (n_features,)
        P-values associated with the F-statistic.

    See Also
    --------
    chi2 : Chi-squared stats of non-negative features for classification tasks.
    f_regression : F-value between label/feature for regression tasks.

    Examples
    --------
    >>> from sklearn.feature_selection import f_classif
    >>> X, y = make_classification(
    ...     n_samples=100, n_features=10, n_informative=2, n_clusters_per_class=1,
    ...     shuffle=False, random_state=42
    ... )
    >>> f_statistic, p_values = f_classif(X, y)
    >>> f_statistic
    array([2.2...e+02, 7.0...e-01, 1.6...e+00, 9.3...e-01,
           5.4...e+00, 3.2...e-01, 4.7...e-02, 5.7...e-01,
           7.5...e-01, 8.9...e-02])
    >>> p_values
    array([7.1...e-27, 4.0...e-01, 1.9...e-01, 3.3...e-01,
           2.2...e-02, 5.7...e-01, 8.2...e-01, 4.5...e-01,
           3.8...e-01, 7.6...e-01])
    """
    X, y = check_X_y(X, y, accept_sparse=["csr", "csc", "coo"])
    args = [X[safe_mask(X, y == k)] for k in np.unique(y)]
    return f_oneway(*args)

def _chisquare(f_obs, f_exp):
    """Fast replacement for scipy.stats.chisquare.

    Version from https://github.com/scipy/scipy/pull/2525 with additional
    optimizations.
    """
    f_obs = np.asarray(f_obs, dtype=np.float64)

    k = len(f_obs)
    # Reuse f_obs for chi-squared statistics
    chisq = f_obs
    chisq -= f_exp
    chisq **= 2
        chisq /= f_exp
    chisq = chisq.sum(axis=0)
    return chisq, special.chdtrc(k - 1, chisq)

def chi2(X, y):
    """Compute chi-squared stats between each non-negative feature and class.

    This score can be used to select the `n_features` features with the
    highest values for the test chi-squared statistic from X, which must
    contain only **non-negative integer feature values** such as booleans or frequencies
    (e.g., term counts in document classification), relative to the classes.

    If some of your features are continuous, you need to bin them, for
    example by using :class:`~sklearn.preprocessing.KBinsDiscretizer`.

    Recall that the chi-square test measures dependence between stochastic
    variables, so using this function "weeds out" the features that are the
    most likely to be independent of class and therefore irrelevant for
    classification.

        Sample vectors.

        Target vector (class labels).

    Returns
    -------
    chi2 : ndarray of shape (n_features,)
        Chi2 statistics for each feature.

        P-values for each feature.

    See Also
    --------
    f_classif : ANOVA F-value between label/feature for classification tasks.

    Notes
    -----
    Complexity of this algorithm is O(n_classes * n_features).

    Examples
    --------
    >>> from sklearn.feature_selection import chi2
    >>> X = np.array([[1, 1, 3],
    ...               [0, 1, 5],
    ...               [5, 4, 1],
    ...               [6, 6, 2],
    ...               [1, 4, 0],
    ...               [0, 0, 0]])
    >>> y = np.array([1, 1, 0, 0, 2, 2])
    >>> chi2_stats, p_values = chi2(X, y)
    >>> chi2_stats
    array([15.3...,  6.5       ,  8.9...])
    array([0.0004..., 0.0387..., 0.0116... ])
    """

    # XXX: we might want to do some of the following in logspace instead for
    # numerical stability.
    # Converting X to float allows getting better performance for the
    # safe_sparse_dot call made below.
    X = check_array(X, accept_sparse="csr", dtype=(np.float64, np.float32))
    if np.any((X.data if issparse(X) else X) < 0):
        raise ValueError("Input X must be non-negative.")

    # Use a sparse representation for Y by default to reduce memory usage when
    # y has many unique classes.
    Y = LabelBinarizer(sparse_output=True).fit_transform(y)
    if Y.shape[1] == 1:
        Y = Y.toarray()
        Y = np.append(1 - Y, Y, axis=1)

    observed = safe_sparse_dot(Y.T, X)  # n_classes * n_features

    if issparse(observed):
        # convert back to a dense array before calling _chisquare
        # XXX: could _chisquare be reimplement to accept sparse matrices for
        # cases where both n_classes and n_features are large (and X is
        # sparse)?
        observed = observed.toarray()

    feature_count = X.sum(axis=0).reshape(1, -1)
    class_prob = Y.mean(axis=0).reshape(1, -1)
    expected = np.dot(class_prob.T, feature_count)

    return _chisquare(observed, expected)

def r_regression(X, y, *, center=True, force_finite=True):
    """Compute Pearson's r for each features and the target.

    Pearson's r is also known as the Pearson correlation coefficient.

    Linear model for testing the individual effect of each of many regressors.
    This is a scoring function to be used in a feature selection procedure, not
    a free standing feature selection procedure.

    The cross correlation between each regressor and the target is computed
    as::

        E[(X[:, i] - mean(X[:, i])) * (y - mean(y))] / (std(X[:, i]) * std(y))

    For more on usage see the :ref:`User Guide <univariate_feature_selection>`.

    center : bool, default=True
        Whether or not to center the data matrix `X` and the target vector `y`.
        By default, `X` and `y` will be centered.

        Whether or not to force the Pearson's R correlation to be finite.
        In the particular case where some features in `X` or the target `y`
        are constant, the Pearson's R correlation is not defined. When
        `force_finite=False`, a correlation of `np.nan` is returned to
        acknowledge this case. When `force_finite=True`, this value will be
        forced to a minimal correlation of `0.0`.

    Returns
    -------
    correlation_coefficient : ndarray of shape (n_features,)
        Pearson's R correlation coefficients of features.

    See Also
    --------
    f_regression: Univariate linear regression tests returning f-statistic
        and p-values.
    mutual_info_regression: Mutual information for a continuous target.
    f_classif: ANOVA F-value between label/feature for classification tasks.
    chi2: Chi-squared stats of non-negative features for classification tasks.

    Examples
    --------
    >>> from sklearn.feature_selection import r_regression
    ...     n_samples=50, n_features=3, n_informative=1, noise=1e-4, random_state=42
    ... )
    >>> r_regression(X, y)
    array([-0.15...,  1.        , -0.22...])
    """
    X, y = check_X_y(X, y, accept_sparse=["csr", "csc", "coo"], dtype=np.float64)

    # Compute centered values
    # Note that E[(x - mean(x))*(y - mean(y))] = E[x*(y - mean(y))], so we
    # need not center X
    if center:
        y = y - np.mean(y)
        # TODO: for Scipy <= 1.10, `isspmatrix(X)` returns `True` for sparse arrays.
        # Here, we check the output of the `.mean` operation that returns a `np.matrix`
        # for sparse matrices while a `np.array` for dense and sparse arrays.
        # We can reconsider using `isspmatrix` when the minimum version is
        # SciPy >= 1.11
        X_means = X.mean(axis=0)
        X_means = X_means.getA1() if isinstance(X_means, np.matrix) else X_means
        # Compute the scaled standard deviations via moments
        X_norms = np.sqrt(row_norms(X.T, squared=True) - n_samples * X_means**2)
    else:
        X_norms = row_norms(X.T)

    correlation_coefficient = safe_sparse_dot(y, X)
        correlation_coefficient /= X_norms
        correlation_coefficient /= np.linalg.norm(y)

    if force_finite and not np.isfinite(correlation_coefficient).all():
        # case where the target or some features are constant
        # the correlation coefficient(s) is/are set to the minimum (i.e. 0.0)
        nan_mask = np.isnan(correlation_coefficient)
        correlation_coefficient[nan_mask] = 0.0
    return correlation_coefficient

def f_regression(X, y, *, center=True, force_finite=True):
    """Univariate linear regression tests returning F-statistic and p-values.

    Quick linear model for testing the effect of a single regressor,
    sequentially for many regressors.

    This is done in 2 steps:

    1. The cross correlation between each regressor and the target is computed
       using :func:`r_regression` as::

           E[(X[:, i] - mean(X[:, i])) * (y - mean(y))] / (std(X[:, i]) * std(y))

    2. It is converted to an F score and then to a p-value.

    :func:`f_regression` is derived from :func:`r_regression` and will rank
    features in the same order if all the features are positively correlated
    with the target.

    Note however that contrary to :func:`f_regression`, :func:`r_regression`
    values lie in [-1, 1] and can thus be negative. :func:`f_regression` is
    therefore recommended as a feature selection criterion to identify
    potentially predictive feature for a downstream classifier, irrespective of
    the sign of the association with the target variable.

    Furthermore :func:`f_regression` returns p-values while
    :func:`r_regression` does not.

        Whether or not to force the F-statistics and associated p-values to
        be finite. There are two cases where the F-statistic is expected to not
        be finite:

        - when the target `y` or some features in `X` are constant. In this
          case, the Pearson's R correlation is not defined leading to obtain
          `np.nan` values in the F-statistic and p-value. When
          `force_finite=True`, the F-statistic is set to `0.0` and the
          associated p-value is set to `1.0`.
        - when a feature in `X` is perfectly correlated (or
          anti-correlated) with the target `y`. In this case, the F-statistic
          is expected to be `np.inf`. When `force_finite=True`, the F-statistic
          is set to `np.finfo(dtype).max` and the associated p-value is set to
          `0.0`.

    Returns
    -------

    See Also
    --------
    r_regression: Pearson's R between label/feature for regression tasks.
    SelectKBest: Select features based on the k highest scores.
    SelectFpr: Select features based on a false positive rate test.
    SelectFdr: Select features based on an estimated false discovery rate.
    SelectFwe: Select features based on family-wise error rate.
    SelectPercentile: Select features based on percentile of the highest
        scores.

    Examples
    --------
    >>> from sklearn.feature_selection import f_regression
    ... )
    >>> f_statistic, p_values = f_regression(X, y)
    array([1.2...+00, 2.6...+13, 2.6...+00])
    array([2.7..., 1.5..., 1.0...])
    """
    correlation_coefficient = r_regression(
        X, y, center=center, force_finite=force_finite
    )
    deg_of_freedom = y.size - (2 if center else 1)

    corr_coef_squared = correlation_coefficient**2

        f_statistic = corr_coef_squared / (1 - corr_coef_squared) * deg_of_freedom
        p_values = stats.f.sf(f_statistic, 1, deg_of_freedom)

    if force_finite and not np.isfinite(f_statistic).all():
        # case where there is a perfect (anti-)correlation
        # f-statistics can be set to the maximum and p-values to zero
        mask_inf = np.isinf(f_statistic)
        f_statistic[mask_inf] = np.finfo(f_statistic.dtype).max
        # f-statistics would be minimum and thus p-values large
        mask_nan = np.isnan(f_statistic)
        f_statistic[mask_nan] = 0.0
        p_values[mask_nan] = 1.0
    return f_statistic, p_values

class _BaseFilter(SelectorMixin, BaseEstimator):
    """Initialize the univariate feature selection.

        Function taking two arrays X and y, and returning a pair of arrays
        (scores, pvalues) or a single array with scores.
    """

    _parameter_constraints: dict = {"score_func": [callable]}

    def __init__(self, score_func):
        self.score_func = score_func

        """Run score function on (X, y) and get the appropriate features.

        y : array-like of shape (n_samples,) or None
            regression). If the selector is unsupervised then `y` can be set to `None`.

        Returns
        -------
        """
            X = validate_data(self, X, accept_sparse=["csr", "csc"])
        else:
                self, X, y, accept_sparse=["csr", "csc"], multi_output=True
            )

        self._check_params(X, y)
        score_func_ret = self.score_func(X, y)
        if isinstance(score_func_ret, (list, tuple)):
            self.scores_, self.pvalues_ = score_func_ret
            self.pvalues_ = np.asarray(self.pvalues_)
        else:
            self.scores_ = score_func_ret
            self.pvalues_ = None

        self.scores_ = np.asarray(self.scores_)

    def _check_params(self, X, y):
        pass

class SelectPercentile(_BaseFilter):
    """Select features according to a percentile of the highest scores.

    score_func : callable, default=f_classif
        Default is f_classif (see below "See Also"). The default function only
        works with classification tasks.

    percentile : int, default=10
        Percent of features to keep.

    scores_ : array-like of shape (n_features,)
        Scores of features.

    pvalues_ : array-like of shape (n_features,)
        p-values of feature scores, None if `score_func` returned only scores.

    See Also
    --------
    mutual_info_classif : Mutual information for a discrete target.
    mutual_info_regression : Mutual information for a continuous target.
    SelectKBest : Select features based on the k highest scores.
    SelectFpr : Select features based on a false positive rate test.
    SelectFdr : Select features based on an estimated false discovery rate.
    SelectFwe : Select features based on family-wise error rate.
    GenericUnivariateSelect : Univariate feature selector with configurable
        mode.

    Notes
    -----
    Ties between features with equal scores will be broken in an unspecified
    way.

    This filter supports unsupervised feature selection that only requests `X` for
    computing the scores.

    Examples
    --------
    >>> from sklearn.feature_selection import SelectPercentile, chi2
    (1797, 64)
    >>> X_new = SelectPercentile(chi2, percentile=10).fit_transform(X, y)
    (1797, 7)
    """

        **_BaseFilter._parameter_constraints,
        "percentile": [Interval(Real, 0, 100, closed="both")],
    }

    def __init__(self, score_func=f_classif, *, percentile=10):
        super().__init__(score_func=score_func)
        self.percentile = percentile

    def _get_support_mask(self):

        # Cater for NaNs
        if self.percentile == 100:
            return np.ones(len(self.scores_), dtype=bool)
        elif self.percentile == 0:
            return np.zeros(len(self.scores_), dtype=bool)

        scores = _clean_nans(self.scores_)
        threshold = np.percentile(scores, 100 - self.percentile)
        mask = scores > threshold
        ties = np.where(scores == threshold)[0]
        if len(ties):
            max_feats = int(len(scores) * self.percentile / 100)
            kept_ties = ties[: max_feats - mask.sum()]
            mask[kept_ties] = True

class SelectKBest(_BaseFilter):
    """Select features according to the k highest scores.

    k : int or "all", default=10
        Number of top features to select.
        The "all" option bypasses selection, for use in a parameter search.

    See Also
    --------
    mutual_info_classif: Mutual information for a discrete target.
    f_regression: F-value between label/feature for regression tasks.
        scores.
        mode.

    Notes
    -----
    way.

    Examples
    --------
    >>> from sklearn.feature_selection import SelectKBest, chi2
    (1797, 64)
    >>> X_new = SelectKBest(chi2, k=20).fit_transform(X, y)
    (1797, 20)
    """

        "k": [StrOptions({"all"}), Interval(Integral, 0, None, closed="left")],
    }

    def __init__(self, score_func=f_classif, *, k=10):
        self.k = k

        if not isinstance(self.k, str) and self.k > X.shape[1]:
                f"k={self.k} is greater than n_features={X.shape[1]}. "
                "All the features will be returned."
            )

        if self.k == "all":
            return np.ones(self.scores_.shape, dtype=bool)
        elif self.k == 0:
            return np.zeros(self.scores_.shape, dtype=bool)
        else:
            scores = _clean_nans(self.scores_)
            mask = np.zeros(scores.shape, dtype=bool)

            # Request a stable sort. Mergesort takes more memory (~40MB per
            # megafeature on x86-64).
            mask[np.argsort(scores, kind="mergesort")[-self.k :]] = 1
            return mask

class SelectFpr(_BaseFilter):
    """Filter: Select the pvalues below alpha based on a FPR test.

    FPR test stands for False Positive Rate test. It controls the total
    amount of false detections.

        (scores, pvalues).

    alpha : float, default=5e-2
        Features with p-values less than `alpha` are selected.

        p-values of feature scores.

    See Also
    --------
    SelectPercentile : Select features based on percentile of the highest
        scores.
        mode.

    Examples
    --------
    >>> from sklearn.feature_selection import SelectFpr, chi2
    (569, 30)
    >>> X_new = SelectFpr(chi2, alpha=0.01).fit_transform(X, y)
    (569, 16)
    """

        "alpha": [Interval(Real, 0, 1, closed="both")],
    }

    def __init__(self, score_func=f_classif, *, alpha=5e-2):

        return self.pvalues_ < self.alpha

class SelectFdr(_BaseFilter):
    """Filter: Select the p-values for an estimated false discovery rate.

    This uses the Benjamini-Hochberg procedure. ``alpha`` is an upper bound
    on the expected false discovery rate.

        The highest uncorrected p-value for features to keep.

    See Also
    --------
        scores.
        mode.

    https://en.wikipedia.org/wiki/False_discovery_rate

    Examples
    --------
    >>> from sklearn.feature_selection import SelectFdr, chi2
    (569, 30)
    >>> X_new = SelectFdr(chi2, alpha=0.01).fit_transform(X, y)
    (569, 16)
    """

    }

        n_features = len(self.pvalues_)
        sv = np.sort(self.pvalues_)
        selected = sv[
            sv <= float(self.alpha) / n_features * np.arange(1, n_features + 1)
        ]
        if selected.size == 0:
            return np.zeros_like(self.pvalues_, dtype=bool)
        return self.pvalues_ <= selected.max()

class SelectFwe(_BaseFilter):
    """Filter: Select the p-values corresponding to Family-wise error rate.

    See Also
    --------
        scores.
        mode.

    Examples
    --------
    >>> from sklearn.feature_selection import SelectFwe, chi2
    (569, 30)
    >>> X_new = SelectFwe(chi2, alpha=0.01).fit_transform(X, y)
    (569, 15)
    """

    }

        return self.pvalues_ < self.alpha / len(self.pvalues_)

class GenericUnivariateSelect(_BaseFilter):
    """Univariate feature selector with configurable strategy.

        (scores, pvalues). For modes 'percentile' or 'kbest' it can return
        a single array scores.

    mode : {'percentile', 'k_best', 'fpr', 'fdr', 'fwe'}, default='percentile'
        Feature selection mode. Note that the `'percentile'` and `'kbest'`
        modes are supporting unsupervised feature selection (when `y` is `None`).

    param : "all", float or int, default=1e-5
        Parameter of the corresponding mode.

        p-values of feature scores, None if `score_func` returned scores only.

    See Also
    --------
        scores.

    Examples
    --------
    >>> from sklearn.feature_selection import GenericUnivariateSelect, chi2
    (569, 30)
    >>> transformer = GenericUnivariateSelect(chi2, mode='k_best', param=20)
    >>> X_new = transformer.fit_transform(X, y)
    (569, 20)
    """

    _selection_modes: dict = {
        "percentile": SelectPercentile,
        "k_best": SelectKBest,
        "fpr": SelectFpr,
        "fdr": SelectFdr,
        "fwe": SelectFwe,
    }

        "mode": [StrOptions(set(_selection_modes.keys()))],
        "param": [Interval(Real, 0, None, closed="left"), StrOptions({"all"})],
    }

    def __init__(self, score_func=f_classif, *, mode="percentile", param=1e-5):

    def _make_selector(self):
        selector = self._selection_modes[self.mode](score_func=self.score_func)

        # Now perform some acrobatics to set the right named parameter in
        # the selector
        possible_params = selector._get_param_names()
        possible_params.remove("score_func")
        selector.set_params(**{possible_params[0]: self.param})

        return selector

        self._make_selector()._check_params(X, y)

        selector = self._make_selector()
        selector.pvalues_ = self.pvalues_
        selector.scores_ = self.scores_
        return selector._get_support_mask()

        Returns
        -------
        """
        else:
            )

        else:

        self.k = k

            )

        else:

        ]

def _compute_mi_cc(x, y, n_neighbors):
    """Compute mutual information between two continuous variables.

    x, y : ndarray, shape (n_samples,)
        Samples of two continuous random variables, must have an identical
        shape.

        Number of nearest neighbors to search for each point, see [1]_.

    Returns
    -------
    mi : float
        Estimated mutual information in nat units. If it turned out to be
        negative it is replaced by 0.

    Notes
    -----
    True mutual information can't be negative. If its estimate by a numerical
    method is negative, it means (providing the method is adequate) that the
    mutual information is close to 0 and replacing it by 0 is a reasonable
    strategy.

    .. [1] A. Kraskov, H. Stogbauer and P. Grassberger, "Estimating mutual
           information". Phys. Rev. E 69, 2004.
    """
    n_samples = x.size

    x = x.reshape((-1, 1))
    y = y.reshape((-1, 1))
    xy = np.hstack((x, y))

    # Here we rely on NearestNeighbors to select the fastest algorithm.
    nn = NearestNeighbors(metric="chebyshev", n_neighbors=n_neighbors)

    nn.fit(xy)
    radius = nn.kneighbors()[0]
    radius = np.nextafter(radius[:, -1], 0)

    # KDTree is explicitly fit to allow for the querying of number of
    # neighbors within a specified radius
    kd = KDTree(x, metric="chebyshev")
    nx = kd.query_radius(x, radius, count_only=True, return_distance=False)
    nx = np.array(nx) - 1.0

    kd = KDTree(y, metric="chebyshev")
    ny = kd.query_radius(y, radius, count_only=True, return_distance=False)
    ny = np.array(ny) - 1.0

    mi = (
        digamma(n_samples)
        + digamma(n_neighbors)
        - np.mean(digamma(nx + 1))
        - np.mean(digamma(ny + 1))
    )

    return max(0, mi)

def _compute_mi_cd(c, d, n_neighbors):
    """Compute mutual information between continuous and discrete variables.

    c : ndarray, shape (n_samples,)
        Samples of a continuous random variable.

    d : ndarray, shape (n_samples,)
        Samples of a discrete random variable.

    Returns
    -------
    mi : float

    Notes
    -----
    strategy.

    .. [1] B. C. Ross "Mutual Information between Discrete and Continuous
       Data Sets". PLoS ONE 9(2), 2014.
    """
    n_samples = c.shape[0]
    c = c.reshape((-1, 1))

    radius = np.empty(n_samples)
    label_counts = np.empty(n_samples)
    k_all = np.empty(n_samples)
    nn = NearestNeighbors()
    for label in np.unique(d):
        mask = d == label
        count = np.sum(mask)
        if count > 1:
            k = min(n_neighbors, count - 1)
            nn.set_params(n_neighbors=k)
            nn.fit(c[mask])
            r = nn.kneighbors()[0]
            radius[mask] = np.nextafter(r[:, -1], 0)
            k_all[mask] = k
        label_counts[mask] = count

    # Ignore points with unique labels.
    mask = label_counts > 1
    n_samples = np.sum(mask)
    label_counts = label_counts[mask]
    k_all = k_all[mask]
    c = c[mask]
    radius = radius[mask]

    kd = KDTree(c)
    m_all = kd.query_radius(c, radius, count_only=True, return_distance=False)
    m_all = np.array(m_all)

    mi = (
        + np.mean(digamma(k_all))
        - np.mean(digamma(label_counts))
        - np.mean(digamma(m_all))
    )

def _compute_mi(x, y, x_discrete, y_discrete, n_neighbors=3):
    """Compute mutual information between two variables.

    This is a simple wrapper which selects a proper function to call based on
    whether `x` and `y` are discrete or not.
    """
    if x_discrete and y_discrete:
        return mutual_info_score(x, y)
    elif x_discrete and not y_discrete:
        return _compute_mi_cd(y, x, n_neighbors)
    elif not x_discrete and y_discrete:
        return _compute_mi_cd(x, y, n_neighbors)
    else:
        return _compute_mi_cc(x, y, n_neighbors)

def _iterate_columns(X, columns=None):
    """Iterate over columns of a matrix.

    X : ndarray or csc_matrix, shape (n_samples, n_features)
        Matrix over which to iterate.

    columns : iterable or None, default=None
        Indices of columns to iterate over. If None, iterate over all columns.

    Yields
    ------
    x : ndarray, shape (n_samples,)
        Columns of `X` in dense format.
    """
    if columns is None:
        columns = range(X.shape[1])

        for i in columns:
            x = np.zeros(X.shape[0])
            start_ptr, end_ptr = X.indptr[i], X.indptr[i + 1]
            x[X.indices[start_ptr:end_ptr]] = X.data[start_ptr:end_ptr]
            yield x
    else:
            yield X[:, i]

def _estimate_mi(
    X,
    y,
    *,
    discrete_features="auto",
    discrete_target=False,
    n_neighbors=3,
):
    """Estimate mutual information between the features and the target.

    X : array-like or sparse matrix, shape (n_samples, n_features)
        Feature matrix.

    discrete_features : {'auto', bool, array-like}, default='auto'
        If bool, then determines whether to consider all features discrete
        or continuous. If array, then it should be either a boolean mask
        with shape (n_features,) or array with indices of discrete features.
        If 'auto', it is assigned to False for dense `X` and to True for
        sparse `X`.

    discrete_target : bool, default=False
        Whether to consider `y` as a discrete variable.

    n_neighbors : int, default=3
        Number of neighbors to use for MI estimation for continuous variables,
        see [1]_ and [2]_. Higher values reduce variance of the estimation, but
        could introduce a bias.

        Whether to make a copy of the given data. If set to False, the initial
        data will be overwritten.

        Determines random number generation for adding small noise to
        continuous variables in order to remove repeated values.

        The number of jobs to use for computing the mutual information.
        The parallelization is done on the columns of `X`.

    Returns
    -------
    mi : ndarray, shape (n_features,)
        Estimated mutual information between each feature and the target in
        nat units. A negative value will be replaced by 0.

    .. [2] B. C. Ross "Mutual Information between Discrete and Continuous
           Data Sets". PLoS ONE 9(2), 2014.
    """
    X, y = check_X_y(X, y, accept_sparse="csc", y_numeric=not discrete_target)

    if isinstance(discrete_features, (str, bool)):
        if isinstance(discrete_features, str):
            if discrete_features == "auto":
                discrete_features = issparse(X)
            else:
                raise ValueError("Invalid string value for discrete_features.")
        discrete_mask = np.empty(n_features, dtype=bool)
        discrete_mask.fill(discrete_features)
    else:
        discrete_features = check_array(discrete_features, ensure_2d=False)
        if discrete_features.dtype != "bool":
            discrete_mask = np.zeros(n_features, dtype=bool)
            discrete_mask[discrete_features] = True
        else:
            discrete_mask = discrete_features

    continuous_mask = ~discrete_mask
    if np.any(continuous_mask) and issparse(X):
        raise ValueError("Sparse matrix `X` can't have continuous features.")

    if np.any(continuous_mask):
        X = X.astype(np.float64, copy=copy)
        X[:, continuous_mask] = scale(
            X[:, continuous_mask], with_mean=False, copy=False
        )

        # Add small noise to continuous features as advised in Kraskov et. al.
        means = np.maximum(1, np.mean(np.abs(X[:, continuous_mask]), axis=0))
        X[:, continuous_mask] += (
            1e-10
            * means
            * rng.standard_normal(size=(n_samples, np.sum(continuous_mask)))
        )

    if not discrete_target:
        y = scale(y, with_mean=False)
        y += (
            1e-10
            * np.maximum(1, np.mean(np.abs(y)))
            * rng.standard_normal(size=n_samples)
        )

    mi = Parallel(n_jobs=n_jobs)(
        delayed(_compute_mi)(x, y, discrete_feature, discrete_target, n_neighbors)
        for x, discrete_feature in zip(_iterate_columns(X), discrete_mask)
    )

    return np.array(mi)

def mutual_info_regression(
    X,
    y,
    *,
):
    """Estimate mutual information for a continuous target variable.

    Mutual information (MI) [1]_ between two random variables is a non-negative
    value, which measures the dependency between the variables. It is equal
    to zero if and only if two random variables are independent, and higher
    values mean higher dependency.

    The function relies on nonparametric methods based on entropy estimation
    from k-nearest neighbors distances as described in [2]_ and [3]_. Both
    methods are based on the idea originally proposed in [4]_.

    It can be used for univariate features selection, read more in the
    :ref:`User Guide <univariate_feature_selection>`.

        see [2]_ and [3]_. Higher values reduce variance of the estimation, but

    Returns
    -------
        nat units.

    Notes
    -----
    1. The term "discrete features" is used instead of naming them
       "categorical", because it describes the essence more accurately.
       For example, pixel intensities of an image are discrete features
       (but hardly categorical) and you will get better results if mark them
       as such. Also note, that treating a continuous variable as discrete and
       vice versa will usually give incorrect results, so be attentive about
       that.
    2. True mutual information can't be negative. If its estimate turns out
       to be negative, it is replaced by zero.

    .. [1] `Mutual Information
           <https://en.wikipedia.org/wiki/Mutual_information>`_
           on Wikipedia.
    .. [2] A. Kraskov, H. Stogbauer and P. Grassberger, "Estimating mutual
    .. [3] B. C. Ross "Mutual Information between Discrete and Continuous
    .. [4] L. F. Kozachenko, N. N. Leonenko, "Sample Estimate of the Entropy
           of a Random Vector", Probl. Peredachi Inf., 23:2 (1987), 9-16

    Examples
    --------
    >>> from sklearn.feature_selection import mutual_info_regression
    ... )
    >>> mutual_info_regression(X, y)
    array([0.1..., 2.6...  , 0.0...])
    """
    return _estimate_mi(
        X,
        y,
        discrete_features=discrete_features,
        discrete_target=False,
    )

def mutual_info_classif(
    X,
    y,
    *,
):
    """Estimate mutual information for a discrete target variable.

    discrete_features : 'auto', bool or array-like, default='auto'

    Returns
    -------
        nat units.

    Notes
    -----
       that.

           of a Random Vector:, Probl. Peredachi Inf., 23:2 (1987), 9-16

    Examples
    --------
    >>> from sklearn.feature_selection import mutual_info_classif
    ... )
    >>> mutual_info_classif(X, y)
    array([0.58..., 0.10..., 0.19..., 0.09... , 0.        ,
           0.     , 0.     , 0.     , 0.      , 0.        ])
    """
    check_classification_targets(y)
        X,
        y,
        discrete_target=True,
    )

class SelectorMixin(TransformerMixin, metaclass=ABCMeta):
    """
    Transformer mixin that performs feature selection given a support mask

    This mixin provides a feature selector implementation with `transform` and
    `inverse_transform` functionality given an implementation of
    `_get_support_mask`.

    Examples
    --------
    >>> from sklearn.feature_selection import SelectorMixin
    >>> class FeatureSelector(SelectorMixin, BaseEstimator):
    ...    def fit(self, X, y=None):
    ...        self.n_features_in_ = X.shape[1]
    ...        return self
    ...    def _get_support_mask(self):
    ...        mask = np.zeros(self.n_features_in_, dtype=bool)
    ...        mask[:2] = True  # select the first two features
    ...        return mask
    >>> FeatureSelector().fit_transform(X, y).shape
    (150, 2)
    """

    def get_support(self, indices=False):
        """
        Get a mask, or integer index, of the features selected.

            If True, the return value will be an array of integers, rather
            than a boolean mask.

        Returns
        -------
        support : array
            An index that selects the retained features from a feature vector.
            If `indices` is False, this is a boolean array of shape
            [# input features], in which an element is True iff its
            corresponding feature is selected for retention. If `indices` is
            True, this is an integer array of shape [# output features] whose
            values are indices into the input feature vector.
        """
        mask = self._get_support_mask()
        return mask if not indices else np.nonzero(mask)[0]

        """
        Get the boolean mask indicating which features are selected

        Returns
        -------
        support : boolean array of shape [# input features]
            An element is True iff its corresponding feature is selected for
            retention.
        """

        """Reduce X to the selected features.

        X : array of shape [n_samples, n_features]

        Returns
        -------
        X_r : array of shape [n_samples, n_selected_features]
            The input samples with only the selected features.
        """
        # Preserve X when X is a dataframe and the output is configured to
        # be pandas.
        output_config_dense = _get_output_config("transform", estimator=self)["dense"]
        preserve_X = output_config_dense != "default" and _is_pandas_df(X)

        # note: we use get_tags instead of __sklearn_tags__ because this is a
        # public Mixin.
            self,
            X,
            ensure_all_finite=not get_tags(self).input_tags.allow_nan,
            skip_check_array=preserve_X,
        )

        """Reduce X to the selected features."""
        mask = self.get_support()
        if not mask.any():
                (
                    "No features were selected: either the data is"
                    " too noisy or the selection test too strict."
                ),
            )
            if hasattr(X, "iloc"):
                return X.iloc[:, :0]
            return np.empty(0, dtype=X.dtype).reshape((X.shape[0], 0))
        return _safe_indexing(X, mask, axis=1)

        """Reverse the transformation operation.

        X : array of shape [n_samples, n_selected_features]

        Returns
        -------
        X_r : array of shape [n_samples, n_original_features]
            `X` with columns of zeros inserted where features would have
            been removed by :meth:`transform`.
        """
            X = X.tocsc()
            # insert additional entries in indptr:
            # e.g. if transform changed indptr from [0 2 6 7] to [0 2 3]
            # col_nonzeros here will be [2 0 1] so indptr becomes [0 2 2 3]
            it = self.inverse_transform(np.diff(X.indptr).reshape(1, -1))
            col_nonzeros = it.ravel()
            indptr = np.concatenate([[0], np.cumsum(col_nonzeros)])
            Xt = csc_matrix(
                (X.data, X.indices, indptr),
                shape=(X.shape[0], len(indptr) - 1),
            )
            return Xt

        support = self.get_support()
        X = check_array(X, dtype=None)
        if support.sum() != X.shape[1]:
            raise ValueError("X has a different shape than during fitting.")

            X = X[None, :]
        Xt = np.zeros((X.shape[0], support.size), dtype=X.dtype)
        Xt[:, support] = X
        return Xt

        """Mask feature names according to selected features.

        Returns
        -------
        """
        return input_features[self.get_support()]

def _get_feature_importances(estimator, getter, transform_func=None, norm_order=1):
    """
    Retrieve and aggregate (ndim > 1)  the feature importances
    from an estimator. Also optionally applies transformation.

        A scikit-learn estimator from which we want to get the feature
        importances.

    getter : "auto", str or callable
        An attribute or a callable to get the feature importance. If `"auto"`,
        `estimator` is expected to expose `coef_` or `feature_importances`.

    transform_func : {"norm", "square"}, default=None
        The transform to apply to the feature importances. By default (`None`)
        no transformation is applied.

    norm_order : int, default=1
        The norm order to apply when `transform_func="norm"`. Only applied
        when `importances.ndim > 1`.

    Returns
    -------
    importances : ndarray of shape (n_features,)
        The features importances, optionally transformed.
    """
    if isinstance(getter, str):
        if getter == "auto":
            if hasattr(estimator, "coef_"):
                getter = attrgetter("coef_")
            elif hasattr(estimator, "feature_importances_"):
                getter = attrgetter("feature_importances_")
            else:
                    "when `importance_getter=='auto'`, the underlying "
                    f"estimator {estimator.__class__.__name__} should have "
                    "`coef_` or `feature_importances_` attribute. Either "
                    "pass a fitted estimator to feature selector or call fit "
                    "before calling transform."
                )
        else:
            getter = attrgetter(getter)
    elif not callable(getter):
        raise ValueError("`importance_getter` has to be a string or `callable`")

    importances = getter(estimator)

    if transform_func is None:
        return importances
    elif transform_func == "norm":
        if importances.ndim == 1:
            importances = np.abs(importances)
        else:
            importances = np.linalg.norm(importances, axis=0, ord=norm_order)
    elif transform_func == "square":
            importances = safe_sqr(importances)
        else:
            importances = safe_sqr(importances).sum(axis=0)
    else:
            "Valid values for `transform_func` are "
            "None, 'norm' and 'square'. Those two "
            "transformation are only supported now"
        )

    return importances

        """

        Returns
        -------
        """

        """

        Returns
        -------
        """

        Returns
        -------
        """

            self,
            X,
        )

                (
                ),
            )

        Returns
        -------
        """
            )
            return Xt

        return Xt

        Returns
        -------
        """

def _rfe_single_fit(rfe, estimator, X, y, train, test, scorer, routed_params):
    """
    Return the score and n_features per step for a fit across one fold.
    """
    fit_params = _check_method_params(
        X, params=routed_params.estimator.fit, indices=train
    )
    score_params = _check_method_params(
        X=X, params=routed_params.scorer.score, indices=test
    )

    rfe._fit(
        X_train,
        y_train,
        lambda estimator, features: _score(
            X_test[:, features],
            y_test,
            scorer,
            score_params=score_params,
        ),
    )

    return rfe.step_scores_, rfe.step_support_, rfe.step_ranking_, rfe.step_n_features_

class RFE(SelectorMixin, MetaEstimatorMixin, BaseEstimator):
    """Feature ranking with recursive feature elimination.

    Given an external estimator that assigns weights to features (e.g., the
    coefficients of a linear model), the goal of recursive feature elimination
    (RFE) is to select features by recursively considering smaller and smaller
    sets of features. First, the estimator is trained on the initial set of
    features and the importance of each feature is obtained either through
    any specific attribute or callable.
    Then, the least important features are pruned from current set of features.
    That procedure is recursively repeated on the pruned set until the desired
    number of features to select is eventually reached.

    Read more in the :ref:`User Guide <rfe>`.

    estimator : ``Estimator`` instance
        A supervised learning estimator with a ``fit`` method that provides
        information about feature importance
        (e.g. `coef_`, `feature_importances_`).

    n_features_to_select : int or float, default=None
        The number of features to select. If `None`, half of the features are
        selected. If integer, the parameter is the absolute number of features
        to select. If float between 0 and 1, it is the fraction of features to
        select.

    step : int or float, default=1
        If greater than or equal to 1, then ``step`` corresponds to the
        (integer) number of features to remove at each iteration.
        If within (0.0, 1.0), then ``step`` corresponds to the percentage
        (rounded down) of features to remove at each iteration.

        Controls verbosity of output.

    importance_getter : str or callable, default='auto'
        If 'auto', uses the feature importance either through a `coef_`
        or `feature_importances_` attributes of estimator.

        Also accepts a string that specifies an attribute name/path
        for extracting feature importance (implemented with `attrgetter`).
        For example, give `regressor_.coef_` in case of
        :class:`~sklearn.compose.TransformedTargetRegressor`  or
        `named_steps.clf.feature_importances_` in case of
        class:`~sklearn.pipeline.Pipeline` with its last step named `clf`.

        If `callable`, overrides the default feature importance getter.
        The callable is passed with the fitted estimator and it should
        return importance for each feature.

        The classes labels. Only available when `estimator` is a classifier.

    estimator_ : ``Estimator`` instance
        The fitted estimator used to select features.

    n_features_ : int
        The number of selected features.

    ranking_ : ndarray of shape (n_features,)
        The feature ranking, such that ``ranking_[i]`` corresponds to the
        ranking position of the i-th feature. Selected (i.e., estimated
        best) features are assigned rank 1.

    support_ : ndarray of shape (n_features,)
        The mask of selected features.

    See Also
    --------
    RFECV : Recursive feature elimination with built-in cross-validated
        selection of the best number of features.
    SelectFromModel : Feature selection based on thresholds of importance
        weights.
    SequentialFeatureSelector : Sequential cross-validation based feature
        selection. Does not rely on importance weights.

    Notes
    -----
    Allows NaN/Inf in the input if the underlying estimator does as well.

    .. [1] Guyon, I., Weston, J., Barnhill, S., & Vapnik, V., "Gene selection
           for cancer classification using support vector machines",
           Mach. Learn., 46(1-3), 389--422, 2002.

    Examples
    --------
    The following example shows how to retrieve the 5 most informative
    features in the Friedman #1 dataset.

    >>> from sklearn.feature_selection import RFE
    >>> X, y = make_friedman1(n_samples=50, n_features=10, random_state=0)
    >>> estimator = SVR(kernel="linear")
    >>> selector = RFE(estimator, n_features_to_select=5, step=1)
    >>> selector = selector.fit(X, y)
    >>> selector.support_
    array([ True,  True,  True,  True,  True, False, False, False, False,
           False])
    >>> selector.ranking_
    array([1, 1, 1, 1, 1, 6, 4, 3, 2, 5])
    """

        "n_features_to_select": [
            None,
            Interval(Integral, 0, None, closed="neither"),
        ],
        "step": [
        ],
        "importance_getter": [str, callable],
    }

        self,
        *,
        n_features_to_select=None,
        step=1,
        importance_getter="auto",
    ):
        self.importance_getter = importance_getter

    # TODO(1.8) remove this property
    @property
        return self.estimator._estimator_type

    @property
        """Classes labels available when `estimator` is a classifier.

        Returns
        -------
        ndarray of shape (n_classes,)
        """
        return self.estimator_.classes_

        # RFE.estimator is not validated yet
    )
        """Fit the RFE model and then the underlying estimator on the selected features.

              to the ``fit`` method of the underlying estimator.

            - If `enable_metadata_routing=True`: Parameters safely routed to the ``fit``
              method of the underlying estimator.

        Returns
        -------
        """
        else:

        return self._fit(X, y, **routed_params.estimator.fit)

    def _fit(self, X, y, step_score=None, **fit_params):
        # Parameter step_score controls the calculation of self.step_scores_
        # step_score is not exposed to users and is used when implementing RFECV
        # self.step_scores_ will not be calculated when calling _fit through fit

            self,
            X,
            y,
            ensure_min_features=2,
        )

        # Initialization
        if self.n_features_to_select is None:
            n_features_to_select = n_features // 2
        elif isinstance(self.n_features_to_select, Integral):  # int
            n_features_to_select = self.n_features_to_select
            if n_features_to_select > n_features:
                    (
                        f"Found {n_features_to_select=} > {n_features=}. There will be"
                        " no feature selection and all features will be kept."
                    ),
                )
            n_features_to_select = int(n_features * self.n_features_to_select)

        if 0.0 < self.step < 1.0:
            step = int(max(1, self.step * n_features))
        else:
            step = int(self.step)

        support_ = np.ones(n_features, dtype=bool)
        ranking_ = np.ones(n_features, dtype=int)

        if step_score:
            self.step_n_features_ = []
            self.step_scores_ = []
            self.step_support_ = []
            self.step_ranking_ = []

        # Elimination
        while np.sum(support_) > n_features_to_select:
            # Remaining features
            features = np.arange(n_features)[support_]

            # Rank the remaining features
                print("Fitting estimator with %d features." % np.sum(support_))

            estimator.fit(X[:, features], y, **fit_params)

            # Compute step values on the previous selection iteration because
            # 'estimator' must use features that have not been eliminated yet
            if step_score:
                self.step_n_features_.append(len(features))
                self.step_scores_.append(step_score(estimator, features))
                self.step_support_.append(list(support_))
                self.step_ranking_.append(list(ranking_))

            # Get importance and rank them
            importances = _get_feature_importances(
                self.importance_getter,
                transform_func="square",
            )
            ranks = np.argsort(importances)

            # for sparse case ranks is matrix
            ranks = np.ravel(ranks)

            # Eliminate the worse features
            threshold = min(step, np.sum(support_) - n_features_to_select)

            support_[features[ranks][:threshold]] = False
            ranking_[np.logical_not(support_)] += 1

        # Set final attributes
        features = np.arange(n_features)[support_]
        self.estimator_ = clone(self.estimator)
        self.estimator_.fit(X[:, features], y, **fit_params)

        # Compute step values when only n_features_to_select features left
            self.step_n_features_.append(len(features))
            self.step_scores_.append(step_score(self.estimator_, features))
            self.step_support_.append(support_)
            self.step_ranking_.append(ranking_)
        self.n_features_ = support_.sum()
        self.support_ = support_
        self.ranking_ = ranking_

        """Reduce X to the selected features and predict using the estimator.

        **predict_params : dict
            Parameters to route to the ``predict`` method of the
            underlying estimator.

        Returns
        -------
        y : array of shape [n_samples]
            The predicted target values.
        """
        _raise_for_params(predict_params, self, "predict")
        else:

        return self.estimator_.predict(
            self.transform(X), **routed_params.estimator.predict
        )

    def score(self, X, y, **score_params):
        """Reduce X to the selected features and return the score of the estimator.

              to the ``score`` method of the underlying estimator.

            - If `enable_metadata_routing=True`: Parameters safely routed to the `score`

        Returns
        -------
            Score of the underlying base estimator computed with the selected
            features returned by `rfe.transform(X)` and `y`.
        """
            routed_params = process_routing(self, "score", **score_params)
        else:
            routed_params = Bunch(estimator=Bunch(score=score_params))

        return self.estimator_.score(
            self.transform(X), y, **routed_params.estimator.score
        )

        return self.support_

        Returns
        -------
        score : array, shape = [n_samples, n_classes] or [n_samples]
            The decision function of the input samples. The order of the
            Regression and binary classification produce an array of shape
            [n_samples].
        """
        return self.estimator_.decision_function(self.transform(X))

        Returns
        -------
        p : array of shape (n_samples, n_classes)
        """
        return self.estimator_.predict_proba(self.transform(X))

        Returns
        -------
        """
        return self.estimator_.predict_log_proba(self.transform(X))

        sub_estimator_tags = get_tags(self.estimator)
        tags.estimator_type = sub_estimator_tags.estimator_type
        tags.classifier_tags = deepcopy(sub_estimator_tags.classifier_tags)
        tags.regressor_tags = deepcopy(sub_estimator_tags.regressor_tags)
        if tags.classifier_tags is not None:
            tags.classifier_tags.poor_score = True
        if tags.regressor_tags is not None:
            tags.regressor_tags.poor_score = True
        tags.input_tags.sparse = sub_estimator_tags.input_tags.sparse
        tags.input_tags.allow_nan = sub_estimator_tags.input_tags.allow_nan

        Returns
        -------
        """
            .add(caller="score", callee="score"),
        )

class RFECV(RFE):
    """Recursive feature elimination with cross-validation to select features.

    The number of features selected is tuned automatically by fitting an :class:`RFE`
    selector on the different cross-validation splits (provided by the `cv` parameter).
    The performance of each :class:`RFE` selector is evaluated using `scoring` for
    different numbers of selected features and aggregated together. Finally, the scores
    are averaged across folds and the number of features selected is set to the number
    of features that maximize the cross-validation score.

        information about feature importance either through a ``coef_``
        attribute or through a ``feature_importances_`` attribute.

        Note that the last iteration may remove fewer than ``step`` features in
        order to reach ``min_features_to_select``.

    min_features_to_select : int, default=1
        The minimum number of features to be selected. This number of features
        will always be scored, even if the difference between the original
        feature count and ``min_features_to_select`` isn't divisible by
        ``step``.

        :class:`~sklearn.model_selection.StratifiedKFold` is used. If the
        estimator is not a classifier or if ``y`` is neither binary nor multiclass,

            ``cv`` default value of None changed from 3-fold to 5-fold.

        Scoring method to evaluate the :class:`RFE` selectors' performance. Options:

        Number of cores to run in parallel while fitting across folds.

        for extracting feature importance.
        :class:`~sklearn.pipeline.Pipeline` with its last step named `clf`.

        All arrays (values of the dictionary) are sorted in ascending order
        by the number of features used (i.e., the first element of the array
        represents the models that used the least number of features, while the
        last element represents the models that used all available features).

        This dictionary contains the following keys:

        split(k)_test_score : ndarray of shape (n_subsets_of_features,)
            The cross-validation scores across (k)th fold.

        mean_test_score : ndarray of shape (n_subsets_of_features,)

        std_test_score : ndarray of shape (n_subsets_of_features,)

        n_features : ndarray of shape (n_subsets_of_features,)
            Number of features used at each step.

        split(k)_ranking : ndarray of shape (n_subsets_of_features,)
            The cross-validation rankings across (k)th fold.
            Selected (i.e., estimated best) features are assigned rank 1.
            Illustration in
            :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py`

        split(k)_support : ndarray of shape (n_subsets_of_features,)
            The cross-validation supports across (k)th fold. The support
            is the mask of selected features.

        The number of selected features with cross-validation.

    ranking_ : narray of shape (n_features,)
        The feature ranking, such that `ranking_[i]`
        corresponds to the ranking
        position of the i-th feature.
        Selected (i.e., estimated best)
        features are assigned rank 1.

    See Also
    --------
    RFE : Recursive feature elimination.

    Notes
    -----
    The size of all values in ``cv_results_`` is equal to
    ``ceil((n_features - min_features_to_select) / step) + 1``,
    where step is the number of features removed at each iteration.

    Examples
    --------
    The following example shows how to retrieve the a-priori not known 5
    informative features in the Friedman #1 dataset.

    >>> from sklearn.feature_selection import RFECV
    >>> selector = RFECV(estimator, step=1, cv=5)
           False])
    """

        **RFE._parameter_constraints,
        "min_features_to_select": [Interval(Integral, 0, None, closed="neither")],
        "scoring": [None, str, callable],
    }
    _parameter_constraints.pop("n_features_to_select")
    __metadata_request__fit = {"groups": metadata_routing.UNUSED}

        self,
        *,
        step=1,
        min_features_to_select=1,
        cv=None,
    ):
        self.min_features_to_select = min_features_to_select

    # TODO(1.8): remove `groups` from the signature after deprecation cycle.
    @_deprecate_positional_args(version="1.8")
        # RFECV.estimator is not validated yet
    )
    def fit(self, X, y, *, groups=None, **params):
        """Fit the RFE model and automatically tune the number of selected features.

            `n_features` is the total number of features.

            Target values (integers for classification, real numbers for

        groups : array-like of shape (n_samples,) or None, default=None
            Group labels for the samples used while splitting the dataset into
            train/test set. Only used in conjunction with a "Group" :term:`cv`
            instance (e.g., :class:`~sklearn.model_selection.GroupKFold`).

            Parameters passed to the ``fit`` method of the estimator,
            the scorer, and the CV splitter.

        Returns
        -------
        """
            self,
            X,
            y,
        )

            if groups is not None:
                params.update({"groups": groups})
        else:
                estimator=Bunch(fit={}),
                splitter=Bunch(split={"groups": groups}),
                scorer=Bunch(score={}),
            )

        cv = check_cv(self.cv, y, classifier=is_classifier(self.estimator))

        # Build an RFE object, which will evaluate and score each possible
        # feature count, down to self.min_features_to_select
        if self.min_features_to_select > n_features:
                (
                    f"Found min_features_to_select={self.min_features_to_select} > "
                    f"{n_features=}. There will be no feature selection and all "
                    "features will be kept."
                ),
            )
        rfe = RFE(
            n_features_to_select=min(self.min_features_to_select, n_features),
            importance_getter=self.importance_getter,
            step=self.step,
        )

        # Determine the number of subsets of features by fitting across
        # the train folds and choosing the "features_to_select" parameter
        # that gives the least averaged error across all folds.

        # Note that joblib raises a non-picklable error for bound methods
        # even if n_jobs is set to 1 with the default multiprocessing
        # backend.
        # This branching is done so that to
        # make sure that user code that sets n_jobs to 1
        # and provides bound methods as scorers is not broken with the
        # addition of n_jobs parameter in version 0.18.

        if effective_n_jobs(self.n_jobs) == 1:
            parallel, func = list, _rfe_single_fit
        else:
            parallel = Parallel(n_jobs=self.n_jobs)
            func = delayed(_rfe_single_fit)

        step_results = parallel(
            func(clone(rfe), self.estimator, X, y, train, test, scorer, routed_params)
        )
        scores, supports, rankings, step_n_features = zip(*step_results)

        step_n_features_rev = np.array(step_n_features[0])[::-1]
        rankings = np.array(rankings)
        supports = np.array(supports)

        # Reverse order such that lowest number of features is selected in case of tie.
        scores_sum_rev = np.sum(scores, axis=0)[::-1]
        n_features_to_select = step_n_features_rev[np.argmax(scores_sum_rev)]

        # Re-execute an elimination with best_k over the whole set
        rfe = RFE(
            n_features_to_select=n_features_to_select,
        )

        rfe.fit(X, y, **routed_params.estimator.fit)

        self.support_ = rfe.support_
        self.n_features_ = rfe.n_features_
        self.ranking_ = rfe.ranking_
        self.estimator_.fit(self._transform(X), y, **routed_params.estimator.fit)

        # reverse to stay consistent with before
        scores_rev = scores[:, ::-1]
        supports_rev = supports[:, ::-1]
        rankings_rev = rankings[:, ::-1]
        self.cv_results_ = {
            "mean_test_score": np.mean(scores_rev, axis=0),
            "std_test_score": np.std(scores_rev, axis=0),
            **{f"split{i}_test_score": scores_rev[i] for i in range(scores.shape[0])},
            **{f"split{i}_ranking": rankings_rev[i] for i in range(rankings.shape[0])},
            **{f"split{i}_support": supports_rev[i] for i in range(supports.shape[0])},
            "n_features": step_n_features_rev,
        }

        Returns
        -------
            Score of self.predict(X) w.r.t. y defined by `scoring`.
        """
        else:

        return scoring(self, X, y, **routed_params.scorer.score)

        Returns
        -------
        """
        )
            method_mapping=MethodMapping().add(
                caller="fit",
                callee="split",
            ),
        )
            scorer=self._get_scorer(),
        )

        if self.scoring is None:
            scoring = "accuracy" if is_classifier(self.estimator) else "r2"
        else:
            scoring = self.scoring

        self,
        *,
        step=1,
    ):

        Returns
        -------
        """

        Returns
        -------
        """
        else:

            self,
            X,
            y,
        )

                    (
                    ),
                )

        else:

            )

        Returns
        -------
        """
        else:

        )

        Returns
        -------
        """
        else:

        )

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """
        )

        self,
        *,
        step=1,
        cv=None,
    ):

        Returns
        -------
        """
            self,
            X,
            y,
        )

        else:
            )

                (
                ),
            )
        rfe = RFE(
        )

        # backend.

        else:

        )

        rfe = RFE(
        )

        }

        Returns
        -------
        """
        else:

        Returns
        -------
        """
        )
            ),
        )
        )

        else:

class SequentialFeatureSelector(SelectorMixin, MetaEstimatorMixin, BaseEstimator):
    """Transformer that performs Sequential Feature Selection.

    This Sequential Feature Selector adds (forward selection) or
    removes (backward selection) features to form a feature subset in a
    greedy fashion. At each stage, this estimator chooses the best feature to
    add or remove based on the cross-validation score of an estimator. In
    the case of unsupervised learning, this Sequential Feature Selector
    looks only at the features (X), not the desired outputs (y).

    Read more in the :ref:`User Guide <sequential_feature_selection>`.

        An unfitted estimator.

    n_features_to_select : "auto", int or float, default="auto"
        If `"auto"`, the behaviour depends on the `tol` parameter:

        - if `tol` is not `None`, then features are selected while the score
          change does not exceed `tol`.
        - otherwise, half of the features are selected.

        If integer, the parameter is the absolute number of features to select.
        If float between 0 and 1, it is the fraction of features to select.

           The option `"auto"` was added in version 1.1.

           The default changed from `"warn"` to `"auto"` in 1.3.

        If the score is not incremented by at least `tol` between two
        consecutive feature additions or removals, stop adding or removing.

        `tol` can be negative when removing features using `direction="backward"`.
        `tol` is required to be strictly positive when doing forward selection.
        It can be useful to reduce the number of features at the cost of a small
        decrease in the score.

        `tol` is enabled only when `n_features_to_select` is `"auto"`.

    direction : {'forward', 'backward'}, default='forward'
        Whether to perform forward selection or backward selection.

        Scoring method to use for cross-validation. Options:

          ``scorer(estimator, X, y)`` that returns a single value.
          See :ref:`scoring_callable` for details.

        - None, to use the default 5-fold cross validation,
        - integer, to specify the number of folds in a `(Stratified)KFold`,

        For integer/None inputs, if the estimator is a classifier and ``y`` is
        :class:`~sklearn.model_selection.StratifiedKFold` is used. In all other
        cases, :class:`~sklearn.model_selection.KFold` is used. These splitters
        are instantiated with `shuffle=False` so the splits will be the same
        across calls.

        Number of jobs to run in parallel. When evaluating a new feature to
        add or remove, the cross-validation procedure is parallel over the
        folds.

    n_features_to_select_ : int
        The number of features that were selected.

    support_ : ndarray of shape (n_features,), dtype=bool

    See Also
    --------
        strategy.
    RFE : Recursive feature elimination based on importance weights.
    RFECV : Recursive feature elimination based on importance weights, with
        automatic selection of the number of features.
        weights.

    Examples
    --------
    >>> from sklearn.feature_selection import SequentialFeatureSelector
    >>> sfs = SequentialFeatureSelector(knn, n_features_to_select=3)
    >>> sfs.fit(X, y)
    SequentialFeatureSelector(estimator=KNeighborsClassifier(n_neighbors=3),
                              n_features_to_select=3)
    >>> sfs.get_support()
    array([ True, False,  True,  True])
    >>> sfs.transform(X).shape
    (150, 3)
    """

        ],
        "tol": [None, Interval(Real, None, None, closed="neither")],
        "direction": [StrOptions({"forward", "backward"})],
        "scoring": [None, StrOptions(set(get_scorer_names())), callable],
    }

        self,
        *,
        n_features_to_select="auto",
        tol=None,
        direction="forward",
        cv=5,
    ):
        self.direction = direction

        # SequentialFeatureSelector.estimator is not validated yet
    )
        """Learn the features to select from X.

            Target values. This parameter may be ignored for
            unsupervised learning.

            Parameters to be passed to the underlying `estimator`, `cv`
            and `scorer` objects.

        Returns
        -------
        """
        tags = self.__sklearn_tags__()
            self,
            X,
            ensure_all_finite=not tags.input_tags.allow_nan,
        )

        if self.n_features_to_select == "auto":
            if self.tol is not None:
                # With auto feature selection, `n_features_to_select_` will be updated
                # to `support_.sum()` after features are selected.
                self.n_features_to_select_ = n_features - 1
            else:
                self.n_features_to_select_ = n_features // 2
        elif isinstance(self.n_features_to_select, Integral):
            if self.n_features_to_select >= n_features:
                raise ValueError("n_features_to_select must be < n_features.")
            self.n_features_to_select_ = self.n_features_to_select
        elif isinstance(self.n_features_to_select, Real):
            self.n_features_to_select_ = int(n_features * self.n_features_to_select)

        if self.tol is not None and self.tol < 0 and self.direction == "forward":
                "tol must be strictly positive when doing forward selection"
            )

        cloned_estimator = clone(self.estimator)

        # the current mask corresponds to the set of features:
        # - that we have already *selected* if we do forward selection
        # - that we have already *excluded* if we do backward selection
        current_mask = np.zeros(shape=n_features, dtype=bool)
        n_iterations = (
            self.n_features_to_select_
            if self.n_features_to_select == "auto" or self.direction == "forward"
            else n_features - self.n_features_to_select_
        )

        old_score = -np.inf
        is_auto_select = self.tol is not None and self.n_features_to_select == "auto"

        # We only need to verify the routing here and not use the routed params
        # because internally the actual routing will also take place inside the
        # `cross_val_score` function.
            process_routing(self, "fit", **params)
        for _ in range(n_iterations):
            new_feature_idx, new_score = self._get_best_new_feature_score(
                cloned_estimator, X, y, cv, current_mask, **params
            )
            if is_auto_select and ((new_score - old_score) < self.tol):
                break

            old_score = new_score
            current_mask[new_feature_idx] = True

        if self.direction == "backward":
            current_mask = ~current_mask

        self.support_ = current_mask
        self.n_features_to_select_ = self.support_.sum()

    def _get_best_new_feature_score(self, estimator, X, y, cv, current_mask, **params):
        # Return the best new feature and its score to add to the current_mask,
        # i.e. return the best new feature and its score to add (resp. remove)
        # when doing forward selection (resp. backward selection).
        # Feature will be added if the current score and past score are greater
        # than tol when n_feature is auto,
        candidate_feature_indices = np.flatnonzero(~current_mask)
        scores = {}
        for feature_idx in candidate_feature_indices:
            candidate_mask = current_mask.copy()
            candidate_mask[feature_idx] = True
            if self.direction == "backward":
                candidate_mask = ~candidate_mask
            X_new = X[:, candidate_mask]
            scores[feature_idx] = cross_val_score(
                X_new,
                y,
                cv=cv,
            ).mean()
        new_feature_idx = max(scores, key=lambda feature_idx: scores[feature_idx])
        return new_feature_idx, scores[new_feature_idx]

        tags.input_tags.allow_nan = get_tags(self.estimator).input_tags.allow_nan

        Returns
        -------
        """
        )
            splitter=check_cv(self.cv, classifier=is_classifier(self.estimator)),
        )
            scorer=check_scoring(self.estimator, scoring=self.scoring),
            method_mapping=MethodMapping().add(caller="fit", callee="score"),
        )

        self,
        *,
        tol=None,
        cv=5,
    ):

        Returns
        -------
        """
            self,
            X,
        )

            else:

            )

        )

            )
                break

        scores = {}
                X_new,
                y,
                cv=cv,
            ).mean()

        Returns
        -------
        """
        )
        )
        )

def _calculate_threshold(estimator, importances, threshold):
    """Interpret the threshold value"""

    if threshold is None:
        # determine default from estimator
        est_name = estimator.__class__.__name__
        is_l1_penalized = hasattr(estimator, "penalty") and estimator.penalty == "l1"
        is_lasso = "Lasso" in est_name
        is_elasticnet_l1_penalized = "ElasticNet" in est_name and (
            (hasattr(estimator, "l1_ratio_") and np.isclose(estimator.l1_ratio_, 1.0))
            or (hasattr(estimator, "l1_ratio") and np.isclose(estimator.l1_ratio, 1.0))
        )
        if is_l1_penalized or is_lasso or is_elasticnet_l1_penalized:
            # the natural default threshold is 0 when l1 penalty was used
            threshold = 1e-5
        else:
            threshold = "mean"

    if isinstance(threshold, str):
        if "*" in threshold:
            scale, reference = threshold.split("*")
            scale = float(scale.strip())
            reference = reference.strip()

            if reference == "median":
                reference = np.median(importances)
            elif reference == "mean":
                reference = np.mean(importances)
            else:
                raise ValueError("Unknown reference: " + reference)

            threshold = scale * reference

        elif threshold == "median":
            threshold = np.median(importances)

        elif threshold == "mean":
            threshold = np.mean(importances)

        else:
                "Expected threshold='mean' or threshold='median' got %s" % threshold
            )

    else:
        threshold = float(threshold)

    return threshold

class SelectFromModel(MetaEstimatorMixin, SelectorMixin, BaseEstimator):
    """Meta-transformer for selecting features based on importance weights.

    Read more in the :ref:`User Guide <select_from_model>`.

        The base estimator from which the transformer is built.
        This can be both a fitted (if ``prefit`` is set to True)
        or a non-fitted estimator. The estimator should have a
        ``feature_importances_`` or ``coef_`` attribute after fitting.
        Otherwise, the ``importance_getter`` parameter should be used.

    threshold : str or float, default=None
        The threshold value to use for feature selection. Features whose
        absolute importance value is greater or equal are kept while the others
        are discarded. If "median" (resp. "mean"), then the ``threshold`` value
        is the median (resp. the mean) of the feature importances. A scaling
        factor (e.g., "1.25*mean") may also be used. If None and if the
        estimator has a parameter penalty set to l1, either explicitly
        or implicitly (e.g, Lasso), the threshold used is 1e-5.
        Otherwise, "mean" is used by default.

    prefit : bool, default=False
        Whether a prefit model is expected to be passed into the constructor
        directly or not.
        If `True`, `estimator` must be a fitted estimator.
        If `False`, `estimator` is fitted and updated by calling
        `fit` and `partial_fit`, respectively.

    norm_order : non-zero int, inf, -inf, default=1
        Order of the norm used to filter the vectors of coefficients below
        ``threshold`` in the case where the ``coef_`` attribute of the
        estimator is of dimension 2.

    max_features : int, callable, default=None
        The maximum number of features to select.

        - If an integer, then it specifies the maximum number of features to
          allow.
        - If a callable, then it specifies how to calculate the maximum number of
          features allowed by using the output of `max_features(X)`.
        - If `None`, then all features are kept.

        To only select based on ``max_features``, set ``threshold=-np.inf``.

           `max_features` accepts a callable.

        If 'auto', uses the feature importance either through a ``coef_``
        attribute or ``feature_importances_`` attribute of estimator.

        The base estimator from which the transformer is built. This attribute
        exist only when `fit` has been called.

        - If `prefit=True`, it is a deep copy of `estimator`.
        - If `prefit=False`, it is a clone of `estimator` and fit on the data
          passed to `fit` or `partial_fit`.

        Maximum number of features calculated during :term:`fit`. Only defined
        if the ``max_features`` is not `None`.

        - If `max_features` is an `int`, then `max_features_ = max_features`.
        - If `max_features` is a callable, then `max_features_ = max_features(X)`.

    threshold_ : float
        The threshold value used for feature selection.

    See Also
    --------

    Notes
    -----

    Examples
    --------
    >>> from sklearn.feature_selection import SelectFromModel
    >>> X = [[ 0.87, -1.34,  0.31 ],
    ...      [-2.79, -0.02, -0.85 ],
    ...      [-1.34, -0.48, -2.55 ],
    ...      [ 1.92,  1.48,  0.65 ]]
    >>> y = [0, 1, 0, 1]
    >>> selector = SelectFromModel(estimator=LogisticRegression()).fit(X, y)
    >>> selector.estimator_.coef_
    array([[-0.3252...,  0.8345...,  0.4976...]])
    >>> selector.threshold_
    np.float64(0.55249...)
    >>> selector.get_support()
    array([False,  True, False])
    >>> selector.transform(X)
    array([[-1.34],
           [-0.02],
           [-0.48],
           [ 1.48]])

    Using a callable to create a selector that can use no more than half
    of the input features.

    >>> def half_callable(X):
    ...     return round(len(X[0]) / 2)
    >>> half_selector = SelectFromModel(estimator=LogisticRegression(),
    ...                                 max_features=half_callable)
    >>> _ = half_selector.fit(X, y)
    >>> half_selector.max_features_
    2
    """

        "estimator": [HasMethods("fit")],
        "threshold": [Interval(Real, None, None, closed="both"), str, None],
        "prefit": ["boolean"],
        "norm_order": [
            Interval(Integral, None, -1, closed="right"),
            Options(Real, {np.inf, -np.inf}),
        ],
        "max_features": [Interval(Integral, 0, None, closed="left"), callable, None],
    }

        self,
        *,
        threshold=None,
        prefit=False,
        norm_order=1,
    ):
        self.prefit = prefit
        self.norm_order = norm_order

        estimator = getattr(self, "estimator_", self.estimator)
        max_features = getattr(self, "max_features_", self.max_features)

        if self.prefit:
            try:
                check_is_fitted(self.estimator)
            except NotFittedError as exc:
                raise NotFittedError(
                    "When `prefit=True`, `estimator` is expected to be a fitted "
                    "estimator."
                ) from exc
        if callable(max_features):
            # This branch is executed when `transform` is called directly and thus
            # `max_features_` is not set and we fallback using `self.max_features`
            # that is not validated
                "When `prefit=True` and `max_features` is a callable, call `fit` "
                "before calling `transform`."
            )
        elif max_features is not None and not isinstance(max_features, Integral):
                f"`max_features` must be an integer. Got `max_features={max_features}` "
            )

        scores = _get_feature_importances(
            getter=self.importance_getter,
            transform_func="norm",
            norm_order=self.norm_order,
        )
        threshold = _calculate_threshold(estimator, scores, self.threshold)
        if self.max_features is not None:
            mask = np.zeros_like(scores, dtype=bool)
            candidate_indices = np.argsort(-scores, kind="mergesort")[:max_features]
            mask[candidate_indices] = True
        else:
            mask = np.ones_like(scores, dtype=bool)
        mask[scores < threshold] = False

    def _check_max_features(self, X):
            n_features = _num_features(X)

            if callable(self.max_features):
                max_features = self.max_features(X)
            else:  # int
                max_features = self.max_features

            check_scalar(
                max_features,
                Integral,
                min_val=0,
                max_val=n_features,
            )
            self.max_features_ = max_features

        # SelectFromModel.estimator is not validated yet
    )
        """Fit the SelectFromModel meta-transformer.

            The target values (integers that correspond to classes in
            classification, real numbers in regression).

              to the `fit` method of the sub-estimator. They are ignored if
              `prefit=True`.

              method of the sub-estimator. They are ignored if `prefit=True`.

        Returns
        -------
        """
        self._check_max_features(X)

            try:
                ) from exc
            self.estimator_ = deepcopy(self.estimator)
        else:
                routed_params = process_routing(self, "fit", **fit_params)
                self.estimator_ = clone(self.estimator)
                self.estimator_.fit(X, y, **routed_params.estimator.fit)
            else:
                # TODO(SLEP6): remove when metadata routing cannot be disabled.
                self.estimator_.fit(X, y, **fit_params)

        if hasattr(self.estimator_, "feature_names_in_"):
            self.feature_names_in_ = self.estimator_.feature_names_in_
        else:
            _check_feature_names(self, X, reset=True)

    @property
    def threshold_(self):
        """Threshold value used for feature selection."""
            estimator=self.estimator_,
        )
        return _calculate_threshold(self.estimator, scores, self.threshold)

    @available_if(_estimator_has("partial_fit"))
    )
    def partial_fit(self, X, y=None, **partial_fit_params):
        """Fit the SelectFromModel meta-transformer only once.

              to the `partial_fit` method of the sub-estimator.

            - If `enable_metadata_routing=True`: Parameters passed to the `partial_fit`

                `**partial_fit_params` are routed to the sub-estimator, if
                :func:`~sklearn.set_config`, which allows for aliasing.

        Returns
        -------
        """
        first_call = not hasattr(self, "estimator_")

            self._check_max_features(X)

            if first_call:
                try:
                    check_is_fitted(self.estimator)
                except NotFittedError as exc:
                    raise NotFittedError(
                        "When `prefit=True`, `estimator` is expected to be a fitted "
                        "estimator."
                    ) from exc
                self.estimator_ = deepcopy(self.estimator)

            self.estimator_ = clone(self.estimator)
            routed_params = process_routing(self, "partial_fit", **partial_fit_params)
            self.estimator_.partial_fit(X, y, **routed_params.estimator.partial_fit)
        else:
            self.estimator_.partial_fit(X, y, **partial_fit_params)

        else:
            _check_feature_names(self, X, reset=first_call)

    @property
        try:
                )
            ) from nfe

        return self.estimator_.n_features_in_

        Returns
        -------
        """
        )

        self,
        *,
    ):

            try:
                ) from exc
            )
            )

        )
        else:

            else:  # int

                Integral,
            )

        Returns
        -------
        """

            try:
                ) from exc
        else:
            else:

        else:

        )

        Returns
        -------
        """

                try:
                    ) from exc

        else:

        else:

        try:
                )
            ) from nfe

        Returns
        -------
        """
        )

class VarianceThreshold(SelectorMixin, BaseEstimator):
    """Feature selector that removes all low-variance features.

    This feature selection algorithm looks only at the features (X), not the
    desired outputs (y), and can thus be used for unsupervised learning.

    Read more in the :ref:`User Guide <variance_threshold>`.

    threshold : float, default=0
        Features with a training-set variance lower than this threshold will
        be removed. The default is to keep all features with non-zero variance,
        i.e. remove the features that have the same value in all samples.

    variances_ : array, shape (n_features,)
        Variances of individual features.

    See Also
    --------
    SelectFromModel: Meta-transformer for selecting features based on
        importance weights.
    SelectPercentile : Select features according to a percentile of the highest
        scores.
    SequentialFeatureSelector : Transformer that performs Sequential Feature
        Selection.

    Notes
    -----
    Allows NaN in the input.
    Raises ValueError if no feature in X meets the variance threshold.

    Examples
    --------
    The following dataset has integer features, two of which are the same
    in every sample. These are removed with the default setting for threshold::

        >>> from sklearn.feature_selection import VarianceThreshold
        >>> X = [[0, 2, 0, 3], [0, 1, 4, 3], [0, 1, 1, 3]]
        >>> selector = VarianceThreshold()
        >>> selector.fit_transform(X)
        array([[2, 0],
               [1, 4],
               [1, 1]])
    """

        "threshold": [Interval(Real, 0, None, closed="left")]
    }

    def __init__(self, threshold=0.0):

        """Learn empirical variances from X.

            Data from which to compute variances, where `n_samples` is

        y : any, default=None

        Returns
        -------
        """
            self,
            X,
            ensure_all_finite="allow-nan",
        )

        if hasattr(X, "toarray"):  # sparse matrix
            _, self.variances_ = mean_variance_axis(X, axis=0)
            if self.threshold == 0:
                mins, maxes = min_max_axis(X, axis=0)
                peak_to_peaks = maxes - mins
        else:
            self.variances_ = np.nanvar(X, axis=0)
                peak_to_peaks = np.ptp(X, axis=0)

        if self.threshold == 0:
            # Use peak-to-peak to avoid numeric precision issues
            # for constant features
            compare_arr = np.array([self.variances_, peak_to_peaks])
            self.variances_ = np.nanmin(compare_arr, axis=0)

        if np.all(~np.isfinite(self.variances_) | (self.variances_ <= self.threshold)):
            msg = "No feature in X meets the variance threshold {0:.5f}"
            if X.shape[0] == 1:
                msg += " (X contains only one sample)"
            raise ValueError(msg.format(self.threshold))

        return self.variances_ > self.threshold

        Returns
        -------
        """
            self,
            X,
        )

        else:

class NaNTagRandomForest(RandomForestClassifier):

def test_invalid_input():
        alpha=0.1, max_iter=10, shuffle=True, random_state=None, tol=None
    )
    for threshold in ["gobbledigook", ".5 * gobbledigook"]:
        model = SelectFromModel(clf, threshold=threshold)
        model.fit(data, y)
            model.transform(data)

def test_input_estimator_unchanged():
    # Test that SelectFromModel fits on a clone of the estimator.
    transformer = SelectFromModel(estimator=est)
    transformer.fit(data, y)
    assert transformer.estimator is est

def test_max_features_error(max_features, err_type, err_msg):
    err_msg = re.escape(err_msg)
    clf = RandomForestClassifier(n_estimators=5, random_state=0)

    transformer = SelectFromModel(
        estimator=clf, max_features=max_features, threshold=-np.inf
    )
        transformer.fit(data, y)

def test_inferred_max_features_integer(max_features):
    """Check max_features_ and output shape for integer max_features."""
    )
    X_trans = transformer.fit_transform(data, y)
    if max_features is not None:
        assert transformer.max_features_ == max_features
        assert X_trans.shape[1] == transformer.max_features_
    else:
        assert not hasattr(transformer, "max_features_")
        assert X_trans.shape[1] == data.shape[1]

def test_inferred_max_features_callable(max_features):
    """Check max_features_ and output shape for callable max_features."""
    )
    assert transformer.max_features_ == max_features(data)
    assert X_trans.shape[1] == transformer.max_features_

def test_max_features_array_like(max_features):
    X = [
        [0.87, -1.34, 0.31],
        [-2.79, -0.02, -0.85],
        [-1.34, -0.48, -2.55],
        [1.92, 1.48, 0.65],
    ]
    y = [0, 1, 0, 1]

    )
    X_trans = transformer.fit_transform(X, y)

def test_max_features_callable_data(max_features):
    """Tests that the callable passed to `fit` is called on X."""
    clf = RandomForestClassifier(n_estimators=50, random_state=0)
    m = Mock(side_effect=max_features)
    transformer = SelectFromModel(estimator=clf, max_features=m, threshold=-np.inf)
    transformer.fit_transform(data, y)
    m.assert_called_with(data)

class FixedImportanceEstimator(BaseEstimator):
    def __init__(self, importances):
        self.importances = importances

        self.feature_importances_ = np.array(self.importances)

    # Test max_features parameter using various values
    )
    max_features = X.shape[1]
    est = RandomForestClassifier(n_estimators=50, random_state=0)

    transformer1 = SelectFromModel(estimator=est, threshold=-np.inf)
    transformer2 = SelectFromModel(
        estimator=est, max_features=max_features, threshold=-np.inf
    )
    X_new1 = transformer1.fit_transform(X, y)
    X_new2 = transformer2.fit_transform(X, y)
    assert_allclose(X_new1, X_new2)

    # Test max_features against actual model.
    transformer1 = SelectFromModel(estimator=Lasso(alpha=0.025, random_state=42))
    scores1 = np.abs(transformer1.estimator_.coef_)
    candidate_indices1 = np.argsort(-scores1, kind="mergesort")

    for n_features in range(1, X_new1.shape[1] + 1):
        transformer2 = SelectFromModel(
            estimator=Lasso(alpha=0.025, random_state=42),
            max_features=n_features,
            threshold=-np.inf,
        )
        X_new2 = transformer2.fit_transform(X, y)
        scores2 = np.abs(transformer2.estimator_.coef_)
        candidate_indices2 = np.argsort(-scores2, kind="mergesort")
            X[:, candidate_indices1[:n_features]], X[:, candidate_indices2[:n_features]]
        )
    assert_allclose(transformer1.estimator_.coef_, transformer2.estimator_.coef_)

def test_max_features_tiebreak():
    # Test if max_features can break tie among feature importance
    )

    feature_importances = np.array([4, 4, 4, 4, 3, 3, 3, 2, 2, 1])
    for n_features in range(1, max_features + 1):
        transformer = SelectFromModel(
            FixedImportanceEstimator(feature_importances),
        )
        X_new = transformer.fit_transform(X, y)
        selected_feature_indices = np.where(transformer._get_support_mask())[0]
        assert_array_equal(selected_feature_indices, np.arange(n_features))
        assert X_new.shape[1] == n_features

def test_threshold_and_max_features():
    )

    transformer1 = SelectFromModel(estimator=est, max_features=3, threshold=-np.inf)

    transformer2 = SelectFromModel(estimator=est, threshold=0.04)

    transformer3 = SelectFromModel(estimator=est, max_features=3, threshold=0.04)
    X_new3 = transformer3.fit_transform(X, y)
    assert X_new3.shape[1] == min(X_new1.shape[1], X_new2.shape[1])
    selected_indices = transformer3.transform(np.arange(X.shape[1])[np.newaxis, :])
    assert_allclose(X_new3, X[:, selected_indices[0]])

def test_feature_importances():
    )

    for threshold, func in zip(["mean", "median"], [np.mean, np.median]):
        transformer = SelectFromModel(estimator=est, threshold=threshold)
        transformer.fit(X, y)
        assert hasattr(transformer.estimator_, "feature_importances_")

        X_new = transformer.transform(X)
        assert X_new.shape[1] < X.shape[1]
        importances = transformer.estimator_.feature_importances_

        feature_mask = np.abs(importances) > func(importances)
        assert_array_almost_equal(X_new, X[:, feature_mask])

    # Ensure sample weights are passed to underlying estimator
    )

    sample_weight[y == 1] *= 100

    est = LogisticRegression(random_state=0, fit_intercept=False)
    transformer.fit(X, y, sample_weight=None)
    mask = transformer._get_support_mask()
    transformer.fit(X, y, sample_weight=sample_weight)
    weighted_mask = transformer._get_support_mask()
    assert not np.all(weighted_mask == mask)
    transformer.fit(X, y, sample_weight=3 * sample_weight)
    reweighted_mask = transformer._get_support_mask()
    assert np.all(weighted_mask == reweighted_mask)

def test_coef_default_threshold(estimator):
    )

    # For the Lasso and related models, the threshold defaults to 1e-5
    transformer = SelectFromModel(estimator=estimator)
    transformer.fit(X, y)
    X_new = transformer.transform(X)
    mask = np.abs(transformer.estimator_.coef_) > 1e-5
    assert_array_almost_equal(X_new, X[:, mask])

def test_2d_coef():
    )

        for order in [1, 2, np.inf]:
            # Fit SelectFromModel a multi-class problem
            transformer = SelectFromModel(
                estimator=LogisticRegression(), threshold=threshold, norm_order=order
            )
            transformer.fit(X, y)
            assert hasattr(transformer.estimator_, "coef_")
            X_new = transformer.transform(X)
            assert X_new.shape[1] < X.shape[1]

            # Manually check that the norm is correctly performed
            importances = np.linalg.norm(est.coef_, axis=0, ord=order)
            feature_mask = importances > func(importances)
            assert_array_almost_equal(X_new, X[:, feature_mask])

    est = PassiveAggressiveClassifier(
        random_state=0, shuffle=False, max_iter=5, tol=None
    )
    transformer.partial_fit(data, y, classes=np.unique(y))
    old_model = transformer.estimator_
    new_model = transformer.estimator_
    assert old_model is new_model

    X_transform = transformer.transform(data)
    transformer.fit(np.vstack((data, data)), np.concatenate((y, y)))
    assert_array_almost_equal(X_transform, transformer.transform(data))

    # check that if est doesn't have partial_fit, neither does SelectFromModel
    transformer = SelectFromModel(estimator=RandomForestClassifier())
    assert not hasattr(transformer, "partial_fit")

def test_calling_fit_reinitializes():
    est = LinearSVC(random_state=0)
    transformer.set_params(estimator__C=100)
    assert transformer.estimator_.C == 100

def test_prefit():
    # Test all possible combinations of the prefit parameter.

    # Passing a prefit parameter with the selected model
    # and fitting a unfit model with prefit=False should give same results.
    clf = SGDClassifier(alpha=0.1, max_iter=10, shuffle=True, random_state=0, tol=None)
    model = SelectFromModel(clf)
    model.fit(data, y)
    X_transform = model.transform(data)
    model = SelectFromModel(clf, prefit=True)
    assert_array_almost_equal(model.transform(data), X_transform)
    assert model.estimator_ is not clf

    # Check that the model is rewritten if prefit=False and a fitted model is
    # passed
    model = SelectFromModel(clf, prefit=False)

    # Check that passing an unfitted estimator with `prefit=True` raises a
    # `ValueError`
    err_msg = "When `prefit=True`, `estimator` is expected to be a fitted estimator."
        model.partial_fit(data, y)
        model.transform(data)

    # Check that the internal parameters of prefitted model are not changed
    # when calling `fit` or `partial_fit` with `prefit=True`
    clf = SGDClassifier(alpha=0.1, max_iter=10, shuffle=True, tol=None).fit(data, y)
    assert_allclose(model.estimator_.coef_, clf.coef_)
    model.partial_fit(data, y)

def test_prefit_max_features():
    """Check the interaction between `prefit` and `max_features`."""
    # case 1: an error should be raised at `transform` if `fit` was not called to
    # validate the attributes
    estimator = RandomForestClassifier(n_estimators=5, random_state=0)
    estimator.fit(data, y)
    model = SelectFromModel(estimator, prefit=True, max_features=lambda X: X.shape[1])

    err_msg = (
        "When `prefit=True` and `max_features` is a callable, call `fit` "
        "before calling `transform`."
    )

    # case 2: `max_features` is not validated and different from an integer
    # FIXME: we cannot validate the upper bound of the attribute at transform
    # and we should force calling `fit` if we intend to force the attribute
    # to have such an upper bound.
    max_features = 2.5
    model.set_params(max_features=max_features)
    with pytest.raises(ValueError, match="`max_features` must be an integer"):

def test_prefit_get_feature_names_out():
    """Check the interaction between prefit and the feature names."""
    clf = RandomForestClassifier(n_estimators=2, random_state=0)
    model = SelectFromModel(clf, prefit=True, max_features=1)

    name = type(model).__name__
    err_msg = (
        f"This {name} instance is not fitted yet. Call 'fit' with "
    )
        model.get_feature_names_out()

    feature_names = model.get_feature_names_out()
    assert feature_names == ["x3"]

def test_threshold_string():
    model = SelectFromModel(est, threshold="0.5*mean")

    # Calculate the threshold from the estimator directly.
    est.fit(data, y)
    threshold = 0.5 * np.mean(est.feature_importances_)
    mask = est.feature_importances_ > threshold
    assert_array_almost_equal(X_transform, data[:, mask])

def test_threshold_without_refitting():
    # Test that the threshold can be set without refitting the model.
    model = SelectFromModel(clf, threshold="0.1 * mean")

    # Set a higher threshold to filter out more features.
    model.threshold = "1.0 * mean"
    assert X_transform.shape[1] > model.transform(data).shape[1]

def test_fit_accepts_nan_inf():
    # Test that fit doesn't check for np.inf and np.nan values.
    clf = HistGradientBoostingClassifier(random_state=0)

    model = SelectFromModel(estimator=clf)

    nan_data = data.copy()
    nan_data[0] = np.nan
    nan_data[1] = np.inf

def test_transform_accepts_nan_inf():
    # Test that transform doesn't check for np.inf and np.nan values.
    clf = NaNTagRandomForest(n_estimators=100, random_state=0)

    model.fit(nan_data, y)

    model.transform(nan_data)

def test_allow_nan_tag_comes_from_estimator():
    allow_nan_est = NaNTag()
    model = SelectFromModel(estimator=allow_nan_est)
    assert model.__sklearn_tags__().input_tags.allow_nan is True

    no_nan_est = NoNaNTag()
    model = SelectFromModel(estimator=no_nan_est)
    assert model.__sklearn_tags__().input_tags.allow_nan is False

def _pca_importances(pca_estimator):
    return np.abs(pca_estimator.explained_variance_)

def test_importance_getter(estimator, importance_getter):
    selector = SelectFromModel(
        estimator, threshold="mean", importance_getter=importance_getter
    )
    selector.fit(data, y)
    assert selector.transform(data).shape[1] == 1

def test_select_from_model_pls(PLSEstimator):
    """Check the behaviour of SelectFromModel with PLS estimators.

    """
    X, y = make_friedman1(n_samples=50, n_features=10, random_state=0)
    estimator = PLSEstimator(n_components=1)
    model = make_pipeline(SelectFromModel(estimator), estimator).fit(X, y)
    assert model.score(X, y) > 0.5

def test_estimator_does_not_support_feature_names():
    """SelectFromModel works with estimators that do not support feature_names_in_.

    Non-regression test for #21949.
    """
    X, y = datasets.load_iris(as_frame=True, return_X_y=True)
    all_feature_names = set(X.columns)

    def importance_getter(estimator):
        return np.arange(X.shape[1])

        MinimalClassifier(), importance_getter=importance_getter

    # selector learns the feature names itself
    assert_array_equal(selector.feature_names_in_, X.columns)

    feature_names_out = set(selector.get_feature_names_out())
    assert feature_names_out < all_feature_names

        selector.transform(X.iloc[1:3])

def test_partial_fit_validate_max_features(error, err_msg, max_features):
    """Test that partial_fit from SelectFromModel validates `max_features`."""
    )

    with pytest.raises(error, match=err_msg):
        SelectFromModel(
            estimator=SGDClassifier(), max_features=max_features
        ).partial_fit(X, y, classes=[0, 1])

def test_partial_fit_validate_feature_names(as_frame):
    """Test that partial_fit from SelectFromModel validates `feature_names_in_`."""
    X, y = datasets.load_iris(as_frame=as_frame, return_X_y=True)

    selector = SelectFromModel(estimator=SGDClassifier(), max_features=4).partial_fit(
        X, y, classes=[0, 1, 2]
    )
        assert_array_equal(selector.feature_names_in_, X.columns)
    else:
        assert not hasattr(selector, "feature_names_in_")

def test_from_model_estimator_attribute_error():
    """Check that we raise the proper AttributeError when the estimator

    """
    # `LinearRegression` does not implement 'partial_fit' and should raise an
    from_model = SelectFromModel(estimator=LinearRegression())

    outer_msg = "This 'SelectFromModel' has no attribute 'partial_fit'"
    inner_msg = "'LinearRegression' object has no attribute 'partial_fit'"
        from_model.fit(data, y).partial_fit(data)

class MockClassifier(ClassifierMixin, BaseEstimator):
    """
    Dummy classifier to test recursive feature elimination
    """

    def __init__(self, foo_param=0):

        assert len(X) == len(y)
        self.coef_ = np.ones(X.shape[1], dtype=np.float64)
        self.classes_ = sorted(set(y))

        return np.ones(T.shape[0])

    predict_proba = predict
    decision_function = predict
    transform = predict

    def score(self, X=None, y=None):
        return 0.0

        return {"foo_param": self.foo_param}

def test_rfe_features_importance():
    generator = check_random_state(0)
    # Add some irrelevant features. Random seed is set to make sure that
    # irrelevant features are always irrelevant.
    X = np.c_[iris.data, generator.normal(size=(len(iris.data), 6))]

    clf = RandomForestClassifier(n_estimators=20, random_state=generator, max_depth=2)
    rfe = RFE(estimator=clf, n_features_to_select=4, step=0.1)
    rfe.fit(X, y)
    assert len(rfe.ranking_) == X.shape[1]

    clf_svc = SVC(kernel="linear")
    rfe_svc = RFE(estimator=clf_svc, n_features_to_select=4, step=0.1)
    rfe_svc.fit(X, y)

    # Check if the supports are equal
    assert_array_equal(rfe.get_support(), rfe_svc.get_support())

def test_rfe(csr_container):

    # dense model
    clf = SVC(kernel="linear")
    X_r = rfe.transform(X)
    clf.fit(X_r, y)

    # sparse model
    clf_sparse = SVC(kernel="linear")
    rfe_sparse = RFE(estimator=clf_sparse, n_features_to_select=4, step=0.1)
    rfe_sparse.fit(X_sparse, y)
    X_r_sparse = rfe_sparse.transform(X_sparse)

    assert X_r.shape == iris.data.shape
    assert_array_almost_equal(X_r[:10], iris.data[:10])

    assert_array_almost_equal(rfe.predict(X), clf.predict(iris.data))
    assert rfe.score(X, y) == clf.score(iris.data, iris.target)
    assert_array_almost_equal(X_r, X_r_sparse.toarray())

def test_RFE_fit_score_params():
    # Make sure RFE passes the metadata down to fit and score methods of the
    # underlying estimator
    class TestEstimator(BaseEstimator, ClassifierMixin):
        def fit(self, X, y, prop=None):
            if prop is None:
                raise ValueError("fit: prop cannot be None")
            self.svc_ = SVC(kernel="linear").fit(X, y)
            self.coef_ = self.svc_.coef_

        def score(self, X, y, prop=None):
                raise ValueError("score: prop cannot be None")
            return self.svc_.score(X, y)

    with pytest.raises(ValueError, match="fit: prop cannot be None"):
        RFE(estimator=TestEstimator()).fit(X, y)
    with pytest.raises(ValueError, match="score: prop cannot be None"):
        RFE(estimator=TestEstimator()).fit(X, y, prop="foo").score(X, y)

    RFE(estimator=TestEstimator()).fit(X, y, prop="foo").score(X, y, prop="foo")

def test_rfe_percent_n_features():
    # test that the results are the same
    # there are 10 features in the data. We select 40%.
    rfe_num = RFE(estimator=clf, n_features_to_select=4, step=0.1)
    rfe_num.fit(X, y)

    rfe_perc = RFE(estimator=clf, n_features_to_select=0.4, step=0.1)
    rfe_perc.fit(X, y)

    assert_array_equal(rfe_perc.ranking_, rfe_num.ranking_)
    assert_array_equal(rfe_perc.support_, rfe_num.support_)

def test_rfe_mockclassifier():

def test_rfecv(csr_container):
    y = list(iris.target)  # regression test: list should be supported

    # Test using the score function
    rfecv = RFECV(estimator=SVC(kernel="linear"), step=1)
    rfecv.fit(X, y)
    # non-regression test for missing worst feature:

    for key in rfecv.cv_results_.keys():
        assert len(rfecv.cv_results_[key]) == X.shape[1]

    assert len(rfecv.ranking_) == X.shape[1]
    X_r = rfecv.transform(X)

    # All the noisy variable were filtered out
    assert_array_equal(X_r, iris.data)

    # same in sparse
    rfecv_sparse = RFECV(estimator=SVC(kernel="linear"), step=1)
    rfecv_sparse.fit(X_sparse, y)
    X_r_sparse = rfecv_sparse.transform(X_sparse)
    assert_array_equal(X_r_sparse.toarray(), iris.data)

    # Test using a customized loss function
    scoring = make_scorer(zero_one_loss, greater_is_better=False)
    rfecv = RFECV(estimator=SVC(kernel="linear"), step=1, scoring=scoring)
    ignore_warnings(rfecv.fit)(X, y)

    # Test using a scorer
    scorer = get_scorer("accuracy")
    rfecv = RFECV(estimator=SVC(kernel="linear"), step=1, scoring=scorer)

    # Test fix on cv_results_
    def test_scorer(estimator, X, y):
        return 1.0

    rfecv = RFECV(estimator=SVC(kernel="linear"), step=1, scoring=test_scorer)

    # In the event of cross validation score ties, the expected behavior of
    # RFECV is to return the FEWEST features that maximize the CV score.
    # Because test_scorer always returns 1.0 in this example, RFECV should
    # reduce the dimensionality to a single feature (i.e. n_features_ = 1)
    assert rfecv.n_features_ == 1

    # Same as the first two tests, but with step=2
    rfecv = RFECV(estimator=SVC(kernel="linear"), step=2)

        assert len(rfecv.cv_results_[key]) == 6

    rfecv_sparse = RFECV(estimator=SVC(kernel="linear"), step=2)

    # Verifying that steps < 1 don't blow up.
    rfecv_sparse = RFECV(estimator=SVC(kernel="linear"), step=0.2)

def test_rfecv_mockclassifier():

    rfecv = RFECV(estimator=MockClassifier(), step=1)

def test_rfecv_verbose_output():
    # Check verbose=1 is producing an output.
    import sys

    y = list(iris.target)

    rfecv = RFECV(estimator=SVC(kernel="linear"), step=1, verbose=1)

    assert len(verbose_output.readline()) > 0

def test_rfecv_cv_results_size(global_random_seed):
    generator = check_random_state(global_random_seed)

    # Non-regression test for varying combinations of step and
    # min_features_to_select.
    for step, min_features_to_select in [[2, 1], [2, 2], [3, 3]]:
        rfecv = RFECV(
            estimator=MockClassifier(),
            step=step,
            min_features_to_select=min_features_to_select,
        )
        rfecv.fit(X, y)

        score_len = np.ceil((X.shape[1] - min_features_to_select) / step) + 1

        for key in rfecv.cv_results_.keys():
            assert len(rfecv.cv_results_[key]) == score_len

        assert len(rfecv.ranking_) == X.shape[1]
        assert rfecv.n_features_ >= min_features_to_select

def test_rfe_estimator_tags():
    rfe = RFE(SVC(kernel="linear"))
    assert is_classifier(rfe)
    # make sure that cross-validation is stratified
    score = cross_val_score(rfe, iris.data, iris.target)
    assert score.min() > 0.7

def test_rfe_min_step(global_random_seed):
        n_samples=50, n_features=n_features, random_state=global_random_seed
    )
    estimator = SVR(kernel="linear")

    # Test when floor(step * n_features) <= 0
    selector = RFE(estimator, step=0.01)
    sel = selector.fit(X, y)
    assert sel.support_.sum() == n_features // 2

    # Test when step is between (0,1) and floor(step * n_features) > 0
    selector = RFE(estimator, step=0.20)

    # Test when step is an integer
    selector = RFE(estimator, step=5)

def test_number_of_subsets_of_features(global_random_seed):
    # In RFE, 'number_of_subsets_of_features'
    # = the number of iterations in '_fit'
    # = max(ranking_)
    # = 1 + (n_features + step - n_features_to_select - 1) // step
    # After optimization #4534, this number
    # = 1 + np.ceil((n_features - n_features_to_select) / float(step))
    # This test case is to test their equivalence, refer to #4534 and #3824

    def formula1(n_features, n_features_to_select, step):
        return 1 + ((n_features + step - n_features_to_select - 1) // step)

    def formula2(n_features, n_features_to_select, step):
        return 1 + np.ceil((n_features - n_features_to_select) / float(step))

    # RFE
    # Case 1, n_features - n_features_to_select is divisible by step
    # Case 2, n_features - n_features_to_select is not divisible by step
    n_features_list = [11, 11]
    n_features_to_select_list = [3, 3]
    step_list = [2, 3]
    for n_features, n_features_to_select, step in zip(
        n_features_list, n_features_to_select_list, step_list
    ):
        generator = check_random_state(global_random_seed)
        X = generator.normal(size=(100, n_features))
        y = generator.rand(100).round()
        rfe = RFE(
            estimator=SVC(kernel="linear"),
        )
        rfe.fit(X, y)
        # this number also equals to the maximum of ranking_
        assert np.max(rfe.ranking_) == formula1(n_features, n_features_to_select, step)
        assert np.max(rfe.ranking_) == formula2(n_features, n_features_to_select, step)

    # In RFECV, 'fit' calls 'RFE._fit'
    # 'number_of_subsets_of_features' of RFE
    # = the size of each score in 'cv_results_' of RFECV
    # = the number of iterations of the for loop before optimization #4534

    # RFECV, n_features_to_select = 1
    # Case 1, n_features - 1 is divisible by step
    # Case 2, n_features - 1 is not divisible by step

    n_features_to_select = 1
    n_features_list = [11, 10]
    step_list = [2, 2]
    for n_features, step in zip(n_features_list, step_list):
        rfecv = RFECV(estimator=SVC(kernel="linear"), step=step)

            assert len(rfecv.cv_results_[key]) == formula1(
                n_features, n_features_to_select, step
            )
            assert len(rfecv.cv_results_[key]) == formula2(
            )

def test_rfe_cv_n_jobs(global_random_seed):

    rfecv = RFECV(estimator=SVC(kernel="linear"))
    rfecv_ranking = rfecv.ranking_

    rfecv_cv_results_ = rfecv.cv_results_

    rfecv.set_params(n_jobs=2)
    assert_array_almost_equal(rfecv.ranking_, rfecv_ranking)

    assert rfecv_cv_results_.keys() == rfecv.cv_results_.keys()
    for key in rfecv_cv_results_.keys():
        assert rfecv_cv_results_[key] == pytest.approx(rfecv.cv_results_[key])

def test_rfe_cv_groups():
    number_groups = 4
    groups = np.floor(np.linspace(0, number_groups, len(iris.target)))
    y = (iris.target > 0).astype(int)

    est_groups = RFECV(
        estimator=RandomForestClassifier(random_state=generator),
        step=1,
        scoring="accuracy",
    )
    est_groups.fit(X, y, groups=groups)
    assert est_groups.n_features_ > 0

def test_rfe_wrapped_estimator(importance_getter, selector, expected_n_features):
    # https://github.com/scikit-learn/scikit-learn/issues/15312
    estimator = LinearSVR(random_state=0)

    log_estimator = TransformedTargetRegressor(
        regressor=estimator, func=np.log, inverse_func=np.exp
    )

    selector = selector(log_estimator, importance_getter=importance_getter)
    assert sel.support_.sum() == expected_n_features

def test_rfe_importance_getter_validation(importance_getter, err_type, Selector):
    X, y = make_friedman1(n_samples=50, n_features=10, random_state=42)
    estimator = LinearSVR()
    )

    with pytest.raises(err_type):
        model = Selector(log_estimator, importance_getter=importance_getter)

def test_rfe_allow_nan_inf_in_x(cv):

    # add nan and inf value to X
    X[0][1] = np.inf

    if cv is not None:
        rfe = RFECV(estimator=clf, cv=cv)
    else:
        rfe = RFE(estimator=clf)
    rfe.transform(X)

def test_w_pipeline_2d_coef_():
    pipeline = make_pipeline(StandardScaler(), LogisticRegression())

    data, y = load_iris(return_X_y=True)
    sfm = RFE(
        pipeline,
        n_features_to_select=2,
        importance_getter="named_steps.logisticregression.coef_",
    )

    sfm.fit(data, y)
    assert sfm.transform(data).shape[1] == 2

def test_rfecv_std_and_mean(global_random_seed):

    split_keys = [
        key
        for key in rfecv.cv_results_.keys()
        if re.search(r"split\d+_test_score", key)
    ]
    cv_scores = np.asarray([rfecv.cv_results_[key] for key in split_keys])
    expected_mean = np.mean(cv_scores, axis=0)
    expected_std = np.std(cv_scores, axis=0)

    assert_allclose(rfecv.cv_results_["mean_test_score"], expected_mean)
    assert_allclose(rfecv.cv_results_["std_test_score"], expected_std)

def test_rfecv_cv_results_n_features(
    min_features_to_select,
    step,
    cv_results_n_features,
):
        n_samples=20, n_features=n_features, n_informative=n_features, n_redundant=0
    )
    rfecv = RFECV(
        estimator=SVC(kernel="linear"),
        step=step,
        min_features_to_select=min_features_to_select,
    )
    assert_array_equal(rfecv.cv_results_["n_features"], cv_results_n_features)
        len(value) == len(rfecv.cv_results_["n_features"])
        for value in rfecv.cv_results_.values()
    )

def test_multioutput(ClsRFE):
    X = np.random.normal(size=(10, 3))
    y = np.random.randint(2, size=(10, 2))
    clf = RandomForestClassifier(n_estimators=5)
    rfe_test = ClsRFE(clf)
    rfe_test.fit(X, y)

def test_pipeline_with_nans(ClsRFE):
    """Check that RFE works with pipeline that accept nans.

    Non-regression test for gh-21743.
    """

        SimpleImputer(),
        StandardScaler(),
    )

    fs = ClsRFE(
        estimator=pipe,
    )

def test_rfe_pls(ClsRFE, PLSEstimator):
    """Check the behaviour of RFE with PLS estimators.

    """
    selector = ClsRFE(estimator, step=1).fit(X, y)
    assert selector.score(X, y) > 0.5

def test_rfe_estimator_attribute_error():

    """

    # `LinearRegression` does not implement 'decision_function' and should raise an
    rfe = RFE(estimator=LinearRegression())

    outer_msg = "This 'RFE' has no attribute 'decision_function'"
    inner_msg = "'LinearRegression' object has no attribute 'decision_function'"
        rfe.fit(iris.data, iris.target).decision_function(iris.data)

def test_rfe_n_features_to_select_warning(ClsRFE, param):
    """Check if the correct warning is raised when trying to initialize a RFE
    object with a n_features_to_select attribute larger than the number of
    features present in the X variable that is passed to the fit method
    """
    X, y = make_classification(n_features=20, random_state=0)

    with pytest.warns(UserWarning, match=f"{param}=21 > n_features=20"):
        # Create RFE/RFECV with n_features_to_select/min_features_to_select
        # larger than the number of features present in the X variable
        clsrfe = ClsRFE(estimator=LogisticRegression(), **{param: 21})
        clsrfe.fit(X, y)

def test_rfe_with_sample_weight():
    """Test that `RFE` works correctly with sample weights."""

    # Assign the first half of the samples with twice the weight
    sample_weight[: n_samples // 2] = 2

    # Duplicate the first half of the data samples to replicate the effect
    # of sample weights for comparison

    estimator = SVC(kernel="linear")

    rfe_sw = RFE(estimator=estimator, step=0.1)
    rfe_sw.fit(X, y, sample_weight=sample_weight)

    rfe = RFE(estimator=estimator, step=0.1)
    rfe.fit(X2, y2)

    assert_array_equal(rfe_sw.ranking_, rfe.ranking_)

    # Also verify that when sample weights are not doubled the results
    # are different from the duplicated data
    rfe_sw_2 = RFE(estimator=estimator, step=0.1)
    sample_weight_2 = np.ones_like(y)
    rfe_sw_2.fit(X, y, sample_weight=sample_weight_2)

    assert not np.array_equal(rfe_sw_2.ranking_, rfe.ranking_)

def test_rfe_with_joblib_threading_backend(global_random_seed):

    rfe = RFECV(
        estimator=clf,
        n_jobs=2,
    )

    ranking_ref = rfe.ranking_

    assert_array_equal(ranking_ref, rfe.ranking_)

def test_results_per_cv_in_rfecv(global_random_seed):
    """
    Test that the results of RFECV are consistent across the different folds
    in terms of length of the arrays.
    """

        n_jobs=2,
        cv=5,
    )

    assert len(rfecv.cv_results_["split1_test_score"]) == len(
        rfecv.cv_results_["split2_test_score"]
    )
    assert len(rfecv.cv_results_["split1_support"]) == len(
        rfecv.cv_results_["split2_support"]
    )
    assert len(rfecv.cv_results_["split1_ranking"]) == len(
        rfecv.cv_results_["split2_ranking"]
    )

def test_bad_n_features_to_select():
    X, y = make_regression(n_features=n_features)
    sfs = SequentialFeatureSelector(LinearRegression(), n_features_to_select=n_features)
    with pytest.raises(ValueError, match="n_features_to_select must be < n_features"):
        sfs.fit(X, y)

def test_n_features_to_select(direction, n_features_to_select):
    # Make sure n_features_to_select is respected

    X, y = make_regression(n_features=n_features, random_state=0)
    sfs = SequentialFeatureSelector(
        n_features_to_select=n_features_to_select,
        direction=direction,
        cv=2,
    )
    sfs.fit(X, y)

    if n_features_to_select == "auto":
        n_features_to_select = n_features // 2

    assert sfs.get_support(indices=True).shape[0] == n_features_to_select
    assert sfs.n_features_to_select_ == n_features_to_select
    assert sfs.transform(X).shape[1] == n_features_to_select

def test_n_features_to_select_auto(direction):
    """Check the behaviour of `n_features_to_select="auto"` with different
    values for the parameter `tol`.
    """

    tol = 1e-3
        tol=tol,
        cv=2,
    )

    max_features_to_select = n_features - 1

    assert sfs.get_support(indices=True).shape[0] <= max_features_to_select
    assert sfs.n_features_to_select_ <= max_features_to_select
    assert sfs.transform(X).shape[1] <= max_features_to_select
    assert sfs.get_support(indices=True).shape[0] == sfs.n_features_to_select_

def test_n_features_to_select_stopping_criterion(direction):
    """Check the behaviour stopping criterion for feature selection
    depending on the values of `n_features_to_select` and `tol`.

    When `direction` is `'forward'`, select a new features at random
    among those not currently selected in selector.support_,
    build a new version of the data that includes all the features
    in selector.support_ + this newly selected feature.
    And check that the cross-validation score of the model trained on
    this new dataset variant is lower than the model with
    the selected forward selected features or at least does not improve
    by more than the tol margin.

    When `direction` is `'backward'`, instead of adding a new feature
    to selector.support_, try to remove one of those selected features at random
    And check that the cross-validation score is either decreasing or
    not improving by more than the tol margin.
    """

    X, y = make_regression(n_features=50, n_informative=10, random_state=0)

    tol = 1e-3

        tol=tol,
        cv=2,
    )
    selected_X = sfs.transform(X)

    added_candidates = list(set(range(X.shape[1])) - set(sfs.get_support(indices=True)))
    added_X = np.hstack(
        [
            selected_X,
            (X[:, rng.choice(added_candidates)])[:, np.newaxis],
        ]
    )

    removed_candidate = rng.choice(list(range(sfs.n_features_to_select_)))
    removed_X = np.delete(selected_X, removed_candidate, axis=1)

    plain_cv_score = cross_val_score(LinearRegression(), X, y, cv=2).mean()
    sfs_cv_score = cross_val_score(LinearRegression(), selected_X, y, cv=2).mean()
    added_cv_score = cross_val_score(LinearRegression(), added_X, y, cv=2).mean()
    removed_cv_score = cross_val_score(LinearRegression(), removed_X, y, cv=2).mean()

    assert sfs_cv_score >= plain_cv_score

    if direction == "forward":
        assert (sfs_cv_score - added_cv_score) <= tol
        assert (sfs_cv_score - removed_cv_score) >= tol
    else:
        assert (added_cv_score - sfs_cv_score) <= tol
        assert (removed_cv_score - sfs_cv_score) <= tol

def test_n_features_to_select_float(direction, n_features_to_select, expected):
    # Test passing a float as n_features_to_select
    X, y = make_regression(n_features=10)
        cv=2,
    )
    assert sfs.n_features_to_select_ == expected

def test_sanity(seed, direction, n_features_to_select, expected_selected_features):
    # Basic sanity check: 3 features, only f0 and f2 are correlated with the
    # target, f2 having a stronger correlation than f0. We expect f1 to be
    # dropped, and f2 to always be selected.

    X = rng.randn(n_samples, 3)
    y = 3 * X[:, 0] - 10 * X[:, 2]

        cv=2,
    )
    assert_array_equal(sfs.get_support(indices=True), expected_selected_features)

def test_sparse_support(csr_container):
    # Make sure sparse data is supported

        LinearRegression(), n_features_to_select="auto", cv=2
    )
    sfs.transform(X)

def test_nan_support():
    # Make sure nans are OK if the underlying estimator supports nans

    n_samples, n_features = 40, 4
    X, y = make_regression(n_samples, n_features, random_state=0)
    nan_mask = rng.randint(0, 2, size=(n_samples, n_features), dtype=bool)
    X[nan_mask] = np.nan
        HistGradientBoostingRegressor(), n_features_to_select="auto", cv=2
    )

        # LinearRegression does not support nans
        SequentialFeatureSelector(
            LinearRegression(), n_features_to_select="auto", cv=2

def test_pipeline_support():
    # Make sure that pipelines can be passed into SFS and that SFS can be
    # passed into a pipeline

    n_samples, n_features = 50, 3

    # pipeline in SFS
    pipe = make_pipeline(StandardScaler(), LinearRegression())
    sfs = SequentialFeatureSelector(pipe, n_features_to_select="auto", cv=2)

    # SFS in pipeline
    )
    pipe = make_pipeline(StandardScaler(), sfs)
    pipe.transform(X)

def test_unsupervised_model_fit(n_features_to_select):
    # Make sure that models without classification labels are not being
    # validated

    X, y = make_blobs(n_features=4)
        KMeans(n_init=1),
    )
    sfs.fit(X)

def test_no_y_validation_model_fit(y):
    # Make sure that other non-conventional y labels are not accepted

    X, clusters = make_blobs(n_features=6)
        KMeans(),
        n_features_to_select=3,
    )

    with pytest.raises((TypeError, ValueError)):

def test_forward_neg_tol_error():
    """Check that we raise an error when tol<0 and direction='forward'"""
    X, y = make_regression(n_features=10, random_state=0)
        tol=-1e-3,
    )

    with pytest.raises(ValueError, match="tol must be strictly positive"):

def test_backward_neg_tol():
    """Check that SequentialFeatureSelector works negative tol

    non-regression test for #25525
    """
    initial_score = lr.fit(X, y).score(X, y)

        lr,
        direction="backward",
    )
    Xr = sfs.fit_transform(X, y)
    new_score = lr.fit(Xr, y).score(Xr, y)

    assert 0 < sfs.get_support().sum() < X.shape[1]
    assert new_score < initial_score

def test_cv_generator_support():
    """Check that no exception raised when cv is generator

    non-regression test for #25957
    """

    groups = np.zeros_like(y, dtype=int)
    groups[y.size // 2 :] = 1

    cv = LeaveOneGroupOut()
    splits = cv.split(X, y, groups=groups)

    knc = KNeighborsClassifier(n_neighbors=5)

    sfs = SequentialFeatureSelector(knc, n_features_to_select=5, cv=splits)

def test_fit_rejects_params_with_no_routing_enabled():
    est = LinearRegression()
    sfs = SequentialFeatureSelector(estimator=est)

        sfs.fit(X, y, sample_weight=np.ones_like(y))

def test_f_oneway_vs_scipy_stats():
    # Test that our f_oneway gives the same result as scipy.stats
    X1 = rng.randn(10, 3)
    X2 = 1 + rng.randn(10, 3)
    f, pv = stats.f_oneway(X1, X2)
    f2, pv2 = f_oneway(X1, X2)
    assert np.allclose(f, f2)
    assert np.allclose(pv, pv2)

def test_f_oneway_ints():
    # Smoke test f_oneway on integers: that it does raise casting errors
    # with recent numpys
    X = rng.randint(10, size=(10, 10))
    fint, pint = f_oneway(X, y)

    # test that is gives the same result as with float
    f, p = f_oneway(X.astype(float), y)
    assert_array_almost_equal(f, fint, decimal=4)
    assert_array_almost_equal(p, pint, decimal=4)

def test_f_classif(csr_container):
    # Test whether the F test yields meaningful results
    # on a simple simulated classification problem
        n_redundant=2,
        n_classes=8,
        flip_y=0.0,
        class_sep=10,
    )

    F, pv = f_classif(X, y)
    F_sparse, pv_sparse = f_classif(csr_container(X), y)
    assert (F > 0).all()
    assert (pv > 0).all()
    assert (pv < 1).all()
    assert (pv[:5] < 0.05).all()
    assert (pv[5:] > 1.0e-4).all()
    assert_array_almost_equal(F_sparse, F)
    assert_array_almost_equal(pv_sparse, pv)

def test_r_regression(center):
        n_samples=2000, n_features=20, n_informative=5, shuffle=False, random_state=0
    )

    corr_coeffs = r_regression(X, y, center=center)
    assert (-1 < corr_coeffs).all()
    assert (corr_coeffs < 1).all()

    sparse_X = _convert_container(X, "sparse")

    sparse_corr_coeffs = r_regression(sparse_X, y, center=center)
    assert_allclose(sparse_corr_coeffs, corr_coeffs)

    # Testing against numpy for reference
    Z = np.hstack((X, y[:, np.newaxis]))
    correlation_matrix = np.corrcoef(Z, rowvar=False)
    np_corr_coeffs = correlation_matrix[:-1, -1]
    assert_array_almost_equal(np_corr_coeffs, corr_coeffs, decimal=3)

def test_f_regression(csr_container):
    # on a simple simulated regression problem
        n_samples=200, n_features=20, n_informative=5, shuffle=False, random_state=0
    )

    F, pv = f_regression(X, y)

    # with centering, compare with sparse
    F, pv = f_regression(X, y, center=True)
    F_sparse, pv_sparse = f_regression(csr_container(X), y, center=True)
    assert_allclose(F_sparse, F)
    assert_allclose(pv_sparse, pv)

    # again without centering, compare with sparse
    F, pv = f_regression(X, y, center=False)
    F_sparse, pv_sparse = f_regression(csr_container(X), y, center=False)

def test_f_regression_input_dtype():
    # Test whether f_regression returns the same value
    # for any numeric data_type
    y = np.arange(10).astype(int)

    F1, pv1 = f_regression(X, y)
    F2, pv2 = f_regression(X, y.astype(float))
    assert_allclose(F1, F2, 5)
    assert_allclose(pv1, pv2, 5)

def test_f_regression_center():
    # Test whether f_regression preserves dof according to 'center' argument
    # We use two centered variates so we have a simple relationship between
    # F-score with variates centering and F-score without variates centering.
    # Create toy example
    X = np.arange(-5, 6).reshape(-1, 1)  # X has zero mean
    n_samples = X.size
    Y = np.ones(n_samples)
    Y[::2] *= -1.0
    Y[0] = 0.0  # have Y mean being null

    F1, _ = f_regression(X, Y, center=True)
    F2, _ = f_regression(X, Y, center=False)
    assert_allclose(F1 * (n_samples - 1.0) / (n_samples - 2.0), F2)
    assert_almost_equal(F2[0], 0.232558139)  # value from statsmodels OLS

def test_r_regression_force_finite(X, y, expected_corr_coef, force_finite):
    """Check the behaviour of `force_finite` for some corner cases with `r_regression`.

    https://github.com/scikit-learn/scikit-learn/issues/15672
    """
        corr_coef = r_regression(X, y, force_finite=force_finite)
    np.testing.assert_array_almost_equal(corr_coef, expected_corr_coef)

def test_f_regression_corner_case(
    X, y, expected_f_statistic, expected_p_values, force_finite
):
    """Check the behaviour of `force_finite` for some corner cases with `f_regression`.

    """
        f_statistic, p_values = f_regression(X, y, force_finite=force_finite)
    np.testing.assert_array_almost_equal(f_statistic, expected_f_statistic)
    np.testing.assert_array_almost_equal(p_values, expected_p_values)

def test_f_classif_multi_class():
    )

def test_select_percentile_classif():
    # Test whether the relative univariate feature selection
    # gets the correct items in a simple classification problem
    # with the percentile heuristic
    )

    univariate_filter = SelectPercentile(f_classif, percentile=25)
    X_r = univariate_filter.fit(X, y).transform(X)
    X_r2 = (
        GenericUnivariateSelect(f_classif, mode="percentile", param=25)
        .fit(X, y)
        .transform(X)
    )
    assert_array_equal(X_r, X_r2)
    support = univariate_filter.get_support()
    gtruth = np.zeros(20)
    gtruth[:5] = 1
    assert_array_equal(support, gtruth)

def test_select_percentile_classif_sparse(csr_container):
    )
    X_r2 = (
        .fit(X, y)
    )
    assert_array_equal(X_r.toarray(), X_r2.toarray())

    X_r2inv = univariate_filter.inverse_transform(X_r2)
    assert sparse.issparse(X_r2inv)
    support_mask = safe_mask(X_r2inv, support)
    assert X_r2inv.shape == X.shape
    assert_array_equal(X_r2inv[:, support_mask].toarray(), X_r.toarray())
    # Check other columns are empty
    assert X_r2inv.nnz == X_r.nnz

def test_select_kbest_classif():
    # with the k best heuristic
    )

    univariate_filter = SelectKBest(f_classif, k=5)
    X_r2 = (
        GenericUnivariateSelect(f_classif, mode="k_best", param=5)
        .fit(X, y)
    )

def test_select_kbest_all():
    # Test whether k="all" correctly returns all features.
        n_samples=20, n_features=10, shuffle=False, random_state=0
    )

    univariate_filter = SelectKBest(f_classif, k="all")
    assert_array_equal(X, X_r)
    # https://github.com/scikit-learn/scikit-learn/issues/24949
    X_r2 = (
        GenericUnivariateSelect(f_classif, mode="k_best", param="all")
        .fit(X, y)
    )

def test_select_kbest_zero(dtype_in):
    # Test whether k=0 correctly returns no features.
    )
    X = X.astype(dtype_in)

    univariate_filter = SelectKBest(f_classif, k=0)
    univariate_filter.fit(X, y)
    gtruth = np.zeros(10, dtype=bool)
    with pytest.warns(UserWarning, match="No features were selected"):
        X_selected = univariate_filter.transform(X)
    assert X_selected.shape == (20, 0)
    assert X_selected.dtype == dtype_in

def test_select_heuristics_classif():
    # with the fdr, fwe and fpr heuristics
    )

    univariate_filter = SelectFwe(f_classif, alpha=0.01)
    for mode in ["fdr", "fpr", "fwe"]:
        X_r2 = (
            GenericUnivariateSelect(f_classif, mode=mode, param=0.01)
            .fit(X, y)
            .transform(X)
        )
        assert_array_equal(X_r, X_r2)
        support = univariate_filter.get_support()
        assert_allclose(support, gtruth)

def assert_best_scores_kept(score_filter):
    scores = score_filter.scores_
    support = score_filter.get_support()
    assert_allclose(np.sort(scores[support]), np.sort(scores)[-support.sum() :])

def test_select_percentile_regression():
    # gets the correct items in a simple regression problem
    )

    univariate_filter = SelectPercentile(f_regression, percentile=25)
    assert_best_scores_kept(univariate_filter)
    X_r2 = (
        GenericUnivariateSelect(f_regression, mode="percentile", param=25)
        .fit(X, y)
    )
    X_2 = X.copy()
    X_2[:, np.logical_not(support)] = 0
    assert_array_equal(X_2, univariate_filter.inverse_transform(X_r))
    # Check inverse_transform respects dtype
        X_2.astype(bool), univariate_filter.inverse_transform(X_r.astype(bool))
    )

def test_select_percentile_regression_full():
    # selects all features when '100%' is asked.
    )

    univariate_filter = SelectPercentile(f_regression, percentile=100)
    X_r2 = (
        GenericUnivariateSelect(f_regression, mode="percentile", param=100)
        .fit(X, y)
    )
    gtruth = np.ones(20)

def test_select_kbest_regression():
        noise=10,
    )

    univariate_filter = SelectKBest(f_regression, k=5)
    X_r2 = (
        GenericUnivariateSelect(f_regression, mode="k_best", param=5)
        .fit(X, y)
    )

def test_select_heuristics_regression():
    # with the fpr, fdr or fwe heuristics
        noise=10,
    )

    univariate_filter = SelectFpr(f_regression, alpha=0.01)
        X_r2 = (
            GenericUnivariateSelect(f_regression, mode=mode, param=0.01)
            .fit(X, y)
        )
        assert_array_equal(support[:5], np.ones((5,), dtype=bool))
        assert np.sum(support[5:] == 1) < 3

def test_boundary_case_ch2():
    # Test boundary case, and always aim to select 1 feature.
    X = np.array([[10, 20], [20, 20], [20, 30]])
    y = np.array([[1], [0], [0]])
    scores, pvalues = chi2(X, y)
    assert_array_almost_equal(scores, np.array([4.0, 0.71428571]))
    assert_array_almost_equal(pvalues, np.array([0.04550026, 0.39802472]))

    filter_fdr = SelectFdr(chi2, alpha=0.1)
    filter_fdr.fit(X, y)
    support_fdr = filter_fdr.get_support()
    assert_array_equal(support_fdr, np.array([True, False]))

    filter_kbest = SelectKBest(chi2, k=1)
    filter_kbest.fit(X, y)
    support_kbest = filter_kbest.get_support()
    assert_array_equal(support_kbest, np.array([True, False]))

    filter_percentile = SelectPercentile(chi2, percentile=50)
    filter_percentile.fit(X, y)
    support_percentile = filter_percentile.get_support()
    assert_array_equal(support_percentile, np.array([True, False]))

    filter_fpr = SelectFpr(chi2, alpha=0.1)
    filter_fpr.fit(X, y)
    support_fpr = filter_fpr.get_support()
    assert_array_equal(support_fpr, np.array([True, False]))

    filter_fwe = SelectFwe(chi2, alpha=0.1)
    filter_fwe.fit(X, y)
    support_fwe = filter_fwe.get_support()
    assert_array_equal(support_fwe, np.array([True, False]))

def test_select_fdr_regression(alpha, n_informative):
    # Test that fdr heuristic actually has low FDR.
    def single_fdr(alpha, n_informative, random_state):
            n_samples=150,
            n_informative=n_informative,
            shuffle=False,
            noise=10,
        )

            # Warnings can be raised when no features are selected
            # (low alpha or very noisy data)
            univariate_filter = SelectFdr(f_regression, alpha=alpha)
            X_r = univariate_filter.fit(X, y).transform(X)
            X_r2 = (
                GenericUnivariateSelect(f_regression, mode="fdr", param=alpha)
                .fit(X, y)
                .transform(X)
            )

        num_false_positives = np.sum(support[n_informative:] == 1)
        num_true_positives = np.sum(support[:n_informative] == 1)

        if num_false_positives == 0:
            return 0.0
        false_discovery_rate = num_false_positives / (
            num_true_positives + num_false_positives
        )
        return false_discovery_rate

    # As per Benjamini-Hochberg, the expected false discovery rate
    # should be lower than alpha:
    # FDR = E(FP / (TP + FP)) <= alpha
    false_discovery_rate = np.mean(
        [single_fdr(alpha, n_informative, random_state) for random_state in range(100)]
    )
    assert alpha >= false_discovery_rate

    # Make sure that the empirical false discovery rate increases
    # with alpha:
    if false_discovery_rate != 0:
        assert false_discovery_rate > alpha / 10

def test_select_fwe_regression():
    # with the fwe heuristic
    )

    univariate_filter = SelectFwe(f_regression, alpha=0.01)
    X_r2 = (
        GenericUnivariateSelect(f_regression, mode="fwe", param=0.01)
        .fit(X, y)
    )
    assert_array_equal(support[:5], np.ones((5,), dtype=bool))
    assert np.sum(support[5:] == 1) < 2

def test_selectkbest_tiebreaking():
    # Test whether SelectKBest actually selects k features in case of ties.
    # Prior to 0.11, SelectKBest would return more features than requested.
    Xs = [[0, 1, 1], [0, 0, 1], [1, 0, 0], [1, 1, 0]]
    y = [1]
    dummy_score = lambda X, y: (X[0], X[0])
    for X in Xs:
        sel = SelectKBest(dummy_score, k=1)
        X1 = ignore_warnings(sel.fit_transform)([X], y)
        assert X1.shape[1] == 1
        assert_best_scores_kept(sel)

        sel = SelectKBest(dummy_score, k=2)
        X2 = ignore_warnings(sel.fit_transform)([X], y)
        assert X2.shape[1] == 2

def test_selectpercentile_tiebreaking():
    # Test if SelectPercentile selects the right n_features in case of ties.
    y = [1]
    for X in Xs:
        sel = SelectPercentile(dummy_score, percentile=34)

        sel = SelectPercentile(dummy_score, percentile=67)

def test_tied_pvalues():
    # Test whether k-best and percentiles work with tied pvalues from chi2.
    # chi2 will return the same p-values for the following features, but it
    # will return different scores.
    X0 = np.array([[10000, 9999, 9998], [1, 1, 1]])
    y = [0, 1]

    for perm in itertools.permutations((0, 1, 2)):
        X = X0[:, perm]
        Xt = SelectKBest(chi2, k=2).fit_transform(X, y)
        assert Xt.shape == (2, 2)
        assert 9998 not in Xt

        Xt = SelectPercentile(chi2, percentile=67).fit_transform(X, y)

def test_scorefunc_multilabel():
    # Test whether k-best and percentiles works with multilabels with chi2.

    X = np.array([[10000, 9999, 0], [100, 9999, 0], [1000, 99, 0]])
    y = [[1, 1], [0, 1], [1, 0]]

    Xt = SelectKBest(chi2, k=2).fit_transform(X, y)
    assert 0 not in Xt

    Xt = SelectPercentile(chi2, percentile=67).fit_transform(X, y)

def test_tied_scores():
    # Test for stable sorting in k-best with tied scores.
    X_train = np.array([[0, 0, 0], [1, 1, 1]])
    y_train = [0, 1]

    for n_features in [1, 2, 3]:
        sel = SelectKBest(chi2, k=n_features).fit(X_train, y_train)
        X_test = sel.transform([[0, 1, 2]])
        assert_array_equal(X_test[0], np.arange(3)[-n_features:])

def test_nans():
    # Assert that SelectKBest and SelectPercentile can handle NaNs.
    # First feature has zero variance to confuse f_classif (ANOVA) and
    # make it return a NaN.
    X = [[0, 1, 0], [0, -1, -1], [0, 0.5, 0.5]]
    y = [1, 0, 1]

    for select in (
        SelectKBest(f_classif, k=2),
        SelectPercentile(f_classif, percentile=67),
    ):
        ignore_warnings(select.fit)(X, y)
        assert_array_equal(select.get_support(indices=True), np.array([1, 2]))

def test_invalid_k():
    y = [1, 0, 1]

    msg = "k=4 is greater than n_features=3. All the features will be returned."
        SelectKBest(k=4).fit(X, y)
        GenericUnivariateSelect(mode="k_best", param=4).fit(X, y)

def test_f_classif_constant_feature():
    # Test that f_classif warns if a feature is constant throughout.

    X, y = make_classification(n_samples=10, n_features=5)
    X[:, 0] = 2.0
        f_classif(X, y)

def test_no_feature_selected():

    # Generate random uncorrelated data: a strict univariate test should
    # rejects all the features
    y = rng.randint(0, 4, size=40)
    strict_selectors = [
        SelectFwe(alpha=0.01).fit(X, y),
        SelectFdr(alpha=0.01).fit(X, y),
        SelectFpr(alpha=0.01).fit(X, y),
        SelectPercentile(percentile=0).fit(X, y),
        SelectKBest(k=0).fit(X, y),
    ]
    for selector in strict_selectors:
        assert_array_equal(selector.get_support(), np.zeros(10))
        with pytest.warns(UserWarning, match="No features were selected"):
            X_selected = selector.transform(X)
        assert X_selected.shape == (40, 0)

def test_mutual_info_classif():
    )

    # Test in KBest mode.
    univariate_filter = SelectKBest(mutual_info_classif, k=2)
    X_r2 = (
        GenericUnivariateSelect(mutual_info_classif, mode="k_best", param=2)
        .fit(X, y)
    )
    gtruth = np.zeros(5)
    gtruth[:2] = 1

    # Test in Percentile mode.
    univariate_filter = SelectPercentile(mutual_info_classif, percentile=40)
    X_r2 = (
        GenericUnivariateSelect(mutual_info_classif, mode="percentile", param=40)
        .fit(X, y)
    )

def test_mutual_info_regression():
        noise=10,
    )

    univariate_filter = SelectKBest(mutual_info_regression, k=2)
    X_r2 = (
        GenericUnivariateSelect(mutual_info_regression, mode="k_best", param=2)
        .fit(X, y)
    )
    gtruth = np.zeros(10)

    univariate_filter = SelectPercentile(mutual_info_regression, percentile=20)
    X_r2 = (
        GenericUnivariateSelect(mutual_info_regression, mode="percentile", param=20)
        .fit(X, y)
    )

def test_dataframe_output_dtypes():
    """Check that the output datafarme dtypes are the same as the input.

    Non-regression test for gh-24860.
    """

    X, y = load_iris(return_X_y=True, as_frame=True)
    X = X.astype(
        {
            "petal length (cm)": np.float32,
            "petal width (cm)": np.float64,
        }
    )
    X["petal_width_binned"] = pd.cut(X["petal width (cm)"], bins=10)

    column_order = X.columns

    def selector(X, y):
        ranking = {
            "sepal length (cm)": 1,
            "sepal width (cm)": 2,
            "petal length (cm)": 3,
            "petal width (cm)": 4,
            "petal_width_binned": 5,
        }
        return np.asarray([ranking[name] for name in column_order])

    univariate_filter = SelectKBest(selector, k=3).set_output(transform="pandas")
    output = univariate_filter.fit_transform(X, y)

        output.columns, ["petal length (cm)", "petal width (cm)", "petal_width_binned"]
    )
    for name, dtype in output.dtypes.items():
        assert dtype == X.dtypes[name]

def test_unsupervised_filter(selector):
    """Check support for unsupervised feature selection for the filter that could
    require only `X`.
    """

    def score_func(X, y=None):
        return np.array([1, 1, 1, 1, 0])

    selector.set_params(score_func=score_func)
    selector.fit(X)
    X_trans = selector.transform(X)
    assert_allclose(X_trans, X[:, :4])
    X_trans = selector.fit_transform(X)

            noise=10,
        )

            X_r2 = (
                .fit(X, y)
            )

            return 0.0
        )

        ranking = {
        }

def test_zero_variance(sparse_container):
    # Test VarianceThreshold with default setting, zero variance.
    X = data if sparse_container is None else sparse_container(data)
    sel = VarianceThreshold().fit(X)
    assert_array_equal([0, 1, 3, 4], sel.get_support(indices=True))

def test_zero_variance_value_error():
    # Test VarianceThreshold with default setting, zero variance, error cases.
        VarianceThreshold().fit([[0, 1, 2, 3]])
        VarianceThreshold().fit([[0, 1], [0, 1]])

def test_variance_threshold(sparse_container):
    # Test VarianceThreshold with custom variance.
    X = VarianceThreshold(threshold=0.4).fit_transform(X)
    assert (len(data), 1) == X.shape

def test_zero_variance_floating_point_error(sparse_container):
    # Test that VarianceThreshold(0.0).fit eliminates features that have
    # the same value in every sample, even when floating point errors
    # cause np.var not to be 0 for the feature.
    # See #13691
    X = data2 if sparse_container is None else sparse_container(data2)
    msg = "No feature in X meets the variance threshold 0.00000"
        VarianceThreshold().fit(X)

def test_variance_nan(sparse_container):
    arr = np.array(data, dtype=np.float64)
    # add single NaN and feature should still be included
    arr[0, 0] = np.nan
    # make all values in feature NaN and feature should be rejected
    arr[:, 1] = np.nan

    X = arr if sparse_container is None else sparse_container(arr)
    assert_array_equal([0, 3, 4], sel.get_support(indices=True))

class StepSelector(SelectorMixin, BaseEstimator):
    """Retain every `step` features (beginning with 0).

    If `step < 1`, then no features are selected.
    """

    def __init__(self, step=2):

        mask = np.zeros(self.n_features_in_, dtype=bool)
        if self.step >= 1:
            mask[:: self.step] = True

def test_transform_dense():
    sel = StepSelector()
    Xt_actual = sel.fit(X, y).transform(X)
    Xt_actual2 = StepSelector().fit_transform(X, y)
    assert_array_equal(Xt, Xt_actual)
    assert_array_equal(Xt, Xt_actual2)

    # Check dtype matches
    assert np.int32 == sel.transform(X.astype(np.int32)).dtype
    assert np.float32 == sel.transform(X.astype(np.float32)).dtype

    # Check 1d list and other dtype:
    names_t_actual = sel.transform([feature_names])
    assert_array_equal(feature_names_t, names_t_actual.ravel())

    # Check wrong shape raises error
        sel.transform(np.array([[1], [2]]))

def test_transform_sparse(csc_container):
    Xt_actual = sel.fit(X_sp).transform(X_sp)
    Xt_actual2 = sel.fit_transform(X_sp)
    assert_array_equal(Xt, Xt_actual.toarray())
    assert_array_equal(Xt, Xt_actual2.toarray())

    assert np.int32 == sel.transform(X_sp.astype(np.int32)).dtype
    assert np.float32 == sel.transform(X_sp.astype(np.float32)).dtype

def test_inverse_transform_dense():
    Xinv_actual = sel.fit(X, y).inverse_transform(Xt)
    assert_array_equal(Xinv, Xinv_actual)

    assert np.int32 == sel.inverse_transform(Xt.astype(np.int32)).dtype
    assert np.float32 == sel.inverse_transform(Xt.astype(np.float32)).dtype

    names_inv_actual = sel.inverse_transform([feature_names_t])
    assert_array_equal(feature_names_inv, names_inv_actual.ravel())

        sel.inverse_transform(np.array([[1], [2]]))

def test_inverse_transform_sparse(csc_container):
    Xt_sp = csc_container(Xt)
    Xinv_actual = sel.fit(X_sp).inverse_transform(Xt_sp)
    assert_array_equal(Xinv, Xinv_actual.toarray())

    assert np.int32 == sel.inverse_transform(Xt_sp.astype(np.int32)).dtype
    assert np.float32 == sel.inverse_transform(Xt_sp.astype(np.float32)).dtype

def test_get_support():
    sel.fit(X, y)
    assert_array_equal(support, sel.get_support())
    assert_array_equal(support_inds, sel.get_support(indices=True))

def test_output_dataframe():
    """Check output dtypes for dataframes is consistent with the input dtypes."""

        {
            "a": pd.Series([1.0, 2.4, 4.5], dtype=np.float32),
            "b": pd.Series(["a", "b", "a"], dtype="category"),
            "c": pd.Series(["j", "b", "b"], dtype="category"),
            "d": pd.Series([3.0, 2.4, 1.2], dtype=np.float64),
        }
    )

    for step in [2, 3]:
        sel = StepSelector(step=step).set_output(transform="pandas")
        sel.fit(X)

        output = sel.transform(X)
        for name, dtype in output.dtypes.items():
            assert dtype == X.dtypes[name]

    # step=0 will select nothing
    sel0 = StepSelector(step=0).set_output(transform="pandas")
    sel0.fit(X, y)

    msg = "No features were selected"
        output0 = sel0.transform(X)

    assert_array_equal(output0.index, X.index)
    assert output0.shape == (X.shape[0], 0)

def test_compute_mi_dd():
    # In discrete case computations are straightforward and can be done
    # by hand on given vectors.
    x = np.array([0, 1, 1, 0, 0])
    y = np.array([1, 0, 0, 0, 1])

    H_x = H_y = -(3 / 5) * np.log(3 / 5) - (2 / 5) * np.log(2 / 5)
    H_xy = -1 / 5 * np.log(1 / 5) - 2 / 5 * np.log(2 / 5) - 2 / 5 * np.log(2 / 5)
    I_xy = H_x + H_y - H_xy

    assert_allclose(_compute_mi(x, y, x_discrete=True, y_discrete=True), I_xy)

def test_compute_mi_cc(global_dtype):
    # For two continuous variables a good approach is to test on bivariate
    # normal distribution, where mutual information is known.

    # Mean of the distribution, irrelevant for mutual information.
    mean = np.zeros(2)

    # Setup covariance matrix with correlation coeff. equal 0.5.
    sigma_1 = 1
    sigma_2 = 10
    corr = 0.5
    cov = np.array(
        [
            [sigma_1**2, corr * sigma_1 * sigma_2],
            [corr * sigma_1 * sigma_2, sigma_2**2],
        ]
    )

    # True theoretical mutual information.
    I_theory = np.log(sigma_1) + np.log(sigma_2) - 0.5 * np.log(np.linalg.det(cov))

    Z = rng.multivariate_normal(mean, cov, size=1000).astype(global_dtype, copy=False)

    x, y = Z[:, 0], Z[:, 1]

    # Theory and computed values won't be very close
    # We here check with a large relative tolerance
    for n_neighbors in [3, 5, 7]:
        I_computed = _compute_mi(
            x, y, x_discrete=False, y_discrete=False, n_neighbors=n_neighbors
        )
        assert_allclose(I_computed, I_theory, rtol=1e-1)

def test_compute_mi_cd(global_dtype):
    # To test define a joint distribution as follows:
    # p(x, y) = p(x) p(y | x)
    # X ~ Bernoulli(p)
    # (Y | x = 0) ~ Uniform(-1, 1)
    # (Y | x = 1) ~ Uniform(0, 2)

    # Use the following formula for mutual information:
    # I(X; Y) = H(Y) - H(Y | X)
    # Two entropies can be computed by hand:
    # H(Y) = -(1-p)/2 * ln((1-p)/2) - p/2*log(p/2) - 1/2*log(1/2)
    # H(Y | X) = ln(2)

    # Now we need to implement sampling from out distribution, which is
    # done easily using conditional distribution logic.

    for p in [0.3, 0.5, 0.7]:
        x = rng.uniform(size=n_samples) > p

        y = np.empty(n_samples, global_dtype)
        mask = x == 0
        y[mask] = rng.uniform(-1, 1, size=np.sum(mask))
        y[~mask] = rng.uniform(0, 2, size=np.sum(~mask))

        I_theory = -0.5 * (
            (1 - p) * np.log(0.5 * (1 - p)) + p * np.log(0.5 * p) + np.log(0.5)
        ) - np.log(2)

        # Assert the same tolerance.
        for n_neighbors in [3, 5, 7]:
            I_computed = _compute_mi(
                x, y, x_discrete=True, y_discrete=False, n_neighbors=n_neighbors
            )
            assert_allclose(I_computed, I_theory, rtol=1e-1)

def test_compute_mi_cd_unique_label(global_dtype):
    # Test that adding unique label doesn't change MI.
    x = np.random.uniform(size=n_samples) > 0.5

    y = np.empty(n_samples, global_dtype)
    mask = x == 0
    y[mask] = np.random.uniform(-1, 1, size=np.sum(mask))
    y[~mask] = np.random.uniform(0, 2, size=np.sum(~mask))

    mi_1 = _compute_mi(x, y, x_discrete=True, y_discrete=False)

    x = np.hstack((x, 2))
    y = np.hstack((y, 10))
    mi_2 = _compute_mi(x, y, x_discrete=True, y_discrete=False)

    assert_allclose(mi_1, mi_2)

def test_mutual_info_classif_discrete(global_dtype):
        [[0, 0, 0], [1, 1, 0], [2, 0, 1], [2, 0, 1], [2, 0, 1]], dtype=global_dtype
    )
    y = np.array([0, 1, 2, 2, 1])

    # Here X[:, 0] is the most informative feature, and X[:, 1] is weakly
    # informative.
    mi = mutual_info_classif(X, y, discrete_features=True)
    assert_array_equal(np.argsort(-mi), np.array([0, 2, 1]))

def test_mutual_info_regression(global_dtype):
    # We generate sample from multivariate normal distribution, using
    # transformation from initially uncorrelated variables. The zero
    # variables after transformation is selected as the target vector,
    # it has the strongest correlation with the variable 2, and
    # the weakest correlation with the variable 1.
    T = np.array([[1, 0.5, 2, 1], [0, 1, 0.1, 0.0], [0, 0.1, 1, 0.1], [0, 0.1, 0.1, 1]])
    cov = T.dot(T.T)
    mean = np.zeros(4)

    X = Z[:, 1:]
    y = Z[:, 0]

    mi = mutual_info_regression(X, y, random_state=0)
    assert_array_equal(np.argsort(-mi), np.array([1, 2, 0]))
    # XXX: should mutual_info_regression be fixed to avoid
    # up-casting float32 inputs to float64?
    assert mi.dtype == np.float64

def test_mutual_info_classif_mixed(global_dtype):
    # Here the target is discrete and there are two continuous and one
    # discrete feature. The idea of this test is clear from the code.
    X = rng.rand(1000, 3).astype(global_dtype, copy=False)
    X[:, 1] += X[:, 0]
    y = ((0.5 * X[:, 0] + X[:, 2]) > 0.5).astype(int)
    X[:, 2] = X[:, 2] > 0.5

    mi = mutual_info_classif(X, y, discrete_features=[2], n_neighbors=3, random_state=0)
    assert_array_equal(np.argsort(-mi), [2, 0, 1])
    for n_neighbors in [5, 7, 9]:
        mi_nn = mutual_info_classif(
            X, y, discrete_features=[2], n_neighbors=n_neighbors, random_state=0
        )
        # Check that the continuous values have an higher MI with greater
        # n_neighbors
        assert mi_nn[0] > mi[0]
        assert mi_nn[1] > mi[1]
        # The n_neighbors should not have any effect on the discrete value
        # The MI should be the same
        assert mi_nn[2] == mi[2]

def test_mutual_info_options(global_dtype, csr_container):
    )
    y = np.array([0, 1, 2, 2, 1], dtype=global_dtype)

    for mutual_info in (mutual_info_regression, mutual_info_classif):
            mutual_info(X_csr, y, discrete_features=False)
            mutual_info(X, y, discrete_features="manual")
            mutual_info(X_csr, y, discrete_features=[True, False, True])
        with pytest.raises(IndexError):
            mutual_info(X, y, discrete_features=[True, False, True, False])
            mutual_info(X, y, discrete_features=[1, 4])

        mi_1 = mutual_info(X, y, discrete_features="auto", random_state=0)
        mi_2 = mutual_info(X, y, discrete_features=False, random_state=0)
        mi_3 = mutual_info(X_csr, y, discrete_features="auto", random_state=0)
        mi_4 = mutual_info(X_csr, y, discrete_features=True, random_state=0)
        mi_5 = mutual_info(X, y, discrete_features=[True, False, True], random_state=0)
        mi_6 = mutual_info(X, y, discrete_features=[0, 2], random_state=0)

        assert_allclose(mi_1, mi_2)
        assert_allclose(mi_3, mi_4)
        assert_allclose(mi_5, mi_6)

        assert not np.allclose(mi_1, mi_3)

def test_mutual_information_symmetry_classif_regression(correlated, global_random_seed):
    """Check that `mutual_info_classif` and `mutual_info_regression` are
    symmetric by switching the target `y` as `feature` in `X` and vice
    versa.

    https://github.com/scikit-learn/scikit-learn/issues/23720
    """
    n = 100
    d = rng.randint(10, size=n)

    if correlated:
        c = d.astype(np.float64)
    else:
        c = rng.normal(0, 1, size=n)

    mi_classif = mutual_info_classif(
        c[:, None], d, discrete_features=[False], random_state=global_random_seed
    )

    mi_regression = mutual_info_regression(
        d[:, None], c, discrete_features=[True], random_state=global_random_seed
    )

    assert mi_classif == pytest.approx(mi_regression)

def test_mutual_info_regression_X_int_dtype(global_random_seed):
    """Check that results agree when X is integer dtype and float dtype.

    Non-regression test for Issue #26696.
    """
    X = rng.randint(100, size=(100, 10))
    X_float = X.astype(np.float64, copy=True)
    y = rng.randint(100, size=100)

    expected = mutual_info_regression(X_float, y, random_state=global_random_seed)
    result = mutual_info_regression(X, y, random_state=global_random_seed)

def test_mutual_info_n_jobs(global_random_seed, mutual_info_func, data_generator):
    """Check that results are consistent with different `n_jobs`."""
    X, y = data_generator(random_state=global_random_seed)
    single_job = mutual_info_func(X, y, random_state=global_random_seed, n_jobs=1)
    multi_job = mutual_info_func(X, y, random_state=global_random_seed, n_jobs=2)
    assert_allclose(single_job, multi_job)

def mkchi2(k):
    """Make k-best chi2 selector"""
    return SelectKBest(chi2, k=k)

def test_chi2(csr_container):
    # Test Chi2 feature extraction

    chi2 = mkchi2(k=1).fit(X, y)
    assert_array_equal(chi2.get_support(indices=True), [0])
    assert_array_equal(chi2.transform(X), np.array(X)[:, [0]])

    chi2 = mkchi2(k=2).fit(X, y)
    assert_array_equal(sorted(chi2.get_support(indices=True)), [0, 2])

    Xsp = csr_container(X, dtype=np.float64)
    chi2 = mkchi2(k=2).fit(Xsp, y)
    Xtrans = chi2.transform(Xsp)
    assert_array_equal(Xtrans.shape, [Xsp.shape[0], 2])

    # == doesn't work on scipy.sparse matrices
    Xtrans = Xtrans.toarray()
    Xtrans2 = mkchi2(k=2).fit_transform(Xsp, y).toarray()
    assert_array_almost_equal(Xtrans, Xtrans2)

def test_chi2_coo(coo_container):
    # Check that chi2 works with a COO matrix
    # (as returned by CountVectorizer, DictVectorizer)
    Xcoo = coo_container(X)
    mkchi2(k=2).fit_transform(Xcoo, y)

def test_chi2_negative(csr_container):
    # Check for proper error on negative numbers in the input X.
    X, y = [[0, 1], [-1e-20, 1]], [0, 1]
    for X in (X, np.array(X), csr_container(X)):
            chi2(X, y)

def test_chi2_unused_feature():
    # Unused feature should evaluate to NaN
    # and should issue no runtime warning
    with warnings.catch_warnings(record=True) as warned:
        chi, p = chi2([[1, 0], [0, 0]], [1, 0])
        for w in warned:
            if "divide by zero" in repr(w):
                raise AssertionError("Found unexpected warning %s" % w)
    assert_array_equal(chi, [1, np.nan])
    assert_array_equal(p[1], np.nan)

def test_chisquare():
    # Test replacement for scipy.stats.chisquare against the original.
    obs = np.array([[2.0, 2.0], [1.0, 1.0]])
    exp = np.array([[1.5, 1.5], [1.5, 1.5]])
    # call SciPy first because our version overwrites obs
    chi_scp, p_scp = scipy.stats.chisquare(obs, exp)
    chi_our, p_our = _chisquare(obs, exp)

    assert_array_almost_equal(chi_scp, chi_our)
    assert_array_almost_equal(p_scp, p_our)

def _check_is_fitted(estimator):
    try:
        check_is_fitted(estimator.estimator)
        check_is_fitted(estimator, "estimator_")

class BaseThresholdClassifier(ClassifierMixin, MetaEstimatorMixin, BaseEstimator):
    """Base class for binary classifiers that set a non-default decision threshold.

    In this base class, we define the following interface:

    - the validation of common parameters in `fit`;
    - the different prediction methods that can be used with the classifier.

        The binary classifier, fitted or not, for which we want to optimize
        the decision threshold used during `predict`.

    response_method : {"auto", "decision_function", "predict_proba"}, default="auto"
        Methods by the classifier `estimator` corresponding to the
        decision function for which we want to find a threshold. It can be:

        * if `"auto"`, it will try to invoke, for each classifier,
          `"predict_proba"` or `"decision_function"` in that order.
        * otherwise, one of `"predict_proba"` or `"decision_function"`.
          If the method is not implemented by the classifier, it will raise an
          error.
    """

        ],
        "response_method": [StrOptions({"auto", "predict_proba", "decision_function"})],
    }

    def __init__(self, estimator, *, response_method="auto"):
        self.response_method = response_method

    def _get_response_method(self):
        """Define the response method."""
        if self.response_method == "auto":
            response_method = ["predict_proba", "decision_function"]
        else:
            response_method = self.response_method
        return response_method

        # *ThresholdClassifier*.estimator is not validated yet
    )
        """Fit the classifier.

        Returns
        -------
        """
        _raise_for_params(params, self, None)

                f"Only binary classification is supported. Unknown label type: {y_type}"
            )

        self._fit(X, y, **params)

        if hasattr(self.estimator_, "n_features_in_"):
            self.n_features_in_ = self.estimator_.n_features_in_

    @property

        """Predict class probabilities for `X` using the fitted estimator.

        Returns
        -------
        """
        _check_is_fitted(self)
        return estimator.predict_proba(X)

        """Predict logarithm class probabilities for `X` using the fitted estimator.

        Returns
        -------
        log_probabilities : ndarray of shape (n_samples, n_classes)
            The logarithm class probabilities of the input samples.
        """
        return estimator.predict_log_proba(X)

        """Decision function for samples in `X` using the fitted estimator.

        Returns
        -------
        decisions : ndarray of shape (n_samples,)
            The decision function computed the fitted estimator.
        """
        return estimator.decision_function(X)

class FixedThresholdClassifier(BaseThresholdClassifier):
    """Binary classifier that manually sets the decision threshold.

    This classifier allows to change the default decision threshold used for
    converting posterior probability estimates (i.e. output of `predict_proba`) or
    decision scores (i.e. output of `decision_function`) into a class label.

    Here, the threshold is not optimized and is set to a constant value.

    Read more in the :ref:`User Guide <FixedThresholdClassifier>`.

    threshold : {"auto"} or float, default="auto"
        The decision threshold to use when converting posterior probability estimates
        (i.e. output of `predict_proba`) or decision scores (i.e. output of
        `decision_function`) into a class label. When `"auto"`, the threshold is set
        to 0.5 if `predict_proba` is used as `response_method`, otherwise it is set to
        0 (i.e. the default threshold for `decision_function`).

        The label of the positive class. Used to process the output of the
        `response_method` method. When `pos_label=None`, if `y_true` is in `{-1, 1}` or
        `{0, 1}`, `pos_label` is set to 1, otherwise an error will be raised.

        * if `"auto"`, it will try to invoke `"predict_proba"` or `"decision_function"`
          in that order.
          error.

    estimator_ : estimator instance
        The fitted classifier used when predicting.

    See Also
    --------
    sklearn.model_selection.TunedThresholdClassifierCV : Classifier that post-tunes
        the decision threshold based on some metrics and using cross-validation.
    sklearn.calibration.CalibratedClassifierCV : Estimator that calibrates
        probabilities.

    Examples
    --------
    >>> from sklearn.model_selection import FixedThresholdClassifier, train_test_split
    ...     n_samples=1_000, weights=[0.9, 0.1], class_sep=0.8, random_state=42
    ... )
    ... )
    >>> classifier = LogisticRegression(random_state=0).fit(X_train, y_train)
    >>> print(confusion_matrix(y_test, classifier.predict(X_test)))
    [[217   7]
     [ 19   7]]
    >>> classifier_other_threshold = FixedThresholdClassifier(
    ...     classifier, threshold=0.1, response_method="predict_proba"
    ... ).fit(X_train, y_train)
    >>> print(confusion_matrix(y_test, classifier_other_threshold.predict(X_test)))
    [[184  40]
     [  6  20]]
    """

        **BaseThresholdClassifier._parameter_constraints,
        "threshold": [StrOptions({"auto"}), Real],
        "pos_label": [Real, str, "boolean", None],
    }

        self,
        *,
        threshold="auto",
    ):
        super().__init__(estimator=estimator, response_method=response_method)

    @property
        if estimator := getattr(self, "estimator_", None):
            return estimator.classes_
        try:
            return self.estimator.classes_
                "The underlying estimator is not fitted yet."
            ) from NotFittedError

    def _fit(self, X, y, **params):

        Returns
        -------
        """
        routed_params = process_routing(self, "fit", **params)
        self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit)

        Returns
        -------
        class_labels : ndarray of shape (n_samples,)
        """

        y_score, _, response_method_used = _get_response_values_binary(
            X,
            self._get_response_method(),
            pos_label=self.pos_label,
            return_response_method_used=True,
        )

        if self.threshold == "auto":
            decision_threshold = 0.5 if response_method_used == "predict_proba" else 0.0
        else:
            decision_threshold = self.threshold

        return _threshold_scores_to_class_labels(
            y_score, decision_threshold, self.classes_, self.pos_label
        )

        Returns
        -------
        """
        )

def _fit_and_score_over_thresholds(
    classifier,
    X,
    y,
    *,
    train_idx,
    val_idx,
    curve_scorer,
):
    """Fit a classifier and compute the scores for different decision thresholds.

    classifier : estimator instance
        The classifier to fit and use for scoring. If `classifier` is already fitted,
        it will be used as is.

        The entire dataset.

        The entire target vector.

    fit_params : dict
        Parameters to pass to the `fit` method of the underlying classifier.

    train_idx : ndarray of shape (n_train_samples,) or None
        The indices of the training set. If `None`, `classifier` is expected to be
        already fitted.

    val_idx : ndarray of shape (n_val_samples,)
        The indices of the validation set used to score `classifier`. If `train_idx`,
        the entire set will be used.

    curve_scorer : scorer instance
        The scorer taking `classifier` and the validation set as input and outputting
        decision thresholds and scores as a curve. Note that this is different from
        the usual scorer that output a single score value:

        * when `score_method` is one of the four constraint metrics, the curve scorer
          will output a curve of two scores parametrized by the decision threshold, e.g.
          TPR/TNR or precision/recall curves for each threshold;
        * otherwise, the curve scorer will output a single score value for each
          threshold.

    Returns
    -------
    scores : ndarray of shape (thresholds,) or tuple of such arrays
        The scores computed for each decision threshold. When TPR/TNR or precision/
        recall are computed, `scores` is a tuple of two arrays.

    potential_thresholds : ndarray of shape (thresholds,)
        The decision thresholds used to compute the scores. They are returned in
        ascending order.
    """

    if train_idx is not None:
        X_train, X_val = _safe_indexing(X, train_idx), _safe_indexing(X, val_idx)
        y_train, y_val = _safe_indexing(y, train_idx), _safe_indexing(y, val_idx)
        fit_params_train = _check_method_params(X, fit_params, indices=train_idx)
        score_params_val = _check_method_params(X, score_params, indices=val_idx)
        classifier.fit(X_train, y_train, **fit_params_train)
    else:  # prefit estimator, only a validation set is provided
        X_val, y_val, score_params_val = X, y, score_params

    return curve_scorer(classifier, X_val, y_val, **score_params_val)

def _mean_interpolated_score(target_thresholds, cv_thresholds, cv_scores):
    """Compute the mean interpolated score across folds by defining common thresholds.

    target_thresholds : ndarray of shape (thresholds,)
        The thresholds to use to compute the mean score.

    cv_thresholds : ndarray of shape (n_folds, thresholds_fold)
        The thresholds used to compute the scores for each fold.

    cv_scores : ndarray of shape (n_folds, thresholds_fold)
        The scores computed for each threshold for each fold.

    Returns
    -------
    mean_score : ndarray of shape (thresholds,)
        The mean score across all folds for each target threshold.
    """
    return np.mean(
        [
            np.interp(target_thresholds, split_thresholds, split_score)
            for split_thresholds, split_score in zip(cv_thresholds, cv_scores)
        ],
        axis=0,
    )

class TunedThresholdClassifierCV(BaseThresholdClassifier):
    """Classifier that post-tunes the decision threshold using cross-validation.

    This estimator post-tunes the decision threshold (cut-off point) that is
    used for converting posterior probability estimates (i.e. output of
    `predict_proba`) or decision scores (i.e. output of `decision_function`)
    into a class label. The tuning is done by optimizing a binary metric,
    potentially constrained by a another metric.

    Read more in the :ref:`User Guide <TunedThresholdClassifierCV>`.

        The classifier, fitted or not, for which we want to optimize

    scoring : str or callable, default="balanced_accuracy"
        The objective metric to be optimized. Can be one of:

        - str: string associated to a scoring function for binary classification,
          see :ref:`scoring_string_names` for options.

          error.

    thresholds : int or array-like, default=100
        The number of decision threshold to use when discretizing the output of the
        classifier `method`. Pass an array-like to manually specify the thresholds
        to use.

    cv : int, float, cross-validation generator, iterable or "prefit", default=None
        Determines the cross-validation splitting strategy to train classifier.

        * `None`, to use the default 5-fold stratified K-fold cross validation;
        * An integer number, to specify the number of folds in a stratified k-fold;
        * A float number, to specify a single shuffle split. The floating number should
          be in (0, 1) and represent the size of the validation set;
        * An object to be used as a cross-validation generator;
        * An iterable yielding train, test splits;
        * `"prefit"`, to bypass the cross-validation.

            Using `cv="prefit"` and passing the same dataset for fitting `estimator`
            and tuning the cut-off point is subject to undesired overfitting. You can
            refer to :ref:`TunedThresholdClassifierCV_no_cv` for an example.

            This option should only be used when the set used to fit `estimator` is
            different from the one used to tune the cut-off point (by calling
            :meth:`TunedThresholdClassifierCV.fit`).

        Whether or not to refit the classifier on the entire training set once
        the decision threshold has been found.
        Note that forcing `refit=False` on cross-validation having more
        than a single split will raise an error. Similarly, `refit=True` in
        conjunction with `cv="prefit"` will raise an error.

        The number of jobs to run in parallel. When `cv` represents a
        cross-validation strategy, the fitting and scoring on each data split
        is done in parallel. ``None`` means 1 unless in a

        Controls the randomness of cross-validation when `cv` is a float.

        Whether to store all scores and thresholds computed during the cross-validation
        process.

    best_threshold_ : float
        The new decision threshold.

    best_score_ : float or None
        The optimal score of the objective metric, evaluated at `best_threshold_`.

    cv_results_ : dict or None
        A dictionary containing the scores and thresholds computed during the
        cross-validation process. Only exist if `store_cv_results=True`. The
        keys are `"thresholds"` and `"scores"`.

    See Also
    --------
    sklearn.model_selection.FixedThresholdClassifier : Classifier that uses a
        constant threshold.

    Examples
    --------
    >>> from sklearn.model_selection import TunedThresholdClassifierCV, train_test_split
    ... )
    ... )
    >>> classifier = RandomForestClassifier(random_state=0).fit(X_train, y_train)
    >>> print(classification_report(y_test, classifier.predict(X_test)))
               0       0.94      0.99      0.96       224
               1       0.80      0.46      0.59        26
        accuracy                           0.93       250
       macro avg       0.87      0.72      0.77       250
    weighted avg       0.93      0.93      0.92       250
    >>> classifier_tuned = TunedThresholdClassifierCV(
    ...     classifier, scoring="balanced_accuracy"
    >>> print(
    ...     f"Cut-off point found at {classifier_tuned.best_threshold_:.3f}"
    ... )
    Cut-off point found at 0.342
    >>> print(classification_report(y_test, classifier_tuned.predict(X_test)))
               0       0.96      0.95      0.96       224
               1       0.61      0.65      0.63        26
        accuracy                           0.92       250
       macro avg       0.78      0.80      0.79       250
    weighted avg       0.92      0.92      0.92       250
    """

        "scoring": [
            StrOptions(set(get_scorer_names())),
            callable,
            MutableMapping,
        ],
        "thresholds": [Interval(Integral, 1, None, closed="left"), "array-like"],
        "cv": [
            "cv_object",
            StrOptions({"prefit"}),
        ],
        "refit": ["boolean"],
    }

        self,
        *,
        scoring="balanced_accuracy",
        thresholds=100,
        cv=None,
    ):
        self.thresholds = thresholds

        """Fit the classifier and post-tune the decision threshold.

            classifier and to the `scoring` scorer.

        Returns
        -------
        """
        if isinstance(self.cv, Real) and 0 < self.cv < 1:
            cv = StratifiedShuffleSplit(
                n_splits=1, test_size=self.cv, random_state=self.random_state
            )
        elif self.cv == "prefit":
            if self.refit is True:
                raise ValueError("When cv='prefit', refit cannot be True.")
            try:
                check_is_fitted(self.estimator, "classes_")
                    """When cv='prefit', `estimator` must be fitted."""
                ) from exc
            cv = self.cv
        else:
            cv = check_cv(self.cv, y=y, classifier=True)
            if self.refit is False and cv.get_n_splits() > 1:
                raise ValueError("When cv has several folds, refit cannot be False.")

        self._curve_scorer = self._get_curve_scorer()

        # in the following block, we:
        # - define the final classifier `self.estimator_` and train it if necessary
        # - define `classifier` to be used to post-tune the decision threshold
        # - define `split` to be used to fit/score `classifier`
        if cv == "prefit":
            classifier = self.estimator_
            splits = [(None, range(_num_samples(X)))]
        else:
            classifier = clone(self.estimator)
            splits = cv.split(X, y, **routed_params.splitter.split)

                # train on the whole dataset
                X_train, y_train, fit_params_train = X, y, routed_params.estimator.fit
            else:
                # single split cross-validation
                train_idx, _ = next(cv.split(X, y, **routed_params.splitter.split))
                X_train = _safe_indexing(X, train_idx)
                y_train = _safe_indexing(y, train_idx)
                fit_params_train = _check_method_params(
                    X, routed_params.estimator.fit, indices=train_idx
                )

            self.estimator_.fit(X_train, y_train, **fit_params_train)

        cv_scores, cv_thresholds = zip(
            *Parallel(n_jobs=self.n_jobs)(
                delayed(_fit_and_score_over_thresholds)(
                    clone(classifier) if cv != "prefit" else classifier,
                    X,
                    y,
                    fit_params=routed_params.estimator.fit,
                    train_idx=train_idx,
                    val_idx=val_idx,
                    curve_scorer=self._curve_scorer,
                    score_params=routed_params.scorer.score,
                )
                for train_idx, val_idx in splits
            )
        )

        if any(np.isclose(th[0], th[-1]) for th in cv_thresholds):
                "The provided estimator makes constant predictions. Therefore, it is "
                "impossible to optimize the decision threshold."
            )

        # find the global min and max thresholds across all folds
        min_threshold = min(
            split_thresholds.min() for split_thresholds in cv_thresholds
        )
        max_threshold = max(
            split_thresholds.max() for split_thresholds in cv_thresholds
        )
        if isinstance(self.thresholds, Integral):
            decision_thresholds = np.linspace(
                min_threshold, max_threshold, num=self.thresholds
            )
        else:
            decision_thresholds = np.asarray(self.thresholds)

        objective_scores = _mean_interpolated_score(
            decision_thresholds, cv_thresholds, cv_scores
        )
        best_idx = objective_scores.argmax()
        self.best_score_ = objective_scores[best_idx]
        self.best_threshold_ = decision_thresholds[best_idx]
            self.cv_results_ = {
                "thresholds": decision_thresholds,
                "scores": objective_scores,
            }

        Returns
        -------
        """
        check_is_fitted(self, "estimator_")
        pos_label = self._curve_scorer._get_pos_label()
        y_score, _ = _get_response_values_binary(
            self.estimator_,
            X,
        )

            y_score, self.best_threshold_, self.classes_, pos_label
        )

        Returns
        -------
        """
        router = (
            .add(
            )
            .add(
                method_mapping=MethodMapping().add(callee="split", caller="fit"),
            )
            .add(
                scorer=self._get_curve_scorer(),
                method_mapping=MethodMapping().add(callee="score", caller="fit"),
            )
        )

    def _get_curve_scorer(self):
        """Get the curve scorer based on the objective metric used."""
        scoring = check_scoring(self.estimator, scoring=self.scoring)
        curve_scorer = _CurveScorer.from_scorer(
            scoring, self._get_response_method(), self.thresholds
        )
        return curve_scorer

        else:

        Returns
        -------
        """

            )

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        self,
        *,
    ):

        try:

        Returns
        -------
        """

        Returns
        -------
        """

            X,
        )

        else:

        )

        Returns
        -------
        """
        )

        self,
        *,
        cv=None,
    ):

        Returns
        -------
        """
            )
            try:
                ) from exc
        else:

        else:

            else:
                )

                    X,
                    y,
                )
            )
        )

            )

        )
        )
            )
        else:

        )
            }

        Returns
        -------
        """
            X,
        )

        )

        Returns
        -------
        """
        router = (
            .add(
            )
            .add(
            )
            .add(
            )
        )

        )

class ParameterGrid:
    """Grid of parameters with a discrete number of values for each.

    Can be used to iterate over parameter value combinations with the
    Python built-in function iter.
    The order of the generated parameter combinations is deterministic.

    Read more in the :ref:`User Guide <grid_search>`.

    param_grid : dict of str to sequence, or sequence of such
        The parameter grid to explore, as a dictionary mapping estimator
        parameters to sequences of allowed values.

        An empty dict signifies default parameters.

        A sequence of dicts signifies a sequence of grids to search, and is
        useful to avoid exploring parameter combinations that make no sense
        or have no effect. See the examples below.

    Examples
    --------
    >>> from sklearn.model_selection import ParameterGrid
    >>> param_grid = {'a': [1, 2], 'b': [True, False]}
    >>> list(ParameterGrid(param_grid)) == (
    ...    [{'a': 1, 'b': True}, {'a': 1, 'b': False},
    ...     {'a': 2, 'b': True}, {'a': 2, 'b': False}])
    True

    >>> grid = [{'kernel': ['linear']}, {'kernel': ['rbf'], 'gamma': [1, 10]}]
    >>> list(ParameterGrid(grid)) == [{'kernel': 'linear'},
    ...                               {'kernel': 'rbf', 'gamma': 1},
    ...                               {'kernel': 'rbf', 'gamma': 10}]
    True
    >>> ParameterGrid(grid)[1] == {'kernel': 'rbf', 'gamma': 1}
    True

    See Also
    --------
    GridSearchCV : Uses :class:`ParameterGrid` to perform a full parallelized
        parameter search.
    """

    def __init__(self, param_grid):
        if not isinstance(param_grid, (Mapping, Iterable)):
                f"Parameter grid should be a dict or a list, got: {param_grid!r} of"
                f" type {type(param_grid).__name__}"
            )

        if isinstance(param_grid, Mapping):
            # wrap dictionary in a singleton list to support either dict
            # or list of dicts
            param_grid = [param_grid]

        # check if all entries are dictionaries of lists
        for grid in param_grid:
            if not isinstance(grid, dict):
                raise TypeError(f"Parameter grid is not a dict ({grid!r})")
            for key, value in grid.items():
                if isinstance(value, np.ndarray) and value.ndim > 1:
                        f"Parameter array for {key!r} should be one-dimensional, got:"
                        f" {value!r} with shape {value.shape}"
                    )
                if isinstance(value, str) or not isinstance(
                    value, (np.ndarray, Sequence)
                ):
                        f"Parameter grid for parameter {key!r} needs to be a list or a"
                        f" numpy array, but got {value!r} (of type "
                        f"{type(value).__name__}) instead. Single values "
                        "need to be wrapped in a list with one element."
                    )
                if len(value) == 0:
                        f"Parameter grid for parameter {key!r} need "
                        f"to be a non-empty sequence, got: {value!r}"
                    )

        """Iterate over the points in the grid.

        Returns
        -------
        params : iterator over dict of str to any
            Yields dictionaries mapping each estimator parameter to one of its
            allowed values.
        """
        for p in self.param_grid:
            # Always sort the keys of a dictionary, for reproducibility
            items = sorted(p.items())
            if not items:
                yield {}
            else:
                keys, values = zip(*items)
                for v in product(*values):
                    params = dict(zip(keys, v))
                    yield params

        """Number of points on the grid."""
        # Product function that can handle iterables (np.prod can't).
        product = partial(reduce, operator.mul)
            product(len(v) for v in p.values()) if p else 1 for p in self.param_grid
        )

        """Get the parameters that would be ``ind``th in iteration

        ind : int
            The iteration index

        Returns
        -------
        params : dict of str to any
            Equal to list(self)[ind]
        """
        # This is used to make discrete sampling without replacement memory
        # efficient.
        for sub_grid in self.param_grid:
            # XXX: could memoize information used here
            if not sub_grid:
                if ind == 0:
                    return {}
                else:
                    ind -= 1
                    continue

            # Reverse so most frequent cycling parameter comes first
            keys, values_lists = zip(*sorted(sub_grid.items())[::-1])
            sizes = [len(v_list) for v_list in values_lists]
            total = np.prod(sizes)

            if ind >= total:
                # Try the next grid
                ind -= total
            else:
                out = {}
                for key, v_list, n in zip(keys, values_lists, sizes):
                    ind, offset = divmod(ind, n)
                    out[key] = v_list[offset]
                return out

        raise IndexError("ParameterGrid index out of range")

class ParameterSampler:
    """Generator on parameters sampled from given distributions.

    Non-deterministic iterable over random candidate combinations for hyper-
    parameter search. If all parameters are presented as a list,
    sampling without replacement is performed. If at least one parameter
    is given as a distribution, sampling with replacement is used.
    It is highly recommended to use continuous distributions for continuous

    param_distributions : dict
        Dictionary with parameters names (`str`) as keys and distributions
        or lists of parameters to try. Distributions must provide a ``rvs``
        method for sampling (such as those from scipy.stats.distributions).
        If a list is given, it is sampled uniformly.
        If a list of dicts is given, first a dict is sampled uniformly, and
        then a parameter is sampled using that dict as above.

        Number of parameter settings that are produced.

        Pseudo random number generator state used for random uniform sampling
        from lists of possible values instead of scipy.stats distributions.

    Returns
    -------
    params : dict of str to any
        **Yields** dictionaries mapping each estimator parameter to
        as sampled value.

    Examples
    --------
    >>> from sklearn.model_selection import ParameterSampler
    >>> from scipy.stats.distributions import expon
    >>> param_grid = {'a':[1, 2], 'b': expon()}
    >>> param_list = list(ParameterSampler(param_grid, n_iter=4,
    ...                                    random_state=rng))
    >>> rounded_list = [dict((k, round(v, 6)) for (k, v) in d.items())
    ...                 for d in param_list]
    >>> rounded_list == [{'b': 0.89856, 'a': 1},
    ...                  {'b': 0.923223, 'a': 1},
    ...                  {'b': 1.878964, 'a': 2},
    ...                  {'b': 1.038159, 'a': 2}]
    True
    """

    def __init__(self, param_distributions, n_iter, *, random_state=None):
        if not isinstance(param_distributions, (Mapping, Iterable)):
                "Parameter distribution is not a dict or a list,"
                f" got: {param_distributions!r} of type "
                f"{type(param_distributions).__name__}"
            )

        if isinstance(param_distributions, Mapping):
            param_distributions = [param_distributions]

        for dist in param_distributions:
            if not isinstance(dist, dict):
                    "Parameter distribution is not a dict ({!r})".format(dist)
                )
            for key in dist:
                if not isinstance(dist[key], Iterable) and not hasattr(
                    dist[key], "rvs"
                ):
                        f"Parameter grid for parameter {key!r} is not iterable "
                        f"or a distribution (value={dist[key]})"
                    )
        self.param_distributions = param_distributions

    def _is_all_lists(self):
        return all(
            all(not hasattr(v, "rvs") for v in dist.values())
            for dist in self.param_distributions
        )

        # if all distributions are given as lists, we want to sample without
        # replacement
        if self._is_all_lists():
            # look up sampled parameter settings in parameter grid
            param_grid = ParameterGrid(self.param_distributions)
            grid_size = len(param_grid)
            n_iter = self.n_iter

            if grid_size < n_iter:
                    "The total space of parameters %d is smaller "
                    "than n_iter=%d. Running %d iterations. For exhaustive "
                    "searches, use GridSearchCV." % (grid_size, self.n_iter, grid_size),
                )
                n_iter = grid_size
            for i in sample_without_replacement(grid_size, n_iter, random_state=rng):
                yield param_grid[i]

        else:
            for _ in range(self.n_iter):
                dist = rng.choice(self.param_distributions)
                # Always sort the keys of a dictionary, for reproducibility
                items = sorted(dist.items())
                params = dict()
                for k, v in items:
                    if hasattr(v, "rvs"):
                        params[k] = v.rvs(random_state=rng)
                    else:
                        params[k] = v[rng.randint(len(v))]
                yield params

        """Number of points that will be sampled."""
            grid_size = len(ParameterGrid(self.param_distributions))
            return min(self.n_iter, grid_size)
        else:
            return self.n_iter

def _check_refit(search_cv, attr):
    if not search_cv.refit:
            f"This {type(search_cv).__name__} instance was initialized with "
            f"`refit=False`. {attr} is available only after refitting on the best "
            "parameters. You can refit an estimator manually using the "
            "`best_params_` attribute"
        )

def _search_estimator_has(attr):

    Calling a prediction method will only be available if `refit=True`. In
    such case, we check first the fitted best estimator. If it is not
    fitted, we check the unfitted estimator.

    Checking the unfitted estimator allows to use `hasattr` on the `SearchCV`
    instance even before calling `fit`.
    """

        _check_refit(self, attr)
        if hasattr(self, "best_estimator_"):
            # raise an AttributeError if `attr` does not exist
            getattr(self.best_estimator_, attr)
        # raise an AttributeError if `attr` does not exist

def _yield_masked_array_for_each_param(candidate_params):
    """
    Yield a masked array for each candidate param.

    `candidate_params` is a sequence of params which were used in
    a `GridSearchCV`. We use masked arrays for the results, as not
    all params are necessarily present in each element of
    `candidate_params`. For example, if using `GridSearchCV` with
    a `SVC` model, then one might search over params like:

        - kernel=["rbf"], gamma=[0.1, 1]
        - kernel=["poly"], degree=[1, 2]

    and then param `'gamma'` would not be present in entries of
    `candidate_params` corresponding to `kernel='poly'`.
    """
    n_candidates = len(candidate_params)
    param_results = defaultdict(dict)

    for cand_idx, params in enumerate(candidate_params):
        for name, value in params.items():
            param_results["param_%s" % name][cand_idx] = value

    for key, param_result in param_results.items():
        param_list = list(param_result.values())
        try:
            arr = np.array(param_list)
            # This can happen when param_list contains lists of different
            # lengths, for example:
            # param_list=[[1], [2, 3]]
            arr_dtype = np.dtype(object)
        else:
            # There are two cases when we don't use the automatically inferred
            # dtype when creating the array and we use object instead:
            # - string dtype
            # - when array.ndim > 1, that means that param_list was something
            #   like a list of same-size sequences, which gets turned into a
            #   multi-dimensional array but we want a 1d array
            arr_dtype = arr.dtype if arr.dtype.kind != "U" and arr.ndim == 1 else object

        # Use one MaskedArray and mask all the places where the param is not
        # applicable for that candidate (which may not contain all the params).
        ma = MaskedArray(np.empty(n_candidates, dtype=arr_dtype), mask=True)
        for index, value in param_result.items():
            # Setting the value at an index unmasks that index
            ma[index] = value
        yield (key, ma)

class BaseSearchCV(MetaEstimatorMixin, BaseEstimator, metaclass=ABCMeta):
    """Abstract base class for hyper parameter search with cross-validation."""

            callable,
            list,
            tuple,
            dict,
            None,
        ],
        "n_jobs": [numbers.Integral, None],
        "refit": ["boolean", str, callable],
        "pre_dispatch": [numbers.Integral, str],
        "error_score": [StrOptions({"raise"}), numbers.Real],
        "return_train_score": ["boolean"],
    }

        self,
        *,
        cv=None,
        error_score=np.nan,
        return_train_score=True,
    ):

    @property

        # allows cross-validation to see 'precomputed' metrics
        tags.input_tags.pairwise = sub_estimator_tags.input_tags.pairwise
        tags.array_api_support = sub_estimator_tags.array_api_support

    def score(self, X, y=None, **params):
        """Return the score on the given data, if the estimator has been refit.

        This uses the score defined by ``scoring`` where provided, and the
        ``best_estimator_.score`` method otherwise.

        y : array-like of shape (n_samples, n_output) \
            or (n_samples,), default=None

            Parameters to be passed to the underlying scorer(s).

                details.

        Returns
        -------
            The score defined by ``scoring`` if provided, and the
            ``best_estimator_.score`` method otherwise.
        """
        _check_refit(self, "score")

            score_params = process_routing(self, "score", **params).scorer["score"]
        else:
            score_params = dict()

        if self.scorer_ is None:
                "No score function explicitly defined, "
                "and the estimator doesn't provide one %s" % self.best_estimator_
            )
        if isinstance(self.scorer_, dict):
            if self.multimetric_:
                scorer = self.scorer_[self.refit]
            else:
                scorer = self.scorer_
            return scorer(self.best_estimator_, X, y, **score_params)

        # callable
        score = self.scorer_(self.best_estimator_, X, y, **score_params)
        if self.multimetric_:
            score = score[self.refit]

    @available_if(_search_estimator_has("score_samples"))
        """Call score_samples on the estimator with the best found parameters.

        Only available if ``refit=True`` and the underlying estimator supports
        ``score_samples``.

            Data to predict on. Must fulfill input requirements
            of the underlying estimator.

        Returns
        -------
            The ``best_estimator_.score_samples`` method.
        """
        return self.best_estimator_.score_samples(X)

    @available_if(_search_estimator_has("predict"))
        """Call predict on the estimator with the best found parameters.

        ``predict``.

        X : indexable, length n_samples
            Must fulfill the input assumptions of the

        Returns
        -------
            The predicted labels or values for `X` based on the estimator with
            the best found parameters.
        """
        return self.best_estimator_.predict(X)

    @available_if(_search_estimator_has("predict_proba"))
        """Call predict_proba on the estimator with the best found parameters.

        ``predict_proba``.

        Returns
        -------
        y_pred : ndarray of shape (n_samples,) or (n_samples, n_classes)
            Predicted class probabilities for `X` based on the estimator with
            the best found parameters. The order of the classes corresponds
            to that in the fitted attribute :term:`classes_`.
        """
        return self.best_estimator_.predict_proba(X)

    @available_if(_search_estimator_has("predict_log_proba"))
        """Call predict_log_proba on the estimator with the best found parameters.

        ``predict_log_proba``.

        Returns
        -------
            Predicted class log-probabilities for `X` based on the estimator
            with the best found parameters. The order of the classes
            corresponds to that in the fitted attribute :term:`classes_`.
        """
        return self.best_estimator_.predict_log_proba(X)

    @available_if(_search_estimator_has("decision_function"))
        """Call decision_function on the estimator with the best found parameters.

        ``decision_function``.

        Returns
        -------
        y_score : ndarray of shape (n_samples,) or (n_samples, n_classes) \
                or (n_samples, n_classes * (n_classes-1) / 2)
            Result of the decision function for `X` based on the estimator with
        """
        return self.best_estimator_.decision_function(X)

    @available_if(_search_estimator_has("transform"))
        """Call transform on the estimator with the best found parameters.

        Only available if the underlying estimator supports ``transform`` and
        ``refit=True``.

        Returns
        -------
        Xt : {ndarray, sparse matrix} of shape (n_samples, n_features)
            `X` transformed in the new space based on the estimator with
        """
        return self.best_estimator_.transform(X)

    @available_if(_search_estimator_has("inverse_transform"))
        """Call inverse_transform on the estimator with the best found params.

        Only available if the underlying estimator implements
        ``inverse_transform`` and ``refit=True``.

        Returns
        -------
            Result of the `inverse_transform` function for `Xt` based on the
            estimator with the best found parameters.
        """
        return self.best_estimator_.inverse_transform(X)

    @property
        """Number of features seen during :term:`fit`.

        Only available when `refit=True`.
        """
        # that hasattr() fails if the search estimator isn't fitted.
        try:
                )
            ) from nfe

        return self.best_estimator_.n_features_in_

    @property
        """Class labels.

        Only available when `refit=True` and the estimator is a classifier.
        """
        _search_estimator_has("classes_")(self)
        return self.best_estimator_.classes_

    def _run_search(self, evaluate_candidates):
        """Repeatedly calls `evaluate_candidates` to conduct a search.

        This method, implemented in sub-classes, makes it possible to
        customize the scheduling of evaluations: GridSearchCV and
        RandomizedSearchCV schedule evaluations for their whole parameter
        search space at once but other more sequential approaches are also
        possible: for instance is possible to iteratively schedule evaluations
        for new regions of the parameter search space based on previously
        collected evaluation results. This makes it possible to implement
        Bayesian optimization or more generally sequential model-based
        optimization by deriving from the BaseSearchCV abstract base class.
        For example, Successive Halving is implemented by calling
        `evaluate_candidates` multiples times (once per iteration of the SH
        process), each time passing a different set of candidates with `X`
        and `y` of increasing sizes.

        evaluate_candidates : callable
            This callback accepts:
                - a list of candidates, where each candidate is a dict of
                  parameter settings.
                - an optional `cv` parameter which can be used to e.g.
                  evaluate candidates on different dataset splits, or
                  evaluate candidates on subsampled data (as done in the
                  Successive Halving estimators). By default, the original
                  `cv` parameter is used, and it is available as a private
                  `_checked_cv_orig` attribute.
                - an optional `more_results` dict. Each key will be added to
                  the `cv_results_` attribute. Values should be lists of
                  length `n_candidates`

            It returns a dict of all results so far, formatted like
            ``cv_results_``.

            Important note (relevant whether the default cv is used or not):
            in randomized splitters, and unless the random_state parameter of
            cv was set to an int, calling cv.split() multiple times will
            yield different splits. Since cv.split() is called in
            evaluate_candidates, this means that candidates will be evaluated
            on different splits each time evaluate_candidates is called. This
            might be a methodological issue depending on the search strategy
            that you're implementing. To prevent randomized splitters from
            being used, you may use _split._yields_constant_splits()

        Examples
        --------

        ::

            def _run_search(self, evaluate_candidates):
                'Try C=0.1 only if C=1 is better than C=10'
                all_results = evaluate_candidates([{'C': 1}, {'C': 10}])
                score = all_results['mean_test_score']
                if score[0] < score[1]:
                    evaluate_candidates([{'C': 0.1}])
        """
        raise NotImplementedError("_run_search not implemented.")

    def _check_refit_for_multimetric(self, scores):
        """Check `refit` is compatible with `scores` is valid"""
        multimetric_refit_msg = (
            "For multi-metric scoring, the parameter refit must be set to a "
            "scorer key or a callable to refit an estimator with the best "
            "parameter setting on the whole data and make the best_* "
            "attributes available for that metric. If this is not needed, "
            f"refit should be set to False explicitly. {self.refit!r} was "
            "passed."
        )

        valid_refit_dict = isinstance(self.refit, str) and self.refit in scores

        if (
            self.refit is not False
            and not valid_refit_dict
            and not callable(self.refit)
        ):
            raise ValueError(multimetric_refit_msg)

    def _select_best_index(refit, refit_metric, results):
        """Select index of the best combination of hyperparemeters."""
        if callable(refit):
            # If callable, refit is expected to return the index of the best
            # parameter set.
            best_index = refit(results)
            if not isinstance(best_index, numbers.Integral):
                raise TypeError("best_index_ returned is not an integer")
            if best_index < 0 or best_index >= len(results["params"]):
                raise IndexError("best_index_ index out of range")
        else:
            best_index = results[f"rank_test_{refit_metric}"].argmin()
        return best_index

    def _get_scorers(self):
        """Get the scorer(s) to be used.

        This is used in ``fit`` and ``get_metadata_routing``.

        Returns
        -------
        scorers, refit_metric
        """
        refit_metric = "score"

        if callable(self.scoring):
            scorers = self.scoring
        elif self.scoring is None or isinstance(self.scoring, str):
            scorers = check_scoring(self.estimator, self.scoring)
        else:
            scorers = _check_multimetric_scoring(self.estimator, self.scoring)
            self._check_refit_for_multimetric(scorers)
            refit_metric = self.refit
            scorers = _MultimetricScorer(
                scorers=scorers, raise_exc=(self.error_score == "raise")
            )

        return scorers, refit_metric

    def _check_scorers_accept_sample_weight(self):
        scorers, _ = self._get_scorers()
        # In the multimetric case, warn the user for each scorer separately
        if isinstance(scorers, _MultimetricScorer):
            for name, scorer in scorers._scorers.items():
                if not scorer._accept_sample_weight():
                        f"The scoring {name}={scorer} does not support sample_weight, "
                        "which may lead to statistically incorrect results when "
                        f"fitting {self} with sample_weight. "
                    )
            return scorers._accept_sample_weight()
        # In most cases, scorers is a Scorer object
        # But it's a function when user passes scoring=function
        if hasattr(scorers, "_accept_sample_weight"):
            accept = scorers._accept_sample_weight()
        else:
            accept = "sample_weight" in signature(scorers).parameters
        if not accept:
                f"The scoring {scorers} does not support sample_weight, "
                "which may lead to statistically incorrect results when "
                f"fitting {self} with sample_weight. "
            )
        return accept

    def _get_routed_params_for_fit(self, params):
        """Get the parameters to be used for routing.

        This is a method instead of a snippet in ``fit`` since it's used twice,
        here in ``fit``, and in ``HalvingRandomSearchCV.fit``.
        """
        else:
            params = params.copy()
            groups = params.pop("groups", None)
                estimator=Bunch(fit=params),
            )
            # NOTE: sample_weight is forwarded to the scorer if sample_weight
            # is not None and scorers accept sample_weight. For _MultimetricScorer,
            # sample_weight is forwarded if any scorer accepts sample_weight
            if (
                params.get("sample_weight") is not None
                and self._check_scorers_accept_sample_weight()
            ):
                routed_params.scorer.score["sample_weight"] = params["sample_weight"]
        return routed_params

        # *SearchCV.estimator is not validated yet
    )
        """Run fit with all sets of parameters.

        X : array-like of shape (n_samples, n_features) or (n_samples, n_samples)
            `n_features` is the number of features. For precomputed kernel or
            distance matrix, the expected shape of X is (n_samples, n_samples).

            Parameters passed to the ``fit`` method of the estimator, the scorer,
            and the CV splitter.

            If a fit parameter is an array-like whose length is equal to
            `num_samples` then it will be split by cross-validation along with
            `X` and `y`. For example, the :term:`sample_weight` parameter is
            split because `len(sample_weights) = len(X)`. However, this behavior
            does not apply to `groups` which is passed to the splitter configured
            via the `cv` parameter of the constructor. Thus, `groups` is used
            *to perform the split* and determines which samples are
            assigned to the each side of the a split.

        Returns
        -------
        """
        estimator = self.estimator
        scorers, refit_metric = self._get_scorers()

        params = _check_method_params(X, params=params)

        routed_params = self._get_routed_params_for_fit(params)

        cv_orig = check_cv(self.cv, y, classifier=is_classifier(estimator))
        n_splits = cv_orig.get_n_splits(X, y, **routed_params.splitter.split)

        base_estimator = clone(self.estimator)

        parallel = Parallel(n_jobs=self.n_jobs, pre_dispatch=self.pre_dispatch)

        fit_and_score_kwargs = dict(
            scorer=scorers,
            fit_params=routed_params.estimator.fit,
            score_params=routed_params.scorer.score,
            return_train_score=self.return_train_score,
            return_n_test_samples=True,
            return_times=True,
            return_parameters=False,
            error_score=self.error_score,
        )
        with parallel:
            all_candidate_params = []
            all_out = []
            all_more_results = defaultdict(list)

            def evaluate_candidates(candidate_params, cv=None, more_results=None):
                cv = cv or cv_orig
                candidate_params = list(candidate_params)
                n_candidates = len(candidate_params)

                    print(
                        "Fitting {0} folds for each of {1} candidates,"
                        " totalling {2} fits".format(
                            n_splits, n_candidates, n_candidates * n_splits
                        )
                    )

                out = parallel(
                    delayed(_fit_and_score)(
                        clone(base_estimator),
                        X,
                        y,
                        parameters=parameters,
                        split_progress=(split_idx, n_splits),
                        candidate_progress=(cand_idx, n_candidates),
                        **fit_and_score_kwargs,
                    )
                    for (cand_idx, parameters), (split_idx, (train, test)) in product(
                        enumerate(candidate_params),
                        enumerate(cv.split(X, y, **routed_params.splitter.split)),
                    )
                )

                if len(out) < 1:
                        "No fits were performed. "
                        "Was the CV iterator empty? "
                        "Were there no candidates?"
                    )
                elif len(out) != n_candidates * n_splits:
                        "cv.split and cv.get_n_splits returned "
                        "inconsistent results. Expected {} "
                        "splits, got {}".format(n_splits, len(out) // n_candidates)
                    )

                _warn_or_raise_about_fit_failures(out, self.error_score)

                # For callable self.scoring, the return type is only know after
                # calling. If the return type is a dictionary, the error scores
                # can now be inserted with the correct key. The type checking
                # of out will be done in `_insert_error_scores`.
                if callable(self.scoring):
                    _insert_error_scores(out, self.error_score)

                all_candidate_params.extend(candidate_params)
                all_out.extend(out)

                if more_results is not None:
                    for key, value in more_results.items():
                        all_more_results[key].extend(value)

                nonlocal results
                results = self._format_results(
                    all_candidate_params, n_splits, all_out, all_more_results
                )

                return results

            self._run_search(evaluate_candidates)

            # multimetric is determined here because in the case of a callable
            # self.scoring the return type is only known after calling
            first_test_score = all_out[0]["test_scores"]
            self.multimetric_ = isinstance(first_test_score, dict)

            # check refit_metric now for a callable scorer that is multimetric
            if callable(self.scoring) and self.multimetric_:
                self._check_refit_for_multimetric(first_test_score)
                refit_metric = self.refit

        # For multi-metric evaluation, store the best_index_, best_params_ and
        # best_score_ iff refit is one of the scorer names
        # In single metric evaluation, refit_metric is "score"
        if self.refit or not self.multimetric_:
            self.best_index_ = self._select_best_index(
                self.refit, refit_metric, results
            )
            if not callable(self.refit):
                # With a non-custom callable, we can select the best score
                # based on the best index
                self.best_score_ = results[f"mean_test_{refit_metric}"][
                    self.best_index_
                ]
            self.best_params_ = results["params"][self.best_index_]

        if self.refit:
            # here we clone the estimator as well as the parameters, since
            # sometimes the parameters themselves might be estimators, e.g.
            # when we search over different estimators in a pipeline.
            # ref: https://github.com/scikit-learn/scikit-learn/pull/26786
            self.best_estimator_ = clone(base_estimator).set_params(
                **clone(self.best_params_, safe=False)
            )

            refit_start_time = time.time()
            if y is not None:
                self.best_estimator_.fit(X, y, **routed_params.estimator.fit)
            else:
                self.best_estimator_.fit(X, **routed_params.estimator.fit)
            refit_end_time = time.time()
            self.refit_time_ = refit_end_time - refit_start_time

            if hasattr(self.best_estimator_, "feature_names_in_"):
                self.feature_names_in_ = self.best_estimator_.feature_names_in_

        # Store the only scorer not as a dict for single metric evaluation
            self.scorer_ = scorers._scorers
        else:
            self.scorer_ = scorers

        self.cv_results_ = results
        self.n_splits_ = n_splits

    def _format_results(self, candidate_params, n_splits, out, more_results=None):
        n_candidates = len(candidate_params)
        out = _aggregate_score_dicts(out)

        results = dict(more_results or {})
        for key, val in results.items():
            # each value is a list (as per evaluate_candidate's convention)
            # we convert it to an array for consistency with the other keys
            results[key] = np.asarray(val)

        def _store(key_name, array, weights=None, splits=False, rank=False):
            """A small helper to store the scores/times to the cv_results_"""
            # When iterated first by splits, then by parameters
            # We want `array` to have `n_candidates` rows and `n_splits` cols.
            array = np.array(array, dtype=np.float64).reshape(n_candidates, n_splits)
            if splits:
                for split_idx in range(n_splits):
                    # Uses closure to alter the results
                    results["split%d_%s" % (split_idx, key_name)] = array[:, split_idx]

            array_means = np.average(array, axis=1, weights=weights)
            results["mean_%s" % key_name] = array_means

            if key_name.startswith(("train_", "test_")) and np.any(
                ~np.isfinite(array_means)
            ):
                    (
                        f"One or more of the {key_name.split('_')[0]} scores "
                        f"are non-finite: {array_means}"
                    ),
                    category=UserWarning,
                )

            # Weighted std is not directly available in numpy
            array_stds = np.sqrt(
                np.average(
                    (array - array_means[:, np.newaxis]) ** 2, axis=1, weights=weights
                )
            )
            results["std_%s" % key_name] = array_stds

            if rank:
                # When the fit/scoring fails `array_means` contains NaNs, we
                # will exclude them from the ranking process and consider them
                # as tied with the worst performers.
                if np.isnan(array_means).all():
                    # All fit/scoring routines failed.
                    rank_result = np.ones_like(array_means, dtype=np.int32)
                else:
                    min_array_means = np.nanmin(array_means) - 1
                    array_means = np.nan_to_num(array_means, nan=min_array_means)
                    rank_result = rankdata(-array_means, method="min").astype(
                        np.int32, copy=False
                    )
                results["rank_%s" % key_name] = rank_result

        _store("fit_time", out["fit_time"])
        _store("score_time", out["score_time"])
        # Store a list of param dicts at the key 'params'
        for param, ma in _yield_masked_array_for_each_param(candidate_params):
            results[param] = ma
        results["params"] = candidate_params

        test_scores_dict = _normalize_score_results(out["test_scores"])
        if self.return_train_score:
            train_scores_dict = _normalize_score_results(out["train_scores"])

        for scorer_name in test_scores_dict:
            # Computed the (weighted) mean and std for test scores alone
            _store(
                "test_%s" % scorer_name,
                test_scores_dict[scorer_name],
                splits=True,
                rank=True,
                weights=None,
            )
            if self.return_train_score:
                _store(
                    "train_%s" % scorer_name,
                    train_scores_dict[scorer_name],
                    splits=True,
                )

        Returns
        -------
        """
        )

        scorer, _ = self._get_scorers()
            scorer=scorer,
            .add(caller="fit", callee="score"),
        )
        )

            key, estimator = "best_estimator_", self.best_estimator_
        else:
            key, estimator = "estimator", self.estimator

            "parallel",
            [estimator],
            names=[f"{key}: {estimator.__class__.__name__}"],
            name_details=[str(estimator)],
        )

class GridSearchCV(BaseSearchCV):
    """Exhaustive search over specified parameter values for an estimator.

    Important members are fit, predict.

    GridSearchCV implements a "fit" and a "score" method.
    It also implements "score_samples", "predict", "predict_proba",
    "decision_function", "transform" and "inverse_transform" if they are
    implemented in the estimator used.

    The parameters of the estimator used to apply these methods are optimized
    by cross-validated grid-search over a parameter grid.

        This is assumed to implement the scikit-learn estimator interface.
        Either estimator needs to provide a ``score`` function,
        or ``scoring`` must be passed.

    param_grid : dict or list of dictionaries
        Dictionary with parameters names (`str`) as keys and lists of
        parameter settings to try as values, or a list of such
        dictionaries, in which case the grids spanned by each dictionary
        in the list are explored. This enables searching over any sequence
        of parameter settings.

    scoring : str, callable, list, tuple or dict, default=None

        - a dictionary with metric names as keys and callables as values.

    refit : bool, str, or callable, default=True
        Refit an estimator using the best found parameters on the whole
        dataset.

        For multiple metric evaluation, this needs to be a `str` denoting the
        scorer that would be used to find the best parameters for refitting
        the estimator at the end.

        Where there are considerations other than maximum score in
        choosing a best estimator, ``refit`` can be set to a function which
        returns the selected ``best_index_`` given ``cv_results_``. In that
        case, the ``best_estimator_`` and ``best_params_`` will be set
        according to the returned ``best_index_`` while the ``best_score_``
        attribute will not be available.

        The refitted estimator is made available at the ``best_estimator_``
        attribute and permits using ``predict`` directly on this
        ``GridSearchCV`` instance.

        Also for multiple metric evaluation, the attributes ``best_index_``,
        ``best_score_`` and ``best_params_`` will only be available if
        ``refit`` is set and all of them will be determined w.r.t this specific
        scorer.

        See ``scoring`` parameter to know more about multiple metric
        evaluation.

        See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py`
        to see how to design a custom selection strategy using a callable
        via `refit`.

            Support for callable added.

        either binary or multiclass, :class:`StratifiedKFold` is used. In all
        other cases, :class:`KFold` is used. These splitters are instantiated
        with `shuffle=False` so the splits will be the same across calls.

        Controls the verbosity: the higher, the more messages.

        - >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.

    pre_dispatch : int, or str, default='2*n_jobs'
        Controls the number of jobs that get dispatched during parallel
        execution. Reducing this number can be useful to avoid an
        explosion of memory consumption when more jobs get dispatched
        than CPUs can process. This parameter can be:

        - None, in which case all the jobs are immediately created and spawned. Use
          this for lightweight and fast-running jobs, to avoid delays due to on-demand
          spawning of the jobs
        - An int, giving the exact number of total jobs that are spawned
        - A str, giving an expression as a function of n_jobs, as in '2*n_jobs'

    error_score : 'raise' or numeric, default=np.nan
        Value to assign to the score if an error occurs in estimator fitting.
        If set to 'raise', the error is raised. If a numeric value is given,
        FitFailedWarning is raised. This parameter does not affect the refit
        step, which will always raise the error.

    return_train_score : bool, default=False
        If ``False``, the ``cv_results_`` attribute will not include training
        scores.
        Computing training scores is used to get insights on how different
        parameter settings impact the overfitting/underfitting trade-off.
        However computing the scores on the training set can be computationally
        expensive and is not strictly required to select the parameters that
        yield the best generalization performance.

            Default value was changed from ``True`` to ``False``

    cv_results_ : dict of numpy (masked) ndarrays
        A dict with keys as column headers and values as columns, that can be
        imported into a pandas ``DataFrame``.

        For instance the below given table

        +------------+-----------+------------+-----------------+---+---------+
        |param_kernel|param_gamma|param_degree|split0_test_score|...|rank_t...|
        +============+===========+============+=================+===+=========+
        |  'poly'    |     --    |      2     |       0.80      |...|    2    |
        |  'poly'    |     --    |      3     |       0.70      |...|    4    |
        |  'rbf'     |     0.1   |     --     |       0.80      |...|    3    |
        |  'rbf'     |     0.2   |     --     |       0.93      |...|    1    |

        will be represented by a ``cv_results_`` dict of::

            {
            'param_kernel': masked_array(data = ['poly', 'poly', 'rbf', 'rbf'],
                                         mask = [False False False False]...)
            'param_gamma': masked_array(data = [-- -- 0.1 0.2],
                                        mask = [ True  True False False]...),
            'param_degree': masked_array(data = [2.0 3.0 -- --],
                                         mask = [False False  True  True]...),
            'split0_test_score'  : [0.80, 0.70, 0.80, 0.93],
            'split1_test_score'  : [0.82, 0.50, 0.70, 0.78],
            'mean_test_score'    : [0.81, 0.60, 0.75, 0.85],
            'std_test_score'     : [0.01, 0.10, 0.05, 0.08],
            'rank_test_score'    : [2, 4, 3, 1],
            'split0_train_score' : [0.80, 0.92, 0.70, 0.93],
            'split1_train_score' : [0.82, 0.55, 0.70, 0.87],
            'mean_train_score'   : [0.81, 0.74, 0.70, 0.90],
            'std_train_score'    : [0.01, 0.19, 0.00, 0.03],
            'mean_fit_time'      : [0.73, 0.63, 0.43, 0.49],
            'std_fit_time'       : [0.01, 0.02, 0.01, 0.01],
            'mean_score_time'    : [0.01, 0.06, 0.04, 0.04],
            'std_score_time'     : [0.00, 0.00, 0.00, 0.01],
            'params'             : [{'kernel': 'poly', 'degree': 2}, ...],
            }

        NOTE

        The key ``'params'`` is used to store a list of parameter
        settings dicts for all the parameter candidates.

        The ``mean_fit_time``, ``std_fit_time``, ``mean_score_time`` and
        ``std_score_time`` are all in seconds.

        For multi-metric evaluation, the scores for all the scorers are
        available in the ``cv_results_`` dict at the keys ending with that
        scorer's name (``'_<scorer_name>'``) instead of ``'_score'`` shown
        above. ('split0_test_precision', 'mean_train_precision' etc.)

    best_estimator_ : estimator
        Estimator that was chosen by the search, i.e. estimator
        which gave highest score (or smallest loss if specified)
        on the left out data. Not available if ``refit=False``.

        See ``refit`` parameter for more information on allowed values.

        Mean cross-validated score of the best_estimator

        For multi-metric evaluation, this is present only if ``refit`` is
        specified.

        This attribute is not available if ``refit`` is a function.

    best_params_ : dict
        Parameter setting that gave the best results on the hold out data.

    best_index_ : int
        The index (of the ``cv_results_`` arrays) which corresponds to the best
        candidate parameter setting.

        The dict at ``search.cv_results_['params'][search.best_index_]`` gives
        the parameter setting for the best model, that gives the highest
        mean score (``search.best_score_``).

    scorer_ : function or a dict
        Scorer function used on the held out data to choose the best
        parameters for the model.

        For multi-metric evaluation, this attribute holds the validated
        ``scoring`` dict which maps the scorer key to the scorer callable.

    n_splits_ : int
        The number of cross-validation splits (folds/iterations).

    refit_time_ : float
        Seconds used for refitting the best model on the whole dataset.

        This is present only if ``refit`` is not False.

    multimetric_ : bool
        Whether or not the scorers compute several metrics.

        The classes labels. This is present only if ``refit`` is specified and
        the underlying estimator is a classifier.

        Number of features seen during :term:`fit`. Only defined if
        `best_estimator_` is defined (see the documentation for the `refit`
        parameter for more details) and that `best_estimator_` exposes
        `n_features_in_` when fit.

        Names of features seen during :term:`fit`. Only defined if
        `feature_names_in_` when fit.

    See Also
    --------
    ParameterGrid : Generates all the combinations of a hyperparameter grid.
    train_test_split : Utility function to split the data into a development
        set usable for fitting a GridSearchCV instance and an evaluation set
        for its final evaluation.
    sklearn.metrics.make_scorer : Make a scorer from a performance metric or
        loss function.

    Notes
    -----
    The parameters selected are those that maximize the score of the left out
    data, unless an explicit score is passed in which case it is used instead.

    If `n_jobs` was set to a value higher than one, the data is copied for each
    point in the grid (and not `n_jobs` times). This is done for efficiency
    reasons if individual jobs take very little time, but may raise errors if
    the dataset is large and not enough memory is available.  A workaround in
    this case is to set `pre_dispatch`. Then, the memory is copied only
    `pre_dispatch` many times. A reasonable value for `pre_dispatch` is `2 *
    n_jobs`.

    Examples
    --------
    >>> from sklearn import svm, datasets
    >>> parameters = {'kernel':('linear', 'rbf'), 'C':[1, 10]}
    >>> svc = svm.SVC()
    >>> clf = GridSearchCV(svc, parameters)
    >>> clf.fit(iris.data, iris.target)
    GridSearchCV(estimator=SVC(),
                 param_grid={'C': [1, 10], 'kernel': ('linear', 'rbf')})
    >>> sorted(clf.cv_results_.keys())
    ['mean_fit_time', 'mean_score_time', 'mean_test_score',...
     'param_C', 'param_kernel', 'params',...
     'rank_test_score', 'split0_test_score',...
     'split2_test_score', ...
     'std_fit_time', 'std_score_time', 'std_test_score']
    """

        **BaseSearchCV._parameter_constraints,
        "param_grid": [dict, list],
    }

        self,
        *,
        cv=None,
    ):
            refit=refit,
            cv=cv,
            pre_dispatch=pre_dispatch,
            error_score=error_score,
            return_train_score=return_train_score,
        )

        """Search all candidates in param_grid"""
        evaluate_candidates(ParameterGrid(self.param_grid))

class RandomizedSearchCV(BaseSearchCV):
    """Randomized search on hyper parameters.

    RandomizedSearchCV implements a "fit" and a "score" method.

    by cross-validated search over parameter settings.

    In contrast to GridSearchCV, not all parameter values are tried out, but
    rather a fixed number of parameter settings is sampled from the specified
    distributions. The number of parameter settings that are tried is
    given by n_iter.

    If all parameters are presented as a list,

    Read more in the :ref:`User Guide <randomized_parameter_search>`.

        An object of that type is instantiated for each grid point.

    param_distributions : dict or list of dicts

        Number of parameter settings that are sampled. n_iter trades
        off runtime vs quality of the solution.

        If None, the estimator's score method is used.

        dataset.

        returns the selected ``best_index_`` given the ``cv_results_``. In that

        ``RandomizedSearchCV`` instance.

        scorer.

        scores.

        +--------------+-------------+-------------------+---+---------------+
        | param_kernel | param_gamma | split0_test_score |...|rank_test_score|
        +==============+=============+===================+===+===============+
        |    'rbf'     |     0.1     |       0.80        |...|       1       |
        |    'rbf'     |     0.2     |       0.84        |...|       3       |
        |    'rbf'     |     0.3     |       0.70        |...|       2       |

            {
            'param_kernel' : masked_array(data = ['rbf', 'rbf', 'rbf'],
                                          mask = False),
            'param_gamma'  : masked_array(data = [0.1 0.2 0.3], mask = False),
            'split0_test_score'  : [0.80, 0.84, 0.70],
            'split1_test_score'  : [0.82, 0.50, 0.70],
            'mean_test_score'    : [0.81, 0.67, 0.70],
            'std_test_score'     : [0.01, 0.24, 0.00],
            'rank_test_score'    : [1, 3, 2],
            'split0_train_score' : [0.80, 0.92, 0.70],
            'split1_train_score' : [0.82, 0.55, 0.70],
            'mean_train_score'   : [0.81, 0.74, 0.70],
            'std_train_score'    : [0.01, 0.19, 0.00],
            'mean_fit_time'      : [0.73, 0.63, 0.43],
            'std_fit_time'       : [0.01, 0.02, 0.01],
            'mean_score_time'    : [0.01, 0.06, 0.04],
            'std_score_time'     : [0.00, 0.00, 0.00],
            'params'             : [{'kernel' : 'rbf', 'gamma' : 0.1}, ...],
            }

        NOTE

        For multi-metric evaluation, this attribute is present only if
        ``refit`` is specified.

        Mean cross-validated score of the best_estimator.

        For multi-metric evaluation, this is not available if ``refit`` is
        ``False``. See ``refit`` parameter for more information.

    See Also
    --------
    GridSearchCV : Does exhaustive search over a grid of parameters.
    ParameterSampler : A generator over parameter settings, constructed from
        param_distributions.

    Notes
    -----
    The parameters selected are those that maximize the score of the held-out
    data, according to the scoring parameter.

    parameter setting(and not `n_jobs` times). This is done for efficiency
    n_jobs`.

    Examples
    --------
    >>> from sklearn.model_selection import RandomizedSearchCV
    >>> from scipy.stats import uniform
    >>> logistic = LogisticRegression(solver='saga', tol=1e-2, max_iter=200,
    ...                               random_state=0)
    >>> distributions = dict(C=uniform(loc=0, scale=4),
    ...                      penalty=['l2', 'l1'])
    >>> clf = RandomizedSearchCV(logistic, distributions, random_state=0)
    >>> search = clf.fit(iris.data, iris.target)
    >>> search.best_params_
    {'C': np.float64(2...), 'penalty': 'l1'}
    """

        "param_distributions": [dict, list],
        "n_iter": [Interval(numbers.Integral, 1, None, closed="left")],
    }

        self,
        param_distributions,
        *,
        cv=None,
    ):
            cv=cv,
        )

        """Search n_iter candidates from param_distributions"""
        evaluate_candidates(
            ParameterSampler(
                self.param_distributions, self.n_iter, random_state=self.random_state
            )
        )

            )

                    )
                ):
                    )
                    )

        Returns
        -------
        """
                yield {}
            else:

        )

        ind : int

        Returns
        -------
        """
                if ind == 0:
                    return {}
                else:
                    ind -= 1
                    continue

            else:
                out = {}
                return out

            )

                )
                ):
                    )

        )

                )

        else:
                    else:

        else:

        self,
        *,
        cv=None,
    ):

                details.

        Returns
        -------
        """

        else:

            )
            else:

        # callable

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        Returns
        -------
        """

        """
        try:
                )
            ) from nfe

        """

        Examples
        --------

        ::

        """

            "passed."
        )

        if (
        ):

        else:

        Returns
        -------
        """

        else:
            )

                    )
        else:
            )

        """
        else:
            )
            if (
            ):

        Returns
        -------
        """

        )

                    print(
                        )
                    )

                        X,
                        y,
                    )
                    )
                )

                    )
                    )

                )

            )
                ]

            )

            else:

        else:

            if splits:

            ):
                    (
                    ),
                )

                )
            )

            if rank:
                else:
                    )

            _store(
            )
                _store(
                )

        Returns
        -------
        """
        )

        )
        )

        else:

        )

        self,
        *,
        cv=None,
    ):
            cv=cv,
        )

        self,
        *,
        cv=None,
    ):
            cv=cv,
        )

            )
        )

            if splits:

            ):
                    (
                    ),
                )

                )
            )

            if rank:
                else:
                    )

                    print(
                        )
                    )

                        X,
                        y,
                    )
                    )
                )

                    )
                    )

                )

def _check_params_groups_deprecation(fit_params, params, groups, version):
    """A helper function to check deprecations on `groups` and `fit_params`.

    # TODO(SLEP6): To be removed when set_config(enable_metadata_routing=False) is not
    # possible.
    """
    if params is not None and fit_params is not None:
            "`params` and `fit_params` cannot both be provided. Pass parameters "
            "via `params`. `fit_params` is deprecated and will be removed in "
            f"version {version}."
        )
    elif fit_params is not None:
            (
                "`fit_params` is deprecated and will be removed in version {version}. "
                "Pass parameters via `params` instead."
            ),
        )
        params = fit_params

    params = {} if params is None else params

    _check_groups_routing_disabled(groups)

    return params

def _check_groups_routing_disabled(groups):
    if groups is not None and _routing_enabled():
            "`groups` can only be passed if metadata routing is not enabled via"
            " `sklearn.set_config(enable_metadata_routing=True)`. When routing is"
            " enabled, pass `groups` alongside other metadata via the `params` argument"
            " instead."
        )

def cross_validate(
    X,
    y=None,
    *,
    groups=None,
    cv=None,
    params=None,
    pre_dispatch="2*n_jobs",
    return_train_score=False,
    return_estimator=False,
    return_indices=False,
    error_score=np.nan,
):
    """Evaluate metric(s) by cross-validation and also record fit/score times.

    Read more in the :ref:`User Guide <multimetric_cross_validation>`.

    estimator : estimator object implementing 'fit'
        The object to use to fit the data.

        The data to fit. Can be for example a list, or an array.

    y : array-like of shape (n_samples,) or (n_samples, n_outputs), default=None
        The target variable to try to predict in the case of
        supervised learning.

    groups : array-like of shape (n_samples,), default=None
        Group labels for the samples used while splitting the dataset into
        train/test set. Only used in conjunction with a "Group" :term:`cv`
        instance (e.g., :class:`GroupKFold`).

            ``groups`` can only be passed if metadata routing is not enabled
            via ``sklearn.set_config(enable_metadata_routing=True)``. When routing
            is enabled, pass ``groups`` alongside other metadata via the ``params``
            argument instead. E.g.:
            ``cross_validate(..., params={'groups': groups})``.

        Strategy to evaluate the performance of the `estimator` across cross-validation
        splits.

        - a callable (see :ref:`scoring_callable`) that returns a single value.

        - int, to specify the number of folds in a `(Stratified)KFold`,

        For int/None inputs, if the estimator is a classifier and ``y`` is

        Number of jobs to run in parallel. Training the estimator and computing
        the score are parallelized over the cross-validation splits.

    params : dict, default=None
        Parameters to pass to the underlying estimator's ``fit``, the scorer,
        and the CV splitter.

    pre_dispatch : int or str, default='2*n_jobs'

        Whether to include train scores.

    return_estimator : bool, default=False
        Whether to return the estimators fitted on each split.

    return_indices : bool, default=False
        Whether to return the train-test indices selected for each split.

        If set to 'raise', the error is raised.
        If a numeric value is given, FitFailedWarning is raised.

    Returns
    -------
    scores : dict of float arrays of shape (n_splits,)
        Array of scores of the estimator for each run of the cross validation.

        A dict of arrays containing the score/time arrays for each scorer is
        returned. The possible keys for this ``dict`` are:

        ``test_score``
            The score array for test scores on each cv split.
            Suffix ``_score`` in ``test_score`` changes to a specific
            metric like ``test_r2`` or ``test_auc`` if there are
            multiple scoring metrics in the scoring parameter.
        ``train_score``
            The score array for train scores on each cv split.
            Suffix ``_score`` in ``train_score`` changes to a specific
            metric like ``train_r2`` or ``train_auc`` if there are
            This is available only if ``return_train_score`` parameter
            is ``True``.
        ``fit_time``
            The time for fitting the estimator on the train
            set for each cv split.
        ``score_time``
            The time for scoring the estimator on the test set for each
            cv split. (Note: time for scoring on the train set is not
            included even if ``return_train_score`` is set to ``True``).
        ``estimator``
            The estimator objects for each cv split.
            This is available only if ``return_estimator`` parameter
            is set to ``True``.
        ``indices``
            The train/test positional indices for each cv split. A dictionary
            is returned where the keys are either `"train"` or `"test"`
            and the associated values are a list of integer-dtyped NumPy
            arrays with the indices. Available only if `return_indices=True`.

    See Also
    --------
    cross_val_score : Run cross-validation for single metric evaluation.

    cross_val_predict : Get predictions from each split of cross-validation for
        diagnostic purposes.

    Examples
    --------
    >>> from sklearn import datasets, linear_model
    >>> from sklearn.model_selection import cross_validate
    >>> from sklearn.metrics import make_scorer
    >>> diabetes = datasets.load_diabetes()
    >>> X = diabetes.data[:150]
    >>> y = diabetes.target[:150]
    >>> lasso = linear_model.Lasso()

    Single metric evaluation using ``cross_validate``

    >>> cv_results = cross_validate(lasso, X, y, cv=3)
    >>> sorted(cv_results.keys())
    ['fit_time', 'score_time', 'test_score']
    >>> cv_results['test_score']
    array([0.3315057 , 0.08022103, 0.03531816])

    Multiple metric evaluation using ``cross_validate``
    (please refer the ``scoring`` parameter doc for more information)

    >>> scores = cross_validate(lasso, X, y, cv=3,
    ...                         scoring=('r2', 'neg_mean_squared_error'),
    ...                         return_train_score=True)
    >>> print(scores['test_neg_mean_squared_error'])
    [-3635.5... -3573.3... -6114.7...]
    >>> print(scores['train_r2'])
    [0.28009951 0.3908844  0.22784907]
    """

    X, y = indexable(X, y)
    cv = check_cv(cv, y, classifier=is_classifier(estimator))

    scorers = check_scoring(
        estimator, scoring=scoring, raise_exc=(error_score == "raise")
    )

    if _routing_enabled():
        # For estimators, a MetadataRouter is created in get_metadata_routing
        # methods. For these router methods, we create the router to use
        # `process_routing` on it.
        router = (
            MetadataRouter(owner="cross_validate")
            .add(
                splitter=cv,
            )
            .add(
                # TODO(SLEP6): also pass metadata to the predict method for
                # scoring?
            )
            .add(
                scorer=scorers,
            )
        )
        try:
            routed_params = process_routing(router, "fit", **params)
        except UnsetMetadataPassedError as e:
            # The default exception would mention `fit` since in the above
            # `process_routing` code, we pass `fit` as the caller. However,
            # the user is not calling `fit` directly, so we change the message
            # to make it more suitable for this case.
                message=str(e).replace("cross_validate.fit", "cross_validate"),
                unrequested_params=e.unrequested_params,
                routed_params=e.routed_params,
            )
    else:
        routed_params = Bunch()
        routed_params.splitter = Bunch(split={"groups": groups})
        routed_params.estimator = Bunch(fit=params)
        routed_params.scorer = Bunch(score={})

    indices = cv.split(X, y, **routed_params.splitter.split)
    if return_indices:
        # materialize the indices since we need to store them in the returned dict
        indices = list(indices)

    # We clone the estimator to make sure that all the folds are
    # independent, and that it is pickle-able.
    parallel = Parallel(n_jobs=n_jobs, verbose=verbose, pre_dispatch=pre_dispatch)
    results = parallel(
        delayed(_fit_and_score)(
            clone(estimator),
            X,
            y,
            train=train,
            test=test,
            parameters=None,
            return_estimator=return_estimator,
        )
        for train, test in indices
    )

    _warn_or_raise_about_fit_failures(results, error_score)

    # For callable scoring, the return type is only know after calling. If the
    # return type is a dictionary, the error scores can now be inserted with
    # the correct key.
        _insert_error_scores(results, error_score)

    results = _aggregate_score_dicts(results)

    ret = {}
    ret["fit_time"] = results["fit_time"]
    ret["score_time"] = results["score_time"]

    if return_estimator:
        ret["estimator"] = results["estimator"]

        ret["indices"] = {}
        ret["indices"]["train"], ret["indices"]["test"] = zip(*indices)

    test_scores_dict = _normalize_score_results(results["test_scores"])
    if return_train_score:
        train_scores_dict = _normalize_score_results(results["train_scores"])

    for name in test_scores_dict:
        ret["test_%s" % name] = test_scores_dict[name]
        if return_train_score:
            key = "train_%s" % name
            ret[key] = train_scores_dict[name]

    return ret

def _insert_error_scores(results, error_score):
    """Insert error in `results` by replacing them inplace with `error_score`.

    This only applies to multimetric scores because `_fit_and_score` will
    handle the single metric case.
    """
    successful_score = None
    failed_indices = []
    for i, result in enumerate(results):
        if result["fit_error"] is not None:
            failed_indices.append(i)
        elif successful_score is None:
            successful_score = result["test_scores"]

    if isinstance(successful_score, dict):
        formatted_error = {name: error_score for name in successful_score}
        for i in failed_indices:
            results[i]["test_scores"] = formatted_error.copy()
            if "train_scores" in results[i]:
                results[i]["train_scores"] = formatted_error.copy()

def _normalize_score_results(scores, scaler_score_key="score"):
    """Creates a scoring dictionary based on the type of `scores`"""
        # multimetric scoring
        return _aggregate_score_dicts(scores)
    # scaler
    return {scaler_score_key: scores}

def _warn_or_raise_about_fit_failures(results, error_score):
    fit_errors = [
        result["fit_error"] for result in results if result["fit_error"] is not None
    ]
    if fit_errors:
        num_failed_fits = len(fit_errors)
        num_fits = len(results)
        fit_errors_counter = Counter(fit_errors)
        delimiter = "-" * 80 + "\n"
        fit_errors_summary = "\n".join(
            f"{delimiter}{n} fits failed with the following error:\n{error}"
            for error, n in fit_errors_counter.items()
        )

        if num_failed_fits == num_fits:
            all_fits_failed_message = (
                f"\nAll the {num_fits} fits failed.\n"
                "It is very likely that your model is misconfigured.\n"
                "You can try to debug the error by setting error_score='raise'.\n\n"
                f"Below are more details about the failures:\n{fit_errors_summary}"
            )
            raise ValueError(all_fits_failed_message)

        else:
            some_fits_failed_message = (
                f"\n{num_failed_fits} fits failed out of a total of {num_fits}.\n"
                "The score on these train-test partitions for these parameters"
                f" will be set to {error_score}.\n"
                "If these failures are not expected, you can try to debug them "
                "by setting error_score='raise'.\n\n"
            )
            warnings.warn(some_fits_failed_message, FitFailedWarning)

def cross_val_score(
    X,
    y=None,
    *,
    cv=None,
):
    """Evaluate a score by cross-validation.

    Read more in the :ref:`User Guide <cross_validation>`.

    y : array-like of shape (n_samples,) or (n_samples, n_outputs), \

            ``cross_val_score(..., params={'groups': groups})``.

        splits.

          ``scorer(estimator, X, y)``, which should return only a single value.

        Similar to the use of `scoring` in :func:`cross_validate` but only a
        single metric is permitted.

        - `None`, to use the default 5-fold cross validation,
        - An iterable that generates (train, test) splits as arrays of indices.

        For `int`/`None` inputs, if the estimator is a classifier and `y` is

            `cv` default value if `None` changed from 3-fold to 5-fold.

        - ``None``, in which case all the jobs are immediately created and spawned. Use

    Returns
    -------
    scores : ndarray of float of shape=(len(list(cv)),)

    See Also
    --------
    cross_validate : To run cross-validation on multiple metrics and also to
        return train scores, fit times and score times.

    Examples
    --------
    >>> print(cross_val_score(lasso, X, y, cv=3))
    [0.3315057  0.08022103 0.03531816]
    """
    # To ensure multimetric format is not supported

    cv_results = cross_validate(
        X=X,
        y=y,
        groups=groups,
        scoring={"score": scorer},
        cv=cv,
        params=params,
        pre_dispatch=pre_dispatch,
        error_score=error_score,
    )
    return cv_results["test_score"]

def _fit_and_score(
    X,
    y,
    *,
    scorer,
    train,
    test,
    verbose,
    parameters,
    return_parameters=False,
    return_n_test_samples=False,
    return_times=False,
    split_progress=None,
    candidate_progress=None,
):
    """Fit estimator and compute scores for a given dataset split.

        The data to fit.

    y : array-like of shape (n_samples,) or (n_samples, n_outputs) or None

    scorer : A single callable or dict mapping scorer name to the callable
        If it is a single callable, the return value for ``train_scores`` and
        ``test_scores`` is a single float.

        For a dict, it should be one mapping the scorer name to the scorer
        callable object / function.

        The callable object / fn should have signature
        ``scorer(estimator, X, y)``.

    train : array-like of shape (n_train_samples,)
        Indices of training samples.

    test : array-like of shape (n_test_samples,)
        Indices of test samples.

    parameters : dict or None
        Parameters to be set on the estimator.

    fit_params : dict or None
        Parameters that will be passed to ``estimator.fit``.

    score_params : dict or None
        Parameters that will be passed to the scorer.

        Compute and return score on training set.

    return_parameters : bool, default=False
        Return parameters that has been used for the estimator.

    split_progress : {list, tuple} of int, default=None
        A list or tuple of format (<current_split_id>, <total_num_of_splits>).

    candidate_progress : {list, tuple} of int, default=None
        A list or tuple of format
        (<current_candidate_id>, <total_number_of_candidates>).

    return_n_test_samples : bool, default=False
        Whether to return the ``n_test_samples``.

    return_times : bool, default=False
        Whether to return the fit/score times.

        Whether to return the fitted estimator.

    Returns
    -------
    result : dict with the following attributes
        train_scores : dict of scorer name -> float
            Score on training set (for all the scorers),
            returned only if `return_train_score` is `True`.
        test_scores : dict of scorer name -> float
            Score on testing set (for all the scorers).
        n_test_samples : int
            Number of test samples.
        fit_time : float
            Time spent for fitting in seconds.
        score_time : float
            Time spent for scoring in seconds.
        parameters : dict or None
            The parameters that have been evaluated.
        estimator : estimator object
            The fitted estimator.
        fit_error : str or None
            Traceback str if the fit failed, None if the fit succeeded.
    """
    X_device = device(X)

    # Make sure that we can fancy index X even if train and test are provided
    # as NumPy arrays by NumPy only cross-validation splitters.
    train, test = xp.asarray(train, device=X_device), xp.asarray(test, device=X_device)

    if not isinstance(error_score, numbers.Number) and error_score != "raise":
            "error_score must be the string 'raise' or a numeric value. "
            "(Hint: if using 'raise', please make sure that it has been "
            "spelled correctly.)"
        )

    progress_msg = ""
    if verbose > 2:
        if split_progress is not None:
            progress_msg = f" {split_progress[0] + 1}/{split_progress[1]}"
        if candidate_progress and verbose > 9:
            progress_msg += f"; {candidate_progress[0] + 1}/{candidate_progress[1]}"

        if parameters is None:
            params_msg = ""
        else:
            sorted_keys = sorted(parameters)  # Ensure deterministic o/p
            params_msg = ", ".join(f"{k}={parameters[k]}" for k in sorted_keys)
    if verbose > 9:
        start_msg = f"[CV{progress_msg}] START {params_msg}"
        print(f"{start_msg}{(80 - len(start_msg)) * '.'}")

    # Adjust length of sample weights
    fit_params = fit_params if fit_params is not None else {}
    fit_params = _check_method_params(X, params=fit_params, indices=train)
    score_params = score_params if score_params is not None else {}
    score_params_train = _check_method_params(X, params=score_params, indices=train)
    score_params_test = _check_method_params(X, params=score_params, indices=test)

    if parameters is not None:
        # here we clone the parameters, since sometimes the parameters
        # themselves might be estimators, e.g. when we search over different
        # estimators in a pipeline.
        # ref: https://github.com/scikit-learn/scikit-learn/pull/26786
        estimator = estimator.set_params(**clone(parameters, safe=False))

    result = {}
    try:
        if y_train is None:
            estimator.fit(X_train, **fit_params)
        else:
            estimator.fit(X_train, y_train, **fit_params)

        # Note fit time as time until error
        fit_time = time.time() - start_time
        score_time = 0.0
        if error_score == "raise":
            raise
        elif isinstance(error_score, numbers.Number):
            if isinstance(scorer, _MultimetricScorer):
                test_scores = {name: error_score for name in scorer._scorers}
                if return_train_score:
                    train_scores = test_scores.copy()
            else:
                test_scores = error_score
                    train_scores = error_score
        result["fit_error"] = format_exc()
    else:
        result["fit_error"] = None

        test_scores = _score(
            estimator, X_test, y_test, scorer, score_params_test, error_score
        )
        score_time = time.time() - start_time - fit_time
            train_scores = _score(
                estimator, X_train, y_train, scorer, score_params_train, error_score
            )

        total_time = score_time + fit_time
        end_msg = f"[CV{progress_msg}] END "
        result_msg = params_msg + (";" if params_msg else "")
        if verbose > 2:
            if isinstance(test_scores, dict):
                for scorer_name in sorted(test_scores):
                    result_msg += f" {scorer_name}: ("
                    if return_train_score:
                        scorer_scores = train_scores[scorer_name]
                        result_msg += f"train={scorer_scores:.3f}, "
                    result_msg += f"test={test_scores[scorer_name]:.3f})"
            else:
                result_msg += ", score="
                    result_msg += f"(train={train_scores:.3f}, test={test_scores:.3f})"
                else:
                    result_msg += f"{test_scores:.3f}"
        result_msg += f" total time={logger.short_format_time(total_time)}"

        # Right align the result_msg
        end_msg += "." * (80 - len(end_msg) - len(result_msg))
        end_msg += result_msg
        print(end_msg)

    result["test_scores"] = test_scores
        result["train_scores"] = train_scores
    if return_n_test_samples:
        result["n_test_samples"] = _num_samples(X_test)
    if return_times:
        result["fit_time"] = fit_time
        result["score_time"] = score_time
    if return_parameters:
        result["parameters"] = parameters
        result["estimator"] = estimator

def _score(estimator, X_test, y_test, scorer, score_params, error_score="raise"):
    """Compute the score(s) of an estimator on a given test set.

    Will return a dict of floats if `scorer` is a _MultiMetricScorer, otherwise a single
    float is returned.
    """
    score_params = {} if score_params is None else score_params

    try:
        if y_test is None:
            scores = scorer(estimator, X_test, **score_params)
        else:
            scores = scorer(estimator, X_test, y_test, **score_params)
        if isinstance(scorer, _MultimetricScorer):
            # If `_MultimetricScorer` raises exception, the `error_score`
            # parameter is equal to "raise".
            raise
        else:
            if error_score == "raise":
                raise
            else:
                scores = error_score
                    (
                        "Scoring failed. The score on this train-test partition for "
                        f"these parameters will be set to {error_score}. Details: \n"
                        f"{format_exc()}"
                    ),
                )

    # Check non-raised error messages in `_MultimetricScorer`
    if isinstance(scorer, _MultimetricScorer):
        exception_messages = [
            (name, str_e) for name, str_e in scores.items() if isinstance(str_e, str)
        ]
        if exception_messages:
            # error_score != "raise"
            for name, str_e in exception_messages:
                scores[name] = error_score
                    (
                        f"{str_e}"
                    ),
                )

    error_msg = "scoring must return a number, got %s (%s) instead. (scorer=%s)"
    if isinstance(scores, dict):
        for name, score in scores.items():
            if hasattr(score, "item"):
                with suppress(ValueError):
                    # e.g. unwrap memmapped scalars
                    score = score.item()
            if not isinstance(score, numbers.Number):
                raise ValueError(error_msg % (score, type(score), name))
            scores[name] = score
    else:  # scalar
        if hasattr(scores, "item"):
            with suppress(ValueError):
                # e.g. unwrap memmapped scalars
                scores = scores.item()
        if not isinstance(scores, numbers.Number):
            raise ValueError(error_msg % (scores, type(scores), scorer))

def cross_val_predict(
    X,
    y=None,
    *,
    cv=None,
    method="predict",
):
    """Generate cross-validated estimates for each input data point.

    The data is split according to the cv parameter. Each sample belongs
    to exactly one test set, and its prediction is computed with an
    estimator fitted on the corresponding training set.

    Passing these predictions into an evaluation metric may not be a valid
    way to measure generalization performance. Results can differ from
    :func:`cross_validate` and :func:`cross_val_score` unless all tests sets
    have equal size and the metric decomposes over samples.

        The estimator instance to use to fit the data. It must implement a `fit`
        method and the method given by the `method` parameter.

        The data to fit. Can be, for example a list, or an array at least 2d.

    y : {array-like, sparse matrix} of shape (n_samples,) or (n_samples, n_outputs), \

            ``cross_val_predict(..., params={'groups': groups})``.

        Number of jobs to run in parallel. Training the estimator and
        predicting are parallelized over the cross-validation splits.

        Parameters to pass to the underlying estimator's ``fit`` and the CV
        splitter.

    method : {'predict', 'predict_proba', 'predict_log_proba', \
              'decision_function'}, default='predict'
        The method to be invoked by `estimator`.

    Returns
    -------
    predictions : ndarray
        This is the result of calling `method`. Shape:

        - When `method` is 'predict' and in special case where `method` is
          'decision_function' and the target is binary: (n_samples,)
        - When `method` is one of {'predict_proba', 'predict_log_proba',
          'decision_function'} (unless special case above):
          (n_samples, n_classes)
        - If `estimator` is :term:`multioutput`, an extra dimension
          'n_outputs' is added to the end of each shape above.

    See Also
    --------
    cross_val_score : Calculate score for each CV split.
    cross_validate : Calculate one or more scores and timings for each CV
        split.

    Notes
    -----
    In the case that one or more classes are absent in a training portion, a
    default score needs to be assigned to all instances for that class if
    ``method`` produces columns per class, as in {'decision_function',
    'predict_proba', 'predict_log_proba'}.  For ``predict_proba`` this value is
    0.  In order to ensure finite output, we approximate negative infinity by
    the minimum finite float value for the dtype in other cases.

    Examples
    --------
    >>> from sklearn.model_selection import cross_val_predict
    >>> y_pred = cross_val_predict(lasso, X, y, cv=3)
    """

        router = (
            MetadataRouter(owner="cross_val_predict")
            .add(
            )
            .add(
                # TODO(SLEP6): also pass metadata for the predict method.
            )
        )
        try:
                message=str(e).replace("cross_val_predict.fit", "cross_val_predict"),
            )
    else:

    splits = list(cv.split(X, y, **routed_params.splitter.split))

    test_indices = np.concatenate([test for _, test in splits])
    if not _check_is_permutation(test_indices, _num_samples(X)):
        raise ValueError("cross_val_predict only works for partitions")

    # If classification methods produce multiple columns of output,
    # we need to manually encode classes to ensure consistent column ordering.
    encode = (
        method in ["decision_function", "predict_proba", "predict_log_proba"]
        and y is not None
    )
    if encode:
            y = le.fit_transform(y)
        elif y.ndim == 2:
            y_enc = np.zeros_like(y, dtype=int)
            for i_label in range(y.shape[1]):
                y_enc[:, i_label] = LabelEncoder().fit_transform(y[:, i_label])
            y = y_enc

    predictions = parallel(
        delayed(_fit_and_predict)(
            X,
            y,
            train,
            test,
            routed_params.estimator.fit,
            method,
        )
        for train, test in splits
    )

    inv_test_indices = np.empty(len(test_indices), dtype=int)
    inv_test_indices[test_indices] = np.arange(len(test_indices))

    if sp.issparse(predictions[0]):
        predictions = sp.vstack(predictions, format=predictions[0].format)
    elif encode and isinstance(predictions[0], list):
        # `predictions` is a list of method outputs from each fold.
        # If each of those is also a list, then treat this as a
        # multioutput-multiclass task. We need to separately concatenate
        # the method outputs for each label into an `n_labels` long list.
        n_labels = y.shape[1]
        concat_pred = []
        for i_label in range(n_labels):
            label_preds = np.concatenate([p[i_label] for p in predictions])
            concat_pred.append(label_preds)
        predictions = concat_pred
    else:
        predictions = np.concatenate(predictions)

    if isinstance(predictions, list):
        return [p[inv_test_indices] for p in predictions]
    else:
        return predictions[inv_test_indices]

def _fit_and_predict(estimator, X, y, train, test, fit_params, method):
    """Fit estimator and predict values for a given dataset split.

    estimator : estimator object implementing 'fit' and 'predict'

            X is only required to be an object with finite length or shape now

        Invokes the passed method name of the passed estimator.

    Returns
    -------
    predictions : sequence
        Result of calling 'estimator.method'
    """

    X_test, _ = _safe_split(estimator, X, y, test, train)

    if y_train is None:
        estimator.fit(X_train, **fit_params)
    else:
        estimator.fit(X_train, y_train, **fit_params)
    func = getattr(estimator, method)
    predictions = func(X_test)

    encode = (
    )

    if encode:
        if isinstance(predictions, list):
                _enforce_prediction_order(
                    estimator.classes_[i_label],
                    predictions[i_label],
                    n_classes=len(set(y[:, i_label])),
                )
                for i_label in range(len(predictions))
            ]
        else:
            # A 2D y array should be a binary label indicator matrix
            n_classes = len(set(y)) if y.ndim == 1 else y.shape[1]
            predictions = _enforce_prediction_order(
                estimator.classes_, predictions, n_classes, method
            )
    return predictions

def _enforce_prediction_order(classes, predictions, n_classes, method):
    """Ensure that prediction arrays have correct column order

    When doing cross-validation, if one or more classes are
    not present in the subset of data used for training,
    then the output prediction array might not have the same
    columns as other folds. Use the list of class names
    (assumed to be ints) to enforce the correct column order.

    Note that `classes` is the list of classes in this fold
    (a subset of the classes in the full training set)
    and `n_classes` is the number of classes in the full training set.
    """
    if n_classes != len(classes):
        recommendation = (
            "To fix this, use a cross-validation "
            "technique resulting in properly "
            "stratified folds"
        )
            "Number of classes in training fold ({}) does "
            "not match total number of classes ({}). "
            "Results may not be appropriate for your use case. "
            "{}".format(len(classes), n_classes, recommendation),
        )
        if method == "decision_function":
            if predictions.ndim == 2 and predictions.shape[1] != len(classes):
                # This handles the case when the shape of predictions
                # does not match the number of classes used to train
                # it with. This case is found when sklearn.svm.SVC is
                # set to `decision_function_shape='ovo'`.
                    "Output shape {} of {} does not match "
                    "number of classes ({}) in fold. "
                    "Irregular decision_function outputs "
                    "are not currently supported by "
                    "cross_val_predict".format(predictions.shape, method, len(classes))
                )
            if len(classes) <= 2:
                # In this special case, `predictions` contains a 1D array.
                    "Only {} class/es in training fold, but {} "
                    "in overall dataset. This "
                    "is not supported for decision_function "
                    "with imbalanced folds. {}".format(
                        len(classes), n_classes, recommendation
                    )
                )

        float_min = np.finfo(predictions.dtype).min
        default_values = {
            "decision_function": float_min,
            "predict_log_proba": float_min,
            "predict_proba": 0,
        }
        predictions_for_all_classes = np.full(
            (_num_samples(predictions), n_classes),
            default_values[method],
            dtype=predictions.dtype,
        )
        predictions_for_all_classes[:, classes] = predictions
        predictions = predictions_for_all_classes

def _check_is_permutation(indices, n_samples):
    """Check whether indices is a reordering of the array np.arange(n_samples)

    indices : ndarray
        int array to test
        number of expected elements

    Returns
    -------
    is_partition : bool
        True iff sorted(indices) is np.arange(n)
    """
    if len(indices) != n_samples:
    hit = np.zeros(n_samples, dtype=bool)
    hit[indices] = True
    if not np.all(hit):

def permutation_test_score(
    X,
    y,
    *,
    cv=None,
    n_permutations=100,
):
    """Evaluate the significance of a cross-validated score with permutations.

    Permutes targets to generate 'randomized data' and compute the empirical
    p-value against the null hypothesis that features and targets are
    independent.

    The p-value represents the fraction of randomized data sets where the
    estimator performed as well or better than on the original data. A small
    p-value suggests that there is a real dependency between features and
    targets which has been used by the estimator to give good predictions.
    A large p-value may be due to lack of real dependency between features
    and targets or the estimator was not able to use the dependency to
    give good predictions.

    Read more in the :ref:`User Guide <permutation_test_score>`.

    X : array-like of shape at least 2D

        Labels to constrain permutation within groups, i.e. ``y`` values
        are permuted among samples with the same group identifier.
        When not specified, ``y`` values are permuted among all samples.

        When a grouped cross-validator is used, the group labels are
        also passed on to the ``split`` method of the cross-validator. The
        cross-validator uses them for grouping the samples  while splitting
        the dataset into train/test set.

            ``permutation_test_score(..., params={'groups': groups})``.

    n_permutations : int, default=100
        Number of times to permute ``y``.

        the cross-validated score are parallelized over the permutations.

        Pass an int for reproducible output for permutation of
        ``y`` values among samples. See :term:`Glossary <random_state>`.

        Scoring method to use to evaluate the predictions on the validation set.

        Parameters to pass to the fit method of the estimator.

            This parameter is deprecated and will be removed in version 1.6. Use
            ``params`` instead.

        Parameters to pass to the `fit` method of the estimator, the scorer
        and the cv splitter.

        - If `enable_metadata_routing=False` (default): Parameters directly passed to
          the `fit` method of the estimator.

        - If `enable_metadata_routing=True`: Parameters safely routed to the `fit`
          method of the estimator, `cv` object and `scorer`. See :ref:`Metadata Routing
          User Guide <metadata_routing>` for more details.

    Returns
    -------
        The true score without permuting targets.

    permutation_scores : array of shape (n_permutations,)
        The scores obtained for each permutations.

    pvalue : float
        The p-value, which approximates the probability that the score would
        be obtained by chance. This is calculated as:

        `(C + 1) / (n_permutations + 1)`

        Where C is the number of permutations whose score >= the true score.

        The best possible p-value is 1/(n_permutations + 1), the worst is 1.0.

    Notes
    -----
    This function implements Test 1 in:

    Ojala and Garriga. `Permutation Tests for Studying Classifier Performance
    <http://www.jmlr.org/papers/volume11/ojala10a/ojala10a.pdf>`_. The
    Journal of Machine Learning Research (2010) vol. 11

    Examples
    --------
    >>> from sklearn.model_selection import permutation_test_score
    >>> estimator = LogisticRegression()
    >>> score, permutation_scores, pvalue = permutation_test_score(
    ...     estimator, X, y, random_state=0
    ... )
    >>> print(f"Original Score: {score:.3f}")
    Original Score: 0.810
    >>> print(
    ...     f"Permutation Scores: {permutation_scores.mean():.3f} +/- "
    ...     f"{permutation_scores.std():.3f}"
    ... )
    Permutation Scores: 0.505 +/- 0.057
    >>> print(f"P-value: {pvalue:.3f}")
    P-value: 0.010
    """
    params = _check_params_groups_deprecation(fit_params, params, groups, "1.8")

    X, y, groups = indexable(X, y, groups)

        router = (
            MetadataRouter(owner="permutation_test_score")
            .add(
                # scoring?
            )
            .add(
            )
            .add(
                scorer=scorer,
            )
        )

        try:
                message=str(e).replace(
                    "permutation_test_score.fit", "permutation_test_score"
                ),
            )

    else:

    score = _permutation_test_score(
        clone(estimator),
        X,
        y,
        cv,
        scorer,
        split_params=routed_params.splitter.split,
        fit_params=routed_params.estimator.fit,
        score_params=routed_params.scorer.score,
    )
    permutation_scores = Parallel(n_jobs=n_jobs, verbose=verbose)(
        delayed(_permutation_test_score)(
            X,
            _shuffle(y, groups, random_state),
            cv,
            scorer,
            split_params=routed_params.splitter.split,
        )
        for _ in range(n_permutations)
    )
    permutation_scores = np.array(permutation_scores)
    pvalue = (np.sum(permutation_scores >= score) + 1.0) / (n_permutations + 1)
    return score, permutation_scores, pvalue

def _permutation_test_score(
    estimator, X, y, cv, scorer, split_params, fit_params, score_params
):
    """Auxiliary function for permutation_test_score"""

    avg_score = []
    for train, test in cv.split(X, y, **split_params):
        X_train, y_train = _safe_split(estimator, X, y, train)
        X_test, y_test = _safe_split(estimator, X, y, test, train)
        fit_params_train = _check_method_params(X, params=fit_params, indices=train)
        score_params_test = _check_method_params(X, params=score_params, indices=test)
        estimator.fit(X_train, y_train, **fit_params_train)
        avg_score.append(scorer(estimator, X_test, y_test, **score_params_test))
    return np.mean(avg_score)

def _shuffle(y, groups, random_state):
    """Return a shuffled copy of y eventually shuffle among same groups."""
    if groups is None:
        indices = random_state.permutation(len(y))
    else:
        indices = np.arange(len(groups))
        for group in np.unique(groups):
            this_mask = groups == group
            indices[this_mask] = random_state.permutation(indices[this_mask])
    return _safe_indexing(y, indices)

def learning_curve(
    X,
    y,
    *,
    train_sizes=np.linspace(0.1, 1.0, 5),
    cv=None,
    exploit_incremental_learning=False,
    pre_dispatch="all",
):
    """Learning curve.

    Determines cross-validated training and test scores for different training
    set sizes.

    A cross-validation generator splits the whole dataset k times in training
    and test data. Subsets of the training set with varying sizes will be used
    to train the estimator and a score for each training subset size and the
    test set will be computed. Afterwards, the scores will be averaged over
    all k runs for each training subset size.

    Read more in the :ref:`User Guide <learning_curve>`.

    estimator : object type that implements the "fit" method
        An object of that type which is cloned for each validation. It must
        also implement "predict" unless `scoring` is a callable that doesn't
        rely on "predict" to compute a score.

        Target relative to X for classification or regression;
        None for unsupervised learning.

            ``learning_curve(..., params={'groups': groups})``.

    train_sizes : array-like of shape (n_ticks,), \
            default=np.linspace(0.1, 1.0, 5)
        Relative or absolute numbers of training examples that will be used to
        generate the learning curve. If the dtype is float, it is regarded as a
        fraction of the maximum size of the training set (that is determined
        by the selected validation method), i.e. it has to be within (0, 1].
        Otherwise it is interpreted as absolute sizes of the training sets.
        Note that for classification the number of samples usually has to
        be big enough to contain at least one sample from each class.

        Scoring method to use to evaluate the training and test sets.

    exploit_incremental_learning : bool, default=False
        If the estimator supports incremental learning, this will be
        used to speed up fitting for different training set sizes.

        the score are parallelized over the different training and test sets.

    pre_dispatch : int or str, default='all'
        Number of predispatched jobs for parallel execution (default is
        all). The option can reduce the allocated memory. The str can
        be an expression like '2*n_jobs'.

        Whether to shuffle training data before taking prefixes of it
        based on``train_sizes``.

        Used when ``shuffle`` is True. Pass an int for reproducible
        output across multiple function calls.

        Whether to return the fit and score times.

            This parameter is deprecated and will be removed in version 1.8. Use

        Parameters to pass to the `fit` method of the estimator and to the scorer.

          method of the estimator. See :ref:`Metadata Routing User Guide
          <metadata_routing>` for more details.

    Returns
    -------
    train_sizes_abs : array of shape (n_unique_ticks,)
        Numbers of training examples that has been used to generate the
        learning curve. Note that the number of ticks might be less
        than n_ticks because duplicate entries will be removed.

    train_scores : array of shape (n_ticks, n_cv_folds)
        Scores on training sets.

    test_scores : array of shape (n_ticks, n_cv_folds)
        Scores on test set.

    fit_times : array of shape (n_ticks, n_cv_folds)
        Times spent for fitting in seconds. Only present if ``return_times``
        is True.

    score_times : array of shape (n_ticks, n_cv_folds)
        Times spent for scoring in seconds. Only present if ``return_times``
        is True.

    Examples
    --------
    >>> from sklearn.model_selection import learning_curve
    >>> X, y = make_classification(n_samples=100, n_features=10, random_state=42)
    >>> tree = DecisionTreeClassifier(max_depth=4, random_state=42)
    >>> train_size_abs, train_scores, test_scores = learning_curve(
    ...     tree, X, y, train_sizes=[0.3, 0.6, 0.9]
    ... )
    >>> for train_size, cv_train_scores, cv_test_scores in zip(
    ...     train_size_abs, train_scores, test_scores
    ... ):
    ...     print(f"{train_size} samples were used to train the model")
    ...     print(f"The average train accuracy is {cv_train_scores.mean():.2f}")
    ...     print(f"The average test accuracy is {cv_test_scores.mean():.2f}")
    24 samples were used to train the model
    The average train accuracy is 1.00
    The average test accuracy is 0.85
    48 samples were used to train the model
    The average test accuracy is 0.90
    72 samples were used to train the model
    The average test accuracy is 0.93
    """
    if exploit_incremental_learning and not hasattr(estimator, "partial_fit"):
            "An estimator must support the partial_fit interface "
            "to exploit incremental learning"
        )

        router = (
            MetadataRouter(owner="learning_curve")
            .add(
                # scoring?
                .add(caller="fit", callee="partial_fit"),
            )
            .add(
            )
            .add(
            )
        )

        try:
                message=str(e).replace("learning_curve.fit", "learning_curve"),
            )

    else:
        routed_params.estimator = Bunch(fit=params, partial_fit=params)

    # Store cv as list as we will be iterating over the list multiple times
    cv_iter = list(cv.split(X, y, **routed_params.splitter.split))

    n_max_training_samples = len(cv_iter[0][0])
    # Because the lengths of folds can be significantly different, it is
    # not guaranteed that we use all of the available training data when we
    # use the first 'n_max_training_samples' samples.
    train_sizes_abs = _translate_train_sizes(train_sizes, n_max_training_samples)
    n_unique_ticks = train_sizes_abs.shape[0]
    if verbose > 0:
        print("[learning_curve] Training set sizes: " + str(train_sizes_abs))

    parallel = Parallel(n_jobs=n_jobs, pre_dispatch=pre_dispatch, verbose=verbose)

        cv_iter = ((rng.permutation(train), test) for train, test in cv_iter)

    if exploit_incremental_learning:
        classes = np.unique(y) if is_classifier(estimator) else None
        out = parallel(
            delayed(_incremental_fit_estimator)(
                clone(estimator),
                X,
                y,
                classes,
                train,
                test,
                train_sizes_abs,
                scorer,
                return_times,
                error_score=error_score,
                fit_params=routed_params.estimator.partial_fit,
            )
            for train, test in cv_iter
        )
        out = np.asarray(out).transpose((2, 1, 0))
    else:
        train_test_proportions = []
        for train, test in cv_iter:
            for n_train_samples in train_sizes_abs:
                train_test_proportions.append((train[:n_train_samples], test))

            delayed(_fit_and_score)(
                X,
                y,
                train=train,
                test=test,
                parameters=None,
                return_train_score=True,
                return_times=return_times,
            )
            for train, test in train_test_proportions
        )
        _warn_or_raise_about_fit_failures(results, error_score)
        results = _aggregate_score_dicts(results)
        train_scores = results["train_scores"].reshape(-1, n_unique_ticks).T
        test_scores = results["test_scores"].reshape(-1, n_unique_ticks).T
        out = [train_scores, test_scores]

        if return_times:
            fit_times = results["fit_time"].reshape(-1, n_unique_ticks).T
            score_times = results["score_time"].reshape(-1, n_unique_ticks).T
            out.extend([fit_times, score_times])

    ret = train_sizes_abs, out[0], out[1]

        ret = ret + (out[2], out[3])

    return ret

def _translate_train_sizes(train_sizes, n_max_training_samples):
    """Determine absolute sizes of training subsets and validate 'train_sizes'.

    Examples:
        _translate_train_sizes([0.5, 1.0], 10) -> [5, 10]
        _translate_train_sizes([5, 10], 10) -> [5, 10]

    train_sizes : array-like of shape (n_ticks,)
        Numbers of training examples that will be used to generate the
        learning curve. If the dtype is float, it is regarded as a
        fraction of 'n_max_training_samples', i.e. it has to be within (0, 1].

    n_max_training_samples : int
        Maximum number of training samples (upper bound of 'train_sizes').

    Returns
    -------
    """
    train_sizes_abs = np.asarray(train_sizes)
    n_ticks = train_sizes_abs.shape[0]
    n_min_required_samples = np.min(train_sizes_abs)
    n_max_required_samples = np.max(train_sizes_abs)
    if np.issubdtype(train_sizes_abs.dtype, np.floating):
        if n_min_required_samples <= 0.0 or n_max_required_samples > 1.0:
                "train_sizes has been interpreted as fractions "
                "of the maximum number of training samples and "
                "must be within (0, 1], but is within [%f, %f]."
                % (n_min_required_samples, n_max_required_samples)
            )
        train_sizes_abs = (train_sizes_abs * n_max_training_samples).astype(
            dtype=int, copy=False
        )
        train_sizes_abs = np.clip(train_sizes_abs, 1, n_max_training_samples)
    else:
        if (
            n_min_required_samples <= 0
            or n_max_required_samples > n_max_training_samples
        ):
                "train_sizes has been interpreted as absolute "
                "numbers of training samples and must be within "
                "(0, %d], but is within [%d, %d]."
                % (
                    n_max_training_samples,
                    n_min_required_samples,
                    n_max_required_samples,
                )
            )

    train_sizes_abs = np.unique(train_sizes_abs)
    if n_ticks > train_sizes_abs.shape[0]:
            "Removed duplicate entries from 'train_sizes'. Number "
            "of ticks will be less than the size of "
            "'train_sizes': %d instead of %d." % (train_sizes_abs.shape[0], n_ticks),
        )

    return train_sizes_abs

def _incremental_fit_estimator(
    X,
    y,
    classes,
    train,
    test,
    train_sizes,
    scorer,
    return_times,
    error_score,
):
    """Train estimator on training subsets incrementally and compute scores."""
    train_scores, test_scores, fit_times, score_times = [], [], [], []
    partitions = zip(train_sizes, np.split(train, train_sizes)[:-1])
    if fit_params is None:
        fit_params = {}
    if classes is None:
        partial_fit_func = partial(estimator.partial_fit, **fit_params)
    else:
        partial_fit_func = partial(estimator.partial_fit, classes=classes, **fit_params)

    for n_train_samples, partial_train in partitions:
        train_subset = train[:n_train_samples]
        X_train, y_train = _safe_split(estimator, X, y, train_subset)
        X_partial_train, y_partial_train = _safe_split(estimator, X, y, partial_train)
        X_test, y_test = _safe_split(estimator, X, y, test, train_subset)
        start_fit = time.time()
        if y_partial_train is None:
            partial_fit_func(X_partial_train)
        else:
            partial_fit_func(X_partial_train, y_partial_train)
        fit_time = time.time() - start_fit
        fit_times.append(fit_time)

        start_score = time.time()

        test_scores.append(
            _score(
                X_test,
                y_test,
                scorer,
                score_params=score_params_test,
            )
        )
        train_scores.append(
            _score(
                X_train,
                y_train,
                scorer,
                score_params=score_params_train,
            )
        )
        score_time = time.time() - start_score
        score_times.append(score_time)

    ret = (
        (train_scores, test_scores, fit_times, score_times)
        if return_times
        else (train_scores, test_scores)
    )

    return np.array(ret).T

def validation_curve(
    X,
    y,
    *,
    param_name,
    param_range,
    cv=None,
):
    """Validation curve.

    Determine training and test scores for varying parameter values.

    Compute scores for an estimator with different values of a specified
    parameter. This is similar to grid search with one parameter. However, this
    will also compute training scores and is merely a utility for plotting the
    results.

    Read more in the :ref:`User Guide <validation_curve>`.

        Name of the parameter that will be varied.

    param_range : array-like of shape (n_values,)
        The values of the parameter that will be evaluated.

            ``validation_curve(..., params={'groups': groups})``.

        the score are parallelized over the combinations of each parameter
        value and each cross-validation split.

        Parameters to pass to the estimator, scorer and cross-validation object.

          method of the estimator, to the scorer and to the cross-validation object.
          See :ref:`Metadata Routing User Guide <metadata_routing>` for more details.

    Returns
    -------

    Notes
    -----
    See :ref:`sphx_glr_auto_examples_model_selection_plot_train_error_vs_test_error.py`

    Examples
    --------
    >>> from sklearn.model_selection import validation_curve
    >>> X, y = make_classification(n_samples=1_000, random_state=0)
    >>> logistic_regression = LogisticRegression()
    >>> param_name, param_range = "C", np.logspace(-8, 3, 10)
    >>> train_scores, test_scores = validation_curve(
    ...     logistic_regression, X, y, param_name=param_name, param_range=param_range
    ... )
    >>> print(f"The average train accuracy is {train_scores.mean():.2f}")
    The average train accuracy is 0.81
    >>> print(f"The average test accuracy is {test_scores.mean():.2f}")
    The average test accuracy is 0.81
    """

        router = (
            MetadataRouter(owner="validation_curve")
            .add(
            )
            .add(
            )
            .add(
            )
        )

        try:
                message=str(e).replace("validation_curve.fit", "validation_curve"),
            )

    else:

            X,
            y,
            parameters={param_name: v},
            return_train_score=True,
        )
        # NOTE do not change order of iteration to allow one time cv splitters
        for train, test in cv.split(X, y, **routed_params.splitter.split)
        for v in param_range
    )
    n_params = len(param_range)

    train_scores = results["train_scores"].reshape(-1, n_params).T
    test_scores = results["test_scores"].reshape(-1, n_params).T

    return train_scores, test_scores

def _aggregate_score_dicts(scores):
    """Aggregate the list of dict to dict of np ndarray

    The aggregated output of _aggregate_score_dicts will be a list of dict
    of form [{'prec': 0.1, 'acc':1.0}, {'prec': 0.1, 'acc':1.0}, ...]
    Convert it to a dict of array {'prec': np.array([0.1 ...]), ...}

    scores : list of dict
        List of dicts of the scores for all scorers. This is a flat list,
        assumed originally to be of row major order.

    Example
    -------

    >>> scores = [{'a': 1, 'b':10}, {'a': 2, 'b':2}, {'a': 3, 'b':3},
    ...           {'a': 10, 'b': 10}]                         # doctest: +SKIP
    >>> _aggregate_score_dicts(scores)                        # doctest: +SKIP
    {'a': array([1, 2, 3, 10]),
     'b': array([10, 2, 3, 10])}
    """
    return {
        key: (
            np.asarray([score[key] for score in scores])
            if isinstance(scores[0][key], numbers.Number)
            else [score[key] for score in scores]
        )
        for key in scores[0]
    }

class _BaseCurveDisplay:
    def _plot_curve(
        self,
        x_data,
        *,
        ax=None,
        negate_score=False,
        score_name=None,
        score_type="test",
        std_display_style="fill_between",
        fill_between_kw=None,
        errorbar_kw=None,
    ):

            train_scores, test_scores = -self.train_scores, -self.test_scores
        else:
            train_scores, test_scores = self.train_scores, self.test_scores

        if std_display_style not in ("errorbar", "fill_between", None):
                f"Unknown std_display_style: {std_display_style}. Should be one of"
                " 'errorbar', 'fill_between', or None."
            )

        if score_type not in ("test", "train", "both"):
                f"Unknown score_type: {score_type}. Should be one of 'test', "
                "'train', or 'both'."
            )

        if score_type == "train":
            scores = {"Train": train_scores}
        elif score_type == "test":
            scores = {"Test": test_scores}
        else:  # score_type == "both"
            scores = {"Train": train_scores, "Test": test_scores}

        if std_display_style in ("fill_between", None):
            # plot the mean score
            if line_kw is None:
                line_kw = {}

            self.lines_ = []
            for line_label, score in scores.items():
                self.lines_.append(
                    *ax.plot(
                        x_data,
                        score.mean(axis=1),
                        label=line_label,
                        **line_kw,
                    )
                )
            self.errorbar_ = None
            self.fill_between_ = None  # overwritten below by fill_between

        if std_display_style == "errorbar":
            if errorbar_kw is None:
                errorbar_kw = {}

            self.errorbar_ = []
                self.errorbar_.append(
                    ax.errorbar(
                        x_data,
                        score.std(axis=1),
                        **errorbar_kw,
                    )
                )
            self.lines_, self.fill_between_ = None, None
        elif std_display_style == "fill_between":
            if fill_between_kw is None:
                fill_between_kw = {}
            default_fill_between_kw = {"alpha": 0.5}
            fill_between_kw = {**default_fill_between_kw, **fill_between_kw}

            self.fill_between_ = []
                self.fill_between_.append(
                    ax.fill_between(
                        x_data,
                        score.mean(axis=1) - score.std(axis=1),
                        score.mean(axis=1) + score.std(axis=1),
                        **fill_between_kw,
                    )
                )

        score_name = self.score_name if score_name is None else score_name

        ax.legend()

        # We found that a ratio, smaller or bigger than 5, between the largest and
        # smallest gap of the x values is a good indicator to choose between linear
        # and log scale.
        if _interval_max_min_ratio(x_data) > 5:
            xscale = "symlog" if x_data.min() <= 0 else "log"
        else:
            xscale = "linear"

        ax.set_xscale(xscale)
        ax.set_ylabel(f"{score_name}")

class LearningCurveDisplay(_BaseCurveDisplay):
    """Learning Curve visualization.

    :meth:`~sklearn.model_selection.LearningCurveDisplay.from_estimator` to
    create a :class:`~sklearn.model_selection.LearningCurveDisplay` instance.
    All parameters are stored as attributes.

    Read more in the :ref:`User Guide <visualizations>` for general information
    about the visualization API and
    :ref:`detailed documentation <learning_curve>` regarding the learning
    curve visualization.

    train_sizes : ndarray of shape (n_unique_ticks,)
        learning curve.

    train_scores : ndarray of shape (n_ticks, n_cv_folds)

    test_scores : ndarray of shape (n_ticks, n_cv_folds)

    score_name : str, default=None
        The name of the score used in `learning_curve`. It will override the name
        inferred from the `scoring` parameter. If `score` is `None`, we use `"Score"` if
        `negate_score` is `False` and `"Negative score"` otherwise. If `scoring` is a
        string or a callable, we infer the name. We replace `_` by spaces and capitalize
        the first letter. We remove `neg_` and replace it by `"Negative"` if
        `negate_score` is `False` or just remove it otherwise.

        Axes with the learning curve.

        Figure containing the learning curve.

    errorbar_ : list of matplotlib Artist or None
        When the `std_display_style` is `"errorbar"`, this is a list of
        `matplotlib.container.ErrorbarContainer` objects. If another style is
        used, `errorbar_` is `None`.

    lines_ : list of matplotlib Artist or None
        When the `std_display_style` is `"fill_between"`, this is a list of
        `matplotlib.lines.Line2D` objects corresponding to the mean train and
        test scores. If another style is used, `line_` is `None`.

    fill_between_ : list of matplotlib Artist or None
        `matplotlib.collections.PolyCollection` objects. If another style is
        used, `fill_between_` is `None`.

    See Also
    --------
    sklearn.model_selection.learning_curve : Compute the learning curve.

    Examples
    --------
    >>> from sklearn.model_selection import LearningCurveDisplay, learning_curve
    >>> tree = DecisionTreeClassifier(random_state=0)
    >>> train_sizes, train_scores, test_scores = learning_curve(
    ...     tree, X, y)
    >>> display = LearningCurveDisplay(train_sizes=train_sizes,
    ...     train_scores=train_scores, test_scores=test_scores, score_name="Score")
    <...>
    """

    def __init__(self, *, train_sizes, train_scores, test_scores, score_name=None):
        self.train_sizes = train_sizes
        self.train_scores = train_scores
        self.test_scores = test_scores
        self.score_name = score_name

    def plot(
        self,
        ax=None,
        *,
        score_type="both",
    ):

            created.

        negate_score : bool, default=False
            Whether or not to negate the scores obtained through
            :func:`~sklearn.model_selection.learning_curve`. This is
            particularly useful when using the error denoted by `neg_*` in
            `scikit-learn`.

        score_name : str, default=None
            The name of the score used to decorate the y-axis of the plot. It will
            override the name inferred from the `scoring` parameter. If `score` is
            `None`, we use `"Score"` if `negate_score` is `False` and `"Negative score"`
            otherwise. If `scoring` is a string or a callable, we infer the name. We
            replace `_` by spaces and capitalize the first letter. We remove `neg_` and
            replace it by `"Negative"` if `negate_score` is
            `False` or just remove it otherwise.

        score_type : {"test", "train", "both"}, default="both"
            The type of score to plot. Can be one of `"test"`, `"train"`, or
            `"both"`.

        std_display_style : {"errorbar", "fill_between"} or None, default="fill_between"
            The style used to display the score standard deviation around the
            mean score. If None, no standard deviation representation is
            displayed.

            Additional keyword arguments passed to the `plt.plot` used to draw
            the mean score.

        fill_between_kw : dict, default=None
            Additional keyword arguments passed to the `plt.fill_between` used
            to draw the score standard deviation.

        errorbar_kw : dict, default=None
            Additional keyword arguments passed to the `plt.errorbar` used to
            draw mean score and standard deviation score.

        Returns
        -------
        display : :class:`~sklearn.model_selection.LearningCurveDisplay`
        """
        self._plot_curve(
            self.train_sizes,
            ax=ax,
            negate_score=negate_score,
            score_name=score_name,
            score_type=score_type,
            std_display_style=std_display_style,
            fill_between_kw=fill_between_kw,
            errorbar_kw=errorbar_kw,
        )
        self.ax_.set_xlabel("Number of samples in the training set")

        cls,
        X,
        y,
        *,
        groups=None,
        train_sizes=np.linspace(0.1, 1.0, 5),
        cv=None,
        exploit_incremental_learning=False,
        pre_dispatch="all",
        fit_params=None,
        ax=None,
    ):
        """Create a learning curve display from an estimator.

        Read more in the :ref:`User Guide <visualizations>` for general
        information about the visualization API and :ref:`detailed
        documentation <learning_curve>` regarding the learning curve
        visualization.

        estimator : object type that implements the "fit" and "predict" methods
            An object of that type which is cloned for each validation.

        y : array-like of shape (n_samples,) or (n_samples, n_outputs) or None

        groups : array-like of shape (n_samples,), default=None
            instance (e.g., :class:`GroupKFold`).

        train_sizes : array-like of shape (n_ticks,), \
                default=np.linspace(0.1, 1.0, 5)
            Relative or absolute numbers of training examples that will be used
            to generate the learning curve. If the dtype is float, it is
            regarded as a fraction of the maximum size of the training set
            (that is determined by the selected validation method), i.e. it has
            to be within (0, 1]. Otherwise it is interpreted as absolute sizes
            of the training sets. Note that for classification the number of
            samples usually have to be big enough to contain at least one
            sample from each class.

        cv : int, cross-validation generator or an iterable, default=None
            Determines the cross-validation splitting strategy.
            Possible inputs for cv are:

            - None, to use the default 5-fold cross validation,
            - int, to specify the number of folds in a `(Stratified)KFold`,
            - :term:`CV splitter`,
            - An iterable yielding (train, test) splits as arrays of indices.

            For int/None inputs, if the estimator is a classifier and `y` is
            either binary or multiclass,
            :class:`~sklearn.model_selection.StratifiedKFold` is used. In all
            other cases, :class:`~sklearn.model_selection.KFold` is used. These
            splitters are instantiated with `shuffle=False` so the splits will
            be the same across calls.

            Refer :ref:`User Guide <cross_validation>` for the various
            cross-validation strategies that can be used here.

        scoring : str or callable, default=None
            The scoring method to use when calculating the learning curve. Options:

            - str: see :ref:`scoring_string_names` for options.
            - callable: a scorer callable object (e.g., function) with signature
              ``scorer(estimator, X, y)``. See :ref:`scoring_callable` for details.
            - `None`: the `estimator`'s
              :ref:`default evaluation criterion <scoring_api_overview>` is used.

        exploit_incremental_learning : bool, default=False
            If the estimator supports incremental learning, this will be
            used to speed up fitting for different training set sizes.

            Number of jobs to run in parallel. Training the estimator and
            computing the score are parallelized over the different training
            and test sets. `None` means 1 unless in a
            :obj:`joblib.parallel_backend` context. `-1` means using all
            processors. See :term:`Glossary <n_jobs>` for more details.

        pre_dispatch : int or str, default='all'
            Number of predispatched jobs for parallel execution (default is
            all). The option can reduce the allocated memory. The str can
            be an expression like '2*n_jobs'.

            Controls the verbosity: the higher, the more messages.

        shuffle : bool, default=False
            Whether to shuffle training data before taking prefixes of it
            based on`train_sizes`.

            Used when `shuffle` is True. Pass an int for reproducible
            output across multiple function calls.

        error_score : 'raise' or numeric, default=np.nan
            Value to assign to the score if an error occurs in estimator
            fitting. If set to 'raise', the error is raised. If a numeric value
            is given, FitFailedWarning is raised.

        fit_params : dict, default=None
            Parameters to pass to the fit method of the estimator.

            created.

            `"both"`.

            mean score. If `None`, no representation of the standard deviation
            is displayed.

        Returns
        -------

        Examples
        --------
        >>> from sklearn.model_selection import LearningCurveDisplay
        >>> from sklearn.tree import DecisionTreeClassifier
        >>> X, y = load_iris(return_X_y=True)
        >>> tree = DecisionTreeClassifier(random_state=0)
        >>> LearningCurveDisplay.from_estimator(tree, X, y)
        <...>
        """

        score_name = _validate_score_name(score_name, scoring, negate_score)

        train_sizes, train_scores, test_scores = learning_curve(
            X,
            y,
            groups=groups,
            train_sizes=train_sizes,
            cv=cv,
            exploit_incremental_learning=exploit_incremental_learning,
            return_times=False,
            fit_params=fit_params,
        )

        viz = cls(
            train_scores=train_scores,
            test_scores=test_scores,
        )
            ax=ax,
        )

class ValidationCurveDisplay(_BaseCurveDisplay):
    """Validation Curve visualization.

    :meth:`~sklearn.model_selection.ValidationCurveDisplay.from_estimator` to
    create a :class:`~sklearn.model_selection.ValidationCurveDisplay` instance.

    about the visualization API and :ref:`detailed documentation
    <validation_curve>` regarding the validation curve visualization.

        Name of the parameter that has been varied.

    param_range : array-like of shape (n_ticks,)
        The values of the parameter that have been evaluated.

        The name of the score used in `validation_curve`. It will override the name

        Axes with the validation curve.

        Figure containing the validation curve.

    See Also
    --------
    sklearn.model_selection.validation_curve : Compute the validation curve.

    Examples
    --------
    >>> from sklearn.model_selection import ValidationCurveDisplay, validation_curve
    ... )
    >>> display = ValidationCurveDisplay(
    ...     param_name=param_name, param_range=param_range,
    ...     train_scores=train_scores, test_scores=test_scores, score_name="Score"
    ... )
    <...>
    """

        self, *, param_name, param_range, train_scores, test_scores, score_name=None
    ):
        self.param_name = param_name
        self.param_range = param_range

    def plot(
        self,
        ax=None,
        *,
    ):

            created.

            :func:`~sklearn.model_selection.validation_curve`. This is

            `"both"`.

        Returns
        -------
        display : :class:`~sklearn.model_selection.ValidationCurveDisplay`
        """
            self.param_range,
            ax=ax,
        )
        self.ax_.set_xlabel(f"{self.param_name}")

        cls,
        X,
        y,
        *,
        param_name,
        param_range,
        cv=None,
        ax=None,
    ):
        """Create a validation curve display from an estimator.

        documentation <validation_curve>` regarding the validation curve

        param_name : str
            Name of the parameter that will be varied.

        param_range : array-like of shape (n_values,)
            The values of the parameter that will be evaluated.

            Scoring method to use when computing the validation curve. Options:

            created.

            `"both"`.

        Returns
        -------

        Examples
        --------
        >>> from sklearn.model_selection import ValidationCurveDisplay
        >>> X, y = make_classification(n_samples=1_000, random_state=0)
        >>> logistic_regression = LogisticRegression()
        >>> param_name, param_range = "C", np.logspace(-8, 3, 10)
        >>> ValidationCurveDisplay.from_estimator(
        ...     logistic_regression, X, y, param_name=param_name,
        ...     param_range=param_range,
        ... )
        <...>
        """

        train_scores, test_scores = validation_curve(
            X,
            y,
            param_name=param_name,
            param_range=param_range,
            cv=cv,
        )

        viz = cls(
            param_range=np.asarray(param_range),
        )
            ax=ax,
        )

        self,
        x_data,
        *,
        ax=None,
    ):

        else:

            )

            )

                    *ax.plot(
                        x_data,
                    )
                )

                        x_data,
                    )
                )

                        x_data,
                    )
                )

        else:

    def plot(
        self,
        ax=None,
        *,
    ):

            created.

            `"both"`.

        Returns
        -------
        """
            ax=ax,
        )

        cls,
        X,
        y,
        *,
        cv=None,
        ax=None,
    ):

            created.

            `"both"`.

        Returns
        -------

        Examples
        --------
        <...>
        """

            X,
            y,
            cv=cv,
        )

        viz = cls(
        )
            ax=ax,
        )

    ):

    def plot(
        self,
        ax=None,
        *,
    ):

            created.

            `"both"`.

        Returns
        -------
        """
            ax=ax,
        )

        cls,
        X,
        y,
        *,
        cv=None,
        ax=None,
    ):

            created.

            `"both"`.

        Returns
        -------

        Examples
        --------
        ... )
        <...>
        """

            X,
            y,
            cv=cv,
        )

        viz = cls(
        )
            ax=ax,
        )

class _SubsampleMetaSplitter:
    """Splitter that subsamples a given fraction of the dataset"""

    def __init__(self, *, base_cv, fraction, subsample_test, random_state):
        self.base_cv = base_cv
        self.fraction = fraction
        self.subsample_test = subsample_test

    def split(self, X, y, **kwargs):
        for train_idx, test_idx in self.base_cv.split(X, y, **kwargs):
            train_idx = resample(
                train_idx,
                n_samples=int(self.fraction * len(train_idx)),
            )
            if self.subsample_test:
                test_idx = resample(
                    test_idx,
                    replace=False,
                    n_samples=int(self.fraction * len(test_idx)),
                )
            yield train_idx, test_idx

def _top_k(results, k, itr):
    # Return the best candidates of a given iteration
    iteration, mean_test_score, params = (
        np.asarray(a)
        for a in (results["iter"], results["mean_test_score"], results["params"])
    )
    iter_indices = np.flatnonzero(iteration == itr)
    scores = mean_test_score[iter_indices]
    # argsort() places NaNs at the end of the array so we move NaNs to the
    # front of the array so the last `k` items are the those with the
    # highest scores.
    sorted_indices = np.roll(np.argsort(scores), np.count_nonzero(np.isnan(scores)))
    return np.array(params[iter_indices][sorted_indices[-k:]])

class BaseSuccessiveHalving(BaseSearchCV):
    """Implements successive halving.

    Ref:
    Almost optimal exploration in multi-armed bandits, ICML 13
    Zohar Karnin, Tomer Koren, Oren Somekh
    """

        # overwrite `scoring` since multi-metrics are not supported
        "max_resources": [
        ],
        "min_resources": [
            StrOptions({"exhaust", "smallest"}),
        ],
        "resource": [str],
        "factor": [Interval(Real, 0, None, closed="neither")],
        "aggressive_elimination": ["boolean"],
    }
    _parameter_constraints.pop("pre_dispatch")  # not used in this class

        self,
        *,
        cv=5,
        max_resources="auto",
        min_resources="exhaust",
        resource="n_samples",
        factor=3,
        aggressive_elimination=False,
    ):
            cv=cv,
        )

        self.max_resources = max_resources
        self.resource = resource
        self.factor = factor
        self.min_resources = min_resources
        self.aggressive_elimination = aggressive_elimination

    def _check_input_parameters(self, X, y, split_params):
        # We need to enforce that successive calls to cv.split() yield the same
        # splits: see https://github.com/scikit-learn/scikit-learn/issues/15149
        if not _yields_constant_splits(self._checked_cv_orig):
                "The cv parameter must yield consistent folds across "
                "calls to split(). Set its random_state to an int, or set "
                "shuffle=False."
            )

        if (
            self.resource != "n_samples"
            and self.resource not in self.estimator.get_params()
        ):
                f"Cannot use resource={self.resource} which is not supported "
                f"by estimator {self.estimator.__class__.__name__}"
            )

        if isinstance(self, HalvingRandomSearchCV):
            if self.min_resources == self.n_candidates == "exhaust":
                # for n_candidates=exhaust to work, we need to know what
                # min_resources is. Similarly min_resources=exhaust needs to
                # know the actual number of candidates.
                    "n_candidates and min_resources cannot be both set to 'exhaust'."
                )

        self.min_resources_ = self.min_resources
        if self.min_resources_ in ("smallest", "exhaust"):
            if self.resource == "n_samples":
                n_splits = self._checked_cv_orig.get_n_splits(X, y, **split_params)
                # please see https://gph.is/1KjihQe for a justification
                magic_factor = 2
                self.min_resources_ = n_splits * magic_factor
                if is_classifier(self.estimator):
                    y = validate_data(self, X="no_validation", y=y)
                    check_classification_targets(y)
                    n_classes = np.unique(y).shape[0]
                    self.min_resources_ *= n_classes
            else:
                self.min_resources_ = 1
            # if 'exhaust', min_resources_ might be set to a higher value later
            # in _run_search

        self.max_resources_ = self.max_resources
        if self.max_resources_ == "auto":
            if not self.resource == "n_samples":
                    "resource can only be 'n_samples' when max_resources='auto'"
                )
            self.max_resources_ = _num_samples(X)

        if self.min_resources_ > self.max_resources_:
                f"min_resources_={self.min_resources_} is greater "
                f"than max_resources_={self.max_resources_}."
            )

        if self.min_resources_ == 0:
                f"min_resources_={self.min_resources_}: you might have passed "
                "an empty dataset X."
            )

        """Custom refit callable to return the index of the best candidate.

        We want the best candidate out of the last iteration. By default
        BaseSearchCV would return the best candidate out of all iterations.

        Currently, we only support for a single metric thus `refit` and
        `refit_metric` are not required.
        """
        last_iter = np.max(results["iter"])
        last_iter_indices = np.flatnonzero(results["iter"] == last_iter)

        test_scores = results["mean_test_score"][last_iter_indices]
        # If all scores are NaNs there is no way to pick between them,
        # so we (arbitrarily) declare the zero'th entry the best one
        if np.isnan(test_scores).all():
            best_idx = 0
        else:
            best_idx = np.nanargmax(test_scores)

        return last_iter_indices[best_idx]

        # Halving*SearchCV.estimator is not validated yet
    )

        y : array-like, shape (n_samples,) or (n_samples, n_output), optional

            Parameters passed to the ``fit`` method of the estimator.

        Returns
        -------
        """
        self._checked_cv_orig = check_cv(
            self.cv, y, classifier=is_classifier(self.estimator)
        )

        self._check_input_parameters(
            X=X, y=y, split_params=routed_params.splitter.split
        )

        self._n_samples_orig = _num_samples(X)

        super().fit(X, y=y, **params)

        # Set best_score_: BaseSearchCV does not set it, as refit is a callable
        self.best_score_ = self.cv_results_["mean_test_score"][self.best_index_]

        candidate_params = self._generate_candidate_params()

        if self.resource != "n_samples" and any(
            self.resource in candidate for candidate in candidate_params
        ):
            # Can only check this now since we need the candidates list
                f"Cannot use parameter {self.resource} as the resource since "
                "it is part of the searched parameters."
            )

        # n_required_iterations is the number of iterations needed so that the
        # last iterations evaluates less than `factor` candidates.
        n_required_iterations = 1 + floor(log(len(candidate_params), self.factor))

        if self.min_resources == "exhaust":
            # To exhaust the resources, we want to start with the biggest
            # min_resources possible so that the last (required) iteration
            # uses as many resources as possible
            last_iteration = n_required_iterations - 1
            self.min_resources_ = max(
                self.min_resources_,
                self.max_resources_ // self.factor**last_iteration,
            )

        # n_possible_iterations is the number of iterations that we can
        # actually do starting from min_resources and without exceeding
        # max_resources. Depending on max_resources and the number of
        # candidates, this may be higher or smaller than
        # n_required_iterations.
        n_possible_iterations = 1 + floor(
            log(self.max_resources_ // self.min_resources_, self.factor)
        )

        if self.aggressive_elimination:
            n_iterations = n_required_iterations
        else:
            n_iterations = min(n_possible_iterations, n_required_iterations)

            print(f"n_iterations: {n_iterations}")
            print(f"n_required_iterations: {n_required_iterations}")
            print(f"n_possible_iterations: {n_possible_iterations}")
            print(f"min_resources_: {self.min_resources_}")
            print(f"max_resources_: {self.max_resources_}")
            print(f"aggressive_elimination: {self.aggressive_elimination}")
            print(f"factor: {self.factor}")

        self.n_resources_ = []
        self.n_candidates_ = []

        for itr in range(n_iterations):
            power = itr  # default
            if self.aggressive_elimination:
                # this will set n_resources to the initial value (i.e. the
                # value of n_resources at the first iteration) for as many
                # iterations as needed (while candidates are being
                # eliminated), and then go on as usual.
                power = max(0, itr - n_required_iterations + n_possible_iterations)

            n_resources = int(self.factor**power * self.min_resources_)
            # guard, probably not needed
            n_resources = min(n_resources, self.max_resources_)
            self.n_resources_.append(n_resources)

            n_candidates = len(candidate_params)
            self.n_candidates_.append(n_candidates)

                print("-" * 10)
                print(f"iter: {itr}")
                print(f"n_candidates: {n_candidates}")
                print(f"n_resources: {n_resources}")

                # subsampling will be done in cv.split()
                cv = _SubsampleMetaSplitter(
                    base_cv=self._checked_cv_orig,
                    fraction=n_resources / self._n_samples_orig,
                    subsample_test=True,
                )

            else:
                # Need copy so that the n_resources of next iteration does
                # not overwrite
                candidate_params = [c.copy() for c in candidate_params]
                for candidate in candidate_params:
                    candidate[self.resource] = n_resources
                cv = self._checked_cv_orig

            more_results = {
                "iter": [itr] * n_candidates,
                "n_resources": [n_resources] * n_candidates,
            }

            results = evaluate_candidates(
                candidate_params, cv, more_results=more_results
            )

            n_candidates_to_keep = ceil(n_candidates / self.factor)
            candidate_params = _top_k(results, n_candidates_to_keep, itr)

        self.n_remaining_candidates_ = len(candidate_params)
        self.n_required_iterations_ = n_required_iterations
        self.n_possible_iterations_ = n_possible_iterations
        self.n_iterations_ = n_iterations

    def _generate_candidate_params(self):
        pass

class HalvingGridSearchCV(BaseSuccessiveHalving):
    """Search over specified parameter values with successive halving.

    The search strategy starts evaluating all the candidates with a small
    amount of resources and iteratively selects the best candidates, using
    more and more resources.

    Read more in the :ref:`User guide <successive_halving_user_guide>`.

    .. note::

      you need to explicitly import ``enable_halving_search_cv``::

        >>> from sklearn.experimental import enable_halving_search_cv # noqa
        >>> # now you can import normally from model_selection
        >>> from sklearn.model_selection import HalvingGridSearchCV

        Dictionary with parameters names (string) as keys and lists of

    factor : int or float, default=3
        The 'halving' parameter, which determines the proportion of candidates
        that are selected for each subsequent iteration. For example,
        ``factor=3`` means that only one third of the candidates are selected.

    resource : ``'n_samples'`` or str, default='n_samples'
        Defines the resource that increases with each iteration. By default,
        the resource is the number of samples. It can also be set to any
        parameter of the base estimator that accepts positive integer
        values, e.g. 'n_iterations' or 'n_estimators' for a gradient
        boosting estimator. In this case ``max_resources`` cannot be 'auto'
        and must be set explicitly.

    max_resources : int, default='auto'
        The maximum amount of resource that any candidate is allowed to use
        for a given iteration. By default, this is set to ``n_samples`` when
        ``resource='n_samples'`` (default), else an error is raised.

    min_resources : {'exhaust', 'smallest'} or int, default='exhaust'
        The minimum amount of resource that any candidate is allowed to use
        for a given iteration. Equivalently, this defines the amount of
        resources `r0` that are allocated for each candidate at the first

        - 'smallest' is a heuristic that sets `r0` to a small value:

          - ``n_splits * 2`` when ``resource='n_samples'`` for a regression problem
          - ``n_classes * n_splits * 2`` when ``resource='n_samples'`` for a
            classification problem
          - ``1`` when ``resource != 'n_samples'``

        - 'exhaust' will set `r0` such that the **last** iteration uses as
          much resources as possible. Namely, the last iteration will use the
          highest value smaller than ``max_resources`` that is a multiple of
          both ``min_resources`` and ``factor``. In general, using 'exhaust'
          leads to a more accurate estimator, but is slightly more time
          consuming.

        Note that the amount of resources used at each iteration is always a
        multiple of ``min_resources``.

    aggressive_elimination : bool, default=False
        This is only relevant in cases where there isn't enough resources to
        reduce the remaining candidates to at most `factor` after the last
        iteration. If ``True``, then the search process will 'replay' the
        first iteration for as long as needed until the number of candidates
        is small enough. This is ``False`` by default, which means that the
        last iteration may evaluate more than ``factor`` candidates. See
        :ref:`aggressive_elimination` for more details.

    cv : int, cross-validation generator or iterable, default=5

        .. note::
            Due to implementation details, the folds produced by `cv` must be
            the same across multiple calls to `cv.split()`. For
            built-in `scikit-learn` iterators, this can be achieved by
            deactivating shuffling (`shuffle=False`), or by setting the
            `cv`'s `random_state` parameter to an integer.

        Scoring method to use to evaluate the predictions on the test set.

        If True, refit an estimator using the best found parameters on the
        whole dataset.

        ``HalvingGridSearchCV`` instance.

    error_score : 'raise' or numeric
        step, which will always raise the error. Default is ``np.nan``.

        scores.

        Pseudo random number generator state used for subsampling the dataset
        when `resources != 'n_samples'`. Ignored otherwise.

    n_resources_ : list of int
        The amount of resources used at each iteration.

    n_candidates_ : list of int
        The number of candidate parameters that were evaluated at each

    n_remaining_candidates_ : int
        The number of candidate parameters that are left after the last
        iteration. It corresponds to `ceil(n_candidates[-1] / factor)`

    max_resources_ : int
        The maximum number of resources that any candidate is allowed to use
        for a given iteration. Note that since the number of resources used
        at each iteration must be a multiple of ``min_resources_``, the
        actual number of resources used at the last iteration may be smaller
        than ``max_resources_``.

    min_resources_ : int
        The amount of resources that are allocated for each candidate at the
        first iteration.

    n_iterations_ : int
        The actual number of iterations that were run. This is equal to
        ``n_required_iterations_`` if ``aggressive_elimination`` is ``True``.
        Else, this is equal to ``min(n_possible_iterations_,
        n_required_iterations_)``.

    n_possible_iterations_ : int
        The number of iterations that are possible starting with
        ``min_resources_`` resources and without exceeding
        ``max_resources_``.

    n_required_iterations_ : int
        The number of iterations that are required to end up with less than
        ``factor`` candidates at the last iteration, starting with
        ``min_resources_`` resources. This will be smaller than
        ``n_possible_iterations_`` when there isn't enough resources.

        imported into a pandas ``DataFrame``. It contains lots of information
        for analysing the results of a search.
        Please refer to the :ref:`User guide<successive_halving_cv_results>`

    best_estimator_ : estimator or dict

    See Also
    --------
    :class:`HalvingRandomSearchCV`:
        Random search over a set of parameters using successive halving.

    Notes
    -----

    All parameter combinations scored with a NaN will share the lowest rank.

    Examples
    --------

    >>> from sklearn.experimental import enable_halving_search_cv  # noqa
    >>> from sklearn.model_selection import HalvingGridSearchCV
    ...
    >>> clf = RandomForestClassifier(random_state=0)
    ...
    >>> param_grid = {"max_depth": [3, None],
    ...               "min_samples_split": [5, 10]}
    >>> search = HalvingGridSearchCV(clf, param_grid, resource='n_estimators',
    ...                              max_resources=10,
    ...                              random_state=0).fit(X, y)
    >>> search.best_params_  # doctest: +SKIP
    {'max_depth': None, 'min_samples_split': 10, 'n_estimators': 9}
    """

        **BaseSuccessiveHalving._parameter_constraints,
    }

        self,
        *,
        factor=3,
        cv=5,
    ):
            cv=cv,
            max_resources=max_resources,
            resource=resource,
            factor=factor,
            min_resources=min_resources,
            aggressive_elimination=aggressive_elimination,
        )

        return ParameterGrid(self.param_grid)

class HalvingRandomSearchCV(BaseSuccessiveHalving):

    amount of resources and iteratively selects the best candidates, using more
    and more resources.

    The candidates are sampled at random from the parameter space and the
    number of sampled candidates is determined by ``n_candidates``.

    Read more in the :ref:`User guide<successive_halving_user_guide>`.

    .. note::

        >>> from sklearn.model_selection import HalvingRandomSearchCV

    n_candidates : "exhaust" or int, default="exhaust"
        The number of candidate parameters to sample, at the first
        iteration. Using 'exhaust' will sample enough candidates so that the
        last iteration uses as many resources as possible, based on
        `min_resources`, `max_resources` and `factor`. In this case,
        `min_resources` cannot be 'exhaust'.

        for a given iteration. By default, this is set ``n_samples`` when

    min_resources : {'exhaust', 'smallest'} or int, default='smallest'

          consuming. 'exhaust' isn't available when `n_candidates='exhaust'`.

    cv : int, cross-validation generator or an iterable, default=5

        .. note::

        ``HalvingRandomSearchCV`` instance.

        scores.

        when `resources != 'n_samples'`. Also used for random uniform
        sampling from lists of possible values instead of scipy.stats

        for a given iteration. Note that since the number of resources used at
        each iteration must be a multiple of ``min_resources_``, the actual
        number of resources used at the last iteration may be smaller than

    See Also
    --------
    :class:`HalvingGridSearchCV`:
        Search over a grid of parameters using successive halving.

    Notes
    -----

    Examples
    --------

    >>> from sklearn.model_selection import HalvingRandomSearchCV
    >>> from scipy.stats import randint
    ...
    ...
    >>> param_distributions = {"max_depth": [3, None],
    ...                        "min_samples_split": randint(2, 11)}
    >>> search = HalvingRandomSearchCV(clf, param_distributions,
    ...                                resource='n_estimators',
    ...                                max_resources=10,
    ...                                random_state=0).fit(X, y)
    """

        "n_candidates": [
            StrOptions({"exhaust"}),
        ],
    }

        self,
        *,
        n_candidates="exhaust",
        factor=3,
        min_resources="smallest",
        cv=5,
    ):
            cv=cv,
        )
        self.n_candidates = n_candidates

        n_candidates_first_iter = self.n_candidates
        if n_candidates_first_iter == "exhaust":
            # This will generate enough candidate so that the last iteration
            # uses as much resources as possible
            n_candidates_first_iter = self.max_resources_ // self.min_resources_
        return ParameterSampler(
            self.param_distributions,
            n_candidates_first_iter,
        )

            )
                    test_idx,
                )

        self,
        *,
        cv=5,
        factor=3,
    ):
            cv=cv,
        )

            )

        if (
        ):
            )

                )

            else:

                )

            )

            )

        """

        else:

        Returns
        -------
        """
        )

        )

        ):
            )

            )

        )

        else:

                )

            else:

            }

            )

        self,
        *,
        factor=3,
        cv=5,
    ):
            cv=cv,
        )

        self,
        *,
        factor=3,
        cv=5,
    ):
            cv=cv,
        )

        )

class _UnsupportedGroupCVMixin:
    """Mixin for splitters that do not support Groups."""

    def split(self, X, y=None, groups=None):
        """Generate indices to split data into training and test set.

            The target variable for supervised learning problems.

        groups : object
            Always ignored, exists for compatibility.

        Yields
        ------
        train : ndarray
            The training set indices for that split.

        test : ndarray
            The testing set indices for that split.
        """
        if groups is not None:
                f"The groups parameter is ignored by {self.__class__.__name__}",
            )
        return super().split(X, y, groups=groups)

class GroupsConsumerMixin(_MetadataRequester):
    """A Mixin to ``groups`` by default.

    This Mixin makes the object to request ``groups`` by default as ``True``.

    """

    __metadata_request__split = {"groups": True}

class BaseCrossValidator(_MetadataRequester, metaclass=ABCMeta):
    """Base class for all cross-validators.

    Implementations must define `_iter_test_masks` or `_iter_test_indices`.
    """

    # This indicates that by default CV splitters don't have a "groups" kwarg,
    # unless indicated by inheriting from ``GroupsConsumerMixin``.
    # This also prevents ``set_split_request`` to be generated for splitters
    # which don't support ``groups``.
    __metadata_request__split = {"groups": metadata_routing.UNUSED}

            train/test set.

        Yields
        ------

        """
        X, y, groups = indexable(X, y, groups)
        indices = np.arange(_num_samples(X))
        for test_index in self._iter_test_masks(X, y, groups):
            train_index = indices[np.logical_not(test_index)]
            test_index = indices[test_index]
            yield train_index, test_index

    # Since subclasses must implement either _iter_test_masks or
    # _iter_test_indices, neither can be abstract.
    def _iter_test_masks(self, X=None, y=None, groups=None):
        """Generates boolean masks corresponding to test sets.

        By default, delegates to _iter_test_indices(X, y, groups)
        """
        for test_index in self._iter_test_indices(X, y, groups):
            test_mask = np.zeros(_num_samples(X), dtype=bool)
            test_mask[test_index] = True
            yield test_mask

        """Generates integer indices corresponding to test sets."""

    def get_n_splits(self, X=None, y=None, groups=None):
        """Returns the number of splitting iterations in the cross-validator."""

        return _build_repr(self)

class LeaveOneOut(_UnsupportedGroupCVMixin, BaseCrossValidator):
    """Leave-One-Out cross-validator.

    Provides train/test indices to split data in train/test sets. Each
    sample is used once as a test set (singleton) while the remaining
    samples form the training set.

    Note: ``LeaveOneOut()`` is equivalent to ``KFold(n_splits=n)`` and
    ``LeavePOut(p=1)`` where ``n`` is the number of samples.

    Due to the high number of test sets (which is the same as the
    number of samples) this cross-validation method can be very costly.
    For large datasets one should favor :class:`KFold`, :class:`ShuffleSplit`
    or :class:`StratifiedKFold`.

    Read more in the :ref:`User Guide <leave_one_out>`.

    Examples
    --------
    >>> from sklearn.model_selection import LeaveOneOut
    >>> y = np.array([1, 2])
    >>> loo = LeaveOneOut()
    >>> loo.get_n_splits(X)
    2
    >>> print(loo)
    LeaveOneOut()
    >>> for i, (train_index, test_index) in enumerate(loo.split(X)):
    ...     print(f"Fold {i}:")
    ...     print(f"  Train: index={train_index}")
    ...     print(f"  Test:  index={test_index}")
    Fold 0:
      Train: index=[1]
      Test:  index=[0]
    Fold 1:
      Train: index=[0]
      Test:  index=[1]

    See Also
    --------
    LeaveOneGroupOut : For splitting the data according to explicit,
        domain-specific stratification of the dataset.
    GroupKFold : K-fold iterator variant with non-overlapping groups.
    """

    def _iter_test_indices(self, X, y=None, groups=None):
        if n_samples <= 1:
                "Cannot perform LeaveOneOut with n_samples={}.".format(n_samples)
            )
        return range(n_samples)

    def get_n_splits(self, X, y=None, groups=None):
        """Returns the number of splitting iterations in the cross-validator.

        y : object

        Returns
        -------
        n_splits : int
            Returns the number of splitting iterations in the cross-validator.
        """
            raise ValueError("The 'X' parameter should not be None.")
        return _num_samples(X)

class LeavePOut(_UnsupportedGroupCVMixin, BaseCrossValidator):
    """Leave-P-Out cross-validator.

    Provides train/test indices to split data in train/test sets. This results
    in testing on all distinct samples of size p, while the remaining n - p
    samples form the training set in each iteration.

    Note: ``LeavePOut(p)`` is NOT equivalent to
    ``KFold(n_splits=n_samples // p)`` which creates non-overlapping test sets.

    Due to the high number of iterations which grows combinatorically with the
    number of samples this cross-validation method can be very costly. For
    large datasets one should favor :class:`KFold`, :class:`StratifiedKFold`
    or :class:`ShuffleSplit`.

    Read more in the :ref:`User Guide <leave_p_out>`.

    p : int
        Size of the test sets. Must be strictly less than the number of
        samples.

    Examples
    --------
    >>> from sklearn.model_selection import LeavePOut
    >>> X = np.array([[1, 2], [3, 4], [5, 6], [7, 8]])
    >>> y = np.array([1, 2, 3, 4])
    >>> lpo = LeavePOut(2)
    >>> lpo.get_n_splits(X)
    6
    >>> print(lpo)
    LeavePOut(p=2)
    >>> for i, (train_index, test_index) in enumerate(lpo.split(X)):
    Fold 0:
      Train: index=[2 3]
      Test:  index=[0 1]
    Fold 1:
      Train: index=[1 3]
      Test:  index=[0 2]
    Fold 2:
      Train: index=[1 2]
      Test:  index=[0 3]
    Fold 3:
      Train: index=[0 3]
      Test:  index=[1 2]
    Fold 4:
      Train: index=[0 2]
      Test:  index=[1 3]
    Fold 5:
      Train: index=[0 1]
      Test:  index=[2 3]
    """

    def __init__(self, p):
        self.p = p

        if n_samples <= self.p:
                "p={} must be strictly less than the number of samples={}".format(
                    self.p, n_samples
                )
            )
        for combination in combinations(range(n_samples), self.p):
            yield np.array(combination)

        y : object

        """
        return int(comb(_num_samples(X), self.p, exact=True))

class _BaseKFold(BaseCrossValidator, metaclass=ABCMeta):
    """Base class for K-Fold cross-validators and TimeSeriesSplit."""

    def __init__(self, n_splits, *, shuffle, random_state):
        if not isinstance(n_splits, numbers.Integral):
                "The number of folds must be of Integral type. "
                "%s of type %s was passed." % (n_splits, type(n_splits))
            )
        n_splits = int(n_splits)

        if n_splits <= 1:
                "k-fold cross-validation requires at least one"
                " train/test split by setting n_splits=2 or more,"
                " got n_splits={0}.".format(n_splits)
            )

        if not isinstance(shuffle, bool):
            raise TypeError("shuffle must be True or False; got {0}".format(shuffle))

        if not shuffle and random_state is not None:  # None is the default
                (
                    "Setting a random_state has no effect since shuffle is "
                    "False. You should leave "
                    "random_state to its default (None), or set shuffle=True."
                ),
            )

        self.n_splits = n_splits

        Yields
        ------

        """
        if self.n_splits > n_samples:
                (
                    "Cannot have number of splits n_splits={0} greater"
                    " than the number of samples: n_samples={1}."
                ).format(self.n_splits, n_samples)
            )

        for train, test in super().split(X, y, groups):
            yield train, test

        X : object

        y : object

        Returns
        -------
        """
        return self.n_splits

class KFold(_UnsupportedGroupCVMixin, _BaseKFold):
    """K-Fold cross-validator.

    Provides train/test indices to split data in train/test sets. Split
    dataset into k consecutive folds (without shuffling by default).

    Each fold is then used once as a validation while the k - 1 remaining
    folds form the training set.

    Read more in the :ref:`User Guide <k_fold>`.

    For visualisation of cross-validation behaviour and
    comparison between common scikit-learn split methods
    refer to :ref:`sphx_glr_auto_examples_model_selection_plot_cv_indices.py`

    n_splits : int, default=5
        Number of folds. Must be at least 2.

            ``n_splits`` default value changed from 3 to 5.

        Whether to shuffle the data before splitting into batches.
        Note that the samples within each split will not be shuffled.

        When `shuffle` is True, `random_state` affects the ordering of the
        indices, which controls the randomness of each fold. Otherwise, this
        parameter has no effect.

    Examples
    --------
    >>> from sklearn.model_selection import KFold
    >>> X = np.array([[1, 2], [3, 4], [1, 2], [3, 4]])
    >>> kf = KFold(n_splits=2)
    >>> kf.get_n_splits(X)
    2
    >>> print(kf)
    KFold(n_splits=2, random_state=None, shuffle=False)
    >>> for i, (train_index, test_index) in enumerate(kf.split(X)):
    Fold 0:
    Fold 1:

    Notes
    -----
    The first ``n_samples % n_splits`` folds have size
    ``n_samples // n_splits + 1``, other folds have size
    ``n_samples // n_splits``, where ``n_samples`` is the number of samples.

    Randomized CV splitters may return different results for each call of
    split. You can make the results identical by setting `random_state`
    to an integer.

    See Also
    --------
    StratifiedKFold : Takes class information into account to avoid building
        folds with imbalanced class distributions (for binary or multiclass
        classification tasks).

    RepeatedKFold : Repeats K-Fold n times.
    """

    def __init__(self, n_splits=5, *, shuffle=False, random_state=None):
        super().__init__(n_splits=n_splits, shuffle=shuffle, random_state=random_state)

        indices = np.arange(n_samples)
            check_random_state(self.random_state).shuffle(indices)

        n_splits = self.n_splits
        fold_sizes = np.full(n_splits, n_samples // n_splits, dtype=int)
        fold_sizes[: n_samples % n_splits] += 1
        current = 0
        for fold_size in fold_sizes:
            start, stop = current, current + fold_size
            yield indices[start:stop]
            current = stop

class GroupKFold(GroupsConsumerMixin, _BaseKFold):
    """K-fold iterator variant with non-overlapping groups.

    Each group will appear exactly once in the test set across all folds (the
    number of distinct groups has to be at least equal to the number of folds).

    The folds are approximately balanced in the sense that the number of
    samples is approximately the same in each test fold when `shuffle` is True.

    Read more in the :ref:`User Guide <group_k_fold>`.

        Whether to shuffle the groups before splitting into batches.

    Notes
    -----
    Groups appear in an arbitrary order throughout the folds.

    Examples
    --------
    >>> from sklearn.model_selection import GroupKFold
    >>> X = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]])
    >>> groups = np.array([0, 0, 2, 2, 3, 3])
    >>> group_kfold = GroupKFold(n_splits=2)
    >>> group_kfold.get_n_splits(X, y, groups)
    2
    >>> print(group_kfold)
    GroupKFold(n_splits=2, random_state=None, shuffle=False)
    >>> for i, (train_index, test_index) in enumerate(group_kfold.split(X, y, groups)):
    ...     print(f"  Train: index={train_index}, group={groups[train_index]}")
    ...     print(f"  Test:  index={test_index}, group={groups[test_index]}")
    Fold 0:
      Train: index=[2 3], group=[2 2]
      Test:  index=[0 1 4 5], group=[0 0 3 3]
    Fold 1:
      Train: index=[0 1 4 5], group=[0 0 3 3]
      Test:  index=[2 3], group=[2 2]

    See Also
    --------
    LeaveOneGroupOut : For splitting the data according to explicit

        folds with imbalanced class proportions (for binary or multiclass
    """

        super().__init__(n_splits, shuffle=shuffle, random_state=random_state)

    def _iter_test_indices(self, X, y, groups):
        if groups is None:
            raise ValueError("The 'groups' parameter should not be None.")
        groups = check_array(groups, input_name="groups", ensure_2d=False, dtype=None)

        unique_groups, group_idx = np.unique(groups, return_inverse=True)
        n_groups = len(unique_groups)

        if self.n_splits > n_groups:
                "Cannot have number of splits n_splits=%d greater"
                " than the number of groups: %d." % (self.n_splits, n_groups)
            )

            # Split and shuffle unique groups across n_splits
            unique_groups = rng.permutation(unique_groups)
            split_groups = np.array_split(unique_groups, self.n_splits)

            for test_group_ids in split_groups:
                test_mask = np.isin(groups, test_group_ids)
                yield np.where(test_mask)[0]

        else:
            # Weight groups by their number of occurrences
            n_samples_per_group = np.bincount(group_idx)

            # Distribute the most frequent groups first
            indices = np.argsort(n_samples_per_group)[::-1]
            n_samples_per_group = n_samples_per_group[indices]

            # Total weight of each fold
            n_samples_per_fold = np.zeros(self.n_splits)

            # Mapping from group index to fold index
            group_to_fold = np.zeros(len(unique_groups))

            # Distribute samples by adding the largest weight to the lightest fold
            for group_index, weight in enumerate(n_samples_per_group):
                lightest_fold = np.argmin(n_samples_per_fold)
                n_samples_per_fold[lightest_fold] += weight
                group_to_fold[indices[group_index]] = lightest_fold

            indices = group_to_fold[group_idx]

            for f in range(self.n_splits):
                yield np.where(indices == f)[0]

        groups : array-like of shape (n_samples,)

        Yields
        ------

        """
        return super().split(X, y, groups)

class StratifiedKFold(_BaseKFold):
    """Class-wise stratified K-Fold cross-validator.

    Provides train/test indices to split data in train/test sets.

    This cross-validation object is a variation of KFold that returns
    stratified folds. The folds are made by preserving the percentage of
    samples for each class in `y` in a binary or multiclass classification
    setting.

    Read more in the :ref:`User Guide <stratified_k_fold>`.

    .. note::

        Stratification on the class label solves an engineering problem rather
        than a statistical one. See :ref:`stratification` for more details.

        Whether to shuffle each class's samples before splitting into batches.

        indices, which controls the randomness of each fold for each class.
        Otherwise, leave `random_state` as `None`.

    Examples
    --------
    >>> from sklearn.model_selection import StratifiedKFold
    >>> y = np.array([0, 0, 1, 1])
    >>> skf = StratifiedKFold(n_splits=2)
    >>> skf.get_n_splits(X, y)
    2
    >>> print(skf)
    StratifiedKFold(n_splits=2, random_state=None, shuffle=False)
    >>> for i, (train_index, test_index) in enumerate(skf.split(X, y)):
    Fold 0:
    Fold 1:

    Notes
    -----
    The implementation is designed to:

    * Generate test sets such that all contain the same distribution of
      classes, or as close as possible.
    * Be invariant to class label: relabelling ``y = ["Happy", "Sad"]`` to
      ``y = [1, 0]`` should not change the indices generated.
    * Preserve order dependencies in the dataset ordering, when
      ``shuffle=False``: all samples from class k in some test set were
      contiguous in y, or separated in y by samples from classes other than k.
    * Generate test sets where the smallest and largest differ by at most one
      sample.

    .. versionchanged:: 0.22
        The previous implementation did not follow the last constraint.

    See Also
    --------
    RepeatedStratifiedKFold : Repeats Stratified K-Fold n times.
    """

    def _make_test_folds(self, X, y=None):
        # XXX: as of now, cross-validation splitters only operate in NumPy-land
        # without attempting to leverage array API namespace features. However
        # they might be fed by array API inputs, e.g. in CV-enabled estimators so
        # we need the following explicit conversion:
        xp, is_array_api = get_namespace(y)
        if is_array_api:
            y = _convert_to_numpy(y, xp)
        else:
        type_of_target_y = type_of_target(y)
        allowed_target_types = ("binary", "multiclass")
        if type_of_target_y not in allowed_target_types:
                "Supported target types are: {}. Got {!r} instead.".format(
                    allowed_target_types, type_of_target_y
                )
            )

        _, y_idx, y_inv = np.unique(y, return_index=True, return_inverse=True)
        # y_inv encodes y according to lexicographic order. We invert y_idx to
        # map the classes so that they are encoded by order of appearance:
        # 0 represents the first label appearing in y, 1 the second, etc.
        _, class_perm = np.unique(y_idx, return_inverse=True)
        y_encoded = class_perm[y_inv]

        n_classes = len(y_idx)
        y_counts = np.bincount(y_encoded)
        min_groups = np.min(y_counts)
        if np.all(self.n_splits > y_counts):
                "n_splits=%d cannot be greater than the"
                " number of members in each class." % (self.n_splits)
            )
        if self.n_splits > min_groups:
                "The least populated class in y has only %d"
                " members, which is less than n_splits=%d."
                % (min_groups, self.n_splits),
            )

        # Determine the optimal number of samples from each class in each fold,
        # using round robin over the sorted y. (This can be done direct from
        # counts, but that code is unreadable.)
        y_order = np.sort(y_encoded)
        allocation = np.asarray(
            [
                np.bincount(y_order[i :: self.n_splits], minlength=n_classes)
                for i in range(self.n_splits)
            ]
        )

        # To maintain the data order dependencies as best as possible within
        # the stratification constraint, we assign samples from each class in
        # blocks (and then mess that up when shuffle=True).
        test_folds = np.empty(len(y), dtype="i")
        for k in range(n_classes):
            # since the kth column of allocation stores the number of samples
            # of class k in each test set, this generates blocks of fold
            # indices corresponding to the allocation for class k.
            folds_for_class = np.arange(self.n_splits).repeat(allocation[:, k])
            if self.shuffle:
                rng.shuffle(folds_for_class)
            test_folds[y_encoded == k] = folds_for_class
        return test_folds

    def _iter_test_masks(self, X, y=None, groups=None):
        test_folds = self._make_test_folds(X, y)
        for i in range(self.n_splits):
            yield test_folds == i

    def split(self, X, y, groups=None):

            Note that providing ``y`` is sufficient to generate the splits and
            hence ``np.zeros(n_samples)`` may be used as a placeholder for
            ``X`` instead of actual training data.

            Stratification is done based on the y labels.

        Yields
        ------

        Notes
        -----
        Randomized CV splitters may return different results for each call of
        split. You can make the results identical by setting `random_state`
        to an integer.
        """
            )
        y = check_array(y, input_name="y", ensure_2d=False, dtype=None)

class StratifiedGroupKFold(GroupsConsumerMixin, _BaseKFold):
    """Class-wise stratified K-Fold iterator variant with non-overlapping groups.

    This cross-validation object is a variation of StratifiedKFold attempts to
    return stratified folds with non-overlapping groups. The folds are made by
    preserving the percentage of samples for each class in `y` in a binary or
    multiclass classification setting.

    The difference between :class:`GroupKFold`
    and `StratifiedGroupKFold` is that
    the former attempts to create balanced folds such that the number of
    distinct groups is approximately the same in each fold, whereas
    `StratifiedGroupKFold` attempts to create folds which preserve the
    percentage of samples for each class as much as possible given the
    constraint of non-overlapping groups between splits.

    Read more in the :ref:`User Guide <stratified_group_k_fold>`.

    .. note::

        This implementation can only shuffle groups that have approximately the
        same y distribution, no global shuffle will be performed.

    Examples
    --------
    >>> from sklearn.model_selection import StratifiedGroupKFold
    >>> X = np.ones((17, 2))
    >>> y = np.array([0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    >>> groups = np.array([1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 8])
    >>> sgkf = StratifiedGroupKFold(n_splits=3)
    >>> sgkf.get_n_splits(X, y)
    3
    >>> print(sgkf)
    StratifiedGroupKFold(n_splits=3, random_state=None, shuffle=False)
    >>> for i, (train_index, test_index) in enumerate(sgkf.split(X, y, groups)):
    ...     print(f"         group={groups[train_index]}")
    ...     print(f"         group={groups[test_index]}")
    Fold 0:
      Train: index=[ 0  1  2  3  7  8  9 10 11 15 16]
             group=[1 1 2 2 4 5 5 5 5 8 8]
      Test:  index=[ 4  5  6 12 13 14]
             group=[3 3 3 6 6 7]
    Fold 1:
      Train: index=[ 4  5  6  7  8  9 10 11 12 13 14]
             group=[3 3 3 4 5 5 5 5 6 6 7]
      Test:  index=[ 0  1  2  3 15 16]
             group=[1 1 2 2 8 8]
    Fold 2:
      Train: index=[ 0  1  2  3  4  5  6 12 13 14 15 16]
             group=[1 1 2 2 3 3 3 6 6 7 8 8]
      Test:  index=[ 7  8  9 10 11]
             group=[4 5 5 5 5]

    Notes
    -----

    * Mimic the behavior of StratifiedKFold as much as possible for trivial
      groups (e.g. when each group contains only one sample).
    * Stratify based on samples as much as possible while keeping
      non-overlapping groups constraint. That means that in some cases when
      there is a small number of groups containing a large number of samples
      the stratification will not be possible and the behavior will be close
      to GroupKFold.

    See also
    --------
    StratifiedKFold: Takes class information into account to build folds which
        retain class distributions (for binary or multiclass classification
        tasks).

    GroupKFold: K-fold iterator variant with non-overlapping groups.
    """

    def __init__(self, n_splits=5, shuffle=False, random_state=None):

        # Implementation is based on this kaggle kernel:
        # https://www.kaggle.com/jakubwasikowski/stratified-group-k-fold-cross-validation
        # and is a subject to Apache 2.0 License. You may obtain a copy of the
        # License at http://www.apache.org/licenses/LICENSE-2.0
        # Changelist:
        # - Refactored function to a class following scikit-learn KFold
        #   interface.
        # - Added heuristic for assigning group to the least populated fold in
        #   cases when all other criteria are equal
        # - Swtch from using python ``Counter`` to ``np.unique`` to get class
        #   distribution
        # - Added scikit-learn checks for input: checking that target is binary
        #   or multiclass, checking passed random state, checking that number
        #   of splits is less than number of members in each class, checking
        #   that least populated class has more members than there are splits.
                )
            )

        _, y_inv, y_cnt = np.unique(y, return_inverse=True, return_counts=True)
        if np.all(self.n_splits > y_cnt):
            )
        n_smallest_class = np.min(y_cnt)
        if self.n_splits > n_smallest_class:
                % (n_smallest_class, self.n_splits),
            )
        n_classes = len(y_cnt)

        _, groups_inv, groups_cnt = np.unique(
            groups, return_inverse=True, return_counts=True
        )
        y_counts_per_group = np.zeros((len(groups_cnt), n_classes))
        for class_idx, group_idx in zip(y_inv, groups_inv):
            y_counts_per_group[group_idx, class_idx] += 1

        y_counts_per_fold = np.zeros((self.n_splits, n_classes))
        groups_per_fold = defaultdict(set)

            rng.shuffle(y_counts_per_group)

        # Stable sort to keep shuffled order for groups with the same
        # class distribution variance
        sorted_groups_idx = np.argsort(
            -np.std(y_counts_per_group, axis=1), kind="mergesort"
        )

        for group_idx in sorted_groups_idx:
            group_y_counts = y_counts_per_group[group_idx]
            best_fold = self._find_best_fold(
                y_counts_per_fold=y_counts_per_fold,
                y_cnt=y_cnt,
                group_y_counts=group_y_counts,
            )
            y_counts_per_fold[best_fold] += group_y_counts
            groups_per_fold[best_fold].add(group_idx)

            test_indices = [
                idx
                for idx, group_idx in enumerate(groups_inv)
                if group_idx in groups_per_fold[i]
            ]
            yield test_indices

    def _find_best_fold(self, y_counts_per_fold, y_cnt, group_y_counts):
        best_fold = None
        min_eval = np.inf
        min_samples_in_fold = np.inf
            y_counts_per_fold[i] += group_y_counts
            # Summarise the distribution over classes in each proposed fold
            std_per_class = np.std(y_counts_per_fold / y_cnt.reshape(1, -1), axis=0)
            y_counts_per_fold[i] -= group_y_counts
            fold_eval = np.mean(std_per_class)
            samples_in_fold = np.sum(y_counts_per_fold[i])
            is_current_fold_better = fold_eval < min_eval or (
                np.isclose(fold_eval, min_eval)
                and samples_in_fold < min_samples_in_fold
            )
            if is_current_fold_better:
                min_eval = fold_eval
                min_samples_in_fold = samples_in_fold
                best_fold = i
        return best_fold

class TimeSeriesSplit(_BaseKFold):
    """Time Series cross-validator.

    Provides train/test indices to split time-ordered data, where other
    cross-validation methods are inappropriate, as they would lead to training
    on future data and evaluating on past data.
    To ensure comparable metrics across folds, samples must be equally spaced.
    Once this condition is met, each test set covers the same time duration,
    while the train set size accumulates data from previous splits.

    This cross-validation object is a variation of :class:`KFold`.
    In the kth split, it returns first k folds as train set and the
    (k+1)th fold as test set.

    Note that unlike standard cross-validation methods, successive
    training sets are supersets of those that come before them.

    Read more in the :ref:`User Guide <time_series_split>`.

        Number of splits. Must be at least 2.

    max_train_size : int, default=None
        Maximum size for a single training set.

    test_size : int, default=None
        Used to limit the size of the test set. Defaults to
        ``n_samples // (n_splits + 1)``, which is the maximum allowed value
        with ``gap=0``.

    gap : int, default=0
        Number of samples to exclude from the end of each train set before

    Examples
    --------
    >>> from sklearn.model_selection import TimeSeriesSplit
    >>> X = np.array([[1, 2], [3, 4], [1, 2], [3, 4], [1, 2], [3, 4]])
    >>> tscv = TimeSeriesSplit()
    >>> print(tscv)
    TimeSeriesSplit(gap=0, max_train_size=None, n_splits=5, test_size=None)
    >>> for i, (train_index, test_index) in enumerate(tscv.split(X)):
    Fold 0:
    Fold 1:
      Test:  index=[2]
    Fold 2:
      Train: index=[0 1 2]
      Test:  index=[3]
    Fold 3:
      Train: index=[0 1 2 3]
      Test:  index=[4]
    Fold 4:
      Train: index=[0 1 2 3 4]
      Test:  index=[5]
    >>> # Fix test_size to 2 with 12 samples
    >>> X = np.random.randn(12, 2)
    >>> y = np.random.randint(0, 2, 12)
    >>> tscv = TimeSeriesSplit(n_splits=3, test_size=2)
    Fold 0:
      Train: index=[0 1 2 3 4 5]
      Test:  index=[6 7]
    Fold 1:
      Train: index=[0 1 2 3 4 5 6 7]
      Test:  index=[8 9]
    Fold 2:
      Train: index=[0 1 2 3 4 5 6 7 8 9]
      Test:  index=[10 11]
    >>> # Add in a 2 period gap
    >>> tscv = TimeSeriesSplit(n_splits=3, test_size=2, gap=2)
    Fold 0:
    Fold 1:
    Fold 2:

    For a more extended example see
    :ref:`sphx_glr_auto_examples_applications_plot_cyclical_feature_engineering.py`.

    Notes
    -----
    The training set has size ``i * n_samples // (n_splits + 1)
    + n_samples % (n_splits + 1)`` in the ``i`` th split,
    with a test set of size ``n_samples//(n_splits + 1)`` by default,
    where ``n_samples`` is the number of samples. Note that this
    formula is only valid when ``test_size`` and ``max_train_size`` are
    left to their default values.
    """

    def __init__(self, n_splits=5, *, max_train_size=None, test_size=None, gap=0):
        super().__init__(n_splits, shuffle=False, random_state=None)
        self.max_train_size = max_train_size
        self.test_size = test_size
        self.gap = gap

        Yields
        ------

        """
            )
        return self._split(X)

    def _split(self, X):

        Yields
        ------

        """
        (X,) = indexable(X)
        n_folds = n_splits + 1
        gap = self.gap
        test_size = (
            self.test_size if self.test_size is not None else n_samples // n_folds
        )

        # Make sure we have enough samples for the given split parameters
        if n_folds > n_samples:
                f"Cannot have number of folds={n_folds} greater"
                f" than the number of samples={n_samples}."
            )
        if n_samples - gap - (test_size * n_splits) <= 0:
                f"Too many splits={n_splits} for number of samples"
                f"={n_samples} with test_size={test_size} and gap={gap}."
            )

        test_starts = range(n_samples - n_splits * test_size, n_samples, test_size)

        for test_start in test_starts:
            train_end = test_start - gap
            if self.max_train_size and self.max_train_size < train_end:
                yield (
                    indices[train_end - self.max_train_size : train_end],
                    indices[test_start : test_start + test_size],
                )
            else:
                yield (
                    indices[:train_end],
                )

class LeaveOneGroupOut(GroupsConsumerMixin, BaseCrossValidator):
    """Leave One Group Out cross-validator.

    Provides train/test indices to split data such that each training set is
    comprised of all samples except ones belonging to one specific group.
    Arbitrary domain specific group information is provided as an array of integers
    that encodes the group of each sample.

    For instance the groups could be the year of collection of the samples
    and thus allow for cross-validation against time-based splits.

    Read more in the :ref:`User Guide <leave_one_group_out>`.

    Notes
    -----
    Splits are ordered according to the index of the group left out. The first
    split has testing set consisting of the group whose index in `groups` is
    lowest, and so on.

    Examples
    --------
    >>> from sklearn.model_selection import LeaveOneGroupOut
    >>> y = np.array([1, 2, 1, 2])
    >>> groups = np.array([1, 1, 2, 2])
    >>> logo = LeaveOneGroupOut()
    >>> logo.get_n_splits(X, y, groups)
    2
    >>> logo.get_n_splits(groups=groups)  # 'groups' is always required
    2
    >>> print(logo)
    LeaveOneGroupOut()
    >>> for i, (train_index, test_index) in enumerate(logo.split(X, y, groups)):
    Fold 0:
      Test:  index=[0 1], group=[1 1]
    Fold 1:
      Train: index=[0 1], group=[1 1]

    See also
    --------
    """

    def _iter_test_masks(self, X, y, groups):
        # We make a copy of groups to avoid side-effects during iteration
        groups = check_array(
            groups, input_name="groups", copy=True, ensure_2d=False, dtype=None
        )
        unique_groups = np.unique(groups)
        if len(unique_groups) <= 1:
                "The groups parameter contains fewer than 2 unique groups "
                "(%s). LeaveOneGroupOut expects at least 2." % unique_groups
            )
        for i in unique_groups:
            yield groups == i

        X : object

        y : object

            train/test set. This 'groups' parameter must always be specified to
            calculate the number of splits, though the other parameters can be
            omitted.

        Returns
        -------
        """
        return len(np.unique(groups))

        Yields
        ------

        """

class LeavePGroupsOut(GroupsConsumerMixin, BaseCrossValidator):
    """Leave P Group(s) Out cross-validator.

    Provides train/test indices to split data according to a third-party
    provided group. This group information can be used to encode arbitrary
    domain specific stratifications of the samples as integers.

    The difference between LeavePGroupsOut and LeaveOneGroupOut is that
    the former builds the test sets with all the samples assigned to
    ``p`` different values of the groups while the latter uses samples
    all assigned the same groups.

    Read more in the :ref:`User Guide <leave_p_groups_out>`.

    n_groups : int
        Number of groups (``p``) to leave out in the test split.

    Examples
    --------
    >>> from sklearn.model_selection import LeavePGroupsOut
    >>> X = np.array([[1, 2], [3, 4], [5, 6]])
    >>> y = np.array([1, 2, 1])
    >>> groups = np.array([1, 2, 3])
    >>> lpgo = LeavePGroupsOut(n_groups=2)
    >>> lpgo.get_n_splits(X, y, groups)
    3
    >>> lpgo.get_n_splits(groups=groups)  # 'groups' is always required
    3
    >>> print(lpgo)
    LeavePGroupsOut(n_groups=2)
    >>> for i, (train_index, test_index) in enumerate(lpgo.split(X, y, groups)):
    Fold 0:
      Train: index=[2], group=[3]
      Test:  index=[0 1], group=[1 2]
    Fold 1:
      Train: index=[1], group=[2]
      Test:  index=[0 2], group=[1 3]
    Fold 2:
      Train: index=[0], group=[1]
      Test:  index=[1 2], group=[2 3]

    See Also
    --------
    """

    def __init__(self, n_groups):
        self.n_groups = n_groups

        )
        if self.n_groups >= len(unique_groups):
                "The groups parameter contains fewer than (or equal to) "
                "n_groups (%d) numbers of unique groups (%s). LeavePGroupsOut "
                "expects that at least n_groups + 1 (%d) unique groups be "
                "present" % (self.n_groups, unique_groups, self.n_groups + 1)
            )
        combi = combinations(range(len(unique_groups)), self.n_groups)
        for indices in combi:
            test_index = np.zeros(_num_samples(X), dtype=bool)
            for l in unique_groups[np.array(indices)]:
                test_index[groups == l] = True
            yield test_index

        X : object

        y : object

            omitted.

        Returns
        -------
        """
        return int(comb(len(np.unique(groups)), self.n_groups, exact=True))

        Yields
        ------

        """

class _RepeatedSplits(_MetadataRequester, metaclass=ABCMeta):
    """Repeated splits for an arbitrary randomized CV splitter.

    Repeats splits for cross-validators n times with different randomization
    in each repetition.

    cv : callable
        Cross-validator class.

    n_repeats : int, default=10
        Number of times cross-validator needs to be repeated.

        Passes `random_state` to the arbitrary repeating cross validator.

    **cvargs : additional params
        Constructor parameters for cv. Must not contain random_state
        and shuffle.
    """

    def __init__(self, cv, *, n_repeats=10, random_state=None, **cvargs):
        if not isinstance(n_repeats, numbers.Integral):
            raise ValueError("Number of repetitions must be of Integral type.")

        if n_repeats <= 0:
            raise ValueError("Number of repetitions must be greater than 0.")

        if any(key in cvargs for key in ("random_state", "shuffle")):
            raise ValueError("cvargs must not contain random_state or shuffle.")

        self.n_repeats = n_repeats
        self.cvargs = cvargs

        """Generates indices to split data into training and test set.

        Yields
        ------

        """
        n_repeats = self.n_repeats

        for idx in range(n_repeats):
            cv = self.cv(random_state=rng, shuffle=True, **self.cvargs)
            for train_index, test_index in cv.split(X, y, groups):
                yield train_index, test_index

        X : object
            ``np.zeros(n_samples)`` may be used as a placeholder.

        y : object

        Returns
        -------
        """
        cv = self.cv(random_state=rng, shuffle=True, **self.cvargs)
        return cv.get_n_splits(X, y, groups) * self.n_repeats

class RepeatedKFold(_UnsupportedGroupCVMixin, _RepeatedSplits):
    """Repeated K-Fold cross validator.

    Repeats K-Fold `n_repeats` times with different randomization in each repetition.

    Read more in the :ref:`User Guide <repeated_k_fold>`.

        Controls the randomness of each repeated cross-validation instance.

    Examples
    --------
    >>> from sklearn.model_selection import RepeatedKFold
    >>> rkf = RepeatedKFold(n_splits=2, n_repeats=2, random_state=2652124)
    >>> rkf.get_n_splits(X, y)
    4
    >>> print(rkf)
    RepeatedKFold(n_repeats=2, n_splits=2, random_state=2652124)
    >>> for i, (train_index, test_index) in enumerate(rkf.split(X)):
    ...
    Fold 0:
    Fold 1:
    Fold 2:
    Fold 3:

    Notes
    -----

    See Also
    --------
    """

    def __init__(self, *, n_splits=5, n_repeats=10, random_state=None):
            KFold, n_repeats=n_repeats, random_state=random_state, n_splits=n_splits
        )

class RepeatedStratifiedKFold(_UnsupportedGroupCVMixin, _RepeatedSplits):
    """Repeated class-wise stratified K-Fold cross validator.

    Repeats Stratified K-Fold n times with different randomization in each
    repetition.

    .. note::

        Controls the generation of the random states for each repetition.

    Examples
    --------
    >>> from sklearn.model_selection import RepeatedStratifiedKFold
    >>> rskf = RepeatedStratifiedKFold(n_splits=2, n_repeats=2,
    ...     random_state=36851234)
    >>> rskf.get_n_splits(X, y)
    4
    >>> print(rskf)
    RepeatedStratifiedKFold(n_repeats=2, n_splits=2, random_state=36851234)
    >>> for i, (train_index, test_index) in enumerate(rskf.split(X, y)):
    ...
    Fold 0:
    Fold 1:
    Fold 2:
    Fold 3:

    Notes
    -----

    See Also
    --------
    """

            StratifiedKFold,
            n_repeats=n_repeats,
            n_splits=n_splits,
        )

        Yields
        ------

        Notes
        -----
        """

class BaseShuffleSplit(_MetadataRequester, metaclass=ABCMeta):
    """Base class for *ShuffleSplit.

    n_splits : int, default=10
        Number of re-shuffling & splitting iterations.

    test_size : float or int, default=None
        of the dataset to include in the test split. If int, represents the
        absolute number of test samples. If None, the value is set to the
        complement of the train size. If ``train_size`` is also None, it will
        be set to 0.1.

    train_size : float or int, default=None
        If float, should be between 0.0 and 1.0 and represent the
        proportion of the dataset to include in the train split. If
        int, represents the absolute number of train samples. If None,
        the value is automatically set to the complement of the test size.

        Controls the randomness of the training and testing indices produced.
    """

        self, n_splits=10, *, test_size=None, train_size=None, random_state=None
    ):
        self.train_size = train_size
        self._default_test_size = 0.1

        Yields
        ------

        Notes
        -----
        """
        for train, test in self._iter_indices(X, y, groups):

    def _iter_indices(self, X, y=None, groups=None):
        """Generate (train, test) indices"""
        n_train, n_test = _validate_shuffle_split(
            self.test_size,
            self.train_size,
            default_test_size=self._default_test_size,
        )

            # random partition
            permutation = rng.permutation(n_samples)
            ind_test = permutation[:n_test]
            ind_train = permutation[n_test : (n_test + n_train)]
            yield ind_train, ind_test

        X : object

        y : object

        Returns
        -------
        """

class ShuffleSplit(_UnsupportedGroupCVMixin, BaseShuffleSplit):
    """Random permutation cross-validator.

    Yields indices to split data into training and test sets.

    Note: contrary to other cross-validation strategies, random splits
    do not guarantee that test sets across all folds will be mutually exclusive,
    and might include overlapping samples. However, this is still very likely for
    sizeable datasets.

    Read more in the :ref:`User Guide <ShuffleSplit>`.

    Examples
    --------
    >>> from sklearn.model_selection import ShuffleSplit
    >>> X = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [3, 4], [5, 6]])
    >>> y = np.array([1, 2, 1, 2, 1, 2])
    >>> rs = ShuffleSplit(n_splits=5, test_size=.25, random_state=0)
    >>> rs.get_n_splits(X)
    5
    >>> print(rs)
    ShuffleSplit(n_splits=5, random_state=0, test_size=0.25, train_size=None)
    >>> for i, (train_index, test_index) in enumerate(rs.split(X)):
    Fold 0:
      Train: index=[1 3 0 4]
      Test:  index=[5 2]
    Fold 1:
      Train: index=[4 0 2 5]
    Fold 2:
      Train: index=[1 2 4 0]
      Test:  index=[3 5]
    Fold 3:
      Train: index=[3 4 1 0]
    Fold 4:
      Train: index=[3 5 1 0]
      Test:  index=[2 4]
    >>> # Specify train and test size
    >>> rs = ShuffleSplit(n_splits=5, train_size=0.5, test_size=.25,
    Fold 0:
      Train: index=[1 3 0]
    Fold 1:
      Train: index=[4 0 2]
    Fold 2:
      Train: index=[1 2 4]
    Fold 3:
      Train: index=[3 4 1]
    Fold 4:
      Train: index=[3 5 1]
    """

    ):
            test_size=test_size,
            train_size=train_size,
        )

class GroupShuffleSplit(GroupsConsumerMixin, BaseShuffleSplit):
    """Shuffle-Group(s)-Out cross-validation iterator.

    Provides randomized train/test indices to split data according to a
    third-party provided group. This group information can be used to encode
    arbitrary domain specific stratifications of the samples as integers.

    The difference between :class:`LeavePGroupsOut` and ``GroupShuffleSplit`` is that
    the former generates splits using all subsets of size ``p`` unique groups,
    whereas ``GroupShuffleSplit`` generates a user-determined number of random
    test splits, each with a user-determined fraction of unique groups.

    For example, a less computationally intensive alternative to
    ``LeavePGroupsOut(p=10)`` would be
    ``GroupShuffleSplit(test_size=10, n_splits=100)``.

    Contrary to other cross-validation strategies, the random splits

    Note: The parameters ``test_size`` and ``train_size`` refer to groups, and
    not to samples as in :class:`ShuffleSplit`.

    Read more in the :ref:`User Guide <group_shuffle_split>`.

    test_size : float, int, default=None
        of groups to include in the test split (rounded up). If int,
        represents the absolute number of test groups. If None, the value is
        set to the complement of the train size. If ``train_size`` is also None,
        it will be set to 0.2.

        proportion of the groups to include in the train split. If
        int, represents the absolute number of train groups. If None,

    Examples
    --------
    >>> from sklearn.model_selection import GroupShuffleSplit
    >>> X = np.ones(shape=(8, 2))
    >>> y = np.ones(shape=(8, 1))
    >>> groups = np.array([1, 1, 2, 2, 2, 3, 3, 3])
    >>> print(groups.shape)
    (8,)
    >>> gss = GroupShuffleSplit(n_splits=2, train_size=.7, random_state=42)
    >>> gss.get_n_splits()
    2
    >>> print(gss)
    GroupShuffleSplit(n_splits=2, random_state=42, test_size=None, train_size=0.7)
    >>> for i, (train_index, test_index) in enumerate(gss.split(X, y, groups)):
    Fold 0:
      Train: index=[2 3 4 5 6 7], group=[2 2 2 3 3 3]
    Fold 1:
      Train: index=[0 1 5 6 7], group=[1 1 3 3 3]
      Test:  index=[2 3 4], group=[2 2 2]

    See Also
    --------
    ShuffleSplit : Shuffles samples to create independent test/train sets.

    LeavePGroupsOut : Train set leaves out all possible subsets of `p` groups.
    """

        self, n_splits=5, *, test_size=None, train_size=None, random_state=None
    ):
        )
        self._default_test_size = 0.2

    def _iter_indices(self, X, y, groups):
        classes, group_indices = np.unique(groups, return_inverse=True)
        for group_train, group_test in super()._iter_indices(X=classes):
            # these are the indices of classes in the partition
            # invert them into data indices

            train = np.flatnonzero(np.isin(group_indices, group_train))
            test = np.flatnonzero(np.isin(group_indices, group_test))

        Yields
        ------

        Notes
        -----
        """

class StratifiedShuffleSplit(BaseShuffleSplit):
    """Class-wise stratified ShuffleSplit cross-validator.

    This cross-validation object is a merge of :class:`StratifiedKFold` and
    :class:`ShuffleSplit`, which returns stratified randomized folds. The folds
    are made by preserving the percentage of samples for each class in `y` in a
    binary or multiclass classification setting.

    Note: like the :class:`ShuffleSplit` strategy, stratified random splits

    Read more in the :ref:`User Guide <stratified_shuffle_split>`.

    .. note::

    Examples
    --------
    >>> from sklearn.model_selection import StratifiedShuffleSplit
    >>> y = np.array([0, 0, 0, 1, 1, 1])
    >>> sss = StratifiedShuffleSplit(n_splits=5, test_size=0.5, random_state=0)
    >>> sss.get_n_splits(X, y)
    5
    >>> print(sss)
    StratifiedShuffleSplit(n_splits=5, random_state=0, ...)
    >>> for i, (train_index, test_index) in enumerate(sss.split(X, y)):
    Fold 0:
      Train: index=[5 2 3]
      Test:  index=[4 1 0]
    Fold 1:
      Train: index=[5 1 4]
      Test:  index=[0 2 3]
    Fold 2:
      Train: index=[5 0 2]
      Test:  index=[4 3 1]
    Fold 3:
      Train: index=[4 1 0]
      Test:  index=[2 3 5]
    Fold 4:
      Train: index=[0 5 1]
      Test:  index=[3 4 2]
    """

    ):
        )

    def _iter_indices(self, X, y, groups=None):
        )

        # Convert to numpy as not all operations are supported by the Array API.
        # `y` is probably never a very large array, which means that converting it
        # should be cheap
        xp, _ = get_namespace(y)
        y = _convert_to_numpy(y, xp=xp)

        if np.min(class_counts) < 2:
                "The least populated class in y has only 1"
                " member, which is too few. The minimum"
                " number of groups for any class cannot"
                " be less than 2."
            )

        if n_train < n_classes:
                "The train_size = %d should be greater or "
                "equal to the number of classes = %d" % (n_train, n_classes)
            )
        if n_test < n_classes:
                "The test_size = %d should be greater or "
                "equal to the number of classes = %d" % (n_test, n_classes)
            )

        )

        for _ in range(self.n_splits):
            # if there are ties in the class-counts, we want
            # to make sure to break them anew in each iteration
            n_i = _approximate_mode(class_counts, n_train, rng)
            class_counts_remaining = class_counts - n_i
            t_i = _approximate_mode(class_counts_remaining, n_test, rng)

            train = []
            test = []

                permutation = rng.permutation(class_counts[i])
                perm_indices_class_i = class_indices[i].take(permutation, mode="clip")

                train.extend(perm_indices_class_i[: n_i[i]])
                test.extend(perm_indices_class_i[n_i[i] : n_i[i] + t_i[i]])

            train = rng.permutation(train)
            test = rng.permutation(test)

        y : array-like of shape (n_samples,) or (n_samples, n_labels)

        Yields
        ------

        Notes
        -----
        """
            )

def _validate_shuffle_split(n_samples, test_size, train_size, default_test_size=None):
    """
    Validation helper to check if the train/test sizes are meaningful w.r.t. the
    size of the data (n_samples).
    """
    if test_size is None and train_size is None:
        test_size = default_test_size

    test_size_type = np.asarray(test_size).dtype.kind
    train_size_type = np.asarray(train_size).dtype.kind

    if (test_size_type == "i" and (test_size >= n_samples or test_size <= 0)) or (
        test_size_type == "f" and (test_size <= 0 or test_size >= 1)
    ):
            "test_size={0} should be either positive and smaller"
            " than the number of samples {1} or a float in the "
            "(0, 1) range".format(test_size, n_samples)
        )

    if (train_size_type == "i" and (train_size >= n_samples or train_size <= 0)) or (
        train_size_type == "f" and (train_size <= 0 or train_size >= 1)
    ):
            "train_size={0} should be either positive and smaller"
            "(0, 1) range".format(train_size, n_samples)
        )

    if train_size is not None and train_size_type not in ("i", "f"):
        raise ValueError("Invalid value for train_size: {}".format(train_size))
    if test_size is not None and test_size_type not in ("i", "f"):
        raise ValueError("Invalid value for test_size: {}".format(test_size))

    if train_size_type == "f" and test_size_type == "f" and train_size + test_size > 1:
            "The sum of test_size and train_size = {}, should be in the (0, 1)"
            " range. Reduce test_size and/or train_size.".format(train_size + test_size)
        )

    if test_size_type == "f":
        n_test = ceil(test_size * n_samples)
    elif test_size_type == "i":
        n_test = float(test_size)

    if train_size_type == "f":
        n_train = floor(train_size * n_samples)
    elif train_size_type == "i":
        n_train = float(train_size)

    if train_size is None:
        n_train = n_samples - n_test
    elif test_size is None:
        n_test = n_samples - n_train

    if n_train + n_test > n_samples:
            "The sum of train_size and test_size = %d, "
            "should be smaller than the number of "
            "samples %d. Reduce test_size and/or "
            "train_size." % (n_train + n_test, n_samples)
        )

    n_train, n_test = int(n_train), int(n_test)

    if n_train == 0:
            "With n_samples={}, test_size={} and train_size={}, the "
            "resulting train set will be empty. Adjust any of the "
            "aforementioned parameters.".format(n_samples, test_size, train_size)
        )

    return n_train, n_test

class PredefinedSplit(BaseCrossValidator):
    """Predefined split cross-validator.

    Provides train/test indices to split data into train/test sets using a
    predefined scheme specified by the user with the ``test_fold`` parameter.

    Read more in the :ref:`User Guide <predefined_split>`.

    test_fold : array-like of shape (n_samples,)
        The entry ``test_fold[i]`` represents the index of the test set that
        sample ``i`` belongs to. It is possible to exclude sample ``i`` from
        any test set (i.e. include sample ``i`` in every training set) by
        setting ``test_fold[i]`` equal to -1.

    Examples
    --------
    >>> from sklearn.model_selection import PredefinedSplit
    >>> test_fold = [0, 1, -1, 1]
    >>> ps = PredefinedSplit(test_fold)
    >>> ps.get_n_splits()
    2
    >>> print(ps)
    PredefinedSplit(test_fold=array([ 0,  1, -1,  1]))
    >>> for i, (train_index, test_index) in enumerate(ps.split()):
    Fold 0:
      Train: index=[1 2 3]
    Fold 1:
    """

    def __init__(self, test_fold):
        self.test_fold = np.array(test_fold, dtype=int)
        self.test_fold = column_or_1d(self.test_fold)
        self.unique_folds = np.unique(self.test_fold)
        self.unique_folds = self.unique_folds[self.unique_folds != -1]

    def split(self, X=None, y=None, groups=None):

        X : object

        y : object

        Yields
        ------

        """
            )
        return self._split()

    def _split(self):

        Yields
        ------

        """
        ind = np.arange(len(self.test_fold))
        for test_index in self._iter_test_masks():
            train_index = ind[np.logical_not(test_index)]
            test_index = ind[test_index]

    def _iter_test_masks(self):
        """Generates boolean masks corresponding to test sets."""
        for f in self.unique_folds:
            test_index = np.where(self.test_fold == f)[0]
            test_mask = np.zeros(len(self.test_fold), dtype=bool)

        X : object

        y : object

        Returns
        -------
        """
        return len(self.unique_folds)

class _CVIterableWrapper(BaseCrossValidator):
    """Wrapper class for old style cv objects and iterables."""

    def __init__(self, cv):
        self.cv = list(cv)

        X : object

        y : object

        Returns
        -------
        """
        return len(self.cv)

        X : object

        y : object

        Yields
        ------

        """
        for train, test in self.cv:

def check_cv(cv=5, y=None, *, classifier=False):
    """Input checker utility for building a cross-validator.

    cv : int, cross-validation generator, iterable or None, default=5

        For integer/None inputs, if classifier is True and ``y`` is either
        binary or multiclass, :class:`StratifiedKFold` is used. In all other
        cases, :class:`KFold` is used.

            ``cv`` default value changed from 3-fold to 5-fold.

    y : array-like, default=None
        The target variable for supervised learning problems.

    classifier : bool, default=False
        Whether the task is a classification task, in which case
        stratified KFold will be used.

    Returns
    -------
    checked_cv : a cross-validator instance.
        The return value is a cross-validator which generates the train/test
        splits via the ``split`` method.

    Examples
    --------
    >>> from sklearn.model_selection import check_cv
    >>> check_cv(cv=5, y=None, classifier=False)
    KFold(...)
    >>> check_cv(cv=5, y=[1, 1, 0, 0, 0, 0], classifier=True)
    StratifiedKFold(...)
    """
    cv = 5 if cv is None else cv
    if isinstance(cv, numbers.Integral):
        if (
            classifier
            and (y is not None)
            and (type_of_target(y, input_name="y") in ("binary", "multiclass"))
        ):
            return StratifiedKFold(cv)
        else:
            return KFold(cv)

    if not hasattr(cv, "split") or isinstance(cv, str):
        if not isinstance(cv, Iterable) or isinstance(cv, str):
                "Expected cv as an integer, cross-validation "
                "object (from sklearn.model_selection) "
                "or an iterable. Got %s." % cv
            )
        return _CVIterableWrapper(cv)

    return cv  # New style cv objects are passed without any modification

def train_test_split(
    *arrays,
    test_size=None,
    train_size=None,
):
    """Split arrays or matrices into random train and test subsets.

    Quick utility that wraps input validation,
    ``next(ShuffleSplit().split(X, y))``, and application to input data
    into a single call for splitting (and optionally subsampling) data into a
    one-liner.

    *arrays : sequence of indexables with same length / shape[0]
        Allowed inputs are lists, numpy arrays, scipy-sparse
        matrices or pandas dataframes.

        be set to 0.25.

        Controls the shuffling applied to the data before applying the split.

        Whether or not to shuffle the data before splitting. If shuffle=False
        then stratify must be None.

    stratify : array-like, default=None
        Read more in the :ref:`User Guide <stratification>`.

    Returns
    -------
    splitting : list, length=2 * len(arrays)
        List containing train-test split of inputs.

        .. versionadded:: 0.16
            If the input is sparse, the output will be a
            ``scipy.sparse.csr_matrix``. Else, output type is the same as the
            input type.

    Examples
    --------
    >>> X, y = np.arange(10).reshape((5, 2)), range(5)
    >>> X
           [2, 3],
           [4, 5],
           [6, 7],
           [8, 9]])
    >>> list(y)
    [0, 1, 2, 3, 4]

    ...     X, y, test_size=0.33, random_state=42)
    ...
    >>> X_train
    array([[4, 5],
           [0, 1],
           [6, 7]])
    >>> y_train
    [2, 0, 3]
    >>> X_test
    array([[2, 3],
           [8, 9]])
    >>> y_test
    [1, 4]

    >>> train_test_split(y, shuffle=False)
    [[0, 1, 2], [3, 4]]

    >>> iris = datasets.load_iris(as_frame=True)
    >>> X, y = iris['data'], iris['target']
    >>> X.head()
        sepal length (cm)  sepal width (cm)  petal length (cm)  petal width (cm)
    0                5.1               3.5                1.4               0.2
    1                4.9               3.0                1.4               0.2
    2                4.7               3.2                1.3               0.2
    3                4.6               3.1                1.5               0.2
    4                5.0               3.6                1.4               0.2
    >>> y.head()
    0    0
    1    0
    2    0
    3    0
    4    0
    ...

    ... X, y, test_size=0.33, random_state=42)
    ...
    >>> X_train.head()
    96                 5.7               2.9                4.2               1.3
    105                7.6               3.0                6.6               2.1
    66                 5.6               3.0                4.5               1.5
    0                  5.1               3.5                1.4               0.2
    122                7.7               2.8                6.7               2.0
    >>> y_train.head()
    96     1
    105    2
    66     1
    0      0
    122    2
    ...
    >>> X_test.head()
    73                 6.1               2.8                4.7               1.2
    18                 5.7               3.8                1.7               0.3
    118                7.7               2.6                6.9               2.3
    78                 6.0               2.9                4.5               1.5
    76                 6.8               2.8                4.8               1.4
    >>> y_test.head()
    73     1
    18     0
    118    2
    78     1
    76     1
    ...
    """
    n_arrays = len(arrays)
    if n_arrays == 0:
        raise ValueError("At least one array required as input")

    arrays = indexable(*arrays)

    n_samples = _num_samples(arrays[0])
    n_train, n_test = _validate_shuffle_split(
        n_samples, test_size, train_size, default_test_size=0.25
    )

    if shuffle is False:
        if stratify is not None:
                "Stratified train/test split is not implemented for shuffle=False"
            )

        train = np.arange(n_train)
        test = np.arange(n_train, n_train + n_test)

    else:
            CVClass = StratifiedShuffleSplit
        else:
            CVClass = ShuffleSplit

        cv = CVClass(test_size=n_test, train_size=n_train, random_state=random_state)

        train, test = next(cv.split(X=arrays[0], y=stratify))

    train, test = ensure_common_namespace_device(arrays[0], train, test)

    return list(
        chain.from_iterable(
            (_safe_indexing(a, train), _safe_indexing(a, test)) for a in arrays
        )
    )

def _pprint(params, offset=0, printer=repr):
    """Pretty print the dictionary 'params'

        The dictionary to pretty print

        The offset in characters to add at the begin of each line.

    printer : callable, default=repr
        The function to convert entries to strings, typically
        the builtin str or repr

    """
    # Do a multi-line justified repr:
    options = np.get_printoptions()
    np.set_printoptions(precision=5, threshold=64, edgeitems=2)
    params_list = list()
    this_line_length = offset
    line_sep = ",\n" + (1 + offset // 2) * " "
    for i, (k, v) in enumerate(sorted(params.items())):
        if isinstance(v, float):
            # use str for representing floating point numbers
            # this way we get consistent representation across
            # architectures and versions.
            this_repr = "%s=%s" % (k, str(v))
        else:
            # use repr of the rest
            this_repr = "%s=%s" % (k, printer(v))
        if len(this_repr) > 500:
            this_repr = this_repr[:300] + "..." + this_repr[-100:]
        if i > 0:
            if this_line_length + len(this_repr) >= 75 or "\n" in this_repr:
                params_list.append(line_sep)
                this_line_length = len(line_sep)
            else:
                params_list.append(", ")
                this_line_length += 2
        params_list.append(this_repr)
        this_line_length += len(this_repr)

    np.set_printoptions(**options)
    lines = "".join(params_list)
    # Strip trailing space to avoid nightmare in doctests
    lines = "\n".join(l.rstrip(" ") for l in lines.split("\n"))
    return lines

def _build_repr(self):
    # XXX This is copied from BaseEstimator's get_params
    cls = self.__class__
    init = getattr(cls.__init__, "deprecated_original", cls.__init__)
    # Ignore varargs, kw and default values and pop self
    init_signature = signature(init)
    # Consider the constructor parameters excluding 'self'
    if init is object.__init__:
        args = []
    else:
        args = sorted(
            [
                p.name
                for p in init_signature.parameters.values()
                if p.name != "self" and p.kind != p.VAR_KEYWORD
            ]
        )
    class_name = self.__class__.__name__
    params = dict()
    for key in args:
        # We need deprecation warnings to always be on in order to
        # catch deprecated param values.
        # This is set in utils/__init__.py but it gets overwritten
        # when running under python3 somehow.
        warnings.simplefilter("always", FutureWarning)
        try:
                value = getattr(self, key, None)
                if value is None and hasattr(self, "cvargs"):
                    value = self.cvargs.get(key, None)
            if len(w) and w[0].category is FutureWarning:
                # if the parameter is deprecated, don't show it
                continue
        finally:
            warnings.filters.pop(0)
        params[key] = value

    return "%s(%s)" % (class_name, _pprint(params, offset=len(class_name)))

def _yields_constant_splits(cv):
    # Return True if calling cv.split() always returns the same splits
    # We assume that if a cv doesn't have a shuffle parameter, it shuffles by
    # default (e.g. ShuffleSplit). If it actually doesn't shuffle (e.g.
    # LeaveOneOut), then it won't have a random_state parameter anyway, in
    # which case it will default to 0, leading to output=True
    shuffle = getattr(cv, "shuffle", True)
    random_state = getattr(cv, "random_state", 0)
    return isinstance(random_state, numbers.Integral) or not shuffle

        Yields
        ------

        """
            )

        Yields
        ------

        """

        """

            )

        y : object

        Returns
        -------
        """

                )
            )

        y : object

        """

            )

            )

                (
                ),
            )

        Yields
        ------

        """
                (
            )

        X : object

        y : object

        Returns
        -------
        """

        current = 0

            )

        else:

        Yields
        ------

        """

        else:
                )
            )

            )
            )

            [
            ]
        )

        Yields
        ------

        Notes
        -----
        """
            )

                )
            )

            )
            )

        )

        )

            )

                idx
            ]

            )

        Yields
        ------

        """
            )

        Yields
        ------

        """
        )

            )
            )

                yield (
                )
            else:
                yield (
                )

        )
            )

        X : object

        y : object

            omitted.

        Returns
        -------
        """

        )
            )

        X : object

        y : object

            omitted.

        Returns
        -------
        """

        Yields
        ------

        """

        X : object

        y : object

        Returns
        -------
        """

        )

        )

        Yields
        ------

        Notes
        -----
        """

    ):

        Yields
        ------

        Notes
        -----
        """

        )

    ):
        )

    ):
        )

        Yields
        ------

        Notes
        -----
        """

        )

            )

            )
            )

        )

            train = []
            test = []

        Yields
        ------

        Notes
        -----
        """
            )

        X : object

        y : object

        Yields
        ------

        """
            )

        Yields
        ------

        """

        X : object

        y : object

        Returns
        -------
        """

        X : object

        y : object

        Returns
        -------
        """

        X : object

        y : object

        Yields
        ------

        """

class OneTimeSplitter:
    """A wrapper to make KFold single entry cv iterator"""

    def __init__(self, n_splits=4, n_samples=99):
        self.indices = iter(KFold(n_splits=n_splits).split(np.ones(n_samples)))

        """Split can be called only once"""
        for index in self.indices:
            yield index

def _split(splitter, X, y, groups):
    if splitter.__class__.__name__ in GROUP_SPLITTER_NAMES:
        return splitter.split(X, y, groups=groups)
    else:
        return splitter.split(X, y)

def test_cross_validator_with_default_params():
    n_unique_groups = 4
    n_splits = 2
    p = 2
    n_shuffle_splits = 10  # (the default value)

    X_1d = np.array([1, 2, 3, 4])
    groups = np.array([1, 2, 3, 4])
    loo = LeaveOneOut()
    lpo = LeavePOut(p)
    kf = KFold(n_splits)
    skf = StratifiedKFold(n_splits)
    lolo = LeaveOneGroupOut()
    lopo = LeavePGroupsOut(p)
    ss = ShuffleSplit(random_state=0)
    ps = PredefinedSplit([1, 1, 2, 2])  # n_splits = np of unique folds = 2
    sgkf = StratifiedGroupKFold(n_splits)

    loo_repr = "LeaveOneOut()"
    lpo_repr = "LeavePOut(p=2)"
    kf_repr = "KFold(n_splits=2, random_state=None, shuffle=False)"
    skf_repr = "StratifiedKFold(n_splits=2, random_state=None, shuffle=False)"
    lolo_repr = "LeaveOneGroupOut()"
    lopo_repr = "LeavePGroupsOut(n_groups=2)"
    ss_repr = (
        "ShuffleSplit(n_splits=10, random_state=0, test_size=None, train_size=None)"
    )
    ps_repr = "PredefinedSplit(test_fold=array([1, 1, 2, 2]))"
    sgkf_repr = "StratifiedGroupKFold(n_splits=2, random_state=None, shuffle=False)"

    n_splits_expected = [
        comb(n_samples, p),
        n_splits,
        n_splits,
        n_unique_groups,
        comb(n_unique_groups, p),
        n_shuffle_splits,
        2,
        n_splits,
    ]

    for i, (cv, cv_repr) in enumerate(
        zip(
            [loo, lpo, kf, skf, lolo, lopo, ss, ps, sgkf],
            [
                loo_repr,
                lpo_repr,
                kf_repr,
                skf_repr,
                lolo_repr,
                lopo_repr,
                ss_repr,
                ps_repr,
                sgkf_repr,
            ],
        )
    ):
        # Test if get_n_splits works correctly
        assert n_splits_expected[i] == cv.get_n_splits(X, y, groups)

        # Test if the cross-validator works as expected even if
        # the data is 1d
        np.testing.assert_equal(
            list(_split(cv, X, y, groups)), list(_split(cv, X_1d, y, groups))
        )
        # Test that train, test indices returned are integers
        for train, test in _split(cv, X, y, groups):
            assert np.asarray(train).dtype.kind == "i"
            assert np.asarray(test).dtype.kind == "i"

        # Test if the repr works without any errors
        assert cv_repr == repr(cv)

    # ValueError for get_n_splits methods
    msg = "The 'X' parameter should not be None."
        loo.get_n_splits(None, y, groups)
        lpo.get_n_splits(None, y, groups)

def test_2d_y():
    # smoke test for 2d y and multi-label
    X = rng.randint(0, 3, size=(n_samples, 2))
    y = rng.randint(0, 3, size=(n_samples,))
    y_2d = y.reshape(-1, 1)
    y_multilabel = rng.randint(0, 2, size=(n_samples, 3))
    groups = rng.randint(0, 3, size=(n_samples,))
    splitters = [
        LeaveOneOut(),
        LeavePOut(p=2),
        KFold(),
        StratifiedKFold(),
        RepeatedKFold(),
        RepeatedStratifiedKFold(),
        StratifiedGroupKFold(),
        ShuffleSplit(),
        StratifiedShuffleSplit(test_size=0.5),
        GroupShuffleSplit(),
        LeaveOneGroupOut(),
        LeavePGroupsOut(n_groups=2),
        GroupKFold(n_splits=3),
        TimeSeriesSplit(),
        PredefinedSplit(test_fold=groups),
    ]
    for splitter in splitters:
        list(_split(splitter, X, y, groups=groups))
        list(_split(splitter, X, y_2d, groups=groups))
        try:
            list(_split(splitter, X, y_multilabel, groups=groups))
            allowed_target_types = ("binary", "multiclass")
            msg = "Supported target types are: {}. Got 'multilabel".format(
                allowed_target_types
            )
            assert msg in str(e)

def check_valid_split(train, test, n_samples=None):
    # Use python sets to get more informative assertion failure messages
    train, test = set(train), set(test)

    # Train and test split should not overlap
    assert train.intersection(test) == set()

        # Check that the union of train an test split cover all the indices
        assert train.union(test) == set(range(n_samples))

def check_cv_coverage(cv, X, y, groups, expected_n_splits):
    # Check that a all the samples appear at least once in a test fold
    assert cv.get_n_splits(X, y, groups) == expected_n_splits

    collected_test_samples = set()
    iterations = 0
    for train, test in cv.split(X, y, groups):
        check_valid_split(train, test, n_samples=n_samples)
        iterations += 1
        collected_test_samples.update(test)

    # Check that the accumulated test samples cover the whole dataset
    assert iterations == expected_n_splits
        assert collected_test_samples == set(range(n_samples))

def test_kfold_valueerrors():
    X1 = np.array([[1, 2], [3, 4], [5, 6]])
    X2 = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]])
    # Check that errors are raised if there is not enough samples
    (ValueError, next, KFold(4).split(X1))

    # Check that a warning is raised if the least populated class has too few
    # members.
    y = np.array([3, 3, -1, -1, 3])

    skf_3 = StratifiedKFold(3)
    with pytest.warns(Warning, match="The least populated class"):
        next(skf_3.split(X2, y))

    sgkf_3 = StratifiedGroupKFold(3)
    naive_groups = np.arange(len(y))
        next(sgkf_3.split(X2, y, naive_groups))

    # Check that despite the warning the folds are still computed even
    # though all the classes are not necessarily represented at on each
    # side of the split at each split
        warnings.simplefilter("ignore")
        check_cv_coverage(skf_3, X2, y, groups=None, expected_n_splits=3)

        check_cv_coverage(sgkf_3, X2, y, groups=naive_groups, expected_n_splits=3)

    # Check that errors are raised if all n_groups for individual
    # classes are less than n_splits.
    y = np.array([3, 3, -1, -1, 2])

        next(sgkf_3.split(X2, y))

    # Error when number of folds is <= 1
        KFold(0)
        KFold(1)
    error_string = "k-fold cross-validation requires at least one train/test split"
    with pytest.raises(ValueError, match=error_string):
        StratifiedKFold(0)
        StratifiedKFold(1)
        StratifiedGroupKFold(0)
        StratifiedGroupKFold(1)

    # When n_splits is not integer:
        KFold(1.5)
        KFold(2.0)
        StratifiedKFold(1.5)
        StratifiedKFold(2.0)
        StratifiedGroupKFold(1.5)
        StratifiedGroupKFold(2.0)

    # When shuffle is not  a bool:
        KFold(n_splits=4, shuffle=None)

def test_kfold_indices():
    # Check all indices are returned in the test folds
    X1 = np.ones(18)
    kf = KFold(3)
    check_cv_coverage(kf, X1, y=None, groups=None, expected_n_splits=3)

    # Check all indices are returned in the test folds even when equal-sized
    # folds are not possible
    X2 = np.ones(17)
    check_cv_coverage(kf, X2, y=None, groups=None, expected_n_splits=3)

    # Check if get_n_splits returns the number of folds
    assert 5 == KFold(5).get_n_splits(X2)

def test_kfold_no_shuffle():
    # Manually check that KFold preserves the data ordering on toy datasets
    X2 = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]

    splits = KFold(2).split(X2[:-1])
    train, test = next(splits)
    assert_array_equal(test, [0, 1])
    assert_array_equal(train, [2, 3])

    assert_array_equal(test, [2, 3])
    assert_array_equal(train, [0, 1])

    splits = KFold(2).split(X2)
    assert_array_equal(test, [0, 1, 2])
    assert_array_equal(train, [3, 4])

    assert_array_equal(test, [3, 4])
    assert_array_equal(train, [0, 1, 2])

def test_stratified_kfold_no_shuffle():
    # Manually check that StratifiedKFold preserves the data ordering as much
    # as possible on toy datasets in order to avoid hiding sample dependencies
    # when possible
    X, y = np.ones(4), [1, 1, 0, 0]
    splits = StratifiedKFold(2).split(X, y)
    assert_array_equal(test, [0, 2])
    assert_array_equal(train, [1, 3])

    assert_array_equal(test, [1, 3])
    assert_array_equal(train, [0, 2])

    X, y = np.ones(7), [1, 1, 1, 0, 0, 0, 0]
    assert_array_equal(test, [0, 1, 3, 4])
    assert_array_equal(train, [2, 5, 6])

    assert_array_equal(test, [2, 5, 6])
    assert_array_equal(train, [0, 1, 3, 4])

    assert 5 == StratifiedKFold(5).get_n_splits(X, y)

    # Make sure string labels are also supported
    X = np.ones(7)
    y1 = ["1", "1", "1", "0", "0", "0", "0"]
    y2 = [1, 1, 1, 0, 0, 0, 0]
    np.testing.assert_equal(
        list(StratifiedKFold(2).split(X, y1)), list(StratifiedKFold(2).split(X, y2))
    )

    # Check equivalence to KFold
    y = [0, 1, 0, 1, 0, 1, 0, 1]
    X = np.ones_like(y)
        list(StratifiedKFold(3).split(X, y)), list(KFold(3).split(X, y))
    )

def test_stratified_kfold_ratios(k, shuffle, kfold):
    # Check that stratified kfold preserves class ratios in individual splits
    # Repeat with shuffling turned off and on
    X = np.ones(n_samples)
        [4] * int(0.10 * n_samples)
        + [0] * int(0.89 * n_samples)
        + [1] * int(0.01 * n_samples)
    )
    # ensure perfect stratification with StratifiedGroupKFold
    groups = np.arange(len(y))
    distr = np.bincount(y) / len(y)

    test_sizes = []
    random_state = None if not shuffle else 0
    skf = kfold(k, random_state=random_state, shuffle=shuffle)
    for train, test in _split(skf, X, y, groups=groups):
        assert_allclose(np.bincount(y[train]) / len(train), distr, atol=0.02)
        assert_allclose(np.bincount(y[test]) / len(test), distr, atol=0.02)
        test_sizes.append(len(test))
    assert np.ptp(test_sizes) <= 1

def test_stratified_kfold_label_invariance(k, shuffle, kfold):
    # Check that stratified kfold gives the same indices regardless of labels
        [2] * int(0.10 * n_samples)
    )
    X = np.ones(len(y))

    def get_splits(y):
        random_state = None if not shuffle else 0
        return [
            (list(train), list(test))
            for train, test in _split(
                kfold(k, random_state=random_state, shuffle=shuffle),
                X,
                y,
                groups=groups,
            )
        ]

    splits_base = get_splits(y)
    for perm in permutations([0, 1, 2]):
        y_perm = np.take(perm, y)
        splits_perm = get_splits(y_perm)
        assert splits_perm == splits_base

def test_kfold_balance():
    # Check that KFold returns folds with balanced sizes
    for i in range(11, 17):
        kf = KFold(5).split(X=np.ones(i))
        sizes = [len(test) for _, test in kf]

        assert (np.max(sizes) - np.min(sizes)) <= 1
        assert np.sum(sizes) == i

def test_stratifiedkfold_balance(kfold):
    # Check that KFold returns folds with balanced sizes (only when
    # stratification is possible)
    X = np.ones(17)
    y = [0] * 3 + [1] * 14

    for shuffle in (True, False):
        cv = kfold(3, shuffle=shuffle)
        for i in range(11, 17):
            skf = _split(cv, X[:i], y[:i], groups[:i])
            sizes = [len(test) for _, test in skf]

            assert (np.max(sizes) - np.min(sizes)) <= 1
            assert np.sum(sizes) == i

def test_shuffle_kfold():
    # Check the indices are shuffled properly
    kf2 = KFold(3, shuffle=True, random_state=0)
    kf3 = KFold(3, shuffle=True, random_state=1)

    X = np.ones(300)

    all_folds = np.zeros(300)
    for (tr1, te1), (tr2, te2), (tr3, te3) in zip(
        kf.split(X), kf2.split(X), kf3.split(X)
    ):
        for tr_a, tr_b in combinations((tr1, tr2, tr3), 2):
            # Assert that there is no complete overlap
            assert len(np.intersect1d(tr_a, tr_b)) != len(tr1)

        # Set all test indices in successive iterations of kf2 to 1
        all_folds[te2] = 1

    # Check that all indices are returned in the different test folds
    assert sum(all_folds) == 300

def test_shuffle_kfold_stratifiedkfold_reproducibility(kfold):
    X = np.ones(15)  # Divisible by 3
    y = [0] * 7 + [1] * 8
    groups_1 = np.arange(len(y))
    X2 = np.ones(16)  # Not divisible by 3
    y2 = [0] * 8 + [1] * 8
    groups_2 = np.arange(len(y2))

    # Check that when the shuffle is True, multiple split calls produce the
    # same split when random_state is int
    kf = kfold(3, shuffle=True, random_state=0)

        list(_split(kf, X, y, groups_1)), list(_split(kf, X, y, groups_1))
    )

    # Check that when the shuffle is True, multiple split calls often
    # (not always) produce different splits when random_state is
    # RandomState instance or None
    kf = kfold(3, shuffle=True, random_state=np.random.RandomState(0))
    for data in zip((X, X2), (y, y2), (groups_1, groups_2)):
        # Test if the two splits are different cv
        for (_, test_a), (_, test_b) in zip(_split(kf, *data), _split(kf, *data)):
            # cv.split(...) returns an array of tuples, each tuple
            # consisting of an array with train indices and test indices
            # Ensure that the splits for data are not same
            # when random state is not set
            with pytest.raises(AssertionError):
                np.testing.assert_array_equal(test_a, test_b)

def test_shuffle_stratifiedkfold():
    # Check that shuffling is happening when requested, and for proper
    # sample coverage
    X_40 = np.ones(40)
    y = [0] * 20 + [1] * 20
    kf0 = StratifiedKFold(5, shuffle=True, random_state=0)
    kf1 = StratifiedKFold(5, shuffle=True, random_state=1)
    for (_, test0), (_, test1) in zip(kf0.split(X_40, y), kf1.split(X_40, y)):
        assert set(test0) != set(test1)
    check_cv_coverage(kf0, X_40, y, groups=None, expected_n_splits=5)

    # Ensure that we shuffle each class's samples with different
    # random_state in StratifiedKFold
    # See https://github.com/scikit-learn/scikit-learn/pull/13124
    y = [0] * 5 + [1] * 5
    kf1 = StratifiedKFold(5, shuffle=True, random_state=0)
    kf2 = StratifiedKFold(5, shuffle=True, random_state=1)
    test_set1 = sorted([tuple(s[1]) for s in kf1.split(X, y)])
    test_set2 = sorted([tuple(s[1]) for s in kf2.split(X, y)])
    assert test_set1 != test_set2

def test_shuffle_groupkfold():
    X = np.ones(40)
    groups = np.arange(40) // 3
    gkf0 = GroupKFold(4, shuffle=True, random_state=0)
    gkf1 = GroupKFold(4, shuffle=True, random_state=1)

    # Check that the groups are shuffled differently
    test_groups0 = [
        set(groups[test_idx]) for _, test_idx in gkf0.split(X, None, groups)
    ]
    test_groups1 = [
        set(groups[test_idx]) for _, test_idx in gkf1.split(X, None, groups)
    ]
    for g0, g1 in zip(test_groups0, test_groups1):
        assert g0 != g1, "Test groups should differ with different random states"

    # Check coverage and splits
    check_cv_coverage(gkf0, X, y, groups, expected_n_splits=4)
    check_cv_coverage(gkf1, X, y, groups, expected_n_splits=4)

def test_kfold_can_detect_dependent_samples_on_digits():  # see #2372
    # The digits samples are dependent: they are apparently grouped by authors
    # although we don't have any information on the groups segment locations
    # for this data. We can highlight this fact by computing k-fold cross-
    # validation with and without shuffling: we observe that the shuffling case
    # wrongly makes the IID assumption and is therefore too optimistic: it
    # estimates a much higher accuracy (around 0.93) than that the non
    # shuffling variant (around 0.81).

    X, y = digits.data[:600], digits.target[:600]
    model = SVC(C=10, gamma=0.005)

    n_splits = 3

    cv = KFold(n_splits=n_splits, shuffle=False)
    mean_score = cross_val_score(model, X, y, cv=cv).mean()
    assert 0.92 > mean_score
    assert mean_score > 0.80

    # Shuffling the data artificially breaks the dependency and hides the
    # overfitting of the model with regards to the writing style of the authors
    # by yielding a seriously overestimated score:

    cv = KFold(n_splits, shuffle=True, random_state=0)
    assert mean_score > 0.92

    cv = KFold(n_splits, shuffle=True, random_state=1)

    # Similarly, StratifiedKFold should try to shuffle the data as little
    # as possible (while respecting the balanced class constraints)
    # and thus be able to detect the dependency by not overestimating
    # the CV score either. As the digits dataset is approximately balanced
    # the estimated mean score is close to the score measured with
    # non-shuffled KFold

    cv = StratifiedKFold(n_splits)
    assert 0.94 > mean_score

def test_stratified_group_kfold_trivial():
    sgkf = StratifiedGroupKFold(n_splits=3)
    # Trivial example - groups with the same distribution
    y = np.array([1] * 6 + [0] * 12)
    X = np.ones_like(y).reshape(-1, 1)
    groups = np.asarray((1, 2, 3, 4, 5, 6, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6))
    for train, test in sgkf.split(X, y, groups):
        # check group constraint
        assert np.intersect1d(groups[train], groups[test]).size == 0
        # check y distribution

def test_stratified_group_kfold_approximate():
    # Not perfect stratification (even though it is possible) because of
    # iteration over groups
    groups = np.array([1, 2, 3, 3, 4, 4, 1, 1, 2, 2, 3, 4, 5, 5, 5, 6, 6, 6])
    expected = np.asarray([[0.833, 0.166], [0.666, 0.333], [0.5, 0.5]])
    for (train, test), expect_dist in zip(sgkf.split(X, y, groups), expected):
        split_dist = np.bincount(y[test]) / len(test)
        assert_allclose(split_dist, expect_dist, atol=0.001)

def test_stratified_group_kfold_homogeneous_groups(y, groups, expected):

def test_stratified_group_kfold_against_group_kfold(cls_distr, n_groups):
    # Check that given sufficient amount of samples StratifiedGroupKFold
    # produces better stratified folds than regular GroupKFold
    n_splits = 5
    sgkf = StratifiedGroupKFold(n_splits=n_splits)
    gkf = GroupKFold(n_splits=n_splits)
    n_points = 1000
    y = rng.choice(2, size=n_points, p=cls_distr)
    g = rng.choice(n_groups, n_points)
    sgkf_folds = sgkf.split(X, y, groups=g)
    gkf_folds = gkf.split(X, y, groups=g)
    sgkf_entr = 0
    gkf_entr = 0
    for (sgkf_train, sgkf_test), (_, gkf_test) in zip(sgkf_folds, gkf_folds):
        assert np.intersect1d(g[sgkf_train], g[sgkf_test]).size == 0
        sgkf_distr = np.bincount(y[sgkf_test]) / len(sgkf_test)
        gkf_distr = np.bincount(y[gkf_test]) / len(gkf_test)
        sgkf_entr += stats.entropy(sgkf_distr, qk=cls_distr)
        gkf_entr += stats.entropy(gkf_distr, qk=cls_distr)
    sgkf_entr /= n_splits
    gkf_entr /= n_splits
    assert sgkf_entr <= gkf_entr

def test_shuffle_split():
    ss1 = ShuffleSplit(test_size=0.2, random_state=0).split(X)
    ss2 = ShuffleSplit(test_size=2, random_state=0).split(X)
    ss3 = ShuffleSplit(test_size=np.int32(2), random_state=0).split(X)
    ss4 = ShuffleSplit(test_size=2, random_state=0).split(X)
    for t1, t2, t3, t4 in zip(ss1, ss2, ss3, ss4):
        assert_array_equal(t1[0], t2[0])
        assert_array_equal(t2[0], t3[0])
        assert_array_equal(t3[0], t4[0])
        assert_array_equal(t1[1], t2[1])
        assert_array_equal(t2[1], t3[1])
        assert_array_equal(t3[1], t4[1])

def test_shuffle_split_default_test_size(split_class, train_size, exp_train, exp_test):
    # Check that the default value has the expected behavior, i.e. 0.1 if both
    # unspecified or complement train_size unless both are specified.
    X = np.ones(10)

    X_train, X_test = next(split_class(train_size=train_size).split(X, y))

    assert len(X_train) == exp_train
    assert len(X_test) == exp_test

def test_group_shuffle_split_default_test_size(train_size, exp_train, exp_test):
    # Check that the default value has the expected behavior, i.e. 0.2 if both
    groups = range(10)

    X_train, X_test = next(GroupShuffleSplit(train_size=train_size).split(X, y, groups))

def test_stratified_shuffle_split_init():
    X = np.arange(7)
    y = np.asarray([0, 1, 1, 1, 2, 2, 2])
    # Check that error is raised if there is a class with only one sample
        next(StratifiedShuffleSplit(3, test_size=0.2).split(X, y))

    # Check that error is raised if the test set size is smaller than n_classes
        next(StratifiedShuffleSplit(3, test_size=2).split(X, y))
    # Check that error is raised if the train set size is smaller than
    # n_classes
        next(StratifiedShuffleSplit(3, test_size=3, train_size=2).split(X, y))

    X = np.arange(9)
    y = np.asarray([0, 0, 0, 1, 1, 1, 2, 2, 2])

    # Train size or test size too small
        next(StratifiedShuffleSplit(train_size=2).split(X, y))
        next(StratifiedShuffleSplit(test_size=2).split(X, y))

def test_stratified_shuffle_split_respects_test_size():
    y = np.array([0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2])
    test_size = 5
    train_size = 10
    sss = StratifiedShuffleSplit(
        6, test_size=test_size, train_size=train_size, random_state=0
    ).split(np.ones(len(y)), y)
    for train, test in sss:
        assert len(train) == train_size
        assert len(test) == test_size

def test_stratified_shuffle_split_iter():
    ys = [
        np.array([1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3]),
        np.array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]),
        np.array([0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2] * 2),
        np.array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4]),
        np.array([-1] * 800 + [1] * 50),
        np.concatenate([[i] * (100 + i) for i in range(11)]),
        [1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3],
        ["1", "1", "1", "1", "2", "2", "2", "3", "3", "3", "3", "3"],
    ]

    for y in ys:
        sss = StratifiedShuffleSplit(6, test_size=0.33, random_state=0).split(
            np.ones(len(y)), y
        )
        y = np.asanyarray(y)  # To make it indexable for y[train]
        # this is how test-size is computed internally
        # in _validate_shuffle_split
        test_size = np.ceil(0.33 * len(y))
        train_size = len(y) - test_size
        for train, test in sss:
            assert_array_equal(np.unique(y[train]), np.unique(y[test]))
            # Checks if folds keep classes proportions
            p_train = np.bincount(np.unique(y[train], return_inverse=True)[1]) / float(
                len(y[train])
            )
            p_test = np.bincount(np.unique(y[test], return_inverse=True)[1]) / float(
                len(y[test])
            )
            assert_array_almost_equal(p_train, p_test, 1)
            assert len(train) + len(test) == y.size
            assert len(train) == train_size
            assert len(test) == test_size
            assert_array_equal(np.intersect1d(train, test), [])

def test_stratified_shuffle_split_even():
    # Test the StratifiedShuffleSplit, indices are drawn with a
    # equal chance
    n_folds = 5
    n_splits = 1000

    def assert_counts_are_ok(idx_counts, p):
        # Here we test that the distribution of the counts
        # per index is close enough to a binomial
        threshold = 0.05 / n_splits
        bf = stats.binom(n_splits, p)
        for count in idx_counts:
            prob = bf.pmf(count)
            assert prob > threshold, (
                "An index is not drawn with chance corresponding to even draws"
            )

    for n_samples in (6, 22):
        groups = np.array((n_samples // 2) * [0, 1])
        splits = StratifiedShuffleSplit(
            n_splits=n_splits, test_size=1.0 / n_folds, random_state=0
        )

        train_counts = [0] * n_samples
        test_counts = [0] * n_samples
        n_splits_actual = 0
        for train, test in splits.split(X=np.ones(n_samples), y=groups):
            n_splits_actual += 1
            for counter, ids in [(train_counts, train), (test_counts, test)]:
                for id in ids:
                    counter[id] += 1
        assert n_splits_actual == n_splits

            n_samples, test_size=1.0 / n_folds, train_size=1.0 - (1.0 / n_folds)
        )

        assert len(train) == n_train
        assert len(test) == n_test
        assert len(set(train).intersection(test)) == 0

        group_counts = np.unique(groups)
        assert splits.test_size == 1.0 / n_folds
        assert n_train + n_test == len(groups)
        assert len(group_counts) == 2
        ex_test_p = float(n_test) / n_samples
        ex_train_p = float(n_train) / n_samples

        assert_counts_are_ok(train_counts, ex_train_p)
        assert_counts_are_ok(test_counts, ex_test_p)

def test_stratified_shuffle_split_overlap_train_test_bug():
    # See https://github.com/scikit-learn/scikit-learn/issues/6121 for
    # the original bug report
    y = [0, 1, 2, 3] * 3 + [4, 5] * 5

    sss = StratifiedShuffleSplit(n_splits=1, test_size=0.5, random_state=0)

    train, test = next(sss.split(X=X, y=y))

    # no overlap
    assert_array_equal(np.intersect1d(train, test), [])

    # complete partition
    assert_array_equal(np.union1d(train, test), np.arange(len(y)))

def test_stratified_shuffle_split_multilabel():
    # fix for issue 9037
    for y in [
        np.array([[0, 1], [1, 0], [1, 0], [0, 1]]),
        np.array([[0, 1], [1, 1], [1, 1], [0, 1]]),
    ]:
        X = np.ones_like(y)
        sss = StratifiedShuffleSplit(n_splits=1, test_size=0.5, random_state=0)
        train, test = next(sss.split(X=X, y=y))
        y_train = y[train]
        y_test = y[test]

        # no overlap
        assert_array_equal(np.intersect1d(train, test), [])

        # complete partition
        assert_array_equal(np.union1d(train, test), np.arange(len(y)))

        # correct stratification of entire rows
        # (by design, here y[:, 0] uniquely determines the entire row of y)
        expected_ratio = np.mean(y[:, 0])
        assert expected_ratio == np.mean(y_train[:, 0])
        assert expected_ratio == np.mean(y_test[:, 0])

def test_stratified_shuffle_split_multilabel_many_labels():
    # fix in PR #9922: for multilabel data with > 1000 labels, str(row)
    # truncates with an ellipsis for elements in positions 4 through
    # len(row) - 4, so labels were not being correctly split using the powerset
    # method for transforming a multilabel problem to a multiclass one; this
    # test checks that this problem is fixed.
    row_with_many_zeros = [1, 0, 1] + [0] * 1000 + [1, 0, 1]
    row_with_many_ones = [1, 0, 1] + [1] * 1000 + [1, 0, 1]
    y = np.array([row_with_many_zeros] * 10 + [row_with_many_ones] * 100)

    # correct stratification of entire rows
    # (by design, here y[:, 4] uniquely determines the entire row of y)
    expected_ratio = np.mean(y[:, 4])
    assert expected_ratio == np.mean(y_train[:, 4])
    assert expected_ratio == np.mean(y_test[:, 4])

def test_predefinedsplit_with_kfold_split():
    # Check that PredefinedSplit can reproduce a split generated by Kfold.
    folds = np.full(10, -1.0)
    kf_train = []
    kf_test = []
    for i, (train_ind, test_ind) in enumerate(KFold(5, shuffle=True).split(X)):
        kf_train.append(train_ind)
        kf_test.append(test_ind)
        folds[test_ind] = i
    ps = PredefinedSplit(folds)
    # n_splits is simply the no of unique folds
    assert len(np.unique(folds)) == ps.get_n_splits()
    ps_train, ps_test = zip(*ps.split())
    assert_array_equal(ps_train, kf_train)
    assert_array_equal(ps_test, kf_test)

def test_group_shuffle_split():
    for groups_i in test_groups:
        X = y = np.ones(len(groups_i))
        n_splits = 6
        test_size = 1.0 / 3
        slo = GroupShuffleSplit(n_splits, test_size=test_size, random_state=0)

        # Make sure the repr works
        repr(slo)

        # Test that the length is correct
        assert slo.get_n_splits(X, y, groups=groups_i) == n_splits

        l_unique = np.unique(groups_i)
        l = np.asarray(groups_i)

        for train, test in slo.split(X, y, groups=groups_i):
            # First test: no train group is in the test set and vice versa
            l_train_unique = np.unique(l[train])
            l_test_unique = np.unique(l[test])
            assert not np.any(np.isin(l[train], l_test_unique))
            assert not np.any(np.isin(l[test], l_train_unique))

            # Second test: train and test add up to all the data
            assert l[train].size + l[test].size == l.size

            # Third test: train and test are disjoint

            # Fourth test:
            # unique train and test groups are correct, +- 1 for rounding error
            assert abs(len(l_test_unique) - round(test_size * len(l_unique))) <= 1
            assert (
                abs(len(l_train_unique) - round((1.0 - test_size) * len(l_unique))) <= 1
            )

def test_leave_one_p_group_out():
    logo = LeaveOneGroupOut()
    lpgo_1 = LeavePGroupsOut(n_groups=1)
    lpgo_2 = LeavePGroupsOut(n_groups=2)

    # Make sure the repr works
    assert repr(logo) == "LeaveOneGroupOut()"
    assert repr(lpgo_1) == "LeavePGroupsOut(n_groups=1)"
    assert repr(lpgo_2) == "LeavePGroupsOut(n_groups=2)"
    assert repr(LeavePGroupsOut(n_groups=3)) == "LeavePGroupsOut(n_groups=3)"

    for j, (cv, p_groups_out) in enumerate(((logo, 1), (lpgo_1, 1), (lpgo_2, 2))):
        for i, groups_i in enumerate(test_groups):
            n_groups = len(np.unique(groups_i))
            n_splits = n_groups if p_groups_out == 1 else n_groups * (n_groups - 1) / 2
            X = y = np.ones(len(groups_i))

            # Test that the length is correct
            assert cv.get_n_splits(X, y, groups=groups_i) == n_splits

            groups_arr = np.asarray(groups_i)

            # Split using the original list / array / list of string groups_i
            for train, test in cv.split(X, y, groups=groups_i):
                # First test: no train group is in the test set and vice versa
                    np.intersect1d(groups_arr[train], groups_arr[test]).tolist(), []
                )

                # Second test: train and test add up to all the data
                assert len(train) + len(test) == len(groups_i)

                # Third test:
                # The number of groups in test must be equal to p_groups_out
                assert np.unique(groups_arr[test]).shape[0], p_groups_out

    # check get_n_splits() with dummy parameters
    assert logo.get_n_splits(None, None, ["a", "b", "c", "b", "c"]) == 3
    assert logo.get_n_splits(groups=[1.0, 1.1, 1.0, 1.2]) == 3
    assert lpgo_2.get_n_splits(None, None, np.arange(4)) == 6
    assert lpgo_1.get_n_splits(groups=np.arange(4)) == 4

    # raise ValueError if a `groups` parameter is illegal
        logo.get_n_splits(None, None, [0.0, np.nan, 0.0])
        lpgo_2.get_n_splits(None, None, [0.0, np.inf, 0.0])

    msg = "The 'groups' parameter should not be None."
        logo.get_n_splits(None, None, None)
        lpgo_1.get_n_splits(None, None, None)

def test_leave_group_out_changing_groups():
    # Check that LeaveOneGroupOut and LeavePGroupsOut work normally if
    # the groups variable is changed before calling split
    groups = np.array([0, 1, 2, 1, 1, 2, 0, 0])
    X = np.ones(len(groups))
    groups_changing = np.array(groups, copy=True)
    lolo = LeaveOneGroupOut().split(X, groups=groups)
    lolo_changing = LeaveOneGroupOut().split(X, groups=groups)
    lplo = LeavePGroupsOut(n_groups=2).split(X, groups=groups)
    lplo_changing = LeavePGroupsOut(n_groups=2).split(X, groups=groups)
    groups_changing[:] = 0
    for llo, llo_changing in [(lolo, lolo_changing), (lplo, lplo_changing)]:
        for (train, test), (train_chan, test_chan) in zip(llo, llo_changing):
            assert_array_equal(train, train_chan)
            assert_array_equal(test, test_chan)

    # n_splits = no of 2 (p) group combinations of the unique groups = 3C2 = 3
    assert 3 == LeavePGroupsOut(n_groups=2).get_n_splits(X, y=X, groups=groups)
    # n_splits = no of unique groups (C(uniq_lbls, 1) = n_unique_groups)
    assert 3 == LeaveOneGroupOut().get_n_splits(X, y=X, groups=groups)

def test_leave_group_out_order_dependence():
    # Check that LeaveOneGroupOut orders the splits according to the index
    # of the group left out.
    groups = np.array([2, 2, 0, 0, 1, 1])

    splits = iter(LeaveOneGroupOut().split(X, groups=groups))

    expected_indices = [
        ([0, 1, 4, 5], [2, 3]),
        ([0, 1, 2, 3], [4, 5]),
        ([2, 3, 4, 5], [0, 1]),
    ]

    for expected_train, expected_test in expected_indices:
        train, test = next(splits)
        assert_array_equal(train, expected_train)
        assert_array_equal(test, expected_test)

def test_leave_one_p_group_out_error_on_fewer_number_of_groups():
    X = y = groups = np.ones(0)
        next(LeaveOneGroupOut().split(X, y, groups))

    X = y = groups = np.ones(1)
        f"The groups parameter contains fewer than 2 unique groups ({groups})."
        " LeaveOneGroupOut expects at least 2."
    )

        "The groups parameter contains fewer than (or equal to) n_groups "
        f"(3) numbers of unique groups ({groups}). LeavePGroupsOut expects "
        "that at least n_groups + 1 (4) unique groups "
        "be present"
    )
        next(LeavePGroupsOut(n_groups=3).split(X, y, groups))

    X = y = groups = np.arange(3)
    )

def test_repeated_cv_value_errors():
    # n_repeats is not integer or <= 0
    for cv in (RepeatedKFold, RepeatedStratifiedKFold):
            cv(n_repeats=0)
            cv(n_repeats=1.5)

def test_repeated_cv_repr(RepeatedCV):
    n_splits, n_repeats = 2, 6
    repeated_cv = RepeatedCV(n_splits=n_splits, n_repeats=n_repeats)
    repeated_cv_repr = "{}(n_repeats=6, n_splits=2, random_state=None)".format(
        repeated_cv.__class__.__name__
    )
    assert repeated_cv_repr == repr(repeated_cv)

def test_repeated_kfold_determinstic_split():
    random_state = 258173307
    rkf = RepeatedKFold(n_splits=2, n_repeats=2, random_state=random_state)

    # split should produce same and deterministic splits on
    # each call
        splits = rkf.split(X)
        assert_array_equal(train, [2, 4])
        assert_array_equal(test, [0, 1, 3])

        assert_array_equal(train, [0, 1, 3])
        assert_array_equal(test, [2, 4])

        assert_array_equal(train, [0, 1])
        assert_array_equal(test, [2, 3, 4])

        assert_array_equal(train, [2, 3, 4])
        assert_array_equal(test, [0, 1])

        with pytest.raises(StopIteration):
            next(splits)

def test_get_n_splits_for_repeated_kfold():
    rkf = RepeatedKFold(n_splits=n_splits, n_repeats=n_repeats)
    expected_n_splits = n_splits * n_repeats
    assert expected_n_splits == rkf.get_n_splits()

def test_get_n_splits_for_repeated_stratified_kfold():
    rskf = RepeatedStratifiedKFold(n_splits=n_splits, n_repeats=n_repeats)
    assert expected_n_splits == rskf.get_n_splits()

def test_repeated_stratified_kfold_determinstic_split():
    y = [1, 1, 1, 0, 0]
    random_state = 1944695409
    rskf = RepeatedStratifiedKFold(n_splits=2, n_repeats=2, random_state=random_state)

    # each call
        splits = rskf.split(X, y)
        assert_array_equal(train, [1, 4])
        assert_array_equal(test, [0, 2, 3])

        assert_array_equal(train, [0, 2, 3])
        assert_array_equal(test, [1, 4])

        assert_array_equal(train, [2, 3])
        assert_array_equal(test, [0, 1, 4])

        assert_array_equal(train, [0, 1, 4])
        assert_array_equal(test, [2, 3])

def test_train_test_split_errors():
    pytest.raises(ValueError, train_test_split)

    pytest.raises(ValueError, train_test_split, range(3), train_size=1.1)

    pytest.raises(ValueError, train_test_split, range(3), test_size=0.6, train_size=0.6)
    pytest.raises(
        train_test_split,
        range(3),
        test_size=np.float32(0.6),
        train_size=np.float32(0.6),
    )
    pytest.raises(ValueError, train_test_split, range(3), test_size="wrong_type")
    pytest.raises(ValueError, train_test_split, range(3), test_size=2, train_size=4)
    pytest.raises(TypeError, train_test_split, range(3), some_argument=1.1)
    pytest.raises(ValueError, train_test_split, range(3), range(42))
    pytest.raises(ValueError, train_test_split, range(10), shuffle=False, stratify=True)

        match=r"train_size=11 should be either positive and "
        r"smaller than the number of samples 10 or a "
        r"float in the \(0, 1\) range",
    ):
        train_test_split(range(10), train_size=11, test_size=1)

def test_train_test_split_default_test_size(train_size, exp_train, exp_test):
    # Check that the default value has the expected behavior, i.e. complement
    # train_size unless both are specified.
    X_train, X_test = train_test_split(X, train_size=train_size)

def test_array_api_train_test_split(
    shuffle, stratify, array_namespace, device, dtype_name
):

    X = np.arange(100).reshape((10, 10))

    X_np = X.astype(dtype_name)

    y_np = y.astype(dtype_name)
    y_xp = xp.asarray(y_np, device=device)

    X_train_np, X_test_np, y_train_np, y_test_np = train_test_split(
        X_np, y, random_state=0, shuffle=shuffle, stratify=stratify
    )
            stratify_xp = xp.asarray(stratify)
        else:
            stratify_xp = stratify
        X_train_xp, X_test_xp, y_train_xp, y_test_xp = train_test_split(
            X_xp, y_xp, shuffle=shuffle, stratify=stratify_xp, random_state=0
        )

        # Check that namespace is preserved, has to happen with
        # array_api_dispatch enabled.
        assert get_namespace(X_train_xp)[0] == get_namespace(X_xp)[0]
        assert get_namespace(X_test_xp)[0] == get_namespace(X_xp)[0]
        assert get_namespace(y_train_xp)[0] == get_namespace(y_xp)[0]
        assert get_namespace(y_test_xp)[0] == get_namespace(y_xp)[0]

    # Check device and dtype is preserved on output
    assert array_api_device(X_train_xp) == array_api_device(X_xp)
    assert array_api_device(y_train_xp) == array_api_device(y_xp)
    assert array_api_device(X_test_xp) == array_api_device(X_xp)
    assert array_api_device(y_test_xp) == array_api_device(y_xp)

    assert X_train_xp.dtype == X_xp.dtype
    assert y_train_xp.dtype == y_xp.dtype
    assert X_test_xp.dtype == X_xp.dtype
    assert y_test_xp.dtype == y_xp.dtype

        _convert_to_numpy(X_train_xp, xp=xp),
        X_train_np,
    )
        _convert_to_numpy(X_test_xp, xp=xp),
        X_test_np,
    )

def test_train_test_split(coo_container):
    X_s = coo_container(X)

    # simple test
    split = train_test_split(X, y, test_size=None, train_size=0.5)
    X_train, X_test, y_train, y_test = split
    assert len(y_test) == len(y_train)
    # test correspondence of X and y
    assert_array_equal(X_train[:, 0], y_train * 10)
    assert_array_equal(X_test[:, 0], y_test * 10)

    # don't convert lists to anything else by default
    split = train_test_split(X, X_s, y.tolist())
    X_train, X_test, X_s_train, X_s_test, y_train, y_test = split
    assert isinstance(y_train, list)
    assert isinstance(y_test, list)

    # allow nd-arrays
    X_4d = np.arange(10 * 5 * 3 * 2).reshape(10, 5, 3, 2)
    y_3d = np.arange(10 * 7 * 11).reshape(10, 7, 11)
    split = train_test_split(X_4d, y_3d)
    assert split[0].shape == (7, 5, 3, 2)
    assert split[1].shape == (3, 5, 3, 2)
    assert split[2].shape == (7, 7, 11)
    assert split[3].shape == (3, 7, 11)

    # test stratification option
    y = np.array([1, 1, 1, 1, 2, 2, 2, 2])
    for test_size, exp_test_size in zip([2, 4, 0.25, 0.5, 0.75], [2, 4, 2, 4, 6]):
        train, test = train_test_split(
            y, test_size=test_size, stratify=y, random_state=0
        )
        assert len(test) == exp_test_size
        assert len(test) + len(train) == len(y)
        # check the 1:1 ratio of ones and twos in the data is preserved
        assert np.sum(train == 1) == np.sum(train == 2)

    # test unshuffled split
    for test_size in [2, 0.2]:
        train, test = train_test_split(y, shuffle=False, test_size=test_size)
        assert_array_equal(test, [8, 9])
        assert_array_equal(train, [0, 1, 2, 3, 4, 5, 6, 7])

def test_train_test_split_32bit_overflow():
    """Check for integer overflow on 32-bit platforms.

    """

    # A number 'n' big enough for expression 'n * n * train_size' to cause
    # an overflow for signed 32-bit integer
    big_number = 100000

    # Definition of 'y' is a part of reproduction - population for at least
    # one class should be in the same order of magnitude as size of X
    X = np.arange(big_number)
    y = X > (0.99 * big_number)

    split = train_test_split(X, y, stratify=y, train_size=0.25)

    assert X_train.size + X_test.size == big_number
    assert y_train.size + y_test.size == big_number

def test_train_test_split_pandas():
    # check train_test_split doesn't destroy pandas dataframe
    types = [MockDataFrame]
    try:
        from pandas import DataFrame

        types.append(DataFrame)
        pass
    for InputFeatureType in types:
        # X dataframe
        X_df = InputFeatureType(X)
        X_train, X_test = train_test_split(X_df)
        assert isinstance(X_train, InputFeatureType)
        assert isinstance(X_test, InputFeatureType)

def test_train_test_split_sparse(sparse_container):
    # check that train_test_split converts scipy sparse matrices
    # to csr, as stated in the documentation
    X_s = sparse_container(X)
    X_train, X_test = train_test_split(X_s)
    assert issparse(X_train) and X_train.format == "csr"
    assert issparse(X_test) and X_test.format == "csr"

def test_train_test_split_mock_pandas():
    # X mock dataframe
    X_df = MockDataFrame(X)
    X_train, X_test = train_test_split(X_df)
    assert isinstance(X_train, MockDataFrame)
    assert isinstance(X_test, MockDataFrame)
    X_train_arr, X_test_arr = train_test_split(X_df)

def test_train_test_split_list_input():
    # Check that when y is a list / list of string labels, it works.
    y1 = ["1"] * 4 + ["0"] * 3
    y2 = np.hstack((np.ones(4), np.zeros(3)))
    y3 = y2.tolist()

    for stratify in (True, False):
        X_train1, X_test1, y_train1, y_test1 = train_test_split(
            X, y1, stratify=y1 if stratify else None, random_state=0
        )
        X_train2, X_test2, y_train2, y_test2 = train_test_split(
            X, y2, stratify=y2 if stratify else None, random_state=0
        )
        X_train3, X_test3, y_train3, y_test3 = train_test_split(
            X, y3, stratify=y3 if stratify else None, random_state=0
        )

        np.testing.assert_equal(X_train1, X_train2)
        np.testing.assert_equal(y_train2, y_train3)
        np.testing.assert_equal(X_test1, X_test3)
        np.testing.assert_equal(y_test3, y_test2)

def test_shufflesplit_errors(test_size, train_size):
        next(ShuffleSplit(test_size=test_size, train_size=train_size).split(X))

def test_shufflesplit_reproducible():
    # Check that iterating twice on the ShuffleSplit gives the same
    # sequence of train-test when the random_state is given
    ss = ShuffleSplit(random_state=21)
    assert_array_equal([a for a, b in ss.split(X)], [a for a, b in ss.split(X)])

def test_stratifiedshufflesplit_list_input():
    sss = StratifiedShuffleSplit(test_size=2, random_state=42)

    np.testing.assert_equal(list(sss.split(X, y1)), list(sss.split(X, y2)))
    np.testing.assert_equal(list(sss.split(X, y3)), list(sss.split(X, y2)))

def test_train_test_split_allow_nans():
    # Check that train_test_split allows input data with NaNs
    X = np.arange(200, dtype=np.float64).reshape(10, -1)
    X[2, :] = np.nan
    y = np.repeat([0, 1], X.shape[0] / 2)
    train_test_split(X, y, test_size=0.2, random_state=42)

def test_check_cv():
    X = np.ones(9)
    cv = check_cv(3, classifier=False)
    # Use numpy.testing.assert_equal which recursively compares
    # lists of lists
    np.testing.assert_equal(list(KFold(3).split(X)), list(cv.split(X)))

    y_binary = np.array([0, 1, 0, 1, 0, 0, 1, 1, 1])
    cv = check_cv(3, y_binary, classifier=True)
        list(StratifiedKFold(3).split(X, y_binary)), list(cv.split(X, y_binary))
    )

    y_multiclass = np.array([0, 1, 0, 1, 2, 1, 2, 0, 2])
    cv = check_cv(3, y_multiclass, classifier=True)
        list(StratifiedKFold(3).split(X, y_multiclass)), list(cv.split(X, y_multiclass))
    )
    # also works with 2d multiclass
    y_multiclass_2d = y_multiclass.reshape(-1, 1)
    cv = check_cv(3, y_multiclass_2d, classifier=True)
        list(StratifiedKFold(3).split(X, y_multiclass_2d)),
        list(cv.split(X, y_multiclass_2d)),
    )

    assert not np.all(
        next(StratifiedKFold(3).split(X, y_multiclass_2d))[0]
        == next(KFold(3).split(X, y_multiclass_2d))[0]
    )

    X = np.ones(5)
    y_multilabel = np.array(
        [[0, 0, 0, 0], [0, 1, 1, 0], [0, 0, 0, 1], [1, 1, 0, 1], [0, 0, 1, 0]]
    )
    cv = check_cv(3, y_multilabel, classifier=True)

    y_multioutput = np.array([[1, 2], [0, 3], [0, 0], [3, 1], [2, 0]])
    cv = check_cv(3, y_multioutput, classifier=True)

        check_cv(cv="lolo")

def test_cv_iterable_wrapper():
    kf_iter = KFold().split(X, y)
    kf_iter_wrapped = check_cv(kf_iter)
    # Since the wrapped iterable is enlisted and stored,
    # split can be called any number of times to produce
    # consistent results.
        list(kf_iter_wrapped.split(X, y)), list(kf_iter_wrapped.split(X, y))
    )
    # If the splits are randomized, successive calls to split yields different
    # results
    kf_randomized_iter = KFold(shuffle=True, random_state=0).split(X, y)
    kf_randomized_iter_wrapped = check_cv(kf_randomized_iter)
    # numpy's assert_array_equal properly compares nested lists
        list(kf_randomized_iter_wrapped.split(X, y)),
    )

    try:
        splits_are_equal = True
            list(kf_iter_wrapped.split(X, y)),
            list(kf_randomized_iter_wrapped.split(X, y)),
        )
        splits_are_equal = False
    assert not splits_are_equal, (
        "If the splits are randomized, "
        "successive calls to split should yield different results"
    )

def test_group_kfold(kfold, shuffle, global_random_seed):

    # Parameters of the test
    n_groups = 15

    X = y = np.ones(n_samples)

    # Construct the test data
    tolerance = 0.05 * n_samples  # 5 percent error allowed
    groups = rng.randint(0, n_groups, n_samples)

    ideal_n_groups_per_fold = n_samples // n_splits

    len(np.unique(groups))
    # Get the test fold indices from the test set indices of each fold
    folds = np.zeros(n_samples)
    random_state = None if not shuffle else global_random_seed
    lkf = kfold(n_splits=n_splits, shuffle=shuffle, random_state=random_state)
    for i, (_, test) in enumerate(lkf.split(X, y, groups)):
        folds[test] = i

    # Check that folds have approximately the same size
    assert len(folds) == len(groups)
    for i in np.unique(folds):
        assert tolerance >= abs(sum(folds == i) - ideal_n_groups_per_fold)

    # Check that each group appears only in 1 fold
    for group in np.unique(groups):
        assert len(np.unique(folds[groups == group])) == 1

    # Check that no group is on both sides of the split
    groups = np.asarray(groups, dtype=object)
    for train, test in lkf.split(X, y, groups):
        assert len(np.intersect1d(groups[train], groups[test])) == 0

    groups = np.array(
        [
            "Albert",
            "Jean",
            "Bertrand",
            "Michel",
            "Jean",
            "Francis",
            "Robert",
            "Michel",
            "Rachel",
            "Lois",
            "Michelle",
            "Bernard",
            "Marion",
            "Laura",
            "Jean",
            "Rachel",
            "Franck",
            "John",
            "Gael",
            "Anna",
            "Alix",
            "Robert",
            "Marion",
            "David",
            "Tony",
            "Abel",
            "Becky",
            "Madmood",
            "Cary",
            "Mary",
            "Alexandre",
            "David",
            "Barack",
            "Abdoul",
            "Rasha",
            "Xi",
            "Silvia",
        ]
    )

    n_groups = len(np.unique(groups))
    n_samples = len(groups)

    if not shuffle:
        for i in np.unique(folds):
            assert tolerance >= abs(sum(folds == i) - ideal_n_groups_per_fold)

        warnings.simplefilter("ignore", FutureWarning)
            assert len(np.unique(folds[groups == group])) == 1

    # groups can also be a list
    # use a new instance for reproducibility when shuffle=True
    lkf_copy = kfold(n_splits=n_splits, shuffle=shuffle, random_state=random_state)
    cv_iter = list(lkf.split(X, y, groups.tolist()))
    for (train1, test1), (train2, test2) in zip(lkf_copy.split(X, y, groups), cv_iter):
        assert_array_equal(train1, train2)
        assert_array_equal(test1, test2)

    # Should fail if there are more folds than groups
    groups = np.array([1, 1, 1, 2, 2])
    X = y = np.ones(len(groups))
    with pytest.raises(ValueError, match="Cannot have number of splits.*greater"):
        next(GroupKFold(n_splits=3).split(X, y, groups))

def test_time_series_cv():
    X = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12], [13, 14]]

    # Should fail if there are more folds than samples
    with pytest.raises(ValueError, match="Cannot have number of folds.*greater"):
        next(TimeSeriesSplit(n_splits=7).split(X))

    tscv = TimeSeriesSplit(2)

    # Manually check that Time Series CV preserves the data
    # ordering on toy datasets
    splits = tscv.split(X[:-1])

    assert_array_equal(train, [0, 1, 2, 3])
    assert_array_equal(test, [4, 5])

    splits = TimeSeriesSplit(2).split(X)

    assert_array_equal(train, [0, 1, 2, 3, 4])
    assert_array_equal(test, [5, 6])

    # Check get_n_splits returns the correct number of splits
    n_splits_actual = len(list(splits))
    assert n_splits_actual == tscv.get_n_splits()
    assert n_splits_actual == 2

def _check_time_series_max_train_size(splits, check_splits, max_train_size):
    for (train, test), (check_train, check_test) in zip(splits, check_splits):
        assert_array_equal(test, check_test)
        assert len(check_train) <= max_train_size
        suffix_start = max(len(train) - max_train_size, 0)
        assert_array_equal(check_train, train[suffix_start:])

def test_time_series_max_train_size():
    X = np.zeros((6, 1))
    splits = TimeSeriesSplit(n_splits=3).split(X)
    check_splits = TimeSeriesSplit(n_splits=3, max_train_size=3).split(X)
    _check_time_series_max_train_size(splits, check_splits, max_train_size=3)

    # Test for the case where the size of a fold is greater than max_train_size
    check_splits = TimeSeriesSplit(n_splits=3, max_train_size=2).split(X)
    _check_time_series_max_train_size(splits, check_splits, max_train_size=2)

    # Test for the case where the size of each fold is less than max_train_size
    check_splits = TimeSeriesSplit(n_splits=3, max_train_size=5).split(X)

def test_time_series_test_size():
    X = np.zeros((10, 1))

    # Test alone
    splits = TimeSeriesSplit(n_splits=3, test_size=3).split(X)

    assert_array_equal(train, [0])
    assert_array_equal(test, [1, 2, 3])

    assert_array_equal(test, [4, 5, 6])

    assert_array_equal(train, [0, 1, 2, 3, 4, 5, 6])
    assert_array_equal(test, [7, 8, 9])

    # Test with max_train_size
    splits = TimeSeriesSplit(n_splits=2, test_size=2, max_train_size=4).split(X)

    assert_array_equal(train, [2, 3, 4, 5])
    assert_array_equal(test, [6, 7])

    assert_array_equal(train, [4, 5, 6, 7])
    assert_array_equal(test, [8, 9])

    # Should fail with not enough data points for configuration
    with pytest.raises(ValueError, match="Too many splits.*with test_size"):
        splits = TimeSeriesSplit(n_splits=5, test_size=2).split(X)
        next(splits)

def test_time_series_gap():

    splits = TimeSeriesSplit(n_splits=2, gap=2).split(X)

    splits = TimeSeriesSplit(n_splits=3, gap=2, max_train_size=2).split(X)

    assert_array_equal(train, [4, 5])

    # Test with test_size
    splits = TimeSeriesSplit(n_splits=2, gap=2, max_train_size=4, test_size=2).split(X)

    # Test with additional test_size
    splits = TimeSeriesSplit(n_splits=2, gap=2, test_size=3).split(X)

    # Verify proper error is thrown
    with pytest.raises(ValueError, match="Too many splits.*and gap"):
        splits = TimeSeriesSplit(n_splits=4, gap=2).split(X)

def test_nested_cv():
    # Test if nested cross validation works with different combinations of cv

    X, y = make_classification(n_samples=15, n_classes=2, random_state=0)
    groups = rng.randint(0, 5, 15)

    cvs = [
        StratifiedKFold(n_splits=2),
        StratifiedShuffleSplit(n_splits=3, random_state=0),
    ]

    for inner_cv, outer_cv in combinations_with_replacement(cvs, 2):
            DummyClassifier(),
            param_grid={"strategy": ["stratified", "most_frequent"]},
            cv=inner_cv,
            error_score="raise",
        )
        cross_val_score(
            gs, X=X, y=y, groups=groups, cv=outer_cv, params={"groups": groups}
        )

def test_build_repr():
    class MockSplitter:
        def __init__(self, a, b=0, c=None):
            self.a = a
            self.b = b
            self.c = c

            return _build_repr(self)

    assert repr(MockSplitter(5, 6)) == "MockSplitter(a=5, b=6, c=None)"

def test_shuffle_split_empty_trainset(CVSplitter):
    cv = CVSplitter(test_size=0.99)
    X, y = [[1]], [0]  # 1 sample
        match=(
            "With n_samples=1, test_size=0.99 and train_size=None, "
            "the resulting train set will be empty"
        ),
    ):
        next(_split(cv, X, y, groups=[1]))

def test_train_test_split_empty_trainset():
    (X,) = [[1]]  # 1 sample
        match=(
        ),
    ):
        train_test_split(X, test_size=0.99)

    X = [[1], [1], [1]]  # 3 samples, ask for more than 2 thirds
        match=(
            "With n_samples=3, test_size=0.67 and train_size=None, "
        ),
    ):
        train_test_split(X, test_size=0.67)

def test_leave_one_out_empty_trainset():
    # LeaveOneGroup out expect at least 2 groups so no need to check
    with pytest.raises(ValueError, match="Cannot perform LeaveOneOut with n_samples=1"):
        next(cv.split(X, y))

def test_leave_p_out_empty_trainset():
    # No need to check LeavePGroupsOut
    cv = LeavePOut(p=2)
    X, y = [[1], [2]], [0, 3]  # 2 samples
        ValueError, match="p=2 must be strictly less than the number of samples=2"
    ):

def test_random_state_shuffle_false(Klass):
    # passing a non-default random_state when shuffle=False makes no sense
    with pytest.raises(ValueError, match="has no effect since shuffle is False"):
        Klass(3, shuffle=False, random_state=0)

def test_yields_constant_splits(cv, expected):
    assert _yields_constant_splits(cv) == expected

def test_splitter_get_metadata_routing(cv):
    """Check get_metadata_routing returns the correct MetadataRouter."""
    assert hasattr(cv, "get_metadata_routing")
    metadata = cv.get_metadata_routing()
    if cv in GROUP_SPLITTERS:
        assert metadata.split.requests["groups"] is True
    elif cv in NO_GROUP_SPLITTERS:
        assert not metadata.split.requests

    assert_request_is_empty(metadata, exclude=["split"])

def test_splitter_set_split_request(cv):
    """Check set_split_request is defined for group splitters and not for others."""
        assert hasattr(cv, "set_split_request")
        assert not hasattr(cv, "set_split_request")

def test_no_group_splitters_warns_with_groups(cv):
    msg = f"The groups parameter is ignored by {cv.__class__.__name__}"

        cv.split(X, y, groups=groups)

def test_stratified_splitter_without_y(cv):
    msg = "missing 1 required positional argument: 'y'"
        cv.split(X)

        return [
                X,
                y,
            )
        ]

            )

            self.a = a
            self.b = b
            self.c = c

            self.a = a
            self.b = b
            self.c = c

    return shuffle(*load_iris(return_X_y=True), random_state=0)

def test_curve_display_parameters_validation(
    pyplot, data, params, err_type, err_msg, CurveDisplay, specific_params
):
    """Check that we raise a proper error when passing invalid parameters."""
    X, y = data
    estimator = DecisionTreeClassifier(random_state=0)

        CurveDisplay.from_estimator(estimator, X, y, **specific_params, **params)

def test_learning_curve_display_default_usage(pyplot, data):
    """Check the default usage of the LearningCurveDisplay class."""
    X, y = data

    train_sizes = [0.3, 0.6, 0.9]
    display = LearningCurveDisplay.from_estimator(
        estimator, X, y, train_sizes=train_sizes
    )

    assert display.errorbar_ is None

    assert isinstance(display.lines_, list)
    for line in display.lines_:
        assert isinstance(line, mpl.lines.Line2D)

    assert isinstance(display.fill_between_, list)
    for fill in display.fill_between_:
        assert isinstance(fill, mpl.collections.PolyCollection)
        assert fill.get_alpha() == 0.5

    assert display.score_name == "Score"
    assert display.ax_.get_xlabel() == "Number of samples in the training set"
    assert display.ax_.get_ylabel() == "Score"

    _, legend_labels = display.ax_.get_legend_handles_labels()
    assert legend_labels == ["Train", "Test"]

    train_sizes_abs, train_scores, test_scores = learning_curve(
    )

    assert_array_equal(display.train_sizes, train_sizes_abs)
    assert_allclose(display.train_scores, train_scores)
    assert_allclose(display.test_scores, test_scores)

def test_validation_curve_display_default_usage(pyplot, data):
    """Check the default usage of the ValidationCurveDisplay class."""
    X, y = data

    param_name, param_range = "max_depth", [1, 3, 5]
    display = ValidationCurveDisplay.from_estimator(
        estimator, X, y, param_name=param_name, param_range=param_range
    )

    assert display.ax_.get_xlabel() == f"{param_name}"

    train_scores, test_scores = validation_curve(
    )

    assert_array_equal(display.param_range, param_range)

def test_curve_display_negate_score(pyplot, data, CurveDisplay, specific_params):
    """Check the behaviour of the `negate_score` parameter calling `from_estimator` and
    `plot`.
    """
    X, y = data
    estimator = DecisionTreeClassifier(max_depth=1, random_state=0)

    negate_score = False
    display = CurveDisplay.from_estimator(
        estimator, X, y, **specific_params, negate_score=negate_score
    )

    positive_scores = display.lines_[0].get_data()[1]
    assert (positive_scores >= 0).all()

    negate_score = True
    )

    negative_scores = display.lines_[0].get_data()[1]
    assert (negative_scores <= 0).all()
    assert_allclose(negative_scores, -positive_scores)
    assert display.ax_.get_ylabel() == "Negative score"

    )
    display.plot(negate_score=not negate_score)
    assert (display.lines_[0].get_data()[1] < 0).all()

def test_curve_display_score_name(
    pyplot, data, score_name, ylabel, CurveDisplay, specific_params
):
    """Check that we can overwrite the default score name shown on the y-axis."""
    X, y = data

        estimator, X, y, **specific_params, score_name=score_name
    )

    assert display.ax_.get_ylabel() == ylabel
    X, y = data

    )

    assert display.score_name == ylabel

def test_learning_curve_display_score_type(pyplot, data, std_display_style):
    """Check the behaviour of setting the `score_type` parameter."""
    X, y = data

    )

    score_type = "train"
        X,
        y,
        train_sizes=train_sizes,
        score_type=score_type,
        std_display_style=std_display_style,
    )

    _, legend_label = display.ax_.get_legend_handles_labels()
    assert legend_label == ["Train"]

    if std_display_style is None:
        assert len(display.lines_) == 1
        assert display.errorbar_ is None
        x_data, y_data = display.lines_[0].get_data()
    else:
        assert display.lines_ is None
        assert len(display.errorbar_) == 1
        x_data, y_data = display.errorbar_[0].lines[0].get_data()

    assert_array_equal(x_data, train_sizes_abs)
    assert_allclose(y_data, train_scores.mean(axis=1))

    score_type = "test"
        X,
        y,
    )

    assert legend_label == ["Test"]

    else:

    assert_allclose(y_data, test_scores.mean(axis=1))

    score_type = "both"
        X,
        y,
    )

    assert legend_label == ["Train", "Test"]

        assert len(display.lines_) == 2
        x_data_train, y_data_train = display.lines_[0].get_data()
        x_data_test, y_data_test = display.lines_[1].get_data()
    else:
        assert len(display.errorbar_) == 2
        x_data_train, y_data_train = display.errorbar_[0].lines[0].get_data()
        x_data_test, y_data_test = display.errorbar_[1].lines[0].get_data()

    assert_array_equal(x_data_train, train_sizes_abs)
    assert_allclose(y_data_train, train_scores.mean(axis=1))
    assert_array_equal(x_data_test, train_sizes_abs)
    assert_allclose(y_data_test, test_scores.mean(axis=1))

def test_validation_curve_display_score_type(pyplot, data, std_display_style):
    X, y = data

    )

        X,
        y,
        param_name=param_name,
        param_range=param_range,
    )

    else:

    assert_array_equal(x_data, param_range)

        X,
        y,
    )

    else:

        X,
        y,
    )

    else:

    assert_array_equal(x_data_train, param_range)
    assert_array_equal(x_data_test, param_range)

def test_curve_display_xscale_auto(
    pyplot, data, CurveDisplay, specific_params, expected_xscale
):
    """Check the behaviour of the x-axis scaling depending on the data provided."""
    X, y = data

    display = CurveDisplay.from_estimator(estimator, X, y, **specific_params)
    assert display.ax_.get_xscale() == expected_xscale

def test_curve_display_std_display_style(pyplot, data, CurveDisplay, specific_params):
    """Check the behaviour of the parameter `std_display_style`."""
    X, y = data

    std_display_style = None
        X,
        y,
        **specific_params,
    )

    assert len(display.lines_) == 2
    assert display.fill_between_ is None
    assert len(legend_label) == 2

    std_display_style = "fill_between"
        X,
        y,
    )

    assert len(display.fill_between_) == 2
    for fill_between in display.fill_between_:
        assert isinstance(fill_between, mpl.collections.PolyCollection)

    std_display_style = "errorbar"
        X,
        y,
    )

    assert display.lines_ is None
    assert len(display.errorbar_) == 2
    for errorbar in display.errorbar_:
        assert isinstance(errorbar, mpl.container.ErrorbarContainer)

def test_curve_display_plot_kwargs(pyplot, data, CurveDisplay, specific_params):
    """Check the behaviour of the different plotting keyword arguments: `line_kw`,
    `fill_between_kw`, and `errorbar_kw`."""
    X, y = data

    line_kw = {"color": "red"}
    fill_between_kw = {"color": "red", "alpha": 1.0}
        X,
        y,
        fill_between_kw=fill_between_kw,
    )

    assert display.lines_[0].get_color() == "red"
        display.fill_between_[0].get_facecolor(),
        [[1.0, 0.0, 0.0, 1.0]],  # trust me, it's red
    )

    errorbar_kw = {"color": "red"}
        X,
        y,
        errorbar_kw=errorbar_kw,
    )

    assert display.errorbar_[0].lines[0].get_color() == "red"

def test_validation_curve_xscale_from_param_range_provided_as_a_list(
    pyplot, data, param_range, xscale
):
    """Check the induced xscale from the provided param_range values."""
    X, y = data

    param_name = "max_depth"
        X,
        y,
    )

    assert display.ax_.get_xscale() == xscale

def test_subclassing_displays(pyplot, data, Display, params):

    """
    X, y = data

        pass

    display = SubclassOfDisplay.from_estimator(estimator, X, y, **params)
    assert isinstance(display, SubclassOfDisplay)

class MockImprovingEstimator(BaseEstimator):
    """Dummy classifier to test the learning curve"""

    def __init__(self, n_max_train_sizes):
        self.n_max_train_sizes = n_max_train_sizes
        self.train_sizes = 0
        self.X_subset = None

    def fit(self, X_subset, y_subset=None):
        self.X_subset = X_subset
        self.train_sizes = X_subset.shape[0]

        # training score becomes worse (2 -> 1), test error better (0 -> 1)
        if self._is_training_data(X):
            return 2.0 - float(self.train_sizes) / self.n_max_train_sizes
        else:
            return float(self.train_sizes) / self.n_max_train_sizes

    def _is_training_data(self, X):
        return X is self.X_subset

class MockIncrementalImprovingEstimator(MockImprovingEstimator):
    """Dummy classifier that provides partial_fit"""

    def __init__(self, n_max_train_sizes, expected_fit_params=None):
        super().__init__(n_max_train_sizes)
        self.x = None

        return self.x in X

    def partial_fit(self, X, y=None, **params):
        self.train_sizes += X.shape[0]
        self.x = X[0]
            missing = set(self.expected_fit_params) - set(params)
                )
                if key in self.expected_fit_params and _num_samples(
                    value
                ) != _num_samples(X):
                    )

class MockEstimatorWithParameter(BaseEstimator):
    """Dummy classifier to test the validation curve"""

    def __init__(self, param=0.5):

    def fit(self, X_subset, y_subset):

        return self.param if self._is_training_data(X) else 1 - self.param

class MockEstimatorWithSingleFitCallAllowed(MockEstimatorWithParameter):
    """Dummy classifier that disallows repeated calls of fit method"""

        assert not hasattr(self, "fit_called_"), "fit is called the second time"
        self.fit_called_ = True
        return super().fit(X_subset, y_subset)

    """Dummy classifier to test the cross-validation"""

    def __init__(self, a=0, allow_nd=False):
        self.a = a
        self.allow_nd = allow_nd

    def fit(
        self,
        X,
        Y=None,
        sparse_sample_weight=None,
        sparse_param=None,
        dummy_int=None,
        dummy_str=None,
        dummy_obj=None,
    ):
        """The dummy arguments are to test that this fit function can
        accept non-array arguments through cross-validation, such as:
            - int
            - str (this is actually array-like)
            - object
            - function
        """
        self.dummy_int = dummy_int
        self.dummy_str = dummy_str
        self.dummy_obj = dummy_obj
        if callback is not None:
            callback(self)

        if self.allow_nd:
            X = X.reshape(len(X), -1)
        if X.ndim >= 3 and not self.allow_nd:
            raise ValueError("X cannot be d")
            assert sample_weight.shape[0] == X.shape[0], (
                "MockClassifier extra fit_param "
                "sample_weight.shape[0] is {0}, should be {1}".format(
                    sample_weight.shape[0], X.shape[0]
                )
            )
            assert class_prior.shape[0] == len(np.unique(y)), (
                "MockClassifier extra fit_param class_prior.shape[0]"
                " is {0}, should be {1}".format(class_prior.shape[0], len(np.unique(y)))
            )
        if sparse_sample_weight is not None:
            fmt = (
                "MockClassifier extra fit_param sparse_sample_weight"
                ".shape[0] is {0}, should be {1}"
            )
            assert sparse_sample_weight.shape[0] == X.shape[0], fmt.format(
                sparse_sample_weight.shape[0], X.shape[0]
            )
        if sparse_param is not None:
            fmt = (
                "MockClassifier extra fit_param sparse_param.shape "
                "is ({0}, {1}), should be ({2}, {3})"
            )
            assert sparse_param.shape == P.shape, fmt.format(
                sparse_param.shape[0],
                sparse_param.shape[1],
                P.shape[0],
                P.shape[1],
            )

            T = T.reshape(len(T), -1)
        return T[:, 0]

    def predict_proba(self, T):
        return T

        return 1.0 / (1 + np.abs(self.a))

        return {"a": self.a, "allow_nd": self.allow_nd}

def test_cross_val_score(coo_container):
    X_sparse = coo_container(X)

    for a in range(-10, 10):
        clf.a = a
        # Smoke test
        scores = cross_val_score(clf, X, y2)
        assert_array_equal(scores, clf.score(X, y2))

        # test with multioutput y
        multioutput_y = np.column_stack([y2, y2[::-1]])
        scores = cross_val_score(clf, X_sparse, multioutput_y)
        assert_array_equal(scores, clf.score(X_sparse, multioutput_y))

        scores = cross_val_score(clf, X_sparse, y2)
        assert_array_equal(scores, clf.score(X_sparse, y2))

    # test with X and y as list
    list_check = lambda x: isinstance(x, list)
    clf = CheckingClassifier(check_X=list_check)
    scores = cross_val_score(clf, X.tolist(), y2.tolist(), cv=3)

    clf = CheckingClassifier(check_y=list_check)
    scores = cross_val_score(clf, X, y2.tolist(), cv=3)

    # test with 3d X and
    X_3d = X[:, :, np.newaxis]
    clf = MockClassifier(allow_nd=True)
    scores = cross_val_score(clf, X_3d, y2)

    clf = MockClassifier(allow_nd=False)
        cross_val_score(clf, X_3d, y2, error_score="raise")

def test_cross_validate_many_jobs():
    # regression test for #12154: cv='warn' with n_jobs>1 trigger a copy of
    # the parameters leading to a failure in check_cv due to cv is 'warn'
    # instead of cv == 'warn'.
    clf = SVC(gamma="auto")
    grid = GridSearchCV(clf, param_grid={"C": [1, 10]})
    cross_validate(grid, X, y, n_jobs=2)

def test_cross_validate_invalid_scoring_param():
    estimator = MockClassifier()

    # Test the errors
    error_message_regexp = ".*must be unique strings.*"

    # List/tuple of callables should raise a message advising users to use
    # dict of names to callables mapping
    with pytest.raises(ValueError, match=error_message_regexp):
        cross_validate(
            X,
            y,
            scoring=(make_scorer(precision_score), make_scorer(accuracy_score)),
        )
        cross_validate(estimator, X, y, scoring=(make_scorer(precision_score),))

    # So should empty lists/tuples
    with pytest.raises(ValueError, match=error_message_regexp + "Empty list.*"):
        cross_validate(estimator, X, y, scoring=())

    # So should duplicated entries
    with pytest.raises(ValueError, match=error_message_regexp + "Duplicate.*"):
        cross_validate(estimator, X, y, scoring=("f1_micro", "f1_micro"))

    # Nested Lists should raise a generic error message
        cross_validate(estimator, X, y, scoring=[[make_scorer(precision_score)]])

    # Empty dict should raise invalid scoring error
    with pytest.raises(ValueError, match="An empty dict"):
        cross_validate(estimator, X, y, scoring=(dict()))

    multiclass_scorer = make_scorer(precision_recall_fscore_support)

    # Multiclass Scorers that return multiple values are not supported yet
    # the warning message we're expecting to see
        "Scoring failed. The score on this train-test "
        f"partition for these parameters will be set to {np.nan}. "
        "Details: \n"
    )

        cross_validate(estimator, X, y, scoring=multiclass_scorer)

        cross_validate(estimator, X, y, scoring={"foo": multiclass_scorer})

def test_cross_validate_nested_estimator():
    # Non-regression test to ensure that nested
    # estimators are properly returned in a list
    # https://github.com/scikit-learn/scikit-learn/pull/17745
    (X, y) = load_iris(return_X_y=True)
        [
            ("classifier", MockClassifier()),
        ]
    )

    results = cross_validate(pipeline, X, y, return_estimator=True)
    estimators = results["estimator"]

    assert isinstance(estimators, list)
    assert all(isinstance(estimator, Pipeline) for estimator in estimators)

def test_cross_validate(use_sparse: bool, csr_container):
    # Compute train and test mse/r2 scores
    cv = KFold()

    X_reg, y_reg = make_regression(n_samples=30, random_state=0)
    reg = Ridge(random_state=0)

    X_clf, y_clf = make_classification(n_samples=30, random_state=0)
    clf = SVC(kernel="linear", random_state=0)

    if use_sparse:
        X_reg = csr_container(X_reg)
        X_clf = csr_container(X_clf)

    for X, y, est in ((X_reg, y_reg, reg), (X_clf, y_clf, clf)):
        # It's okay to evaluate regression metrics on classification too
        mse_scorer = check_scoring(est, scoring="neg_mean_squared_error")
        r2_scorer = check_scoring(est, scoring="r2")
        train_mse_scores = []
        test_mse_scores = []
        train_r2_scores = []
        test_r2_scores = []
        fitted_estimators = []

        for train, test in cv.split(X, y):
            est = clone(est).fit(X[train], y[train])
            train_mse_scores.append(mse_scorer(est, X[train], y[train]))
            train_r2_scores.append(r2_scorer(est, X[train], y[train]))
            test_mse_scores.append(mse_scorer(est, X[test], y[test]))
            test_r2_scores.append(r2_scorer(est, X[test], y[test]))
            fitted_estimators.append(est)

        train_mse_scores = np.array(train_mse_scores)
        test_mse_scores = np.array(test_mse_scores)
        train_r2_scores = np.array(train_r2_scores)
        test_r2_scores = np.array(test_r2_scores)
        fitted_estimators = np.array(fitted_estimators)

        scores = (
            train_mse_scores,
            test_mse_scores,
            train_r2_scores,
            test_r2_scores,
            fitted_estimators,
        )

        # To ensure that the test does not suffer from
        # large statistical fluctuations due to slicing small datasets,
        # we pass the cross-validation instance
        check_cross_validate_single_metric(est, X, y, scores, cv)
        check_cross_validate_multi_metric(est, X, y, scores, cv)

def check_cross_validate_single_metric(clf, X, y, scores, cv):
    (
        train_mse_scores,
        test_mse_scores,
        train_r2_scores,
        test_r2_scores,
        fitted_estimators,
    ) = scores
    # Test single metric evaluation when scoring is string or singleton list
    for return_train_score, dict_len in ((True, 4), (False, 3)):
        # Single metric passed as a string
            mse_scores_dict = cross_validate(
                clf,
                X,
                y,
                scoring="neg_mean_squared_error",
                cv=cv,
            )
            assert_array_almost_equal(mse_scores_dict["train_score"], train_mse_scores)
        else:
                clf,
                X,
                y,
                return_train_score=False,
                cv=cv,
            )
        assert isinstance(mse_scores_dict, dict)
        assert len(mse_scores_dict) == dict_len
        assert_array_almost_equal(mse_scores_dict["test_score"], test_mse_scores)

        # Single metric passed as a list
            # It must be True by default - deprecated
            r2_scores_dict = cross_validate(
                clf, X, y, scoring=["r2"], return_train_score=True, cv=cv
            )
            assert_array_almost_equal(r2_scores_dict["train_r2"], train_r2_scores, True)
        else:
                clf, X, y, scoring=["r2"], return_train_score=False, cv=cv
            )
        assert isinstance(r2_scores_dict, dict)
        assert len(r2_scores_dict) == dict_len
        assert_array_almost_equal(r2_scores_dict["test_r2"], test_r2_scores)

    # Test return_estimator option
    mse_scores_dict = cross_validate(
        clf, X, y, scoring="neg_mean_squared_error", return_estimator=True, cv=cv
    )
    for k, est in enumerate(mse_scores_dict["estimator"]):
        est_coef = est.coef_.copy()
        if issparse(est_coef):
            est_coef = est_coef.toarray()

        fitted_est_coef = fitted_estimators[k].coef_.copy()
        if issparse(fitted_est_coef):
            fitted_est_coef = fitted_est_coef.toarray()

        assert_almost_equal(est_coef, fitted_est_coef)
        assert_almost_equal(est.intercept_, fitted_estimators[k].intercept_)

def check_cross_validate_multi_metric(clf, X, y, scores, cv):
    # Test multimetric evaluation when scoring is a list / dict
    (
    ) = scores

    def custom_scorer(clf, X, y):
        return {
            "r2": r2_score(y, y_pred),
            "neg_mean_squared_error": -mean_squared_error(y, y_pred),
        }

    all_scoring = (
        ("r2", "neg_mean_squared_error"),
        {
            "r2": make_scorer(r2_score),
            "neg_mean_squared_error": "neg_mean_squared_error",
        },
        custom_scorer,
    )

    keys_sans_train = {
        "test_r2",
        "test_neg_mean_squared_error",
        "fit_time",
        "score_time",
    }
    keys_with_train = keys_sans_train.union(
        {"train_r2", "train_neg_mean_squared_error"}
    )

    for return_train_score in (True, False):
        for scoring in all_scoring:
            if return_train_score:
                # return_train_score must be True by default - deprecated
                cv_results = cross_validate(
                    clf, X, y, scoring=scoring, return_train_score=True, cv=cv
                )
                assert_array_almost_equal(cv_results["train_r2"], train_r2_scores)
                    cv_results["train_neg_mean_squared_error"], train_mse_scores
                )
            else:
                    clf, X, y, scoring=scoring, return_train_score=False, cv=cv
                )
            assert isinstance(cv_results, dict)
            assert set(cv_results.keys()) == (
                keys_with_train if return_train_score else keys_sans_train
            )
            assert_array_almost_equal(cv_results["test_r2"], test_r2_scores)
                cv_results["test_neg_mean_squared_error"], test_mse_scores
            )

            # Make sure all the arrays are of np.ndarray type
            assert isinstance(cv_results["test_r2"], np.ndarray)
            assert isinstance(cv_results["test_neg_mean_squared_error"], np.ndarray)
            assert isinstance(cv_results["fit_time"], np.ndarray)
            assert isinstance(cv_results["score_time"], np.ndarray)

            # Ensure all the times are within sane limits
            assert np.all(cv_results["fit_time"] >= 0)
            assert np.all(cv_results["fit_time"] < 10)
            assert np.all(cv_results["score_time"] >= 0)
            assert np.all(cv_results["score_time"] < 10)

def test_cross_val_score_predict_groups():
    # Check if ValueError (when groups is None) propagates to cross_val_score
    # and cross_val_predict
    # And also check if groups is correctly passed to the cv object
    X, y = make_classification(n_samples=20, n_classes=2, random_state=0)

    group_cvs = [
        LeavePGroupsOut(2),
        GroupKFold(),
    ]
    error_message = "The 'groups' parameter should not be None."
    for cv in group_cvs:
        with pytest.raises(ValueError, match=error_message):
            cross_val_score(estimator=clf, X=X, y=y, cv=cv)
            cross_val_predict(estimator=clf, X=X, y=y, cv=cv)

def test_cross_val_score_pandas():
    # check cross_val_score doesn't destroy pandas dataframe
    try:

        pass
    for TargetType, InputFeatureType in types:
        # X dataframe, y series
        # 3 fold cross val is used so we need at least 3 samples per class
        X_df, y_ser = InputFeatureType(X), TargetType(y2)
        check_df = lambda x: isinstance(x, InputFeatureType)
        check_series = lambda x: isinstance(x, TargetType)
        clf = CheckingClassifier(check_X=check_df, check_y=check_series)
        cross_val_score(clf, X_df, y_ser, cv=3)

def test_cross_val_score_mask():
    # test that cross_val_score works with boolean masks
    svm = SVC(kernel="linear")
    kfold = KFold(5)
    scores_indices = cross_val_score(svm, X, y, cv=kfold)
    cv_masks = []
    for train, test in kfold.split(X, y):
        mask_train = np.zeros(len(y), dtype=bool)
        mask_test = np.zeros(len(y), dtype=bool)
        mask_train[train] = 1
        mask_test[test] = 1
        cv_masks.append((train, test))
    scores_masks = cross_val_score(svm, X, y, cv=cv_masks)
    assert_array_equal(scores_indices, scores_masks)

def test_cross_val_score_precomputed():
    # test for svm with precomputed kernel
    svm = SVC(kernel="precomputed")
    score_precomputed = cross_val_score(svm, linear_kernel, y)
    score_linear = cross_val_score(svm, X, y)
    assert_array_almost_equal(score_precomputed, score_linear)

    # test with callable
    svm = SVC(kernel=lambda x, y: np.dot(x, y.T))
    score_callable = cross_val_score(svm, X, y)
    assert_array_almost_equal(score_precomputed, score_callable)

    # Error raised for non-square X
        cross_val_score(svm, X, y)

    # test error is raised when the precomputed kernel is not array-like
    # or sparse
        cross_val_score(svm, linear_kernel.tolist(), y)

def test_cross_val_score_fit_params(coo_container):

    W_sparse = coo_container(
        (np.array([1]), (np.array([1]), np.array([0]))), shape=(15, 1)
    )
    P_sparse = coo_container(np.eye(5))

    DUMMY_INT = 42
    DUMMY_STR = "42"
    DUMMY_OBJ = object()

    def assert_fit_params(clf):
        # Function to test that the values are passed correctly to the
        # classifier arguments for non-array type

        assert clf.dummy_int == DUMMY_INT
        assert clf.dummy_str == DUMMY_STR
        assert clf.dummy_obj == DUMMY_OBJ

        "sample_weight": np.ones(n_samples),
        "class_prior": np.full(n_classes, 1.0 / n_classes),
        "sparse_sample_weight": W_sparse,
        "sparse_param": P_sparse,
        "dummy_int": DUMMY_INT,
        "dummy_str": DUMMY_STR,
        "dummy_obj": DUMMY_OBJ,
        "callback": assert_fit_params,
    }
    cross_val_score(clf, X, y2, params=fit_params)

def test_cross_val_score_score_func():
    _score_func_args = []

    def score_func(y_test, y_predict):
        _score_func_args.append((y_test, y_predict))
        return 1.0

        scoring = make_scorer(score_func)
        score = cross_val_score(clf, X, y, scoring=scoring, cv=3)
    assert_array_equal(score, [1.0, 1.0, 1.0])
    # Test that score function is called only 3 times (for cv=3)
    assert len(_score_func_args) == 3

def test_cross_val_score_with_score_func_classification():

    # Default score (should be the accuracy score)
    scores = cross_val_score(clf, iris.data, iris.target)
    assert_array_almost_equal(scores, [0.97, 1.0, 0.97, 0.97, 1.0], 2)

    # Correct classification score (aka. zero / one score) - should be the
    # same as the default estimator score
    zo_scores = cross_val_score(clf, iris.data, iris.target, scoring="accuracy")
    assert_array_almost_equal(zo_scores, [0.97, 1.0, 0.97, 0.97, 1.0], 2)

    # F1 score (class are balanced so f1_score should be equal to zero/one
    # score
    f1_scores = cross_val_score(clf, iris.data, iris.target, scoring="f1_weighted")
    assert_array_almost_equal(f1_scores, [0.97, 1.0, 0.97, 0.97, 1.0], 2)

def test_cross_val_score_with_score_func_regression():
    X, y = make_regression(n_samples=30, n_features=20, n_informative=5, random_state=0)
    reg = Ridge()

    # Default score of the Ridge regression estimator
    scores = cross_val_score(reg, X, y)
    assert_array_almost_equal(scores, [0.94, 0.97, 0.97, 0.99, 0.92], 2)

    # R2 score (aka. determination coefficient) - should be the
    r2_scores = cross_val_score(reg, X, y, scoring="r2")
    assert_array_almost_equal(r2_scores, [0.94, 0.97, 0.97, 0.99, 0.92], 2)

    # Mean squared error; this is a loss function, so "scores" are negative
    neg_mse_scores = cross_val_score(reg, X, y, scoring="neg_mean_squared_error")
    expected_neg_mse = np.array([-763.07, -553.16, -274.38, -273.26, -1681.99])
    assert_array_almost_equal(neg_mse_scores, expected_neg_mse, 2)

    # Explained variance
    scoring = make_scorer(explained_variance_score)
    ev_scores = cross_val_score(reg, X, y, scoring=scoring)
    assert_array_almost_equal(ev_scores, [0.94, 0.97, 0.97, 0.99, 0.92], 2)

def test_permutation_score(coo_container):
    cv = StratifiedKFold(2)

    score, scores, pvalue = permutation_test_score(
        svm, X, y, n_permutations=30, cv=cv, scoring="accuracy"
    )
    assert_almost_equal(pvalue, 0.0, 1)

    score_group, _, pvalue_group = permutation_test_score(
        svm,
        X,
        y,
        n_permutations=30,
        cv=cv,
        groups=np.ones(y.size),
    )
    assert score_group == score
    assert pvalue_group == pvalue

    # check that we obtain the same results with a sparse representation
    svm_sparse = SVC(kernel="linear")
    cv_sparse = StratifiedKFold(2)
        svm_sparse,
        X_sparse,
        y,
        cv=cv_sparse,
    )

    # test with custom scoring object
    def custom_score(y_true, y_pred):
        return ((y_true == y_pred).sum() - (y_true != y_pred).sum()) / y_true.shape[0]

    scorer = make_scorer(custom_score)
    score, _, pvalue = permutation_test_score(
        svm, X, y, n_permutations=100, scoring=scorer, cv=cv, random_state=0
    )
    assert_almost_equal(score, 0.93, 2)
    assert_almost_equal(pvalue, 0.01, 3)

    # set random y
    y = np.mod(np.arange(len(y)), 3)

    )

    assert score < 0.5
    assert pvalue > 0.2

def test_permutation_test_score_allow_nans():
    # Check that permutation_test_score allows input data with NaNs
    p = Pipeline(
        [
            ("imputer", SimpleImputer(strategy="mean", missing_values=np.nan)),
        ]
    )
    permutation_test_score(p, X, y)

def test_permutation_test_score_params():
    X = np.arange(100).reshape(10, 10)

    err_msg = r"Expected sample_weight to be passed"
        permutation_test_score(clf, X, y)

    err_msg = r"sample_weight.shape == \(1,\), expected \(8,\)!"
        permutation_test_score(clf, X, y, params={"sample_weight": np.ones(1)})
    permutation_test_score(clf, X, y, params={"sample_weight": np.ones(10)})

def test_cross_val_score_allow_nans():
    # Check that cross_val_score allows input data with NaNs
        [
        ]
    )
    cross_val_score(p, X, y)

def test_cross_val_score_multilabel():
        [
            [-3, 4],
            [2, 4],
            [3, 3],
            [0, 2],
            [-3, 1],
            [-2, 1],
            [0, 0],
            [-2, -1],
            [-1, -2],
            [1, -2],
        ]
    )
        [[1, 1], [0, 1], [0, 1], [0, 1], [1, 1], [0, 1], [1, 0], [1, 1], [1, 0], [0, 0]]
    )
    clf = KNeighborsClassifier(n_neighbors=1)
    scoring_micro = make_scorer(precision_score, average="micro")
    scoring_macro = make_scorer(precision_score, average="macro")
    scoring_samples = make_scorer(precision_score, average="samples")
    score_micro = cross_val_score(clf, X, y, scoring=scoring_micro)
    score_macro = cross_val_score(clf, X, y, scoring=scoring_macro)
    score_samples = cross_val_score(clf, X, y, scoring=scoring_samples)
    assert_almost_equal(score_micro, [1, 1 / 2, 3 / 4, 1 / 2, 1 / 3])
    assert_almost_equal(score_macro, [1, 1 / 2, 3 / 4, 1 / 2, 1 / 4])
    assert_almost_equal(score_samples, [1, 1 / 2, 3 / 4, 1 / 2, 1 / 4])

def test_cross_val_predict(coo_container):

    est = Ridge()

    # Naive loop (should be same as cross_val_predict):
    preds2 = np.zeros_like(y)
    for train, test in cv.split(X, y):
        est.fit(X[train], y[train])
        preds2[test] = est.predict(X[test])

    preds = cross_val_predict(est, X, y, cv=cv)
    assert_array_almost_equal(preds, preds2)

    preds = cross_val_predict(est, X, y)
    assert len(preds) == len(y)

    Xsp = X.copy()
    Xsp *= Xsp > np.median(Xsp)
    Xsp = coo_container(Xsp)
    preds = cross_val_predict(est, Xsp, y)
    assert_array_almost_equal(len(preds), len(y))

    preds = cross_val_predict(KMeans(n_init="auto"), X)

    class BadCV:
        def split(self, X, y=None, groups=None):
            for i in range(4):
                yield np.array([0, 1, 2, 3]), np.array([4, 5, 6, 7, 8])

        cross_val_predict(est, X, y, cv=BadCV())

        r"Number of classes in training fold \(2\) does "
        r"not match total number of classes \(3\). "
        "Results may not be appropriate for your use case."
    )
        cross_val_predict(
            LogisticRegression(solver="liblinear"),
            X,
            y,
            method="predict_proba",
            cv=KFold(2),
        )

def test_cross_val_predict_decision_function_shape():

    preds = cross_val_predict(
        LogisticRegression(solver="liblinear"), X, y, method="decision_function"
    )
    assert preds.shape == (50,)

    )
    assert preds.shape == (150, 3)

    # This specifically tests imbalanced splits for binary
    # classification with decision_function. This is only
    # applicable to classifiers that can be fit on a single
    # class.
    X = X[:100]
    y = y[:100]
        "Only 1 class/es in training fold,"
        " but 2 in overall dataset. This"
        " is not supported for decision_function"
        " with imbalanced folds. To fix "
        "this, use a cross-validation technique "
        "resulting in properly stratified folds"
    )
            RidgeClassifier(), X, y, method="decision_function", cv=KFold(2)
        )

    est = SVC(kernel="linear", decision_function_shape="ovo")

    preds = cross_val_predict(est, X, y, method="decision_function")
    assert preds.shape == (1797, 45)

    ind = np.argsort(y)
    X, y = X[ind], y[ind]
    error_message_regexp = (
        r"Output shape \(599L?, 21L?\) of "
        "decision_function does not match number of "
        r"classes \(7\) in fold. Irregular "
        "decision_function .*"
    )
        cross_val_predict(est, X, y, cv=KFold(n_splits=3), method="decision_function")

def test_cross_val_predict_predict_proba_shape():

        LogisticRegression(solver="liblinear"), X, y, method="predict_proba"
    )
    assert preds.shape == (50, 2)

    )

def test_cross_val_predict_predict_log_proba_shape():

        LogisticRegression(solver="liblinear"), X, y, method="predict_log_proba"
    )

    )

def test_cross_val_predict_input_types(coo_container):
    multioutput_y = np.column_stack([y, y[::-1]])

    clf = Ridge(fit_intercept=False, random_state=0)
    # 3 fold cv is used --> at least 3 samples per class
    # Smoke test
    predictions = cross_val_predict(clf, X, y)
    assert predictions.shape == (150,)

    # test with multioutput y
    predictions = cross_val_predict(clf, X_sparse, multioutput_y)
    assert predictions.shape == (150, 2)

    predictions = cross_val_predict(clf, X_sparse, y)
    assert_array_equal(predictions.shape, (150,))

    assert_array_equal(predictions.shape, (150, 2))

    predictions = cross_val_predict(clf, X.tolist(), y.tolist())

    predictions = cross_val_predict(clf, X, y.tolist())

    # test with X and y as list and non empty method
    predictions = cross_val_predict(
        LogisticRegression(solver="liblinear"),
        X.tolist(),
        y.tolist(),
        method="decision_function",
    )
        X,
    )

    check_3d = lambda x: x.ndim == 3
    clf = CheckingClassifier(check_X=check_3d)
    predictions = cross_val_predict(clf, X_3d, y)

def test_cross_val_predict_pandas():
    try:

        pass
        cross_val_predict(clf, X_df, y_ser, cv=3)

def test_cross_val_predict_unbalanced():
    )
    # Change the first sample to a new class
    y[0] = 2
    clf = LogisticRegression(random_state=1, solver="liblinear")
    cv = StratifiedKFold(n_splits=2)
    train, test = list(cv.split(X, y))
    yhat_proba = cross_val_predict(clf, X, y, cv=cv, method="predict_proba")
    assert y[test[0]][0] == 2  # sanity check for further assertions
    assert np.all(yhat_proba[test[0]][:, 2] == 0)
    assert np.all(yhat_proba[test[0]][:, 0:1] > 0)
    assert np.all(yhat_proba[test[1]] > 0)
    assert_array_almost_equal(yhat_proba.sum(axis=1), np.ones(y.shape), decimal=12)

def test_cross_val_predict_y_none():
    # ensure that cross_val_predict works when y is None
    mock_classifier = MockClassifier()
    y_hat = cross_val_predict(mock_classifier, X, y=None, cv=5, method="predict")
    assert_allclose(X[:, 0], y_hat)
    y_hat_proba = cross_val_predict(
        mock_classifier, X, y=None, cv=5, method="predict_proba"
    )
    assert_allclose(X, y_hat_proba)

def test_cross_val_score_sparse_fit_params(coo_container):
    fit_params = {"sparse_sample_weight": coo_container(np.eye(X.shape[0]))}
    a = cross_val_score(clf, X, y, params=fit_params, cv=3)
    assert_array_equal(a, np.ones(3))

def test_learning_curve():
    )
    estimator = MockImprovingEstimator(n_samples * ((n_splits - 1) / n_splits))
    for shuffle_train in [False, True]:
        with warnings.catch_warnings(record=True) as w:
            (
                train_sizes,
                train_scores,
                test_scores,
                fit_times,
                score_times,
            ) = learning_curve(
                X,
                y,
                cv=KFold(n_splits=n_splits),
                train_sizes=np.linspace(0.1, 1.0, 10),
                shuffle=shuffle_train,
                return_times=True,
            )
        if len(w) > 0:
            raise RuntimeError("Unexpected warning: %r" % w[0].message)
        assert train_scores.shape == (10, 3)
        assert test_scores.shape == (10, 3)
        assert fit_times.shape == (10, 3)
        assert score_times.shape == (10, 3)
        assert_array_equal(train_sizes, np.linspace(2, 20, 10))
        assert_array_almost_equal(train_scores.mean(axis=1), np.linspace(1.9, 1.0, 10))
        assert_array_almost_equal(test_scores.mean(axis=1), np.linspace(0.1, 1.0, 10))

        # Cannot use assert_array_almost_equal for fit and score times because
        # the values are hardware-dependant
        assert fit_times.dtype == "float64"
        assert score_times.dtype == "float64"

        # Test a custom cv splitter that can iterate only once
            train_sizes2, train_scores2, test_scores2 = learning_curve(
                X,
                y,
                cv=OneTimeSplitter(n_splits=n_splits, n_samples=n_samples),
            )
        assert_array_almost_equal(train_scores2, train_scores)
        assert_array_almost_equal(test_scores2, test_scores)

def test_learning_curve_unsupervised():
    X, _ = make_classification(
    )
    estimator = MockImprovingEstimator(20)
    train_sizes, train_scores, test_scores = learning_curve(
        estimator, X, y=None, cv=3, train_sizes=np.linspace(0.1, 1.0, 10)
    )
    assert_array_equal(train_sizes, np.linspace(2, 20, 10))
    assert_array_almost_equal(train_scores.mean(axis=1), np.linspace(1.9, 1.0, 10))
    assert_array_almost_equal(test_scores.mean(axis=1), np.linspace(0.1, 1.0, 10))

def test_learning_curve_verbose():
    )

    try:
            estimator, X, y, cv=3, verbose=1
        )
    finally:

    assert "[learning_curve]" in out

def test_learning_curve_incremental_learning_not_possible():
        n_samples=2,
    )
    # The mockup does not have partial_fit()
    estimator = MockImprovingEstimator(1)
        learning_curve(estimator, X, y, exploit_incremental_learning=True)

def test_learning_curve_incremental_learning():
    )
    estimator = MockIncrementalImprovingEstimator(20)
            X,
            y,
            cv=3,
            exploit_incremental_learning=True,
            train_sizes=np.linspace(0.1, 1.0, 10),
            shuffle=shuffle_train,
        )

def test_learning_curve_incremental_learning_unsupervised():
    )
        X,
        y=None,
        cv=3,
        exploit_incremental_learning=True,
        train_sizes=np.linspace(0.1, 1.0, 10),
    )

def test_learning_curve_batch_and_incremental_learning_are_equal():
    )
    train_sizes = np.linspace(0.2, 1.0, 5)
    estimator = PassiveAggressiveClassifier(max_iter=1, tol=None, shuffle=False)

    train_sizes_inc, train_scores_inc, test_scores_inc = learning_curve(
        X,
        y,
        cv=3,
    )
    train_sizes_batch, train_scores_batch, test_scores_batch = learning_curve(
        X,
        y,
        cv=3,
    )

    assert_array_equal(train_sizes_inc, train_sizes_batch)
        train_scores_inc.mean(axis=1), train_scores_batch.mean(axis=1)
    )
        test_scores_inc.mean(axis=1), test_scores_batch.mean(axis=1)
    )

def test_learning_curve_n_sample_range_out_of_bounds():
    )
        learning_curve(estimator, X, y, cv=3, train_sizes=[0, 1])
        learning_curve(estimator, X, y, cv=3, train_sizes=[0.0, 1.0])
        learning_curve(estimator, X, y, cv=3, train_sizes=[0.1, 1.1])
        learning_curve(estimator, X, y, cv=3, train_sizes=[0, 20])
        learning_curve(estimator, X, y, cv=3, train_sizes=[1, 21])

def test_learning_curve_remove_duplicate_sample_sizes():
        n_samples=3,
    )
    estimator = MockImprovingEstimator(2)
        "Removed duplicate entries from 'train_sizes'. Number of ticks "
        "will be less than the size of 'train_sizes': 2 instead of 3."
    )
        train_sizes, _, _ = learning_curve(
            estimator, X, y, cv=3, train_sizes=np.linspace(0.33, 1.0, 3)
        )
    assert_array_equal(train_sizes, [1, 2])

def test_learning_curve_with_boolean_indices():
    )
    cv = KFold(n_splits=3)
        estimator, X, y, cv=cv, train_sizes=np.linspace(0.1, 1.0, 10)
    )

def test_learning_curve_with_shuffle():
    # Following test case was designed this way to verify the code
    # changes made in pull request: #7506.
        [
            [1, 2],
            [3, 4],
            [5, 6],
            [7, 8],
            [11, 12],
            [13, 14],
            [15, 16],
            [17, 18],
            [19, 20],
            [7, 8],
            [9, 10],
            [11, 12],
            [13, 14],
            [15, 16],
            [17, 18],
        ]
    )
    y = np.array([1, 1, 1, 2, 3, 4, 1, 1, 2, 3, 4, 1, 2, 3, 4])
    groups = np.array([1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 4, 4, 4, 4])
    # Splits on these groups fail without shuffle as the first iteration
    # of the learning curve doesn't contain label 4 in the training set.
    estimator = PassiveAggressiveClassifier(max_iter=5, tol=None, shuffle=False)

    cv = GroupKFold(n_splits=2)
        X,
        y,
        cv=cv,
        n_jobs=1,
        train_sizes=np.linspace(0.3, 1.0, 3),
    )
        train_scores_batch.mean(axis=1), np.array([0.75, 0.3, 0.36111111])
    )
        test_scores_batch.mean(axis=1), np.array([0.36111111, 0.25, 0.25])
    )
        learning_curve(
            X,
            y,
            cv=cv,
            n_jobs=1,
            train_sizes=np.linspace(0.3, 1.0, 3),
        )

        X,
        y,
        cv=cv,
        n_jobs=1,
    )
    )
    )

def test_learning_curve_params():

        learning_curve(clf, X, y, error_score="raise")

    err_msg = r"sample_weight.shape == \(1,\), expected \(2,\)!"
            clf, X, y, error_score="raise", params={"sample_weight": np.ones(1)}
        )
    learning_curve(
        clf, X, y, error_score="raise", params={"sample_weight": np.ones(10)}
    )

def test_learning_curve_incremental_learning_params():
    )
    estimator = MockIncrementalImprovingEstimator(20, ["sample_weight"])
    err_msg = r"Expected fit parameter\(s\) \['sample_weight'\] not seen."
            X,
            y,
            cv=3,
        )

    err_msg = "Fit parameter sample_weight has length 3; expected"
            X,
            y,
            cv=3,
            params={"sample_weight": np.ones(3)},
        )

        X,
        y,
        cv=3,
        params={"sample_weight": np.ones(2)},
    )

def test_validation_curve():
    )
    param_range = np.linspace(0, 1, 10)
            MockEstimatorWithParameter(),
            X,
            y,
            param_name="param",
            cv=2,
        )
    if len(w) > 0:
        raise RuntimeError("Unexpected warning: %r" % w[0].message)

    assert_array_almost_equal(train_scores.mean(axis=1), param_range)
    assert_array_almost_equal(test_scores.mean(axis=1), 1 - param_range)

def test_validation_curve_clone_estimator():
    )

    param_range = np.linspace(1, 0, 10)
    _, _ = validation_curve(
        MockEstimatorWithSingleFitCallAllowed(),
        X,
        y,
        param_name="param",
        cv=2,
    )

def test_validation_curve_cv_splits_consistency():
    X, y = make_classification(n_samples=100, random_state=0)

    scores1 = validation_curve(
        SVC(kernel="linear", random_state=0),
        X,
        y,
        param_name="C",
        param_range=[0.1, 0.1, 0.2, 0.2],
        cv=OneTimeSplitter(n_splits=n_splits, n_samples=n_samples),
    )
    # The OneTimeSplitter is a non-re-entrant cv splitter. Unless, the
    # `split` is called for each parameter, the following should produce
    # identical results for param setting 1 and param setting 2 as both have
    # the same C value.
    assert_array_almost_equal(*np.vsplit(np.hstack(scores1)[(0, 2, 1, 3), :], 2))

    scores2 = validation_curve(
        X,
        y,
        cv=KFold(n_splits=n_splits, shuffle=True),
    )

    # For scores2, compare the 1st and 2nd parameter's scores
    # (Since the C value for 1st two param setting is 0.1, they must be
    # consistent unless the train test folds differ between the param settings)
    assert_array_almost_equal(*np.vsplit(np.hstack(scores2)[(0, 2, 1, 3), :], 2))

    scores3 = validation_curve(
        X,
        y,
        cv=KFold(n_splits=n_splits),
    )

    # OneTimeSplitter is basically unshuffled KFold(n_splits=5). Sanity check.
    assert_array_almost_equal(np.array(scores3), np.array(scores1))

def test_validation_curve_params():

        validation_curve(
            clf,
            X,
            y,
            param_name="foo_param",
            param_range=[1, 2, 3],
        )

            clf,
            X,
            y,
            params={"sample_weight": np.ones(1)},
        )
    validation_curve(
        clf,
        X,
        y,
        param_name="foo_param",
        param_range=[1, 2, 3],
        params={"sample_weight": np.ones(10)},
    )

def test_check_is_permutation():
    p = np.arange(100)
    assert _check_is_permutation(p, 100)
    assert not _check_is_permutation(np.delete(p, 23), 100)

    p[0] = 23
    assert not _check_is_permutation(p, 100)

    # Check if the additional duplicate indices are caught
    assert not _check_is_permutation(np.hstack((p, 0)), 100)

def test_cross_val_predict_sparse_prediction(csr_container):
    # check that cross_val_predict gives same result for sparse and dense input
        n_labels=1,
        return_indicator=True,
    )
    y_sparse = csr_container(y)
    classif = OneVsRestClassifier(SVC(kernel="linear"))
    preds = cross_val_predict(classif, X, y, cv=10)
    preds_sparse = cross_val_predict(classif, X_sparse, y_sparse, cv=10)
    preds_sparse = preds_sparse.toarray()
    assert_array_almost_equal(preds_sparse, preds)

def check_cross_val_predict_binary(est, X, y, method):
    """Helper for tests of cross_val_predict with binary classification"""
    cv = KFold(n_splits=3, shuffle=False)

    # Generate expected outputs
        exp_shape = (len(X),) if method == "decision_function" else (len(X), 2)
    else:
        exp_shape = y.shape
    expected_predictions = np.zeros(exp_shape)
        est = clone(est).fit(X[train], y[train])
        expected_predictions[test] = getattr(est, method)(X[test])

    # Check actual outputs for several representations of y
    for tg in [y, y + 1, y - 2, y.astype("str")]:
            cross_val_predict(est, X, tg, method=method, cv=cv), expected_predictions
        )

def check_cross_val_predict_multiclass(est, X, y, method):
    """Helper for tests of cross_val_predict with multiclass classification"""

    float_min = np.finfo(np.float64).min
    default_values = {
        "decision_function": float_min,
        "predict_log_proba": float_min,
        "predict_proba": 0,
    }
    expected_predictions = np.full(
        (len(X), len(set(y))), default_values[method], dtype=np.float64
    )
    _, y_enc = np.unique(y, return_inverse=True)
    for train, test in cv.split(X, y_enc):
        est = clone(est).fit(X[train], y_enc[train])
        fold_preds = getattr(est, method)(X[test])
        i_cols_fit = np.unique(y_enc[train])
        expected_predictions[np.ix_(test, i_cols_fit)] = fold_preds

        )

def check_cross_val_predict_multilabel(est, X, y, method):
    """Check the output of cross_val_predict for 2D targets using
    Estimators which provide a predictions as a list with one
    element per class.
    """

    # Create empty arrays of the correct size to hold outputs
    }
    expected_preds = []
    for i_col in range(n_targets):
        n_classes_in_label = len(set(y[:, i_col]))
        if n_classes_in_label == 2 and method == "decision_function":
            exp_shape = (len(X),)
        else:
            exp_shape = (len(X), n_classes_in_label)
        expected_preds.append(
            np.full(exp_shape, default_values[method], dtype=np.float64)
        )

    y_enc_cols = [
        np.unique(y[:, i], return_inverse=True)[1][:, np.newaxis]
        for i in range(y.shape[1])
    ]
    y_enc = np.concatenate(y_enc_cols, axis=1)
        for i_col in range(n_targets):
            fold_cols = np.unique(y_enc[train][:, i_col])
            if expected_preds[i_col].ndim == 1:
                # Decision function with <=2 classes
                expected_preds[i_col][test] = fold_preds[i_col]
            else:
                idx = np.ix_(test, fold_cols)
                expected_preds[i_col][idx] = fold_preds[i_col]

        cv_predict_output = cross_val_predict(est, X, tg, method=method, cv=cv)
        assert len(cv_predict_output) == len(expected_preds)
        for i in range(len(cv_predict_output)):
            assert_allclose(cv_predict_output[i], expected_preds[i])

def check_cross_val_predict_with_method_binary(est):
    # This test includes the decision_function with two classes.
    # This is a special case: it has only one column of output.
    for method in ["decision_function", "predict_proba", "predict_log_proba"]:
        check_cross_val_predict_binary(est, X, y, method)

def check_cross_val_predict_with_method_multiclass(est):
    X, y = shuffle(X, y, random_state=0)
        check_cross_val_predict_multiclass(est, X, y, method)

def test_cross_val_predict_with_method():
    check_cross_val_predict_with_method_binary(LogisticRegression(solver="liblinear"))
    check_cross_val_predict_with_method_multiclass(
        LogisticRegression(solver="liblinear")
    )

def test_cross_val_predict_method_checking():
    # Regression test for issue #9639. Tests that cross_val_predict does not
    # check estimator methods (e.g. predict_proba) before fitting
        est = SGDClassifier(loss="log_loss", random_state=2)

def test_gridsearchcv_cross_val_predict_with_method():
    est = GridSearchCV(
        LogisticRegression(random_state=42, solver="liblinear"), {"C": [0.1, 1]}, cv=2
    )

def test_cross_val_predict_with_method_multilabel_ovr():
    # OVR does multilabel predictions, but only arrays of
    # binary indicator columns. The output of predict_proba
    # is a 2D array with shape (n_samples, n_classes).
    n_samp = 100
        n_samples=n_samp, n_labels=3, n_classes=n_classes, n_features=5, random_state=42
    )
    est = OneVsRestClassifier(LogisticRegression(solver="liblinear", random_state=0))
    for method in ["predict_proba", "decision_function"]:
        check_cross_val_predict_binary(est, X, y, method=method)

class RFWithDecisionFunction(RandomForestClassifier):
    # None of the current multioutput-multiclass estimators have
    # decision function methods. Create a mock decision function
    # to test the cross_val_predict function's handling of this case.
        msg = "This helper should only be used on multioutput-multiclass tasks"
        assert isinstance(probs, list), msg
        probs = [p[:, -1] if p.shape[1] == 2 else p for p in probs]
        return probs

def test_cross_val_predict_with_method_multilabel_rf():
    # The RandomForest allows multiple classes in each label.
    # Output of predict_proba is a list of outputs of predict_proba
    # for each individual label.
        n_samples=100, n_labels=3, n_classes=n_classes, n_features=5, random_state=42
    )
    y[:, 0] += y[:, 1]  # Put three classes in the first column
    for method in ["predict_proba", "predict_log_proba", "decision_function"]:
        est = RFWithDecisionFunction(n_estimators=5, random_state=0)
            # Suppress "RuntimeWarning: divide by zero encountered in log"
            check_cross_val_predict_multilabel(est, X, y, method=method)

def test_cross_val_predict_with_method_rare_class():
    # Test a multiclass problem where one class will be missing from
    # one of the CV training sets.
    X = rng.normal(0, 1, size=(14, 10))
    y = np.array([0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 3])
    est = LogisticRegression(solver="liblinear")
            # Suppress warning about too few examples of a class
            check_cross_val_predict_multiclass(est, X, y, method)

def test_cross_val_predict_with_method_multilabel_rf_rare_class():
    # The RandomForest allows anything for the contents of the labels.
    # In this test, the first label has a class with a single example.
    # We'll have one CV fold where the training data don't include it.
    X = rng.normal(0, 1, size=(5, 10))
    y = np.array([[0, 0], [1, 1], [2, 1], [0, 1], [1, 0]])
    for method in ["predict_proba", "predict_log_proba"]:

def get_expected_predictions(X, y, cv, classes, est, method):
    expected_predictions = np.zeros([len(y), classes])

        expected_predictions_ = func(X[test])
        # To avoid 2 dimensional indexing
        if method == "predict_proba":
            exp_pred_test = np.zeros((len(test), classes))
        else:
            exp_pred_test = np.full(
                (len(test), classes), np.finfo(expected_predictions.dtype).min
            )
        exp_pred_test[:, est.classes_] = expected_predictions_
        expected_predictions[test] = exp_pred_test

    return expected_predictions

def test_cross_val_predict_class_subset():
    X = np.arange(200).reshape(100, 2)
    y = np.array([x // 10 for x in range(100)])
    classes = 10

    kfold3 = KFold(n_splits=3)
    kfold4 = KFold(n_splits=4)

    methods = ["decision_function", "predict_proba", "predict_log_proba"]
        est = LogisticRegression(solver="liblinear")

        # Test with n_splits=3
        predictions = cross_val_predict(est, X, y, method=method, cv=kfold3)

        # Runs a naive loop (should be same as cross_val_predict):
        expected_predictions = get_expected_predictions(
            X, y, kfold3, classes, est, method
        )
        assert_array_almost_equal(expected_predictions, predictions)

        # Test with n_splits=4
        predictions = cross_val_predict(est, X, y, method=method, cv=kfold4)
            X, y, kfold4, classes, est, method
        )

        # Testing unordered labels
        y = shuffle(np.repeat(range(10), 10), random_state=0)
        y = le.fit_transform(y)
        )

def test_score_memmap():
    # Ensure a scalar score of memmap type is accepted
    tf = tempfile.NamedTemporaryFile(mode="wb", delete=False)
    tf.write(b"Hello world!!!!!")
    tf.close()
    scores = np.memmap(tf.name, dtype=np.float64)
    score = np.memmap(tf.name, shape=(), mode="r", dtype=np.float64)
    try:
        cross_val_score(clf, X, y, scoring=lambda est, X, y: score)
            cross_val_score(clf, X, y, scoring=lambda est, X, y: scores)
    finally:
        # Best effort to release the mmap file handles before deleting the
        # backing file under Windows
        scores, score = None, None
        for _ in range(3):
            try:
                os.unlink(tf.name)
                break
            except OSError:
                sleep(1.0)

def test_permutation_test_score_pandas():
    # check permutation_test_score doesn't destroy pandas dataframe
    try:

        pass
        iris = load_iris()
        X, y = iris.data, iris.target
        X_df, y_ser = InputFeatureType(X), TargetType(y)
        permutation_test_score(clf, X_df, y_ser)

def test_fit_and_score_failing():
    # Create a failing classifier to deliberately fail
    failing_clf = FailingClassifier(FailingClassifier.FAILING_PARAMETER)
    # dummy X data
    X = np.arange(1, 10)
    train, test = np.arange(0, 5), np.arange(5, 9)
    fit_and_score_args = dict(
        estimator=failing_clf,
        X=X,
        y=None,
        scorer=dict(),
        train=train,
        test=test,
        parameters=None,
        score_params=None,
    )
    # passing error score to trigger the warning message
    fit_and_score_args["error_score"] = "raise"
    # check if exception was raised, with default error_score='raise'
    with pytest.raises(ValueError, match="Failing classifier failed as required"):
        _fit_and_score(**fit_and_score_args)

    assert failing_clf.score() == 0.0  # FailingClassifier coverage

def test_fit_and_score_working():
    X, y = make_classification(n_samples=30, random_state=0)
    train, test = next(ShuffleSplit().split(X))
    # Test return_parameters option
        X=X,
        y=y,
        parameters={"max_iter": 100, "tol": 0.1},
        return_parameters=True,
    )
    result = _fit_and_score(**fit_and_score_args)
    assert result["parameters"] == fit_and_score_args["parameters"]

class DataDependentFailingClassifier(BaseEstimator):
    def __init__(self, max_x_value=None):
        self.max_x_value = max_x_value

        num_values_too_high = (X > self.max_x_value).sum()
        if num_values_too_high:
                f"Classifier fit failed with {num_values_too_high} values too high"
            )

        return 0.0

def test_cross_validate_some_failing_fits_warning(error_score):
    failing_clf = DataDependentFailingClassifier(max_x_value=8)
    y = np.ones(9)
    cross_validate_args = [failing_clf, X, y]
    cross_validate_kwargs = {"cv": 3, "error_score": error_score}
    # check if the warning message type is as expected

    individual_fit_error_message = (
        "ValueError: Classifier fit failed with 1 values too high"
    )
    warning_message = re.compile(
        (
            "2 fits failed.+total of 3.+The score on these"
            " train-test partitions for these parameters will be set to"
            f" {cross_validate_kwargs['error_score']}.+{individual_fit_error_message}"
        ),
        flags=re.DOTALL,
    )

    with pytest.warns(FitFailedWarning, match=warning_message):
        cross_validate(*cross_validate_args, **cross_validate_kwargs)

def test_cross_validate_all_failing_fits_error(error_score):

    cross_validate_kwargs = {"cv": 7, "error_score": error_score}

    individual_fit_error_message = "ValueError: Failing classifier failed as required"
    error_message = re.compile(
        (
            "All the 7 fits failed.+your model is misconfigured.+"
            f"{individual_fit_error_message}"
        ),
    )

def _failing_scorer(estimator, X, y, error_msg):
    raise ValueError(error_msg)

def test_cross_val_score_failing_scorer(error_score):
    # check that an estimator can fail during scoring in `cross_val_score` and
    # that we can optionally replaced it with `error_score`
    clf = LogisticRegression(max_iter=5).fit(X, y)

    error_msg = "This scorer is supposed to fail!!!"
    failing_scorer = partial(_failing_scorer, error_msg=error_msg)

    if error_score == "raise":
                clf, X, y, cv=3, scoring=failing_scorer, error_score=error_score
            )
    else:
        warning_msg = (
            "Scoring failed. The score on this train-test partition for "
            f"these parameters will be set to {error_score}"
        )
        with pytest.warns(UserWarning, match=warning_msg):
            scores = cross_val_score(
            )
            assert_allclose(scores, error_score)

def test_cross_validate_failing_scorer(
    error_score, return_train_score, with_multimetric
):
    # Check that an estimator can fail during scoring in `cross_validate` and
    # that we can optionally replace it with `error_score`. In the multimetric
    # case also check the result of a non-failing scorer where the other scorers
    # are failing.

    if with_multimetric:
        non_failing_scorer = make_scorer(mean_squared_error)
        scoring = {
            "score_1": failing_scorer,
            "score_2": non_failing_scorer,
            "score_3": failing_scorer,
        }
    else:
        scoring = failing_scorer

            cross_validate(
                clf,
                X,
                y,
                cv=3,
                scoring=scoring,
                return_train_score=return_train_score,
            )
    else:
        )
            results = cross_validate(
                clf,
                X,
                y,
                cv=3,
            )
            for key in results:
                if "_score" in key:
                    if "_score_2" in key:
                        # check the test (and optionally train) score for the
                        # scorer that should be non-failing
                        for i in results[key]:
                            assert isinstance(i, float)
                    else:
                        # check the test (and optionally train) score for all
                        # scorers that should be assigned to `error_score`.
                        assert_allclose(results[key], error_score)

def test_fit_and_score_verbosity(
    capsys, train_score, scorer, verbose, split_prg, cdt_prg, expected
):

    # test print without train score
        X=X,
        y=y,
        scorer=scorer,
        return_train_score=train_score,
        split_progress=split_prg,
        candidate_progress=cdt_prg,
    )
    _fit_and_score(**fit_and_score_args)
    outlines = out.split("\n")
    if len(outlines) > 2:
        assert re.match(expected, outlines[1])
    else:
        assert re.match(expected, outlines[0])

def test_score():
    error_message = "scoring must return a number, got None"

    def two_params_scorer(estimator, X_test):

        _score(
            X_test=None,
            y_test=None,
            scorer=two_params_scorer,
            score_params=None,
            error_score=np.nan,
        )

def test_callable_multimetric_confusion_matrix_cross_validate():
        cm = confusion_matrix(y, y_pred)
        return {"tn": cm[0, 0], "fp": cm[0, 1], "fn": cm[1, 0], "tp": cm[1, 1]}

    X, y = make_classification(n_samples=40, n_features=4, random_state=42)
    est = LinearSVC(random_state=42)
    cv_results = cross_validate(est, X, y, cv=5, scoring=custom_scorer)

    score_names = ["tn", "fp", "fn", "tp"]
    for name in score_names:
        assert "test_{}".format(name) in cv_results

def test_learning_curve_partial_fit_regressors():
    """Check that regressors with partial_fit is supported.

    Non-regression test for #22981.
    """

    # Does not error
    learning_curve(MLPRegressor(), X, y, exploit_incremental_learning=True, cv=2)

def test_learning_curve_some_failing_fits_warning(global_random_seed):
    """Checks for fit failures in `learning_curve` and raises the required warning"""

        n_informative=6,
    )
    # sorting the target to trigger SVC error on the 2 first splits because a single
    # class is present
    sorted_idx = np.argsort(y)
    X, y = X[sorted_idx], y[sorted_idx]

    svc = SVC()
    warning_message = "10 fits failed out of a total of 25"

        _, train_score, test_score, *_ = learning_curve(
            svc, X, y, cv=5, error_score=np.nan
        )

    # the first 2 splits should lead to warnings and thus np.nan scores
    for idx in range(2):
        assert np.isnan(train_score[idx]).all()
        assert np.isnan(test_score[idx]).all()

    for idx in range(2, train_score.shape[0]):
        assert not np.isnan(train_score[idx]).any()
        assert not np.isnan(test_score[idx]).any()

def test_cross_validate_return_indices(global_random_seed):
    """Check the behaviour of `return_indices` in `cross_validate`."""
    X = scale(X)  # scale features for better convergence

    cv = KFold(n_splits=3, shuffle=True, random_state=global_random_seed)
    cv_results = cross_validate(estimator, X, y, cv=cv, n_jobs=2, return_indices=False)
    assert "indices" not in cv_results

    cv_results = cross_validate(estimator, X, y, cv=cv, n_jobs=2, return_indices=True)
    assert "indices" in cv_results
    train_indices = cv_results["indices"]["train"]
    test_indices = cv_results["indices"]["test"]
    assert len(train_indices) == cv.n_splits
    assert len(test_indices) == cv.n_splits

    assert_array_equal([indices.size for indices in train_indices], 100)
    assert_array_equal([indices.size for indices in test_indices], 50)

    for split_idx, (expected_train_idx, expected_test_idx) in enumerate(cv.split(X, y)):
        assert_array_equal(train_indices[split_idx], expected_train_idx)
        assert_array_equal(test_indices[split_idx], expected_test_idx)

def test_fit_param_deprecation(func, extra_args):
    """Check that we warn about deprecating `fit_params`."""
    with pytest.warns(FutureWarning, match="`fit_params` is deprecated"):
        func(
            estimator=ConsumingClassifier(), X=X, y=y, cv=2, fit_params={}, **extra_args
        )

        ValueError, match="`params` and `fit_params` cannot both be provided"
    ):
        func(
            estimator=ConsumingClassifier(),
            X=X,
            y=y,
            fit_params={},
            params={},
            **extra_args,
        )

def test_groups_with_routing_validation(func, extra_args):
    """Check that we raise an error if `groups` are passed to the cv method instead
    of `params` when metadata routing is enabled.
    """
    with pytest.raises(ValueError, match="`groups` can only be passed if"):
        func(
            X=X,
            y=y,
            groups=[],
        )

def test_cross_validate_params_none(func, extra_args):
    """Test that no errors are raised when passing `params=None`, which is the
    default value.
    Non-regression test for: https://github.com/scikit-learn/scikit-learn/issues/30447
    """
    X, y = make_classification(n_samples=100, n_classes=2, random_state=0)
    func(estimator=ConsumingClassifier(), X=X, y=y, **extra_args)

def test_passed_unrequested_metadata(func, extra_args):
    """Check that we raise an error when passing metadata that is not

        "[metadata] are passed but are not explicitly set as requested or not "
        "requested for ConsumingClassifier.fit, which is used within"
    )
    with pytest.raises(UnsetMetadataPassedError, match=err_msg):
        func(
            X=X,
            y=y2,
            params=dict(metadata=[]),
        )

    # cross_val_predict doesn't use scoring
    if func == cross_val_predict:
        return

        "requested for ConsumingClassifier.score, which is used within"
    )
        func(
            estimator=ConsumingClassifier()
            .set_fit_request(metadata=True)
            .set_partial_fit_request(metadata=True),
            X=X,
            y=y2,
        )

def test_validation_functions_routing(func, extra_args):
    """Check that the respective cv method is properly dispatching the metadata
    to the consumer."""
    scorer_registry = _Registry()
    scorer = ConsumingScorer(registry=scorer_registry).set_score_request(
        sample_weight="score_weights", metadata="score_metadata"
    )
    splitter_registry = _Registry()
    splitter = ConsumingSplitter(registry=splitter_registry).set_split_request(
        groups="split_groups", metadata="split_metadata"
    )
    estimator_registry = _Registry()
    estimator = ConsumingClassifier(registry=estimator_registry).set_fit_request(
        sample_weight="fit_sample_weight", metadata="fit_metadata"
    )

    score_weights = rng.rand(n_samples)
    score_metadata = rng.rand(n_samples)
    split_groups = rng.randint(0, 3, n_samples)
    split_metadata = rng.rand(n_samples)
    fit_sample_weight = rng.rand(n_samples)
    fit_metadata = rng.rand(n_samples)

    scoring_args = {
        cross_validate: dict(scoring=dict(my_scorer=scorer, accuracy="accuracy")),
        cross_val_score: dict(scoring=scorer),
        learning_curve: dict(scoring=scorer),
        validation_curve: dict(scoring=scorer),
        permutation_test_score: dict(scoring=scorer),
        cross_val_predict: dict(),
    }

        split_groups=split_groups,
        split_metadata=split_metadata,
        fit_sample_weight=fit_sample_weight,
        fit_metadata=fit_metadata,
    )

    if func is not cross_val_predict:
        params.update(
            score_weights=score_weights,
            score_metadata=score_metadata,
        )

    func(
        X=X,
        y=y,
        cv=splitter,
        **scoring_args[func],
        **extra_args,
    )

        # cross_val_predict doesn't need a scorer
        assert len(scorer_registry)
    for _scorer in scorer_registry:
            obj=_scorer,
            method="score",
            parent=func.__name__,
            split_params=("sample_weight", "metadata"),
            sample_weight=score_weights,
            metadata=score_metadata,
        )

    assert len(splitter_registry)
    for _splitter in splitter_registry:
            obj=_splitter,
            method="split",
            groups=split_groups,
            metadata=split_metadata,
        )

    assert len(estimator_registry)
    for _estimator in estimator_registry:
            obj=_estimator,
            method="fit",
            sample_weight=fit_sample_weight,
            metadata=fit_metadata,
        )

def test_learning_curve_exploit_incremental_learning_routing():
    """Test that learning_curve routes metadata to the estimator correctly while
    partial_fitting it with `exploit_incremental_learning=True`."""

    estimator = ConsumingClassifier(
        registry=estimator_registry
    ).set_partial_fit_request(
    )

        X=X,
        y=y,
        cv=ConsumingSplitter(),
        params=dict(fit_sample_weight=fit_sample_weight, fit_metadata=fit_metadata),
    )

            method="partial_fit",
            parent="learning_curve",
        )

        else:

                )
                    value
                    )

        self.a = a

    def fit(
        self,
        X,
        Y=None,
    ):
            - int
            - object
            - function
        """

                )
            )
            )
            fmt = (
            )
            )
            fmt = (
            )
            )

        return {
        }

        return 1.0

            )

def test_fit_and_score_over_thresholds_curve_scorers():
    """Check that `_fit_and_score_over_thresholds` returns thresholds in ascending order
    for the different accepted curve scorers."""
    train_idx, val_idx = np.arange(50), np.arange(50, 100)

        score_func=balanced_accuracy_score,
        sign=1,
    )
    scores, thresholds = _fit_and_score_over_thresholds(
        X,
        y,
        fit_params={},
        train_idx=train_idx,
        val_idx=val_idx,
        curve_scorer=curve_scorer,
        score_params={},
    )

    assert np.all(thresholds[:-1] <= thresholds[1:])
    assert isinstance(scores, np.ndarray)
    assert np.logical_and(scores >= 0, scores <= 1).all()

def test_fit_and_score_over_thresholds_prefit():
    """Check the behaviour with a prefit classifier."""

    # `train_idx is None` to indicate that the classifier is prefit
    train_idx, val_idx = None, np.arange(50, 100)
    classifier = DecisionTreeClassifier(random_state=0).fit(X, y)
    # make sure that the classifier memorized the full dataset such that
    # we get perfect predictions and thus match the expected score
    assert classifier.score(X[val_idx], y[val_idx]) == pytest.approx(1.0)

        sign=1,
        thresholds=2,
    )
        X,
        y,
    )
    assert_allclose(scores, [0.5, 1.0])

def test_fit_and_score_over_thresholds_sample_weight():
    """Check that we dispatch the sample-weight to fit and score the classifier."""
    X, y = X[:100], y[:100]  # only 2 classes

    # create a dataset and repeat twice the sample of class #0
    X_repeated, y_repeated = np.vstack([X, X[y == 0]]), np.hstack([y, y[y == 0]])
    # create a sample weight vector that is equivalent to the repeated dataset
    sample_weight[:50] *= 2

    train_repeated_idx = np.arange(X_repeated.shape[0])
    val_repeated_idx = np.arange(X_repeated.shape[0])
        sign=1,
    )
    scores_repeated, thresholds_repeated = _fit_and_score_over_thresholds(
        X_repeated,
        y_repeated,
        train_idx=train_repeated_idx,
        val_idx=val_repeated_idx,
    )

    train_idx, val_idx = np.arange(X.shape[0]), np.arange(X.shape[0])
        classifier.set_fit_request(sample_weight=True),
        X,
        y,
        fit_params={"sample_weight": sample_weight},
        curve_scorer=curve_scorer.set_score_request(sample_weight=True),
        score_params={"sample_weight": sample_weight},
    )

    assert_allclose(thresholds_repeated, thresholds)
    assert_allclose(scores_repeated, scores)

def test_fit_and_score_over_thresholds_fit_params(fit_params_type):
    """Check that we pass `fit_params` to the classifier when calling `fit`."""
    }

    classifier = CheckingClassifier(expected_fit_params=["a", "b"], random_state=0)
    classifier.set_fit_request(a=True, b=True)

        sign=1,
    )
    _fit_and_score_over_thresholds(
        X,
        y,
        fit_params=fit_params,
    )

def test_tuned_threshold_classifier_no_binary(data):
    """Check that we raise an informative error message for non-binary problem."""
    err_msg = "Only binary classification is supported."
        TunedThresholdClassifierCV(LogisticRegression()).fit(*data)

def test_tuned_threshold_classifier_conflict_cv_refit(params, err_type, err_msg):
    """Check that we raise an informative error message when `cv` and `refit`
    cannot be used together.
    """
        TunedThresholdClassifierCV(LogisticRegression(), **params).fit(X, y)

def test_threshold_classifier_estimator_response_methods(
    ThresholdClassifier, estimator, response_method
):
    """Check that `TunedThresholdClassifierCV` exposes the same response methods as the
    underlying estimator.
    """

    model = ThresholdClassifier(estimator=estimator)
    assert hasattr(model, response_method) == hasattr(estimator, response_method)

    if hasattr(model, response_method):
        y_pred_cutoff = getattr(model, response_method)(X)
        y_pred_underlying_estimator = getattr(model.estimator_, response_method)(X)

        assert_allclose(y_pred_cutoff, y_pred_underlying_estimator)

def test_tuned_threshold_classifier_without_constraint_value(response_method):
    """Check that `TunedThresholdClassifierCV` is optimizing a given objective
    metric."""
    # remove feature to degrade performances
    X = X[:, :5]

    # make the problem completely imbalanced such that the balanced accuracy is low
    indices_pos = np.flatnonzero(y == 1)
    indices_pos = indices_pos[: indices_pos.size // 50]
    indices_neg = np.flatnonzero(y == 0)

    X = np.vstack([X[indices_neg], X[indices_pos]])
    y = np.hstack([y[indices_neg], y[indices_pos]])

    lr = make_pipeline(StandardScaler(), LogisticRegression()).fit(X, y)
    thresholds = 100
    model = TunedThresholdClassifierCV(
        estimator=lr,
        thresholds=thresholds,
    )
    score_optimized = balanced_accuracy_score(y, model.fit(X, y).predict(X))
    score_baseline = balanced_accuracy_score(y, lr.predict(X))
    assert score_optimized > score_baseline
    assert model.cv_results_["thresholds"].shape == (thresholds,)
    assert model.cv_results_["scores"].shape == (thresholds,)

def test_tuned_threshold_classifier_metric_with_parameter():
    """Check that we can pass a metric with a parameter in addition check that
    `f_beta` with `beta=1` is equivalent to `f1` and different from `f_beta` with
    `beta=2`.
    """
    model_fbeta_1 = TunedThresholdClassifierCV(
        estimator=lr, scoring=make_scorer(fbeta_score, beta=1)
    model_fbeta_2 = TunedThresholdClassifierCV(
        estimator=lr, scoring=make_scorer(fbeta_score, beta=2)
    model_f1 = TunedThresholdClassifierCV(
        estimator=lr, scoring=make_scorer(f1_score)

    assert model_fbeta_1.best_threshold_ == pytest.approx(model_f1.best_threshold_)
    assert model_fbeta_1.best_threshold_ != pytest.approx(model_fbeta_2.best_threshold_)

def test_tuned_threshold_classifier_with_string_targets(response_method, metric):
    """Check that targets represented by str are properly managed.
    Also, check with several metrics to be sure that `pos_label` is properly
    dispatched.
    """
    # Encode numeric targets by meaningful strings. We purposely designed the class
    # names such that the `pos_label` is the first alphabetically sorted class and thus
    # encoded as 0.
    classes = np.array(["cancer", "healthy"], dtype=object)
    y = classes[y]
        estimator=make_pipeline(StandardScaler(), LogisticRegression()),
        scoring=metric,
    assert_array_equal(model.classes_, np.sort(classes))
    assert_array_equal(np.unique(y_pred), np.sort(classes))

def test_tuned_threshold_classifier_refit(with_sample_weight, global_random_seed):
    """Check the behaviour of the `refit` parameter."""
        sample_weight = rng.randn(X.shape[0])
        sample_weight = np.abs(sample_weight, out=sample_weight)
    else:

    # check that `estimator_` if fitted on the full dataset when `refit=True`
    estimator = LogisticRegression().set_fit_request(sample_weight=True)
    model = TunedThresholdClassifierCV(estimator, refit=True).fit(
        X, y, sample_weight=sample_weight
    )

    assert model.estimator_ is not estimator
    assert_allclose(model.estimator_.coef_, estimator.coef_)
    assert_allclose(model.estimator_.intercept_, estimator.intercept_)

    # check that `estimator_` was not altered when `refit=False` and `cv="prefit"`
    coef = estimator.coef_.copy()
    model = TunedThresholdClassifierCV(estimator, cv="prefit", refit=False).fit(
    )

    assert model.estimator_ is estimator
    assert_allclose(model.estimator_.coef_, coef)

    # check that we train `estimator_` on the training split of a given cross-validation
    cv = [
        (np.arange(50), np.arange(50, 100)),
    ]  # single split
    model = TunedThresholdClassifierCV(estimator, cv=cv, refit=False).fit(
    )

        sw_train = sample_weight[cv[0][0]]
    else:
    estimator.fit(X[cv[0][0]], y[cv[0][0]], sample_weight=sw_train)

def test_tuned_threshold_classifier_fit_params(fit_params_type):
    }

    model = TunedThresholdClassifierCV(classifier)
    model.fit(X, y, **fit_params)

def test_tuned_threshold_classifier_cv_zeros_sample_weights_equivalence():
    """Check that passing removing some sample from the dataset `X` is
    equivalent to passing a `sample_weight` with a factor 0."""
    # Only use 2 classes and select samples such that 2-fold cross-validation
    # split will lead to an equivalence with a `sample_weight` of 0
    X = np.vstack((X[:40], X[50:90]))
    y = np.hstack((y[:40], y[50:90]))
    sample_weight[::2] = 1

    model_without_weights = TunedThresholdClassifierCV(estimator, cv=2)
    model_with_weights = clone(model_without_weights)

    model_with_weights.fit(X, y, sample_weight=sample_weight)
    model_without_weights.fit(X[::2], y[::2])

        model_with_weights.estimator_.coef_, model_without_weights.estimator_.coef_
    )

    y_pred_with_weights = model_with_weights.predict_proba(X)
    y_pred_without_weights = model_without_weights.predict_proba(X)

def test_tuned_threshold_classifier_thresholds_array():
    """Check that we can pass an array to `thresholds` and it is used as candidate
    threshold internally."""
    thresholds = np.linspace(0, 1, 11)
    tuned_model = TunedThresholdClassifierCV(
    assert_allclose(tuned_model.cv_results_["thresholds"], thresholds)

def test_tuned_threshold_classifier_store_cv_results(store_cv_results):
    """Check that if `cv_results_` exists depending on `store_cv_results`."""
        estimator, store_cv_results=store_cv_results
    if store_cv_results:
        assert hasattr(tuned_model, "cv_results_")
    else:
        assert not hasattr(tuned_model, "cv_results_")

def test_tuned_threshold_classifier_cv_float():
    """Check the behaviour when `cv` is set to a float."""

    # case where `refit=False` and cv is a float: the underlying estimator will be fit
    # on the training set given by a ShuffleSplit. We check that we get the same model
    # coefficients.
    test_size = 0.3
        estimator, cv=test_size, refit=False, random_state=0
    tuned_model.fit(X, y)

    cv = StratifiedShuffleSplit(n_splits=1, test_size=test_size, random_state=0)
    train_idx, val_idx = next(cv.split(X, y))
    cloned_estimator = clone(estimator).fit(X[train_idx], y[train_idx])

    assert_allclose(tuned_model.estimator_.coef_, cloned_estimator.coef_)

    # case where `refit=True`, then the underlying estimator is fitted on the full
    # dataset.
    tuned_model.set_params(refit=True).fit(X, y)
    cloned_estimator = clone(estimator).fit(X, y)

def test_tuned_threshold_classifier_error_constant_predictor():
    """Check that we raise a ValueError if the underlying classifier returns constant
    probabilities such that we cannot find any threshold.
    """
    estimator = DummyClassifier(strategy="constant", constant=1)
    tuned_model = TunedThresholdClassifierCV(estimator, response_method="predict_proba")
    err_msg = "The provided estimator makes constant predictions"
        tuned_model.fit(X, y)

def test_fixed_threshold_classifier_equivalence_default(response_method):
    """Check that `FixedThresholdClassifier` has the same behaviour as the vanilla
    classifier.
    """
    classifier_default_threshold = FixedThresholdClassifier(
        estimator=clone(classifier), response_method=response_method
    )
    classifier_default_threshold.fit(X, y)

    # emulate the response method that should take into account the `pos_label`
    if response_method in ("auto", "predict_proba"):
        y_score = classifier_default_threshold.predict_proba(X)[:, 1]
        threshold = 0.5
    else:  # response_method == "decision_function"
        y_score = classifier_default_threshold.decision_function(X)
        threshold = 0.0

    y_pred_lr = (y_score >= threshold).astype(int)
    assert_allclose(classifier_default_threshold.predict(X), y_pred_lr)

def test_fixed_threshold_classifier(response_method, threshold, pos_label):
    """Check that applying `predict` lead to the same prediction as applying the
    threshold to the output of the response method.
    """
    logistic_regression = LogisticRegression().fit(X, y)
    model = FixedThresholdClassifier(
        estimator=clone(logistic_regression),

    # check that the underlying estimator is the same
    assert_allclose(model.estimator_.coef_, logistic_regression.coef_)

        y_score = model.predict_proba(X)[:, pos_label]
        y_score = model.decision_function(X)
        y_score = y_score if pos_label == 1 else -y_score

    # create a mapping from boolean values to class labels
    map_to_label = np.array([0, 1]) if pos_label == 1 else np.array([1, 0])
    y_pred_lr = map_to_label[(y_score >= threshold).astype(int)]
    assert_allclose(model.predict(X), y_pred_lr)

    for method in ("predict_proba", "predict_log_proba", "decision_function"):
            getattr(model, method)(X), getattr(logistic_regression, method)(X)
        )
            getattr(model.estimator_, method)(X),
            getattr(logistic_regression, method)(X),
        )

def test_fixed_threshold_classifier_metadata_routing():
    """Check that everything works with metadata routing."""
    sample_weight[::2] = 2
    classifier = LogisticRegression().set_fit_request(sample_weight=True)
    classifier.fit(X, y, sample_weight=sample_weight)
    classifier_default_threshold = FixedThresholdClassifier(estimator=clone(classifier))
    classifier_default_threshold.fit(X, y, sample_weight=sample_weight)
    assert_allclose(classifier_default_threshold.estimator_.coef_, classifier.coef_)

def test_fixed_threshold_classifier_fitted_estimator(method):
    """Check that if the underlying estimator is already fitted, no fit is required."""
    fixed_threshold_classifier = FixedThresholdClassifier(estimator=classifier)
    # This should not raise an error
    getattr(fixed_threshold_classifier, method)(X)

def test_fixed_threshold_classifier_classes_():
    """Check that the classes_ attribute is properly set."""
        AttributeError, match="The underlying estimator is not fitted yet."
    ):
        FixedThresholdClassifier(estimator=LogisticRegression()).classes_

    assert_array_equal(fixed_threshold_classifier.classes_, classifier.classes_)

class FastClassifier(DummyClassifier):
    """Dummy classifier that accepts parameters a, b, ... z.

    These parameter don't affect the predictions and are useful for fast
    grid searching."""

    # update the constraints such that we accept all parameters from a to z
        **DummyClassifier._parameter_constraints,
        **{chr(key): "no_validation" for key in range(ord("a"), ord("z") + 1)},
    }

        self, strategy="stratified", random_state=None, constant=None, **kwargs
    ):
            strategy=strategy, random_state=random_state, constant=constant
        )

        params = super().get_params(deep=deep)
        for char in range(ord("a"), ord("z") + 1):
            params[chr(char)] = "whatever"

class SometimesFailClassifier(DummyClassifier):
        self,
        strategy="stratified",
        constant=None,
        fail_fit=False,
        fail_predict=False,
        a=0,
    ):
        self.fail_fit = fail_fit
        self.fail_predict = fail_predict
        self.a = a

        )

        if self.fail_fit:
            raise Exception("fitting failed")

        if self.fail_predict:
            raise Exception("predict failed")

def test_nan_handling(HalvingSearch, fail_at):
    """Check the selection of the best scores in presence of failure represented by
    NaN values."""
    X, y = make_classification(n_samples=n_samples, random_state=0)

    search = HalvingSearch(
        SometimesFailClassifier(),
        {f"fail_{fail_at}": [False, True], "a": range(3)},
        resource="n_estimators",
        max_resources=6,
        min_resources=1,
        factor=2,
    )

    search.fit(X, y)

    # estimators that failed during fit/predict should always rank lower
    # than ones where the fit/predict succeeded
    assert not search.best_params_[f"fail_{fail_at}"]
    scores = search.cv_results_["mean_test_score"]
    ranks = search.cv_results_["rank_test_score"]

    # some scores should be NaN
    assert np.isnan(scores).any()

    unique_nan_ranks = np.unique(ranks[np.isnan(scores)])
    # all NaN scores should have the same rank
    assert unique_nan_ranks.shape[0] == 1
    # NaNs should have the lowest rank
    assert (unique_nan_ranks[0] >= ranks).all()

def test_aggressive_elimination(
    Est,
    aggressive_elimination,
    max_resources,
    expected_n_iterations,
    expected_n_required_iterations,
    expected_n_possible_iterations,
    expected_n_remaining_candidates,
    expected_n_candidates,
    expected_n_resources,
):
    # Test the aggressive_elimination parameter.

    param_grid = {"a": ("l1", "l2"), "b": list(range(30))}
    base_estimator = FastClassifier()

    if max_resources == "limited":
        max_resources = 180
    else:
        max_resources = n_samples

    sh = Est(
        base_estimator,
        aggressive_elimination=aggressive_elimination,
        max_resources=max_resources,
        factor=3,
    )
    sh.set_params(verbose=True)  # just for test coverage

    if Est is HalvingRandomSearchCV:
        # same number of candidates as with the grid
        sh.set_params(n_candidates=2 * 30, min_resources="exhaust")

    sh.fit(X, y)

    assert sh.n_iterations_ == expected_n_iterations
    assert sh.n_required_iterations_ == expected_n_required_iterations
    assert sh.n_possible_iterations_ == expected_n_possible_iterations
    assert sh.n_resources_ == expected_n_resources
    assert sh.n_candidates_ == expected_n_candidates
    assert sh.n_remaining_candidates_ == expected_n_remaining_candidates
    assert ceil(sh.n_candidates_[-1] / sh.factor) == sh.n_remaining_candidates_

def test_min_max_resources(
    Est,
    min_resources,
):
    # Test the min_resources and max_resources parameters, and how they affect
    # the number of resources used at each iteration
    param_grid = {"a": [1, 2], "b": [1, 2, 3]}

    sh = Est(
        factor=3,
        min_resources=min_resources,
    )
        sh.set_params(n_candidates=6)  # same number as with the grid

    expected_n_required_iterations = 2  # given 6 combinations and factor = 3
    if min_resources == "exhaust":
        assert sh.n_possible_iterations_ == sh.n_iterations_ == len(sh.n_resources_)

def test_n_iterations(Est, max_resources, n_iterations, n_possible_iterations):
    # test the number of actual iterations that were run depending on
    # max_resources

    n_samples = 1024
    X, y = make_classification(n_samples=n_samples, random_state=1)
    param_grid = {"a": [1, 2], "b": list(range(10))}
    factor = 2

    sh = Est(
        cv=2,
        factor=factor,
        min_resources=4,
    )
        sh.set_params(n_candidates=20)  # same as for HalvingGridSearchCV
    assert sh.n_required_iterations_ == 5
    assert sh.n_iterations_ == n_iterations
    assert sh.n_possible_iterations_ == n_possible_iterations

def test_resource_parameter(Est):
    # Test the resource parameter

    sh = Est(base_estimator, param_grid, cv=2, resource="c", max_resources=10, factor=3)
    assert set(sh.n_resources_) == set([1, 3, 9])
    for r_i, params, param_c in zip(
        sh.cv_results_["n_resources"],
        sh.cv_results_["params"],
        sh.cv_results_["param_c"],
    ):
        assert r_i == params["c"] == param_c

        ValueError, match="Cannot use resource=1234 which is not supported "
    ):
        sh = HalvingGridSearchCV(
            base_estimator, param_grid, cv=2, resource="1234", max_resources=10
        )
        sh.fit(X, y)

        match=(
            "Cannot use parameter c as the resource since it is part "
            "of the searched parameters."
        ),
    ):
        param_grid = {"a": [1, 2], "b": [1, 2], "c": [1, 3]}
            base_estimator, param_grid, cv=2, resource="c", max_resources=10
        )

def test_random_search(max_resources, n_candidates, expected_n_candidates):
    # Test random search and make sure the number of generated candidates is

    param_grid = {"a": norm, "b": norm}
    sh = HalvingRandomSearchCV(
        n_candidates=n_candidates,
        cv=2,
        factor=2,
    )
    assert sh.n_candidates_[0] == expected_n_candidates
    if n_candidates == "exhaust":
        # Make sure 'exhaust' makes the last iteration use as much resources as
        # we can
        assert sh.n_resources_[-1] == max_resources

def test_random_search_discrete_distributions(
    param_distributions, expected_n_candidates
):
    # Make sure random search samples the appropriate number of candidates when
    # we ask for more than what's possible. How many parameters are sampled
    # depends whether the distributions are 'all lists' or not (see
    # ParameterSampler for details). This is somewhat redundant with the checks
    # in ParameterSampler but interaction bugs were discovered during
    # development of SH

    sh = HalvingRandomSearchCV(base_estimator, param_distributions, n_candidates=10)

def test_input_errors(Est, params, expected_error_message):
    param_grid = {"a": [1]}
    X, y = make_classification(100)

    sh = Est(base_estimator, param_grid, **params)

def test_input_errors_randomized(params, expected_error_message):
    # tests specific to HalvingRandomSearchCV

    sh = HalvingRandomSearchCV(base_estimator, param_grid, **params)

def test_subsample_splitter_shapes(
    fraction, subsample_test, expected_train_size, expected_test_size
):
    # Make sure splits returned by SubsampleMetaSplitter are of appropriate
    # size

    X, y = make_classification(n_samples)
    cv = _SubsampleMetaSplitter(
        base_cv=KFold(5),
        fraction=fraction,
        subsample_test=subsample_test,
    )

        assert train.shape[0] == expected_train_size
        assert test.shape[0] == expected_test_size
        if subsample_test:
            assert train.shape[0] + test.shape[0] == int(n_samples * fraction)
        else:
            assert test.shape[0] == n_samples // cv.base_cv.get_n_splits()

def test_subsample_splitter_determinism(subsample_test):
    # Make sure _SubsampleMetaSplitter is consistent across calls to split():
    # - we're OK having training sets differ (they're always sampled with a
    #   different fraction anyway)
    # - when we don't subsample the test set, we want it to be always the same.
    #   This check is the most important. This is ensured by the determinism
    #   of the base_cv.

    # Note: we could force both train and test splits to be always the same if
    # we drew an int seed in _SubsampleMetaSplitter.__init__

        base_cv=KFold(5), fraction=0.5, subsample_test=subsample_test, random_state=None
    )

    folds_a = list(cv.split(X, y, groups=None))
    folds_b = list(cv.split(X, y, groups=None))

    for (train_a, test_a), (train_b, test_b) in zip(folds_a, folds_b):
        assert not np.all(train_a == train_b)

            assert not np.all(test_a == test_b)
        else:
            assert np.all(test_a == test_b)
            assert np.all(X[test_a] == X[test_b])

def test_top_k(k, itr, expected):
    results = {  # this isn't a 'real world' result dict
        "iter": [0, 0, 0, 0, 1, 1, 2, 2, 2],
        "mean_test_score": [4, 3, 5, 1, 11, 10, 5, 6, 9],
        "params": ["a", "b", "c", "d", "e", "f", "g", "h", "i"],
    }
    got = _top_k(results, k=k, itr=itr)
    assert np.all(got == expected)

def test_cv_results(Est):
    # test that the cv_results_ matches correctly the logic of the
    # tournament: in particular that the candidates continued in each
    # successive iteration are those that were best in the previous iteration

    # generate random scores: we want to avoid ties, which would otherwise
    # mess with the ordering and make testing harder
    def scorer(est, X, y):
        return rng.rand()

    sh = Est(base_estimator, param_grid, factor=2, scoring=scorer)

    # non-regression check for
    # https://github.com/scikit-learn/scikit-learn/issues/19203
    assert isinstance(sh.cv_results_["iter"], np.ndarray)
    assert isinstance(sh.cv_results_["n_resources"], np.ndarray)

    cv_results_df = pd.DataFrame(sh.cv_results_)

    # just make sure we don't have ties
    assert len(cv_results_df["mean_test_score"].unique()) == len(cv_results_df)

    cv_results_df["params_str"] = cv_results_df["params"].apply(str)
    table = cv_results_df.pivot(
        index="params_str", columns="iter", values="mean_test_score"
    )

    # table looks like something like this:
    # iter                    0      1       2        3   4   5
    # params_str
    # {'a': 'l2', 'b': 23} 0.75    NaN     NaN      NaN NaN NaN
    # {'a': 'l1', 'b': 30} 0.90  0.875     NaN      NaN NaN NaN
    # {'a': 'l1', 'b': 0}  0.75    NaN     NaN      NaN NaN NaN
    # {'a': 'l2', 'b': 3}  0.85  0.925  0.9125  0.90625 NaN NaN
    # {'a': 'l1', 'b': 5}  0.80    NaN     NaN      NaN NaN NaN
    # ...

    # where a NaN indicates that the candidate wasn't evaluated at a given
    # iteration, because it wasn't part of the top-K at some previous
    # iteration. We here make sure that candidates that aren't in the top-k at
    # any given iteration are indeed not evaluated at the subsequent
    nan_mask = pd.isna(table)
    n_iter = sh.n_iterations_
    for it in range(n_iter - 1):
        already_discarded_mask = nan_mask[it]

        # make sure that if a candidate is already discarded, we don't evaluate
        # it later
        assert (
            already_discarded_mask & nan_mask[it + 1] == already_discarded_mask
        ).all()

        # make sure that the number of discarded candidate is correct
        discarded_now_mask = ~already_discarded_mask & nan_mask[it + 1]
        kept_mask = ~already_discarded_mask & ~discarded_now_mask
        assert kept_mask.sum() == sh.n_candidates_[it + 1]

        # make sure that all discarded candidates have a lower score than the
        # kept candidates
        discarded_max_score = table[it].where(discarded_now_mask).max()
        kept_min_score = table[it].where(kept_mask).min()
        assert discarded_max_score < kept_min_score

    # We now make sure that the best candidate is chosen only from the last
    # iteration.
    # We also make sure this is true even if there were higher scores in
    # earlier rounds (this isn't generally the case, but worth ensuring it's
    # possible).

    last_iter = cv_results_df["iter"].max()
    idx_best_last_iter = cv_results_df[cv_results_df["iter"] == last_iter][
        "mean_test_score"
    ].idxmax()
    idx_best_all_iters = cv_results_df["mean_test_score"].idxmax()

    assert sh.best_params_ == cv_results_df.iloc[idx_best_last_iter]["params"]
    assert (
        cv_results_df.iloc[idx_best_last_iter]["mean_test_score"]
        < cv_results_df.iloc[idx_best_all_iters]["mean_test_score"]
    )
    assert (
        cv_results_df.iloc[idx_best_last_iter]["params"]
        != cv_results_df.iloc[idx_best_all_iters]["params"]
    )

def test_base_estimator_inputs(Est):
    # make sure that the base estimators are passed the correct parameters and
    # number of samples at each iteration.

    passed_n_samples_fit = []
    passed_n_samples_predict = []
    passed_params = []

    class FastClassifierBookKeeping(FastClassifier):
            passed_n_samples_fit.append(X.shape[0])

            passed_n_samples_predict.append(X.shape[0])

            passed_params.append(params)
            return super().set_params(**params)

    base_estimator = FastClassifierBookKeeping()

    sh = Est(
        factor=2,
        cv=n_splits,
    )

    assert len(passed_n_samples_fit) == len(passed_n_samples_predict)
    passed_n_samples = [
        x + y for (x, y) in zip(passed_n_samples_fit, passed_n_samples_predict)
    ]

    # Lists are of length n_splits * n_iter * n_candidates_at_i.
    # Each chunk of size n_splits corresponds to the n_splits folds for the
    # same candidate at the same iteration, so they contain equal values. We
    # subsample such that the lists are of length n_iter * n_candidates_at_it
    passed_n_samples = passed_n_samples[::n_splits]
    passed_params = passed_params[::n_splits]

    assert len(passed_params) == len(passed_n_samples) == len(cv_results_df)

    uniques, counts = np.unique(passed_n_samples, return_counts=True)
    assert (sh.n_resources_ == uniques).all()
    assert (sh.n_candidates_ == counts).all()

    assert (cv_results_df["params"] == passed_params).all()
    assert (cv_results_df["n_resources"] == passed_n_samples).all()

def test_groups_support(Est):
    # Check if ValueError (when groups is None) propagates to
    # HalvingGridSearchCV and HalvingRandomSearchCV

    X, y = make_classification(n_samples=50, n_classes=2, random_state=0)
    groups = rng.randint(0, 3, 50)

    grid = {"C": [1]}

        GroupShuffleSplit(random_state=0),
    ]
    error_msg = "The 'groups' parameter should not be None."
        gs = Est(clf, grid, cv=cv, random_state=0)
            gs.fit(X, y)
        gs.fit(X, y, groups=groups)

    non_group_cvs = [StratifiedKFold(), StratifiedShuffleSplit(random_state=0)]
    for cv in non_group_cvs:
        gs = Est(clf, grid, cv=cv)
        # Should not raise an error
        gs.fit(X, y)

def test_min_resources_null(SearchCV):
    """Check that we raise an error if the minimum resources is set to 0."""
    X = np.empty(0).reshape(0, 3)

    search = SearchCV(base_estimator, param_grid, min_resources="smallest")

    err_msg = "min_resources_=0: you might have passed an empty dataset X."
        search.fit(X, [])

def test_select_best_index(SearchCV):
    """Check the selection strategy of the halving search."""
        "iter": np.array([0, 0, 0, 0, 1, 1, 2, 2, 2]),
        "mean_test_score": np.array([4, 3, 5, 1, 11, 10, 5, 6, 9]),
        "params": np.array(["a", "b", "c", "d", "e", "f", "g", "h", "i"]),
    }

    # we expect the index of 'i'
    best_index = SearchCV._select_best_index(None, None, results)
    assert best_index == 8

def test_halving_random_search_list_of_dicts():
    """Check the behaviour of the `HalvingRandomSearchCV` with `param_distribution`
    being a list of dictionary.
    """
    X, y = make_classification(n_samples=150, n_features=4, random_state=42)

    params = [
        {"kernel": ["rbf"], "C": expon(scale=10), "gamma": expon(scale=0.1)},
        {"kernel": ["poly"], "degree": [2, 3]},
    ]
    param_keys = (
        "param_C",
        "param_degree",
        "param_gamma",
        "param_kernel",
    )
    score_keys = (
        "mean_test_score",
        "mean_train_score",
        "rank_test_score",
        "split0_test_score",
        "split1_test_score",
        "split2_test_score",
        "split0_train_score",
        "split1_train_score",
        "split2_train_score",
        "std_test_score",
        "std_train_score",
        "mean_fit_time",
        "std_fit_time",
        "mean_score_time",
        "std_score_time",
    )
    extra_keys = ("n_resources", "iter")

    search = HalvingRandomSearchCV(
        SVC(), cv=3, param_distributions=params, return_train_score=True, random_state=0
    )
    n_candidates = sum(search.n_candidates_)
    cv_results = search.cv_results_
    # Check results structure
    check_cv_results_keys(cv_results, param_keys, score_keys, n_candidates, extra_keys)
    expected_cv_results_kinds = {
        "param_C": "f",
        "param_degree": "i",
        "param_gamma": "f",
        "param_kernel": "O",
    }
    check_cv_results_array_types(
        search, param_keys, score_keys, expected_cv_results_kinds
    )

        (
            cv_results["param_C"].mask[i]
            and cv_results["param_gamma"].mask[i]
            and not cv_results["param_degree"].mask[i]
        )
        for i in range(n_candidates)
        if cv_results["param_kernel"][i] == "poly"
    )
        (
            not cv_results["param_C"].mask[i]
            and not cv_results["param_gamma"].mask[i]
            and cv_results["param_degree"].mask[i]
        )
        if cv_results["param_kernel"][i] == "rbf"
    )

    ):
        )

        self,
        a=0,
    ):
        self.a = a

        )

    """Dummy classifier to test the parameter search algorithms"""

    def fit(self, X, Y):
        assert len(X) == len(Y)
        self.classes_ = np.unique(Y)

        return T.shape[0]

        return X + self.foo_param

        return X - self.foo_param

    predict_log_proba = predict

            score = 1.0
        else:
            score = 0.0

        self.foo_param = params["foo_param"]

class LinearSVCNoScore(LinearSVC):
    """A LinearSVC classifier that has no score method."""

    @property
    def score(self):

def assert_grid_iter_equals_getitem(grid):
    assert list(grid) == [grid[i] for i in range(len(grid))]

def test_validate_parameter_input(klass, input, error_type, error_message):
    with pytest.raises(error_type, match=error_message):
        klass(input)

def test_parameter_grid():
    # Test basic properties of ParameterGrid.
    params1 = {"foo": [1, 2, 3]}
    grid1 = ParameterGrid(params1)
    assert isinstance(grid1, Iterable)
    assert isinstance(grid1, Sized)
    assert len(grid1) == 3
    assert_grid_iter_equals_getitem(grid1)

    params2 = {"foo": [4, 2], "bar": ["ham", "spam", "eggs"]}
    grid2 = ParameterGrid(params2)
    assert len(grid2) == 6

    # loop to assert we can iterate over the grid multiple times
        # tuple + chain transforms {"a": 1, "b": 2} to ("a", 1, "b", 2)
        points = set(tuple(chain(*(sorted(p.items())))) for p in grid2)
        assert points == set(
            ("bar", x, "foo", y) for x, y in product(params2["bar"], params2["foo"])
        )
    assert_grid_iter_equals_getitem(grid2)

    # Special case: empty grid (useful to get default estimator settings)
    empty = ParameterGrid({})
    assert len(empty) == 1
    assert list(empty) == [{}]
    assert_grid_iter_equals_getitem(empty)
        empty[1]

    has_empty = ParameterGrid([{"C": [1, 10]}, {}, {"C": [0.5]}])
    assert len(has_empty) == 4
    assert list(has_empty) == [{"C": 1}, {"C": 10}, {}, {"C": 0.5}]
    assert_grid_iter_equals_getitem(has_empty)

def test_grid_search():
    # Test that the best estimator contains the right value for foo_param
    grid_search = GridSearchCV(clf, {"foo_param": [1, 2, 3]}, cv=2, verbose=3)
    # make sure it selects the smallest parameter in case of ties
    assert grid_search.best_estimator_.foo_param == 2

    assert_array_equal(grid_search.cv_results_["param_foo_param"].data, [1, 2, 3])

    # Smoke test the score etc:
    grid_search.score(X, y)
    grid_search.predict_proba(X)
    grid_search.decision_function(X)
    grid_search.transform(X)

    # Test exception handling on scoring
    grid_search.scoring = "sklearn"

def test_grid_search_pipeline_steps():
    # check that parameters that are estimators are cloned before fitting
    pipe = Pipeline([("regressor", LinearRegression())])
    param_grid = {"regressor": [LinearRegression(), Ridge()]}
    grid_search = GridSearchCV(pipe, param_grid, cv=2)
    regressor_results = grid_search.cv_results_["param_regressor"]
    assert isinstance(regressor_results[0], LinearRegression)
    assert isinstance(regressor_results[1], Ridge)
    assert not hasattr(regressor_results[0], "coef_")
    assert not hasattr(regressor_results[1], "coef_")
    assert regressor_results[0] is not grid_search.best_estimator_
    assert regressor_results[1] is not grid_search.best_estimator_
    # check that we didn't modify the parameter grid that was passed
    assert not hasattr(param_grid["regressor"][0], "coef_")
    assert not hasattr(param_grid["regressor"][1], "coef_")

def test_SearchCV_with_fit_params(SearchCV):
    clf = CheckingClassifier(expected_fit_params=["spam", "eggs"])
    searcher = SearchCV(clf, {"foo_param": [1, 2, 3]}, cv=2, error_score="raise")

    # The CheckingClassifier generates an assertion error if
    # a parameter is missing or has length != len(X).
    err_msg = r"Expected fit parameter\(s\) \['eggs'\] not seen."
        searcher.fit(X, y, spam=np.ones(10))

    err_msg = "Fit parameter spam has length 1; expected"
        searcher.fit(X, y, spam=np.ones(1), eggs=np.zeros(10))
    searcher.fit(X, y, spam=np.ones(10), eggs=np.zeros(10))

def test_grid_search_no_score():
    # Test grid-search on classifier that has no score function.
    clf_no_score = LinearSVCNoScore(random_state=0)
    grid_search = GridSearchCV(clf, {"C": Cs}, scoring="accuracy")

    grid_search_no_score = GridSearchCV(clf_no_score, {"C": Cs}, scoring="accuracy")
    # smoketest grid search
    grid_search_no_score.fit(X, y)

    # check that best params are equal
    assert grid_search_no_score.best_params_ == grid_search.best_params_
    # check that we can call score and that it gives the correct result
    assert grid_search.score(X, y) == grid_search_no_score.score(X, y)

    # giving no scoring function raises an error
    grid_search_no_score = GridSearchCV(clf_no_score, {"C": Cs})
    with pytest.raises(TypeError, match="no scoring"):
        grid_search_no_score.fit([[1]])

def test_grid_search_score_method():
    X, y = make_classification(n_samples=100, n_classes=2, flip_y=0.2, random_state=0)
    grid = {"C": [0.1]}

    search_no_scoring = GridSearchCV(clf, grid, scoring=None).fit(X, y)
    search_accuracy = GridSearchCV(clf, grid, scoring="accuracy").fit(X, y)
    search_no_score_method_auc = GridSearchCV(
        LinearSVCNoScore(), grid, scoring="roc_auc"
    search_auc = GridSearchCV(clf, grid, scoring="roc_auc").fit(X, y)

    # Check warning only occurs in situation where behavior changed:
    # estimator requires score method to compete with scoring parameter
    score_no_scoring = search_no_scoring.score(X, y)
    score_accuracy = search_accuracy.score(X, y)
    score_no_score_auc = search_no_score_method_auc.score(X, y)
    score_auc = search_auc.score(X, y)

    # ensure the test is sane
    assert score_auc < 1.0
    assert score_accuracy < 1.0
    assert score_auc != score_accuracy

    assert_almost_equal(score_accuracy, score_no_scoring)
    assert_almost_equal(score_auc, score_no_score_auc)

def test_grid_search_groups():
    # Check if ValueError (when groups is None) propagates to GridSearchCV

    groups = rng.randint(0, 3, 15)

    ]
        gs = GridSearchCV(clf, grid, cv=cv)

    non_group_cvs = [StratifiedKFold(), StratifiedShuffleSplit()]

def test_classes__property():
    # Test that classes_ property matches best_estimator_.classes_

    grid_search = GridSearchCV(LinearSVC(random_state=0), {"C": Cs})
    assert_array_equal(grid_search.best_estimator_.classes_, grid_search.classes_)

    # Test that regressors do not have a classes_ attribute
    grid_search = GridSearchCV(Ridge(), {"alpha": [1.0, 2.0]})
    assert not hasattr(grid_search, "classes_")

    # Test that the grid searcher has no classes_ attribute before it's fit

    # Test that the grid searcher has no classes_ attribute without a refit
    grid_search = GridSearchCV(LinearSVC(random_state=0), {"C": Cs}, refit=False)

def test_trivial_cv_results_attr():
    # Test search over a "grid" with only one point.
    grid_search = GridSearchCV(clf, {"foo_param": [1]}, cv=2)
    assert hasattr(grid_search, "cv_results_")

    random_search = RandomizedSearchCV(clf, {"foo_param": [0]}, n_iter=1, cv=2)
    random_search.fit(X, y)

def test_no_refit():
    # Test that GSCV can be used for model selection alone without refitting
    for scoring in [None, ["accuracy", "precision"]]:
        grid_search = GridSearchCV(clf, {"foo_param": [1, 2, 3]}, refit=False, cv=2)
        assert (
            not hasattr(grid_search, "best_estimator_")
            and hasattr(grid_search, "best_index_")
            and hasattr(grid_search, "best_params_")
        )

        # Make sure the functions predict/transform etc. raise meaningful
        # error messages
        for fn_name in (
            "predict",
            "predict_log_proba",
            "transform",
            "inverse_transform",
        ):
            outer_msg = f"has no attribute '{fn_name}'"
            inner_msg = (
                f"`refit=False`. {fn_name} is available only after "
                "refitting on the best parameters"
            )
            with pytest.raises(AttributeError, match=outer_msg) as exec_info:
                getattr(grid_search, fn_name)(X)

    # Test that an invalid refit param raises appropriate error messages
        "For multi-metric scoring, the parameter refit must be set to a scorer key"
    )
    for refit in [True, "recall", "accuracy"]:
            GridSearchCV(
                clf, {}, refit=refit, scoring={"acc": "accuracy", "prec": "precision"}

def test_grid_search_error():
    # Test that grid search will capture errors on data with different length
    X_, y_ = make_classification(n_samples=200, n_features=100, random_state=0)

    clf = LinearSVC()
    cv = GridSearchCV(clf, {"C": [0.1, 1.0]})
        cv.fit(X_[:180], y_)

def test_grid_search_one_grid_point():
    param_dict = {"C": [1.0], "kernel": ["rbf"], "gamma": [0.1]}

    cv = GridSearchCV(clf, param_dict)
    cv.fit(X_, y_)

    clf = SVC(C=1.0, kernel="rbf", gamma=0.1)

    assert_array_equal(clf.dual_coef_, cv.best_estimator_.dual_coef_)

def test_grid_search_when_param_grid_includes_range():
    grid_search = None
    grid_search = GridSearchCV(clf, {"foo_param": range(1, 4)}, cv=2)

def test_grid_search_bad_param_grid():
    param_dict = {"C": 1}
        "Parameter grid for parameter 'C' needs to be a list or "
        "a numpy array, but got 1 (of type int) instead. Single "
        "values need to be wrapped in a list with one element."
    )
    search = GridSearchCV(clf, param_dict)
        search.fit(X, y)

    param_dict = {"C": []}
    clf = SVC()
        "Parameter grid for parameter 'C' need to be a non-empty sequence, got: []"
    )

    param_dict = {"C": "1,2,3"}
        "Parameter grid for parameter 'C' needs to be a list or a numpy array, "
        "but got '1,2,3' (of type str) instead. Single values need to be "
        "wrapped in a list with one element."
    )

    param_dict = {"C": np.ones((3, 2))}
    clf = SVC()

def test_grid_search_sparse(csr_container):
    # Test that grid search works with both dense and sparse matrices

    cv.fit(X_[:180], y_[:180])
    y_pred = cv.predict(X_[180:])
    C = cv.best_estimator_.C

    cv.fit(X_[:180].tocoo(), y_[:180])
    y_pred2 = cv.predict(X_[180:])
    C2 = cv.best_estimator_.C

    assert np.mean(y_pred == y_pred2) >= 0.9
    assert C == C2

def test_grid_search_sparse_scoring(csr_container):

    cv = GridSearchCV(clf, {"C": [0.1, 1.0]}, scoring="f1")

    assert_array_equal(y_pred, y_pred2)
    # Smoke test the score
    # np.testing.assert_allclose(f1_score(cv.predict(X_[:180]), y[:180]),
    #                            cv.score(X_[:180], y[:180]))

    # test loss where greater is worse
    def f1_loss(y_true_, y_pred_):
        return -f1_score(y_true_, y_pred_)

    F1Loss = make_scorer(f1_loss, greater_is_better=False)
    cv = GridSearchCV(clf, {"C": [0.1, 1.0]}, scoring=F1Loss)
    y_pred3 = cv.predict(X_[180:])
    C3 = cv.best_estimator_.C

    assert C == C3
    assert_array_equal(y_pred, y_pred3)

def test_grid_search_precomputed_kernel():
    # Test that grid search works when the input features are given in the
    # form of a precomputed kernel matrix

    # compute the training kernel matrix corresponding to the linear kernel
    K_train = np.dot(X_[:180], X_[:180].T)
    y_train = y_[:180]

    clf = SVC(kernel="precomputed")
    cv.fit(K_train, y_train)

    assert cv.best_score_ >= 0

    # compute the test kernel matrix
    K_test = np.dot(X_[180:], X_[:180].T)
    y_test = y_[180:]

    y_pred = cv.predict(K_test)

    assert np.mean(y_pred == y_test) >= 0

    # or sparse
        cv.fit(K_train.tolist(), y_train)

def test_grid_search_precomputed_kernel_error_nonsquare():
    # Test that grid search returns an error with a non-square precomputed
    # training kernel matrix
    K_train = np.zeros((10, 20))
    y_train = np.ones((10,))
        cv.fit(K_train, y_train)

class BrokenClassifier(BaseEstimator):
    """Broken classifier that cannot be fit twice"""

    def __init__(self, parameter=None):
        self.parameter = parameter

        assert not hasattr(self, "has_been_fit_")
        self.has_been_fit_ = True

def test_refit():
    # Regression test for bug in refitting
    # Simulates re-fitting a broken estimator; this used to break with
    # sparse SVMs.

    clf = GridSearchCV(
        BrokenClassifier(), [{"parameter": [0, 1]}], scoring="precision", refit=True
    )

def test_refit_callable():
    """
    Test refit=callable, which adds flexibility in identifying the
    "best" estimator.
    """

    def refit_callable(cv_results):
        """
        A dummy function tests `refit=callable` interface.
        Return the index of a model that has the least
        `mean_test_score`.
        """
        # Fit a dummy clf with `refit=True` to get a list of keys in
        # clf.cv_results_.
        X, y = make_classification(n_samples=100, n_features=4, random_state=42)
        clf = GridSearchCV(
            LinearSVC(random_state=42),
            {"C": [0.01, 0.1, 1]},
            scoring="precision",
            refit=True,
        )
        # Ensure that `best_index_ != 0` for this dummy clf
        assert clf.best_index_ != 0

        # Assert every key matches those in `cv_results`
        for key in clf.cv_results_.keys():
            assert key in cv_results

        return cv_results["mean_test_score"].argmin()

    X, y = make_classification(n_samples=100, n_features=4, random_state=42)
        LinearSVC(random_state=42),
        {"C": [0.01, 0.1, 1]},
        scoring="precision",
        refit=refit_callable,
    )

    assert clf.best_index_ == 0
    # Ensure `best_score_` is disabled when using `refit=callable`
    assert not hasattr(clf, "best_score_")

def test_refit_callable_invalid_type():
    """
    Test implementation catches the errors when 'best_index_' returns an
    invalid result.
    """

    def refit_callable_invalid_type(cv_results):
        """
        A dummy function tests when returned 'best_index_' is not integer.
        """

        {"C": [0.1, 1]},
        refit=refit_callable_invalid_type,
    )
    with pytest.raises(TypeError, match="best_index_ returned is not an integer"):

def test_refit_callable_out_bound(out_bound_value, search_cv):
    """
    out of bound result.
    """

    def refit_callable_out_bound(cv_results):
        """
        A dummy function tests when returned 'best_index_' is out of bounds.
        """
        return out_bound_value

    clf = search_cv(
        refit=refit_callable_out_bound,
    )
    with pytest.raises(IndexError, match="best_index_ index out of range"):

def test_refit_callable_multi_metric():
    """
    Test refit=callable in multiple metric evaluation setting
    """

        """
        `mean_test_prec`.
        """
        assert "mean_test_prec" in cv_results
        return cv_results["mean_test_prec"].argmin()

    scoring = {"Accuracy": make_scorer(accuracy_score), "prec": "precision"}
    )

def test_gridsearch_nd():
    # Pass X as list in GridSearchCV

    def check_X(x):
        return x.shape[1:] == (5, 3, 2)

    def check_y(x):
        return x.shape[1:] == (7, 11)

        check_X=check_X,
        check_y=check_y,
        methods_to_check=["fit"],
    )
    grid_search = GridSearchCV(clf, {"foo_param": [1, 2, 3]})
    grid_search.fit(X_4d, y_3d).score(X, y)

def test_X_as_list():

        check_X=lambda x: isinstance(x, list),
    )
    grid_search = GridSearchCV(clf, {"foo_param": [1, 2, 3]}, cv=cv)
    grid_search.fit(X.tolist(), y).score(X, y)

def test_y_as_list():
    # Pass y as list in GridSearchCV

        check_y=lambda x: isinstance(x, list),
    )
    grid_search.fit(X, y.tolist()).score(X, y)

def test_pandas_input():
    try:

        types.append((DataFrame, Series))
        pass

    for InputFeatureType, TargetType in types:

        def check_df(x):
            return isinstance(x, InputFeatureType)

        def check_series(x):
            return isinstance(x, TargetType)

        grid_search = GridSearchCV(clf, {"foo_param": [1, 2, 3]})
        grid_search.fit(X_df, y_ser).score(X_df, y_ser)
        grid_search.predict(X_df)
        assert hasattr(grid_search, "cv_results_")

def test_unsupervised_grid_search():
    # test grid-search with unsupervised estimator
    X, y = make_blobs(n_samples=50, random_state=0)
    km = KMeans(random_state=0, init="random", n_init=1)

    # Multi-metric evaluation unsupervised
    scoring = ["adjusted_rand_score", "fowlkes_mallows_score"]
    for refit in ["adjusted_rand_score", "fowlkes_mallows_score"]:
            km, param_grid=dict(n_clusters=[2, 3, 4]), scoring=scoring, refit=refit
        )
        # Both ARI and FMS can find the right number :)
        assert grid_search.best_params_["n_clusters"] == 3

    # Single metric evaluation unsupervised
        km, param_grid=dict(n_clusters=[2, 3, 4]), scoring="fowlkes_mallows_score"
    )
    assert grid_search.best_params_["n_clusters"] == 3

    # Now without a score, and without y
    grid_search = GridSearchCV(km, param_grid=dict(n_clusters=[2, 3, 4]))
    grid_search.fit(X)
    assert grid_search.best_params_["n_clusters"] == 4

def test_gridsearch_no_predict():
    # test grid-search with an estimator without predict.
    # slight duplication of a test from KDE
    def custom_scoring(estimator, X):
        return 42 if estimator.bandwidth == 0.1 else 0

    search = GridSearchCV(
        KernelDensity(),
        param_grid=dict(bandwidth=[0.01, 0.1, 1]),
        scoring=custom_scoring,
    )
    assert search.best_params_["bandwidth"] == 0.1
    assert search.best_score_ == 42

def test_param_sampler():
    # test basic properties of param sampler
    param_distributions = {"kernel": ["rbf", "linear"], "C": uniform(0, 1)}
    sampler = ParameterSampler(
        param_distributions=param_distributions, n_iter=10, random_state=0
    )
    samples = [x for x in sampler]
    assert len(samples) == 10
    for sample in samples:
        assert sample["kernel"] in ["rbf", "linear"]
        assert 0 <= sample["C"] <= 1

    # test that repeated calls yield identical parameters
    param_distributions = {"C": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
        param_distributions=param_distributions, n_iter=3, random_state=0
    )
    assert [x for x in sampler] == [x for x in sampler]

    param_distributions = {"C": uniform(0, 1)}
    )

def check_cv_results_array_types(
    search, param_keys, score_keys, expected_cv_results_kinds
):
    # Check if the search `cv_results`'s array are of correct types
    assert all(isinstance(cv_results[param], np.ma.MaskedArray) for param in param_keys)
    assert {
        key: cv_results[key].dtype.kind for key in param_keys
    } == expected_cv_results_kinds
    assert not any(isinstance(cv_results[key], np.ma.MaskedArray) for key in score_keys)
        cv_results[key].dtype == np.float64
        for key in score_keys
        if not key.startswith("rank")
    )

    scorer_keys = search.scorer_.keys() if search.multimetric_ else ["score"]

    for key in scorer_keys:
        assert cv_results["rank_test_%s" % key].dtype == np.int32

def check_cv_results_keys(cv_results, param_keys, score_keys, n_cand, extra_keys=()):
    # Test the search.cv_results_ contains all the required results
    all_keys = param_keys + score_keys + extra_keys
    assert_array_equal(sorted(cv_results.keys()), sorted(all_keys + ("params",)))
    assert all(cv_results[key].shape == (n_cand,) for key in param_keys + score_keys)

def test_grid_search_cv_results():
    X, y = make_classification(n_samples=50, n_features=4, random_state=42)

    n_grid_points = 6
    params = [
        dict(
            kernel=[
                "rbf",
            ],
            C=[1, 10],
            gamma=[0.1, 1],
        ),
        dict(
            kernel=[
                "poly",
            ],
            degree=[1, 2],
        ),
    ]

    param_keys = ("param_C", "param_degree", "param_gamma", "param_kernel")
    )
    n_candidates = n_grid_points

    search = GridSearchCV(SVC(), cv=3, param_grid=params, return_train_score=True)
    # Check if score and timing are reasonable
    assert all(cv_results["rank_test_score"] >= 1)
    assert (all(cv_results[k] >= 0) for k in score_keys if k != "rank_test_score")
    assert (
        all(cv_results[k] <= 1)
        for k in score_keys
        if "time" not in k and k != "rank_test_score"
    )
    # Check cv_results structure
        "param_C": "i",
    }
    )
    check_cv_results_keys(cv_results, param_keys, score_keys, n_candidates)
    # Check masking

    poly_results = [
        (
        )
    ]
    assert all(poly_results)
    assert len(poly_results) == 2

    rbf_results = [
        (
        )
    ]
    assert all(rbf_results)
    assert len(rbf_results) == 4

def test_random_search_cv_results():

    n_search_iter = 30

    params = [
    ]
    )
    n_candidates = n_search_iter

    search = RandomizedSearchCV(
        SVC(),
        n_iter=n_search_iter,
        cv=3,
        param_distributions=params,
    )
    }
    )
        (
        )
    )
        (
        )
    )

def test_search_default_iid(SearchCV, specialized_params):
    # Test the IID parameter  TODO: Clearly this test does something else???
    # noise-free simple 2d-data
        centers=[[0, 0], [1, 0], [0, 1], [1, 1]],
        n_samples=80,
    )
    # split dataset into two folds that are not iid
    # first one contains data of all 4 blobs, second only from two.
    mask = np.ones(X.shape[0], dtype=bool)
    mask[np.where(y == 1)[0][::2]] = 0
    mask[np.where(y == 2)[0][::2]] = 0
    # this leads to perfect classification on one fold and a score of 1/3 on
    # the other
    # create "cv" for splits
    cv = [[mask, ~mask], [~mask, mask]]

    common_params = {"estimator": SVC(), "cv": cv, "return_train_score": True}
    search = SearchCV(**common_params, **specialized_params)

    test_cv_scores = np.array(
        [
            search.cv_results_["split%d_test_score" % s][0]
            for s in range(search.n_splits_)
        ]
    )
    test_mean = search.cv_results_["mean_test_score"][0]
    test_std = search.cv_results_["std_test_score"][0]

    train_cv_scores = np.array(
        [
            search.cv_results_["split%d_train_score" % s][0]
        ]
    )
    train_mean = search.cv_results_["mean_train_score"][0]
    train_std = search.cv_results_["std_train_score"][0]

    assert search.cv_results_["param_C"][0] == 1
    # scores are the same as above
    assert_allclose(test_cv_scores, [1, 1.0 / 3.0])
    assert_allclose(train_cv_scores, [1, 1])
    # Unweighted mean/std is used
    assert test_mean == pytest.approx(np.mean(test_cv_scores))
    assert test_std == pytest.approx(np.std(test_cv_scores))

    # For the train scores, we do not take a weighted mean irrespective of
    # i.i.d. or not
    assert train_mean == pytest.approx(1)
    assert train_std == pytest.approx(0)

def test_grid_search_cv_results_multimetric():

    params = [
        dict(
            kernel=[
                "rbf",
            ],
            C=[1, 10],
        ),
        dict(
            kernel=[
                "poly",
            ],
        ),
    ]

    grid_searches = []
    for scoring in (
        {"accuracy": make_scorer(accuracy_score), "recall": make_scorer(recall_score)},
        "accuracy",
        "recall",
    ):
            SVC(), cv=n_splits, param_grid=params, scoring=scoring, refit=False
        )
        grid_searches.append(grid_search)

    compare_cv_results_multimetric_with_single(*grid_searches)

def test_random_search_cv_results_multimetric():

    # Scipy 0.12's stats dists do not accept seed, hence we use param grid
    params = dict(C=np.logspace(-4, 1, 3), gamma=np.logspace(-5, 0, 3, base=0.1))
    for refit in (True, False):
        random_searches = []
        for scoring in (("accuracy", "recall"), "accuracy", "recall"):
            # If True, for multi-metric pass refit='accuracy'
            if refit:
                probability = True
                refit = "accuracy" if isinstance(scoring, tuple) else refit
            else:
                probability = False
            clf = SVC(probability=probability, random_state=42)
            random_search = RandomizedSearchCV(
                clf,
                n_iter=n_search_iter,
                cv=n_splits,
                param_distributions=params,
                refit=refit,
            )
            random_search.fit(X, y)
            random_searches.append(random_search)

        compare_cv_results_multimetric_with_single(*random_searches)
        compare_refit_methods_when_refit_with_acc(
            random_searches[0], random_searches[1], refit
        )

def compare_cv_results_multimetric_with_single(search_multi, search_acc, search_rec):
    """Compare multi-metric cv_results with the ensemble of multiple
    single metric cv_results from single metric grid/random search"""

    assert search_multi.multimetric_
    assert_array_equal(sorted(search_multi.scorer_), ("accuracy", "recall"))

    cv_results_multi = search_multi.cv_results_
    cv_results_acc_rec = {
        re.sub("_score$", "_accuracy", k): v for k, v in search_acc.cv_results_.items()
    }
    cv_results_acc_rec.update(
        {re.sub("_score$", "_recall", k): v for k, v in search_rec.cv_results_.items()}
    )

    # Check if score and timing are reasonable, also checks if the keys
    # are present
        (
            np.all(cv_results_multi[k] <= 1)
            for k in (
                "mean_score_time",
                "std_score_time",
                "mean_fit_time",
                "std_fit_time",
            )
        )
    )

    # Compare the keys, other than time keys, among multi-metric and
    # single metric grid search results. np.testing.assert_equal performs a
    # deep nested comparison of the two cv_results dicts
        {k: v for k, v in cv_results_multi.items() if not k.endswith("_time")},
        {k: v for k, v in cv_results_acc_rec.items() if not k.endswith("_time")},
    )

def compare_refit_methods_when_refit_with_acc(search_multi, search_acc, refit):
    """Compare refit multi-metric search methods with single metric methods"""
    assert search_acc.refit == refit
    if refit:
        assert search_multi.refit == "accuracy"
    else:
        assert not search_multi.refit
        return  # search cannot predict/score without refit

    X, y = make_blobs(n_samples=100, n_features=4, random_state=42)
    for method in ("predict", "predict_proba", "predict_log_proba"):
            getattr(search_multi, method)(X), getattr(search_acc, method)(X)
        )
    assert_almost_equal(search_multi.score(X, y), search_acc.score(X, y))
    for key in ("best_index_", "best_score_", "best_params_"):
        assert getattr(search_multi, key) == getattr(search_acc, key)

def test_search_cv_score_samples_error(search_cv):
    search_cv.fit(X, y)

    # Make sure to error out when underlying estimator does not implement
    # the method `score_samples`
    outer_msg = f"'{search_cv.__class__.__name__}' has no attribute 'score_samples'"
    inner_msg = "'DecisionTreeClassifier' object has no attribute 'score_samples'"

        search_cv.score_samples(X)

def test_unsupported_sample_weight_scorer():
    """Checks that fitting with sample_weight raises a warning if the scorer does not
    support sample_weight"""

    def fake_score_func(y_true, y_pred):
        "Fake scoring function that does not support sample_weight"
        return 0.5

    fake_scorer = make_scorer(fake_score_func)

    X, y = make_classification(n_samples=10, n_features=4, random_state=42)
    search_cv = GridSearchCV(estimator=LogisticRegression(), param_grid={"C": [1, 10]})
    # function
    search_cv.set_params(scoring=fake_score_func)
    with pytest.warns(UserWarning, match="does not support sample_weight"):
        search_cv.fit(X, y, sample_weight=sw)
    # scorer
    search_cv.set_params(scoring=fake_scorer)
    # multi-metric evaluation
    search_cv.set_params(
        scoring=dict(fake=fake_scorer, accuracy="accuracy"), refit=False
    )
    # only fake scorer does not support sample_weight
        UserWarning, match=r"The scoring fake=.* does not support sample_weight"
    ):

def test_search_cv_sample_weight_equivalence(estimator):
    estimator_weighted = clone(estimator)
    estimator_repeated = clone(estimator)

    n_samples_per_group = 30
    n_groups = 4
    n_samples = n_groups * n_samples_per_group
    y = rng.randint(0, n_classes, size=n_samples)
    # we use groups with LeaveOneGroupOut to ensure that
    # the splits are the same in the repeated/weighted datasets
    groups = np.tile(np.arange(n_groups), n_samples_per_group)

    groups_weighted = groups
    splits_weighted = list(LeaveOneGroupOut().split(X_weighted, groups=groups_weighted))
    estimator_weighted.set_params(cv=splits_weighted)
    groups_repeated = groups_weighted.repeat(repeats=sw)
    splits_repeated = list(LeaveOneGroupOut().split(X_repeated, groups=groups_repeated))
    estimator_repeated.set_params(cv=splits_repeated)

    # check that scores stored in cv_results_
    # are equal for the weighted/repeated datasets
    score_keys = [
        key for key in estimator_repeated.cv_results_ if key.endswith("score")
    ]
    for key in score_keys:
        s1 = estimator_repeated.cv_results_[key]
        s2 = estimator_weighted.cv_results_[key]
        err_msg = f"{key} values are not equal for weighted/repeated datasets"
        assert_allclose(s1, s2, err_msg=err_msg)

    for key in ["best_score_", "best_index_"]:
        s1 = getattr(estimator_repeated, key)
        s2 = getattr(estimator_weighted, key)
        assert_almost_equal(s1, s2, err_msg=err_msg)

            s1 = getattr(estimator_repeated, method)(X)
            s2 = getattr(estimator_weighted, method)(X)
            err_msg = (
                f"Comparing the output of {method} revealed that fitting "
            )
            assert_allclose_dense_sparse(s1, s2, err_msg=err_msg)

def test_search_cv_score_samples_method(search_cv):
    # Set parameters
    outliers_fraction = 0.15
    n_outliers = int(outliers_fraction * n_samples)
    n_inliers = n_samples - n_outliers

    # Create dataset
    X = make_blobs(
        n_samples=n_inliers,
        centers=[[0, 0], [0, 0]],
        cluster_std=0.5,
    )[0]
    # Add some noisy points
    X = np.concatenate([X, rng.uniform(low=-6, high=6, size=(n_outliers, 2))], axis=0)

    # Define labels to be able to score the estimator with `search_cv`
    y_true = np.array([1] * n_samples)
    y_true[-n_outliers:] = -1

    # Fit on data
    search_cv.fit(X, y_true)

    # Verify that the stand alone estimator yields the same results
    # as the ones obtained with *SearchCV
        search_cv.score_samples(X), search_cv.best_estimator_.score_samples(X)
    )

def test_search_cv_results_rank_tie_breaking():
    X, y = make_blobs(n_samples=50, random_state=42)

    # The two C values are close enough to give similar models
    # which would result in a tie of their mean cv-scores
    param_grid = {"C": [1, 1.001, 0.001]}

    grid_search = GridSearchCV(SVC(), param_grid=param_grid, return_train_score=True)
    random_search = RandomizedSearchCV(
        SVC(), n_iter=3, param_distributions=param_grid, return_train_score=True
    )

    for search in (grid_search, random_search):
        cv_results = search.cv_results_
        # Check tie breaking strategy -
        # Check that there is a tie in the mean scores between
        # candidates 1 and 2 alone
            cv_results["mean_test_score"][0], cv_results["mean_test_score"][1]
        )
            cv_results["mean_train_score"][0], cv_results["mean_train_score"][1]
        )
            cv_results["mean_test_score"][1], cv_results["mean_test_score"][2]
        )
            cv_results["mean_train_score"][1], cv_results["mean_train_score"][2]
        )
        # 'min' rank should be assigned to the tied candidates
        assert_almost_equal(search.cv_results_["rank_test_score"], [1, 1, 3])

def test_search_cv_results_none_param():
    X, y = [[1], [2], [3], [4], [5]], [0, 0, 0, 0, 1]
    estimators = (DecisionTreeRegressor(), DecisionTreeClassifier())
    est_parameters = {"random_state": [0, None]}

            est,
            est_parameters,
            cv=cv,
        assert_array_equal(grid_search.cv_results_["param_random_state"], [0, None])

def test_search_cv_timing():

    X = [
        [
            1,
        ],
        [
            2,
        ],
        [
            3,
        ],
        [
            4,
        ],
    ]
    y = [0, 1, 1, 0]

    gs = GridSearchCV(svc, {"C": [0, 1]}, cv=2, error_score=0)
    rs = RandomizedSearchCV(svc, {"C": [0, 1]}, cv=2, error_score=0, n_iter=2)

    for search in (gs, rs):
        for key in ["mean_fit_time", "std_fit_time"]:
            # NOTE The precision of time.time in windows is not high
            # enough for the fit/score times to be non-zero for trivial X and y
            assert np.all(search.cv_results_[key] >= 0)
            assert np.all(search.cv_results_[key] < 1)

        for key in ["mean_score_time", "std_score_time"]:
            assert search.cv_results_[key][1] >= 0
            assert search.cv_results_[key][0] == 0.0

        assert hasattr(search, "refit_time_")
        assert isinstance(search.refit_time_, float)
        assert search.refit_time_ >= 0

def test_grid_search_correct_score_results():
    # test that correct scores are used
    for score in ["f1", "roc_auc"]:
        grid_search = GridSearchCV(clf, {"C": Cs}, scoring=score, cv=n_splits)
        cv_results = grid_search.fit(X, y).cv_results_

        # Test scorer names
        result_keys = list(cv_results.keys())
        expected_keys = ("mean_test_score", "rank_test_score") + tuple(
            "split%d_test_score" % cv_i for cv_i in range(n_splits)
        )
        assert all(np.isin(expected_keys, result_keys))

        cv = StratifiedKFold(n_splits=n_splits)
        n_splits = grid_search.n_splits_
        for candidate_i, C in enumerate(Cs):
            clf.set_params(C=C)
            cv_scores = np.array(
                [
                    grid_search.cv_results_["split%d_test_score" % s][candidate_i]
                    for s in range(n_splits)
                ]
            )
            for i, (train, test) in enumerate(cv.split(X, y)):
                clf.fit(X[train], y[train])
                if score == "f1":
                    correct_score = f1_score(y[test], clf.predict(X[test]))
                elif score == "roc_auc":
                    dec = clf.decision_function(X[test])
                    correct_score = roc_auc_score(y[test], dec)
                assert_almost_equal(correct_score, cv_scores[i])

    # Test that a fit search can be pickled
    grid_search = GridSearchCV(clf, {"foo_param": [1, 2, 3]}, refit=True, cv=2)
    grid_search_pickled = pickle.loads(pickle.dumps(grid_search))
    assert_array_almost_equal(grid_search.predict(X), grid_search_pickled.predict(X))

        clf, {"foo_param": [1, 2, 3]}, refit=True, n_iter=3, cv=2
    )
    random_search_pickled = pickle.loads(pickle.dumps(random_search))
        random_search.predict(X), random_search_pickled.predict(X)
    )

def test_grid_search_with_multioutput_data():
    # Test search with multi-output estimator

    X, y = make_multilabel_classification(return_indicator=True, random_state=0)

    est_parameters = {"max_depth": [1, 2, 3, 4]}

        DecisionTreeRegressor(random_state=0),
        DecisionTreeClassifier(random_state=0),
    ]

    # Test with grid search cv
        grid_search = GridSearchCV(est, est_parameters, cv=cv)
        res_params = grid_search.cv_results_["params"]
        for cand_i in range(len(res_params)):
            est.set_params(**res_params[cand_i])

                est.fit(X[train], y[train])
                correct_score = est.score(X[test], y[test])
                    correct_score,
                    grid_search.cv_results_["split%d_test_score" % i][cand_i],
                )

    # Test with a randomized search
        random_search = RandomizedSearchCV(est, est_parameters, cv=cv, n_iter=3)
        random_search.fit(X, y)
        res_params = random_search.cv_results_["params"]

                    random_search.cv_results_["split%d_test_score" % i][cand_i],
                )

def test_predict_proba_disabled():
    # Test predict_proba when disabled on estimator.
    X = np.arange(20).reshape(5, -1)
    y = [0, 0, 1, 1, 1]
    clf = SVC(probability=False)
    gs = GridSearchCV(clf, {}, cv=2).fit(X, y)
    assert not hasattr(gs, "predict_proba")

def test_grid_search_allows_nans():
    # Test GridSearchCV with SimpleImputer
    X = np.arange(20, dtype=np.float64).reshape(5, -1)
        [
        ]
    )
    GridSearchCV(p, {"classifier__foo_param": [1, 2, 3]}, cv=2).fit(X, y)

class FailingClassifier(BaseEstimator):
    """Classifier that raises a ValueError on fit()"""

    FAILING_PARAMETER = 2

        if self.parameter == FailingClassifier.FAILING_PARAMETER:
            raise ValueError("Failing classifier failed as required")

        return 0.0

def test_grid_search_failing_classifier():
    # GridSearchCV with on_error != 'raise'
    # Ensures that a warning is raised and score reset where appropriate.

    X, y = make_classification(n_samples=20, n_features=10, random_state=0)

    clf = FailingClassifier()

    # refit=False because we only want to check that errors caused by fits
    # to individual folds will be caught and warnings raised instead. If
    # refit was done, then an exception would be raised on refit and not
    # caught by grid_search (expected behavior), and this would cause an
    # error in this test.
        clf,
        [{"parameter": [0, 1, 2]}],
        error_score=0.0,
    )

        "5 fits failed.+total of 15.+The score on these"
        r" train-test partitions for these parameters will be set to 0\.0.+"
        "5 fits failed with the following error.+ValueError.+Failing classifier failed"
        " as required",
    )
    n_candidates = len(gs.cv_results_["params"])

    # Ensure that grid scores were set to zero as required for those fits
    # that are expected to fail.
    def get_cand_scores(i):
            [gs.cv_results_["split%d_test_score" % s][i] for s in range(gs.n_splits_)]
        )

        (
            np.all(get_cand_scores(cand_i) == 0.0)
            for cand_i in range(n_candidates)
            if gs.cv_results_["param_parameter"][cand_i]
            == FailingClassifier.FAILING_PARAMETER
        )
    )

        clf,
        error_score=float("nan"),
    )
        r" train-test partitions for these parameters will be set to nan.+"
    )
        np.all(np.isnan(get_cand_scores(cand_i)))
        for cand_i in range(n_candidates)
        if gs.cv_results_["param_parameter"][cand_i]
        == FailingClassifier.FAILING_PARAMETER
    )

    ranks = gs.cv_results_["rank_test_score"]

    # Check that succeeded estimators have lower ranks
    assert ranks[0] <= 2 and ranks[1] <= 2
    # Check that failed estimator has the highest rank
    assert ranks[clf.FAILING_PARAMETER] == 3
    assert gs.best_index_ != clf.FAILING_PARAMETER

def test_grid_search_classifier_all_fits_fail():

        clf,
        [{"parameter": [FailingClassifier.FAILING_PARAMETER] * 3}],
    )

        (
            "All the 15 fits failed.+15 fits failed with the following"
            " error.+ValueError.+Failing classifier failed as required"
        ),
    )
    with pytest.raises(ValueError, match=warning_message):

def test_grid_search_failing_classifier_raise():
    # GridSearchCV with on_error == 'raise' raises the error

    # refit=False because we want to test the behaviour of the grid search part
        clf,
    )

    # FailingClassifier issues a ValueError so this is what we look for.

def test_parameters_sampler_replacement():
    # raise warning if n_iter is bigger than total parameter space
    params = [
        {"first": [0, 1], "second": ["a", "b", "c"]},
        {"third": ["two", "values"]},
    ]
    sampler = ParameterSampler(params, n_iter=9)
    n_iter = 9
    grid_size = 8
    expected_warning = (
        "The total space of parameters %d is smaller "
        "than n_iter=%d. Running %d iterations. For "
        "exhaustive searches, use GridSearchCV." % (grid_size, n_iter, grid_size)
    )
    with pytest.warns(UserWarning, match=expected_warning):
        list(sampler)

    # degenerates to GridSearchCV if n_iter the same as grid_size
    sampler = ParameterSampler(params, n_iter=8)
    samples = list(sampler)
    assert len(samples) == 8
    for values in ParameterGrid(params):
        assert values in samples
    assert len(ParameterSampler(params, n_iter=1000)) == 8

    # test sampling without replacement in a large grid
    params = {"a": range(10), "b": range(10), "c": range(10)}
    sampler = ParameterSampler(params, n_iter=99, random_state=42)
    assert len(samples) == 99
    hashable_samples = ["a%db%dc%d" % (p["a"], p["b"], p["c"]) for p in samples]
    assert len(set(hashable_samples)) == 99

    # doesn't go into infinite loops
    params_distribution = {"first": bernoulli(0.5), "second": ["a", "b", "c"]}
    sampler = ParameterSampler(params_distribution, n_iter=7)
    assert len(samples) == 7

def test_stochastic_gradient_loss_param():
    # Make sure the predict_proba works when loss is specified
    # as one of the parameters in the param_grid.
        "loss": ["log_loss"],
    }
    X = np.arange(24).reshape(6, -1)
    y = [0, 0, 0, 1, 1, 1]
        estimator=SGDClassifier(loss="hinge"), param_grid=param_grid, cv=3
    )

    # When the estimator is not fitted, `predict_proba` is not available as the
    # loss is 'hinge'.
    clf.predict_proba(X)
    clf.predict_log_proba(X)

    # Make sure `predict_proba` is not available when setting loss=['hinge']
    # in param_grid
        "loss": ["hinge"],
    }
    )

def test_search_train_scores_set_to_false():
    X = np.arange(6).reshape(6, -1)

    gs = GridSearchCV(clf, param_grid={"C": [0.1, 0.2]}, cv=3)

def test_grid_search_cv_splits_consistency():
    # Check if a one time iterable is accepted as a cv parameter.

        param_grid={"C": [0.1, 0.2, 0.3]},
    )

    gs2 = GridSearchCV(
    )
    gs2.fit(X, y)

    # Give generator as a cv parameter
    assert isinstance(
        KFold(n_splits=n_splits, shuffle=True, random_state=0).split(X, y),
        GeneratorType,
    )
    gs3 = GridSearchCV(
        cv=KFold(n_splits=n_splits, shuffle=True, random_state=0).split(X, y),
    )
    gs3.fit(X, y)

    gs4 = GridSearchCV(
        cv=KFold(n_splits=n_splits, shuffle=True, random_state=0),
    )
    gs4.fit(X, y)

    def _pop_time_keys(cv_results):
        for key in (
            "mean_fit_time",
            "std_fit_time",
            "mean_score_time",
            "std_score_time",
        ):
            cv_results.pop(key)
        return cv_results

    # Check if generators are supported as cv and
    # that the splits are consistent
        _pop_time_keys(gs3.cv_results_), _pop_time_keys(gs4.cv_results_)
    )

    # OneTimeSplitter is a non-re-entrant cv where split can be called only
    # once if ``cv.split`` is called once per param setting in GridSearchCV.fit
    # the 2nd and 3rd parameter will not be evaluated as no train/test indices
    # will be generated for the 2nd and subsequent cv.split calls.
    # This is a check to make sure cv.split is not called once per param
    # setting.
        {k: v for k, v in gs.cv_results_.items() if not k.endswith("_time")},
        {k: v for k, v in gs2.cv_results_.items() if not k.endswith("_time")},
    )

    # Check consistency of folds across the parameters
        param_grid={"C": [0.1, 0.1, 0.2, 0.2]},
    )

    # As the first two param settings (C=0.1) and the next two param
    # settings (C=0.2) are same, the test and train scores must also be
    # same as long as the same train/test indices are generated for all
    # the cv splits, for both param setting
    for score_type in ("train", "test"):
        per_param_scores = {}
        for param_i in range(4):
            per_param_scores[param_i] = [
                gs.cv_results_["split%d_%s_score" % (s, score_type)][param_i]
                for s in range(5)
            ]

        assert_array_almost_equal(per_param_scores[0], per_param_scores[1])
        assert_array_almost_equal(per_param_scores[2], per_param_scores[3])

def test_transform_inverse_transform_round_trip():

    X_round_trip = grid_search.inverse_transform(grid_search.transform(X))
    assert_array_equal(X, X_round_trip)

def test_custom_run_search():
    def check_results(results, gscv):
        exp_results = gscv.cv_results_
        assert sorted(results.keys()) == sorted(exp_results)
        for k in results:
            if not k.endswith("_time"):
                # XXX: results['params'] is a list :|
                results[k] = np.asanyarray(results[k])
                if results[k].dtype.kind == "O":
                    assert_array_equal(
                        exp_results[k], results[k], err_msg="Checking " + k
                    )
                else:
                    assert_allclose(exp_results[k], results[k], err_msg="Checking " + k)

    def fit_grid(param_grid):
        return GridSearchCV(clf, param_grid, return_train_score=True).fit(X, y)

    class CustomSearchCV(BaseSearchCV):
        def __init__(self, estimator, **kwargs):
            super().__init__(estimator, **kwargs)

        def _run_search(self, evaluate):
            results = evaluate([{"max_depth": 1}, {"max_depth": 2}])
            check_results(results, fit_grid({"max_depth": [1, 2]}))
            results = evaluate([{"min_samples_split": 5}, {"min_samples_split": 10}])
            check_results(
                results,
                fit_grid([{"max_depth": [1, 2]}, {"min_samples_split": [5, 10]}]),
            )

    # Using regressor to make sure each score differs
    clf = DecisionTreeRegressor(random_state=0)
    X, y = make_classification(n_samples=100, n_informative=4, random_state=0)
    mycv = CustomSearchCV(clf, return_train_score=True).fit(X, y)
    gscv = fit_grid([{"max_depth": [1, 2]}, {"min_samples_split": [5, 10]}])

    results = mycv.cv_results_
    check_results(results, gscv)
    for attr in dir(gscv):
        if (
            attr[0].islower()
            and attr[-1:] == "_"
            and attr
            not in {
                "cv_results_",
                "best_estimator_",
                "refit_time_",
                "classes_",
                "scorer_",
            }
        ):
            assert getattr(gscv, attr) == getattr(mycv, attr), (
                "Attribute %s not equal" % attr
            )

def test__custom_fit_no_run_search():
    class NoRunSearchSearchCV(BaseSearchCV):

        def fit(self, X, y=None, groups=None, **fit_params):

    # this should not raise any exceptions
    NoRunSearchSearchCV(SVC()).fit(X, y)

    class BadSearchCV(BaseSearchCV):

    with pytest.raises(NotImplementedError, match="_run_search not implemented."):
        # this should raise a NotImplementedError
        BadSearchCV(SVC()).fit(X, y)

def test_empty_cv_iterator_error():
    # Use global X, y

    # create cv
    cv = KFold(n_splits=3).split(X)

    # pop all of it, this should cause the expected ValueError
    [u for u in cv]
    # cv is empty now

    train_size = 100
    ridge = RandomizedSearchCV(Ridge(), {"alpha": [1e-3, 1e-2, 1e-1]}, cv=cv, n_jobs=4)

    # assert that this raises an error
        match=(
            "No fits were performed. "
            "Was the CV iterator empty\\? "
            "Were there no candidates\\?"
        ),
    ):
        ridge.fit(X[:train_size], y[:train_size])

def test_random_search_bad_cv():

    class BrokenKFold(KFold):
        def get_n_splits(self, *args, **kw):
            return 1

    # create bad cv
    cv = BrokenKFold(n_splits=3)

        match=(
            "cv.split and cv.get_n_splits returned "
            "inconsistent results. Expected \\d+ "
            "splits, got \\d+"
        ),
    ):

def test_searchcv_raise_warning_with_non_finite_score(
    SearchCV, specialized_params, return_train_score
):
    # https://github.com/scikit-learn/scikit-learn/issues/10529
    # Check that we raise a UserWarning when a non-finite score is
    # computed in the SearchCV

    class FailingScorer:
        """Scorer that will fail for some split but not all."""

            self.n_counts = 0

        def __call__(self, estimator, X, y):
            self.n_counts += 1
            if self.n_counts % 5 == 0:
                return np.nan
            return 1

    grid = SearchCV(
        scoring=FailingScorer(),
        cv=3,
        return_train_score=return_train_score,
        **specialized_params,
    )

    with pytest.warns(UserWarning) as warn_msg:
        grid.fit(X, y)

    set_with_warning = ["test", "train"] if return_train_score else ["test"]
    assert len(warn_msg) == len(set_with_warning)
    for msg, dataset in zip(warn_msg, set_with_warning):
        assert f"One or more of the {dataset} scores are non-finite" in str(msg.message)

    # all non-finite scores should be equally ranked last
    last_rank = grid.cv_results_["rank_test_score"].max()
    non_finite_mask = np.isnan(grid.cv_results_["mean_test_score"])
    assert_array_equal(grid.cv_results_["rank_test_score"][non_finite_mask], last_rank)
    # all finite scores should be better ranked than the non-finite scores
    assert np.all(grid.cv_results_["rank_test_score"][~non_finite_mask] < last_rank)

def test_callable_multimetric_confusion_matrix():
    # Test callable with many metrics inserts the correct names and metrics
    # into the search cv object

    search = GridSearchCV(est, {"C": [0.1, 1]}, scoring=custom_scorer, refit="fp")

        assert "mean_test_{}".format(name) in search.cv_results_

    y_pred = search.predict(X)
    assert search.score(X, y) == pytest.approx(cm[0, 1])

def test_callable_multimetric_same_as_list_of_strings():
    # Test callable multimetric is the same as a list of strings
    def custom_scorer(est, X, y):
        y_pred = est.predict(X)
        return {
            "recall": recall_score(y, y_pred),
            "accuracy": accuracy_score(y, y_pred),
        }

    search_callable = GridSearchCV(
        est, {"C": [0.1, 1]}, scoring=custom_scorer, refit="recall"
    )
    search_str = GridSearchCV(
        est, {"C": [0.1, 1]}, scoring=["recall", "accuracy"], refit="recall"
    )

    search_callable.fit(X, y)
    search_str.fit(X, y)

    assert search_callable.best_score_ == pytest.approx(search_str.best_score_)
    assert search_callable.best_index_ == search_str.best_index_
    assert search_callable.score(X, y) == pytest.approx(search_str.score(X, y))

def test_callable_single_metric_same_as_single_string():
    # Tests callable scorer is the same as scoring with a single string
        return recall_score(y, y_pred)

        est, {"C": [0.1, 1]}, scoring=custom_scorer, refit=True
    )
    search_str = GridSearchCV(est, {"C": [0.1, 1]}, scoring="recall", refit="recall")
    search_list_str = GridSearchCV(
        est, {"C": [0.1, 1]}, scoring=["recall"], refit="recall"
    )
    search_list_str.fit(X, y)

    assert search_list_str.best_score_ == pytest.approx(search_str.best_score_)
    assert search_list_str.best_index_ == search_str.best_index_
    assert search_list_str.score(X, y) == pytest.approx(search_str.score(X, y))

def test_callable_multimetric_error_on_invalid_key():
    # Raises when the callable scorer does not return a dict with `refit` key.
    def bad_scorer(est, X, y):
        return {"bad_name": 1}

        scoring=bad_scorer,
        refit="good_name",
    )

    msg = (
        "For multi-metric scoring, the parameter refit must be set to a "
        "scorer key or a callable to refit"
    )

def test_callable_multimetric_error_failing_clf():
    # Warns when there is an estimator the fails to fit with a float
    # error_score
        return {"acc": 1}

        clf,
        error_score=0.1,
    )

        r" train-test partitions for these parameters will be set to 0\.1",
    )

    assert_allclose(gs.cv_results_["mean_test_acc"], [1, 1, 0.1])

def test_callable_multimetric_clf_all_fits_fail():
    # Warns and raises when all estimator fails to fit.

        clf,
    )

        (
            "All the 15 fits failed.+your model is misconfigured.+"
        ),
    )

    # make sure grid search and random search delegate n_features_in to the
    # best estimator
    X, y = make_classification(n_features=n_features)
    param_grid = {"max_iter": [3, 4]}
    gs = GridSearchCV(gbdt, param_grid)
    rs = RandomizedSearchCV(gbdt, param_grid, n_iter=1)
    assert not hasattr(gs, "n_features_in_")
    assert not hasattr(rs, "n_features_in_")
    rs.fit(X, y)
    assert gs.n_features_in_ == n_features
    assert rs.n_features_in_ == n_features

def test_search_cv_pairwise_property_delegated_to_base_estimator(pairwise):
    """
    Test implementation of BaseSearchCV has the pairwise tag
    which matches the pairwise tag of its estimator.
    This test make sure pairwise tag is delegated to the base estimator.

    Non-regression test for issue #13920.
    """

    class TestEstimator(BaseEstimator):
            tags.input_tags.pairwise = pairwise

    est = TestEstimator()
    attr_message = "BaseSearchCV pairwise tag must match estimator"
    cv = GridSearchCV(est, {"n_neighbors": [10]})
    assert pairwise == cv.__sklearn_tags__().input_tags.pairwise, attr_message

def test_search_cv__pairwise_property_delegated_to_base_estimator():
    """
    Test implementation of BaseSearchCV has the pairwise property

    """

    class EstimatorPairwise(BaseEstimator):
        def __init__(self, pairwise=True):
            self.pairwise = pairwise

            tags.input_tags.pairwise = self.pairwise

    est = EstimatorPairwise()
    attr_message = "BaseSearchCV _pairwise property must match estimator"

    for _pairwise_setting in [True, False]:
        est.set_params(pairwise=_pairwise_setting)
        cv = GridSearchCV(est, {"n_neighbors": [10]})
        assert _pairwise_setting == cv.__sklearn_tags__().input_tags.pairwise, (
            attr_message
        )

def test_search_cv_pairwise_property_equivalence_of_precomputed():
    """
    This test ensures the equivalence of 'precomputed'.

    """
    grid_params = {"n_neighbors": [10]}

    # defaults to euclidean metric (minkowski p = 2)
    clf = KNeighborsClassifier()
    cv = GridSearchCV(clf, grid_params, cv=n_splits)
    cv.fit(X, y)
    preds_original = cv.predict(X)

    # precompute euclidean metric to validate pairwise is working
    X_precomputed = euclidean_distances(X)
    clf = KNeighborsClassifier(metric="precomputed")
    cv.fit(X_precomputed, y)
    preds_precomputed = cv.predict(X_precomputed)

    attr_message = "GridSearchCV not identical with precomputed metric"
    assert (preds_original == preds_precomputed).all(), attr_message

def test_scalar_fit_param(SearchCV, param_search):
    # unofficially sanctioned tolerance for scalar values in fit_params
    # https://github.com/scikit-learn/scikit-learn/issues/15805
    class TestEstimator(ClassifierMixin, BaseEstimator):
        def __init__(self, a=None):
            self.a = a

        def fit(self, X, y, r=None):
            self.r_ = r

            return np.zeros(shape=(len(X)))

    model = SearchCV(TestEstimator(), param_search)
    model.fit(X, y, r=42)
    assert model.best_estimator_.r_ == 42

def test_scalar_fit_param_compat(SearchCV, param_search):
    # check support for scalar values in fit_params, for instance in LightGBM
    # that do not exactly respect the scikit-learn API contract but that we do
    # not want to break without an explicit deprecation cycle and API
    # recommendations for implementing early stopping with a user provided
    # validation set. non-regression test for:
    X_train, X_valid, y_train, y_valid = train_test_split(
        *make_classification(random_state=42), random_state=42
    )

    class _FitParamClassifier(SGDClassifier):
        def fit(
            self,
            X,
            y,
            tuple_of_arrays=None,
            scalar_param=None,
            callable_param=None,
        ):
            assert scalar_param > 0
            assert callable(callable_param)

            # The tuple of arrays should be preserved as tuple.
            assert isinstance(tuple_of_arrays, tuple)
            assert tuple_of_arrays[0].ndim == 2
            assert tuple_of_arrays[1].ndim == 1

    def _fit_param_callable():
        pass

    model = SearchCV(_FitParamClassifier(), param_search)

    # NOTE: `fit_params` should be data dependent (e.g. `sample_weight`) which
    # is not the case for the following parameters. But this abuse is common in
    # popular third-party libraries and we should tolerate this behavior for
    # now and be careful not to break support for those without following
    # proper deprecation cycle.
        "tuple_of_arrays": (X_valid, y_valid),
        "callable_param": _fit_param_callable,
        "scalar_param": 42,
    }
    model.fit(X_train, y_train, **fit_params)

def test_search_cv_using_minimal_compatible_estimator(SearchCV, Predictor):

    )

    params = {
        "transformer__param": [1, 10],
        "predictor__parama": [1, 10],
    }
    search = SearchCV(model, params, error_score="raise")

    assert search.best_params_.keys() == params.keys()

    if is_classifier(search):
        assert search.score(X, y) == pytest.approx(accuracy_score(y, y_pred))
    else:
        assert search.score(X, y) == pytest.approx(r2_score(y, y_pred))

def test_search_cv_verbose_3(capsys, return_train_score):
    """Check that search cv with verbose>2 shows the score for single
    metrics. non-regression test for #19658."""

    GridSearchCV(
        clf,
        grid,
        verbose=3,
        cv=3,
    captured = capsys.readouterr().out
        match = re.findall(r"score=\(train=[\d\.]+, test=[\d.]+\)", captured)
    else:
        match = re.findall(r"score=[\d\.]+", captured)
    assert len(match) == 3

def test_search_estimator_param(SearchCV, param_search):
    # test that SearchCV object doesn't change the object given in the parameter grid

    params = {"clf": [LinearSVC()], "clf__C": [0.01]}
    orig_C = params["clf"][0].C

    pipe = Pipeline([("trs", MinimalTransformer()), ("clf", None)])

    param_grid_search = {param_search: params}
    gs = SearchCV(pipe, refit=True, cv=2, scoring="accuracy", **param_grid_search).fit(
        X, y
    )

    # testing that the original object in params is not changed
    assert params["clf"][0].C == orig_C
    # testing that the GS is setting the parameter of the step correctly
    assert gs.best_estimator_.named_steps["clf"].C == 0.01

def test_search_with_2d_array():
    parameter_grid = {
        "vect__ngram_range": ((1, 1), (1, 2)),  # unigrams or bigrams
    }
        [
            ("vect", TfidfVectorizer()),
            ("clf", ComplementNB()),
        ]
    )
        estimator=pipeline,
        param_distributions=parameter_grid,
        n_iter=3,
        n_jobs=2,
        cv=3,
    )
    data_train = ["one", "two", "three", "four", "five"]
    data_target = [0, 0, 1, 0, 1]
    random_search.fit(data_train, data_target)
    result = random_search.cv_results_["param_vect__ngram_range"]
    expected_data = np.empty(3, dtype=object)
    expected_data[:] = [(1, 2), (1, 2), (1, 1)]
    np.testing.assert_array_equal(result.data, expected_data)

def test_search_html_repr():
    """Test different HTML representations for GridSearchCV."""

    pipeline = Pipeline([("scale", StandardScaler()), ("clf", DummyClassifier())])
    param_grid = {"clf": [DummyClassifier(), LogisticRegression()]}

    # Unfitted shows the original pipeline
    search_cv = GridSearchCV(pipeline, param_grid=param_grid, refit=False)
    with config_context(display="diagram"):
        repr_html = search_cv._repr_html_()
        assert "<pre>DummyClassifier()</pre>" in repr_html

    # Fitted with `refit=False` shows the original pipeline

    # Fitted with `refit=True` shows the best estimator
    search_cv = GridSearchCV(pipeline, param_grid=param_grid, refit=True)
        assert "<pre>DummyClassifier()</pre>" not in repr_html
        assert "<pre>LogisticRegression()</pre>" in repr_html

def test_multi_metric_search_forwards_metadata(SearchCV, param_search):
    """Test that *SearchCV forwards metadata correctly when passed multiple metrics."""

    est = LinearSVC()
    param_grid_search = {param_search: {"C": [1]}}

    )
    scoring = dict(my_scorer=scorer, accuracy="accuracy")
    SearchCV(est, refit="accuracy", cv=2, scoring=scoring, **param_grid_search).fit(
        X, y, score_weights=score_weights, score_metadata=score_metadata
    )
    assert len(scorer_registry)
            parent="_score",
        )

def test_score_rejects_params_with_no_routing_enabled(SearchCV, param_search):
    """*SearchCV should reject **params when metadata routing is not enabled
    since this is added only when routing is enabled."""

    gs = SearchCV(est, cv=2, **param_grid_search).fit(X, y)

        gs.score(X, y, metadata=1)

def test_cv_results_dtype_issue_29074():
    """Non-regression test for https://github.com/scikit-learn/scikit-learn/issues/29074"""

    class MetaEstimator(BaseEstimator, ClassifierMixin):
        def __init__(
            self,
            base_clf,
            parameter1=None,
            parameter2=None,
            parameter3=None,
            parameter4=None,
        ):
            self.base_clf = base_clf
            self.parameter1 = parameter1
            self.parameter2 = parameter2
            self.parameter3 = parameter3
            self.parameter4 = parameter4

            self.base_clf.fit(X, y)

            return self.base_clf.score(X, y)

    # Values of param_grid are such that np.result_type gives slightly
    # different errors, in particular ValueError and TypeError
        "parameter1": [None, {"option": "A"}, {"option": "B"}],
        "parameter2": [None, [1, 2]],
        "parameter3": [{"a": 1}],
        "parameter4": ["str1", "str2"],
    }
        estimator=MetaEstimator(LogisticRegression()),
        param_grid=param_grid,
        cv=3,
    )

    for param in param_grid:
        assert grid_search.cv_results_[f"param_{param}"].dtype == object

def test_search_with_estimators_issue_29157():
    """Check cv_results_ for estimators with a `dtype` parameter, e.g. OneHotEncoder."""
        {
            "numeric_1": [1, 2, 3, 4, 5],
            "object_1": ["a", "a", "a", "a", "a"],
            "target": [1.0, 4.1, 2.0, 3.0, 1.0],
        }
    )
    X = df.drop("target", axis=1)
    y = df["target"]
    enc = ColumnTransformer(
        [("enc", OneHotEncoder(sparse_output=False), ["object_1"])],
    )
        [
            ("enc", enc),
            ("regressor", LinearRegression()),
        ]
    )
    grid_params = {
        "enc__enc": [
            OneHotEncoder(sparse_output=False),
            OrdinalEncoder(),
        ]
    }
    grid_search = GridSearchCV(pipe, grid_params, cv=2)
    assert grid_search.cv_results_["param_enc__enc"].dtype == object

def test_cv_results_multi_size_array():
    """Check that GridSearchCV works with params that are arrays of different sizes.

    Non-regression test for #29277.
    """
    X, y = make_classification(n_features=10)

    spline_reg_pipe = make_pipeline(
        SplineTransformer(extrapolation="periodic"),
    )

    n_knots_list = [n_features * i for i in [10, 11, 12]]
    knots_list = [
        np.linspace(0, np.pi * 2, n_knots).reshape((-1, n_features))
        for n_knots in n_knots_list
    ]
    spline_reg_pipe_cv = GridSearchCV(
        estimator=spline_reg_pipe,
        param_grid={
            "splinetransformer__knots": knots_list,
        },
    )

    spline_reg_pipe_cv.fit(X, y)
    assert (
        spline_reg_pipe_cv.cv_results_["param_splinetransformer__knots"].dtype == object
    )

def test_array_api_search_cv_classifier(SearchCV, array_namespace, device, dtype):

    X_np = X.astype(dtype)

    # y should always be an integer, no matter what `dtype` is
    y_np = np.array([0] * 5 + [1] * 5)

        searcher = SearchCV(
            LinearDiscriminantAnalysis(),
            {"tol": [1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7]},
            cv=2,
        )
        searcher.fit(X_xp, y_xp)
        searcher.score(X_xp, y_xp)

def test_yield_masked_array_for_each_param(candidate_params, expected):
    result = list(_yield_masked_array_for_each_param(candidate_params))
    for (key, value), (expected_key, expected_value) in zip(result, expected):
        assert key == expected_key
        assert value.dtype == expected_value.dtype
        np.testing.assert_array_equal(value, expected_value)
        np.testing.assert_array_equal(value.mask, expected_value.mask)

def test_yield_masked_array_no_runtime_warning():
    # non-regression test for https://github.com/scikit-learn/scikit-learn/issues/29929
    candidate_params = [{"param": i} for i in range(1000)]
        list(_yield_masked_array_for_each_param(candidate_params))

            score = 1.0
        else:
            score = 0.0

        """
        """
        )

        """
        """

        """
        """

        """
        """

        return 0.5

        )

        for key in (
        ):

                    )
                else:

                results,
            )

            return 1

            return 1

        return {
        }

            self.a = a

            self.r_ = r

        def fit(
            self,
            X,
            y,
        ):

            self,
            base_clf,
        ):

                results,
            )

            return 1

            return 1

        def fit(
            self,
            X,
            y,
        ):

            self,
            base_clf,
        ):

def _is_constant_feature(var, mean, n_samples):
    """Detect if a feature is indistinguishable from a constant feature.

    The detection is based on its computed variance and on the theoretical
    error bounds of the '2 pass algorithm' for variance computation.

    See "Algorithms for computing the sample variance: analysis and
    """
    # In scikit-learn, variance is always computed using float64 accumulators.

    upper_bound = n_samples * eps * var + (n_samples * mean * eps) ** 2
    return var <= upper_bound

def _handle_zeros_in_scale(scale, copy=True, constant_mask=None):
    """Set scales of near constant features to 1.

    The goal is to avoid division by very small or zero values.

    Near constant features are detected automatically by identifying
    scales close to machine precision unless they are precomputed by
    the caller and passed with the `constant_mask` kwarg.

    Typically for standard scaling, the scales are the standard
    deviation while near constant features are better detected on the
    computed variances which are closer to machine precision by
    construction.
    """
    # if we are fitting on 1D arrays, scale might be a scalar
    if np.isscalar(scale):
        if scale == 0.0:
            scale = 1.0
        return scale
    # scale is an array
    else:
        xp, _ = get_namespace(scale)
        if constant_mask is None:
            # Detect near constant values to avoid dividing by a very small
            # value that could lead to surprising results and numerical
            # stability issues.
            constant_mask = scale < 10 * xp.finfo(scale.dtype).eps

        if copy:
            # New array to avoid side-effects
            scale = xp.asarray(scale, copy=True)
        scale[constant_mask] = 1.0

def scale(X, *, axis=0, with_mean=True, with_std=True, copy=True):
    """Standardize a dataset along any axis.

    Center to the mean and component wise scale to unit variance.

    Read more in the :ref:`User Guide <preprocessing_scaler>`.

        The data to center and scale.

    axis : {0, 1}, default=0
        Axis used to compute the means and standard deviations along. If 0,
        independently standardize each feature, otherwise (if 1) standardize
        each sample.

    with_mean : bool, default=True
        If True, center the data before scaling.

    with_std : bool, default=True
        If True, scale the data to unit variance (or equivalently,
        unit standard deviation).

        If False, try to avoid a copy and scale in place.
        This is not guaranteed to always work in place; e.g. if the data is
        a numpy array with an int dtype, a copy will be returned even with
        copy=False.

    Returns
    -------
    X_tr : {ndarray, sparse matrix} of shape (n_samples, n_features)
        The transformed data.

    See Also
    --------
    StandardScaler : Performs scaling to unit variance using the Transformer
        API (e.g. as part of a preprocessing
        :class:`~sklearn.pipeline.Pipeline`).

    Notes
    -----
    This implementation will refuse to center scipy.sparse matrices
    since it would make them non-sparse and would potentially crash the
    program with memory exhaustion problems.

    Instead the caller is expected to either set explicitly
    `with_mean=False` (in that case, only variance scaling will be
    performed on the features of the CSC matrix) or to call `X.toarray()`
    if he/she expects the materialized dense array to fit in memory.

    To avoid memory copy the caller should pass a CSC matrix.

    NaNs are treated as missing values: disregarded to compute the statistics,
    and maintained during the data transformation.

    We use a biased estimator for the standard deviation, equivalent to
    `numpy.std(x, ddof=0)`. Note that the choice of `ddof` is unlikely to
    affect model performance.

    For a comparison of the different scalers, transformers, and normalizers,
    see: :ref:`sphx_glr_auto_examples_preprocessing_plot_all_scaling.py`.

    .. warning:: Risk of data leak

        Do not use :func:`~sklearn.preprocessing.scale` unless you know
        what you are doing. A common mistake is to apply it to the entire data
        *before* splitting into training and test sets. This will bias the
        model evaluation because information would have leaked from the test
        set to the training set.
        In general, we recommend using
        :class:`~sklearn.preprocessing.StandardScaler` within a
        :ref:`Pipeline <pipeline>` in order to prevent most risks of data
        leaking: `pipe = make_pipeline(StandardScaler(), LogisticRegression())`.

    Examples
    --------
    >>> from sklearn.preprocessing import scale
    >>> X = [[-2, 1, 2], [-1, 0, 1]]
    >>> scale(X, axis=0)  # scaling each column independently
    array([[-1.,  1.,  1.],
           [ 1., -1., -1.]])
    >>> scale(X, axis=1)  # scaling each row independently
    array([[-1.37...,  0.39...,  0.98...],
           [-1.22...,  0.     ,  1.22...]])
    """
        X,
        accept_sparse="csc",
        estimator="the scale function",
        dtype=FLOAT_DTYPES,
        ensure_all_finite="allow-nan",
    )
        if with_mean:
                "Cannot center sparse matrices: pass `with_mean=False` instead"
                " See docstring for motivation and alternatives."
            )
        if axis != 0:
                "Can only scale sparse matrix on axis=0,  got axis=%d" % axis
            )
        if with_std:
            _, var = mean_variance_axis(X, axis=0)
            var = _handle_zeros_in_scale(var, copy=False)
            inplace_column_scale(X, 1 / np.sqrt(var))
    else:
            mean_ = np.nanmean(X, axis)
            scale_ = np.nanstd(X, axis)
        # Xr is a view on the original array that enables easy use of
        # broadcasting on the axis in which we are interested in
        Xr = np.rollaxis(X, axis)
            Xr -= mean_
            mean_1 = np.nanmean(Xr, axis=0)
            # Verify that mean_1 is 'close to zero'. If X contains very
            # large values, mean_1 can also be very large, due to a lack of
            # precision of mean_. In this case, a pre-scaling of the
            # concerned feature is efficient, for instance by its mean or
            # maximum.
            if not np.allclose(mean_1, 0):
                    "Numerical issues were encountered "
                    "when centering the data "
                    "and might not be solved. Dataset may "
                    "contain too large values. You may need "
                    "to prescale your features."
                )
                Xr -= mean_1
            scale_ = _handle_zeros_in_scale(scale_, copy=False)
            Xr /= scale_
            if with_mean:
                mean_2 = np.nanmean(Xr, axis=0)
                # If mean_2 is not 'close to zero', it comes from the fact that
                # scale_ is very small so that mean_2 = mean_1/scale_ > 0, even
                # if mean_1 was close to zero. The problem is thus essentially
                # due to the lack of precision of mean_. A solution is then to
                # subtract the mean again:
                if not np.allclose(mean_2, 0):
                        "Numerical issues were encountered "
                        "when scaling the data "
                        "and might not be solved. The standard "
                        "deviation of the data is probably "
                        "very close to 0. "
                    )
                    Xr -= mean_2
    return X

class MinMaxScaler(OneToOneFeatureMixin, TransformerMixin, BaseEstimator):
    """Transform features by scaling each feature to a given range.

    This estimator scales and translates each feature individually such
    that it is in the given range on the training set, e.g. between
    zero and one.

    The transformation is given by::

        X_std = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))
        X_scaled = X_std * (max - min) + min

    where min, max = feature_range.

    This transformation is often used as an alternative to zero mean,
    unit variance scaling.

    `MinMaxScaler` doesn't reduce the effect of outliers, but it linearly
    scales them down into a fixed range, where the largest occurring data point
    corresponds to the maximum value and the smallest one corresponds to the
    minimum value. For an example visualization, refer to :ref:`Compare
    MinMaxScaler with other scalers <plot_all_scaling_minmax_scaler_section>`.

    feature_range : tuple (min, max), default=(0, 1)
        Desired range of transformed data.

        Set to False to perform inplace row normalization and avoid a
        copy (if the input is already a numpy array).

    clip : bool, default=False
        Set to True to clip transformed values of held-out data to
        provided `feature range`.

    min_ : ndarray of shape (n_features,)
        Per feature adjustment for minimum. Equivalent to
        ``min - X.min(axis=0) * self.scale_``

    scale_ : ndarray of shape (n_features,)
        Per feature relative scaling of the data. Equivalent to
        ``(max - min) / (X.max(axis=0) - X.min(axis=0))``

           *scale_* attribute.

    data_min_ : ndarray of shape (n_features,)
        Per feature minimum seen in the data

           *data_min_*

    data_max_ : ndarray of shape (n_features,)
        Per feature maximum seen in the data

           *data_max_*

    data_range_ : ndarray of shape (n_features,)
        Per feature range ``(data_max_ - data_min_)`` seen in the data

           *data_range_*

        The number of samples processed by the estimator.
        It will be reset on new calls to fit, but increments across
        ``partial_fit`` calls.

    See Also
    --------
    minmax_scale : Equivalent function without the estimator API.

    Notes
    -----
    NaNs are treated as missing values: disregarded in fit, and maintained in
    transform.

    Examples
    --------
    >>> data = [[-1, 2], [-0.5, 6], [0, 10], [1, 18]]
    >>> scaler = MinMaxScaler()
    >>> print(scaler.fit(data))
    MinMaxScaler()
    >>> print(scaler.data_max_)
    [ 1. 18.]
    >>> print(scaler.transform(data))
    [[0.   0.  ]
     [0.25 0.25]
     [0.5  0.5 ]
     [1.   1.  ]]
    >>> print(scaler.transform([[2, 2]]))
    [[1.5 0. ]]
    """

        "feature_range": [tuple],
        "clip": ["boolean"],
    }

    def __init__(self, feature_range=(0, 1), *, copy=True, clip=False):
        self.feature_range = feature_range
        self.clip = clip

    def _reset(self):
        """Reset internal data-dependent state of the scaler, if necessary.

        __init__ parameters are not touched.
        """
        # Checking one attribute is enough, because they are all set together
        # in partial_fit
        if hasattr(self, "scale_"):
            del self.scale_
            del self.min_
            del self.n_samples_seen_
            del self.data_min_
            del self.data_max_
            del self.data_range_

        """Compute the minimum and maximum to be used for later scaling.

            The data used to compute the per-feature minimum and maximum
            used for later scaling along the features axis.

        y : None
            Ignored.

        Returns
        -------
            Fitted scaler.
        """
        # Reset internal state before fitting
        self._reset()
        return self.partial_fit(X, y)

        """Online computation of min and max on X for later scaling.

        All of X is processed as a single batch. This is intended for cases
        when :meth:`fit` is not feasible due to very large number of
        `n_samples` or because X is read from a continuous stream.

            The data used to compute the mean and standard deviation

        y : None
            Ignored.

        Returns
        -------
        """
        feature_range = self.feature_range
        if feature_range[0] >= feature_range[1]:
                "Minimum of desired feature range must be smaller than maximum. Got %s."
                % str(feature_range)
            )

                "MinMaxScaler does not support sparse input. "
                "Consider using MaxAbsScaler instead."
            )

        first_pass = not hasattr(self, "n_samples_seen_")
            self,
            X,
            reset=first_pass,
            dtype=_array_api.supported_float_dtypes(xp),
        )

        device_ = device(X)
        feature_range = (
            xp.asarray(feature_range[0], dtype=X.dtype, device=device_),
            xp.asarray(feature_range[1], dtype=X.dtype, device=device_),
        )

        data_min = _array_api._nanmin(X, axis=0, xp=xp)
        data_max = _array_api._nanmax(X, axis=0, xp=xp)

            self.n_samples_seen_ = X.shape[0]
        else:
            data_min = xp.minimum(self.data_min_, data_min)
            data_max = xp.maximum(self.data_max_, data_max)
            self.n_samples_seen_ += X.shape[0]

        data_range = data_max - data_min
        self.scale_ = (feature_range[1] - feature_range[0]) / _handle_zeros_in_scale(
            data_range, copy=True
        )
        self.min_ = feature_range[0] - data_min * self.scale_
        self.data_min_ = data_min
        self.data_max_ = data_max
        self.data_range_ = data_range

        """Scale features of X according to feature_range.

            Input data that will be transformed.

        Returns
        -------
        """

            self,
            X,
        )

        X *= self.scale_
        X += self.min_
        if self.clip:
            device_ = device(X)
            X = _modify_in_place_if_numpy(
                xp,
                xp.clip,
                X,
                xp.asarray(self.feature_range[0], dtype=X.dtype, device=device_),
                xp.asarray(self.feature_range[1], dtype=X.dtype, device=device_),
                out=X,
            )
        return X

        """Undo the scaling of X according to feature_range.

            Input data that will be transformed. It cannot be sparse.

        Returns
        -------
        """

            X,
        )

        X -= self.min_
        X /= self.scale_
        return X

def minmax_scale(X, feature_range=(0, 1), *, axis=0, copy=True):

    that it is in the given range on the training set, i.e. between

    The transformation is given by (when ``axis=0``)::

    The transformation is calculated as (when ``axis=0``)::

       X_scaled = scale * X + min - X.min(axis=0) * scale
       where scale = (max - min) / (X.max(axis=0) - X.min(axis=0))

       *minmax_scale* function interface
       to :class:`~sklearn.preprocessing.MinMaxScaler`.

        The data.

        Axis used to scale along. If 0, independently scale each feature,
        otherwise (if 1) scale each sample.

    Returns
    -------
    X_tr : ndarray of shape (n_samples, n_features)

        Do not use :func:`~sklearn.preprocessing.minmax_scale` unless you know
        :class:`~sklearn.preprocessing.MinMaxScaler` within a
        leaking: `pipe = make_pipeline(MinMaxScaler(), LogisticRegression())`.

    See Also
    --------
    MinMaxScaler : Performs scaling to a given range using the Transformer

    Notes
    -----

    Examples
    --------
    >>> from sklearn.preprocessing import minmax_scale
    >>> minmax_scale(X, axis=0)  # scale each column independently
           [1., 0., 0.]])
    >>> minmax_scale(X, axis=1)  # scale each row independently
    array([[0.  , 0.75, 1.  ],
           [0.  , 0.5 , 1.  ]])
    """
    # Unlike the scaler object, this function allows 1d input.
    # If copy is required, it will be done inside the scaler object.
        X,
    )
    original_ndim = X.ndim

    if original_ndim == 1:
        X = X.reshape(X.shape[0], 1)

    s = MinMaxScaler(feature_range=feature_range, copy=copy)
        X = s.fit_transform(X)
    else:
        X = s.fit_transform(X.T).T

        X = X.ravel()

    return X

class StandardScaler(OneToOneFeatureMixin, TransformerMixin, BaseEstimator):
    """Standardize features by removing the mean and scaling to unit variance.

    The standard score of a sample `x` is calculated as:

        z = (x - u) / s

    where `u` is the mean of the training samples or zero if `with_mean=False`,
    and `s` is the standard deviation of the training samples or one if
    `with_std=False`.

    Centering and scaling happen independently on each feature by computing
    the relevant statistics on the samples in the training set. Mean and
    standard deviation are then stored to be used on later data using

    Standardization of a dataset is a common requirement for many
    machine learning estimators: they might behave badly if the
    individual features do not more or less look like standard normally
    distributed data (e.g. Gaussian with 0 mean and unit variance).

    For instance many elements used in the objective function of
    a learning algorithm (such as the RBF kernel of Support Vector
    Machines or the L1 and L2 regularizers of linear models) assume that
    all features are centered around 0 and have variance in the same
    order. If a feature has a variance that is orders of magnitude larger
    than others, it might dominate the objective function and make the
    estimator unable to learn from other features correctly as expected.

    `StandardScaler` is sensitive to outliers, and the features may scale
    differently from each other in the presence of outliers. For an example
    visualization, refer to :ref:`Compare StandardScaler with other scalers
    <plot_all_scaling_standard_scaler_section>`.

    This scaler can also be applied to sparse CSR or CSC matrices by passing
    `with_mean=False` to avoid breaking the sparsity structure of the data.

        If False, try to avoid a copy and do inplace scaling instead.
        This is not guaranteed to always work inplace; e.g. if the data is
        not a NumPy array or scipy.sparse CSR matrix, a copy may still be
        returned.

        This does not work (and will raise an exception) when attempted on
        sparse matrices, because centering them entails building a dense
        matrix which in common use cases is likely to be too large to fit in
        memory.

    scale_ : ndarray of shape (n_features,) or None
        Per feature relative scaling of the data to achieve zero mean and unit
        variance. Generally this is calculated using `np.sqrt(var_)`. If a
        variance is zero, we can't achieve unit variance, and the data is left
        as-is, giving a scaling factor of 1. `scale_` is equal to `None`
        when `with_std=False`.

           *scale_*

    mean_ : ndarray of shape (n_features,) or None
        The mean value for each feature in the training set.
        Equal to ``None`` when ``with_mean=False`` and ``with_std=False``.

    var_ : ndarray of shape (n_features,) or None
        The variance for each feature in the training set. Used to compute
        `scale_`. Equal to ``None`` when ``with_mean=False`` and
        ``with_std=False``.

    n_samples_seen_ : int or ndarray of shape (n_features,)
        The number of samples processed by the estimator for each feature.
        If there are no missing samples, the ``n_samples_seen`` will be an
        integer, otherwise it will be an array of dtype int. If
        `sample_weights` are used it will be a float (if no missing data)
        or an array of dtype float that sums the weights seen so far.
        Will be reset on new calls to fit, but increments across

    See Also
    --------
    scale : Equivalent function without the estimator API.

    :class:`~sklearn.decomposition.PCA` : Further removes the linear
        correlation across features with 'whiten=True'.

    Notes
    -----

    Examples
    --------
    >>> data = [[0, 0], [0, 0], [1, 1], [1, 1]]
    >>> scaler = StandardScaler()
    StandardScaler()
    >>> print(scaler.mean_)
    [0.5 0.5]
    [[-1. -1.]
     [-1. -1.]
     [ 1.  1.]
     [ 1.  1.]]
    [[3. 3.]]
    """

        "with_mean": ["boolean"],
        "with_std": ["boolean"],
    }

    def __init__(self, *, copy=True, with_mean=True, with_std=True):

        """
            del self.mean_
            del self.var_

        """Compute the mean and std to be used for later scaling.

        y : None
            Ignored.

               parameter *sample_weight* support to StandardScaler.

        Returns
        -------
        """
        return self.partial_fit(X, y, sample_weight)

        """Online computation of mean and std on X for later scaling.

        The algorithm for incremental mean and std is given in Equation 1.5a,b
        in Chan, Tony F., Gene H. Golub, and Randall J. LeVeque. "Algorithms
        for computing the sample variance: Analysis and recommendations."
        The American Statistician 37.3 (1983): 242-247:

        y : None
            Ignored.

        Returns
        -------
        """
        first_call = not hasattr(self, "n_samples_seen_")
            self,
            X,
        )

        # Even in the case of `with_mean=False`, we update the mean anyway
        # This is needed for the incremental computation of the var
        # See incr_mean_variance_axis and _incremental_mean_variance_axis

        # if n_samples_seen_ is an integer (i.e. no missing values), we need to
        # transform it to a NumPy array of shape (n_features,) required by
        # incr_mean_variance_axis and _incremental_variance_axis
        dtype = np.int64 if sample_weight is None else X.dtype
            self.n_samples_seen_ = np.zeros(n_features, dtype=dtype)
        elif np.size(self.n_samples_seen_) == 1:
            self.n_samples_seen_ = np.repeat(self.n_samples_seen_, X.shape[1])
            self.n_samples_seen_ = self.n_samples_seen_.astype(dtype, copy=False)

            if self.with_mean:
                    "Cannot center sparse matrices: pass `with_mean=False` "
                    "instead. See docstring for motivation and alternatives."
                )
            sparse_constructor = (
                sparse.csr_matrix if X.format == "csr" else sparse.csc_matrix
            )

            if self.with_std:
                # First pass
                if not hasattr(self, "scale_"):
                    self.mean_, self.var_, self.n_samples_seen_ = mean_variance_axis(
                        X, axis=0, weights=sample_weight, return_sum_weights=True
                    )
                # Next passes
                else:
                    (
                        self.mean_,
                        self.var_,
                        self.n_samples_seen_,
                    ) = incr_mean_variance_axis(
                        X,
                        axis=0,
                        last_mean=self.mean_,
                        last_var=self.var_,
                        last_n=self.n_samples_seen_,
                        weights=sample_weight,
                    )
                # We force the mean and variance to float64 for large arrays
                # See https://github.com/scikit-learn/scikit-learn/pull/12338
                self.mean_ = self.mean_.astype(np.float64, copy=False)
                self.var_ = self.var_.astype(np.float64, copy=False)
            else:
                self.mean_ = None  # as with_mean must be False for sparse
                self.var_ = None
                weights = _check_sample_weight(sample_weight, X)
                sum_weights_nan = weights @ sparse_constructor(
                    (np.isnan(X.data), X.indices, X.indptr), shape=X.shape
                )
                self.n_samples_seen_ += (np.sum(weights) - sum_weights_nan).astype(
                    dtype
                )
        else:
            # First pass
            if not hasattr(self, "scale_"):
                self.mean_ = 0.0
                if self.with_std:
                    self.var_ = 0.0
                else:
                    self.var_ = None

            if not self.with_mean and not self.with_std:
                self.mean_ = None
                self.n_samples_seen_ += X.shape[0] - np.isnan(X).sum(axis=0)

            else:
                self.mean_, self.var_, self.n_samples_seen_ = _incremental_mean_and_var(
                    X,
                    self.mean_,
                    self.var_,
                    self.n_samples_seen_,
                )

        # for backward-compatibility, reduce n_samples_seen_ to an integer
        # if the number of samples is the same for each feature (i.e. no
        # missing values)
        if np.ptp(self.n_samples_seen_) == 0:
            self.n_samples_seen_ = self.n_samples_seen_[0]

        if self.with_std:
            # Extract the list of near constant features on the raw variances,
            # before taking the square root.
            constant_mask = _is_constant_feature(
                self.var_, self.mean_, self.n_samples_seen_
            )
            self.scale_ = _handle_zeros_in_scale(
                np.sqrt(self.var_), copy=False, constant_mask=constant_mask
            )
        else:
            self.scale_ = None

        """Perform standardization by centering and scaling.

        X : {array-like, sparse matrix of shape (n_samples, n_features)
            The data used to scale along the features axis.
        copy : bool, default=None
            Copy the input X or not.

        Returns
        -------
        X_tr : {ndarray, sparse matrix} of shape (n_samples, n_features)
        """

        copy = copy if copy is not None else self.copy
            self,
            X,
        )

                )
            if self.scale_ is not None:
                inplace_column_scale(X, 1 / self.scale_)
        else:
                X -= self.mean_
                X /= self.scale_
        return X

    def inverse_transform(self, X, copy=None):
        """Scale back the data to the original representation.

        Returns
        -------
        """

            X,
        )

                    "Cannot uncenter sparse matrices: pass `with_mean=False` "
                    "instead See docstring for motivation and alternatives."
                )
                inplace_column_scale(X, self.scale_)
        else:
                X *= self.scale_
                X += self.mean_
        return X

        tags.input_tags.sparse = not self.with_mean

class MaxAbsScaler(OneToOneFeatureMixin, TransformerMixin, BaseEstimator):
    """Scale each feature by its maximum absolute value.

    that the maximal absolute value of each feature in the
    training set will be 1.0. It does not shift/center the data, and
    thus does not destroy any sparsity.

    This scaler can also be applied to sparse CSR or CSC matrices.

    `MaxAbsScaler` doesn't reduce the effect of outliers; it only linearly
    scales them down. For an example visualization, refer to :ref:`Compare
    MaxAbsScaler with other scalers <plot_all_scaling_max_abs_scaler_section>`.

        Set to False to perform inplace scaling and avoid a copy (if the input
        is already a numpy array).

        Per feature relative scaling of the data.

    max_abs_ : ndarray of shape (n_features,)
        Per feature maximum absolute value.

    See Also
    --------
    maxabs_scale : Equivalent function without the estimator API.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.preprocessing import MaxAbsScaler
    >>> X = [[ 1., -1.,  2.],
    ...      [ 2.,  0.,  0.],
    ...      [ 0.,  1., -1.]]
    >>> transformer = MaxAbsScaler().fit(X)
    >>> transformer
    MaxAbsScaler()
    >>> transformer.transform(X)
    array([[ 0.5, -1. ,  1. ],
           [ 1. ,  0. ,  0. ],
           [ 0. ,  1. , -0.5]])
    """

    _parameter_constraints: dict = {"copy": ["boolean"]}

    def __init__(self, *, copy=True):

        """
            del self.max_abs_

        """Compute the maximum absolute value to be used for later scaling.

        y : None
            Ignored.

        Returns
        -------
        """

        """Online computation of max absolute value of X for later scaling.

        y : None
            Ignored.

        Returns
        -------
        """

            self,
            X,
        )

            mins, maxs = min_max_axis(X, axis=0, ignore_nan=True)
            max_abs = np.maximum(np.abs(mins), np.abs(maxs))
        else:
            max_abs = _array_api._nanmax(xp.abs(X), axis=0, xp=xp)

        else:
            max_abs = xp.maximum(self.max_abs_, max_abs)

        self.max_abs_ = max_abs
        self.scale_ = _handle_zeros_in_scale(max_abs, copy=True)

        """Scale the data.

            The data that should be scaled.

        Returns
        -------
        """

            self,
            X,
        )

            inplace_column_scale(X, 1.0 / self.scale_)
        else:
            X /= self.scale_
        return X

            The data that should be transformed back.

        Returns
        -------
        """

            X,
        )

            inplace_column_scale(X, self.scale_)
        else:
            X *= self.scale_
        return X

def maxabs_scale(X, *, axis=0, copy=True):
    """Scale each feature to the [-1, 1] range without breaking the sparsity.

    This estimator scales each feature individually such
    training set will be 1.0.

        The data.

    Returns
    -------

        Do not use :func:`~sklearn.preprocessing.maxabs_scale` unless you know
        :class:`~sklearn.preprocessing.MaxAbsScaler` within a
        leaking: `pipe = make_pipeline(MaxAbsScaler(), LogisticRegression())`.

    See Also
    --------
    MaxAbsScaler : Performs scaling to the [-1, 1] range using
        the Transformer API (e.g. as part of a preprocessing

    Notes
    -----

    Examples
    --------
    >>> from sklearn.preprocessing import maxabs_scale
    >>> maxabs_scale(X, axis=0)  # scale each column independently
    array([[-1. ,  1. ,  1. ],
           [-0.5,  0. ,  0.5]])
    >>> maxabs_scale(X, axis=1)  # scale each row independently
    array([[-1. ,  0.5,  1. ],
           [-1. ,  0. ,  1. ]])
    """

        X,
        accept_sparse=("csr", "csc"),
    )

    s = MaxAbsScaler(copy=copy)
    else:

    return X

class RobustScaler(OneToOneFeatureMixin, TransformerMixin, BaseEstimator):
    """Scale features using statistics that are robust to outliers.

    This Scaler removes the median and scales the data according to
    the quantile range (defaults to IQR: Interquartile Range).
    The IQR is the range between the 1st quartile (25th quantile)
    and the 3rd quartile (75th quantile).

    Centering and scaling happen independently on each feature by
    computing the relevant statistics on the samples in the training
    set. Median and interquartile range are then stored to be used on
    later data using the :meth:`transform` method.

    Standardization of a dataset is a common preprocessing for many machine
    learning estimators. Typically this is done by removing the mean and
    scaling to unit variance. However, outliers can often influence the sample
    mean / variance in a negative way. In such cases, using the median and the
    interquartile range often give better results. For an example visualization
    and comparison to other scalers, refer to :ref:`Compare RobustScaler with
    other scalers <plot_all_scaling_robust_scaler_section>`.

    with_centering : bool, default=True
        If `True`, center the data before scaling.
        This will cause :meth:`transform` to raise an exception when attempted
        on sparse matrices, because centering them entails building a dense
        memory.

    with_scaling : bool, default=True
        If `True`, scale the data to interquartile range.

    quantile_range : tuple (q_min, q_max), 0.0 < q_min < q_max < 100.0, \
        default=(25.0, 75.0)
        Quantile range used to calculate `scale_`. By default this is equal to
        the IQR, i.e., `q_min` is the first quantile and `q_max` is the third
        quantile.

        If `False`, try to avoid a copy and do inplace scaling instead.
        returned.

    unit_variance : bool, default=False
        If `True`, scale data so that normally distributed features have a
        variance of 1. In general, if the difference between the x-values of
        `q_max` and `q_min` for a standard normal distribution is greater
        than 1, the dataset will be scaled down. If less than 1, the dataset
        will be scaled up.

    center_ : array of floats
        The median value for each feature in the training set.

    scale_ : array of floats
        The (scaled) interquartile range for each feature in the training set.

    See Also
    --------
    robust_scale : Equivalent function without the estimator API.
    sklearn.decomposition.PCA : Further removes the linear correlation across
        features with 'whiten=True'.

    Notes
    -----

    https://en.wikipedia.org/wiki/Median
    https://en.wikipedia.org/wiki/Interquartile_range

    Examples
    --------
    >>> from sklearn.preprocessing import RobustScaler
    >>> X = [[ 1., -2.,  2.],
    ...      [ -2.,  1.,  3.],
    ...      [ 4.,  1., -2.]]
    >>> transformer = RobustScaler().fit(X)
    RobustScaler()
    array([[ 0. , -2. ,  0. ],
           [-1. ,  0. ,  0.4],
           [ 1. ,  0. , -1.6]])
    """

        "with_centering": ["boolean"],
        "with_scaling": ["boolean"],
        "quantile_range": [tuple],
        "unit_variance": ["boolean"],
    }

        self,
        *,
        with_centering=True,
        with_scaling=True,
        quantile_range=(25.0, 75.0),
        unit_variance=False,
    ):
        self.with_centering = with_centering
        self.with_scaling = with_scaling
        self.quantile_range = quantile_range
        self.unit_variance = unit_variance

        """Compute the median and quantiles to be used for scaling.

            The data used to compute the median and quantiles

        y : Ignored

        Returns
        -------
        """
        # at fit, convert sparse matrices to csc for optimized computation of
        # the quantiles
            self,
            X,
        )

        q_min, q_max = self.quantile_range
        if not 0 <= q_min <= q_max <= 100:
            raise ValueError("Invalid quantile range: %s" % str(self.quantile_range))

        if self.with_centering:
                    "Cannot center sparse matrices: use `with_centering=False`"
                    " instead. See docstring for motivation and alternatives."
                )
            self.center_ = np.nanmedian(X, axis=0)
        else:
            self.center_ = None

        if self.with_scaling:
            quantiles = []
                    column_nnz_data = X.data[
                        X.indptr[feature_idx] : X.indptr[feature_idx + 1]
                    ]
                    column_data = np.zeros(shape=X.shape[0], dtype=X.dtype)
                    column_data[: len(column_nnz_data)] = column_nnz_data
                else:
                    column_data = X[:, feature_idx]

                quantiles.append(np.nanpercentile(column_data, self.quantile_range))

            quantiles = np.transpose(quantiles)

            self.scale_ = quantiles[1] - quantiles[0]
            self.scale_ = _handle_zeros_in_scale(self.scale_, copy=False)
            if self.unit_variance:
                adjust = stats.norm.ppf(q_max / 100.0) - stats.norm.ppf(q_min / 100.0)
                self.scale_ = self.scale_ / adjust
        else:

        """Center and scale the data.

            The data used to scale along the specified axis.

        Returns
        -------
        """
            self,
            X,
        )

            if self.with_scaling:
                inplace_column_scale(X, 1.0 / self.scale_)
        else:
            if self.with_centering:
                X -= self.center_
        return X

            The rescaled data to be transformed back.

        Returns
        -------
        """
            X,
        )

        else:
                X += self.center_
        return X

        tags.input_tags.sparse = not self.with_centering

def robust_scale(
    X,
    *,
    axis=0,
    with_centering=True,
    with_scaling=True,
    quantile_range=(25.0, 75.0),
    unit_variance=False,
):

    Center to the median and component wise scale
    according to the interquartile range.

    X : {array-like, sparse matrix} of shape (n_sample, n_features)

        Axis used to compute the medians and IQR along. If 0,
        independently scale each feature, otherwise (if 1) scale

        If `True`, scale the data to unit variance (or equivalently,

    quantile_range : tuple (q_min, q_max), 0.0 < q_min < q_max < 100.0,\
        quantile.

    Returns
    -------

    See Also
    --------
    RobustScaler : Performs centering and scaling using the Transformer API
        (e.g. as part of a preprocessing :class:`~sklearn.pipeline.Pipeline`).

    Notes
    -----

    `with_centering=False` (in that case, only variance scaling will be
    performed on the features of the CSR matrix) or to call `X.toarray()`

    To avoid memory copy the caller should pass a CSR matrix.

        Do not use :func:`~sklearn.preprocessing.robust_scale` unless you know
        :class:`~sklearn.preprocessing.RobustScaler` within a
        leaking: `pipe = make_pipeline(RobustScaler(), LogisticRegression())`.

    Examples
    --------
    >>> from sklearn.preprocessing import robust_scale
    >>> robust_scale(X, axis=0)  # scale each column independently
    >>> robust_scale(X, axis=1)  # scale each row independently
    array([[-1.5,  0. ,  0.5],
    """
        X,
    )

    s = RobustScaler(
        with_centering=with_centering,
        with_scaling=with_scaling,
        quantile_range=quantile_range,
        unit_variance=unit_variance,
    )
    else:

    return X

def normalize(X, norm="l2", *, axis=1, copy=True, return_norm=False):
    """Scale input vectors individually to unit norm (vector length).

    Read more in the :ref:`User Guide <preprocessing_normalization>`.

        The data to normalize, element by element.
        scipy.sparse matrices should be in CSR format to avoid an
        un-necessary copy.

    norm : {'l1', 'l2', 'max'}, default='l2'
        The norm to use to normalize each non zero sample (or each non-zero
        feature if axis is 0).

    axis : {0, 1}, default=1
        Define axis used to normalize the data along. If 1, independently
        normalize each sample, otherwise (if 0) normalize each feature.

        If False, try to avoid a copy and normalize in place.

    return_norm : bool, default=False
        Whether to return the computed norms.

    Returns
    -------
        Normalized input X.

    norms : ndarray of shape (n_samples, ) if axis=1 else (n_features, )
        An array of norms along given axis for X.
        When X is sparse, a NotImplementedError will be raised
        for norm 'l1' or 'l2'.

    See Also
    --------
    Normalizer : Performs normalization using the Transformer API

    Notes
    -----

    Examples
    --------
    >>> from sklearn.preprocessing import normalize
    >>> normalize(X, norm="l1")  # L1 normalization each row independently
    array([[-0.4,  0.2,  0.4],
    >>> normalize(X, norm="l2")  # L2 normalization each row independently
    array([[-0.66...,  0.33...,  0.66...],
           [-0.70...,  0.     ,  0.70...]])
    """
        sparse_format = "csc"
    else:  # axis == 1:
        sparse_format = "csr"

        X,
        accept_sparse=sparse_format,
        estimator="the normalize function",
        dtype=_array_api.supported_float_dtypes(xp),
        force_writeable=True,
    )
        X = X.T

        if return_norm and norm in ("l1", "l2"):
                "return_norm=True is not implemented "
                "for sparse matrices with norm 'l1' "
                "or norm 'l2'"
            )
        if norm == "l1":
            inplace_csr_row_normalize_l1(X)
        elif norm == "l2":
            inplace_csr_row_normalize_l2(X)
        elif norm == "max":
            mins, maxes = min_max_axis(X, 1)
            norms = np.maximum(abs(mins), maxes)
            norms_elementwise = norms.repeat(np.diff(X.indptr))
            mask = norms_elementwise != 0
            X.data[mask] /= norms_elementwise[mask]
    else:
            norms = xp.sum(xp.abs(X), axis=1)
            norms = row_norms(X)
            norms = xp.max(xp.abs(X), axis=1)
        norms = _handle_zeros_in_scale(norms, copy=False)
        X /= norms[:, None]

        X = X.T

    if return_norm:
        return X, norms
    else:
        return X

class Normalizer(OneToOneFeatureMixin, TransformerMixin, BaseEstimator):
    """Normalize samples individually to unit norm.

    Each sample (i.e. each row of the data matrix) with at least one
    non zero component is rescaled independently of other samples so
    that its norm (l1, l2 or inf) equals one.

    This transformer is able to work both with dense numpy arrays and
    scipy.sparse matrix (use CSR format if you want to avoid the burden of
    a copy / conversion).

    Scaling inputs to unit norms is a common operation for text
    classification or clustering for instance. For instance the dot
    product of two l2-normalized TF-IDF vectors is the cosine similarity
    of the vectors and is the base similarity metric for the Vector
    Space Model commonly used by the Information Retrieval community.

    For an example visualization, refer to :ref:`Compare Normalizer with other
    scalers <plot_all_scaling_normalizer_section>`.

        The norm to use to normalize each non zero sample. If norm='max'
        is used, values will be rescaled by the maximum of the absolute
        values.

        copy (if the input is already a numpy array or a scipy.sparse
        CSR matrix).

    See Also
    --------
    normalize : Equivalent function without the estimator API.

    Notes
    -----

    Examples
    --------
    >>> X = [[4, 1, 2, 2],
    ...      [1, 3, 9, 3],
    ...      [5, 7, 5, 1]]
    >>> transformer = Normalizer().fit(X)  # fit does nothing.
    Normalizer()
    array([[0.8, 0.2, 0.4, 0.4],
           [0.1, 0.3, 0.9, 0.3],
           [0.5, 0.7, 0.5, 0.1]])
    """

        "norm": [StrOptions({"l1", "l2", "max"})],
    }

    def __init__(self, norm="l2", *, copy=True):

            The data to estimate the normalization parameters.

        y : Ignored

        Returns
        -------
        """
        validate_data(self, X, accept_sparse="csr")

        """Scale each non zero row of X to unit norm.

            The data to normalize, row by row. scipy.sparse matrices should be
            in CSR format to avoid an un-necessary copy.

        Returns
        -------
        """
            self, X, accept_sparse="csr", force_writeable=True, copy=copy, reset=False
        )
        return normalize(X, norm=self.norm, axis=1, copy=False)

def binarize(X, *, threshold=0.0, copy=True):
    """Boolean thresholding of array-like or scipy.sparse matrix.

    Read more in the :ref:`User Guide <preprocessing_binarization>`.

        The data to binarize, element by element.
        scipy.sparse matrices should be in CSR or CSC format to avoid an

    threshold : float, default=0.0
        Feature values below or equal to this are replaced by 0, above it by 1.
        Threshold may not be less than 0 for operations on sparse matrices.

        If False, try to avoid a copy and binarize in place.
        a numpy array with an object dtype, a copy will be returned even with

    Returns
    -------

    See Also
    --------
    Binarizer : Performs binarization using the Transformer API

    Examples
    --------
    >>> from sklearn.preprocessing import binarize
    >>> X = [[0.4, 0.6, 0.5], [0.6, 0.1, 0.2]]
    >>> binarize(X, threshold=0.5)
    array([[0., 1., 0.],
    """
    X = check_array(X, accept_sparse=["csr", "csc"], force_writeable=True, copy=copy)
        if threshold < 0:
            raise ValueError("Cannot binarize a sparse matrix with threshold < 0")
        cond = X.data > threshold
        not_cond = np.logical_not(cond)
        X.data[cond] = 1
        X.data[not_cond] = 0
        X.eliminate_zeros()
    else:
        xp, _, device = get_namespace_and_device(X)
        float_dtype = _find_matching_floating_dtype(X, threshold, xp=xp)
        cond = xp.astype(X, float_dtype, copy=False) > threshold
        not_cond = xp.logical_not(cond)
        X[cond] = 1
        X[not_cond] = 0
    return X

class Binarizer(OneToOneFeatureMixin, TransformerMixin, BaseEstimator):
    """Binarize data (set feature values to 0 or 1) according to a threshold.

    Values greater than the threshold map to 1, while values less than
    or equal to the threshold map to 0. With the default threshold of 0,
    only positive values map to 1.

    Binarization is a common operation on text count data where the
    analyst can decide to only consider the presence or absence of a
    feature rather than a quantified number of occurrences for instance.

    It can also be used as a pre-processing step for estimators that
    consider boolean random variables (e.g. modelled using the Bernoulli
    distribution in a Bayesian setting).

        Set to False to perform inplace binarization and avoid a copy (if
        the input is already a numpy array or a scipy.sparse CSR matrix).

    See Also
    --------
    binarize : Equivalent function without the estimator API.
    KBinsDiscretizer : Bin continuous data into intervals.
    OneHotEncoder : Encode categorical features as a one-hot numeric array.

    Notes
    -----
    If the input is a sparse matrix, only the non-zero values are subject
    to update by the :class:`Binarizer` class.

    Examples
    --------
    >>> from sklearn.preprocessing import Binarizer
    >>> transformer = Binarizer().fit(X)  # fit does nothing.
    Binarizer()
    """

        "threshold": [Real],
    }

    def __init__(self, *, threshold=0.0, copy=True):

            The data.

        y : None
            Ignored.

        Returns
        -------
        """

        """Binarize each element of X.

            The data to binarize, element by element.
            scipy.sparse matrices should be in CSR format to avoid an
            un-necessary copy.

        copy : bool

        Returns
        -------
        """
        # TODO: This should be refactored because binarize also calls
        # check_array
            self,
            X,
        )
        return binarize(X, threshold=self.threshold, copy=False)

class KernelCenterer(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
    r"""Center an arbitrary kernel matrix :math:`K`.

    Let define a kernel :math:`K` such that:

    .. math::
        K(X, Y) = \phi(X) . \phi(Y)^{T}

    :math:`\phi(X)` is a function mapping of rows of :math:`X` to a
    Hilbert space and :math:`K` is of shape `(n_samples, n_samples)`.

    This class allows to compute :math:`\tilde{K}(X, Y)` such that:

    .. math::
        \tilde{K(X, Y)} = \tilde{\phi}(X) . \tilde{\phi}(Y)^{T}

    :math:`\tilde{\phi}(X)` is the centered mapped data in the Hilbert
    space.

    `KernelCenterer` centers the features without explicitly computing the
    mapping :math:`\phi(\cdot)`. Working with centered kernels is sometime
    expected when dealing with algebra computation such as eigendecomposition
    for :class:`~sklearn.decomposition.KernelPCA` for instance.

    Read more in the :ref:`User Guide <kernel_centering>`.

    K_fit_rows_ : ndarray of shape (n_samples,)
        Average of each column of kernel matrix.

    K_fit_all_ : float
        Average of kernel matrix.

    See Also
    --------
    sklearn.kernel_approximation.Nystroem : Approximate a kernel map
        using a subset of the training data.

       "Nonlinear component analysis as a kernel eigenvalue problem."
       Neural computation 10.5 (1998): 1299-1319.
       <https://www.mlpack.org/papers/kpca.pdf>`_

    Examples
    --------
    >>> from sklearn.preprocessing import KernelCenterer
    >>> K = pairwise_kernels(X, metric='linear')
    >>> K
    array([[  9.,   2.,  -2.],
           [  2.,  14., -13.],
           [ -2., -13.,  21.]])
    >>> transformer = KernelCenterer().fit(K)
    KernelCenterer()
    >>> transformer.transform(K)
    array([[  5.,   0.,  -5.],
           [  0.,  14., -14.],
           [ -5., -14.,  19.]])
    """

    # X is called K in these methods.
    __metadata_request__transform = {"K": metadata_routing.UNUSED}
    __metadata_request__fit = {"K": metadata_routing.UNUSED}

    def fit(self, K, y=None):
        """Fit KernelCenterer.

        K : ndarray of shape (n_samples, n_samples)
            Kernel matrix.

        y : None
            Ignored.

        Returns
        -------
        """
        xp, _ = get_namespace(K)

        K = validate_data(self, K, dtype=_array_api.supported_float_dtypes(xp))

        if K.shape[0] != K.shape[1]:
                "Kernel matrix must be a square matrix."
                " Input is a {}x{} matrix.".format(K.shape[0], K.shape[1])
            )

        n_samples = K.shape[0]
        self.K_fit_rows_ = xp.sum(K, axis=0) / n_samples
        self.K_fit_all_ = xp.sum(self.K_fit_rows_) / n_samples

    def transform(self, K, copy=True):
        """Center kernel matrix.

        K : ndarray of shape (n_samples1, n_samples2)

            Set to False to perform inplace computation.

        Returns
        -------
        K_new : ndarray of shape (n_samples1, n_samples2)
        """

        K = validate_data(
            self,
            K,
        )

        K_pred_cols = (xp.sum(K, axis=1) / self.K_fit_rows_.shape[0])[:, None]

        K -= self.K_fit_rows_
        K -= K_pred_cols
        K += self.K_fit_all_

        return K

    @property
        # Used by ClassNamePrefixFeaturesOutMixin. This model preserves the
        # number of input features but this is not a one-to-one mapping in the
        # usual sense. Hence the choice not to use OneToOneFeatureMixin to
        # implement get_feature_names_out for this class.
        return self.n_features_in_

        tags.input_tags.pairwise = True

def add_dummy_feature(X, value=1.0):
    """Augment dataset with an additional dummy feature.

    This is useful for fitting an intercept term with implementations which
    cannot otherwise fit it directly.

        Data.

        Value to use for the dummy feature.

    Returns
    -------
    X : {ndarray, sparse matrix} of shape (n_samples, n_features + 1)
        Same data with dummy feature added as first column.

    Examples
    --------
    >>> from sklearn.preprocessing import add_dummy_feature
    >>> add_dummy_feature([[0, 1], [1, 0]])
           [1., 1., 0.]])
    """
    X = check_array(X, accept_sparse=["csc", "csr", "coo"], dtype=FLOAT_DTYPES)
    shape = (n_samples, n_features + 1)
            # Shift columns to the right.
            col = X.col + 1
            # Column indices of dummy feature are 0 everywhere.
            col = np.concatenate((np.zeros(n_samples), col))
            # Row indices of dummy feature are 0, ..., n_samples-1.
            row = np.concatenate((np.arange(n_samples), X.row))
            # Prepend the dummy feature n_samples times.
            data = np.concatenate((np.full(n_samples, value), X.data))
            return sparse.coo_matrix((data, (row, col)), shape)
        elif X.format == "csc":
            # Shift index pointers since we need to add n_samples elements.
            indptr = X.indptr + n_samples
            # indptr[0] must be 0.
            indptr = np.concatenate((np.array([0]), indptr))
            indices = np.concatenate((np.arange(n_samples), X.indices))
            return sparse.csc_matrix((data, indices, indptr), shape)
        else:
            klass = X.__class__
            return klass(add_dummy_feature(X.tocoo(), value))
    else:
        return np.hstack((np.full((n_samples, 1), value), X))

class QuantileTransformer(OneToOneFeatureMixin, TransformerMixin, BaseEstimator):
    """Transform features using quantiles information.

    This method transforms the features to follow a uniform or a normal
    distribution. Therefore, for a given feature, this transformation tends
    to spread out the most frequent values. It also reduces the impact of
    (marginal) outliers: this is therefore a robust preprocessing scheme.

    The transformation is applied on each feature independently. First an
    estimate of the cumulative distribution function of a feature is
    used to map the original values to a uniform distribution. The obtained
    values are then mapped to the desired output distribution using the
    associated quantile function. Features values of new/unseen data that fall
    below or above the fitted range will be mapped to the bounds of the output
    distribution. Note that this transform is non-linear. It may distort linear
    correlations between variables measured at the same scale but renders
    variables measured at different scales more directly comparable.

    For example visualizations, refer to :ref:`Compare QuantileTransformer with
    other scalers <plot_all_scaling_quantile_transformer_section>`.

    Read more in the :ref:`User Guide <preprocessing_transformer>`.

    n_quantiles : int, default=1000 or n_samples
        Number of quantiles to be computed. It corresponds to the number
        of landmarks used to discretize the cumulative distribution function.
        If n_quantiles is larger than the number of samples, n_quantiles is set
        to the number of samples as a larger number of quantiles does not give
        a better approximation of the cumulative distribution function

    output_distribution : {'uniform', 'normal'}, default='uniform'
        Marginal distribution for the transformed data. The choices are
        'uniform' (default) or 'normal'.

    ignore_implicit_zeros : bool, default=False
        Only applies to sparse matrices. If True, the sparse entries of the
        matrix are discarded to compute the quantile statistics. If False,
        these entries are treated as zeros.

    subsample : int or None, default=10_000
        Maximum number of samples used to estimate the quantiles for
        computational efficiency. Note that the subsampling procedure may
        differ for value-identical sparse and dense matrices.
        Disable subsampling by setting `subsample=None`.

           The option `None` to disable subsampling was added.

        Determines random number generation for subsampling and smoothing
        noise.
        Please see ``subsample`` for more details.

        Set to False to perform inplace transformation and avoid a copy (if the
        input is already a numpy array).

    n_quantiles_ : int
        The actual number of quantiles used to discretize the cumulative
        distribution function.

    quantiles_ : ndarray of shape (n_quantiles, n_features)
        The values corresponding the quantiles of reference.

    references_ : ndarray of shape (n_quantiles, )
        Quantiles of references.

    See Also
    --------
    quantile_transform : Equivalent function without the estimator API.
    PowerTransformer : Perform mapping to a normal distribution using a power
        transform.
    StandardScaler : Perform standardization that is faster, but less robust
        to outliers.
    RobustScaler : Perform robust standardization that removes the influence
        of outliers but does not put outliers and inliers on the same scale.

    Notes
    -----

    Examples
    --------
    >>> from sklearn.preprocessing import QuantileTransformer
    >>> X = np.sort(rng.normal(loc=0.5, scale=0.25, size=(25, 1)), axis=0)
    >>> qt = QuantileTransformer(n_quantiles=10, random_state=0)
    >>> qt.fit_transform(X)
    """

        "n_quantiles": [Interval(Integral, 1, None, closed="left")],
        "output_distribution": [StrOptions({"uniform", "normal"})],
        "ignore_implicit_zeros": ["boolean"],
        "subsample": [Interval(Integral, 1, None, closed="left"), None],
    }

        self,
        *,
        n_quantiles=1000,
        output_distribution="uniform",
        ignore_implicit_zeros=False,
        subsample=10_000,
    ):
        self.n_quantiles = n_quantiles
        self.output_distribution = output_distribution
        self.ignore_implicit_zeros = ignore_implicit_zeros

    def _dense_fit(self, X, random_state):
        """Compute percentiles for dense matrices.

        """
        if self.ignore_implicit_zeros:
                "'ignore_implicit_zeros' takes effect only with"
                " sparse matrix. This parameter has no effect."
            )

        references = self.references_ * 100

        if self.subsample is not None and self.subsample < n_samples:
            # Take a subsample of `X`
            X = resample(
                X, replace=False, n_samples=self.subsample, random_state=random_state
            )

        self.quantiles_ = np.nanpercentile(X, references, axis=0)
        # Due to floating-point precision error in `np.nanpercentile`,
        # make sure that quantiles are monotonically increasing.
        # Upstream issue in numpy:
        # https://github.com/numpy/numpy/issues/14685
        self.quantiles_ = np.maximum.accumulate(self.quantiles_)

    def _sparse_fit(self, X, random_state):
        """Compute percentiles for sparse matrices.

            The data used to scale along the features axis. The sparse matrix
            needs to be nonnegative. If a sparse matrix is provided,
            it will be converted into a sparse ``csc_matrix``.
        """

        self.quantiles_ = []
        for feature_idx in range(n_features):
            column_nnz_data = X.data[X.indptr[feature_idx] : X.indptr[feature_idx + 1]]
            if self.subsample is not None and len(column_nnz_data) > self.subsample:
                column_subsample = self.subsample * len(column_nnz_data) // n_samples
                if self.ignore_implicit_zeros:
                    column_data = np.zeros(shape=column_subsample, dtype=X.dtype)
                else:
                    column_data = np.zeros(shape=self.subsample, dtype=X.dtype)
                column_data[:column_subsample] = random_state.choice(
                    column_nnz_data, size=column_subsample, replace=False
                )
            else:
                    column_data = np.zeros(shape=len(column_nnz_data), dtype=X.dtype)
                else:
                    column_data = np.zeros(shape=n_samples, dtype=X.dtype)
                column_data[: len(column_nnz_data)] = column_nnz_data

            if not column_data.size:
                # if no nnz, an error will be raised for computing the
                # quantiles. Force the quantiles to be zeros.
                self.quantiles_.append([0] * len(references))
            else:
                self.quantiles_.append(np.nanpercentile(column_data, references))
        self.quantiles_ = np.transpose(self.quantiles_)
        # due to floating-point precision error in `np.nanpercentile`,
        # make sure the quantiles are monotonically increasing

        """Compute the quantiles used for transforming.

            The data used to scale along the features axis. If a sparse
            matrix is provided, it will be converted into a sparse
            ``csc_matrix``. Additionally, the sparse matrix needs to be
            nonnegative if `ignore_implicit_zeros` is False.

        y : None
            Ignored.

        Returns
        -------
           Fitted transformer.
        """
        if self.subsample is not None and self.n_quantiles > self.subsample:
                "The number of quantiles cannot be greater than"
                " the number of samples used. Got {} quantiles"
                " and {} samples.".format(self.n_quantiles, self.subsample)
            )

        X = self._check_inputs(X, in_fit=True, copy=False)

        if self.n_quantiles > n_samples:
                "n_quantiles (%s) is greater than the total number "
                "of samples (%s). n_quantiles is set to "
                "n_samples." % (self.n_quantiles, n_samples)
            )
        self.n_quantiles_ = max(1, min(self.n_quantiles, n_samples))

        # Create the quantiles of reference
        self.references_ = np.linspace(0, 1, self.n_quantiles_, endpoint=True)
            self._sparse_fit(X, rng)
        else:
            self._dense_fit(X, rng)

    def _transform_col(self, X_col, quantiles, inverse):
        """Private function to transform a single feature."""

        output_distribution = self.output_distribution

        if not inverse:
            lower_bound_x = quantiles[0]
            upper_bound_x = quantiles[-1]
            lower_bound_y = 0
            upper_bound_y = 1
        else:
            lower_bound_x = 0
            upper_bound_x = 1
            lower_bound_y = quantiles[0]
            upper_bound_y = quantiles[-1]
            # for inverse transform, match a uniform distribution
            with np.errstate(invalid="ignore"):  # hide NaN comparison warnings
                if output_distribution == "normal":
                    X_col = stats.norm.cdf(X_col)
                # else output distribution is already a uniform distribution

        # find index for lower and higher bounds
        with np.errstate(invalid="ignore"):  # hide NaN comparison warnings
            if output_distribution == "normal":
                lower_bounds_idx = X_col - BOUNDS_THRESHOLD < lower_bound_x
                upper_bounds_idx = X_col + BOUNDS_THRESHOLD > upper_bound_x
            if output_distribution == "uniform":
                lower_bounds_idx = X_col == lower_bound_x
                upper_bounds_idx = X_col == upper_bound_x

        isfinite_mask = ~np.isnan(X_col)
        X_col_finite = X_col[isfinite_mask]
            # Interpolate in one direction and in the other and take the
            # mean. This is in case of repeated values in the features
            # and hence repeated quantiles
            #
            # If we don't do this, only one extreme of the duplicated is
            # used (the upper when we do ascending, and the
            # lower for descending). We take the mean of these two
            X_col[isfinite_mask] = 0.5 * (
                np.interp(X_col_finite, quantiles, self.references_)
                - np.interp(-X_col_finite, -quantiles[::-1], -self.references_[::-1])
            )
        else:
            X_col[isfinite_mask] = np.interp(X_col_finite, self.references_, quantiles)

        X_col[upper_bounds_idx] = upper_bound_y
        X_col[lower_bounds_idx] = lower_bound_y
        # for forward transform, match the output distribution
                    X_col = stats.norm.ppf(X_col)
                    # find the value to clip the data to avoid mapping to
                    # infinity. Clip such that the inverse transform will be
                    # consistent
                    clip_min = stats.norm.ppf(BOUNDS_THRESHOLD - np.spacing(1))
                    clip_max = stats.norm.ppf(1 - (BOUNDS_THRESHOLD - np.spacing(1)))
                    X_col = np.clip(X_col, clip_min, clip_max)
                # else output distribution is uniform and the ppf is the
                # identity function so we let X_col unchanged

        return X_col

    def _check_inputs(self, X, in_fit, accept_sparse_negative=False, copy=False):
        """Check inputs before fit and transform."""
            self,
            X,
            # only set force_writeable for the validation at transform time because
            # it's the only place where QuantileTransformer performs inplace operations.
            force_writeable=True if not in_fit else None,
        )
        # we only accept positive sparse matrix when ignore_implicit_zeros is
        # false and that we call fit or transform.
            if (
                not accept_sparse_negative
                and not self.ignore_implicit_zeros
                and (sparse.issparse(X) and np.any(X.data < 0))
            ):
                    "QuantileTransformer only accepts non-negative sparse matrices."
                )

        return X

    def _transform(self, X, inverse=False):
        """Forward and inverse transform.

        inverse : bool, default=False
            If False, apply forward transform. If True, apply
            inverse transform.

        Returns
        -------
            Projected data.
        """
                column_slice = slice(X.indptr[feature_idx], X.indptr[feature_idx + 1])
                X.data[column_slice] = self._transform_col(
                    X.data[column_slice], self.quantiles_[:, feature_idx], inverse
                )
        else:
                X[:, feature_idx] = self._transform_col(
                    X[:, feature_idx], self.quantiles_[:, feature_idx], inverse
                )

        return X

        """Feature-wise transformation of the data.

        Returns
        -------
            The projected data.
        """
        X = self._check_inputs(X, in_fit=False, copy=self.copy)

        return self._transform(X, inverse=False)

        """Back-projection to the original space.

        Returns
        -------
        Xt : {ndarray, sparse matrix} of (n_samples, n_features)
        """
        X = self._check_inputs(
            X, in_fit=False, accept_sparse_negative=True, copy=self.copy
        )

        return self._transform(X, inverse=True)

def quantile_transform(
    X,
    *,
    axis=0,
    n_quantiles=1000,
    output_distribution="uniform",
    ignore_implicit_zeros=False,
    subsample=int(1e5),
):

        The data to transform.

        transform each feature, otherwise (if 1) transform each sample.

    subsample : int or None, default=1e5

        noise.

        If False, try to avoid a copy and transform in place.

            The default value of `copy` changed from False to True in 0.23.

    Returns
    -------
    Xt : {ndarray, sparse matrix} of shape (n_samples, n_features)

    See Also
    --------
    QuantileTransformer : Performs quantile-based scaling using the
        Transformer API (e.g. as part of a preprocessing
    power_transform : Maps data to a normal distribution using a
        power transformation.
    scale : Performs standardization that is faster, but less robust
    robust_scale : Performs robust standardization that removes the influence

    Notes
    -----

        Do not use :func:`~sklearn.preprocessing.quantile_transform` unless
        you know what you are doing. A common mistake is to apply it
        to the entire data *before* splitting into training and
        test sets. This will bias the model evaluation because
        information would have leaked from the test set to the
        training set.
        :class:`~sklearn.preprocessing.QuantileTransformer` within a
        leaking:`pipe = make_pipeline(QuantileTransformer(),
        LogisticRegression())`.

    Examples
    --------
    >>> from sklearn.preprocessing import quantile_transform
    >>> quantile_transform(X, n_quantiles=10, random_state=0, copy=True)
    """
    n = QuantileTransformer(
        n_quantiles=n_quantiles,
        output_distribution=output_distribution,
        ignore_implicit_zeros=ignore_implicit_zeros,
    )
        X = n.fit_transform(X)
    else:  # axis == 1
        X = n.fit_transform(X.T).T
    return X

class PowerTransformer(OneToOneFeatureMixin, TransformerMixin, BaseEstimator):
    """Apply a power transform featurewise to make data more Gaussian-like.

    Power transforms are a family of parametric, monotonic transformations
    that are applied to make data more Gaussian-like. This is useful for
    modeling issues related to heteroscedasticity (non-constant variance),
    or other situations where normality is desired.

    Currently, PowerTransformer supports the Box-Cox transform and the
    Yeo-Johnson transform. The optimal parameter for stabilizing variance and
    minimizing skewness is estimated through maximum likelihood.

    Box-Cox requires input data to be strictly positive, while Yeo-Johnson
    supports both positive or negative data.

    By default, zero-mean, unit-variance normalization is applied to the
    transformed data.

    For an example visualization, refer to :ref:`Compare PowerTransformer with
    other scalers <plot_all_scaling_power_transformer_section>`. To see the
    effect of Box-Cox and Yeo-Johnson transformations on different
    distributions, see:
    :ref:`sphx_glr_auto_examples_preprocessing_plot_map_data_to_normal.py`.

    method : {'yeo-johnson', 'box-cox'}, default='yeo-johnson'
        The power transform method. Available methods are:

        - 'yeo-johnson' [1]_, works with positive and negative values
        - 'box-cox' [2]_, only works with strictly positive values

    standardize : bool, default=True
        Set to True to apply zero-mean, unit-variance normalization to the
        transformed output.

        Set to False to perform inplace computation during transformation.

    lambdas_ : ndarray of float of shape (n_features,)
        The parameters of the power transformation for the selected features.

    See Also
    --------
    power_transform : Equivalent function without the estimator API.

    QuantileTransformer : Maps data to a standard normal distribution with
        the parameter `output_distribution='normal'`.

    Notes
    -----
    NaNs are treated as missing values: disregarded in ``fit``, and maintained
    in ``transform``.

    .. [1] :doi:`I.K. Yeo and R.A. Johnson, "A new family of power
           transformations to improve normality or symmetry." Biometrika,
           87(4), pp.954-959, (2000). <10.1093/biomet/87.4.954>`

    .. [2] :doi:`G.E.P. Box and D.R. Cox, "An Analysis of Transformations",
           Journal of the Royal Statistical Society B, 26, 211-252 (1964).
           <10.1111/j.2517-6161.1964.tb00553.x>`

    Examples
    --------
    >>> from sklearn.preprocessing import PowerTransformer
    >>> pt = PowerTransformer()
    >>> data = [[1, 2], [3, 2], [4, 5]]
    >>> print(pt.fit(data))
    PowerTransformer()
    >>> print(pt.lambdas_)
    [ 1.386... -3.100...]
    >>> print(pt.transform(data))
    [[-1.316... -0.707...]
     [ 0.209... -0.707...]
     [ 1.106...  1.414...]]
    """

        "method": [StrOptions({"yeo-johnson", "box-cox"})],
        "standardize": ["boolean"],
    }

    def __init__(self, method="yeo-johnson", *, standardize=True, copy=True):
        self.standardize = standardize

        """Estimate the optimal parameter lambda for each feature.

        The optimal lambda parameter for minimizing skewness is estimated on
        each feature independently using maximum likelihood.

            The data used to estimate the optimal transformation parameters.

        y : None
            Ignored.

        Returns
        -------
        """
        self._fit(X, y=y, force_transform=False)

        """Fit `PowerTransformer` to `X`, then transform `X`.

            The data used to estimate the optimal transformation parameters
            and to be transformed using a power transformation.

        y : Ignored

        Returns
        -------
        """
        return self._fit(X, y, force_transform=True)

    def _fit(self, X, y=None, force_transform=False):
        X = self._check_input(X, in_fit=True, check_positive=True)

        if not self.copy and not force_transform:  # if call from fit()
            X = X.copy()  # force copy so that fit does not change X inplace

        mean = np.mean(X, axis=0, dtype=np.float64)
        var = np.var(X, axis=0, dtype=np.float64)

        optim_function = {
            "box-cox": self._box_cox_optimize,
            "yeo-johnson": self._yeo_johnson_optimize,
        }[self.method]

        transform_function = {
            "box-cox": boxcox,
            "yeo-johnson": self._yeo_johnson_transform,

        with np.errstate(invalid="ignore"):  # hide NaN warnings
            self.lambdas_ = np.empty(X.shape[1], dtype=X.dtype)
            for i, col in enumerate(X.T):
                # For yeo-johnson, leave constant features unchanged
                # lambda=1 corresponds to the identity transformation
                is_constant_feature = _is_constant_feature(var[i], mean[i], n_samples)
                if self.method == "yeo-johnson" and is_constant_feature:
                    self.lambdas_[i] = 1.0
                    continue

                self.lambdas_[i] = optim_function(col)

                if self.standardize or force_transform:
                    X[:, i] = transform_function(X[:, i], self.lambdas_[i])

        if self.standardize:
            self._scaler = StandardScaler(copy=False).set_output(transform="default")
            if force_transform:
                X = self._scaler.fit_transform(X)
            else:
                self._scaler.fit(X)

        return X

        """Apply the power transform to each feature using the fitted lambdas.

            The data to be transformed using a power transformation.

        Returns
        -------
        X_trans : ndarray of shape (n_samples, n_features)
        """
        X = self._check_input(X, in_fit=False, check_positive=True, check_shape=True)

        for i, lmbda in enumerate(self.lambdas_):
            with np.errstate(invalid="ignore"):  # hide NaN warnings
                X[:, i] = transform_function(X[:, i], lmbda)

            X = self._scaler.transform(X)

        return X

        """Apply the inverse power transformation using the fitted lambdas.

        The inverse of the Box-Cox transformation is given by::

            if lambda_ == 0:
                X = exp(X_trans)
            else:
                X = (X_trans * lambda_ + 1) ** (1 / lambda_)

        The inverse of the Yeo-Johnson transformation is given by::

            if X >= 0 and lambda_ == 0:
                X = exp(X_trans) - 1
            elif X >= 0 and lambda_ != 0:
                X = (X_trans * lambda_ + 1) ** (1 / lambda_) - 1
            elif X < 0 and lambda_ != 2:
                X = 1 - (-(2 - lambda_) * X_trans + 1) ** (1 / (2 - lambda_))
            elif X < 0 and lambda_ == 2:
                X = 1 - exp(-X_trans)

        Returns
        -------
            The original data.
        """
        X = self._check_input(X, in_fit=False, check_shape=True)

            X = self._scaler.inverse_transform(X)

        inv_fun = {
            "box-cox": inv_boxcox,
            "yeo-johnson": self._yeo_johnson_inverse_transform,
                X[:, i] = inv_fun(X[:, i], lmbda)

        return X

    def _yeo_johnson_inverse_transform(self, x, lmbda):
        """Return inverse-transformed input x following Yeo-Johnson inverse
        transform with parameter lambda.
        """
        x_inv = np.zeros_like(x)
        pos = x >= 0

        # when x >= 0
        if abs(lmbda) < np.spacing(1.0):
            x_inv[pos] = np.exp(x[pos]) - 1
        else:  # lmbda != 0
            x_inv[pos] = np.power(x[pos] * lmbda + 1, 1 / lmbda) - 1

        # when x < 0
        if abs(lmbda - 2) > np.spacing(1.0):
            x_inv[~pos] = 1 - np.power(-(2 - lmbda) * x[~pos] + 1, 1 / (2 - lmbda))
        else:  # lmbda == 2
            x_inv[~pos] = 1 - np.exp(-x[~pos])

        return x_inv

    def _yeo_johnson_transform(self, x, lmbda):
        """Return transformed input x following Yeo-Johnson transform with
        parameter lambda.
        """

        out = np.zeros_like(x)
        pos = x >= 0  # binary mask

        # when x >= 0
            out[pos] = np.log1p(x[pos])
            out[pos] = (np.power(x[pos] + 1, lmbda) - 1) / lmbda

        # when x < 0
            out[~pos] = -(np.power(-x[~pos] + 1, 2 - lmbda) - 1) / (2 - lmbda)
            out[~pos] = -np.log1p(-x[~pos])

        return out

    def _box_cox_optimize(self, x):
        """Find and return optimal lambda parameter of the Box-Cox transform by
        MLE, for observed data x.

        We here use scipy builtins which uses the brent optimizer.
        """
        mask = np.isnan(x)
        if np.all(mask):
            raise ValueError("Column must not be all nan.")

        # the computation of lambda is influenced by NaNs so we need to
        # get rid of them
        _, lmbda = stats.boxcox(x[~mask], lmbda=None)

        return lmbda

    def _yeo_johnson_optimize(self, x):
        """Find and return optimal lambda parameter of the Yeo-Johnson
        transform by MLE, for observed data x.

        Like for Box-Cox, MLE is done via the brent optimizer.
        """
        x_tiny = np.finfo(np.float64).tiny

        def _neg_log_likelihood(lmbda):
            """Return the negative log likelihood of the observed data x as a
            function of lambda."""
            x_trans = self._yeo_johnson_transform(x, lmbda)
            n_samples = x.shape[0]
            x_trans_var = x_trans.var()

            # Reject transformed data that would raise a RuntimeWarning in np.log
            if x_trans_var < x_tiny:
                return np.inf

            log_var = np.log(x_trans_var)
            loglike = -n_samples / 2 * log_var
            loglike += (lmbda - 1) * (np.sign(x) * np.log1p(np.abs(x))).sum()

            return -loglike

        x = x[~np.isnan(x)]
        # choosing bracket -2, 2 like for boxcox
        return optimize.brent(_neg_log_likelihood, brack=(-2, 2))

    def _check_input(self, X, in_fit, check_positive=False, check_shape=False):
        """Validate the input before fit and transform.

        in_fit : bool
            Whether or not `_check_input` is called from `fit` or other
            methods, e.g. `predict`, `transform`, etc.

        check_positive : bool, default=False
            If True, check that all data is positive and non-zero (only if
            ``self.method=='box-cox'``).

        check_shape : bool, default=False
            If True, check that n_features matches the length of self.lambdas_
        """
            self,
            X,
        )

            warnings.filterwarnings("ignore", r"All-NaN (slice|axis) encountered")
            if check_positive and self.method == "box-cox" and np.nanmin(X) <= 0:
                    "The Box-Cox transformation can only be "
                    "applied to strictly positive data"
                )

        if check_shape and not X.shape[1] == len(self.lambdas_):
                "Input data has a different number of features "
                "than fitting data. Should have {n}, data has {m}".format(
                    n=len(self.lambdas_), m=X.shape[1]
                )
            )

        return X

def power_transform(X, method="yeo-johnson", *, standardize=True, copy=True):
    """Parametric, monotonic transformation to make data more Gaussian-like.

    Currently, power_transform supports the Box-Cox transform and the

        The data to be transformed using a power transformation.

            The default value of the `method` parameter changed from
            'box-cox' to 'yeo-johnson' in 0.23.

    Returns
    -------
    X_trans : ndarray of shape (n_samples, n_features)

    See Also
    --------
    PowerTransformer : Equivalent transformation with the

    quantile_transform : Maps data to a standard normal distribution with

    Notes
    -----

    .. [1] I.K. Yeo and R.A. Johnson, "A new family of power transformations to
           improve normality or symmetry." Biometrika, 87(4), pp.954-959,
           (2000).

    .. [2] G.E.P. Box and D.R. Cox, "An Analysis of Transformations", Journal
           of the Royal Statistical Society B, 26, 211-252 (1964).

    Examples
    --------
    >>> from sklearn.preprocessing import power_transform
    >>> print(power_transform(data, method='box-cox'))
    [[-1.332... -0.707...]
     [ 0.256... -0.707...]
     [ 1.076...  1.414...]]

    .. warning:: Risk of data leak.
        Do not use :func:`~sklearn.preprocessing.power_transform` unless you
        know what you are doing. A common mistake is to apply it to the entire
        data *before* splitting into training and test sets. This will bias the
        :class:`~sklearn.preprocessing.PowerTransformer` within a
        leaking, e.g.: `pipe = make_pipeline(PowerTransformer(),
    """
    pt = PowerTransformer(method=method, standardize=standardize, copy=copy)
    return pt.fit_transform(X)

        """

        y : None
            Ignored.

        Returns
        -------
        """

        y : None
            Ignored.

        Returns
        -------
        """
            )

            )

            self,
            X,
        )

        )

        else:

        )

        Returns
        -------
        """

            self,
            X,
        )

                xp,
                xp.clip,
                X,
                out=X,
            )
        return X

        Returns
        -------
        """

            X,
        )

        return X

        """

        y : None
            Ignored.

        Returns
        -------
        """

        y : None
            Ignored.

        Returns
        -------
        """
            self,
            X,
        )

                )
            )

                    )
                else:
                    (
                        X,
                        axis=0,
                    )
            else:
                )
                    dtype
                )
        else:
                else:

            else:
                    X,
                )

            )
            )
        else:

        Returns
        -------
        """

            self,
            X,
        )

                )
        else:
        return X

        Returns
        -------
        """

            X,
        )

                )
        else:
        return X

        """

        y : None
            Ignored.

        Returns
        -------
        """

        y : None
            Ignored.

        Returns
        -------
        """

            self,
            X,
        )

        else:

        else:

        Returns
        -------
        """

            self,
            X,
        )

        else:
        return X

        Returns
        -------
        """

            X,
        )

        else:
        return X

        self,
        *,
    ):

        y : Ignored

        Returns
        -------
        """
            self,
            X,
        )

                )
        else:

                    ]
                else:

        else:

        Returns
        -------
        """
            self,
            X,
        )

        else:
        return X

        Returns
        -------
        """
            X,
        )

        else:
        return X

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """
        )

            The data.

        y : None
            Ignored.

        Returns
        -------
        """

        copy : bool

        Returns
        -------
        """
            self,
            X,
        )

        y : None
            Ignored.

        Returns
        -------
        """

            )

        Returns
        -------
        """

            self,
            K,
        )

        return K

        self,
        *,
    ):

        """
            )

            )

        """

                else:
                )
            else:
                else:

            else:

        y : None
            Ignored.

        Returns
        -------
        """
            )

            )

        else:

        else:

            #
            )
        else:

            self,
            X,
        )
            if (
            ):
                )

        return X

        Returns
        -------
        """
                )
        else:
                )

        return X

        Returns
        -------
        """

        Returns
        -------
        """
        )

        y : None
            Ignored.

        Returns
        -------
        """

        y : Ignored

        Returns
        -------
        """

                    continue

            else:

        return X

        Returns
        -------
        """

        return X

            else:

        Returns
        -------
        """

        inv_fun = {

        return X

        """
        pos = x >= 0

        # when x >= 0

        # when x < 0

        """

        # when x >= 0

        # when x < 0

        return out

        """

        """

        """
            self,
            X,
        )

                )

                )
            )

        return X

class FunctionTransformer(TransformerMixin, BaseEstimator):
    """Constructs a transformer from an arbitrary callable.

    A FunctionTransformer forwards its X (and optionally y) arguments to a
    user-defined function or function object and returns the result of this
    function. This is useful for stateless transformations such as taking the
    log of frequencies, doing custom scaling, etc.

    Note: If a lambda is used as the function, then the resulting
    transformer will not be pickleable.

    Read more in the :ref:`User Guide <function_transformer>`.

        The callable to use for the transformation. This will be passed
        the same arguments as transform, with args and kwargs forwarded.
        If func is None, then func will be the identity function.

    inverse_func : callable, default=None
        The callable to use for the inverse transformation. This will be
        passed the same arguments as inverse transform, with args and
        kwargs forwarded. If inverse_func is None, then inverse_func
        will be the identity function.

    validate : bool, default=False
        Indicate that the input X array should be checked before calling
        ``func``. The possibilities are:

        - If False, there is no input validation.
        - If True, then X will be converted to a 2-dimensional NumPy array or
          sparse matrix. If the conversion is not possible an exception is
          raised.

           The default of ``validate`` changed from True to False.

    accept_sparse : bool, default=False
        Indicate that func accepts a sparse matrix as input. If validate is
        False, this has no effect. Otherwise, if accept_sparse is false,
        sparse matrix inputs will cause an exception to be raised.

       Whether to check that or ``func`` followed by ``inverse_func`` leads to
       the original inputs. It can be used for a sanity check, raising a
       warning when the condition is not fulfilled.

       .. versionadded:: 0.20

    feature_names_out : callable, 'one-to-one' or None, default=None
        Determines the list of feature names that will be returned by the
        `get_feature_names_out` method. If it is 'one-to-one', then the output
        feature names will be equal to the input feature names. If it is a
        callable, then it must take two positional arguments: this
        `FunctionTransformer` (`self`) and an array-like of input feature names
        (`input_features`). It must return an array-like of output feature
        names. The `get_feature_names_out` method is only defined if
        `feature_names_out` is not None.

        See ``get_feature_names_out`` for more details.

    kw_args : dict, default=None
        Dictionary of additional keyword arguments to pass to func.

    inv_kw_args : dict, default=None
        Dictionary of additional keyword arguments to pass to inverse_func.

        Names of features seen during :term:`fit`. Defined only when `X` has feature
        names that are all strings.

    See Also
    --------
    MaxAbsScaler : Scale each feature by its maximum absolute value.
    StandardScaler : Standardize features by removing the mean and
        scaling to unit variance.
    LabelBinarizer : Binarize labels in a one-vs-all fashion.
    MultiLabelBinarizer : Transform between iterable of iterables
        and a multilabel format.

    Notes
    -----
    If `func` returns an output with a `columns` attribute, then the columns is enforced
    to be consistent with the output of `get_feature_names_out`.

    Examples
    --------
    >>> from sklearn.preprocessing import FunctionTransformer
    >>> transformer = FunctionTransformer(np.log1p)
    >>> X = np.array([[0, 1], [2, 3]])
    array([[0.       , 0.6931...],
           [1.0986..., 1.3862...]])
    """

        "validate": ["boolean"],
        "accept_sparse": ["boolean"],
        "feature_names_out": [callable, StrOptions({"one-to-one"}), None],
        "kw_args": [dict, None],
        "inv_kw_args": [dict, None],
    }

        self,
        *,
        validate=False,
        accept_sparse=False,
        feature_names_out=None,
        kw_args=None,
        inv_kw_args=None,
    ):
        self.validate = validate
        self.accept_sparse = accept_sparse
        self.kw_args = kw_args
        self.inv_kw_args = inv_kw_args

    def _check_input(self, X, *, reset):
        if self.validate:
            return validate_data(self, X, accept_sparse=self.accept_sparse, reset=reset)
        elif reset:
            # Set feature_names_in_ and n_features_in_ even if validate=False
            # We run this only when reset==True to store the attributes but not
            # validate them, because validate=False
            _check_n_features(self, X, reset=reset)
            _check_feature_names(self, X, reset=reset)
        return X

    def _check_inverse_transform(self, X):
        """Check that func and inverse_func are the inverse."""
        idx_selected = slice(None, None, max(1, X.shape[0] // 100))
        X_round_trip = self.inverse_transform(self.transform(X[idx_selected]))

        if hasattr(X, "dtype"):
            dtypes = [X.dtype]
        elif hasattr(X, "dtypes"):
            # Dataframes can have multiple dtypes
            dtypes = X.dtypes

        if not all(np.issubdtype(d, np.number) for d in dtypes):
                "'check_inverse' is only supported when all the elements in `X` is"
                " numerical."
            )

        if not _allclose_dense_sparse(X[idx_selected], X_round_trip):
                (
                    "The provided functions are not strictly"
                    " inverse of each other. If you are sure you"
                    " want to proceed regardless, set"
                    " 'check_inverse=False'."
                ),
            )

        """Fit transformer by checking X.

        If ``validate`` is ``True``, ``X`` will be checked.

        X : {array-like, sparse-matrix} of shape (n_samples, n_features) \
                if `validate=True` else any object that `func` can handle
            Input array.

        y : Ignored

        Returns
        -------
            FunctionTransformer class instance.
        """
        X = self._check_input(X, reset=True)
        if self.check_inverse and not (self.func is None or self.inverse_func is None):
            self._check_inverse_transform(X)

        """Transform X using the forward function.

        Returns
        -------
        X_out : array-like, shape (n_samples, n_features)
            Transformed input.
        """
        X = self._check_input(X, reset=False)
        out = self._transform(X, func=self.func, kw_args=self.kw_args)
        output_config = _get_output_config("transform", self)["dense"]

        if hasattr(out, "columns") and self.feature_names_out is not None:
            # check the consistency between the column provided by `transform` and
            # the the column names provided by `get_feature_names_out`.
            feature_names_out = self.get_feature_names_out()
            if list(out.columns) != list(feature_names_out):
                # we can override the column names of the output if it is inconsistent
                # with the column names provided by `get_feature_names_out` in the
                # following cases:
                # * `func` preserved the column names between the input and the output
                # * the input column names are all numbers
                # * the output is requested to be a DataFrame (pandas or polars)
                feature_names_in = getattr(
                    X, "feature_names_in_", _get_feature_names(X)
                )
                same_feature_names_in_out = feature_names_in is not None and list(
                    feature_names_in
                ) == list(out.columns)
                not_all_str_columns = not all(
                    isinstance(col, str) for col in out.columns
                )
                if same_feature_names_in_out or not_all_str_columns:
                    adapter = _get_adapter_from_container(out)
                    out = adapter.create_container(
                        X_output=out,
                        X_original=out,
                        columns=feature_names_out,
                        inplace=False,
                    )
                else:
                        "The output generated by `func` have different column names "
                        "than the ones provided by `get_feature_names_out`. "
                        f"Got output with columns names: {list(out.columns)} and "
                        "`get_feature_names_out` returned: "
                        f"{list(self.get_feature_names_out())}. "
                        "The column names can be overridden by setting "
                        "`set_output(transform='pandas')` or "
                        "`set_output(transform='polars')` such that the column names "
                        "are set to the names provided by `get_feature_names_out`."
                    )

        if self.feature_names_out is None:
            warn_msg = (
                "When `set_output` is configured to be '{0}', `func` should return "
                "a {0} DataFrame to follow the `set_output` API  or `feature_names_out`"
                " should be defined."
            )
            if output_config == "pandas" and not _is_pandas_df(out):
                warnings.warn(warn_msg.format("pandas"))
            elif output_config == "polars" and not _is_polars_df(out):
                warnings.warn(warn_msg.format("polars"))

        return out

        """Transform X using the inverse function.

                if `validate=True` else any object that `inverse_func` can handle

        Returns
        -------
        """
            X = check_array(X, accept_sparse=self.accept_sparse)
        return self._transform(X, func=self.inverse_func, kw_args=self.inv_kw_args)

    @available_if(lambda self: self.feature_names_out is not None)

        This method is only defined if `feature_names_out` is not None.

            Input feature names.

            - If `input_features` is None, then `feature_names_in_` is
              used as the input feature names. If `feature_names_in_` is not
              defined, then names are generated:
              `[x0, x1, ..., x(n_features_in_ - 1)]`.
            - If `input_features` is array-like, then `input_features` must

        Returns
        -------

            - If `feature_names_out` is 'one-to-one', the input feature names
              are returned (see `input_features` above). This requires
              `feature_names_in_` and/or `n_features_in_` to be defined, which
              is done automatically if `validate=True`. Alternatively, you can
              set them in `func`.
            - If `feature_names_out` is a callable, then it is called with two
              arguments, `self` and `input_features`, and its return value is
              returned by this method.
        """
        if hasattr(self, "n_features_in_") or input_features is not None:
            input_features = _check_feature_names_in(self, input_features)
        if self.feature_names_out == "one-to-one":
            names_out = input_features
        elif callable(self.feature_names_out):
            names_out = self.feature_names_out(self, input_features)
        else:
                f"feature_names_out={self.feature_names_out!r} is invalid. "
                'It must either be "one-to-one" or a callable with two '
                "arguments: the function transformer and an array-like of "
                "input feature names. The callable must return an array-like "
                "of output feature names."
            )

    def _transform(self, X, func=None, kw_args=None):
        if func is None:
            func = _identity

        return func(X, **(kw_args if kw_args else {}))

        """Return True since FunctionTransfomer is stateless."""

        tags.no_validation = not self.validate
        tags.input_tags.sparse = not self.validate or self.accept_sparse

        Returns
        -------
        """

    def _get_function_name(self):
        """Get the name display of the `func` used in HTML representation."""
        if hasattr(self.func, "__name__"):
            return self.func.__name__
        if isinstance(self.func, partial):
            return self.func.func.__name__
        return f"{self.func.__class__.__name__}(...)"

            "single",
            self,
            names=self._get_function_name(),
            name_details=str(self),
            name_caption="FunctionTransformer",
            doc_link_label="FunctionTransformer",
        )

        self,
        *,
    ):

        return X

            )

                (
                ),
            )

        y : Ignored

        Returns
        -------
        """

        Returns
        -------
        """

                )
                )
                    )
                else:
                    )

            )

        return out

        Returns
        -------
        """

        Returns
        -------

        """
        else:
            )

        Returns
        -------
        """

            "single",
            self,
        )

def _create_expansion(X, interaction_only, deg, n_features, cumulative_size=0):
    """Helper function for creating and appending sparse expansion matrices"""

    total_nnz = _calc_total_nnz(X.indptr, interaction_only, deg)
    expanded_col = _calc_expanded_nnz(n_features, interaction_only, deg)

    if expanded_col == 0:
    # This only checks whether each block needs 64bit integers upon
    # expansion. We prefer to keep int32 indexing where we can,
    # since currently SciPy's CSR construction downcasts when possible,
    # so we prefer to avoid an unnecessary cast. The dtype may still
    # change in the concatenation process if needed.
    # See: https://github.com/scipy/scipy/issues/16569
    max_indices = expanded_col - 1
    max_indptr = total_nnz
    max_int32 = np.iinfo(np.int32).max
    needs_int64 = max(max_indices, max_indptr) > max_int32
    index_dtype = np.int64 if needs_int64 else np.int32

    # Result of the expansion, modified in place by the
    # `_csr_polynomial_expansion` routine.
    expanded_data = np.empty(shape=total_nnz, dtype=X.data.dtype)
    expanded_indices = np.empty(shape=total_nnz, dtype=index_dtype)
    expanded_indptr = np.empty(shape=X.indptr.shape[0], dtype=index_dtype)
    _csr_polynomial_expansion(
        X.data,
        X.indices,
        X.indptr,
        X.shape[1],
        expanded_data,
        expanded_indices,
        expanded_indptr,
        interaction_only,
        deg,
    )
    return sparse.csr_matrix(
        (expanded_data, expanded_indices, expanded_indptr),
        shape=(X.indptr.shape[0] - 1, expanded_col),
    )

class PolynomialFeatures(TransformerMixin, BaseEstimator):
    """Generate polynomial and interaction features.

    Generate a new feature matrix consisting of all polynomial combinations
    of the features with degree less than or equal to the specified degree.
    For example, if an input sample is two dimensional and of the form
    [a, b], the degree-2 polynomial features are [1, a, b, a^2, ab, b^2].

    Read more in the :ref:`User Guide <polynomial_features>`.

    degree : int or tuple (min_degree, max_degree), default=2
        If a single int is given, it specifies the maximal degree of the
        polynomial features. If a tuple `(min_degree, max_degree)` is passed,
        then `min_degree` is the minimum and `max_degree` is the maximum
        polynomial degree of the generated features. Note that `min_degree=0`
        and `min_degree=1` are equivalent as outputting the degree zero term is
        determined by `include_bias`.

    interaction_only : bool, default=False
        If `True`, only interaction features are produced: features that are
        products of at most `degree` *distinct* input features, i.e. terms with
        power of 2 or higher of the same input feature are excluded:

        - included: `x[0]`, `x[1]`, `x[0] * x[1]`, etc.
        - excluded: `x[0] ** 2`, `x[0] ** 2 * x[1]`, etc.

    include_bias : bool, default=True
        If `True` (default), then include a bias column, the feature in which
        all polynomial powers are zero (i.e. a column of ones - acts as an
        intercept term in a linear model).

    order : {'C', 'F'}, default='C'
        Order of output array in the dense case. `'F'` order is faster to
        compute, but may slow down subsequent estimators.

    powers_ : ndarray of shape (`n_output_features_`, `n_features_in_`)
        `powers_[i, j]` is the exponent of the jth input in the ith output.

    n_output_features_ : int
        The total number of polynomial output features. The number of output
        features is computed by iterating over all suitably sized combinations
        of input features.

    See Also
    --------
    SplineTransformer : Transformer that generates univariate B-spline bases
        for features.

    Notes
    -----
    Be aware that the number of features in the output array scales
    polynomially in the number of features of the input array, and
    exponentially in the degree. High degrees can cause overfitting.

    See :ref:`examples/linear_model/plot_polynomial_interpolation.py
    <sphx_glr_auto_examples_linear_model_plot_polynomial_interpolation.py>`

    Examples
    --------
    >>> from sklearn.preprocessing import PolynomialFeatures
    >>> X = np.arange(6).reshape(3, 2)
    >>> X
           [2, 3],
           [4, 5]])
    >>> poly = PolynomialFeatures(2)
    >>> poly.fit_transform(X)
    array([[ 1.,  0.,  1.,  0.,  0.,  1.],
           [ 1.,  2.,  3.,  4.,  6.,  9.],
           [ 1.,  4.,  5., 16., 20., 25.]])
    >>> poly = PolynomialFeatures(interaction_only=True)
    array([[ 1.,  0.,  1.,  0.],
           [ 1.,  2.,  3.,  6.],
           [ 1.,  4.,  5., 20.]])
    """

        "degree": [Interval(Integral, 0, None, closed="left"), "array-like"],
        "interaction_only": ["boolean"],
        "include_bias": ["boolean"],
        "order": [StrOptions({"C", "F"})],
    }

        self, degree=2, *, interaction_only=False, include_bias=True, order="C"
    ):
        self.interaction_only = interaction_only
        self.include_bias = include_bias

    def _combinations(
        n_features, min_degree, max_degree, interaction_only, include_bias
    ):
        comb = combinations if interaction_only else combinations_w_r
        start = max(1, min_degree)
        iter = chain.from_iterable(
            comb(range(n_features), i) for i in range(start, max_degree + 1)
        )
        if include_bias:
            iter = chain(comb(range(n_features), 0), iter)
        return iter

    def _num_combinations(
    ):
        """Calculate number of terms in polynomial expansion

        This should be equivalent to counting the number of terms returned by
        _combinations(...) but much faster.
        """

        if interaction_only:
            combinations = sum(
                [
                    comb(n_features, i, exact=True)
                    for i in range(max(1, min_degree), min(max_degree, n_features) + 1)
                ]
            )
        else:
            combinations = comb(n_features + max_degree, max_degree, exact=True) - 1
            if min_degree > 0:
                d = min_degree - 1
                combinations -= comb(n_features + d, d, exact=True) - 1

            combinations += 1

        return combinations

    @property
    def powers_(self):
        """Exponent for each of the inputs in the output."""

        combinations = self._combinations(
            n_features=self.n_features_in_,
            min_degree=self._min_degree,
            max_degree=self._max_degree,
            interaction_only=self.interaction_only,
            include_bias=self.include_bias,
        )
        return np.vstack(
            [np.bincount(c, minlength=self.n_features_in_) for c in combinations]
        )

            - If `input_features is None`, then `feature_names_in_` is

        Returns
        -------
        """
        powers = self.powers_
        for row in powers:
            inds = np.where(row)[0]
            if len(inds):
                name = " ".join(
                    (
                        "%s^%d" % (input_features[ind], exp)
                        if exp != 1
                        else input_features[ind]
                    )
                    for ind, exp in zip(inds, row[inds])
                )
            else:
                name = "1"
            feature_names.append(name)

        """
        Compute number of output features.

            The data.

        y : Ignored

        Returns
        -------
        """
        _, n_features = validate_data(self, X, accept_sparse=True).shape

        if isinstance(self.degree, Integral):
            if self.degree == 0 and not self.include_bias:
                    "Setting degree to zero and include_bias to False would result in"
                    " an empty output array."
                )

            self._min_degree = 0
            self._max_degree = self.degree
        elif (
            isinstance(self.degree, collections.abc.Iterable) and len(self.degree) == 2
        ):
            self._min_degree, self._max_degree = self.degree
            if not (
                isinstance(self._min_degree, Integral)
                and isinstance(self._max_degree, Integral)
                and self._min_degree >= 0
                and self._min_degree <= self._max_degree
            ):
                    "degree=(min_degree, max_degree) must "
                    "be non-negative integers that fulfil "
                    "min_degree <= max_degree, got "
                    f"{self.degree}."
                )
            elif self._max_degree == 0 and not self.include_bias:
                    "Setting both min_degree and max_degree to zero and include_bias to"
                    " False would result in an empty output array."
                )
        else:
                "degree must be a non-negative int or tuple "
                "(min_degree, max_degree), got "
                f"{self.degree}."
            )

        self.n_output_features_ = self._num_combinations(
        )
        if self.n_output_features_ > np.iinfo(np.intp).max:
            msg = (
                "The output that would result from the current configuration would"
                f" have {self.n_output_features_} features which is too large to be"
                f" indexed by {np.intp().dtype.name}. Please change some or all of the"
                " following:\n- The number of features in the input, currently"
                f" {n_features=}\n- The range of degrees to calculate, currently"
                f" [{self._min_degree}, {self._max_degree}]\n- Whether to include only"
                f" interaction terms, currently {self.interaction_only}\n- Whether to"
                f" include a bias term, currently {self.include_bias}."
            )
            if (
                np.intp == np.int32
                and self.n_output_features_ <= np.iinfo(np.int64).max
            ):  # pragma: nocover
                msg += (
                    "\nNote that the current Python runtime has a limited 32 bit "
                    "address space and that this configuration would have been "
                    "admissible if run on a 64 bit Python runtime."
                )
        # We also record the number of output features for
        # _min_degree = 0
        self._n_out_full = self._num_combinations(
            min_degree=0,
        )

        """Transform data to polynomial features.

            The data to transform, row by row.

            Prefer CSR over CSC for sparse input (for speed), but CSC is
            required if the degree is 4 or higher. If the degree is less than
            4 and the input format is CSC, it will be converted to CSR, have
            its polynomial features generated, then converted back to CSC.

            If the degree is 2 or 3, the method described in "Leveraging
            Sparsity to Speed Up Polynomial Feature Expansions of CSR Matrices
            Using K-Simplex Numbers" by Andrew Nystrom and John Hughes is
            used, which is much faster than the method used on CSC input. For
            this reason, a CSC input will be converted to CSR, and the output
            will be converted back to CSC prior to being returned, hence the
            preference of CSR.

        Returns
        -------
        XP : {ndarray, sparse matrix} of shape (n_samples, NP)
            The matrix of features, where `NP` is the number of polynomial
            features generated from the combination of inputs. If a sparse
            `csr_matrix`.
        """

            self,
            X,
        )

        max_int32 = np.iinfo(np.int32).max
        if sparse.issparse(X) and X.format == "csr":
            if self._max_degree > 3:
                return self.transform(X.tocsc()).tocsr()
            to_stack = []
            if self.include_bias:
                to_stack.append(
                    sparse.csr_matrix(np.ones(shape=(n_samples, 1), dtype=X.dtype))
                )
            if self._min_degree <= 1 and self._max_degree > 0:
                to_stack.append(X)

            cumulative_size = sum(mat.shape[1] for mat in to_stack)
            for deg in range(max(2, self._min_degree), self._max_degree + 1):
                expanded = _create_expansion(
                    X=X,
                    interaction_only=self.interaction_only,
                    deg=deg,
                    cumulative_size=cumulative_size,
                )
                if expanded is not None:
                    to_stack.append(expanded)
                    cumulative_size += expanded.shape[1]
            if len(to_stack) == 0:
                # edge case: deal with empty matrix
                XP = sparse.csr_matrix((n_samples, 0), dtype=X.dtype)
            else:
                # `scipy.sparse.hstack` breaks in scipy<1.9.2
                # when `n_output_features_ > max_int32`
                all_int32 = all(mat.indices.dtype == np.int32 for mat in to_stack)
                if (
                    sp_version < parse_version("1.9.2")
                    and self.n_output_features_ > max_int32
                    and all_int32
                ):
                    raise ValueError(  # pragma: no cover
                        "In scipy versions `<1.9.2`, the function `scipy.sparse.hstack`"
                        " produces negative columns when:\n1. The output shape contains"
                        " `n_cols` too large to be represented by a 32bit signed"
                        " integer.\n2. All sub-matrices to be stacked have indices of"
                        " dtype `np.int32`.\nTo avoid this error, either use a version"
                        " of scipy `>=1.9.2` or alter the `PolynomialFeatures`"
                        " transformer to produce fewer than 2^31 output features"
                    )
                XP = sparse.hstack(to_stack, dtype=X.dtype, format="csr")
        elif sparse.issparse(X) and X.format == "csc" and self._max_degree < 4:
            return self.transform(X.tocsr()).tocsc()
        elif sparse.issparse(X):
            combinations = self._combinations(
                min_degree=self._min_degree,
                max_degree=self._max_degree,
                interaction_only=self.interaction_only,
                include_bias=self.include_bias,
            )
            columns = []
            for combi in combinations:
                if combi:
                    out_col = 1
                    for col_idx in combi:
                        out_col = X[:, [col_idx]].multiply(out_col)
                    columns.append(out_col)
                else:
                    bias = sparse.csc_matrix(np.ones((X.shape[0], 1)))
                    columns.append(bias)
            XP = sparse.hstack(columns, dtype=X.dtype).tocsc()
        else:
            # Do as if _min_degree = 0 and cut down array after the
            # computation, i.e. use _n_out_full instead of n_output_features_.
            XP = np.empty(
                shape=(n_samples, self._n_out_full), dtype=X.dtype, order=self.order
            )

            # What follows is a faster implementation of:
            # for i, comb in enumerate(combinations):
            #     XP[:, i] = X[:, comb].prod(1)
            # This implementation uses two optimisations.
            # First one is broadcasting,
            # multiply ([X1, ..., Xn], X1) -> [X1 X1, ..., Xn X1]
            # multiply ([X2, ..., Xn], X2) -> [X2 X2, ..., Xn X2]
            # ...
            # multiply ([X[:, start:end], X[:, start]) -> ...
            # Second optimisation happens for degrees >= 3.
            # Xi^3 is computed reusing previous computation:
            # Xi^3 = Xi^2 * Xi.

            # degree 0 term
                XP[:, 0] = 1
                current_col = 1
            else:
                current_col = 0

            if self._max_degree == 0:
                return XP

            # degree 1 term
            XP[:, current_col : current_col + n_features] = X
            index = list(range(current_col, current_col + n_features))
            current_col += n_features
            index.append(current_col)

            # loop over degree >= 2 terms
            for _ in range(2, self._max_degree + 1):
                new_index = []
                end = index[-1]
                for feature_idx in range(n_features):
                    start = index[feature_idx]
                    new_index.append(current_col)
                    if self.interaction_only:
                        start += index[feature_idx + 1] - index[feature_idx]
                    next_col = current_col + end - start
                    if next_col <= current_col:
                        break
                    # XP[:, start:end] are terms of degree d - 1
                    # that exclude feature #feature_idx.
                    np.multiply(
                        XP[:, start:end],
                        X[:, feature_idx : feature_idx + 1],
                        out=XP[:, current_col:next_col],
                        casting="no",
                    )
                    current_col = next_col

                new_index.append(current_col)
                index = new_index

            if self._min_degree > 1:
                n_XP, n_Xout = self._n_out_full, self.n_output_features_
                if self.include_bias:
                    Xout = np.empty(
                        shape=(n_samples, n_Xout), dtype=XP.dtype, order=self.order
                    )
                    Xout[:, 0] = 1
                    Xout[:, 1:] = XP[:, n_XP - n_Xout + 1 :]
                else:
                    Xout = XP[:, n_XP - n_Xout :].copy()
                XP = Xout
        return XP

class SplineTransformer(TransformerMixin, BaseEstimator):
    """Generate univariate B-spline bases for features.

    Generate a new feature matrix consisting of
    `n_splines=n_knots + degree - 1` (`n_knots - 1` for
    `extrapolation="periodic"`) spline basis functions
    (B-splines) of polynomial order=`degree` for each feature.

    In order to learn more about the SplineTransformer class go to:
    :ref:`sphx_glr_auto_examples_applications_plot_cyclical_feature_engineering.py`

    Read more in the :ref:`User Guide <spline_transformer>`.

    n_knots : int, default=5
        Number of knots of the splines if `knots` equals one of
        {'uniform', 'quantile'}. Must be larger or equal 2. Ignored if `knots`
        is array-like.

        The polynomial degree of the spline basis. Must be a non-negative
        integer.

    knots : {'uniform', 'quantile'} or array-like of shape \
        (n_knots, n_features), default='uniform'
        Set knot positions such that first knot <= features <= last knot.

        - If 'uniform', `n_knots` number of knots are distributed uniformly
          from min to max values of the features.
        - If 'quantile', they are distributed uniformly along the quantiles of
          the features.
        - If an array-like is given, it directly specifies the sorted knot
          positions including the boundary knots. Note that, internally,
          `degree` number of knots are added before the first knot, the same
          after the last knot.

    extrapolation : {'error', 'constant', 'linear', 'continue', 'periodic'}, \
        default='constant'
        If 'error', values outside the min and max values of the training
        features raises a `ValueError`. If 'constant', the value of the
        splines at minimum and maximum value of the features is used as
        constant extrapolation. If 'linear', a linear extrapolation is used.
        If 'continue', the splines are extrapolated as is, i.e. option
        `extrapolate=True` in :class:`scipy.interpolate.BSpline`. If
        'periodic', periodic splines with a periodicity equal to the distance
        between the first and last knot are used. Periodic splines enforce
        equal function values and derivatives at the first and last knot.
        For example, this makes it possible to avoid introducing an arbitrary
        jump between Dec 31st and Jan 1st in spline features derived from a
        naturally periodic "day-of-year" input feature. In this case it is
        recommended to manually set the knot values to control the period.

        If False, then the last spline element inside the data range
        of a feature is dropped. As B-splines sum to one over the spline basis
        functions for each data point, they implicitly include a bias term,
        i.e. a column of ones. It acts as an intercept term in a linear models.

        Order of output array in the dense case. `'F'` order is faster to compute, but
        may slow down subsequent estimators.

    sparse_output : bool, default=False
        Will return sparse CSR matrix if set True else will return an array.

    bsplines_ : list of shape (n_features,)
        List of BSplines objects, one for each feature.

        The total number of input features.

    n_features_out_ : int
        The total number of output features, which is computed as
        `n_features * n_splines`, where `n_splines` is
        the number of bases elements of the B-splines,
        `n_knots + degree - 1` for non-periodic splines and
        `n_knots - 1` for periodic ones.
        If `include_bias=False`, then it is only
        `n_features * (n_splines - 1)`.

    See Also
    --------
    KBinsDiscretizer : Transformer that bins continuous data into intervals.

    PolynomialFeatures : Transformer that generates polynomial and interaction
        features.

    Notes
    -----
    High degrees and a high number of knots can cause overfitting.

    <sphx_glr_auto_examples_linear_model_plot_polynomial_interpolation.py>`.

    Examples
    --------
    >>> from sklearn.preprocessing import SplineTransformer
    >>> X = np.arange(6).reshape(6, 1)
    >>> spline = SplineTransformer(degree=2, n_knots=3)
    >>> spline.fit_transform(X)
    array([[0.5 , 0.5 , 0.  , 0.  ],
           [0.18, 0.74, 0.08, 0.  ],
           [0.02, 0.66, 0.32, 0.  ],
           [0.  , 0.32, 0.66, 0.02],
           [0.  , 0.08, 0.74, 0.18],
           [0.  , 0.  , 0.5 , 0.5 ]])
    """

        "n_knots": [Interval(Integral, 2, None, closed="left")],
        "knots": [StrOptions({"uniform", "quantile"}), "array-like"],
        "extrapolation": [
            StrOptions({"error", "constant", "linear", "continue", "periodic"})
        ],
    }

        self,
        n_knots=5,
        degree=3,
        *,
        knots="uniform",
        extrapolation="constant",
        include_bias=True,
        order="C",
        sparse_output=False,
    ):
        self.n_knots = n_knots
        self.knots = knots
        self.extrapolation = extrapolation

    def _get_base_knot_positions(X, n_knots=10, knots="uniform", sample_weight=None):
        """Calculate base knot positions.

        Base knots such that first knot <= feature <= last knot. For the
        B-spline construction with scipy.interpolate.BSpline, 2*degree knots
        beyond the base interval are added.

        Returns
        -------
        knots : ndarray of shape (n_knots, n_features), dtype=np.float64
            Knot positions (points) of base interval.
        """
        if knots == "quantile":
            percentile_ranks = 100 * np.linspace(
                start=0, stop=1, num=n_knots, dtype=np.float64
            )

                knots = np.percentile(X, percentile_ranks, axis=0)
            else:
                knots = np.array(
                    [
                        _weighted_percentile(X, sample_weight, percentile_rank)
                        for percentile_rank in percentile_ranks
                    ]
                )

        else:
            # knots == 'uniform':
            # Note that the variable `knots` has already been validated and
            # `else` is therefore safe.
            # Disregard observations with zero weight.
            mask = slice(None, None, 1) if sample_weight is None else sample_weight > 0
            x_min = np.amin(X[mask], axis=0)
            x_max = np.amax(X[mask], axis=0)

            knots = np.linspace(
                start=x_min,
                stop=x_max,
                num=n_knots,
                endpoint=True,
            )

        return knots

        Returns
        -------
        """
        n_splines = self.bsplines_[0].c.shape[1]

            for j in range(n_splines - 1 + self.include_bias):
                feature_names.append(f"{input_features[i]}_sp_{j}")

        """Compute knot positions of splines.

            The data.

        y : None
            Ignored.

        sample_weight : array-like of shape (n_samples,), default = None
            Individual weights for each sample. Used to calculate quantiles if
            `knots="quantile"`. For `knots="uniform"`, zero weighted
            observations are ignored for finding the min and max of `X`.

        Returns
        -------
        """
            self,
            X,
        )

        if isinstance(self.knots, str):
            base_knots = self._get_base_knot_positions(
                X, n_knots=self.n_knots, knots=self.knots, sample_weight=sample_weight
            )
        else:
            base_knots = check_array(self.knots, dtype=np.float64)
            if base_knots.shape[0] < 2:
                raise ValueError("Number of knots, knots.shape[0], must be >= 2.")
            elif base_knots.shape[1] != n_features:
                raise ValueError("knots.shape[1] == n_features is violated.")
            elif not np.all(np.diff(base_knots, axis=0) > 0):
                raise ValueError("knots must be sorted without duplicates.")

        # number of knots for base interval
        n_knots = base_knots.shape[0]

        if self.extrapolation == "periodic" and n_knots <= self.degree:
                "Periodic splines require degree < n_knots. Got n_knots="
                f"{n_knots} and degree={self.degree}."
            )

        # number of splines basis functions
        if self.extrapolation != "periodic":
            n_splines = n_knots + self.degree - 1
        else:
            # periodic splines have self.degree less degrees of freedom
            n_splines = n_knots - 1

        degree = self.degree
        n_out = n_features * n_splines
        # We have to add degree number of knots below, and degree number knots
        # above the base knots in order to make the spline basis complete.
        if self.extrapolation == "periodic":
            # For periodic splines the spacing of the first / last degree knots
            # needs to be a continuation of the spacing of the last / first
            # base knots.
            period = base_knots[-1] - base_knots[0]
            knots = np.r_[
                base_knots[-(degree + 1) : -1] - period,
                base_knots,
                base_knots[1 : (degree + 1)] + period,
            ]

        else:
            # Eilers & Marx in "Flexible smoothing with B-splines and
            # penalties" https://doi.org/10.1214/ss/1038425655 advice
            # against repeating first and last knot several times, which
            # would have inferior behaviour at boundaries if combined with
            # a penalty (hence P-Spline). We follow this advice even if our
            # splines are unpenalized. Meaning we do not:
            # knots = np.r_[
            #     np.tile(base_knots.min(axis=0), reps=[degree, 1]),
            #     base_knots,
            #     np.tile(base_knots.max(axis=0), reps=[degree, 1])
            # ]
            # Instead, we reuse the distance of the 2 fist/last knots.
            dist_min = base_knots[1] - base_knots[0]
            dist_max = base_knots[-1] - base_knots[-2]

                np.linspace(
                    base_knots[0] - degree * dist_min,
                    base_knots[0] - dist_min,
                    num=degree,
                ),
                    base_knots[-1] + dist_max,
                    base_knots[-1] + degree * dist_max,
                ),
            ]

        # With a diagonal coefficient matrix, we get back the spline basis
        # elements, i.e. the design matrix of the spline.
        # Note, BSpline appreciates C-contiguous float64 arrays as c=coef.
        coef = np.eye(n_splines, dtype=np.float64)
            coef = np.concatenate((coef, coef[:degree, :]))

        extrapolate = self.extrapolation in ["periodic", "continue"]

        bsplines = [
            BSpline.construct_fast(
                knots[:, i], coef, self.degree, extrapolate=extrapolate
            )
            for i in range(n_features)
        ]
        self.bsplines_ = bsplines

        self.n_features_out_ = n_out - n_features * (1 - self.include_bias)

        """Transform each feature data to B-splines.

            The data to transform.

        Returns
        -------
        XBS : {ndarray, sparse matrix} of shape (n_samples, n_features * n_splines)
            The matrix of features, where n_splines is the number of bases
            elements of the B-splines, n_knots + degree - 1.
        """

        X = validate_data(self, X, reset=False, accept_sparse=False, ensure_2d=True)

        # TODO: Remove this condition, once scipy 1.10 is the minimum version.
        #       Only scipy => 1.10 supports design_matrix(.., extrapolate=..).
        #       The default (implicit in scipy < 1.10) is extrapolate=False.
        scipy_1_10 = sp_version >= parse_version("1.10.0")
        # Note: self.bsplines_[0].extrapolate is True for extrapolation in
        # ["periodic", "continue"]
        if scipy_1_10:
            use_sparse = self.sparse_output
            kwargs_extrapolate = {"extrapolate": self.bsplines_[0].extrapolate}
        else:
            use_sparse = self.sparse_output and not self.bsplines_[0].extrapolate
            kwargs_extrapolate = dict()

        # Note that scipy BSpline returns float64 arrays and converts input
        # x=X[:, i] to c-contiguous float64.
        n_out = self.n_features_out_ + n_features * (1 - self.include_bias)
        if X.dtype in FLOAT_DTYPES:
            dtype = X.dtype
        else:
        if use_sparse:
            output_list = []
        else:
            XBS = np.zeros((n_samples, n_out), dtype=dtype, order=self.order)

        for i in range(n_features):
            spl = self.bsplines_[i]

            if self.extrapolation in ("continue", "error", "periodic"):
                if self.extrapolation == "periodic":
                    # With periodic extrapolation we map x to the segment
                    # [spl.t[k], spl.t[n]].
                    # This is equivalent to BSpline(.., extrapolate="periodic")
                    # for scipy>=1.0.0.
                    n = spl.t.size - spl.k - 1
                    # Assign to new array to avoid inplace operation
                    x = spl.t[spl.k] + (X[:, i] - spl.t[spl.k]) % (
                        spl.t[n] - spl.t[spl.k]
                    )
                else:
                    x = X[:, i]

                if use_sparse:
                    XBS_sparse = BSpline.design_matrix(
                        x, spl.t, spl.k, **kwargs_extrapolate
                    )
                    if self.extrapolation == "periodic":
                        # See the construction of coef in fit. We need to add the last
                        # degree spline basis function to the first degree ones and
                        # then drop the last ones.
                        # Note: See comment about SparseEfficiencyWarning below.
                        XBS_sparse = XBS_sparse.tolil()
                        XBS_sparse[:, :degree] += XBS_sparse[:, -degree:]
                        XBS_sparse = XBS_sparse[:, :-degree]
                else:
                    XBS[:, (i * n_splines) : ((i + 1) * n_splines)] = spl(x)
            else:  # extrapolation in ("constant", "linear")
                xmin, xmax = spl.t[degree], spl.t[-degree - 1]
                # spline values at boundaries
                f_min, f_max = spl(xmin), spl(xmax)
                mask = (xmin <= X[:, i]) & (X[:, i] <= xmax)
                    mask_inv = ~mask
                    x = X[:, i].copy()
                    # Set some arbitrary values outside boundary that will be reassigned
                    # later.
                    x[mask_inv] = spl.t[self.degree]
                    XBS_sparse = BSpline.design_matrix(x, spl.t, spl.k)
                    # Note: Without converting to lil_matrix we would get:
                    # scipy.sparse._base.SparseEfficiencyWarning: Changing the sparsity
                    # structure of a csr_matrix is expensive. lil_matrix is more
                    # efficient.
                    if np.any(mask_inv):
                        XBS_sparse[mask_inv, :] = 0
                else:
                    XBS[mask, (i * n_splines) : ((i + 1) * n_splines)] = spl(X[mask, i])

            # Note for extrapolation:
            # 'continue' is already returned as is by scipy BSplines
            if self.extrapolation == "error":
                # BSpline with extrapolate=False does not raise an error, but
                # outputs np.nan.
                if (use_sparse and np.any(np.isnan(XBS_sparse.data))) or (
                    not use_sparse
                    and np.any(
                        np.isnan(XBS[:, (i * n_splines) : ((i + 1) * n_splines)])
                    )
                ):
                        "X contains values beyond the limits of the knots."
                    )
            elif self.extrapolation == "constant":
                # Set all values beyond xmin and xmax to the value of the
                # spline basis functions at those two positions.
                # Only the first degree and last degree number of splines
                # have non-zero values at the boundaries.

                mask = X[:, i] < xmin
                if np.any(mask):
                    if use_sparse:
                        # Note: See comment about SparseEfficiencyWarning above.
                        XBS_sparse[mask, :degree] = f_min[:degree]

                    else:
                        XBS[mask, (i * n_splines) : (i * n_splines + degree)] = f_min[
                            :degree
                        ]

                mask = X[:, i] > xmax
                        XBS_sparse[mask, -degree:] = f_max[-degree:]
                    else:
                        XBS[
                            mask,
                            ((i + 1) * n_splines - degree) : ((i + 1) * n_splines),
                        ] = f_max[-degree:]

            elif self.extrapolation == "linear":
                # Continue the degree first and degree last spline bases
                # linearly beyond the boundaries, with slope = derivative at
                # the boundary.
                # Note that all others have derivative = value = 0 at the
                # boundaries.

                # spline derivatives = slopes at boundaries
                fp_min, fp_max = spl(xmin, nu=1), spl(xmax, nu=1)
                # Compute the linear continuation.
                if degree <= 1:
                    # For degree=1, the derivative of 2nd spline is not zero at
                    # boundary. For degree=0 it is the same as 'constant'.
                    degree += 1
                for j in range(degree):
                    mask = X[:, i] < xmin
                    if np.any(mask):
                        linear_extr = f_min[j] + (X[mask, i] - xmin) * fp_min[j]
                        if use_sparse:
                            # Note: See comment about SparseEfficiencyWarning above.
                            XBS_sparse = XBS_sparse.tolil()
                            XBS_sparse[mask, j] = linear_extr
                        else:
                            XBS[mask, i * n_splines + j] = linear_extr

                    mask = X[:, i] > xmax
                        k = n_splines - 1 - j
                        linear_extr = f_max[k] + (X[mask, i] - xmax) * fp_max[k]
                            XBS_sparse[mask, k : k + 1] = linear_extr[:, None]
                        else:
                            XBS[mask, i * n_splines + k] = linear_extr

            if use_sparse:
                XBS_sparse = XBS_sparse.tocsr()
                output_list.append(XBS_sparse)

            # TODO: Remove this conditional error when the minimum supported version of
            # SciPy is 1.9.2
            # `scipy.sparse.hstack` breaks in scipy<1.9.2
            # when `n_features_out_ > max_int32`
            max_int32 = np.iinfo(np.int32).max
            all_int32 = True
            for mat in output_list:
                all_int32 &= mat.indices.dtype == np.int32
            if (
                sp_version < parse_version("1.9.2")
                and self.n_features_out_ > max_int32
                and all_int32
            ):
                    "In scipy versions `<1.9.2`, the function `scipy.sparse.hstack`"
                    " produces negative columns when:\n1. The output shape contains"
                    " `n_cols` too large to be represented by a 32bit signed"
                    " integer.\n. All sub-matrices to be stacked have indices of"
                    " dtype `np.int32`.\nTo avoid this error, either use a version"
                    " of scipy `>=1.9.2` or alter the `SplineTransformer`"
                    " transformer to produce fewer than 2^31 output features"
                )
            XBS = sparse.hstack(output_list, format="csr")
        elif self.sparse_output:
            # TODO: Remove ones scipy 1.10 is the minimum version. See comments above.
            XBS = sparse.csr_matrix(XBS)

        if self.include_bias:
            return XBS
        else:
            # We throw away one spline basis per feature.
            # We chose the last one.
            indices = [j for j in range(XBS.shape[1]) if (j + 1) % n_splines != 0]
            return XBS[:, indices]

    ):

    ):
        )

    ):

        """

                [
                ]
            )
        else:

        )
        )

        Returns
        -------
        """
                    (
                        if exp != 1
                    )
                )
            else:
                name = "1"

        """

            The data.

        y : Ignored

        Returns
        -------
        """

                )

        elif (
        ):
            if not (
            ):
                )
                )
        else:
            )

        )
            msg = (
            )
            if (
                msg += (
                )
        )

        Returns
        -------
        """

            self,
            X,
        )

                )

                    X=X,
                    deg=deg,
                )
            else:
                if (
                ):
                    )
            )
                if combi:
                    out_col = 1
                else:
        else:
            )

            # ...

                XP[:, 0] = 1
            else:

                return XP

                        break
                    )

                    )
                else:
                XP = Xout
        return XP

        self,
        degree=3,
        *,
    ):

        Returns
        -------
        """
            )

            else:
                    [
                    ]
                )

        else:

            )

        Returns
        -------
        """

            The data.

        y : None
            Ignored.

        Returns
        -------
        """
            self,
            X,
        )

            )
        else:

            )

        else:

            ]

        else:
            # ]

                ),
                ),
            ]

            )
        ]

        Returns
        -------
        """

        else:

        else:
        else:

                    )
                else:
                    x = X[:, i]

                    )
                else:
                    # later.
                else:

                    )
                ):
                    )

                    else:
                            :degree
                        ]

                    else:
                        XBS[
                            mask,

                    degree += 1
                        else:

                        else:

            if (
            ):
                )

            return XBS
        else:

class TargetEncoder(OneToOneFeatureMixin, _BaseEncoder):
    """Target Encoder for regression and classification targets.

    Each category is encoded based on a shrunk estimate of the average target
    values for observations belonging to the category. The encoding scheme mixes
    the global target mean with the target mean conditioned on the value of the
    category (see [MIC]_).

    When the target type is "multiclass", encodings are based
    on the conditional probability estimate for each class. The target is first
    binarized using the "one-vs-all" scheme via
    :class:`~sklearn.preprocessing.LabelBinarizer`, then the average target
    value for each class and each category is used for encoding, resulting in
    `n_features` * `n_classes` encoded output features.

    :class:`TargetEncoder` considers missing values, such as `np.nan` or `None`,
    as another category and encodes them like any other category. Categories
    that are not seen during :meth:`fit` are encoded with the target mean, i.e.
    `target_mean_`.

    For a demo on the importance of the `TargetEncoder` internal cross-fitting,
    see
    :ref:`sphx_glr_auto_examples_preprocessing_plot_target_encoder_cross_val.py`.
    For a comparison of different encoders, refer to
    :ref:`sphx_glr_auto_examples_preprocessing_plot_target_encoder.py`. Read
    more in the :ref:`User Guide <target_encoder>`.

    .. note::
        `fit(X, y).transform(X)` does not equal `fit_transform(X, y)` because a
        :term:`cross fitting` scheme is used in `fit_transform` for encoding.
        See the :ref:`User Guide <target_encoder>` for details.

    categories : "auto" or list of shape (n_features,) of array-like, default="auto"
        Categories (unique values) per feature:

        - `"auto"` : Determine categories automatically from the training data.
        - list : `categories[i]` holds the categories expected in the i-th column. The
          passed categories should not mix strings and numeric values within a single
          feature, and should be sorted in case of numeric values.

        The used categories are stored in the `categories_` fitted attribute.

    target_type : {"auto", "continuous", "binary", "multiclass"}, default="auto"
        Type of target.

        - `"auto"` : Type of target is inferred with
          :func:`~sklearn.utils.multiclass.type_of_target`.
        - `"continuous"` : Continuous target
        - `"binary"` : Binary target
        - `"multiclass"` : Multiclass target

        .. note::
            The type of target inferred with `"auto"` may not be the desired target
            type used for modeling. For example, if the target consisted of integers
            between 0 and 100, then :func:`~sklearn.utils.multiclass.type_of_target`
            will infer the target as `"multiclass"`. In this case, setting
            `target_type="continuous"` will specify the target as a regression
            problem. The `target_type_` attribute gives the target type used by the
            encoder.

           Added the option 'multiclass'.

    smooth : "auto" or float, default="auto"
        The amount of mixing of the target mean conditioned on the value of the
        category with the global target mean. A larger `smooth` value will put
        more weight on the global target mean.
        If `"auto"`, then `smooth` is set to an empirical Bayes estimate.

    cv : int, default=5
        Determines the number of folds in the :term:`cross fitting` strategy used in
        :meth:`fit_transform`. For classification targets, `StratifiedKFold` is used
        and for continuous targets, `KFold` is used.

        Whether to shuffle the data in :meth:`fit_transform` before splitting into
        folds. Note that the samples within each split will not be shuffled.

    encodings_ : list of shape (n_features,) or (n_features * n_classes) of \
                    ndarray
        Encodings learnt on all of `X`.
        For feature `i`, `encodings_[i]` are the encodings matching the
        categories listed in `categories_[i]`. When `target_type_` is
        "multiclass", the encoding for feature `i` and class `j` is stored in
        `encodings_[j + (i * len(classes_))]`. E.g., for 2 features (f) and
        3 classes (c), encodings are ordered:
        f0_c0, f0_c1, f0_c2, f1_c0, f1_c1, f1_c2,

    categories_ : list of shape (n_features,) of ndarray
        The categories of each input feature determined during fitting or
        specified in `categories`
        (in order of the features in `X` and corresponding with the output
        of :meth:`transform`).

    target_type_ : str

    target_mean_ : float
        The overall mean of the target. This value is only used in :meth:`transform`
        to encode categories.

    classes_ : ndarray or None
        If `target_type_` is 'binary' or 'multiclass', holds the label for each class,
        otherwise `None`.

    See Also
    --------
    OrdinalEncoder : Performs an ordinal (integer) encoding of the categorical features.
        Contrary to TargetEncoder, this encoding is not supervised. Treating the
        resulting encoding as a numerical features therefore lead arbitrarily
        ordered values and therefore typically lead to lower predictive performance
        when used as preprocessing for a classifier or regressor.
    OneHotEncoder : Performs a one-hot encoding of categorical features. This
        unsupervised encoding is better suited for low cardinality categorical
        variables as it generate one new feature per unique category.

    .. [MIC] :doi:`Micci-Barreca, Daniele. "A preprocessing scheme for high-cardinality
       categorical attributes in classification and prediction problems"
       SIGKDD Explor. Newsl. 3, 1 (July 2001), 27–32. <10.1145/507533.507538>`

    Examples
    --------
    With `smooth="auto"`, the smoothing parameter is set to an empirical Bayes estimate:

    >>> from sklearn.preprocessing import TargetEncoder
    >>> X = np.array([["dog"] * 20 + ["cat"] * 30 + ["snake"] * 38], dtype=object).T
    >>> y = [90.3] * 5 + [80.1] * 15 + [20.4] * 5 + [20.1] * 25 + [21.2] * 8 + [49] * 30
    >>> enc_auto = TargetEncoder(smooth="auto")
    >>> X_trans = enc_auto.fit_transform(X, y)

    >>> # A high `smooth` parameter puts more weight on global mean on the categorical
    >>> # encodings:
    >>> enc_high_smooth = TargetEncoder(smooth=5000.0).fit(X, y)
    >>> enc_high_smooth.target_mean_
    np.float64(44...)
    >>> enc_high_smooth.encodings_
    [array([44..., 44..., 44...])]

    >>> # On the other hand, a low `smooth` parameter puts more weight on target
    >>> # conditioned on the value of the categorical:
    >>> enc_low_smooth = TargetEncoder(smooth=1.0).fit(X, y)
    >>> enc_low_smooth.encodings_
    [array([20..., 80..., 43...])]
    """

        "categories": [StrOptions({"auto"}), list],
        "target_type": [StrOptions({"auto", "continuous", "binary", "multiclass"})],
        "smooth": [StrOptions({"auto"}), Interval(Real, 0, None, closed="left")],
        "cv": [Interval(Integral, 2, None, closed="left")],
    }

        self,
        categories="auto",
        target_type="auto",
        smooth="auto",
        cv=5,
    ):
        self.categories = categories
        self.smooth = smooth
        self.target_type = target_type

        """Fit the :class:`TargetEncoder` to X and y.

            The data to determine the categories of each feature.

            The target data used to encode the categories.

        Returns
        -------
            Fitted encoder.
        """
        self._fit_encodings_all(X, y)

    def fit_transform(self, X, y):
        """Fit :class:`TargetEncoder` and transform X with the target encoding.

        .. note::
            `fit(X, y).transform(X)` does not equal `fit_transform(X, y)` because a
            :term:`cross fitting` scheme is used in `fit_transform` for encoding.
            See the :ref:`User Guide <target_encoder>`. for details.

        Returns
        -------
        X_trans : ndarray of shape (n_samples, n_features) or \
                    (n_samples, (n_features * n_classes))
        """
        from ..model_selection import KFold, StratifiedKFold  # avoid circular import

        X_ordinal, X_known_mask, y_encoded, n_categories = self._fit_encodings_all(X, y)

        # The cv splitter is voluntarily restricted to *KFold to enforce non
        # overlapping validation folds, otherwise the fit_transform output will
        # not be well-specified.
        if self.target_type_ == "continuous":
            cv = KFold(self.cv, shuffle=self.shuffle, random_state=self.random_state)
        else:
            cv = StratifiedKFold(
                self.cv, shuffle=self.shuffle, random_state=self.random_state
            )

        # If 'multiclass' multiply axis=1 by num classes else keep shape the same
        if self.target_type_ == "multiclass":
            X_out = np.empty(
                (X_ordinal.shape[0], X_ordinal.shape[1] * len(self.classes_)),
            )
        else:
            X_out = np.empty_like(X_ordinal, dtype=np.float64)

        for train_idx, test_idx in cv.split(X, y):
            X_train, y_train = X_ordinal[train_idx, :], y_encoded[train_idx]
            y_train_mean = np.mean(y_train, axis=0)

            if self.target_type_ == "multiclass":
                encodings = self._fit_encoding_multiclass(
                    X_train,
                    y_train,
                    n_categories,
                    y_train_mean,
                )
            else:
                encodings = self._fit_encoding_binary_or_continuous(
                    X_train,
                    y_train,
                )
            self._transform_X_ordinal(
                X_out,
                X_ordinal,
                ~X_known_mask,
                test_idx,
                encodings,
                y_train_mean,
            )
        return X_out

        """Transform X with the target encoding.

        .. note::

        Returns
        -------
        """
        X_ordinal, X_known_mask = self._transform(
            X, handle_unknown="ignore", ensure_all_finite="allow-nan"
        )

        # If 'multiclass' multiply axis=1 by num of classes else keep shape the same
            )
        else:

        self._transform_X_ordinal(
            X_out,
            X_ordinal,
            ~X_known_mask,
            slice(None),
            self.encodings_,
            self.target_mean_,
        )

    def _fit_encodings_all(self, X, y):
        """Fit a target encoding with all the data."""
        from ..preprocessing import (
            LabelBinarizer,
            LabelEncoder,
        )

        self._fit(X, handle_unknown="ignore", ensure_all_finite="allow-nan")

        if self.target_type == "auto":
            accepted_target_types = ("binary", "multiclass", "continuous")
            inferred_type_of_target = type_of_target(y, input_name="y")
            if inferred_type_of_target not in accepted_target_types:
                    "Unknown label type: Target type was inferred to be "
                    f"{inferred_type_of_target!r}. Only {accepted_target_types} are "
                )
            self.target_type_ = inferred_type_of_target
        else:
            self.target_type_ = self.target_type

        self.classes_ = None
        if self.target_type_ == "binary":
            label_encoder = LabelEncoder()
            y = label_encoder.fit_transform(y)
            self.classes_ = label_encoder.classes_
        elif self.target_type_ == "multiclass":
            label_binarizer = LabelBinarizer()
            y = label_binarizer.fit_transform(y)
            self.classes_ = label_binarizer.classes_
        else:  # continuous
            y = _check_y(y, y_numeric=True, estimator=self)

        self.target_mean_ = np.mean(y, axis=0)

        )
        n_categories = np.fromiter(
            (len(category_for_feature) for category_for_feature in self.categories_),
            dtype=np.int64,
            count=len(self.categories_),
        )
            encodings = self._fit_encoding_multiclass(
                y,
                n_categories,
                self.target_mean_,
            )
        else:
            encodings = self._fit_encoding_binary_or_continuous(
                y,
            )
        self.encodings_ = encodings

        return X_ordinal, X_known_mask, y, n_categories

    def _fit_encoding_binary_or_continuous(
        self, X_ordinal, y, n_categories, target_mean
    ):
        """Learn target encodings."""
        if self.smooth == "auto":
            y_variance = np.var(y)
            encodings = _fit_encoding_fast_auto_smooth(
                y,
                target_mean,
                y_variance,
            )
        else:
            encodings = _fit_encoding_fast(
                y,
                self.smooth,
            )
        return encodings

    def _fit_encoding_multiclass(self, X_ordinal, y, n_categories, target_mean):
        """Learn multiclass encodings.

        Learn encodings for each class (c) then reorder encodings such that
        the same features (f) are grouped together. `reorder_index` enables
        converting from:
        f0_c0, f1_c0, f0_c1, f1_c1, f0_c2, f1_c2
        to:
        f0_c0, f0_c1, f0_c2, f1_c0, f1_c1, f1_c2
        """
        n_features = self.n_features_in_

        encodings = []
            y_class = y[:, i]
            encoding = self._fit_encoding_binary_or_continuous(
                y_class,
                target_mean[i],
            )
            encodings.extend(encoding)

        reorder_index = (
            idx
            for start in range(n_features)
            for idx in range(start, (n_classes * n_features), n_features)
        )
        return [encodings[idx] for idx in reorder_index]

    def _transform_X_ordinal(
        self,
        X_out,
        X_ordinal,
        X_unknown_mask,
        row_indices,
        encodings,
        target_mean,
    ):
        """Transform X_ordinal using encodings.

        In the multiclass case, `X_ordinal` and `X_unknown_mask` have column
        (axis=1) size `n_features`, while `encodings` has length of size
        `n_features * n_classes`. `feat_idx` deals with this by repeating
        feature indices by `n_classes` E.g., for 3 features, 2 classes:
        0,0,1,1,2,2

        Additionally, `target_mean` is of shape (`n_classes`,) so `mean_idx`
        cycles through 0 to `n_classes` - 1, `n_features` times.
        """
            for e_idx, encoding in enumerate(encodings):
                # Repeat feature indices by n_classes
                feat_idx = e_idx // n_classes
                # Cycle through each class
                mean_idx = e_idx % n_classes
                X_out[row_indices, e_idx] = encoding[X_ordinal[row_indices, feat_idx]]
                X_out[X_unknown_mask[:, feat_idx], e_idx] = target_mean[mean_idx]
        else:
                X_out[row_indices, e_idx] = encoding[X_ordinal[row_indices, e_idx]]
                X_out[X_unknown_mask[:, e_idx], e_idx] = target_mean

        Returns
        -------
            Transformed feature names. `feature_names_in_` is used unless it is
            not defined, in which case the following input feature names are
            generated: `["x0", "x1", ..., "x(n_features_in_ - 1)"]`.
            When `type_of_target_` is "multiclass" the names are of the format
            '<feature_name>_<class_name>'.
        """
        feature_names = _check_feature_names_in(self, input_features)
                f"{feature_name}_{class_name}"
                for feature_name in feature_names
                for class_name in self.classes_
            ]
            return np.asarray(feature_names, dtype=object)
        else:
            return feature_names

        self,
        cv=5,
    ):

        Returns
        -------
        """

        .. note::

        Returns
        -------
        """

        else:
            )

            )
        else:

                    X_train,
                    y_train,
                )
            else:
                    X_train,
                    y_train,
                )
                X_out,
                test_idx,
            )

        .. note::

        Returns
        -------
        """
        )

            )
        else:

            X_out,
        )

        )

                )
        else:

        )
        )
                y,
            )
        else:
                y,
            )

    ):
                y,
            )
        else:
                y,
            )

        to:
        """

                y_class,
            )

            idx
        )

        self,
        X_out,
    ):

        """
        else:

        Returns
        -------
        """
            ]
        else:

class LabelEncoder(TransformerMixin, BaseEstimator, auto_wrap_output_keys=None):
    """Encode target labels with value between 0 and n_classes-1.

    This transformer should be used to encode target values, *i.e.* `y`, and
    not the input `X`.

    Read more in the :ref:`User Guide <preprocessing_targets>`.

    .. versionadded:: 0.12

        Holds the label for each class.

    See Also
    --------
    OrdinalEncoder : Encode categorical features using an ordinal encoding
        scheme.

    Examples
    --------
    `LabelEncoder` can be used to normalize labels.

    >>> from sklearn.preprocessing import LabelEncoder
    >>> le = LabelEncoder()
    >>> le.fit([1, 2, 2, 6])
    LabelEncoder()
    >>> le.classes_
    array([1, 2, 6])
    >>> le.transform([1, 1, 2, 6])
    array([0, 0, 1, 2]...)
    >>> le.inverse_transform([0, 0, 1, 2])
    array([1, 1, 2, 6])

    It can also be used to transform non-numerical labels (as long as they are
    hashable and comparable) to numerical labels.

    >>> le.fit(["paris", "paris", "tokyo", "amsterdam"])
    >>> list(le.classes_)
    [np.str_('amsterdam'), np.str_('paris'), np.str_('tokyo')]
    >>> le.transform(["tokyo", "tokyo", "paris"])
    array([2, 2, 1]...)
    >>> list(le.inverse_transform([2, 2, 1]))
    [np.str_('tokyo'), np.str_('tokyo'), np.str_('paris')]
    """

    def fit(self, y):
        """Fit label encoder.

        Returns
        -------
            Fitted label encoder.
        """
        self.classes_ = _unique(y)

    def fit_transform(self, y):
        """Fit label encoder and return encoded labels.

        Returns
        -------
            Encoded labels.
        """
        self.classes_, y = _unique(y, return_inverse=True)
        return y

    def transform(self, y):
        """Transform labels to normalized encoding.

        Returns
        -------
            Labels as normalized encodings.
        """
        y = column_or_1d(y, dtype=self.classes_.dtype, warn=True)
        # transform of empty array is empty array
        if _num_samples(y) == 0:
            return xp.asarray([])

        return _encode(y, uniques=self.classes_)

    def inverse_transform(self, y):
        """Transform labels back to original encoding.

        Returns
        -------
            Original encoding.
        """
        # inverse transform of empty array is empty array

        diff = xpx.setdiff1d(
            y,
            xp.arange(self.classes_.shape[0], device=device(y)),
            xp=xp,
        )
        if diff.shape[0]:
            raise ValueError("y contains previously unseen labels: %s" % str(diff))
        return xp.take(self.classes_, y, axis=0)

class LabelBinarizer(TransformerMixin, BaseEstimator, auto_wrap_output_keys=None):
    """Binarize labels in a one-vs-all fashion.

    Several regression and binary classification algorithms are
    available in scikit-learn. A simple way to extend these algorithms
    to the multi-class classification case is to use the so-called
    one-vs-all scheme.

    At learning time, this simply consists in learning one regressor
    or binary classifier per class. In doing so, one needs to convert
    multi-class labels to binary labels (belong or does not belong
    to the class). `LabelBinarizer` makes this process easy with the
    transform method.

    At prediction time, one assigns the class for which the corresponding
    model gave the greatest confidence. `LabelBinarizer` makes this easy
    with the :meth:`inverse_transform` method.

    neg_label : int, default=0
        Value with which negative labels must be encoded.

    pos_label : int, default=1
        Value with which positive labels must be encoded.

        True if the returned array from transform is desired to be in sparse

    y_type_ : str
        Represents the type of the target data as evaluated by
        :func:`~sklearn.utils.multiclass.type_of_target`. Possible type are
        'continuous', 'continuous-multioutput', 'binary', 'multiclass',
        'multiclass-multioutput', 'multilabel-indicator', and 'unknown'.

    sparse_input_ : bool
        `True` if the input data to transform is given as a sparse matrix,
         `False` otherwise.

    See Also
    --------
    label_binarize : Function to perform the transform operation of
        LabelBinarizer with fixed classes.
    OneHotEncoder : Encode categorical features using a one-hot aka one-of-K
        scheme.

    Examples
    --------
    >>> from sklearn.preprocessing import LabelBinarizer
    >>> lb = LabelBinarizer()
    >>> lb.fit([1, 2, 6, 4, 2])
    LabelBinarizer()
    >>> lb.classes_
    array([1, 2, 4, 6])
    >>> lb.transform([1, 6])
    array([[1, 0, 0, 0],
           [0, 0, 0, 1]])

    Binary targets transform to a column vector

    >>> lb.fit_transform(['yes', 'no', 'no', 'yes'])
    array([[1],
           [0],
           [0],
           [1]])

    Passing a 2D matrix for multilabel classification

    >>> lb.fit(np.array([[0, 1, 1], [1, 0, 0]]))
    >>> lb.transform([0, 1, 2, 1])
    array([[1, 0, 0],
           [0, 1, 0],
           [0, 0, 1],
           [0, 1, 0]])
    """

        "neg_label": [Integral],
        "pos_label": [Integral],
    }

    def __init__(self, *, neg_label=0, pos_label=1, sparse_output=False):
        self.neg_label = neg_label

        """Fit label binarizer.

        y : ndarray of shape (n_samples,) or (n_samples, n_classes)
            Target values. The 2-d matrix should only contain 0 and 1,
            represents multilabel classification.

        Returns
        -------
        """
        if self.neg_label >= self.pos_label:
                f"neg_label={self.neg_label} must be strictly less than "
                f"pos_label={self.pos_label}."
            )

        if self.sparse_output and (self.pos_label == 0 or self.neg_label != 0):
                "Sparse binarization is only supported with non "
                "zero pos_label and zero neg_label, got "
                f"pos_label={self.pos_label} and neg_label={self.neg_label}"
            )

        self.y_type_ = type_of_target(y, input_name="y")

        if "multioutput" in self.y_type_:
                "Multioutput target data is not supported with label binarization"
            )
            raise ValueError("y has 0 samples: %r" % y)

        self.sparse_input_ = sp.issparse(y)

        """Fit label binarizer/transform multi-class labels to binary labels.

        The output of transform is sometimes referred to as
        the 1-of-K coding scheme.

        y : {ndarray, sparse matrix} of shape (n_samples,) or \
                (n_samples, n_classes)
            represents multilabel classification. Sparse matrix can be
            CSR, CSC, COO, DOK, or LIL.

        Returns
        -------
        Y : {ndarray, sparse matrix} of shape (n_samples, n_classes)
            Shape will be (n_samples, 1) for binary problems. Sparse matrix
            will be of CSR format.
        """
        return self.fit(y).transform(y)

        """Transform multi-class labels to binary labels.

        The output of transform is sometimes referred to by some authors as

        y : {array, sparse matrix} of shape (n_samples,) or \

        Returns
        -------
        """

        y_is_multilabel = type_of_target(y).startswith("multilabel")
        if y_is_multilabel and not self.y_type_.startswith("multilabel"):
            raise ValueError("The object was not fitted with multilabel input.")

        return label_binarize(
            y,
            classes=self.classes_,
            neg_label=self.neg_label,
            sparse_output=self.sparse_output,
        )

    def inverse_transform(self, Y, threshold=None):
        """Transform binary labels back to multi-class labels.

            Target values. All sparse matrices are converted to CSR before
            inverse transformation.

        threshold : float, default=None
            Threshold used in the binary and multi-label cases.

            Use 0 when ``Y`` contains the output of :term:`decision_function`
            (classifier).
            Use 0.5 when ``Y`` contains the output of :term:`predict_proba`.

            If None, the threshold is assumed to be half way between
            neg_label and pos_label.

        Returns
        -------
        y : {ndarray, sparse matrix} of shape (n_samples,)
            Target values. Sparse matrix will be of CSR format.

        Notes
        -----
        In the case when the binary labels are fractional
        (probabilistic), :meth:`inverse_transform` chooses the class with the
        greatest value. Typically, this allows to use the output of a
        linear model's :term:`decision_function` method directly as the input
        of :meth:`inverse_transform`.
        """

        if threshold is None:
            threshold = (self.pos_label + self.neg_label) / 2.0

        if self.y_type_ == "multiclass":
            y_inv = _inverse_binarize_multiclass(Y, self.classes_)
        else:
            y_inv = _inverse_binarize_thresholding(
                Y, self.y_type_, self.classes_, threshold
            )

        if self.sparse_input_:
            y_inv = sp.csr_matrix(y_inv)
        elif sp.issparse(y_inv):
            y_inv = y_inv.toarray()

        return y_inv

def label_binarize(y, *, classes, neg_label=0, pos_label=1, sparse_output=False):

    This function makes it possible to compute this transformation for a
    fixed set of class labels known ahead of time.

    y : array-like or sparse matrix
        Sequence of integer labels or multilabel data to encode.

        Uniquely holds the label for each class.

    sparse_output : bool, default=False,
        Set to true if output binary array is desired in CSR sparse format.

    Returns
    -------
        Shape will be (n_samples, 1) for binary problems. Sparse matrix will
        be of CSR format.

    See Also
    --------
    LabelBinarizer : Class used to wrap the functionality of label_binarize and
        allow for fitting to classes independently of the transform operation.

    Examples
    --------
    >>> from sklearn.preprocessing import label_binarize
    >>> label_binarize([1, 6], classes=[1, 2, 4, 6])

    The class ordering is preserved:

    >>> label_binarize([1, 6], classes=[1, 6, 4, 2])
           [0, 1, 0, 0]])

    >>> label_binarize(['yes', 'no', 'no', 'yes'], classes=['no', 'yes'])
           [0],
           [0],
           [1]])
    """
    if not isinstance(y, list):
        # XXX Workaround that will be removed when list of list format is
        # dropped
            y, input_name="y", accept_sparse="csr", ensure_2d=False, dtype=None
        )
    else:
    if neg_label >= pos_label:
            "neg_label={0} must be strictly less than pos_label={1}.".format(
                neg_label, pos_label
            )
        )

    if sparse_output and (pos_label == 0 or neg_label != 0):
            "Sparse binarization is only supported with non "
            "zero pos_label and zero neg_label, got "
            "pos_label={0} and neg_label={1}"
            "".format(pos_label, neg_label)
        )

    # To account for pos_label == 0 in the dense case
    pos_switch = pos_label == 0
    if pos_switch:
        pos_label = -neg_label

    if "multioutput" in y_type:
            "Multioutput target data is not supported with label binarization"
        )
    if y_type == "unknown":
        raise ValueError("The type of target data is not known")

    n_samples = y.shape[0] if sp.issparse(y) else len(y)
    n_classes = len(classes)
    classes = np.asarray(classes)

            if sparse_output:
                return sp.csr_matrix((n_samples, 1), dtype=int)
            else:
                Y = np.zeros((len(y), 1), dtype=int)
                Y += neg_label
                return Y
        elif len(classes) >= 3:
            y_type = "multiclass"

    sorted_class = np.sort(classes)
    if y_type == "multilabel-indicator":
        y_n_classes = y.shape[1] if hasattr(y, "shape") else len(y[0])
        if classes.size != y_n_classes:
                "classes {0} mismatch with the labels {1} found in the data".format(
                    classes, unique_labels(y)
                )
            )

    if y_type in ("binary", "multiclass"):

        # pick out the known labels from y
        y_in_classes = np.isin(y, classes)
        y_seen = y[y_in_classes]
        indices = np.searchsorted(sorted_class, y_seen)
        indptr = np.hstack((0, np.cumsum(y_in_classes)))

        data = np.empty_like(indices)
        data.fill(pos_label)
        Y = sp.csr_matrix((data, indices, indptr), shape=(n_samples, n_classes))
    elif y_type == "multilabel-indicator":
        Y = sp.csr_matrix(y)
            data = np.empty_like(Y.data)
            data.fill(pos_label)
            Y.data = data
    else:
            "%s target data is not supported with label binarization" % y_type
        )

    if not sparse_output:
        Y = Y.astype(int, copy=False)

        if neg_label != 0:
            Y[Y == 0] = neg_label

        if pos_switch:
            Y[Y == pos_label] = 0
    else:
        Y.data = Y.data.astype(int, copy=False)

    # preserve label ordering
    if np.any(classes != sorted_class):
        indices = np.searchsorted(sorted_class, classes)
        Y = Y[:, indices]

        if sparse_output:
            Y = Y[:, [-1]]
        else:
            Y = Y[:, -1].reshape((-1, 1))

    return Y

def _inverse_binarize_multiclass(y, classes):
    """Inverse label binarization transformation for multiclass.

    Multiclass uses the maximal score instead of a threshold.
    """

        # Find the argmax for each row in y where y is a CSR matrix

        n_samples, n_outputs = y.shape
        outputs = np.arange(n_outputs)
        row_max = min_max_axis(y, 1)[1]
        row_nnz = np.diff(y.indptr)

        y_data_repeated_max = np.repeat(row_max, row_nnz)
        # picks out all indices obtaining the maximum per row
        y_i_all_argmax = np.flatnonzero(y_data_repeated_max == y.data)

        # For corner case where last row has a max of 0
        if row_max[-1] == 0:
            y_i_all_argmax = np.append(y_i_all_argmax, [len(y.data)])

        # Gets the index of the first argmax in each row from y_i_all_argmax
        index_first_argmax = np.searchsorted(y_i_all_argmax, y.indptr[:-1])
        # first argmax of each row
        y_ind_ext = np.append(y.indices, [0])
        y_i_argmax = y_ind_ext[y_i_all_argmax[index_first_argmax]]
        # Handle rows of all 0
        y_i_argmax[np.where(row_nnz == 0)[0]] = 0

        # Handles rows with max of 0 that contain negative numbers
        samples = np.arange(n_samples)[(row_nnz > 0) & (row_max.ravel() == 0)]
        for i in samples:
            ind = y.indices[y.indptr[i] : y.indptr[i + 1]]
            y_i_argmax[i] = classes[np.setdiff1d(outputs, ind)][0]

        return classes[y_i_argmax]
    else:
        return classes.take(y.argmax(axis=1), mode="clip")

def _inverse_binarize_thresholding(y, output_type, classes, threshold):
    """Inverse label binarization transformation using thresholding."""

    if output_type == "binary" and y.ndim == 2 and y.shape[1] > 2:
        raise ValueError("output_type='binary', but y.shape = {0}".format(y.shape))

    if output_type != "binary" and y.shape[1] != len(classes):
            "The number of class is not equal to the number of dimension of y."
        )

    # Perform thresholding
        if threshold > 0:
            if y.format not in ("csr", "csc"):
                y = y.tocsr()
            y.data = np.array(y.data > threshold, dtype=int)
            y.eliminate_zeros()
        else:
            y = np.array(y.toarray() > threshold, dtype=int)
    else:
        y = np.array(y > threshold, dtype=int)

    # Inverse transform data
    if output_type == "binary":
        if sp.issparse(y):
        if y.ndim == 2 and y.shape[1] == 2:
            return classes[y[:, 1]]
        else:
            if len(classes) == 1:
                return np.repeat(classes[0], len(y))
            else:
                return classes[y.ravel()]

    elif output_type == "multilabel-indicator":
        return y

    else:
        raise ValueError("{0} format is not supported".format(output_type))

class MultiLabelBinarizer(TransformerMixin, BaseEstimator, auto_wrap_output_keys=None):
    """Transform between iterable of iterables and a multilabel format.

    Although a list of sets or tuples is a very intuitive format for multilabel
    data, it is unwieldy to process. This transformer converts between this
    intuitive format and the supported multilabel format: a (samples x classes)
    binary matrix indicating the presence of a class label.

    classes : array-like of shape (n_classes,), default=None
        Indicates an ordering for the class labels.
        All entries should be unique (cannot contain duplicate classes).

        Set to True if output binary array is desired in CSR sparse format.

        A copy of the `classes` parameter when provided.
        Otherwise it corresponds to the sorted set of classes found
        when fitting.

    See Also
    --------
        scheme.

    Examples
    --------
    >>> from sklearn.preprocessing import MultiLabelBinarizer
    >>> mlb = MultiLabelBinarizer()
    >>> mlb.fit_transform([(1, 2), (3,)])
           [0, 0, 1]])
    >>> mlb.classes_

    >>> mlb.fit_transform([{'sci-fi', 'thriller'}, {'comedy'}])
    array([[0, 1, 1],
           [1, 0, 0]])
    >>> list(mlb.classes_)
    ['comedy', 'sci-fi', 'thriller']

    A common mistake is to pass in a list, which leads to the following issue:

    >>> mlb.fit(['sci-fi', 'thriller', 'comedy'])
    MultiLabelBinarizer()
    array(['-', 'c', 'd', 'e', 'f', 'h', 'i', 'l', 'm', 'o', 'r', 's', 't',
        'y'], dtype=object)

    To correct this, the list of labels should be passed in as:

    >>> mlb.fit([['sci-fi', 'thriller', 'comedy']])
    array(['comedy', 'sci-fi', 'thriller'], dtype=object)
    """

        "classes": ["array-like", None],
    }

    def __init__(self, *, classes=None, sparse_output=False):

        """Fit the label sets binarizer, storing :term:`classes_`.

        y : iterable of iterables
            A set of labels (any orderable and hashable object) for each
            sample. If the `classes` parameter is set, `y` will not be
            iterated.

        Returns
        -------
        """
        self._cached_dict = None

        if self.classes is None:
            classes = sorted(set(itertools.chain.from_iterable(y)))
        elif len(set(self.classes)) < len(self.classes):
                "The classes argument contains duplicate "
                "classes. Remove these duplicates before passing "
                "them to MultiLabelBinarizer."
            )
        else:
            classes = self.classes
        dtype = int if all(isinstance(c, int) for c in classes) else object
        self.classes_ = np.empty(len(classes), dtype=dtype)
        self.classes_[:] = classes

        """Fit the label sets binarizer and transform the given label sets.

            iterated.

        Returns
        -------
        y_indicator : {ndarray, sparse matrix} of shape (n_samples, n_classes)
            A matrix such that `y_indicator[i, j] = 1` iff `classes_[j]`
            is in `y[i]`, and 0 otherwise. Sparse matrix will be of CSR
            format.
        """
        if self.classes is not None:
            return self.fit(y).transform(y)

        # Automatically increment on new class
        class_mapping = defaultdict(int)
        class_mapping.default_factory = class_mapping.__len__
        yt = self._transform(y, class_mapping)

        # sort classes and reorder columns
        tmp = sorted(class_mapping, key=class_mapping.get)

        # (make safe for tuples)
        dtype = int if all(isinstance(c, int) for c in tmp) else object
        class_mapping = np.empty(len(tmp), dtype=dtype)
        class_mapping[:] = tmp
        self.classes_, inverse = np.unique(class_mapping, return_inverse=True)
        # ensure yt.indices keeps its current dtype
        yt.indices = np.asarray(inverse[yt.indices], dtype=yt.indices.dtype)

        if not self.sparse_output:
            yt = yt.toarray()

        return yt

        """Transform the given label sets.

            iterated.

        Returns
        -------
        y_indicator : array or CSR matrix, shape (n_samples, n_classes)
            A matrix such that `y_indicator[i, j] = 1` iff `classes_[j]` is in
            `y[i]`, and 0 otherwise.
        """

        class_to_index = self._build_cache()
        yt = self._transform(y, class_to_index)

        return yt

    def _build_cache(self):
        if self._cached_dict is None:
            self._cached_dict = dict(zip(self.classes_, range(len(self.classes_))))

        return self._cached_dict

    def _transform(self, y, class_mapping):
        """Transforms the label sets with a given mapping.

            iterated.

        class_mapping : Mapping
            Maps from label to column index in label indicator matrix.

        Returns
        -------
        y_indicator : sparse matrix of shape (n_samples, n_classes)
            Label indicator matrix. Will be of CSR format.
        """
        indices = array.array("i")
        indptr = array.array("i", [0])
        unknown = set()
        for labels in y:
            index = set()
            for label in labels:
                try:
                    index.add(class_mapping[label])
                    unknown.add(label)
            indices.extend(index)
        if unknown:
                "unknown class(es) {0} will be ignored".format(sorted(unknown, key=str))
            )
        data = np.ones(len(indices), dtype=int)

        return sp.csr_matrix(
            (data, indices, indptr), shape=(len(indptr) - 1, len(class_mapping))
        )

    def inverse_transform(self, yt):
        """Transform the given indicator matrix into label sets.

        yt : {ndarray, sparse matrix} of shape (n_samples, n_classes)
            A matrix containing only 1s ands 0s.

        Returns
        -------
        y : list of tuples
            The set of labels for each sample such that `y[i]` consists of
            `classes_[j]` for each `yt[i, j] == 1`.
        """

        if yt.shape[1] != len(self.classes_):
                "Expected indicator for {0} classes, but got {1}".format(
                    len(self.classes_), yt.shape[1]
                )
            )

        if sp.issparse(yt):
            yt = yt.tocsr()
            if len(yt.data) != 0 and len(np.setdiff1d(yt.data, [0, 1])) > 0:
                raise ValueError("Expected only 0s and 1s in label indicator.")
            return [
                tuple(self.classes_.take(yt.indices[start:end]))
                for start, end in zip(yt.indptr[:-1], yt.indptr[1:])
            ]
        else:
            unexpected = np.setdiff1d(yt, [0, 1])
            if len(unexpected) > 0:
                    "Expected only 0s and 1s in label indicator. Also got {0}".format(
                        unexpected
                    )
                )
            return [tuple(self.classes_.compress(indicators)) for indicators in yt]

        tags.target_tags.two_d_labels = True

        Returns
        -------
        """

        Returns
        -------
        """
        return y

        Returns
        -------
        """

        Returns
        -------
        """

            y,
            xp=xp,
        )

        Returns
        -------
        """
            )

            )

            )

        Returns
        -------
        """

        Returns
        -------
        """

            y,
        )

        Returns
        -------

        Notes
        -----
        """

        else:
            )

            iterated.

        Returns
        -------
        """

            )
        else:

            iterated.

        Returns
        -------
            format.
        """

        return yt

            iterated.

        Returns
        -------
        """

        return yt

            iterated.

        Returns
        -------
        """
                try:
            )

        )

        Returns
        -------
        """

                )
            )

            return [
            ]
        else:
                    )
                )

class _BaseEncoder(TransformerMixin, BaseEstimator):
    """
    Base class for encoders that includes the code to categorize and
    transform the input features.

    """

    def _check_X(self, X, ensure_all_finite=True):
        """
        Perform custom check_array:
        - convert list of strings to object dtype
        - check for missing values for object dtype data (check_array does
          not do that)
        - return list of features (arrays): this list of features is
          constructed feature by feature to preserve the data types
          of pandas DataFrame columns, as otherwise information is lost
          and cannot be used, e.g. for the `categories_` attribute.

        """
        if not (hasattr(X, "iloc") and getattr(X, "ndim", 0) == 2):
            # if not a dataframe, do normal check_array validation
            X_temp = check_array(X, dtype=None, ensure_all_finite=ensure_all_finite)
            if not hasattr(X, "dtype") and np.issubdtype(X_temp.dtype, np.str_):
                X = check_array(X, dtype=object, ensure_all_finite=ensure_all_finite)
            else:
                X = X_temp
            needs_validation = False
        else:
            # pandas dataframe, do validation later column by column, in order
            # to keep the dtype information to be used in the encoder.
            needs_validation = ensure_all_finite

        X_columns = []

            Xi = _safe_indexing(X, indices=i, axis=1)
            Xi = check_array(
                Xi, ensure_2d=False, dtype=None, ensure_all_finite=needs_validation
            )
            X_columns.append(Xi)

        return X_columns, n_samples, n_features

    def _fit(
        self,
        X,
        handle_unknown="error",
        ensure_all_finite=True,
        return_and_ignore_missing_for_infrequent=False,
    ):
        self._check_infrequent_enabled()
        X_list, n_samples, n_features = self._check_X(
            X, ensure_all_finite=ensure_all_finite
        )
        self.n_features_in_ = n_features

        if self.categories != "auto":
            if len(self.categories) != n_features:
                    "Shape mismatch: if categories is an array,"
                    " it has to be of shape (n_features,)."
                )

        self.categories_ = []
        category_counts = []
        compute_counts = return_counts or self._infrequent_enabled

            Xi = X_list[i]

            if self.categories == "auto":
                result = _unique(Xi, return_counts=compute_counts)
                if compute_counts:
                    cats, counts = result
                    category_counts.append(counts)
                else:
                    cats = result
            else:
                if np.issubdtype(Xi.dtype, np.str_):
                    # Always convert string categories to objects to avoid
                    # unexpected string truncation for longer category labels
                    # passed in the constructor.
                    Xi_dtype = object
                else:
                    Xi_dtype = Xi.dtype

                cats = np.array(self.categories[i], dtype=Xi_dtype)
                if (
                    cats.dtype == object
                    and isinstance(cats[0], bytes)
                    and Xi.dtype.kind != "S"
                ):
                    msg = (
                        f"In column {i}, the predefined categories have type 'bytes'"
                        " which is incompatible with values of type"
                        f" '{type(Xi[0]).__name__}'."
                    )
                    raise ValueError(msg)

                # `nan` must be the last stated category
                for category in cats[:-1]:
                    if is_scalar_nan(category):
                            "Nan should be the last element in user"
                            f" provided categories, see categories {cats}"
                            f" in column #{i}"
                        )

                if cats.size != len(_unique(cats)):
                    msg = (
                        f"In column {i}, the predefined categories"
                        " contain duplicate elements."
                    )

                if Xi.dtype.kind not in "OUS":
                    sorted_cats = np.sort(cats)
                    error_msg = (
                        "Unsorted categories are not supported for numerical categories"
                    )
                    # if there are nans, nan should be the last element
                    stop_idx = -1 if np.isnan(sorted_cats[-1]) else None
                    if np.any(sorted_cats[:stop_idx] != cats[:stop_idx]):
                        raise ValueError(error_msg)

                if handle_unknown == "error":
                    diff = _check_unknown(Xi, cats)
                    if diff:
                        msg = (
                            "Found unknown categories {0} in column {1}"
                            " during fit".format(diff, i)
                        )
                    category_counts.append(_get_counts(Xi, cats))

            self.categories_.append(cats)

        output = {"n_samples": n_samples}
            output["category_counts"] = category_counts

        missing_indices = {}
        if return_and_ignore_missing_for_infrequent:
            for feature_idx, categories_for_idx in enumerate(self.categories_):
                if is_scalar_nan(categories_for_idx[-1]):
                    # `nan` values can only be placed in the latest position
                    missing_indices[feature_idx] = categories_for_idx.size - 1
            output["missing_indices"] = missing_indices

        if self._infrequent_enabled:
            self._fit_infrequent_category_mapping(
                category_counts,
                missing_indices,
            )

    def _transform(
        self,
        X,
        warn_on_unknown=False,
        ignore_category_indices=None,
    ):
        )
        _check_feature_names(self, X, reset=False)

        X_int = np.zeros((n_samples, n_features), dtype=int)
        X_mask = np.ones((n_samples, n_features), dtype=bool)

        columns_with_unknown = []
            diff, valid_mask = _check_unknown(Xi, self.categories_[i], return_mask=True)

            if not np.all(valid_mask):
                    msg = (
                        "Found unknown categories {0} in column {1}"
                        " during transform".format(diff, i)
                    )
                else:
                    if warn_on_unknown:
                        columns_with_unknown.append(i)
                    # Set the problematic rows to an acceptable value and
                    # continue `The rows are marked `X_mask` and will be
                    # removed later.
                    X_mask[:, i] = valid_mask
                    # cast Xi into the largest string type necessary
                    # to handle different lengths of numpy strings
                    if (
                        self.categories_[i].dtype.kind in ("U", "S")
                        and self.categories_[i].itemsize > Xi.itemsize
                    ):
                        Xi = Xi.astype(self.categories_[i].dtype)
                    elif self.categories_[i].dtype.kind == "O" and Xi.dtype.kind == "U":
                        # categories are objects and Xi are numpy strings.
                        # Cast Xi to an object dtype to prevent truncation
                        # when setting invalid values.
                        Xi = Xi.astype("O")
                    else:
                        Xi = Xi.copy()

                    Xi[~valid_mask] = self.categories_[i][0]
            # We use check_unknown=False, since _check_unknown was
            # already called above.
            X_int[:, i] = _encode(Xi, uniques=self.categories_[i], check_unknown=False)
        if columns_with_unknown:
                (
                    "Found unknown categories in columns "
                    f"{columns_with_unknown} during transform. These "
                    "unknown categories will be encoded as all zeros"
                ),
            )

        self._map_infrequent_categories(X_int, X_mask, ignore_category_indices)
        return X_int, X_mask

    @property
    def infrequent_categories_(self):
        """Infrequent categories for each feature."""
        # raises an AttributeError if `_infrequent_indices` is not defined
        infrequent_indices = self._infrequent_indices
        return [
            None if indices is None else category[indices]
            for category, indices in zip(self.categories_, infrequent_indices)
        ]

    def _check_infrequent_enabled(self):
        """
        This functions checks whether _infrequent_enabled is True or False.
        This has to be called after parameter validation in the fit function.
        """
        max_categories = getattr(self, "max_categories", None)
        min_frequency = getattr(self, "min_frequency", None)
        self._infrequent_enabled = (
            max_categories is not None and max_categories >= 1
        ) or min_frequency is not None

    def _identify_infrequent(self, category_count, n_samples, col_idx):
        """Compute the infrequent indices.

        category_count : ndarray of shape (n_cardinality,)
            Category counts.

            Number of samples.

        col_idx : int
            Index of the current category. Only used for the error message.

        Returns
        -------
        output : ndarray of shape (n_infrequent_categories,) or None
            If there are infrequent categories, indices of infrequent
            categories. Otherwise None.
        """
        if isinstance(self.min_frequency, numbers.Integral):
            infrequent_mask = category_count < self.min_frequency
        elif isinstance(self.min_frequency, numbers.Real):
            min_frequency_abs = n_samples * self.min_frequency
            infrequent_mask = category_count < min_frequency_abs
        else:
            infrequent_mask = np.zeros(category_count.shape[0], dtype=bool)

        n_current_features = category_count.size - infrequent_mask.sum() + 1
        if self.max_categories is not None and self.max_categories < n_current_features:
            # max_categories includes the one infrequent category
            frequent_category_count = self.max_categories - 1
            if frequent_category_count == 0:
                # All categories are infrequent
                infrequent_mask[:] = True
            else:
                # stable sort to preserve original count order
                smallest_levels = np.argsort(category_count, kind="mergesort")[
                    :-frequent_category_count
                ]
                infrequent_mask[smallest_levels] = True

        output = np.flatnonzero(infrequent_mask)
        return output if output.size > 0 else None

    def _fit_infrequent_category_mapping(
        self, n_samples, category_counts, missing_indices
    ):
        """Fit infrequent categories.

        Defines the private attribute: `_default_to_infrequent_mappings`. For
        feature `i`, `_default_to_infrequent_mappings[i]` defines the mapping
        from the integer encoding returned by `super().transform()` into
        infrequent categories. If `_default_to_infrequent_mappings[i]` is None,
        there were no infrequent categories in the training set.

        For example if categories 0, 2 and 4 were frequent, while categories
        1, 3, 5 were infrequent for feature 7, then these categories are mapped
        to a single output:
        `_default_to_infrequent_mappings[7] = array([0, 3, 1, 3, 2, 3])`

        Defines private attribute: `_infrequent_indices`. `_infrequent_indices[i]`
        is an array of indices such that
        `categories_[i][_infrequent_indices[i]]` are all the infrequent category
        labels. If the feature `i` has no infrequent categories
        `_infrequent_indices[i]` is None.

            Number of samples in training set.
        category_counts: list of ndarray
            `category_counts[i]` is the category counts corresponding to
            `self.categories_[i]`.
        missing_indices : dict
            Dict mapping from feature_idx to category index with a missing value.
        """
        # Remove missing value from counts, so it is not considered as infrequent
        if missing_indices:
            category_counts_ = []
            for feature_idx, count in enumerate(category_counts):
                if feature_idx in missing_indices:
                    category_counts_.append(
                        np.delete(count, missing_indices[feature_idx])
                    )
                else:
                    category_counts_.append(count)
        else:
            category_counts_ = category_counts

        self._infrequent_indices = [
            self._identify_infrequent(category_count, n_samples, col_idx)
            for col_idx, category_count in enumerate(category_counts_)
        ]

        # compute mapping from default mapping to infrequent mapping
        self._default_to_infrequent_mappings = []

        for feature_idx, infreq_idx in enumerate(self._infrequent_indices):
            cats = self.categories_[feature_idx]
            # no infrequent categories
            if infreq_idx is None:
                self._default_to_infrequent_mappings.append(None)
                continue

            n_cats = len(cats)
            if feature_idx in missing_indices:
                # Missing index was removed from this category when computing
                # infrequent indices, thus we need to decrease the number of
                # total categories when considering the infrequent mapping.
                n_cats -= 1

            # infrequent indices exist
            mapping = np.empty(n_cats, dtype=np.int64)
            n_infrequent_cats = infreq_idx.size

            # infrequent categories are mapped to the last element.
            n_frequent_cats = n_cats - n_infrequent_cats
            mapping[infreq_idx] = n_frequent_cats

            frequent_indices = np.setdiff1d(np.arange(n_cats), infreq_idx)
            mapping[frequent_indices] = np.arange(n_frequent_cats)

            self._default_to_infrequent_mappings.append(mapping)

    def _map_infrequent_categories(self, X_int, X_mask, ignore_category_indices):
        """Map infrequent categories to integer representing the infrequent category.

        This modifies X_int in-place. Values that were invalid based on `X_mask`
        are mapped to the infrequent category if there was an infrequent
        category for that feature.

        X_int: ndarray of shape (n_samples, n_features)
            Integer encoded categories.

        X_mask: ndarray of shape (n_samples, n_features)
            Bool mask for valid values in `X_int`.

        ignore_category_indices : dict
            Dictionary mapping from feature_idx to category index to ignore.
            Ignored indexes will not be grouped and the original ordinal encoding
            will remain.
        """
        if not self._infrequent_enabled:
            return

        ignore_category_indices = ignore_category_indices or {}

        for col_idx in range(X_int.shape[1]):
            infrequent_idx = self._infrequent_indices[col_idx]
            if infrequent_idx is None:
                continue

            X_int[~X_mask[:, col_idx], col_idx] = infrequent_idx[0]
            if self.handle_unknown == "infrequent_if_exist":
                # All the unknown values are now mapped to the
                # infrequent_idx[0], which makes the unknown values valid
                # This is needed in `transform` when the encoding is formed
                # using `X_mask`.
                X_mask[:, col_idx] = True

        # Remaps encoding in `X_int` where the infrequent categories are
        # grouped together.
        for i, mapping in enumerate(self._default_to_infrequent_mappings):
            if mapping is None:
                continue

            if i in ignore_category_indices:
                # Update rows that are **not** ignored
                rows_to_update = X_int[:, i] != ignore_category_indices[i]
            else:
                rows_to_update = slice(None)

            X_int[rows_to_update, i] = np.take(mapping, X_int[rows_to_update, i])

        tags.input_tags.categorical = True

class OneHotEncoder(_BaseEncoder):
    """
    Encode categorical features as a one-hot numeric array.

    The input to this transformer should be an array-like of integers or
    strings, denoting the values taken on by categorical (discrete) features.
    The features are encoded using a one-hot (aka 'one-of-K' or 'dummy')
    encoding scheme. This creates a binary column for each category and
    returns a sparse matrix or dense array (depending on the ``sparse_output``
    parameter).

    By default, the encoder derives the categories based on the unique values
    in each feature. Alternatively, you can also specify the `categories`
    manually.

    This encoding is needed for feeding categorical data to many scikit-learn
    estimators, notably linear models and SVMs with the standard kernels.

    Note: a one-hot encoding of y labels should use a LabelBinarizer
    instead.

    Read more in the :ref:`User Guide <preprocessing_categorical_features>`.
    For a comparison of different encoders, refer to:
    :ref:`sphx_glr_auto_examples_preprocessing_plot_target_encoder.py`.

    categories : 'auto' or a list of array-like, default='auto'

        - 'auto' : Determine categories automatically from the training data.
        - list : ``categories[i]`` holds the categories expected in the ith
          column. The passed categories should not mix strings and numeric
          values within a single feature, and should be sorted in case of
          numeric values.

        The used categories can be found in the ``categories_`` attribute.

    drop : {'first', 'if_binary'} or an array-like of shape (n_features,), \
        Specifies a methodology to use to drop one of the categories per
        feature. This is useful in situations where perfectly collinear
        features cause problems, such as when feeding the resulting data
        into an unregularized linear regression model.

        However, dropping one category breaks the symmetry of the original
        representation and can therefore induce a bias in downstream models,
        for instance for penalized linear classification or regression models.

        - None : retain all features (the default).
        - 'first' : drop the first category in each feature. If only one
          category is present, the feature will be dropped entirely.
        - 'if_binary' : drop the first category in each feature with two
          categories. Features with 1 or more than 2 categories are
          left intact.
        - array : ``drop[i]`` is the category in feature ``X[:, i]`` that
          should be dropped.

        When `max_categories` or `min_frequency` is configured to group
        infrequent categories, the dropping behavior is handled after the
        grouping.

           The parameter `drop` was added in 0.21.

           The option `drop='if_binary'` was added in 0.23.

            Support for dropping infrequent categories.

        When ``True``, it returns a :class:`scipy.sparse.csr_matrix`,
        i.e. a sparse matrix in "Compressed Sparse Row" (CSR) format.

           `sparse` was renamed to `sparse_output`

    dtype : number type, default=np.float64
        Desired dtype of output.

    handle_unknown : {'error', 'ignore', 'infrequent_if_exist', 'warn'}, \
                     default='error'
        Specifies the way unknown categories are handled during :meth:`transform`.

        - 'error' : Raise an error if an unknown category is present during transform.
        - 'ignore' : When an unknown category is encountered during
          transform, the resulting one-hot encoded columns for this feature
          will be all zeros. In the inverse transform, an unknown category
          will be denoted as None.
        - 'infrequent_if_exist' : When an unknown category is encountered
          during transform, the resulting one-hot encoded columns for this
          feature will map to the infrequent category if it exists. The
          infrequent category will be mapped to the last position in the
          encoding. During inverse transform, an unknown category will be
          mapped to the category denoted `'infrequent'` if it exists. If the
          `'infrequent'` category does not exist, then :meth:`transform` and
          :meth:`inverse_transform` will handle an unknown category as with
          `handle_unknown='ignore'`. Infrequent categories exist based on
          `min_frequency` and `max_categories`. Read more in the
          :ref:`User Guide <encoder_infrequent_categories>`.
        - 'warn' : When an unknown category is encountered during transform
          a warning is issued, and the encoding then proceeds as described for
          `handle_unknown="infrequent_if_exist"`.

            `'infrequent_if_exist'` was added to automatically handle unknown
            categories and infrequent categories.

           The option `"warn"` was added in 1.6.

    min_frequency : int or float, default=None
        Specifies the minimum frequency below which a category will be
        considered infrequent.

        - If `int`, categories with a smaller cardinality will be considered
          infrequent.

        - If `float`, categories with a smaller cardinality than
          `min_frequency * n_samples`  will be considered infrequent.

            Read more in the :ref:`User Guide <encoder_infrequent_categories>`.

    max_categories : int, default=None
        Specifies an upper limit to the number of output features for each input
        feature when considering infrequent categories. If there are infrequent
        categories, `max_categories` includes the category representing the
        infrequent categories along with the frequent categories. If `None`,
        there is no limit to the number of output features.

    feature_name_combiner : "concat" or callable, default="concat"
        Callable with signature `def callable(input_feature, category)` that returns a
        string. This is used to create feature names to be returned by
        :meth:`get_feature_names_out`.

        `"concat"` concatenates encoded feature name and category with
        `feature + "_" + str(category)`.E.g. feature X with values 1, 6, 7 create
        feature names `X_1, X_6, X_7`.

    categories_ : list of arrays
        The categories of each feature determined during fitting
        (in order of the features in X and corresponding with the output
        of ``transform``). This includes the category specified in ``drop``
        (if any).

    drop_idx_ : array of shape (n_features,)
        - ``drop_idx_[i]`` is the index in ``categories_[i]`` of the category
          to be dropped for each feature.
        - ``drop_idx_[i] = None`` if no category is to be dropped from the
          feature with index ``i``, e.g. when `drop='if_binary'` and the
          feature isn't binary.
        - ``drop_idx_ = None`` if all the transformed features will be
          retained.

        If infrequent categories are enabled by setting `min_frequency` or
        `max_categories` to a non-default value and `drop_idx[i]` corresponds
        to a infrequent category, then the entire infrequent category is
        dropped.

           Added the possibility to contain `None` values.

    infrequent_categories_ : list of ndarray
        Defined only if infrequent categories are enabled by setting
        `min_frequency` or `max_categories` to a non-default value.
        `infrequent_categories_[i]` are the infrequent categories for feature
        `i`. If the feature `i` has no infrequent categories
        `infrequent_categories_[i]` is None.

    feature_name_combiner : callable or None

    See Also
    --------
    OrdinalEncoder : Performs an ordinal (integer)
      encoding of the categorical features.
    TargetEncoder : Encodes categorical features using the target.
    sklearn.feature_extraction.DictVectorizer : Performs a one-hot encoding of
      dictionary items (also handles string-valued features).
    sklearn.feature_extraction.FeatureHasher : Performs an approximate one-hot
      encoding of dictionary items or strings.
    LabelBinarizer : Binarizes labels in a one-vs-all
      fashion.
    MultiLabelBinarizer : Transforms between iterable of
      iterables and a multilabel format, e.g. a (samples x classes) binary
      matrix indicating the presence of a class label.

    Examples
    --------
    Given a dataset with two features, we let the encoder find the unique
    values per feature and transform the data to a binary one-hot encoding.

    >>> from sklearn.preprocessing import OneHotEncoder

    One can discard categories not seen during `fit`:

    >>> enc = OneHotEncoder(handle_unknown='ignore')
    >>> X = [['Male', 1], ['Female', 3], ['Female', 2]]
    >>> enc.fit(X)
    OneHotEncoder(handle_unknown='ignore')
    >>> enc.categories_
    [array(['Female', 'Male'], dtype=object), array([1, 2, 3], dtype=object)]
    >>> enc.transform([['Female', 1], ['Male', 4]]).toarray()
    array([[1., 0., 1., 0., 0.],
           [0., 1., 0., 0., 0.]])
    >>> enc.inverse_transform([[0, 1, 1, 0, 0], [0, 0, 0, 1, 0]])
    array([['Male', 1],
           [None, 2]], dtype=object)
    >>> enc.get_feature_names_out(['gender', 'group'])
    array(['gender_Female', 'gender_Male', 'group_1', 'group_2', 'group_3'], ...)

    One can always drop the first column for each feature:

    >>> drop_enc = OneHotEncoder(drop='first').fit(X)
    >>> drop_enc.categories_
    >>> drop_enc.transform([['Female', 1], ['Male', 2]]).toarray()
    array([[0., 0., 0.],

    Or drop a column for feature only having 2 categories:

    >>> drop_binary_enc = OneHotEncoder(drop='if_binary').fit(X)
    >>> drop_binary_enc.transform([['Female', 1], ['Male', 2]]).toarray()
    array([[0., 1., 0., 0.],
           [1., 0., 1., 0.]])

    One can change the way feature names are created.

    >>> def custom_combiner(feature, category):
    ...     return str(feature) + "_" + type(category).__name__ + "_" + str(category)
    >>> custom_fnames_enc = OneHotEncoder(feature_name_combiner=custom_combiner).fit(X)
    >>> custom_fnames_enc.get_feature_names_out()
    array(['x0_str_Female', 'x0_str_Male', 'x1_int_1', 'x1_int_2', 'x1_int_3'],
          dtype=object)

    Infrequent categories are enabled by setting `max_categories` or `min_frequency`.

    >>> X = np.array([["a"] * 5 + ["b"] * 20 + ["c"] * 10 + ["d"] * 3], dtype=object).T
    >>> ohe = OneHotEncoder(max_categories=3, sparse_output=False).fit(X)
    >>> ohe.infrequent_categories_
    [array(['a', 'd'], dtype=object)]
    >>> ohe.transform([["a"], ["b"]])
    array([[0., 0., 1.],
    """

        "drop": [StrOptions({"first", "if_binary"}), "array-like", None],
        "dtype": "no_validation",  # validation delegated to numpy
        "handle_unknown": [
            StrOptions({"error", "ignore", "infrequent_if_exist", "warn"})
        ],
        "max_categories": [Interval(Integral, 1, None, closed="left"), None],
        "min_frequency": [
            None,
        ],
        "feature_name_combiner": [StrOptions({"concat"}), callable],
    }

        self,
        *,
        drop=None,
        min_frequency=None,
        max_categories=None,
        feature_name_combiner="concat",
    ):
        self.handle_unknown = handle_unknown
        self.drop = drop
        self.min_frequency = min_frequency
        self.max_categories = max_categories
        self.feature_name_combiner = feature_name_combiner

    def _map_drop_idx_to_infrequent(self, feature_idx, drop_idx):
        """Convert `drop_idx` into the index for infrequent categories.

        If there are no infrequent categories, then `drop_idx` is
        returned. This method is called in `_set_drop_idx` when the `drop`
        parameter is an array-like.
        """
            return drop_idx

        default_to_infrequent = self._default_to_infrequent_mappings[feature_idx]
        if default_to_infrequent is None:

        # Raise error when explicitly dropping a category that is infrequent
        infrequent_indices = self._infrequent_indices[feature_idx]
        if infrequent_indices is not None and drop_idx in infrequent_indices:
            categories = self.categories_[feature_idx]
                f"Unable to drop category {categories[drop_idx].item()!r} from"
                f" feature {feature_idx} because it is infrequent"
            )
        return default_to_infrequent[drop_idx]

    def _set_drop_idx(self):
        """Compute the drop indices associated with `self.categories_`.

        If `self.drop` is:
        - `None`, No categories have been dropped.
        - `'first'`, All zeros to drop the first category.
        - `'if_binary'`, All zeros if the category is binary and `None`
          otherwise.
        - array-like, The indices of the categories that match the
          categories in `self.drop`. If the dropped category is an infrequent
          category, then the index for the infrequent category is used. This
          means that the entire infrequent category is dropped.

        This methods defines a public `drop_idx_` and a private
        `_drop_idx_after_grouping`.

        - `drop_idx_`: Public facing API that references the drop category in
          `self.categories_`.
        - `_drop_idx_after_grouping`: Used internally to drop categories *after* the
          infrequent categories are grouped together.

        If there are no infrequent categories or drop is `None`, then
        `drop_idx_=_drop_idx_after_grouping`.
        """
        if self.drop is None:
            drop_idx_after_grouping = None
        elif isinstance(self.drop, str):
            if self.drop == "first":
                drop_idx_after_grouping = np.zeros(len(self.categories_), dtype=object)
            elif self.drop == "if_binary":
                n_features_out_no_drop = [len(cat) for cat in self.categories_]
                if self._infrequent_enabled:
                    for i, infreq_idx in enumerate(self._infrequent_indices):
                        if infreq_idx is None:
                            continue
                        n_features_out_no_drop[i] -= infreq_idx.size - 1

                drop_idx_after_grouping = np.array(
                    [
                        0 if n_features_out == 2 else None
                        for n_features_out in n_features_out_no_drop
                    ],
                    dtype=object,
                )

        else:
            drop_array = np.asarray(self.drop, dtype=object)
            droplen = len(drop_array)

            if droplen != len(self.categories_):
                msg = (
                    "`drop` should have length equal to the number "
                    "of features ({}), got {}"
                )
                raise ValueError(msg.format(len(self.categories_), droplen))
            missing_drops = []
            drop_indices = []
            for feature_idx, (drop_val, cat_list) in enumerate(
                zip(drop_array, self.categories_)
            ):
                if not is_scalar_nan(drop_val):
                    drop_idx = np.where(cat_list == drop_val)[0]
                    if drop_idx.size:  # found drop idx
                        drop_indices.append(
                            self._map_drop_idx_to_infrequent(feature_idx, drop_idx[0])
                        )
                    else:
                        missing_drops.append((feature_idx, drop_val))
                    continue

                # drop_val is nan, find nan in categories manually
                if is_scalar_nan(cat_list[-1]):
                    drop_indices.append(
                        self._map_drop_idx_to_infrequent(feature_idx, cat_list.size - 1)
                    )
                else:  # nan is missing
                    missing_drops.append((feature_idx, drop_val))

            if any(missing_drops):
                msg = (
                    "The following categories were supposed to be "
                    "dropped, but were not found in the training "
                    "data.\n{}".format(
                        "\n".join(
                            [
                                "Category: {}, Feature: {}".format(c, v)
                                for c, v in missing_drops
                            ]
                        )
                    )
                )
            drop_idx_after_grouping = np.array(drop_indices, dtype=object)

        # `_drop_idx_after_grouping` are the categories to drop *after* the infrequent
        # categories are grouped together. If needed, we remap `drop_idx` back
        # to the categories seen in `self.categories_`.
        self._drop_idx_after_grouping = drop_idx_after_grouping

        if not self._infrequent_enabled or drop_idx_after_grouping is None:
            self.drop_idx_ = self._drop_idx_after_grouping
        else:
            drop_idx_ = []
            for feature_idx, drop_idx in enumerate(drop_idx_after_grouping):
                default_to_infrequent = self._default_to_infrequent_mappings[
                    feature_idx
                ]
                if drop_idx is None or default_to_infrequent is None:
                    orig_drop_idx = drop_idx
                else:
                    orig_drop_idx = np.flatnonzero(default_to_infrequent == drop_idx)[0]

                drop_idx_.append(orig_drop_idx)

            self.drop_idx_ = np.asarray(drop_idx_, dtype=object)

    def _compute_transformed_categories(self, i, remove_dropped=True):
        """Compute the transformed categories used for column `i`.

        1. If there are infrequent categories, the category is named
        'infrequent_sklearn'.
        2. Dropped columns are removed when remove_dropped=True.
        """
        cats = self.categories_[i]

            infreq_map = self._default_to_infrequent_mappings[i]
            if infreq_map is not None:
                frequent_mask = infreq_map < infreq_map.max()
                infrequent_cat = "infrequent_sklearn"
                # infrequent category is always at the end
                cats = np.concatenate(
                    (cats[frequent_mask], np.array([infrequent_cat], dtype=object))
                )

        if remove_dropped:
            cats = self._remove_dropped_categories(cats, i)
        return cats

    def _remove_dropped_categories(self, categories, i):
        """Remove dropped categories."""
        if (
            self._drop_idx_after_grouping is not None
            and self._drop_idx_after_grouping[i] is not None
        ):
            return np.delete(categories, self._drop_idx_after_grouping[i])
        return categories

    def _compute_n_features_outs(self):
        """Compute the n_features_out for each input feature."""
        output = [len(cats) for cats in self.categories_]

        if self._drop_idx_after_grouping is not None:
            for i, drop_idx in enumerate(self._drop_idx_after_grouping):
                if drop_idx is not None:
                    output[i] -= 1

        # infrequent is enabled, the number of features out are reduced
        # because the infrequent categories are grouped together
        for i, infreq_idx in enumerate(self._infrequent_indices):
                continue
            output[i] -= infreq_idx.size - 1

        """
        Fit OneHotEncoder to X.

        y : None

        Returns
        -------
        self
        """
            X,
            handle_unknown=self.handle_unknown,
        )
        self._set_drop_idx()
        self._n_features_outs = self._compute_n_features_outs()

        """
        Transform X using one-hot encoding.

        If `sparse_output=True` (default), it returns an instance of
        :class:`scipy.sparse._csr.csr_matrix` (CSR format).

        If there are infrequent categories for a feature, set by specifying
        `max_categories` or `min_frequency`, the infrequent categories are
        grouped into a single category.

            The data to encode.

        Returns
        -------
        X_out : {ndarray, sparse matrix} of shape \
                (n_samples, n_encoded_features)
            Transformed input. If `sparse_output=True`, a sparse matrix will be
            returned.
        """
        transform_output = _get_output_config("transform", estimator=self)["dense"]
        if transform_output != "default" and self.sparse_output:
            capitalize_transform_output = transform_output.capitalize()
                f"{capitalize_transform_output} output does not support sparse data."
                f" Set sparse_output=False to output {transform_output} dataframes or"
                f" disable {capitalize_transform_output} output via"
                '` ohe.set_output(transform="default").'
            )

        # validation of X happens in _check_X called by _transform
        if self.handle_unknown == "warn":
            warn_on_unknown, handle_unknown = True, "infrequent_if_exist"
        else:
            warn_on_unknown = self.drop is not None and self.handle_unknown in {
                "ignore",
                "infrequent_if_exist",
            }
            handle_unknown = self.handle_unknown
        X_int, X_mask = self._transform(
            X,
            handle_unknown=handle_unknown,
            warn_on_unknown=warn_on_unknown,
        )

        n_samples, n_features = X_int.shape

            to_drop = self._drop_idx_after_grouping.copy()
            # We remove all the dropped categories from mask, and decrement all
            # categories that occur after them to avoid an empty column.
            keep_cells = X_int != to_drop
            for i, cats in enumerate(self.categories_):
                # drop='if_binary' but feature isn't binary
                if to_drop[i] is None:
                    # set to cardinality to not drop from X_int
                    to_drop[i] = len(cats)

            to_drop = to_drop.reshape(1, -1)
            X_int[X_int > to_drop] -= 1
            X_mask &= keep_cells

        mask = X_mask.ravel()
        feature_indices = np.cumsum([0] + self._n_features_outs)
        indices = (X_int + feature_indices[:-1]).ravel()[mask]

        indptr = np.empty(n_samples + 1, dtype=int)
        indptr[0] = 0
        np.sum(X_mask, axis=1, out=indptr[1:], dtype=indptr.dtype)
        np.cumsum(indptr[1:], out=indptr[1:])
        data = np.ones(indptr[-1])

        out = sparse.csr_matrix(
            (data, indices, indptr),
            shape=(n_samples, feature_indices[-1]),
        )
            return out.toarray()
        else:
            return out

        """
        Convert the data back to the original representation.

        When unknown categories are encountered (all zeros in the
        one-hot encoding), ``None`` is used to represent this category. If the
        feature with the unknown category has a dropped category, the dropped
        category will be its inverse.

        For a given input feature, if there is an infrequent category,
        'infrequent_sklearn' will be used to represent the infrequent category.

        X : {array-like, sparse matrix} of shape \

        Returns
        -------
        X_tr : ndarray of shape (n_samples, n_features)
            Inverse transformed array.
        """

        n_features = len(self.categories_)

        n_features_out = np.sum(self._n_features_outs)

        # validate shape of passed X
        msg = (
            "Shape of the passed X data is not correct. Expected {0} columns, got {1}."
        )
        if X.shape[1] != n_features_out:
            raise ValueError(msg.format(n_features_out, X.shape[1]))

        transformed_features = [
            self._compute_transformed_categories(i, remove_dropped=False)
            for i, _ in enumerate(self.categories_)
        ]

        # create resulting array of appropriate dtype
        dt = np.result_type(*[cat.dtype for cat in transformed_features])
        X_tr = np.empty((n_samples, n_features), dtype=dt)

        j = 0
        found_unknown = {}

            infrequent_indices = self._infrequent_indices
        else:
            infrequent_indices = [None] * n_features

            cats_wo_dropped = self._remove_dropped_categories(
                transformed_features[i], i
            )
            n_categories = cats_wo_dropped.shape[0]

            # Only happens if there was a column with a unique
            # category. In this case we just fill the column with this
            # unique category value.
            if n_categories == 0:
                X_tr[:, i] = self.categories_[i][self._drop_idx_after_grouping[i]]
                j += n_categories
                continue
            sub = X[:, j : j + n_categories]
            # for sparse X argmax returns 2D matrix, ensure 1D array
            labels = np.asarray(sub.argmax(axis=1)).flatten()
            X_tr[:, i] = cats_wo_dropped[labels]

            if self.handle_unknown == "ignore" or (
                self.handle_unknown in ("infrequent_if_exist", "warn")
                and infrequent_indices[i] is None
            ):
                unknown = np.asarray(sub.sum(axis=1) == 0).flatten()
                # ignored unknown categories: we have a row of all zero
                if unknown.any():
                    # if categories were dropped then unknown categories will
                    # be mapped to the dropped category
                    if (
                        self._drop_idx_after_grouping is None
                        or self._drop_idx_after_grouping[i] is None
                    ):
                        found_unknown[i] = unknown
                    else:
                        X_tr[unknown, i] = self.categories_[i][
                            self._drop_idx_after_grouping[i]
                        ]
            else:
                dropped = np.asarray(sub.sum(axis=1) == 0).flatten()
                if dropped.any():
                    if self._drop_idx_after_grouping is None:
                        all_zero_samples = np.flatnonzero(dropped)
                            f"Samples {all_zero_samples} can not be inverted "
                            "when drop=None and handle_unknown='error' "
                            "because they contain all zeros"
                        )
                    # we can safely assume that all of the nulls in each column
                    # are the dropped value
                    drop_idx = self._drop_idx_after_grouping[i]
                    X_tr[dropped, i] = transformed_features[i][drop_idx]

            j += n_categories

        # if ignored are found: potentially need to upcast result to
        # insert None values
        if found_unknown:
            if X_tr.dtype != object:
                X_tr = X_tr.astype(object)

            for idx, mask in found_unknown.items():
                X_tr[mask, idx] = None

        return X_tr

        Returns
        -------
        """
        cats = [
            self._compute_transformed_categories(i)
        ]

        name_combiner = self._check_get_feature_name_combiner()
        for i in range(len(cats)):
            names = [name_combiner(input_features[i], t) for t in cats[i]]
            feature_names.extend(names)

        return np.array(feature_names, dtype=object)

    def _check_get_feature_name_combiner(self):
        if self.feature_name_combiner == "concat":
            return lambda feature, category: feature + "_" + str(category)
        else:  # callable
            dry_run_combiner = self.feature_name_combiner("feature", "category")
            if not isinstance(dry_run_combiner, str):
                    "When `feature_name_combiner` is a callable, it should return a "
                    f"Python string. Got {type(dry_run_combiner)} instead."
                )
            return self.feature_name_combiner

class OrdinalEncoder(OneToOneFeatureMixin, _BaseEncoder):
    """
    Encode categorical features as an integer array.

    The features are converted to ordinal integers. This results in
    a single column of integers (0 to n_categories - 1) per feature.

          values, and should be sorted in case of numeric values.

    handle_unknown : {'error', 'use_encoded_value'}, default='error'
        When set to 'error' an error will be raised in case an unknown
        categorical feature is present during transform. When set to
        'use_encoded_value', the encoded value of unknown categories will be
        set to the value given for the parameter `unknown_value`. In
        :meth:`inverse_transform`, an unknown category will be denoted as None.

    unknown_value : int or np.nan, default=None
        When the parameter handle_unknown is set to 'use_encoded_value', this
        parameter is required and will set the encoded value of unknown
        categories. It has to be distinct from the values used to encode any of
        the categories in `fit`. If set to np.nan, the `dtype` parameter must
        be a float dtype.

    encoded_missing_value : int or np.nan, default=np.nan
        Encoded value of missing categories. If set to `np.nan`, then the `dtype`
        parameter must be a float dtype.

        Specifies an upper limit to the number of output categories for each input

        `max_categories` do **not** take into account missing or unknown
        categories. Setting `unknown_value` or `encoded_missing_value` to an
        integer will increase the number of unique integer codes by one each.
        This can result in up to `max_categories + 2` integer codes.

        The categories of each feature determined during ``fit`` (in order of
        the features in X and corresponding with the output of ``transform``).
        This does not include categories that weren't seen during ``fit``.

    See Also
    --------
    OneHotEncoder : Performs a one-hot encoding of categorical features. This encoding
        is suitable for low to medium cardinality categorical variables, both in
        supervised and unsupervised settings.
    TargetEncoder : Encodes categorical features using supervised signal
        in a classification or regression pipeline. This encoding is typically
        suitable for high cardinality categorical variables.
    LabelEncoder : Encodes target labels with values between 0 and
        ``n_classes-1``.

    Notes
    -----
    With a high proportion of `nan` values, inferring categories becomes slow with
    Python versions before 3.10. The handling of `nan` values was improved
    from Python 3.10 onwards, (c.f.
    `bpo-43475 <https://github.com/python/cpython/issues/87641>`_).

    Examples
    --------
    values per feature and transform the data to an ordinal encoding.

    >>> from sklearn.preprocessing import OrdinalEncoder
    >>> enc = OrdinalEncoder()
    OrdinalEncoder()
    >>> enc.transform([['Female', 3], ['Male', 1]])
           [1., 0.]])

    >>> enc.inverse_transform([[1, 0], [0, 1]])
           ['Female', 2]], dtype=object)

    By default, :class:`OrdinalEncoder` is lenient towards missing values by
    propagating them.

    >>> X = [['Male', 1], ['Female', 3], ['Female', np.nan]]
    >>> enc.fit_transform(X)
    array([[ 1.,  0.],
           [ 0.,  1.],
           [ 0., nan]])

    You can use the parameter `encoded_missing_value` to encode missing values.

    >>> enc.set_params(encoded_missing_value=-1).fit_transform(X)
           [ 0.,  1.],
           [ 0., -1.]])

    In the following example, "a" and "d" are considered infrequent and grouped
    together into a single category, "b" and "c" are their own categories, unknown
    values are encoded as 3 and missing values are encoded as 4.

    >>> X_train = np.array(
    ...     [["a"] * 5 + ["b"] * 20 + ["c"] * 10 + ["d"] * 3 + [np.nan]],
    ...     dtype=object).T
    >>> enc = OrdinalEncoder(
    ...     handle_unknown="use_encoded_value", unknown_value=3,
    ...     max_categories=3, encoded_missing_value=4)
    >>> _ = enc.fit(X_train)
    >>> X_test = np.array([["a"], ["b"], ["c"], ["d"], ["e"], [np.nan]], dtype=object)
    >>> enc.transform(X_test)
    array([[2.],
           [0.],
           [1.],
           [2.],
           [3.],
           [4.]])
    """

        "encoded_missing_value": [Integral, type(np.nan)],
        "handle_unknown": [StrOptions({"error", "use_encoded_value"})],
        "unknown_value": [Integral, type(np.nan), None],
            None,
        ],
    }

        self,
        *,
        unknown_value=None,
        encoded_missing_value=np.nan,
    ):
        self.unknown_value = unknown_value
        self.encoded_missing_value = encoded_missing_value

        """
        Fit the OrdinalEncoder to X.

        y : None

        Returns
        -------
        """
        if self.handle_unknown == "use_encoded_value":
            if is_scalar_nan(self.unknown_value):
                if np.dtype(self.dtype).kind != "f":
                        "When unknown_value is np.nan, the dtype "
                        "parameter should be "
                        f"a float dtype. Got {self.dtype}."
                    )
            elif not isinstance(self.unknown_value, numbers.Integral):
                    "unknown_value should be an integer or "
                    "np.nan when "
                    "handle_unknown is 'use_encoded_value', "
                    f"got {self.unknown_value}."
                )
        elif self.unknown_value is not None:
                "unknown_value should only be set when "
                "handle_unknown is 'use_encoded_value', "
                f"got {self.unknown_value}."
            )

        # `_fit` will only raise an error when `self.handle_unknown="error"`
        fit_results = self._fit(
            X,
            return_and_ignore_missing_for_infrequent=True,
        )
        self._missing_indices = fit_results["missing_indices"]

        cardinalities = [len(categories) for categories in self.categories_]
            # Cardinality decreases because the infrequent categories are grouped
            # together
            for feature_idx, infrequent in enumerate(self.infrequent_categories_):
                if infrequent is not None:
                    cardinalities[feature_idx] -= len(infrequent)

        # missing values are not considered part of the cardinality
        # when considering unknown categories or encoded_missing_value
        for cat_idx, categories_for_idx in enumerate(self.categories_):
            if is_scalar_nan(categories_for_idx[-1]):
                cardinalities[cat_idx] -= 1

            for cardinality in cardinalities:
                if 0 <= self.unknown_value < cardinality:
                        "The used value for unknown_value "
                        f"{self.unknown_value} is one of the "
                        "values already used for encoding the "
                        "seen categories."
                    )

        if self._missing_indices:
            if np.dtype(self.dtype).kind != "f" and is_scalar_nan(
                self.encoded_missing_value
            ):
                    "There are missing values in features "
                    f"{list(self._missing_indices)}. For OrdinalEncoder to "
                    f"encode missing values with dtype: {self.dtype}, set "
                    "encoded_missing_value to a non-nan value, or "
                    "set dtype to a float"
                )

            if not is_scalar_nan(self.encoded_missing_value):
                # Features are invalid when they contain a missing category
                # and encoded_missing_value was already used to encode a
                # known category
                invalid_features = [
                    cat_idx
                    for cat_idx, cardinality in enumerate(cardinalities)
                    if cat_idx in self._missing_indices
                    and 0 <= self.encoded_missing_value < cardinality
                ]

                if invalid_features:
                    # Use feature names if they are available
                    if hasattr(self, "feature_names_in_"):
                        invalid_features = self.feature_names_in_[invalid_features]
                        f"encoded_missing_value ({self.encoded_missing_value}) "
                        "is already used to encode a known category in features: "
                        f"{invalid_features}"
                    )

        """
        Transform X to ordinal codes.

        Returns
        -------
        X_out : ndarray of shape (n_samples, n_features)
        """
        check_is_fitted(self, "categories_")
            X,
            ignore_category_indices=self._missing_indices,
        )
        X_trans = X_int.astype(self.dtype, copy=False)

        for cat_idx, missing_idx in self._missing_indices.items():
            X_missing_mask = X_int[:, cat_idx] == missing_idx
            X_trans[X_missing_mask, cat_idx] = self.encoded_missing_value

        # create separate category for unknown values
            X_trans[~X_mask] = self.unknown_value
        return X_trans

        """

        X : array-like of shape (n_samples, n_encoded_features)

        Returns
        -------
        """
        X = check_array(X, ensure_all_finite="allow-nan")

        msg = (
        )
        if X.shape[1] != n_features:
            raise ValueError(msg.format(n_features, X.shape[1]))

        dt = np.result_type(*[cat.dtype for cat in self.categories_])

        infrequent_masks = {}

        infrequent_indices = getattr(self, "_infrequent_indices", None)

            labels = X[:, i]

            # replace values of X[:, i] that were nan with actual indices
            if i in self._missing_indices:
                X_i_mask = _get_mask(labels, self.encoded_missing_value)
                labels[X_i_mask] = self._missing_indices[i]

            rows_to_update = slice(None)
            categories = self.categories_[i]

            if infrequent_indices is not None and infrequent_indices[i] is not None:
                # Compute mask for frequent categories
                infrequent_encoding_value = len(categories) - len(infrequent_indices[i])
                infrequent_masks[i] = labels == infrequent_encoding_value
                rows_to_update = ~infrequent_masks[i]

                # Remap categories to be only frequent categories. The infrequent
                # categories will be mapped to "infrequent_sklearn" later
                frequent_categories_mask = np.ones_like(categories, dtype=bool)
                frequent_categories_mask[infrequent_indices[i]] = False
                categories = categories[frequent_categories_mask]

            if self.handle_unknown == "use_encoded_value":
                unknown_labels = _get_mask(labels, self.unknown_value)
                found_unknown[i] = unknown_labels

                known_labels = ~unknown_labels
                if isinstance(rows_to_update, np.ndarray):
                    rows_to_update &= known_labels
                else:
                    rows_to_update = known_labels

            labels_int = labels[rows_to_update].astype("int64", copy=False)
            X_tr[rows_to_update, i] = categories[labels_int]

        if found_unknown or infrequent_masks:
            X_tr = X_tr.astype(object, copy=False)

        # insert None values for unknown values

        if infrequent_masks:
            for idx, mask in infrequent_masks.items():
                X_tr[mask, idx] = "infrequent_sklearn"

        """

        """
            else:
                X = X_temp
        else:

            )

    def _fit(
        self,
        X,
    ):
        )

                )

                else:
            else:
                else:

                if (
                ):
                    msg = (
                    )

                        )

                    msg = (
                    )

                    )

                    if diff:
                        msg = (
                        )

            )

        self,
        X,
    ):
        )

                    msg = (
                    )
                else:
                    if (
                    ):
                    else:

                (
                ),
            )

        return [
        ]

        """
        """

        Returns
        -------
        """
        else:

            else:
                ]

    ):

        """
                    )
                else:
        else:

        ]

                continue

                n_cats -= 1

        """
            return

                continue

                continue

            else:

        self,
        *,
    ):

        """

            )

        """
                            continue

                    [
                    ],
                )

        else:

                msg = (
                )
            ):
                        )
                    else:
                    continue

                    )

                msg = (
                            [
                            ]
                        )
                    )
                )

        else:
                ]
                else:

        """

                )

        if (
        ):

                continue

        """

        y : None

        Returns
        -------
        self
        """
            X,
        )

        """

        Returns
        -------
            returned.
        """
            )

        else:
                "ignore",
            }
            X,
        )

        )
        else:
            return out

        """

        Returns
        -------
        """

        msg = (
        )

        ]

        j = 0

        else:

            )

                continue

            ):
                    if (
                    ):
                    else:
                        ]
            else:
                        )

        Returns
        -------
        """
        cats = [
        ]

                )

        self,
        *,
    ):

        """

        y : None

        Returns
        -------
        """
                    )
                )
            )

            X,
        )

            # together

                    )

            ):
                )

                    cat_idx
                ]

                    )

        """

        Returns
        -------
        """
            X,
        )

        """

        Returns
        -------
        """

        msg = (
        )

                else:

class KBinsDiscretizer(TransformerMixin, BaseEstimator):
    """
    Bin continuous data into intervals.

    Read more in the :ref:`User Guide <preprocessing_discretization>`.

    n_bins : int or array-like of shape (n_features,), default=5
        The number of bins to produce. Raises ValueError if ``n_bins < 2``.

    encode : {'onehot', 'onehot-dense', 'ordinal'}, default='onehot'
        Method used to encode the transformed result.

        - 'onehot': Encode the transformed result with one-hot encoding
          and return a sparse matrix. Ignored features are always
          stacked to the right.
        - 'onehot-dense': Encode the transformed result with one-hot encoding
          and return a dense array. Ignored features are always
        - 'ordinal': Return the bin identifier encoded as an integer value.

    strategy : {'uniform', 'quantile', 'kmeans'}, default='quantile'

        - 'uniform': All bins in each feature have identical widths.
        - 'quantile': All bins in each feature have the same number of points.
        - 'kmeans': Values in each bin have the same nearest center of a 1D
          k-means cluster.

        For an example of the different strategies see:
        :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization_strategies.py`.

    quantile_method : {"inverted_cdf", "averaged_inverted_cdf",
            "closest_observation", "interpolated_inverted_cdf", "hazen",
            "weibull", "linear", "median_unbiased", "normal_unbiased"},
            default="linear"
            Method to pass on to np.percentile calculation when using
            strategy="quantile". Only `averaged_inverted_cdf` and `inverted_cdf`
            support the use of `sample_weight != None` when subsampling is not
            active.

    dtype : {np.float32, np.float64}, default=None
        The desired data-type for the output. If None, output dtype is
        consistent with input dtype. Only np.float32 and np.float64 are
        supported.

    subsample : int or None, default=200_000
        Maximum number of samples, used to fit the model, for computational
        efficiency.
        `subsample=None` means that all the training samples are used when
        computing the quantiles that determine the binning thresholds.
        Since quantile computation relies on sorting each column of `X` and
        that sorting has an `n log(n)` time complexity,
        it is recommended to use subsampling on datasets with a
        very large number of samples.

            The default value of `subsample` changed from `None` to `200_000` when
            `strategy="quantile"`.

            `strategy="uniform"` or `strategy="kmeans"`.

        Determines random number generation for subsampling.
        See the `subsample` parameter for more details.

    bin_edges_ : ndarray of ndarray of shape (n_features,)
        The edges of each bin. Contain arrays of varying shapes ``(n_bins_, )``
        Ignored features will have empty arrays.

    n_bins_ : ndarray of shape (n_features,), dtype=np.int64
        Number of bins per feature. Bins whose width are too small
        (i.e., <= 1e-8) are removed with a warning.

    See Also
    --------
    Binarizer : Class used to bin values as ``0`` or
        ``1`` based on a parameter ``threshold``.

    Notes
    -----

    For a visualization of discretization on different datasets refer to
    :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization_classification.py`.
    On the effect of discretization on linear models see:
    :ref:`sphx_glr_auto_examples_preprocessing_plot_discretization.py`.

    In bin edges for feature ``i``, the first and last values are used only for
    ``inverse_transform``. During transform, bin edges are extended to::

      np.concatenate([-np.inf, bin_edges_[i][1:-1], np.inf])

    You can combine ``KBinsDiscretizer`` with
    :class:`~sklearn.compose.ColumnTransformer` if you only want to preprocess
    part of the features.

    ``KBinsDiscretizer`` might produce constant features (e.g., when
    ``encode = 'onehot'`` and certain bins do not contain any data).
    These features can be removed with feature selection algorithms
    (e.g., :class:`~sklearn.feature_selection.VarianceThreshold`).

    Examples
    --------
    >>> from sklearn.preprocessing import KBinsDiscretizer
    >>> X = [[-2, 1, -4,   -1],
    ...      [-1, 2, -3, -0.5],
    ...      [ 0, 3, -2,  0.5],
    ...      [ 1, 4, -1,    2]]
    >>> est = KBinsDiscretizer(
    ...     n_bins=3, encode='ordinal', strategy='uniform'
    ... )
    >>> est.fit(X)
    KBinsDiscretizer(...)
    >>> Xt = est.transform(X)
    >>> Xt  # doctest: +SKIP
    array([[ 0., 0., 0., 0.],
           [ 1., 1., 1., 0.],
           [ 2., 2., 2., 1.],
           [ 2., 2., 2., 2.]])

    Sometimes it may be useful to convert the data back into the original
    feature space. The ``inverse_transform`` function converts the binned
    data into the original feature space. Each value will be equal to the mean
    of the two bin edges.

    >>> est.bin_edges_[0]
    array([-2., -1.,  0.,  1.])
    >>> est.inverse_transform(Xt)
    array([[-1.5,  1.5, -3.5, -0.5],
           [-0.5,  2.5, -2.5, -0.5],
           [ 0.5,  3.5, -1.5,  0.5],
           [ 0.5,  3.5, -1.5,  1.5]])
    """

        "n_bins": [Interval(Integral, 2, None, closed="left"), "array-like"],
        "encode": [StrOptions({"onehot", "onehot-dense", "ordinal"})],
        "strategy": [StrOptions({"uniform", "quantile", "kmeans"})],
        "quantile_method": [
                {
                    "warn",
                    "inverted_cdf",
                    "averaged_inverted_cdf",
                    "closest_observation",
                    "interpolated_inverted_cdf",
                    "hazen",
                    "weibull",
                    "linear",
                    "median_unbiased",
                    "normal_unbiased",
                }
            )
        ],
        "dtype": [Options(type, {np.float64, np.float32}), None],
    }

        self,
        n_bins=5,
        *,
        encode="onehot",
        strategy="quantile",
        quantile_method="warn",
        subsample=200_000,
    ):
        self.encode = encode
        self.quantile_method = quantile_method

        """
        Fit the estimator.

            Data to be discretized.

        y : None

            Contains weight values to be associated with each sample.

            .. versionchanged:: 1.7
               Added support for strategy="uniform".

        Returns
        -------
        """
        X = validate_data(self, X, dtype="numeric")

        if self.dtype in (np.float64, np.float32):
            output_dtype = self.dtype
        else:  # self.dtype is None
            output_dtype = X.dtype

        if self.subsample is not None and n_samples > self.subsample:
            # When resampling, it is important to subsample **with replacement** to
            # preserve the distribution, in particular in the presence of a few data
            # points with large weights. You can check this by setting `replace=False`
            # in sklearn.utils.test.test_indexing.test_resample_weighted and check that
            # it fails as a justification for this claim.
                X,
                n_samples=self.subsample,
            )
            # Since we already used the weights when resampling when provided,
            # we set them back to `None` to avoid accounting for the weights twice
            # in subsequent operations to compute weight-aware bin edges with
            # quantiles or k-means.

        n_bins = self._validate_n_bins(n_features)

        bin_edges = np.zeros(n_features, dtype=object)

        # TODO(1.9): remove and switch to quantile_method="averaged_inverted_cdf"
        # by default.
        quantile_method = self.quantile_method
        if self.strategy == "quantile" and quantile_method == "warn":
                "The current default behavior, quantile_method='linear', will be "
                "changed to quantile_method='averaged_inverted_cdf' in "
                "scikit-learn version 1.9 to naturally support sample weight "
                "equivalence properties by default. Pass "
                "quantile_method='averaged_inverted_cdf' explicitly to silence this "
                "warning.",
            )
            quantile_method = "linear"

        if (
            self.strategy == "quantile"
            and quantile_method not in ["inverted_cdf", "averaged_inverted_cdf"]
            and sample_weight is not None
        ):
                "When fitting with strategy='quantile' and sample weights, "
                "quantile_method should either be set to 'averaged_inverted_cdf' or "
                f"'inverted_cdf', got quantile_method='{quantile_method}' instead."
            )

        if self.strategy != "quantile" and sample_weight is not None:
            # Prepare a mask to filter out zero-weight samples when extracting
            # the min and max values of each columns which are needed for the
            # "uniform" and "kmeans" strategies.
            nnz_weight_mask = sample_weight != 0
        else:
            # Otherwise, all samples are used. Use a slice to avoid creating a
            # new array.
            nnz_weight_mask = slice(None)

        for jj in range(n_features):
            column = X[:, jj]
            col_min = column[nnz_weight_mask].min()
            col_max = column[nnz_weight_mask].max()

            if col_min == col_max:
                    "Feature %d is constant and will be replaced with 0." % jj
                )
                n_bins[jj] = 1
                bin_edges[jj] = np.array([-np.inf, np.inf])
                continue

            if self.strategy == "uniform":
                bin_edges[jj] = np.linspace(col_min, col_max, n_bins[jj] + 1)

            elif self.strategy == "quantile":
                percentile_levels = np.linspace(0, 100, n_bins[jj] + 1)

                # method="linear" is the implicit default for any numpy
                # version. So we keep it version independent in that case by
                # using an empty param dict.
                percentile_kwargs = {}
                if quantile_method != "linear" and sample_weight is None:
                    percentile_kwargs["method"] = quantile_method

                if sample_weight is None:
                    bin_edges[jj] = np.asarray(
                        np.percentile(column, percentile_levels, **percentile_kwargs),
                        dtype=np.float64,
                    )
                else:
                    # TODO: make _weighted_percentile and
                    # _averaged_weighted_percentile accept an array of
                    # quantiles instead of calling it multiple times and
                    # sorting the column multiple times as a result.
                    percentile_func = {
                        "inverted_cdf": _weighted_percentile,
                        "averaged_inverted_cdf": _averaged_weighted_percentile,
                    }[quantile_method]
                        [
                            percentile_func(column, sample_weight, percentile_rank=p)
                            for p in percentile_levels
                        ],
                    )
            elif self.strategy == "kmeans":
                from ..cluster import KMeans  # fixes import loops

                # Deterministic initialization with uniform spacing
                uniform_edges = np.linspace(col_min, col_max, n_bins[jj] + 1)
                init = (uniform_edges[1:] + uniform_edges[:-1])[:, None] * 0.5

                # 1D k-means procedure
                km = KMeans(n_clusters=n_bins[jj], init=init, n_init=1)
                centers = km.fit(
                    column[:, None], sample_weight=sample_weight
                ).cluster_centers_[:, 0]
                # Must sort, centers may be unsorted even with sorted init
                centers.sort()
                bin_edges[jj] = (centers[1:] + centers[:-1]) * 0.5
                bin_edges[jj] = np.r_[col_min, bin_edges[jj], col_max]

            # Remove bins whose width are too small (i.e., <= 1e-8)
            if self.strategy in ("quantile", "kmeans"):
                mask = np.ediff1d(bin_edges[jj], to_begin=np.inf) > 1e-8
                bin_edges[jj] = bin_edges[jj][mask]
                if len(bin_edges[jj]) - 1 != n_bins[jj]:
                        "Bins whose width are too small (i.e., <= "
                        "1e-8) in feature %d are removed. Consider "
                        "decreasing the number of bins." % jj
                    )
                    n_bins[jj] = len(bin_edges[jj]) - 1

        self.bin_edges_ = bin_edges
        self.n_bins_ = n_bins

        if "onehot" in self.encode:
            self._encoder = OneHotEncoder(
                categories=[np.arange(i) for i in self.n_bins_],
                sparse_output=self.encode == "onehot",
                dtype=output_dtype,
            )
            # Fit the OneHotEncoder with toy datasets
            # so that it's ready for use after the KBinsDiscretizer is fitted
            self._encoder.fit(np.zeros((1, len(self.n_bins_))))

    def _validate_n_bins(self, n_features):
        """Returns n_bins_, the number of bins per feature."""
        orig_bins = self.n_bins
        if isinstance(orig_bins, Integral):
            return np.full(n_features, orig_bins, dtype=int)

        n_bins = check_array(orig_bins, dtype=int, copy=True, ensure_2d=False)

        if n_bins.ndim > 1 or n_bins.shape[0] != n_features:
            raise ValueError("n_bins must be a scalar or array of shape (n_features,).")

        bad_nbins_value = (n_bins < 2) | (n_bins != orig_bins)

        violating_indices = np.where(bad_nbins_value)[0]
        if violating_indices.shape[0] > 0:
            indices = ", ".join(str(i) for i in violating_indices)
                "{} received an invalid number "
                "of bins at indices {}. Number of bins "
                "must be at least 2, and must be an int.".format(
                    KBinsDiscretizer.__name__, indices
                )
            )
        return n_bins

        """
        Discretize the data.

        Returns
        -------
        Xt : {ndarray, sparse matrix}, dtype={np.float32, np.float64}
            Data in the binned space. Will be a sparse matrix if
            `self.encode='onehot'` and ndarray otherwise.
        """

        # check input and attribute dtypes
        dtype = (np.float64, np.float32) if self.dtype is None else self.dtype
        Xt = validate_data(self, X, copy=True, dtype=dtype, reset=False)

        bin_edges = self.bin_edges_
        for jj in range(Xt.shape[1]):
            Xt[:, jj] = np.searchsorted(bin_edges[jj][1:-1], Xt[:, jj], side="right")

        if self.encode == "ordinal":
            return Xt

        dtype_init = None
            dtype_init = self._encoder.dtype
            self._encoder.dtype = Xt.dtype
        try:
            Xt_enc = self._encoder.transform(Xt)
        finally:
            # revert the initial dtype to avoid modifying self.
            self._encoder.dtype = dtype_init
        return Xt_enc

        """
        Transform discretized data back to original feature space.

        Note that this function does not regenerate the original data
        due to discretization rounding.

            Transformed data in the binned space.

        Returns
        -------
        Xinv : ndarray, dtype={np.float32, np.float64}
            Data in the original feature space.
        """

            X = self._encoder.inverse_transform(X)

        Xinv = check_array(X, copy=True, dtype=(np.float64, np.float32))
        n_features = self.n_bins_.shape[0]
        if Xinv.shape[1] != n_features:
                "Incorrect number of features. Expecting {}, received {}.".format(
                    n_features, Xinv.shape[1]
                )
            )

            bin_edges = self.bin_edges_[jj]
            bin_centers = (bin_edges[1:] + bin_edges[:-1]) * 0.5
            Xinv[:, jj] = bin_centers[(Xinv[:, jj]).astype(np.int64)]

        return Xinv

        """Get output feature names.

        Returns
        -------
        """
        if hasattr(self, "_encoder"):
            return self._encoder.get_feature_names_out(input_features)

        # ordinal encoding

        self,
        n_bins=5,
        *,
    ):

        """

        y : None

        Returns
        -------
        """

                X,
            )

            )

        if (
        ):
            )

        else:

                )
                continue

                    )
                else:
                        [
                        ],
                    )

                    )

            )

                )
            )

        """

        Returns
        -------
        """

            return Xt

        try:
        finally:

        """

        Returns
        -------
        """

                )
            )

        Returns
        -------
        """

def _get_valid_samples_by_column(X, col):
    """Get non NaN samples in column of X"""
    return X[:, [col]][~np.isnan(X[:, col])]

def test_missing_value_handling(
    est, func, support_sparse, strictly_positive, omit_kwargs
):
    # check that the preprocessing method let pass nan
    n_missing = 50
    X[
        rng.randint(X.shape[0], size=n_missing), rng.randint(X.shape[1], size=n_missing)
    ] = np.nan
    if strictly_positive:
        X += np.nanmin(X) + 0.1
    X_train, X_test = train_test_split(X, random_state=1)
    assert not np.all(np.isnan(X_train), axis=0).any()
    assert np.any(np.isnan(X_train), axis=0).all()
    assert np.any(np.isnan(X_test), axis=0).all()
    X_test[:, 0] = np.nan  # make sure this boundary case is tested

        Xt = est.fit(X_train).transform(X_test)
    # ensure no warnings are raised
    # missing values should still be missing, and only them
    assert_array_equal(np.isnan(Xt), np.isnan(X_test))

    # check that the function leads to the same results as the class
        Xt_class = est.transform(X_train)
    kwargs = est.get_params()
    # remove the parameters which should be omitted because they
    # are not defined in the counterpart function of the preprocessing class
    for kwarg in omit_kwargs:
        _ = kwargs.pop(kwarg)
    Xt_func = func(X_train, **kwargs)
    assert_array_equal(np.isnan(Xt_func), np.isnan(Xt_class))
    assert_allclose(Xt_func[~np.isnan(Xt_func)], Xt_class[~np.isnan(Xt_class)])

    # check that the inverse transform keep NaN
    Xt_inv = est.inverse_transform(Xt)
    assert_array_equal(np.isnan(Xt_inv), np.isnan(X_test))
    # FIXME: we can introduce equal_nan=True in recent version of numpy.
    # For the moment which just check that non-NaN values are almost equal.
    assert_allclose(Xt_inv[~np.isnan(Xt_inv)], X_test[~np.isnan(X_test)])

    for i in range(X.shape[1]):
        # train only on non-NaN
        est.fit(_get_valid_samples_by_column(X_train, i))
        # check transforming with NaN works even when training without NaN
            Xt_col = est.transform(X_test[:, [i]])
        assert_allclose(Xt_col, Xt[:, [i]])
        # check non-NaN is handled as before - the 1st column is all nan
        if not np.isnan(X_test[:, i]).all():
            Xt_col_nonan = est.transform(_get_valid_samples_by_column(X_test, i))
            assert_array_equal(Xt_col_nonan, Xt_col[~np.isnan(Xt_col.squeeze())])

    if support_sparse:
        est_dense = clone(est)
        est_sparse = clone(est)

            Xt_dense = est_dense.fit(X_train).transform(X_test)
            Xt_inv_dense = est_dense.inverse_transform(Xt_dense)

        for sparse_container in (
            BSR_CONTAINERS
            + CSR_CONTAINERS
            + DIA_CONTAINERS
            + LIL_CONTAINERS
        ):
            # check that the dense and sparse inputs lead to the same results
            # precompute the matrix to avoid catching side warnings
            X_train_sp = sparse_container(X_train)
            X_test_sp = sparse_container(X_test)
                warnings.simplefilter("ignore", PendingDeprecationWarning)
                warnings.simplefilter("error", RuntimeWarning)
                Xt_sp = est_sparse.fit(X_train_sp).transform(X_test_sp)

            assert_allclose(Xt_sp.toarray(), Xt_dense)
                Xt_inv_sp = est_sparse.inverse_transform(Xt_sp)

            assert_allclose(Xt_inv_sp.toarray(), Xt_inv_dense)

def test_missing_value_pandas_na_support(est, func):

        [
            [1, 2, 3, np.nan, np.nan, 4, 5, 1],
            [np.nan, np.nan, 8, 4, 6, np.nan, np.nan, 8],
            [1, 2, 3, 4, 5, 6, 7, 8],
        ]
    ).T

    X_df = pd.DataFrame(X, dtype="Int16", columns=["a", "b", "c"])
    X_df["c"] = X_df["c"].astype("int")

    X_trans = est.fit_transform(X)
    X_df_trans = est.fit_transform(X_df)

    assert_allclose(X_trans, X_df_trans)

def _make_func(args_store, kwargs_store, func=lambda X, *a, **k: X):
    def _func(X, *args, **kwargs):
        args_store.append(X)
        args_store.extend(args)
        kwargs_store.update(kwargs)
        return func(X)

    return _func

def test_delegate_to_func():
    # (args|kwargs)_store will hold the positional and keyword arguments
    # passed to the function inside the FunctionTransformer.
    args_store = []
    kwargs_store = {}
    X = np.arange(10).reshape((5, 2))
        FunctionTransformer(_make_func(args_store, kwargs_store)).transform(X),
        X,
        "transform should have returned X unchanged",
    )

    # The function should only have received X.
    assert args_store == [X], (
        "Incorrect positional arguments passed to func: {args}".format(args=args_store)
    )

    assert not kwargs_store, (
        "Unexpected keyword arguments passed to func: {args}".format(args=kwargs_store)
    )

    # reset the argument stores.
    args_store[:] = []
    kwargs_store.clear()
    transformed = FunctionTransformer(
        _make_func(args_store, kwargs_store),
    ).transform(X)

        transformed, X, err_msg="transform should have returned X unchanged"
    )

    # The function should have received X
    )

    )

def test_np_log():

    # Test that the numpy.log example still works.
        FunctionTransformer(np.log1p).transform(X),
        np.log1p(X),
    )

def test_kw_arg():
    X = np.linspace(0, 1, num=10).reshape((5, 2))

    F = FunctionTransformer(np.around, kw_args=dict(decimals=3))

    # Test that rounding is correct
    assert_array_equal(F.transform(X), np.around(X, decimals=3))

def test_kw_arg_update():

    F.kw_args["decimals"] = 1

    assert_array_equal(F.transform(X), np.around(X, decimals=1))

def test_kw_arg_reset():

    F.kw_args = dict(decimals=1)

def test_inverse_transform():
    X = np.array([1, 4, 9, 16]).reshape((2, 2))

    # Test that inverse_transform works correctly
    F = FunctionTransformer(
        inverse_func=np.around,
        inv_kw_args=dict(decimals=3),
    )
        F.inverse_transform(F.transform(X)),
        np.around(np.sqrt(X), decimals=3),
    )

def test_check_inverse(sparse_container):
    X = np.array([1, 4, 9, 16], dtype=np.float64).reshape((2, 2))

    trans = FunctionTransformer(
        accept_sparse=sparse_container is not None,
        validate=True,
    )
        "The provided functions are not strictly"
        " inverse of each other. If you are sure you"
        " want to proceed regardless, set"
        " 'check_inverse=False'."
    )
        trans.fit(X)

        func=np.expm1,
        inverse_func=np.log1p,
    )
        Xt = trans.fit_transform(X)

    assert_allclose_dense_sparse(X, trans.inverse_transform(Xt))

def test_check_inverse_func_or_inverse_not_provided():
    # check that we don't check inverse when one of the func or inverse is not

        func=np.expm1, inverse_func=None, check_inverse=True, validate=True
    )
        func=None, inverse_func=np.expm1, check_inverse=True, validate=True
    )

def test_function_transformer_frame():
    X_df = pd.DataFrame(np.random.randn(100, 10))
    transformer = FunctionTransformer()
    X_df_trans = transformer.fit_transform(X_df)
    assert hasattr(X_df_trans, "loc")

def test_function_transformer_raise_error_with_mixed_dtype(X_type):
    """Check that `FunctionTransformer.check_inverse` raises error on mixed dtype."""
    mapping = {"one": 1, "two": 2, "three": 3, 5: "five", 6: "six"}
    inverse_mapping = {value: key for key, value in mapping.items()}
    dtype = "object"

    data = ["one", "two", "three", "one", "one", 5, 6]
    data = _convert_container(data, X_type, columns_name=["value"], dtype=dtype)

        return np.array([mapping[X[i]] for i in range(X.size)], dtype=object)

    def inverse_func(X):
        return _convert_container(
            [inverse_mapping[x] for x in X],
            X_type,
            columns_name=["value"],
        )

        func=func, inverse_func=inverse_func, validate=False, check_inverse=True
    )

    msg = "'check_inverse' is only supported when all the elements in `X` is numerical."
        transformer.fit(data)

def test_function_transformer_support_all_nummerical_dataframes_check_inverse_True():
    """Check support for dataframes with only numerical values."""

    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
        func=lambda x: x + 2, inverse_func=lambda x: x - 2, check_inverse=True
    )

    # Does not raise an error
    df_out = transformer.fit_transform(df)
    assert_allclose_dense_sparse(df_out, df + 2)

def test_function_transformer_with_dataframe_and_check_inverse_True():
    """Check error is raised when check_inverse=True.

    Non-regresion test for gh-25261.
    """
        func=lambda x: x, inverse_func=lambda x: x, check_inverse=True
    )

    df_mixed = pd.DataFrame({"a": [1, 2, 3], "b": ["a", "b", "c"]})
        transformer.fit(df_mixed)

def test_function_transformer_get_feature_names_out(
    X, feature_names_out, input_features, expected, validate
):
    if isinstance(X, dict):

        feature_names_out=feature_names_out, validate=validate
    )
    transformer.fit(X)
    names = transformer.get_feature_names_out(input_features)
    assert_array_equal(names, expected)

def test_function_transformer_get_feature_names_out_without_validation():
    transformer = FunctionTransformer(feature_names_out="one-to-one", validate=False)
    X = np.random.rand(100, 2)
    transformer.fit_transform(X)

    names = transformer.get_feature_names_out(("a", "b"))
    assert_array_equal(names, ("a", "b"))

def test_function_transformer_feature_names_out_is_None():

    msg = "This 'FunctionTransformer' has no attribute 'get_feature_names_out'"
        transformer.get_feature_names_out()

def test_function_transformer_feature_names_out_uses_estimator():
    def add_n_random_features(X, n):
        return np.concatenate([X, np.random.rand(len(X), n)], axis=1)

    def feature_names_out(transformer, input_features):
        n = transformer.kw_args["n"]
        return list(input_features) + [f"rnd{i}" for i in range(n)]

        func=add_n_random_features,
        feature_names_out=feature_names_out,
        kw_args=dict(n=3),
    )
    df = pd.DataFrame({"a": np.random.rand(100), "b": np.random.rand(100)})
    transformer.fit_transform(df)
    names = transformer.get_feature_names_out()

    assert_array_equal(names, ("a", "b", "rnd0", "rnd1", "rnd2"))

def test_function_transformer_validate_inverse():
    """Test that function transformer does not reset estimator in
    `inverse_transform`."""

    def add_constant_feature(X):
        X_one = np.ones((X.shape[0], 1))
        return np.concatenate((X, X_one), axis=1)

    def inverse_add_constant(X):
        return X[:, :-1]

    X = np.array([[1, 2], [3, 4], [3, 4]])
        func=add_constant_feature,
        inverse_func=inverse_add_constant,
    )
    assert trans.n_features_in_ == X.shape[1]

    trans.inverse_transform(X_trans)

def test_get_feature_names_out_dataframe_with_string_data(
    feature_names_out, expected, in_pipeline
):
    """Check that get_feature_names_out works with DataFrames with string data."""
    X = pd.DataFrame({"pet": ["dog", "cat"], "color": ["red", "green"]})

        if feature_names_out == "one-to-one":
            return X
        else:
            name = feature_names_out(None, X.columns)
            return X.rename(columns=dict(zip(X.columns, name)))

    transformer = FunctionTransformer(func=func, feature_names_out=feature_names_out)
    if in_pipeline:
        transformer = make_pipeline(transformer)

    X_trans = transformer.fit_transform(X)

def test_set_output_func():
    """Check behavior of set_output with different settings."""

    ft = FunctionTransformer(np.log, feature_names_out="one-to-one")

    # no warning is raised when feature_names_out is defined
        ft.set_output(transform="pandas")

    X_trans = ft.fit_transform(X)
    assert_array_equal(X_trans.columns, ["a", "b"])

    ft = FunctionTransformer(lambda x: 2 * x)
    ft.set_output(transform="pandas")

    # no warning is raised when func returns a panda dataframe
        X_trans = ft.fit_transform(X)

    # Warning is raised when func returns a ndarray
    ft_np = FunctionTransformer(lambda x: np.asarray(x))

    for transform in ("pandas", "polars"):
        ft_np.set_output(transform=transform)
        msg = (
            f"When `set_output` is configured to be '{transform}'.*{transform} "
            "DataFrame.*"
        )
        with pytest.warns(UserWarning, match=msg):
            ft_np.fit_transform(X)

    # default transform does not warn
    ft_np.set_output(transform="default")
        ft_np.fit_transform(X)

def test_consistence_column_name_between_steps():
    """Check that we have a consistence between the feature names out of
    `FunctionTransformer` and the feature names in of the next step in the pipeline.

    https://github.com/scikit-learn/scikit-learn/issues/27695
    """

    def with_suffix(_, names):
        return [name + "__log" for name in names]

        FunctionTransformer(np.log1p, feature_names_out=with_suffix), StandardScaler()
    )

    df = pd.DataFrame([[1, 2], [3, 4], [5, 6]], columns=["a", "b"])
    X_trans = pipeline.fit_transform(df)
    assert pipeline.get_feature_names_out().tolist() == ["a__log", "b__log"]
    # StandardScaler will convert to a numpy array

def test_function_transformer_overwrite_column_names(dataframe_lib, transform_output):
    """Check that we overwrite the column names when we should."""
    if transform_output != "numpy":
        pytest.importorskip(transform_output)

    df = lib.DataFrame({"a": [1, 2, 3], "b": [10, 20, 100]})

    transformer = FunctionTransformer(feature_names_out=with_suffix).set_output(
        transform=transform_output
    )
    X_trans = transformer.fit_transform(df)
    assert_array_equal(np.asarray(X_trans), np.asarray(df))

    feature_names = transformer.get_feature_names_out()
    assert list(X_trans.columns) == with_suffix(None, df.columns)
    assert feature_names.tolist() == with_suffix(None, df.columns)

def test_function_transformer_overwrite_column_names_numerical(feature_names_out):
    """Check the same as `test_function_transformer_overwrite_column_names`
    but for the specific case of pandas where column names can be numerical."""

    df = pd.DataFrame({0: [1, 2, 3], 1: [10, 20, 100]})

    transformer = FunctionTransformer(feature_names_out=feature_names_out)

    assert list(X_trans.columns) == list(feature_names)

def test_function_transformer_error_column_inconsistent(
    dataframe_lib, feature_names_out
):
    """Check that we raise an error when `func` returns a dataframe with new
    column names that become inconsistent with `get_feature_names_out`."""

    def func(df):
        if dataframe_lib == "pandas":
            return df.rename(columns={"a": "c"})
        else:
            return df.rename({"a": "c"})

    err_msg = "The output generated by `func` have different column names"
        transformer.fit_transform(df).columns

            X_type,
        )

            return X
        else:

        else:

def test_one_hot_encoder_sparse_dense():
    # check that sparse and dense will give the same results

    X = np.array([[3, 2, 1], [0, 1, 1]])
    enc_sparse = OneHotEncoder()
    enc_dense = OneHotEncoder(sparse_output=False)

    X_trans_sparse = enc_sparse.fit_transform(X)
    X_trans_dense = enc_dense.fit_transform(X)

    assert X_trans_sparse.shape == (2, 5)
    assert X_trans_dense.shape == (2, 5)

    assert sparse.issparse(X_trans_sparse)
    assert not sparse.issparse(X_trans_dense)

    # check outcome
        X_trans_sparse.toarray(), [[0.0, 1.0, 0.0, 1.0, 1.0], [1.0, 0.0, 1.0, 0.0, 1.0]]
    )
    assert_array_equal(X_trans_sparse.toarray(), X_trans_dense)

def test_one_hot_encoder_handle_unknown(handle_unknown):
    X = np.array([[0, 2, 1], [1, 0, 3], [1, 0, 2]])
    X2 = np.array([[4, 1, 1]])

    # Test that one hot encoder raises error for unknown features
    # present during transform.
    oh = OneHotEncoder(handle_unknown="error")
    oh.fit(X)
    with pytest.raises(ValueError, match="Found unknown categories"):
        oh.transform(X2)

    # Test the ignore option, ignores unknown features (giving all 0's)
    oh = OneHotEncoder(handle_unknown=handle_unknown)
    oh.fit(X)
    X2_passed = X2.copy()
        oh.transform(X2_passed).toarray(),
        np.array([[0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0]]),
    )
    # ensure transformed data was not modified in place
    assert_allclose(X2, X2_passed)

def test_one_hot_encoder_handle_unknown_strings(handle_unknown):
    X = np.array(["11111111", "22", "333", "4444"]).reshape((-1, 1))
    X2 = np.array(["55555", "22"]).reshape((-1, 1))
    # Non Regression test for the issue #12470
    # Test the ignore option, when categories are numpy string dtype
    # particularly when the known category strings are larger
    # than the unknown category strings
    oh.fit(X)
        np.array([[0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]]),
    )
    assert_array_equal(X2, X2_passed)

def test_one_hot_encoder_dtype(input_dtype, output_dtype):
    X = np.asarray([[0, 1]], dtype=input_dtype).T
    X_expected = np.asarray([[1, 0], [0, 1]], dtype=output_dtype)

    oh = OneHotEncoder(categories="auto", dtype=output_dtype)
    assert_array_equal(oh.fit_transform(X).toarray(), X_expected)
    assert_array_equal(oh.fit(X).transform(X).toarray(), X_expected)

    oh = OneHotEncoder(categories="auto", dtype=output_dtype, sparse_output=False)
    assert_array_equal(oh.fit_transform(X), X_expected)
    assert_array_equal(oh.fit(X).transform(X), X_expected)

def test_one_hot_encoder_dtype_pandas(output_dtype):

    X_df = pd.DataFrame({"A": ["a", "b"], "B": [1, 2]})
    X_expected = np.array([[1, 0, 1, 0], [0, 1, 0, 1]], dtype=output_dtype)

    oh = OneHotEncoder(dtype=output_dtype)
    assert_array_equal(oh.fit_transform(X_df).toarray(), X_expected)
    assert_array_equal(oh.fit(X_df).transform(X_df).toarray(), X_expected)

    oh = OneHotEncoder(dtype=output_dtype, sparse_output=False)
    assert_array_equal(oh.fit_transform(X_df), X_expected)
    assert_array_equal(oh.fit(X_df).transform(X_df), X_expected)

def test_one_hot_encoder_feature_names():
    enc = OneHotEncoder()
    X = [
        ["Male", 1, "girl", 2, 3],
        ["Female", 41, "girl", 1, 10],
        ["Male", 51, "boy", 12, 3],
        ["Male", 91, "girl", 21, 30],
    ]

    enc.fit(X)
    feature_names = enc.get_feature_names_out()

        [
            "x0_Female",
            "x0_Male",
            "x1_1",
            "x1_41",
            "x1_51",
            "x1_91",
            "x2_boy",
            "x2_girl",
            "x3_1",
            "x3_2",
            "x3_12",
            "x3_21",
            "x4_3",
            "x4_10",
            "x4_30",
        ],
    )

    feature_names2 = enc.get_feature_names_out(["one", "two", "three", "four", "five"])

        [
            "one_Female",
            "one_Male",
            "two_1",
            "two_41",
            "two_51",
            "two_91",
            "three_boy",
            "three_girl",
            "four_1",
            "four_2",
            "four_12",
            "four_21",
            "five_3",
            "five_10",
            "five_30",
        ],
        feature_names2,
    )

    with pytest.raises(ValueError, match="input_features should have length"):
        enc.get_feature_names_out(["one", "two"])

def test_one_hot_encoder_feature_names_unicode():
    X = np.array([["c❤t1", "dat2"]], dtype=object).T
    assert_array_equal(["x0_c❤t1", "x0_dat2"], feature_names)
    feature_names = enc.get_feature_names_out(input_features=["n👍me"])
    assert_array_equal(["n👍me_c❤t1", "n👍me_dat2"], feature_names)

def test_one_hot_encoder_custom_feature_name_combiner():
    """Check the behaviour of `feature_name_combiner` as a callable."""

    def name_combiner(feature, category):
        return feature + "_" + repr(category)

    enc = OneHotEncoder(feature_name_combiner=name_combiner)
    X = np.array([["None", None]], dtype=object).T
    assert_array_equal(["x0_'None'", "x0_None"], feature_names)
    feature_names = enc.get_feature_names_out(input_features=["a"])
    assert_array_equal(["a_'None'", "a_None"], feature_names)

    def wrong_combiner(feature, category):
        # we should be returning a Python string
        return 0

    enc = OneHotEncoder(feature_name_combiner=wrong_combiner).fit(X)
    err_msg = (
        "When `feature_name_combiner` is a callable, it should return a Python string."
    )
        enc.get_feature_names_out()

def test_one_hot_encoder_set_params():
    X = np.array([[1, 2]]).T
    oh = OneHotEncoder()
    # set params on not yet fitted object
    oh.set_params(categories=[[0, 1, 2, 3]])
    assert oh.get_params()["categories"] == [[0, 1, 2, 3]]
    assert oh.fit_transform(X).toarray().shape == (2, 4)
    # set params on already fitted object
    oh.set_params(categories=[[0, 1, 2, 3, 4]])
    assert oh.fit_transform(X).toarray().shape == (2, 5)

def check_categorical_onehot(X):
    enc = OneHotEncoder(categories="auto")
    Xtr1 = enc.fit_transform(X)

    enc = OneHotEncoder(categories="auto", sparse_output=False)
    Xtr2 = enc.fit_transform(X)

    assert_allclose(Xtr1.toarray(), Xtr2)

    assert sparse.issparse(Xtr1) and Xtr1.format == "csr"
    return Xtr1.toarray()

def test_one_hot_encoder(X):
    Xtr = check_categorical_onehot(np.array(X)[:, [0]])
    assert_allclose(Xtr, [[0, 1], [1, 0]])

    Xtr = check_categorical_onehot(np.array(X)[:, [0, 1]])
    assert_allclose(Xtr, [[0, 1, 1, 0], [1, 0, 0, 1]])

    Xtr = OneHotEncoder(categories="auto").fit_transform(X)
    assert_allclose(Xtr.toarray(), [[0, 1, 1, 0, 1], [1, 0, 0, 1, 1]])

def test_one_hot_encoder_inverse(handle_unknown, sparse_, drop):
    X = [["abc", 2, 55], ["def", 1, 55], ["abc", 3, 55]]
    enc = OneHotEncoder(sparse_output=sparse_, drop=drop)
    X_tr = enc.fit_transform(X)
    exp = np.array(X, dtype=object)
    assert_array_equal(enc.inverse_transform(X_tr), exp)

    X = [[2, 55], [1, 55], [3, 55]]
    enc = OneHotEncoder(sparse_output=sparse_, categories="auto", drop=drop)
    exp = np.array(X)

    if drop is None:
        # with unknown categories
        # drop is incompatible with handle_unknown=ignore
        X = [["abc", 2, 55], ["def", 1, 55], ["abc", 3, 55]]
        enc = OneHotEncoder(
            sparse_output=sparse_,
            categories=[["abc", "def"], [1, 2], [54, 55, 56]],
        )
        X_tr = enc.fit_transform(X)
        exp = np.array(X, dtype=object)
        exp[2, 1] = None
        assert_array_equal(enc.inverse_transform(X_tr), exp)

        # with an otherwise numerical output, still object if unknown
        X = [[2, 55], [1, 55], [3, 55]]
            categories=[[1, 2], [54, 56]],
        )
        exp[2, 0] = None
        exp[:, 1] = None

    # incorrect shape raises
    X_tr = np.array([[0, 1, 1], [1, 0, 1]])
    msg = re.escape("Shape of the passed X data is not correct")
        enc.inverse_transform(X_tr)

def test_one_hot_encoder_inverse_transform_raise_error_with_unknown(
    X, X_trans, sparse_
):
    """Check that `inverse_transform` raise an error with unknown samples, no
    dropped feature, and `handle_unknow="error`.
    https://github.com/scikit-learn/scikit-learn/issues/14934
    """
    enc = OneHotEncoder(sparse_output=sparse_).fit(X)
    msg = (
        r"Samples \[(\d )*\d\] can not be inverted when drop=None and "
        r"handle_unknown='error' because they contain all zeros"
    )

    if sparse_:
        # emulate sparse data transform by a one-hot encoder sparse.
        X_trans = _convert_container(X_trans, "sparse")
        enc.inverse_transform(X_trans)

def test_one_hot_encoder_inverse_if_binary():
    X = np.array([["Male", 1], ["Female", 3], ["Female", 2]], dtype=object)
    ohe = OneHotEncoder(drop="if_binary", sparse_output=False)
    X_tr = ohe.fit_transform(X)
    assert_array_equal(ohe.inverse_transform(X_tr), X)

def test_one_hot_encoder_drop_reset(drop, reset_drop):
    # check that resetting drop option without refitting does not throw an error
    ohe = OneHotEncoder(drop=drop, sparse_output=False)
    ohe.fit(X)
    X_tr = ohe.transform(X)
    feature_names = ohe.get_feature_names_out()
    ohe.set_params(drop=reset_drop)
    assert_allclose(ohe.transform(X), X_tr)
    assert_array_equal(ohe.get_feature_names_out(), feature_names)

def test_X_is_not_1D(X, method):

        getattr(oh, method)(X)

def test_X_is_not_1D_pandas(method):
    X = pd.Series([6, 3, 4, 6])

    msg = f"Expected a 2-dimensional container but got {type(X)} instead."

def test_one_hot_encoder_categories(X, cat_exp, cat_dtype):
    # order of categories should not depend on order of samples
    for Xi in [X, X[::-1]]:
        enc = OneHotEncoder(categories="auto")
        enc.fit(Xi)
        # assert enc.categories == 'auto'
        assert isinstance(enc.categories_, list)
        for res, exp in zip(enc.categories_, cat_exp):
            res_list = res.tolist()
            if is_scalar_nan(exp[-1]):
                assert is_scalar_nan(res_list[-1])
                assert res_list[:-1] == exp[:-1]
            else:
                assert res.tolist() == exp
            assert np.issubdtype(res.dtype, cat_dtype)

def test_one_hot_encoder_specified_categories(X, X2, cats, cat_dtype, handle_unknown):
    enc = OneHotEncoder(categories=cats)
    exp = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
    assert_array_equal(enc.fit_transform(X).toarray(), exp)
    assert list(enc.categories[0]) == list(cats[0])
    assert enc.categories_[0].tolist() == list(cats[0])
    # manually specified categories should have same dtype as
    # the data when coerced from lists
    assert enc.categories_[0].dtype == cat_dtype

    # when specifying categories manually, unknown categories should already
    # raise when fitting
        enc.fit(X2)
    enc = OneHotEncoder(categories=cats, handle_unknown=handle_unknown)
    exp = np.array([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
    assert_array_equal(enc.fit(X2).transform(X2).toarray(), exp)

def test_one_hot_encoder_unsorted_categories():
    X = np.array([["a", "b"]], dtype=object).T

    enc = OneHotEncoder(categories=[["b", "a", "c"]])
    exp = np.array([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0]])
    assert_array_equal(enc.fit(X).transform(X).toarray(), exp)
    assert enc.categories_[0].tolist() == ["b", "a", "c"]
    assert np.issubdtype(enc.categories_[0].dtype, np.object_)

    # unsorted passed categories still raise for numerical values
    enc = OneHotEncoder(categories=[[2, 1, 3]])
    msg = "Unsorted categories are not supported"
        enc.fit_transform(X)

def test_encoder_nan_ending_specified_categories(Encoder):
    """Test encoder for specified categories that nan is at the end.

    https://github.com/scikit-learn/scikit-learn/issues/27088
    """
    cats = [np.array([0, np.nan, 1])]
    enc = Encoder(categories=cats)
    X = np.array([[0, 1]], dtype=object).T
    with pytest.raises(ValueError, match="Nan should be the last element"):
        enc.fit(X)

def test_one_hot_encoder_specified_categories_mixed_columns():
    # multiple columns
    X = np.array([["a", "b"], [0, 2]], dtype=object).T
    enc = OneHotEncoder(categories=[["a", "b", "c"], [0, 1, 2]])
    exp = np.array([[1.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 1.0]])
    assert enc.categories_[0].tolist() == ["a", "b", "c"]
    assert enc.categories_[1].tolist() == [0, 1, 2]
    # integer categories but from object dtype data
    assert np.issubdtype(enc.categories_[1].dtype, np.object_)

def test_one_hot_encoder_pandas():

    Xtr = check_categorical_onehot(X_df)
    assert_allclose(Xtr, [[1, 0, 1, 0], [0, 1, 0, 1]])

def test_one_hot_encoder_feature_names_drop(drop, expected_names):
    X = [["c", 2, "a"], ["b", 2, "b"]]

    ohe = OneHotEncoder(drop=drop)

def test_one_hot_encoder_drop_equals_if_binary():
    # Canonical case
    X = [[10, "yes"], [20, "no"], [30, "yes"]]
    expected = np.array(
        [[1.0, 0.0, 0.0, 1.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 1.0]]
    )
    expected_drop_idx = np.array([None, 0])

    result = ohe.fit_transform(X)
    assert_array_equal(ohe.drop_idx_, expected_drop_idx)

    # with only one cat, the behaviour is equivalent to drop=None
    X = [["true", "a"], ["false", "a"], ["false", "a"]]
    expected = np.array([[1.0, 1.0], [0.0, 1.0], [0.0, 1.0]])
    expected_drop_idx = np.array([0, None])

def test_ordinal_encoder(X):
    enc = OrdinalEncoder()
    exp = np.array([[0, 1, 0], [1, 0, 0]], dtype="int64")
    assert_array_equal(enc.fit_transform(X), exp.astype("float64"))
    enc = OrdinalEncoder(dtype="int64")
    assert_array_equal(enc.fit_transform(X), exp)

def test_ordinal_encoder_specified_categories(X, X2, cats, cat_dtype):
    enc = OrdinalEncoder(categories=cats)
    exp = np.array([[0.0], [1.0]])

def test_ordinal_encoder_inverse():
    X = [["abc", 2, 55], ["def", 1, 55]]

    X_tr = np.array([[0, 1, 1, 2], [1, 0, 1, 0]])

def test_ordinal_encoder_handle_unknowns_string():
    enc = OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-2)
    X_fit = np.array([["a", "x"], ["b", "y"], ["c", "z"]], dtype=object)
    X_trans = np.array([["c", "xy"], ["bla", "y"], ["a", "x"]], dtype=object)
    enc.fit(X_fit)

    X_trans_enc = enc.transform(X_trans)
    exp = np.array([[2, -2], [-2, 1], [0, 0]], dtype="int64")
    assert_array_equal(X_trans_enc, exp)

    X_trans_inv = enc.inverse_transform(X_trans_enc)
    inv_exp = np.array([["c", None], [None, "y"], ["a", "x"]], dtype=object)
    assert_array_equal(X_trans_inv, inv_exp)

def test_ordinal_encoder_handle_unknowns_numeric(dtype):
    enc = OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-999)
    X_fit = np.array([[1, 7], [2, 8], [3, 9]], dtype=dtype)
    X_trans = np.array([[3, 12], [23, 8], [1, 7]], dtype=dtype)

    exp = np.array([[2, -999], [-999, 1], [0, 0]], dtype="int64")

    inv_exp = np.array([[3, None], [None, 8], [1, 7]], dtype=object)

def test_ordinal_encoder_handle_unknowns_nan():
    # Make sure unknown_value=np.nan properly works

    enc = OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=np.nan)

    X_fit = np.array([[1], [2], [3]])
    X_trans = enc.transform([[1], [2], [4]])
    assert_array_equal(X_trans, [[0], [1], [np.nan]])

def test_ordinal_encoder_handle_unknowns_nan_non_float_dtype():
    # Make sure an error is raised when unknown_value=np.nan and the dtype
    # isn't a float dtype
    enc = OrdinalEncoder(
        handle_unknown="use_encoded_value", unknown_value=np.nan, dtype=int
    )

    with pytest.raises(ValueError, match="dtype parameter should be a float dtype"):
        enc.fit(X_fit)

def test_ordinal_encoder_raise_categories_shape():
    X = np.array([["Low", "Medium", "High", "Medium", "Low"]], dtype=object).T
    cats = ["Low", "Medium", "High"]
    msg = "Shape mismatch: if categories is an array,"

def test_encoder_dtypes():
    # check that dtypes are preserved when determining categories
    exp = np.array([[1.0, 0.0, 1.0, 0.0], [0.0, 1.0, 0.0, 1.0]], dtype="float64")

    for X in [
        np.array([[1, 2], [3, 4]], dtype="int64"),
        np.array([[1, 2], [3, 4]], dtype="float64"),
        np.array([["a", "b"], ["c", "d"]]),  # str dtype
        np.array([[b"a", b"b"], [b"c", b"d"]]),  # bytes dtype
        np.array([[1, "a"], [3, "b"]], dtype="object"),
    ]:
        assert all([enc.categories_[i].dtype == X.dtype for i in range(2)])
        assert_array_equal(enc.transform(X).toarray(), exp)

    assert all([np.issubdtype(enc.categories_[i].dtype, np.integer) for i in range(2)])
    assert_array_equal(enc.transform(X).toarray(), exp)

    X = [[1, "a"], [3, "b"]]
    assert all([enc.categories_[i].dtype == "object" for i in range(2)])

def test_encoder_dtypes_pandas():
    # check dtype (similar to test_categorical_encoder_dtypes for dataframes)

    exp = np.array(
        [[1.0, 0.0, 1.0, 0.0, 1.0, 0.0], [0.0, 1.0, 0.0, 1.0, 0.0, 1.0]],
        dtype="float64",
    )

    X = pd.DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}, dtype="int64")
    assert all([enc.categories_[i].dtype == "int64" for i in range(2)])

    X = pd.DataFrame({"A": [1, 2], "B": ["a", "b"], "C": [3.0, 4.0]})
    X_type = [X["A"].dtype, X["B"].dtype, X["C"].dtype]
    assert all([enc.categories_[i].dtype == X_type[i] for i in range(3)])

def test_one_hot_encoder_warning():
    X = [["Male", 1], ["Female", 3]]

def test_ohe_handle_unknown_warn(drop):
    """Check handle_unknown='warn' works correctly."""

    X = [["a", 0], ["b", 2], ["b", 1]]

    ohe = OneHotEncoder(
        drop=drop,
        handle_unknown="warn",
        categories=[["b", "a"], [1, 2]],
    )

    X_test = [["c", 1]]
    X_expected = np.array([[0, 0]])

        r"Found unknown categories in columns \[0\] during transform. "
        r"These unknown categories will be encoded as all zeros"
    )
        X_trans = ohe.transform(X_test)
    assert_allclose(X_trans, X_expected)

def test_one_hot_encoder_drop_manual(missing_value):
    cats_to_drop = ["def", 12, 3, 56, missing_value]
    enc = OneHotEncoder(drop=cats_to_drop)
    X = [
        ["abc", 12, 2, 55, "a"],
        ["def", 12, 1, 55, "a"],
        ["def", 12, 3, 56, missing_value],
    ]
    trans = enc.fit_transform(X).toarray()
    exp = [[1, 0, 1, 1, 1], [0, 1, 0, 1, 1], [0, 0, 0, 0, 0]]
    assert_array_equal(trans, exp)
    assert enc.drop is cats_to_drop

    dropped_cats = [
        cat[feature] for cat, feature in zip(enc.categories_, enc.drop_idx_)
    ]
    X_inv_trans = enc.inverse_transform(trans)
    X_array = np.array(X, dtype=object)

    # last value is np.nan
    if is_scalar_nan(cats_to_drop[-1]):
        assert_array_equal(dropped_cats[:-1], cats_to_drop[:-1])
        assert is_scalar_nan(dropped_cats[-1])
        assert is_scalar_nan(cats_to_drop[-1])
        # do not include the last column which includes missing values
        assert_array_equal(X_array[:, :-1], X_inv_trans[:, :-1])

        # check last column is the missing value
        assert_array_equal(X_array[-1, :-1], X_inv_trans[-1, :-1])
        assert is_scalar_nan(X_array[-1, -1])
        assert is_scalar_nan(X_inv_trans[-1, -1])
    else:
        assert_array_equal(dropped_cats, cats_to_drop)
        assert_array_equal(X_array, X_inv_trans)

def test_invalid_drop_length(drop):
    enc = OneHotEncoder(drop=drop)
    err_msg = "`drop` should have length equal to the number"
        enc.fit([["abc", 2, 55], ["def", 1, 55], ["def", 3, 59]])

def test_categories(density, drop):
    ohe_base = OneHotEncoder(sparse_output=density)
    ohe_test = OneHotEncoder(sparse_output=density, drop=drop)
    X = [["c", 1, "a"], ["a", 2, "b"]]
    ohe_base.fit(X)
    ohe_test.fit(X)
    assert_array_equal(ohe_base.categories_, ohe_test.categories_)
    if drop == "first":
        assert_array_equal(ohe_test.drop_idx_, 0)
    else:
        for drop_cat, drop_idx, cat_list in zip(
            drop, ohe_test.drop_idx_, ohe_test.categories_
        ):
            assert cat_list[int(drop_idx)] == drop_cat
    assert isinstance(ohe_test.drop_idx_, np.ndarray)
    assert ohe_test.drop_idx_.dtype == object

def test_encoders_has_categorical_tags(Encoder):
    assert Encoder().__sklearn_tags__().input_tags.categorical

def test_ohe_infrequent_two_levels(kwargs, categories):
    """Test that different parameters for combine 'a', 'c', and 'd' into
    the infrequent category works as expected."""

    X_train = np.array([["a"] * 5 + ["b"] * 20 + ["c"] * 10 + ["d"] * 3]).T
        handle_unknown="infrequent_if_exist",
        **kwargs,
    assert_array_equal(ohe.infrequent_categories_, [["a", "c", "d"]])

    X_test = [["b"], ["a"], ["c"], ["d"], ["e"]]
    expected = np.array([[1, 0], [0, 1], [0, 1], [0, 1], [0, 1]])

    X_trans = ohe.transform(X_test)
    assert_allclose(expected, X_trans)

    expected_inv = [[col] for col in ["b"] + ["infrequent_sklearn"] * 4]
    X_inv = ohe.inverse_transform(X_trans)
    assert_array_equal(expected_inv, X_inv)

    assert_array_equal(["x0_b", "x0_infrequent_sklearn"], feature_names)

def test_ohe_infrequent_two_levels_drop_frequent(drop):
    """Test two levels and dropping the frequent category."""

        max_categories=2,
    assert ohe.categories_[0][ohe.drop_idx_[0]] == "b"

    X_test = np.array([["b"], ["c"]])
    assert_allclose([[0], [1]], X_trans)

    assert_array_equal(["x0_infrequent_sklearn"], feature_names)

    X_inverse = ohe.inverse_transform(X_trans)
    assert_array_equal([["b"], ["infrequent_sklearn"]], X_inverse)

def test_ohe_infrequent_two_levels_drop_infrequent_errors(drop):
    """Test two levels and dropping any infrequent category removes the
    whole infrequent category."""

    )

    msg = f"Unable to drop category {drop[0]!r} from feature 0 because it is infrequent"
        ohe.fit(X_train)

def test_ohe_infrequent_three_levels(kwargs):
    """Test that different parameters for combing 'a', and 'd' into

        handle_unknown="infrequent_if_exist", sparse_output=False, **kwargs
    assert_array_equal(ohe.infrequent_categories_, [["a", "d"]])

    expected = np.array([[1, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 1], [0, 0, 1]])

    expected_inv = [
        ["b"],
        ["infrequent_sklearn"],
        ["c"],
    ]

    assert_array_equal(["x0_b", "x0_c", "x0_infrequent_sklearn"], feature_names)

def test_ohe_infrequent_three_levels_drop_frequent(drop):
    """Test three levels and dropping the frequent category."""

        max_categories=3,

    X_test = np.array([["b"], ["c"], ["d"]])
    assert_allclose([[0, 0], [1, 0], [0, 1]], ohe.transform(X_test))

    # Check handle_unknown="ignore"
    ohe.set_params(handle_unknown="ignore").fit(X_train)
    msg = "Found unknown categories"
        X_trans = ohe.transform([["b"], ["e"]])

    assert_allclose([[0, 0], [0, 0]], X_trans)

def test_ohe_infrequent_three_levels_drop_infrequent_errors(drop):
    """Test three levels and dropping the infrequent category."""
    )

def test_ohe_infrequent_handle_unknown_error():
    """Test that different parameters for combining 'a', and 'd' into

        handle_unknown="error", sparse_output=False, max_categories=3

    # all categories are known
    X_test = [["b"], ["a"], ["c"], ["d"]]
    expected = np.array([[1, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 1]])

    # 'bad' is not known and will error
    X_test = [["bad"]]
    msg = r"Found unknown categories \['bad'\] in column 0"
        ohe.transform(X_test)

def test_ohe_infrequent_two_levels_user_cats_one_frequent(kwargs):
    """'a' is the only frequent category, all other categories are infrequent."""

    X_train = np.array([["a"] * 5 + ["e"] * 30], dtype=object).T
        categories=[["c", "d", "a", "b"]],
        **kwargs,

    X_test = [["a"], ["b"], ["c"], ["d"], ["e"]]

    # 'a' is dropped
    drops = ["first", "if_binary", ["a"]]
    X_test = [["a"], ["c"]]
    for drop in drops:
        ohe.set_params(drop=drop).fit(X_train)
        assert_allclose([[0], [1]], ohe.transform(X_test))

def test_ohe_infrequent_two_levels_user_cats():
    """Test that the order of the categories provided by a user is respected."""
        [["a"] * 5 + ["b"] * 20 + ["c"] * 10 + ["d"] * 3], dtype=object
    ).T

    assert_array_equal(ohe.infrequent_categories_, [["c", "d", "a"]])

    # 'infrequent' is used to denote the infrequent categories for
    # `inverse_transform`

def test_ohe_infrequent_three_levels_user_cats():
    """Test that the order of the categories provided by a user is respected.
    In this case 'c' is encoded as the first category and 'b' is encoded
    as the second one."""

    ).T
        categories=[["c", "d", "b", "a"]],

    assert_array_equal(ohe.infrequent_categories_, [["d", "a"]])

    expected = np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0], [0, 0, 1], [0, 0, 1]])

        ["b"],
        ["c"],
    ]

def test_ohe_infrequent_mixed():
    """Test infrequent categories where feature 0 has infrequent categories,
    and feature 1 does not."""

    # X[:, 0] 1 and 2 are infrequent
    # X[:, 1] nothing is infrequent
    X = np.c_[[0, 1, 3, 3, 3, 3, 2, 0, 3], [0, 0, 0, 0, 1, 1, 1, 1, 1]]

    ohe = OneHotEncoder(max_categories=3, drop="if_binary", sparse_output=False)

    X_test = [[3, 0], [1, 1]]

    # feature 1 is binary so it drops a category 0
    assert_allclose(X_trans, [[0, 1, 0, 0], [0, 0, 1, 1]])

def test_ohe_infrequent_multiple_categories():
    """Test infrequent categories with feature matrix with 3 features."""

    X = np.c_[
        [0, 1, 3, 3, 3, 3, 2, 0, 3],
        [0, 0, 5, 1, 1, 10, 5, 5, 0],
        [1, 0, 1, 0, 1, 0, 1, 0, 1],
    ]

        categories="auto", max_categories=3, handle_unknown="infrequent_if_exist"
    )
    # X[:, 1] 1 and 10 are infrequent
    # X[:, 2] nothing is infrequent

    X_trans = ohe.fit_transform(X).toarray()
    assert_array_equal(ohe.infrequent_categories_[0], [1, 2])
    assert_array_equal(ohe.infrequent_categories_[1], [1, 10])
    assert_array_equal(ohe.infrequent_categories_[2], None)

    # 'infrequent' is used to denote the infrequent categories
    # For the first column, 1 and 2 have the same frequency. In this case,
    # 1 will be chosen to be the feature name because is smaller lexiconically
        [
            "x0_0",
            "x0_3",
            "x0_infrequent_sklearn",
            "x1_0",
            "x1_5",
            "x1_infrequent_sklearn",
            "x2_0",
            "x2_1",
        ],
    )

        [1, 0, 0, 1, 0, 0, 0, 1],
        [0, 0, 1, 1, 0, 0, 1, 0],
        [0, 1, 0, 0, 1, 0, 0, 1],
        [0, 1, 0, 0, 0, 1, 1, 0],
        [0, 1, 0, 0, 0, 1, 0, 1],
        [0, 0, 1, 0, 1, 0, 0, 1],
        [1, 0, 0, 0, 1, 0, 1, 0],
        [0, 1, 0, 1, 0, 0, 0, 1],
    ]

    X_test = [[3, 1, 2], [4, 0, 3]]

    X_test_trans = ohe.transform(X_test)

    # X[:, 2] does not have an infrequent category, thus it is encoded as all
    # zeros
    expected = [[0, 1, 0, 0, 0, 1, 0, 0], [0, 0, 1, 1, 0, 0, 0, 0]]
    assert_allclose(expected, X_test_trans.toarray())

    X_inv = ohe.inverse_transform(X_test_trans)
    expected_inv = np.array(
        [[3, "infrequent_sklearn", None], ["infrequent_sklearn", 0, None]], dtype=object
    )

    # error for unknown categories
        categories="auto", max_categories=3, handle_unknown="error"
    ).fit(X)

    # only infrequent or known categories
    X_test = [[1, 1, 1], [3, 10, 0]]

    expected = [[0, 0, 1, 0, 0, 1, 0, 1], [0, 1, 0, 0, 0, 1, 1, 0]]

        [["infrequent_sklearn", "infrequent_sklearn", 1], [3, "infrequent_sklearn", 0]],
    )

def test_ohe_infrequent_multiple_categories_dtypes():
    """Test infrequent categories with a pandas dataframe with multiple dtypes."""

        {
            "str": ["a", "f", "c", "f", "f", "a", "c", "b", "b"],
            "int": [5, 3, 0, 10, 10, 12, 0, 3, 5],
        },
        columns=["str", "int"],
    )

    )
    # X[:, 0] 'a', 'b', 'c' have the same frequency. 'a' and 'b' will be
    # considered infrequent because they are greater

    # X[:, 1] 0, 3, 5, 10 has frequency 2 and 12 has frequency 1.
    # 0, 3, 12 will be considered infrequent

    assert_array_equal(ohe.infrequent_categories_[0], ["a", "b"])
    assert_array_equal(ohe.infrequent_categories_[1], [0, 3, 12])

        [0, 0, 1, 1, 0, 0],
        [0, 1, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 1],
        [0, 1, 0, 0, 1, 0],
        [0, 0, 1, 0, 0, 1],
    ]

    X_test = pd.DataFrame({"str": ["b", "f"], "int": [14, 12]}, columns=["str", "int"])

    expected = [[0, 0, 1, 0, 0, 1], [0, 1, 0, 0, 0, 1]]

        [["infrequent_sklearn", "infrequent_sklearn"], ["f", "infrequent_sklearn"]],
    )

    X_test = pd.DataFrame({"str": ["c", "b"], "int": [12, 5]}, columns=["str", "int"])
    X_test_trans = ohe.transform(X_test).toarray()
    expected = [[1, 0, 0, 0, 0, 1], [0, 0, 1, 1, 0, 0]]
    assert_allclose(expected, X_test_trans)

        [["c", "infrequent_sklearn"], ["infrequent_sklearn", 5]], dtype=object
    )

def test_ohe_infrequent_one_level_errors(kwargs):
    """All user provided categories are infrequent."""
    X_train = np.array([["a"] * 5 + ["b"] * 20 + ["c"] * 10 + ["d"] * 2]).T

    )
    ohe.fit(X_train)

    X_trans = ohe.transform([["a"]])
    assert_allclose(X_trans, [[1]])

def test_ohe_infrequent_user_cats_unknown_training_errors(kwargs):

    X_train = np.array([["e"] * 3], dtype=object).T
        **kwargs,

    X_trans = ohe.transform([["a"], ["e"]])
    assert_allclose(X_trans, [[1], [1]])

def test_encoders_string_categories(input_dtype, category_dtype, array_type):
    """Check that encoding work with object, unicode, and byte string dtypes.
    https://github.com/scikit-learn/scikit-learn/issues/15616
    https://github.com/scikit-learn/scikit-learn/issues/15726
    https://github.com/scikit-learn/scikit-learn/issues/19677
    """

    X = np.array([["b"], ["a"]], dtype=input_dtype)
    categories = [np.array(["b", "a"], dtype=category_dtype)]
    ohe = OneHotEncoder(categories=categories, sparse_output=False).fit(X)

    X_test = _convert_container(
        [["a"], ["a"], ["b"], ["a"]], array_type, dtype=input_dtype
    )

    expected = np.array([[0, 1], [0, 1], [1, 0], [0, 1]])
    assert_allclose(X_trans, expected)

    oe = OrdinalEncoder(categories=categories).fit(X)
    X_trans = oe.transform(X_test)

    expected = np.array([[1], [1], [0], [1]])
    assert_array_equal(X_trans, expected)

def test_mixed_string_bytes_categoricals():
    """Check that this mixture of predefined categories and X raises an error.

    Categories defined as bytes can not easily be compared to data that is
    a string.
    """
    # data as unicode
    X = np.array([["b"], ["a"]], dtype="U")
    # predefined categories as bytes
    categories = [np.array(["b", "a"], dtype="S")]
    ohe = OneHotEncoder(categories=categories, sparse_output=False)

        "In column 0, the predefined categories have type 'bytes' which is incompatible"
        " with values of type 'str_'."
    )

        ohe.fit(X)

def test_ohe_missing_values_get_feature_names(missing_value):
    # encoder with missing values with object dtypes
    X = np.array([["a", "b", missing_value, "a", missing_value]], dtype=object).T
    ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore").fit(X)
    names = ohe.get_feature_names_out()
    assert_array_equal(names, ["x0_a", "x0_b", f"x0_{missing_value}"])

def test_ohe_missing_value_support_pandas():
    # check support for pandas with mixed dtypes and missing values
        {
            "col1": ["dog", "cat", None, "cat"],
            "col2": np.array([3, 0, 4, np.nan], dtype=float),
        },
        columns=["col1", "col2"],
    )
    expected_df_trans = np.array(
        [
            [0, 1, 0, 0, 1, 0, 0],
            [1, 0, 0, 1, 0, 0, 0],
            [0, 0, 1, 0, 0, 1, 0],
            [1, 0, 0, 0, 0, 0, 1],
        ]
    )

    Xtr = check_categorical_onehot(df)
    assert_allclose(Xtr, expected_df_trans)

def test_ohe_missing_value_support_pandas_categorical(pd_nan_type, handle_unknown):
    # checks pandas dataframe with categorical features

    pd_missing_value = pd.NA if pd_nan_type == "pd.NA" else np.nan

        {
            "col1": pd.Series(["c", "a", pd_missing_value, "b", "a"], dtype="category"),
        }
    )
        [
            [0, 0, 1, 0],
            [1, 0, 0, 0],
            [0, 0, 0, 1],
        ]
    )

    ohe = OneHotEncoder(sparse_output=False, handle_unknown=handle_unknown)
    df_trans = ohe.fit_transform(df)
    assert_allclose(expected_df_trans, df_trans)

    assert len(ohe.categories_) == 1
    assert_array_equal(ohe.categories_[0][:-1], ["a", "b", "c"])
    assert np.isnan(ohe.categories_[0][-1])

def test_ohe_drop_first_handle_unknown_ignore_warns(handle_unknown):
    """Check drop='first' and handle_unknown='ignore'/'infrequent_if_exist'
    during transform."""

        drop="first", sparse_output=False, handle_unknown=handle_unknown
    )
    X_trans = ohe.fit_transform(X)

    X_expected = np.array(
        [
            [0, 0, 0],
            [1, 0, 1],
            [1, 1, 0],
        ]
    )

    # Both categories are unknown
    X_test = [["c", 3]]
    X_expected = np.array([[0, 0, 0]])

        r"Found unknown categories in columns \[0, 1\] during "
        "transform. These unknown categories will be encoded as all "
        "zeros"
    )

    # inverse_transform maps to None
    X_inv = ohe.inverse_transform(X_expected)
    assert_array_equal(X_inv, np.array([["a", 0]], dtype=object))

def test_ohe_drop_if_binary_handle_unknown_ignore_warns(handle_unknown):
    """Check drop='if_binary' and handle_unknown='ignore' during transform."""

        drop="if_binary", sparse_output=False, handle_unknown=handle_unknown
    )

        [
            [1, 0, 1, 0],
        ]
    )

    X_expected = np.array([[0, 0, 0, 0]])

        "zeros"
    )

    assert_array_equal(X_inv, np.array([["a", None]], dtype=object))

def test_ohe_drop_first_explicit_categories(handle_unknown):
    during fit with categories passed in."""

        drop="first",
        handle_unknown=handle_unknown,
    )

    )

def test_ohe_more_informative_error_message():
    """Raise informative error message when pandas output and sparse_output=True."""
    df = pd.DataFrame({"a": [1, 2, 3], "b": ["z", "b", "b"]}, columns=["a", "b"])

    ohe = OneHotEncoder(sparse_output=True)
    ohe.set_output(transform="pandas")

    msg = (
        "Pandas output does not support sparse data. Set "
        "sparse_output=False to output pandas dataframes or disable Pandas output"
    )
        ohe.fit_transform(df)

    ohe.fit(df)
        ohe.transform(df)

def test_ordinal_encoder_passthrough_missing_values_float_errors_dtype():
    """Test ordinal encoder with nan passthrough fails when dtype=np.int32."""

    X = np.array([[np.nan, 3.0, 1.0, 3.0]]).T
    oe = OrdinalEncoder(dtype=np.int32)

    msg = (
        r"There are missing values in features \[0\]. For OrdinalEncoder "
        f"to encode missing values with dtype: {np.int32}"
    )
        oe.fit(X)

def test_ordinal_encoder_passthrough_missing_values_float(encoded_missing_value):
    """Test ordinal encoder with nan on float dtypes."""

    X = np.array([[np.nan, 3.0, 1.0, 3.0]], dtype=np.float64).T
    oe = OrdinalEncoder(encoded_missing_value=encoded_missing_value).fit(X)

    assert len(oe.categories_) == 1

    assert_allclose(oe.categories_[0], [1.0, 3.0, np.nan])

    X_trans = oe.transform(X)
    assert_allclose(X_trans, [[encoded_missing_value], [1.0], [0.0], [1.0]])

    X_inverse = oe.inverse_transform(X_trans)
    assert_allclose(X_inverse, X)

def test_ordinal_encoder_missing_value_support_pandas_categorical(
    pd_nan_type, encoded_missing_value
):
    """Check ordinal encoder is compatible with pandas."""

        {
        }
    )

    oe = OrdinalEncoder(encoded_missing_value=encoded_missing_value).fit(df)
    assert_array_equal(oe.categories_[0][:3], ["a", "b", "c"])
    assert np.isnan(oe.categories_[0][-1])

    df_trans = oe.transform(df)

    assert_allclose(df_trans, [[2.0], [0.0], [encoded_missing_value], [1.0], [0.0]])

    X_inverse = oe.inverse_transform(df_trans)
    assert X_inverse.shape == (5, 1)
    assert_array_equal(X_inverse[:2, 0], ["c", "a"])
    assert_array_equal(X_inverse[3:, 0], ["b", "a"])
    assert np.isnan(X_inverse[2, 0])

def test_ordinal_encoder_specified_categories_missing_passthrough(
    X, X2, cats, cat_dtype
):
    """Test ordinal encoder for specified categories."""
    oe = OrdinalEncoder(categories=cats)
    exp = np.array([[0.0], [np.nan]])
    assert_array_equal(oe.fit_transform(X), exp)
    assert oe.categories_[0].dtype == cat_dtype

        oe.fit(X2)

def test_encoder_duplicate_specified_categories(Encoder):
    """Test encoder for specified categories have duplicate values.

    """
    cats = [np.array(["a", "b", "a"], dtype=object)]
        ValueError, match="the predefined categories contain duplicate elements."
    ):

def test_ordinal_encoder_handle_missing_and_unknown(X, expected_X_trans, X_test):
    """Test the interaction between missing values and handle_unknown"""

    oe = OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1)

    X_trans = oe.fit_transform(X)
    assert_allclose(X_trans, expected_X_trans)

    assert_allclose(oe.transform(X_test), [[-1.0]])

def test_ordinal_encoder_sparse(csr_container):
    """Check that we raise proper error with sparse input in OrdinalEncoder.
    https://github.com/scikit-learn/scikit-learn/issues/19878
    """

    encoder = OrdinalEncoder()

    err_msg = "Sparse data was passed, but dense data is required"
        encoder.fit(X_sparse)
        encoder.fit_transform(X_sparse)

    X_trans = encoder.fit_transform(X)
    X_trans_sparse = csr_container(X_trans)
        encoder.inverse_transform(X_trans_sparse)

def test_ordinal_encoder_fit_with_unseen_category():
    """Check OrdinalEncoder.fit works with unseen category when
    `handle_unknown="use_encoded_value"`.
    https://github.com/scikit-learn/scikit-learn/issues/19872
    """
    X = np.array([0, 0, 1, 0, 2, 5])[:, np.newaxis]
    oe = OrdinalEncoder(
        categories=[[-1, 0, 1]], handle_unknown="use_encoded_value", unknown_value=-999
    )
    oe.fit(X)

    oe = OrdinalEncoder(categories=[[-1, 0, 1]], handle_unknown="error")
        oe.fit(X)

def test_ordinal_encoder_handle_unknown_string_dtypes(X_train, X_test):
    """Checks that `OrdinalEncoder` transforms string dtypes.
    """
    enc = OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-9)
    enc.fit(X_train)

    X_trans = enc.transform(X_test)
    assert_allclose(X_trans, [[-9, 0]])

def test_ordinal_encoder_python_integer():
    """Check that `OrdinalEncoder` accepts Python integers that are potentially
    larger than 64 bits.
    https://github.com/scikit-learn/scikit-learn/issues/20721
    """
        [
            44253463435747313673,
            9867966753463435747313673,
            44253462342215747313673,
            442534634357764313673,
        ]
    ).reshape(-1, 1)
    encoder = OrdinalEncoder().fit(X)
    assert_array_equal(encoder.categories_, np.sort(X, axis=0).T)
    X_trans = encoder.transform(X)
    assert_array_equal(X_trans, [[0], [3], [2], [1]])

def test_ordinal_encoder_features_names_out_pandas():
    """Check feature names out is same as the input."""

    names = ["b", "c", "a"]
    X = pd.DataFrame([[1, 2, 3]], columns=names)
    enc = OrdinalEncoder().fit(X)

    feature_names_out = enc.get_feature_names_out()
    assert_array_equal(names, feature_names_out)

def test_ordinal_encoder_unknown_missing_interaction():
    """Check interactions between encode_unknown and missing value encoding."""

    X = np.array([["a"], ["b"], [np.nan]], dtype=object)

        handle_unknown="use_encoded_value",
        unknown_value=np.nan,
        encoded_missing_value=-3,
    ).fit(X)

    assert_allclose(X_trans, [[0], [1], [-3]])

    # "c" is unknown and is mapped to np.nan
    # "None" is a missing value and is set to -3
    X_test = np.array([["c"], [np.nan]], dtype=object)
    X_test_trans = oe.transform(X_test)
    assert_allclose(X_test_trans, [[np.nan], [-3]])

    # Non-regression test for #24082
    X_roundtrip = oe.inverse_transform(X_test_trans)

    # np.nan is unknown so it maps to None
    assert X_roundtrip[0][0] is None

    # -3 is the encoded missing value so it maps back to nan
    assert np.isnan(X_roundtrip[1][0])

def test_ordinal_encoder_encoded_missing_value_error(with_pandas):
    """Check OrdinalEncoder errors when encoded_missing_value is used by
    an known category."""
    X = np.array([["a", "dog"], ["b", "cat"], ["c", np.nan]], dtype=object)

    # The 0-th feature has no missing values so it is not included in the list of
    # features
        r"encoded_missing_value \(1\) is already used to encode a known category "
        r"in features: "
    )

    if with_pandas:
        X = pd.DataFrame(X, columns=["letter", "pet"])
        error_msg = error_msg + r"\['pet'\]"
    else:
        error_msg = error_msg + r"\[1\]"

    oe = OrdinalEncoder(encoded_missing_value=1)

        oe.fit(X)

def test_ordinal_encoder_unknown_missing_interaction_both_nan(
    X_train, X_test_trans_expected, X_roundtrip_expected
):
    """Check transform when unknown_value and encoded_missing_value is nan.

    Non-regression test for #24082.
    """

    X_test = np.array([["1"], [np.nan], ["b"]])

    # both nan and unknown are encoded as nan
    assert_allclose(X_test_trans, X_test_trans_expected)

    n_samples = X_roundtrip_expected.shape[0]
        expected_val = X_roundtrip_expected[i, 0]
        val = X_roundtrip[i, 0]

        if expected_val is None:
            assert val is None
        elif is_scalar_nan(expected_val):
            assert np.isnan(val)
        else:
            assert val == expected_val

def test_one_hot_encoder_set_output():
    """Check OneHotEncoder works with set_output."""

    match = "Pandas output does not support sparse data. Set sparse_output=False"
        ohe.fit_transform(X_df)

    ohe_default = OneHotEncoder(sparse_output=False).set_output(transform="default")
    ohe_pandas = OneHotEncoder(sparse_output=False).set_output(transform="pandas")

    X_default = ohe_default.fit_transform(X_df)
    X_pandas = ohe_pandas.fit_transform(X_df)

    assert_allclose(X_pandas.to_numpy(), X_default)
    assert_array_equal(ohe_pandas.get_feature_names_out(), X_pandas.columns)

def test_ordinal_set_output():
    """Check OrdinalEncoder works with set_output."""

    ord_default = OrdinalEncoder().set_output(transform="default")
    ord_pandas = OrdinalEncoder().set_output(transform="pandas")

    X_default = ord_default.fit_transform(X_df)
    X_pandas = ord_pandas.fit_transform(X_df)

    assert_array_equal(ord_pandas.get_feature_names_out(), X_pandas.columns)

def test_predefined_categories_dtype():
    """Check that the categories_ dtype is `object` for string categories

    Regression test for gh-25171.
    """
    categories = [["as", "mmas", "eas", "ras", "acs"], ["1", "2"]]

    enc = OneHotEncoder(categories=categories)

    enc.fit([["as", "1"]])

    assert len(categories) == len(enc.categories_)
    for n, cat in enumerate(enc.categories_):
        assert cat.dtype == object
        assert_array_equal(categories[n], cat)

def test_ordinal_encoder_missing_unknown_encoding_max():
    """Check missing value or unknown encoding can equal the cardinality."""
    X = np.array([["dog"], ["cat"], [np.nan]], dtype=object)
    X_trans = OrdinalEncoder(encoded_missing_value=2).fit_transform(X)
    assert_allclose(X_trans, [[1], [0], [2]])

    enc = OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=2).fit(X)
    X_test = np.array([["snake"]])
    assert_allclose(X_trans, [[2]])

def test_drop_idx_infrequent_categories():
    """Check drop_idx is defined correctly with infrequent categories.

    Non-regression test for gh-25550.
    """
        [["a"] * 2 + ["b"] * 4 + ["c"] * 4 + ["d"] * 4 + ["e"] * 4], dtype=object
    ).T
    ohe = OneHotEncoder(min_frequency=4, sparse_output=False, drop="first").fit(X)
        ohe.get_feature_names_out(), ["x0_c", "x0_d", "x0_e", "x0_infrequent_sklearn"]
    )

    X = np.array([["a"] * 2 + ["b"] * 2 + ["c"] * 10], dtype=object).T
    ohe = OneHotEncoder(min_frequency=4, sparse_output=False, drop="if_binary").fit(X)
    assert_array_equal(ohe.get_feature_names_out(), ["x0_infrequent_sklearn"])
    assert ohe.categories_[0][ohe.drop_idx_[0]] == "c"

    ).T
    ohe = OneHotEncoder(min_frequency=4, sparse_output=False, drop=["d"]).fit(X)
        ohe.get_feature_names_out(), ["x0_b", "x0_c", "x0_e", "x0_infrequent_sklearn"]
    )
    assert ohe.categories_[0][ohe.drop_idx_[0]] == "d"

    ohe = OneHotEncoder(min_frequency=4, sparse_output=False, drop=None).fit(X)
        ohe.get_feature_names_out(),
        ["x0_b", "x0_c", "x0_d", "x0_e", "x0_infrequent_sklearn"],
    )
    assert ohe.drop_idx_ is None

def test_ordinal_encoder_infrequent_three_levels(kwargs):
    """Test parameters for grouping 'a', and 'd' into the infrequent category."""

    ordinal = OrdinalEncoder(
        handle_unknown="use_encoded_value", unknown_value=-1, **kwargs
    assert_array_equal(ordinal.categories_, [["a", "b", "c", "d"]])
    assert_array_equal(ordinal.infrequent_categories_, [["a", "d"]])

    X_test = [["a"], ["b"], ["c"], ["d"], ["z"]]
    expected_trans = [[2], [0], [1], [2], [-1]]

    X_trans = ordinal.transform(X_test)
    assert_allclose(X_trans, expected_trans)

    X_inverse = ordinal.inverse_transform(X_trans)
    expected_inverse = [
        ["b"],
        ["c"],
        [None],
    ]
    assert_array_equal(X_inverse, expected_inverse)

def test_ordinal_encoder_infrequent_three_levels_user_cats():

    as the second one.
    """

    ).T
        unknown_value=-1,
    assert_array_equal(ordinal.categories_, [["c", "d", "b", "a"]])
    assert_array_equal(ordinal.infrequent_categories_, [["d", "a"]])

    expected_trans = [[2], [1], [0], [2], [-1]]

        ["b"],
        ["c"],
        [None],
    ]

def test_ordinal_encoder_infrequent_mixed():
    """Test when feature 0 has infrequent categories and feature 1 does not."""

    X = np.column_stack(([0, 1, 3, 3, 3, 3, 2, 0, 3], [0, 0, 0, 0, 1, 1, 1, 1, 1]))

    ordinal = OrdinalEncoder(max_categories=3).fit(X)

    assert_array_equal(ordinal.infrequent_categories_[0], [1, 2])
    assert ordinal.infrequent_categories_[1] is None

    expected_trans = [[1, 0], [2, 1]]

    expected_inverse = np.array([[3, 0], ["infrequent_sklearn", 1]], dtype=object)

def test_ordinal_encoder_infrequent_multiple_categories_dtypes():
    """Test infrequent categories with a pandas DataFrame with multiple dtypes."""

    categorical_dtype = pd.CategoricalDtype(["bird", "cat", "dog", "snake"])
        {
            "categorical": pd.Series(
                ["dog"] * 4 + ["cat"] * 3 + ["snake"] + ["bird"],
                dtype=categorical_dtype,
            ),
        },
        columns=["str", "int", "categorical"],
    )

    # considered infrequent because they appear first when sorted

    # 0, 3, 12 will be considered infrequent because they appear first when
    # sorted.

    # X[:, 2] "snake" and "bird" or infrequent

    assert_array_equal(ordinal.infrequent_categories_[0], ["a", "b"])
    assert_array_equal(ordinal.infrequent_categories_[1], [0, 3, 12])
    assert_array_equal(ordinal.infrequent_categories_[2], ["bird", "snake"])

    X_test = pd.DataFrame(
        {
            "str": ["a", "b", "f", "c"],
            "int": [12, 0, 10, 5],
                ["cat"] + ["snake"] + ["bird"] + ["dog"],
            ),
        },
    )
    expected_trans = [[2, 2, 0], [2, 2, 2], [1, 1, 2], [0, 0, 1]]

def test_ordinal_encoder_infrequent_custom_mapping():
    """Check behavior of unknown_value and encoded_missing_value with infrequent."""
        [["a"] * 5 + ["b"] * 20 + ["c"] * 10 + ["d"] * 3 + [np.nan]], dtype=object
    ).T

        unknown_value=2,
        encoded_missing_value=3,
    assert_array_equal(ordinal.infrequent_categories_, [["a", "c", "d"]])

    X_test = np.array([["a"], ["b"], ["c"], ["d"], ["e"], [np.nan]], dtype=object)
    expected_trans = [[1], [0], [1], [1], [2], [3]]

def test_ordinal_encoder_all_frequent(kwargs):
    """All categories are considered frequent have same encoding as default encoder."""
    ).T

    adjusted_encoder = OrdinalEncoder(
        **kwargs, handle_unknown="use_encoded_value", unknown_value=-1
    default_encoder = OrdinalEncoder(
        handle_unknown="use_encoded_value", unknown_value=-1

        adjusted_encoder.transform(X_test), default_encoder.transform(X_test)
    )

def test_ordinal_encoder_all_infrequent(kwargs):
    """When all categories are infrequent, they are all encoded as zero."""
    ).T
    encoder = OrdinalEncoder(

    assert_allclose(encoder.transform(X_test), [[0], [0], [0], [0], [-1]])

def test_ordinal_encoder_missing_appears_frequent():
    """Check behavior when missing value appears frequently."""
        [[np.nan] * 20 + ["dog"] * 10 + ["cat"] * 5 + ["snake"] + ["deer"]],
    ).T

    X_test = np.array([["snake", "cat", "dog", np.nan]], dtype=object).T
    assert_allclose(X_trans, [[2], [0], [1], [np.nan]])

def test_ordinal_encoder_missing_appears_infrequent():
    """Check behavior when missing value appears infrequently."""

    # feature 0 has infrequent categories
    # feature 1 has no infrequent categories
        [
            [np.nan] + ["dog"] * 10 + ["cat"] * 5 + ["snake"] + ["deer"],
            ["red"] * 9 + ["green"] * 9,
        ],
    ).T
    ordinal = OrdinalEncoder(min_frequency=4).fit(X)

        [
            ["snake", "red"],
            ["deer", "green"],
            [np.nan, "green"],
            ["dog", "green"],
            ["cat", "red"],
        ],
    )
    assert_allclose(X_trans, [[2, 1], [2, 0], [np.nan, 0], [1, 0], [0, 1]])

def test_encoder_not_fitted(Encoder):
    """Check that we raise a `NotFittedError` by calling transform before fit with
    the encoders.

    One could expect that the passing the `categories` argument to the encoder
    would make it stateless. However, `fit` is making a couple of check, such as the
    position of `np.nan`.
    """
    X = np.array([["A"], ["B"], ["C"]], dtype=object)
    encoder = Encoder(categories=[["A", "B", "C"]])
        encoder.transform(X)

        return 0

def toarray(a):
    if hasattr(a, "toarray"):
        a = a.toarray()
    return a

def test_label_binarizer():
    # one-class case defaults to negative label
    # For dense case:
    inp = ["pos", "pos", "pos", "pos"]
    lb = LabelBinarizer(sparse_output=False)
    expected = np.array([[0, 0, 0, 0]]).T
    got = lb.fit_transform(inp)
    assert_array_equal(lb.classes_, ["pos"])
    assert_array_equal(expected, got)
    assert_array_equal(lb.inverse_transform(got), inp)

    # For sparse case:
    lb = LabelBinarizer(sparse_output=True)
    assert issparse(got)
    assert_array_equal(expected, got.toarray())
    assert_array_equal(lb.inverse_transform(got.toarray()), inp)

    # two-class case
    inp = ["neg", "pos", "pos", "neg"]
    expected = np.array([[0, 1, 1, 0]]).T
    assert_array_equal(lb.classes_, ["neg", "pos"])

    to_invert = np.array([[1, 0], [0, 1], [0, 1], [1, 0]])
    assert_array_equal(lb.inverse_transform(to_invert), inp)

    inp = ["spam", "ham", "eggs", "ham", "0"]
        [[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 1, 0], [1, 0, 0, 0]]
    )
    assert_array_equal(lb.classes_, ["0", "eggs", "ham", "spam"])

def test_label_binarizer_unseen_labels():

    expected = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
    got = lb.fit_transform(["b", "d", "e"])

        [[0, 0, 0], [1, 0, 0], [0, 0, 0], [0, 1, 0], [0, 0, 1], [0, 0, 0]]
    )
    got = lb.transform(["a", "b", "c", "d", "e", "f"])

def test_label_binarizer_set_label_encoding():
    lb = LabelBinarizer(neg_label=-2, pos_label=0)

    # two-class case with pos_label=0
    inp = np.array([0, 1, 1, 0])
    expected = np.array([[-2, 0, 0, -2]]).T

    lb = LabelBinarizer(neg_label=-2, pos_label=2)

    inp = np.array([3, 2, 1, 2, 0])
        [
            [-2, -2, -2, +2],
            [-2, -2, +2, -2],
            [-2, +2, -2, -2],
            [+2, -2, -2, -2],
        ]
    )

def test_label_binarizer_pandas_nullable(dtype, unique_first):
    """Checks that LabelBinarizer works with pandas nullable dtypes.

    """

    if unique_first:
        # Calling unique creates a pandas array which has a different interface
        # compared to a pandas Series. Specifically, pandas arrays do not have "iloc".
        y_true = y_true.unique()
    y_out = lb.transform([1, 0])

    assert_array_equal(y_out, [[1], [0]])

def test_label_binarizer_errors():
    one_class = np.array([0, 0, 0, 0])
    lb = LabelBinarizer().fit(one_class)

    multi_label = [(2, 3), (0,), (0, 2)]
    err_msg = "You appear to be using a legacy multi-label data representation."
        lb.transform(multi_label)

    err_msg = "This LabelBinarizer instance is not fitted yet"
        lb.transform([])
        lb.inverse_transform([])

    input_labels = [0, 1, 0, 1]
    err_msg = "neg_label=2 must be strictly less than pos_label=1."
    lb = LabelBinarizer(neg_label=2, pos_label=1)
        lb.fit(input_labels)
    err_msg = "neg_label=2 must be strictly less than pos_label=2."
    lb = LabelBinarizer(neg_label=2, pos_label=2)
    err_msg = (
        "Sparse binarization is only supported with non zero pos_label and zero "
        "neg_label, got pos_label=2 and neg_label=1"
    )
    lb = LabelBinarizer(neg_label=1, pos_label=2, sparse_output=True)

    # Sequence of seq type should raise ValueError
    y_seq_of_seqs = [[], [1, 2], [3], [0, 1, 3], [2]]
    err_msg = "You appear to be using a legacy multi-label data representation"
        LabelBinarizer().fit_transform(y_seq_of_seqs)

    # Fail on the dimension of 'binary'
    err_msg = "output_type='binary', but y.shape"
        _inverse_binarize_thresholding(
            y=np.array([[1, 2, 3], [2, 1, 3]]),
            output_type="binary",
            classes=[1, 2, 3],
            threshold=0,
        )

    err_msg = "Multioutput target data is not supported with label binarization"
        LabelBinarizer().fit(np.array([[1, 3], [2, 1]]))
        label_binarize(np.array([[1, 3], [2, 1]]), classes=[1, 2, 3])

def test_label_binarizer_sparse_errors(csr_container):
    # Fail on y_type
    err_msg = "foo format is not supported"
            y=csr_container([[1, 2], [2, 1]]),
            output_type="foo",
            classes=[1, 2],
        )

    # Fail on the number of classes
    err_msg = "The number of class is not equal to the number of dimension of y."
        )

def test_label_encoder(values, classes, unknown):
    # Test LabelEncoder's transform, fit_transform and
    # inverse_transform methods
    le.fit(values)
    assert_array_equal(le.classes_, classes)
    assert_array_equal(le.transform(values), [1, 0, 2, 0, 2])
    assert_array_equal(le.inverse_transform([1, 0, 2, 0, 2]), values)
    ret = le.fit_transform(values)
    assert_array_equal(ret, [1, 0, 2, 0, 2])

    with pytest.raises(ValueError, match="unseen labels"):
        le.transform(unknown)

def test_label_encoder_negative_ints():
    le.fit([1, 1, 4, 5, -1, 0])
    assert_array_equal(le.classes_, [-1, 0, 1, 4, 5])
    assert_array_equal(le.transform([0, 1, 4, 4, 5, -1, -1]), [1, 2, 3, 3, 4, 0, 0])
        le.inverse_transform([1, 2, 3, 3, 4, 0, 0]), [0, 1, 4, 4, 5, -1, -1]
    )
        le.transform([0, 6])

def test_label_encoder_str_bad_shape(dtype):
    le.fit(np.array(["apple", "orange"], dtype=dtype))
    msg = "should be a 1d array"
        le.transform("apple")

def test_label_encoder_errors():
        le.transform([])
        le.inverse_transform([])

    # Fail on unseen labels
    le.fit([1, 2, 3, -1, 1])
    msg = "contains previously unseen labels"
        le.inverse_transform([-2])
        le.inverse_transform([-2, -3, -4])

    # Fail on inverse_transform("")
    msg = r"should be a 1d array.+shape \(\)"
        le.inverse_transform("")

def test_label_encoder_empty_array(values):
    # test empty transform
    transformed = le.transform([])
    assert_array_equal(np.array([]), transformed)
    # test empty inverse transform
    inverse_transformed = le.inverse_transform([])
    assert_array_equal(np.array([]), inverse_transformed)

def test_sparse_output_multilabel_binarizer():
    # test input as iterable of iterables
    inputs = [
        lambda: [(2, 3), (1,), (1, 2)],
        lambda: ({2, 3}, {1}, {1, 2}),
        lambda: iter([iter((2, 3)), iter((1,)), {1, 2}]),
    ]
    indicator_mat = np.array([[0, 1, 1], [1, 0, 0], [1, 1, 0]])

    inverse = inputs[0]()
    for sparse_output in [True, False]:
        for inp in inputs:
            # With fit_transform
            mlb = MultiLabelBinarizer(sparse_output=sparse_output)
            got = mlb.fit_transform(inp())
            assert issparse(got) == sparse_output
                # verify CSR assumption that indices and indptr have same dtype
                assert got.indices.dtype == got.indptr.dtype
                got = got.toarray()
            assert_array_equal(indicator_mat, got)
            assert_array_equal([1, 2, 3], mlb.classes_)
            assert mlb.inverse_transform(got) == inverse

            # With fit
            got = mlb.fit(inp()).transform(inp())

def test_sparse_output_multilabel_binarizer_errors(csr_container):
    inp = iter([iter((2, 3)), iter((1,)), {1, 2}])
    mlb = MultiLabelBinarizer(sparse_output=False)
    mlb.fit(inp)
        mlb.inverse_transform(
            csr_container(np.array([[0, 1, 1], [2, 0, 0], [1, 1, 0]]))
        )

def test_multilabel_binarizer():
    inputs = [
    ]
    for inp in inputs:
        # With fit_transform
        mlb = MultiLabelBinarizer()
        got = mlb.fit_transform(inp())
        assert_array_equal(indicator_mat, got)
        assert_array_equal([1, 2, 3], mlb.classes_)
        assert mlb.inverse_transform(got) == inverse

        # With fit
        got = mlb.fit(inp()).transform(inp())

def test_multilabel_binarizer_empty_sample():
    mlb = MultiLabelBinarizer()
    y = [[1, 2], [1], []]
    Y = np.array([[1, 1], [1, 0], [0, 0]])
    assert_array_equal(mlb.fit_transform(y), Y)

def test_multilabel_binarizer_unknown_class():
    y = [[1, 2]]
    Y = np.array([[1, 0], [0, 1]])
    warning_message = "unknown class.* will be ignored"
        matrix = mlb.fit(y).transform([[4, 1], [2, 0]])

    Y = np.array([[1, 0, 0], [0, 1, 0]])
    mlb = MultiLabelBinarizer(classes=[1, 2, 3])
    assert_array_equal(matrix, Y)

def test_multilabel_binarizer_given_classes():
    inp = [(2, 3), (1,), (1, 2)]
    indicator_mat = np.array([[0, 1, 1], [1, 0, 0], [1, 0, 1]])
    # fit_transform()
    mlb = MultiLabelBinarizer(classes=[1, 3, 2])
    assert_array_equal(mlb.fit_transform(inp), indicator_mat)
    assert_array_equal(mlb.classes_, [1, 3, 2])

    # fit().transform()
    assert_array_equal(mlb.fit(inp).transform(inp), indicator_mat)

    # ensure works with extra class
    mlb = MultiLabelBinarizer(classes=[4, 1, 3, 2])
        mlb.fit_transform(inp), np.hstack(([[0], [0], [0]], indicator_mat))
    )
    assert_array_equal(mlb.classes_, [4, 1, 3, 2])

    # ensure fit is no-op as iterable is not consumed
    inp = iter(inp)

    # ensure a ValueError is thrown if given duplicate classes
    err_msg = (
        "The classes argument contains duplicate classes. Remove "
        "these duplicates before passing them to MultiLabelBinarizer."
    )
    mlb = MultiLabelBinarizer(classes=[1, 3, 2, 3])
        mlb.fit(inp)

def test_multilabel_binarizer_multiple_calls():

    indicator_mat2 = np.array([[0, 1, 1], [1, 0, 0], [1, 1, 0]])

    # first call
    # second call change class
    mlb.classes = [1, 2, 3]
    assert_array_equal(mlb.fit_transform(inp), indicator_mat2)

def test_multilabel_binarizer_same_length_sequence():
    # Ensure sequences of the same length are not interpreted as a 2-d array
    inp = [[1], [0], [2]]
    indicator_mat = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]])
    assert_array_equal(mlb.inverse_transform(indicator_mat), inp)

def test_multilabel_binarizer_non_integer_labels():
    tuple_classes = _to_object_array([(1,), (2,), (3,)])
    inputs = [
        ([("2", "3"), ("1",), ("1", "2")], ["1", "2", "3"]),
        ([("b", "c"), ("a",), ("a", "b")], ["a", "b", "c"]),
        ([((2,), (3,)), ((1,),), ((1,), (2,))], tuple_classes),
    ]
    for inp, classes in inputs:
        # fit_transform()
        inp = np.array(inp, dtype=object)
        assert_array_equal(mlb.fit_transform(inp), indicator_mat)
        assert_array_equal(mlb.classes_, classes)
        indicator_mat_inv = np.array(mlb.inverse_transform(indicator_mat), dtype=object)
        assert_array_equal(indicator_mat_inv, inp)

        # fit().transform()
        assert_array_equal(mlb.fit(inp).transform(inp), indicator_mat)

        mlb.fit_transform([({}), ({}, {"a": "b"})])

def test_multilabel_binarizer_non_unique():
    inp = [(1, 1, 1, 0)]
    indicator_mat = np.array([[1, 1]])

def test_multilabel_binarizer_inverse_validation():
    mlb.fit_transform(inp)
    # Not binary
        mlb.inverse_transform(np.array([[1, 3]]))
    # The following binary cases are fine, however
    mlb.inverse_transform(np.array([[0, 0]]))
    mlb.inverse_transform(np.array([[1, 1]]))
    mlb.inverse_transform(np.array([[1, 0]]))

    # Wrong shape
        mlb.inverse_transform(np.array([[1]]))
        mlb.inverse_transform(np.array([[1, 1, 1]]))

def test_label_binarize_with_class_order():
    out = label_binarize([1, 6], classes=[1, 2, 4, 6])
    expected = np.array([[1, 0, 0, 0], [0, 0, 0, 1]])
    assert_array_equal(out, expected)

    # Modified class order
    out = label_binarize([1, 6], classes=[1, 6, 4, 2])
    expected = np.array([[1, 0, 0, 0], [0, 1, 0, 0]])

    out = label_binarize([0, 1, 2, 3], classes=[3, 2, 0, 1])
    expected = np.array([[0, 0, 1, 0], [0, 0, 0, 1], [0, 1, 0, 0], [1, 0, 0, 0]])

def check_binarized_results(y, classes, pos_label, neg_label, expected):
        if (pos_label == 0 or neg_label != 0) and sparse_output:
                label_binarize(
                    y,
                    classes=classes,
                    neg_label=neg_label,
                    pos_label=pos_label,
                    sparse_output=sparse_output,
                )
            continue

        # check label_binarize
        binarized = label_binarize(
            y,
            neg_label=neg_label,
            sparse_output=sparse_output,
        )
        assert_array_equal(toarray(binarized), expected)
        assert issparse(binarized) == sparse_output

        # check inverse
        y_type = type_of_target(y)
        if y_type == "multiclass":
            inversed = _inverse_binarize_multiclass(binarized, classes=classes)

        else:
            inversed = _inverse_binarize_thresholding(
                binarized,
                output_type=y_type,
                threshold=((neg_label + pos_label) / 2.0),
            )

        assert_array_equal(toarray(inversed), toarray(y))

        # Check label binarizer
        lb = LabelBinarizer(
            neg_label=neg_label, pos_label=pos_label, sparse_output=sparse_output
        )
        binarized = lb.fit_transform(y)
        inverse_output = lb.inverse_transform(binarized)
        assert_array_equal(toarray(inverse_output), toarray(y))
        assert issparse(inverse_output) == issparse(y)

def test_label_binarize_binary():
    y = [0, 1, 0]
    classes = [0, 1]
    pos_label = 2
    neg_label = -1
    expected = np.array([[2, -1], [-1, 2], [2, -1]])[:, 1].reshape((-1, 1))

    check_binarized_results(y, classes, pos_label, neg_label, expected)

    # Binary case where sparse_output = True will not result in a ValueError
    y = [0, 1, 0]
    pos_label = 3
    neg_label = 0
    expected = np.array([[3, 0], [0, 3], [3, 0]])[:, 1].reshape((-1, 1))

def test_label_binarize_multiclass():
    y = [0, 1, 2]
    classes = [0, 1, 2]
    expected = 2 * np.eye(3)

        label_binarize(
            y, classes=classes, neg_label=-1, pos_label=pos_label, sparse_output=True
        )

def test_label_binarize_multilabel(arr_type):
    y_ind = np.array([[0, 1, 0], [1, 1, 1], [0, 0, 0]])
    expected = pos_label * y_ind
    y = arr_type(y_ind)

        )

def test_invalid_input_label_binarize():
        label_binarize([0, 2], classes=[0, 2], pos_label=0, neg_label=1)
    with pytest.raises(ValueError, match="continuous target data is not "):
        label_binarize([1.2, 2.7], classes=[0, 1])
    with pytest.raises(ValueError, match="mismatch with the labels"):
        label_binarize([[1, 3]], classes=[1, 2, 3])

def test_inverse_binarize_multiclass(csr_container):
    got = _inverse_binarize_multiclass(
        csr_container([[0, 1, 0], [-1, 0, -1], [0, 0, 0]]), np.arange(3)
    )
    assert_array_equal(got, np.array([1, 1, 0]))

def test_nan_label_encoder():
    """Check that label encoder encodes nans in transform.

    Non-regression test for #22628.
    """
    le.fit(["a", "a", "b", np.nan])

    y_trans = le.transform([np.nan])
    assert_array_equal(y_trans, [2])

def test_label_encoders_do_not_have_set_output(encoder):
    """Check that label encoders do not define set_output and work with y as a kwarg.

    Non-regression test for #26854.
    """
    assert not hasattr(encoder, "set_output")
    y_encoded_with_kwarg = encoder.fit_transform(y=["a", "b", "c"])
    y_encoded_positional = encoder.fit_transform(["a", "b", "c"])
    assert_array_equal(y_encoded_with_kwarg, y_encoded_positional)

def test_label_encoder_array_api_compliance(y, array_namespace, device, dtype):
    xp_y = xp.asarray(y, device=device)
        xp_label = LabelEncoder()
        np_label = LabelEncoder()
        xp_label = xp_label.fit(xp_y)
        xp_transformed = xp_label.transform(xp_y)
        xp_inv_transformed = xp_label.inverse_transform(xp_transformed)
        np_label = np_label.fit(y)
        np_transformed = np_label.transform(y)
        assert get_namespace(xp_transformed)[0].__name__ == xp.__name__
        assert get_namespace(xp_inv_transformed)[0].__name__ == xp.__name__
        assert get_namespace(xp_label.classes_)[0].__name__ == xp.__name__
        assert_array_equal(_convert_to_numpy(xp_transformed, xp), np_transformed)
        assert_array_equal(_convert_to_numpy(xp_inv_transformed, xp), y)
        assert_array_equal(_convert_to_numpy(xp_label.classes_, xp), np_label.classes_)

        xp_transformed = xp_label.fit_transform(xp_y)
        np_transformed = np_label.fit_transform(y)

def test_fit_transform(strategy, quantile_method, expected, sample_weight):
    est = KBinsDiscretizer(
        n_bins=3, encode="ordinal", strategy=strategy, quantile_method=quantile_method
    )
    with ignore_warnings(category=UserWarning):
        # Ignore the warning on removed small bins.
        est.fit(X, sample_weight=sample_weight)
    assert_array_equal(est.transform(X), expected)

def test_valid_n_bins():
    KBinsDiscretizer(n_bins=2, quantile_method="averaged_inverted_cdf").fit_transform(X)
    KBinsDiscretizer(
        n_bins=np.array([2])[0], quantile_method="averaged_inverted_cdf"
    assert KBinsDiscretizer(n_bins=2, quantile_method="averaged_inverted_cdf").fit(
        X
    ).n_bins_.dtype == np.dtype(int)

def test_invalid_n_bins_array():
    # Bad shape
    n_bins = np.full((2, 4), 2.0)
    est = KBinsDiscretizer(n_bins=n_bins, quantile_method="averaged_inverted_cdf")
    err_msg = r"n_bins must be a scalar or array of shape \(n_features,\)."

    # Incorrect number of features
    n_bins = [1, 2, 2]

    # Bad bin values
    n_bins = [1, 2, 2, 1]
    err_msg = (
        "KBinsDiscretizer received an invalid number of bins "
        "at indices 0, 3. Number of bins must be at least 2, "
        "and must be an int."
    )

    # Float bin values
    n_bins = [2.1, 2, 2.1, 2]
    err_msg = (
        "at indices 0, 2. Number of bins must be at least 2, "
    )

def test_fit_transform_n_bins_array(strategy, quantile_method, expected, sample_weight):
        n_bins=[2, 3, 3, 3],
        quantile_method=quantile_method,

    # test the shape of bin_edges_
    n_features = np.array(X).shape[1]
    assert est.bin_edges_.shape == (n_features,)
    for bin_edges, n_bins in zip(est.bin_edges_, est.n_bins_):
        assert bin_edges.shape == (n_bins + 1,)

def test_kbinsdiscretizer_effect_sample_weight():
    """Check the impact of `sample_weight` one computed quantiles."""
    X = np.array([[-2], [-1], [1], [3], [500], [1000]])
    # add a large number of bins such that each sample with a non-null weight
    # will be used as bin edge
        n_bins=10,
    )
    est.fit(X, sample_weight=[1, 1, 1, 1, 0, 0])
    assert_allclose(est.bin_edges_[0], [-2, -1, 0, 1, 3])
    assert_allclose(est.transform(X), [[0.0], [1.0], [3.0], [3.0], [3.0], [3.0]])

def test_kbinsdiscretizer_no_mutating_sample_weight(strategy):
    """Make sure that `sample_weight` is not changed in place."""

    if strategy == "quantile":
        est = KBinsDiscretizer(
            n_bins=3,
            encode="ordinal",
            quantile_method="averaged_inverted_cdf",
        )
    else:
        est = KBinsDiscretizer(n_bins=3, encode="ordinal", strategy=strategy)
    sample_weight = np.array([1, 3, 1, 2], dtype=np.float64)
    sample_weight_copy = np.copy(sample_weight)
    assert_allclose(sample_weight, sample_weight_copy)

def test_same_min_max(strategy):
    X = np.array([[1, -2], [1, -1], [1, 0], [1, 1]])
            n_bins=3,
        )
    else:
        est = KBinsDiscretizer(strategy=strategy, n_bins=3, encode="ordinal")
    warning_message = "Feature 0 is constant and will be replaced with 0."
        est.fit(X)
    assert est.n_bins_[0] == 1
    # replace the feature with zeros
    Xt = est.transform(X)
    assert_array_equal(Xt[:, 0], np.zeros(X.shape[0]))

def test_transform_1d_behavior():
    X = np.arange(4)
    est = KBinsDiscretizer(n_bins=2, quantile_method="averaged_inverted_cdf")

    est.fit(X.reshape(-1, 1))

def test_numeric_stability(i):
    X_init = np.array([2.0, 4.0, 6.0, 8.0, 10.0]).reshape(-1, 1)
    Xt_expected = np.array([0, 0, 1, 1, 1]).reshape(-1, 1)

    # Test up to discretizing nano units
    X = X_init / 10**i
    Xt = KBinsDiscretizer(
        n_bins=2, encode="ordinal", quantile_method="averaged_inverted_cdf"
    assert_array_equal(Xt_expected, Xt)

def test_encode_options():
        n_bins=[2, 3, 3, 3], encode="ordinal", quantile_method="averaged_inverted_cdf"
    ).fit(X)
    Xt_1 = est.transform(X)
        encode="onehot-dense",
    ).fit(X)
    Xt_2 = est.transform(X)
    assert not sp.issparse(Xt_2)
        OneHotEncoder(
            categories=[np.arange(i) for i in [2, 3, 3, 3]], sparse_output=False
        ).fit_transform(Xt_1),
        Xt_2,
    )
        n_bins=[2, 3, 3, 3], encode="onehot", quantile_method="averaged_inverted_cdf"
    ).fit(X)
    Xt_3 = est.transform(X)
    assert sp.issparse(Xt_3)
            categories=[np.arange(i) for i in [2, 3, 3, 3]], sparse_output=True
        )
        .fit_transform(Xt_1)
        .toarray(),
        Xt_3.toarray(),
    )

def test_nonuniform_strategies(
    strategy, quantile_method, expected_2bins, expected_3bins, expected_5bins
):
    X = np.array([0, 0.5, 2, 3, 9, 10]).reshape(-1, 1)

    # with 2 bins
        n_bins=2, strategy=strategy, quantile_method=quantile_method, encode="ordinal"
    )
    Xt = est.fit_transform(X)
    assert_array_equal(expected_2bins, Xt.ravel())

    # with 3 bins
        n_bins=3, strategy=strategy, quantile_method=quantile_method, encode="ordinal"
    )
    assert_array_equal(expected_3bins, Xt.ravel())

    # with 5 bins
        n_bins=5, strategy=strategy, quantile_method=quantile_method, encode="ordinal"
    )
    assert_array_equal(expected_5bins, Xt.ravel())

def test_inverse_transform(strategy, encode, expected_inv, quantile_method):
    kbd = KBinsDiscretizer(
        n_bins=3, strategy=strategy, quantile_method=quantile_method, encode=encode
    )
    Xt = kbd.fit_transform(X)
    Xinv = kbd.inverse_transform(Xt)
    assert_array_almost_equal(expected_inv, Xinv)

def test_transform_outside_fit_range(strategy):
    X = np.array([0, 1, 2, 3])[:, None]

        kbd = KBinsDiscretizer(
            n_bins=4,
        )
    else:
        kbd = KBinsDiscretizer(n_bins=4, strategy=strategy, encode="ordinal")
    kbd.fit(X)

    X2 = np.array([-2, 5])[:, None]
    X2t = kbd.transform(X2)
    assert_array_equal(X2t.max(axis=0) + 1, kbd.n_bins_)
    assert_array_equal(X2t.min(axis=0), [0])

def test_overwrite():

        n_bins=3, quantile_method="averaged_inverted_cdf", encode="ordinal"
    )
    assert_array_equal(X, X_before)

    Xt_before = Xt.copy()
    Xinv = est.inverse_transform(Xt)
    assert_array_equal(Xt, Xt_before)
    assert_array_equal(Xinv, np.array([[0.5], [1.5], [2.5], [2.5]]))

def test_redundant_bins(strategy, expected_bin_edges, quantile_method):
    X = [[0], [0], [0], [0], [3], [3]]
        n_bins=3, strategy=strategy, quantile_method=quantile_method, subsample=None
    )
    warning_message = "Consider decreasing the number of bins."
        kbd.fit(X)

    assert_array_almost_equal(kbd.bin_edges_[0], expected_bin_edges)

def test_percentile_numeric_stability():
    X = np.array([0.05, 0.05, 0.95]).reshape(-1, 1)
    bin_edges = np.array([0.05, 0.23, 0.41, 0.59, 0.77, 0.95])
    Xt = np.array([0, 0, 4]).reshape(-1, 1)
        quantile_method="linear",
    )
    ## TODO: change to averaged inverted cdf, but that means we only get bin
    ## edges of 0.05 and 0.95 and nothing in between

    assert_array_almost_equal(kbd.bin_edges_[0], bin_edges)
    assert_array_almost_equal(kbd.transform(X), Xt)

def test_consistent_dtype(in_dtype, out_dtype, encode):
    X_input = np.array(X, dtype=in_dtype)
        n_bins=3,
        encode=encode,
        dtype=out_dtype,
    )
    kbd.fit(X_input)

    # test output dtype
    if out_dtype is not None:
        expected_dtype = out_dtype
    elif out_dtype is None and X_input.dtype == np.float16:
        # wrong numeric input dtype are cast in np.float64
        expected_dtype = np.float64
    else:
        expected_dtype = X_input.dtype
    Xt = kbd.transform(X_input)
    assert Xt.dtype == expected_dtype

def test_32_equal_64(input_dtype, encode):
    # TODO this check is redundant with common checks and can be removed
    #  once #16290 is merged
    X_input = np.array(X, dtype=input_dtype)

    # 32 bit output
    kbd_32 = KBinsDiscretizer(
        n_bins=3,
    )
    kbd_32.fit(X_input)
    Xt_32 = kbd_32.transform(X_input)

    # 64 bit output
    kbd_64 = KBinsDiscretizer(
        n_bins=3,
    )
    kbd_64.fit(X_input)
    Xt_64 = kbd_64.transform(X_input)

    assert_allclose_dense_sparse(Xt_32, Xt_64)

def test_kbinsdiscretizer_subsample_default():
    # Since the size of X is small (< 2e5), subsampling will not take place.
    X = np.array([-2, 1.5, -4, -1]).reshape(-1, 1)
    kbd_default = KBinsDiscretizer(
    )
    kbd_default.fit(X)

    kbd_without_subsampling = clone(kbd_default)
    kbd_without_subsampling.set_params(subsample=None)
    kbd_without_subsampling.fit(X)

    for bin_kbd_default, bin_kbd_with_subsampling in zip(
        kbd_default.bin_edges_[0], kbd_without_subsampling.bin_edges_[0]
    ):
        np.testing.assert_allclose(bin_kbd_default, bin_kbd_with_subsampling)
    assert kbd_default.bin_edges_.shape == kbd_without_subsampling.bin_edges_.shape

def test_kbinsdiscrtizer_get_feature_names_out(encode, expected_names):
    """Check get_feature_names_out for different settings.
    Non-regression test for #22731
    """
    X = [[-2, 1, -4], [-1, 2, -3], [0, 3, -2], [1, 4, -1]]

        n_bins=4, encode=encode, quantile_method="averaged_inverted_cdf"
    ).fit(X)
    Xt = kbd.transform(X)

    input_features = [f"feat{i}" for i in range(3)]
    output_names = kbd.get_feature_names_out(input_features)
    assert Xt.shape[1] == output_names.shape[0]

    assert_array_equal(output_names, expected_names)

def test_kbinsdiscretizer_subsample(strategy, global_random_seed):
    # Check that the bin edges are almost the same when subsampling is used.
    X = np.random.RandomState(global_random_seed).random_sample((100000, 1)) + 1

        kbd_subsampling = KBinsDiscretizer(
            subsample=50000,
        )
    else:
            strategy=strategy, subsample=50000, random_state=global_random_seed
        )
    kbd_subsampling.fit(X)

    kbd_no_subsampling = clone(kbd_subsampling)
    kbd_no_subsampling.set_params(subsample=None)
    kbd_no_subsampling.fit(X)

    # We use a large tolerance because we can't expect the bin edges to be exactly the
    # same when subsampling is used.
        kbd_subsampling.bin_edges_[0], kbd_no_subsampling.bin_edges_[0], rtol=1e-2
    )

def test_quantile_method_future_warnings():
        match="The current default behavior, quantile_method='linear', will be "
        "changed to quantile_method='averaged_inverted_cdf' in "
        "scikit-learn version 1.9 to naturally support sample weight "
        "equivalence properties by default. Pass "
        "quantile_method='averaged_inverted_cdf' explicitly to silence this "
        "warning.",
    ):
        KBinsDiscretizer(strategy="quantile").fit(X)

def test_invalid_quantile_method_with_sample_weight():
        "When fitting with strategy='quantile' and sample weights, "
        "quantile_method should either be set to 'averaged_inverted_cdf' or "
        "'inverted_cdf', got quantile_method='linear' instead."
    )
        match=expected_msg,
    ):
        KBinsDiscretizer(strategy="quantile", quantile_method="linear").fit(
            X,
            sample_weight=[1, 1, 2, 2],
        )

def _encode_target(X_ordinal, y_numeric, n_categories, smooth):
    """Simple Python implementation of target encoding."""
    cur_encodings = np.zeros(n_categories, dtype=np.float64)
    y_mean = np.mean(y_numeric)

    if smooth == "auto":
        y_variance = np.var(y_numeric)
        for c in range(n_categories):
            y_subset = y_numeric[X_ordinal == c]
            n_i = y_subset.shape[0]

            if n_i == 0:
                cur_encodings[c] = y_mean
                continue

            y_subset_variance = np.var(y_subset)
            m = y_subset_variance / y_variance
            lambda_ = n_i / (n_i + m)

            cur_encodings[c] = lambda_ * np.mean(y_subset) + (1 - lambda_) * y_mean
        return cur_encodings
    else:  # float
            current_sum = np.sum(y_subset) + y_mean * smooth
            current_cnt = y_subset.shape[0] + smooth
            cur_encodings[c] = current_sum / current_cnt

def test_encoding(categories, unknown_value, global_random_seed, smooth, target_type):
    """Check encoding for binary and continuous targets.

    Compare the values returned by `TargetEncoder.fit_transform` against the
    expected encodings for cv splits from a naive reference Python
    implementation in _encode_target.
    """

    n_categories = 3
    X_train_int_array = np.array([[0] * 20 + [1] * 30 + [2] * 40], dtype=np.int64).T
    X_test_int_array = np.array([[0, 1, 2]], dtype=np.int64).T
    n_samples = X_train_int_array.shape[0]

    if categories == "auto":
        X_train = X_train_int_array
        X_test = X_test_int_array
    else:
        X_train = categories[0][X_train_int_array]
        X_test = categories[0][X_test_int_array]

    X_test = np.concatenate((X_test, [[unknown_value]]))

    data_rng = np.random.RandomState(global_random_seed)
        y_numeric = data_rng.randint(low=0, high=2, size=n_samples)
        target_names = np.array(["cat", "dog"], dtype=object)
        y_train = target_names[y_numeric]

    else:
        assert target_type == "continuous"
        y_numeric = data_rng.uniform(low=-10, high=20, size=n_samples)
        y_train = y_numeric

    shuffled_idx = data_rng.permutation(n_samples)
    X_train_int_array = X_train_int_array[shuffled_idx]
    X_train = X_train[shuffled_idx]
    y_train = y_train[shuffled_idx]
    y_numeric = y_numeric[shuffled_idx]

    # Define our CV splitting strategy
        cv = StratifiedKFold(
            n_splits=n_splits, random_state=global_random_seed, shuffle=True
        )
    else:
        cv = KFold(n_splits=n_splits, random_state=global_random_seed, shuffle=True)

    # Compute the expected values using our reference Python implementation of
    # target encoding:
    expected_X_fit_transform = np.empty_like(X_train_int_array, dtype=np.float64)

    for train_idx, test_idx in cv.split(X_train_int_array, y_train):
        X_, y_ = X_train_int_array[train_idx, 0], y_numeric[train_idx]
        cur_encodings = _encode_target(X_, y_, n_categories, smooth)
        expected_X_fit_transform[test_idx, 0] = cur_encodings[
            X_train_int_array[test_idx, 0]
        ]

    # Check that we can obtain the same encodings by calling `fit_transform` on
    # the estimator with the same CV parameters:
    target_encoder = TargetEncoder(
        smooth=smooth,
    )

    X_fit_transform = target_encoder.fit_transform(X_train, y_train)

    assert target_encoder.target_type_ == target_type
    assert_allclose(X_fit_transform, expected_X_fit_transform)
    assert len(target_encoder.encodings_) == 1
        assert_array_equal(target_encoder.classes_, target_names)
    else:
        assert target_encoder.classes_ is None

    # compute encodings for all data to validate `transform`
    expected_encodings = _encode_target(
        X_train_int_array[:, 0], y_numeric, n_categories, smooth
    )
    assert_allclose(target_encoder.encodings_[0], expected_encodings)
    assert target_encoder.target_mean_ == pytest.approx(y_mean)

    # Transform on test data, the last value is unknown so it is encoded as the target
    # mean
    expected_X_test_transform = np.concatenate(
        (expected_encodings, np.array([y_mean]))

    X_test_transform = target_encoder.transform(X_test)
    assert_allclose(X_test_transform, expected_X_test_transform)

def test_encoding_multiclass(
    global_random_seed, categories, unknown_values, target_labels, smooth
):
    """Check encoding for multiclass targets."""

    n_samples = 80
    feat_1_int = np.array(rng.randint(low=0, high=2, size=n_samples))
    feat_2_int = np.array(rng.randint(low=0, high=3, size=n_samples))
    feat_1 = categories[0][feat_1_int]
    feat_2 = categories[0][feat_2_int]
    X_train = np.column_stack((feat_1, feat_2))
    X_train_int = np.column_stack((feat_1_int, feat_2_int))
    categories_ = [[0, 1], [0, 1, 2]]

    y_train_int = np.array(rng.randint(low=0, high=n_classes, size=n_samples))
    y_train = target_labels[y_train_int]
    y_train_enc = LabelBinarizer().fit_transform(y_train)

    cv = StratifiedKFold(
        n_splits=n_splits, random_state=global_random_seed, shuffle=True
    )

    # Manually compute encodings for cv splits to validate `fit_transform`
    expected_X_fit_transform = np.empty(
        (X_train_int.shape[0], X_train_int.shape[1] * n_classes),
    )
    for f_idx, cats in enumerate(categories_):
        for c_idx in range(n_classes):
            for train_idx, test_idx in cv.split(X_train, y_train):
                y_class = y_train_enc[:, c_idx]
                X_, y_ = X_train_int[train_idx, f_idx], y_class[train_idx]
                current_encoding = _encode_target(X_, y_, len(cats), smooth)
                # f_idx:   0, 0, 0, 1, 1, 1
                # c_idx:   0, 1, 2, 0, 1, 2
                # exp_idx: 0, 1, 2, 3, 4, 5
                exp_idx = c_idx + (f_idx * n_classes)
                expected_X_fit_transform[test_idx, exp_idx] = current_encoding[
                    X_train_int[test_idx, f_idx]
                ]

    )

    assert target_encoder.target_type_ == "multiclass"

    # Manually compute encoding to validate `transform`
    expected_encodings = []
            y_class = y_train_enc[:, c_idx]
            current_encoding = _encode_target(
                X_train_int[:, f_idx], y_class, len(cats), smooth
            )
            expected_encodings.append(current_encoding)

    assert len(target_encoder.encodings_) == n_features * n_classes
    for i in range(n_features * n_classes):
        assert_allclose(target_encoder.encodings_[i], expected_encodings[i])
    assert_array_equal(target_encoder.classes_, target_labels)

    # Include unknown values at the end
    X_test_int = np.array([[0, 1], [1, 2], [4, 5]])
    if unknown_values == "auto":
        X_test = X_test_int
    else:
        X_test = np.empty_like(X_test_int[:-1, :], dtype=object)
        for column_idx in range(X_test_int.shape[1]):
            X_test[:, column_idx] = categories[0][X_test_int[:-1, column_idx]]
        # Add unknown values at end
        X_test = np.vstack((X_test, unknown_values))

    y_mean = np.mean(y_train_enc, axis=0)
    expected_X_test_transform = np.empty(
        (X_test_int.shape[0], X_test_int.shape[1] * n_classes),
    )
    n_rows = X_test_int.shape[0]
    f_idx = [0, 0, 0, 1, 1, 1]
    # Last row are unknowns, dealt with later
    for row_idx in range(n_rows - 1):
        for i, enc in enumerate(expected_encodings):
            expected_X_test_transform[row_idx, i] = enc[X_test_int[row_idx, f_idx[i]]]

    # Unknowns encoded as target mean for each class
    # `y_mean` contains target mean for each class, thus cycle through mean of
    # each class, `n_features` times
    mean_idx = [0, 1, 2, 0, 1, 2]
    for i in range(n_classes * n_features):
        expected_X_test_transform[n_rows - 1, i] = y_mean[mean_idx[i]]

def test_custom_categories(X, categories, smooth):
    """Custom categories with unknown categories that are not in training data."""
    y = rng.uniform(low=-10, high=20, size=X.shape[0])
    enc = TargetEncoder(categories=categories, smooth=smooth, random_state=0).fit(X, y)

    # The last element is unknown and encoded as the mean
    y_mean = y.mean()
    X_trans = enc.transform(X[-1:])
    assert X_trans[0, 0] == pytest.approx(y_mean)

    assert len(enc.encodings_) == 1
    # custom category that is not in training data
    assert enc.encodings_[0][-1] == pytest.approx(y_mean)

def test_errors(y, msg):
    """Check invalidate input."""
    X = np.array([[1, 0, 1]]).T

    enc = TargetEncoder()
        enc.fit_transform(X, y)

def test_use_regression_target():
    """Check inferred and specified `target_type` on regression target."""
    X = np.array([[0, 1, 0, 1, 0, 1]]).T
    y = np.array([1.0, 2.0, 3.0, 2.0, 3.0, 4.0])

    enc = TargetEncoder(cv=2)
            "The least populated class in y has only 1 members, which is less than"
            " n_splits=2."
        ),
    ):
    assert enc.target_type_ == "multiclass"

    enc = TargetEncoder(cv=2, target_type="continuous")
    enc.fit_transform(X, y)
    assert enc.target_type_ == "continuous"

def test_feature_names_out_set_output(y, feature_names):
    """Check TargetEncoder works with set_output."""

    X_df = pd.DataFrame({"A": ["a", "b"] * 10, "B": [1, 2] * 10})

    enc_default = TargetEncoder(cv=2, smooth=3.0, random_state=0)
    enc_default.set_output(transform="default")
    enc_pandas = TargetEncoder(cv=2, smooth=3.0, random_state=0)
    enc_pandas.set_output(transform="pandas")

    X_default = enc_default.fit_transform(X_df, y)
    X_pandas = enc_pandas.fit_transform(X_df, y)

    assert_array_equal(enc_pandas.get_feature_names_out(), feature_names)
    assert_array_equal(enc_pandas.get_feature_names_out(), X_pandas.columns)

def test_multiple_features_quick(to_pandas, smooth, target_type):
    """Check target encoder with multiple features."""
    X_ordinal = np.array(
        [[1, 1], [0, 1], [1, 1], [2, 1], [1, 0], [0, 1], [1, 0], [0, 0]], dtype=np.int64
    )
    if target_type == "binary-str":
        y_train = np.array(["a", "b", "a", "a", "b", "b", "a", "b"])
        y_integer = LabelEncoder().fit_transform(y_train)
        cv = StratifiedKFold(2, random_state=0, shuffle=True)
    elif target_type == "binary-ints":
        y_train = np.array([3, 4, 3, 3, 3, 4, 4, 4])
    else:
        y_train = np.array([3.0, 5.1, 2.4, 3.5, 4.1, 5.5, 10.3, 7.3], dtype=np.float32)
        y_integer = y_train
        cv = KFold(2, random_state=0, shuffle=True)
    y_mean = np.mean(y_integer)
    categories = [[0, 1, 2], [0, 1]]

        [
            [0, 1],
            [3, 0],  # 3 is unknown
            [1, 10],  # 10 is unknown
        ],
    )

    if to_pandas:
        # convert second feature to an object
        X_train = pd.DataFrame(
            {
                "feat0": X_ordinal[:, 0],
                "feat1": np.array(["cat", "dog"], dtype=object)[X_ordinal[:, 1]],
            }
        )
        # "snake" is unknown
        X_test = pd.DataFrame({"feat0": X_test[:, 0], "feat1": ["dog", "cat", "snake"]})
    else:
        X_train = X_ordinal

    # manually compute encoding for fit_transform
    expected_X_fit_transform = np.empty_like(X_ordinal, dtype=np.float64)
    for f_idx, cats in enumerate(categories):
        for train_idx, test_idx in cv.split(X_ordinal, y_integer):
            X_, y_ = X_ordinal[train_idx, f_idx], y_integer[train_idx]
            current_encoding = _encode_target(X_, y_, len(cats), smooth)
            expected_X_fit_transform[test_idx, f_idx] = current_encoding[
                X_ordinal[test_idx, f_idx]
            ]

    # manually compute encoding for transform
        current_encoding = _encode_target(
            X_ordinal[:, f_idx], y_integer, len(cats), smooth
        )
        expected_encodings.append(current_encoding)

    expected_X_test_transform = np.array(
        [
            [expected_encodings[0][0], expected_encodings[1][1]],
            [y_mean, expected_encodings[1][0]],
            [expected_encodings[0][1], y_mean],
        ],
    )

    enc = TargetEncoder(smooth=smooth, cv=2, random_state=0)
    X_fit_transform = enc.fit_transform(X_train, y_train)

    assert len(enc.encodings_) == 2
        assert_allclose(enc.encodings_[i], expected_encodings[i])

    X_test_transform = enc.transform(X_test)

def test_constant_target_and_feature(y, y_mean, smooth):
    """Check edge case where feature and target is constant."""
    X = np.array([[1] * 20]).T

    enc = TargetEncoder(cv=2, smooth=smooth, random_state=0)
    X_trans = enc.fit_transform(X, y)
    assert_allclose(X_trans, np.repeat([[y_mean]], n_samples, axis=0))
    assert enc.encodings_[0][0] == pytest.approx(y_mean)
    assert enc.target_mean_ == pytest.approx(y_mean)

    X_test = np.array([[1], [0]])
    X_test_trans = enc.transform(X_test)
    assert_allclose(X_test_trans, np.repeat([[y_mean]], 2, axis=0))

def test_fit_transform_not_associated_with_y_if_ordinal_categorical_is_not(
):
    cardinality = 30  # not too large, otherwise we need a very large n_samples
    y_train = rng.normal(size=n_samples)
    X_train = rng.randint(0, cardinality, size=n_samples).reshape(-1, 1)

    # Sort by y_train to attempt to cause a leak
    y_sorted_indices = y_train.argsort()
    y_train = y_train[y_sorted_indices]
    X_train = X_train[y_sorted_indices]

    target_encoder = TargetEncoder(shuffle=True, random_state=global_random_seed)
    X_encoded_train_shuffled = target_encoder.fit_transform(X_train, y_train)

    target_encoder = TargetEncoder(shuffle=False)
    X_encoded_train_no_shuffled = target_encoder.fit_transform(X_train, y_train)

    # Check that no information about y_train has leaked into X_train:
    regressor = RandomForestRegressor(
        n_estimators=10, min_samples_leaf=20, random_state=global_random_seed
    )

    # It's impossible to learn a good predictive model on the training set when
    # using the original representation X_train or the target encoded
    # representation with shuffled inner CV. For the latter, no information
    # about y_train has inadvertently leaked into the prior used to generate
    # `X_encoded_train_shuffled`:
    cv = ShuffleSplit(n_splits=50, random_state=global_random_seed)
    assert cross_val_score(regressor, X_train, y_train, cv=cv).mean() < 0.1
    assert (
        cross_val_score(regressor, X_encoded_train_shuffled, y_train, cv=cv).mean()
        < 0.1
    )

    # Without the inner CV shuffling, a lot of information about y_train goes into the
    # the per-fold y_train.mean() priors: shrinkage is no longer effective in this
    # case and would no longer be able to prevent downstream over-fitting.
    assert (
        cross_val_score(regressor, X_encoded_train_no_shuffled, y_train, cv=cv).mean()
        > 0.5
    )

def test_smooth_zero():
    """Check edge case with zero smoothing and cv does not contain category."""
    X = np.array([[0, 0, 0, 0, 0, 1, 1, 1, 1, 1]]).T
    y = np.array([2.1, 4.3, 1.2, 3.1, 1.0, 9.0, 10.3, 14.2, 13.3, 15.0])

    enc = TargetEncoder(smooth=0.0, shuffle=False, cv=2)

    # With cv = 2, category 0 does not exist in the second half, thus
    # it will be encoded as the mean of the second half
    assert_allclose(X_trans[0], np.mean(y[5:]))

    # category 1 does not exist in the first half, thus it will be encoded as
    # the mean of the first half
    assert_allclose(X_trans[-1], np.mean(y[:5]))

def test_invariance_of_encoding_under_label_permutation(smooth, global_random_seed):
    # Check that the encoding does not depend on the integer of the value of
    # the integer labels. This is quite a trivial property but it is helpful
    # to understand the following test.

    # Random y and informative categorical X to make the test non-trivial when
    # using smoothing.
    y = rng.normal(size=1000)
    n_categories = 30
    X = KBinsDiscretizer(
        n_bins=n_categories, quantile_method="averaged_inverted_cdf", encode="ordinal"
    ).fit_transform(y.reshape(-1, 1))

    )

    # Shuffle the labels to make sure that the encoding is invariant to the
    # permutation of the labels
    permutated_labels = rng.permutation(n_categories)
    X_train_permuted = permutated_labels[X_train.astype(np.int32)]
    X_test_permuted = permutated_labels[X_test.astype(np.int32)]

    target_encoder = TargetEncoder(smooth=smooth, random_state=global_random_seed)
    X_train_encoded = target_encoder.fit_transform(X_train, y_train)
    X_test_encoded = target_encoder.transform(X_test)

    X_train_permuted_encoded = target_encoder.fit_transform(X_train_permuted, y_train)
    X_test_permuted_encoded = target_encoder.transform(X_test_permuted)

    assert_allclose(X_train_encoded, X_train_permuted_encoded)
    assert_allclose(X_test_encoded, X_test_permuted_encoded)

def test_target_encoding_for_linear_regression(smooth, global_random_seed):
    # Check some expected statistical properties when fitting a linear
    # regression model on target encoded features depending on their relation
    # with that target.

    # In this test, we use the Ridge class with the "lsqr" solver and a little
    # bit of regularization to implement a linear regression model that
    # converges quickly for large `n_samples` and robustly in case of
    # correlated features. Since we will fit this model on a mean centered
    # target, we do not need to fit an intercept and this will help simplify
    # the analysis with respect to the expected coefficients.
    linear_regression = Ridge(alpha=1e-6, solver="lsqr", fit_intercept=False)

    # Construct a random target variable. We need a large number of samples for
    # this test to be stable across all values of the random seed.
    n_samples = 50_000

    # Generate a single informative ordinal feature with medium cardinality.
    # Inject some irreducible noise to make it harder for a multivariate model
    # to identify the informative feature from other pure noise features.
    noise = 0.8 * rng.randn(n_samples)
    n_categories = 100
    X_informative = KBinsDiscretizer(
        n_bins=n_categories,
    ).fit_transform((y + noise).reshape(-1, 1))

    # Let's permute the labels to hide the fact that this feature is
    # informative to naive linear regression model trained on the raw ordinal
    # values. As highlighted in the previous test, the target encoding should be
    # invariant to such a permutation.
    X_informative = permutated_labels[X_informative.astype(np.int32)]

    # Generate a shuffled copy of the informative feature to destroy the
    # relationship with the target.
    X_shuffled = rng.permutation(X_informative)

    # Also include a very high cardinality categorical feature that is by
    # itself independent of the target variable: target encoding such a feature
    # without internal cross-validation should cause catastrophic overfitting
    # for the downstream regressor, even with shrinkage. This kind of features
    # typically represents near unique identifiers of samples. In general they
    # should be removed from a machine learning datasets but here we want to
    # study the ability of the default behavior of TargetEncoder to mitigate
    # them automatically.
    X_near_unique_categories = rng.choice(
        int(0.9 * n_samples), size=n_samples, replace=True

    # Assemble the dataset and do a train-test split:
        [X_informative, X_shuffled, X_near_unique_categories],
        axis=1,
    )

    # Let's first check that a linear regression model trained on the raw
    # features underfits because of the meaning-less ordinal encoding of the
    # labels.
    raw_model = linear_regression.fit(X_train, y_train)
    assert raw_model.score(X_train, y_train) < 0.1
    assert raw_model.score(X_test, y_test) < 0.1

    # Now do the same with target encoding using the internal CV mechanism
    # implemented when using fit_transform.
    model_with_cv = make_pipeline(
        TargetEncoder(smooth=smooth, random_state=rng), linear_regression

    # This model should be able to fit the data well and also generalise to the
    # test data (assuming that the binning is fine-grained enough). The R2
    # scores are not perfect because of the noise injected during the
    # generation of the unique informative feature.
    coef = model_with_cv[-1].coef_
    assert model_with_cv.score(X_train, y_train) > 0.5, coef
    assert model_with_cv.score(X_test, y_test) > 0.5, coef

    # The target encoder recovers the linear relationship with slope 1 between
    # the target encoded unique informative predictor and the target. Since the
    # target encoding of the 2 other features is not informative thanks to the
    # use of internal cross-validation, the multivariate linear regressor
    # assigns a coef of 1 to the first feature and 0 to the other 2.
    assert coef[0] == pytest.approx(1, abs=1e-2)
    assert (np.abs(coef[1:]) < 0.2).all()

    # Let's now disable the internal cross-validation by calling fit and then
    # transform separately on the training set:
    target_encoder = TargetEncoder(smooth=smooth, random_state=rng).fit(
    )
    X_enc_no_cv_train = target_encoder.transform(X_train)
    X_enc_no_cv_test = target_encoder.transform(X_test)
    model_no_cv = linear_regression.fit(X_enc_no_cv_train, y_train)

    # The linear regression model should always overfit because it assigns
    # too much weight to the extremely high cardinality feature relatively to
    # the informative feature. Note that this is the case even when using
    # the empirical Bayes smoothing which is not enough to prevent such
    # overfitting alone.
    coef = model_no_cv.coef_
    assert model_no_cv.score(X_enc_no_cv_train, y_train) > 0.7, coef
    assert model_no_cv.score(X_enc_no_cv_test, y_test) < 0.5, coef

    # The model overfits because it assigns too much weight to the high
    # cardinality yet non-informative feature instead of the lower
    # cardinality yet informative feature:
    assert abs(coef[0]) < abs(coef[2])

def test_pandas_copy_on_write():
    """
    Test target-encoder cython code when y is read-only.

    The numpy array underlying df["y"] is read-only when copy-on-write is enabled.
    Non-regression test for gh-27879.
    """
    pd = pytest.importorskip("pandas", minversion="2.0")
    with pd.option_context("mode.copy_on_write", True):
        df = pd.DataFrame({"x": ["a", "b", "b"], "y": [4.0, 5.0, 6.0]})
        TargetEncoder(target_type="continuous").fit(df[["x"]], df["y"])

def assert_correct_incr(i, batch_start, batch_stop, n, chunk_size, n_samples_seen):
    if batch_stop != n:
        assert (i + 1) * chunk_size == n_samples_seen
    else:
        assert i * chunk_size + (batch_stop - batch_start) == n_samples_seen

        scaler = StandardScaler()

        # make sure Error is raised the sample weights greater than 1d
        sample_weight_notOK = rng.randn(n_samples, 1) ** 2
            scaler.fit(X, y, sample_weight=sample_weight_notOK)

def test_standard_scaler_sample_weight(Xw, X, sample_weight, array_constructor):
    with_mean = not array_constructor.startswith("sparse")
    X = _convert_container(X, array_constructor)
    Xw = _convert_container(Xw, array_constructor)

    # weighted StandardScaler
    yw = np.ones(Xw.shape[0])
    scaler_w = StandardScaler(with_mean=with_mean)
    scaler_w.fit(Xw, yw, sample_weight=sample_weight)

    y = np.ones(X.shape[0])
    scaler = StandardScaler(with_mean=with_mean)
    scaler.fit(X, y)

    assert_almost_equal(scaler.mean_, scaler_w.mean_)
    assert_almost_equal(scaler.var_, scaler_w.var_)
    assert_almost_equal(scaler.transform(X_test), scaler_w.transform(X_test))

def test_standard_scaler_1d():
    # Test scaling of dataset along single axis
    for X in [X_1row, X_1col, X_list_1row, X_list_1row]:
        X_scaled = scaler.fit(X).transform(X, copy=True)

        if isinstance(X, list):
            X = np.array(X)  # cast only after scaling done

        if _check_dim_1axis(X) == 1:
            assert_almost_equal(scaler.mean_, X.ravel())
            assert_almost_equal(scaler.scale_, np.ones(n_features))
            assert_array_almost_equal(X_scaled.mean(axis=0), np.zeros_like(n_features))
            assert_array_almost_equal(X_scaled.std(axis=0), np.zeros_like(n_features))
        else:
            assert_almost_equal(scaler.mean_, X.mean())
            assert_almost_equal(scaler.scale_, X.std())
            assert_array_almost_equal(X_scaled.mean(axis=0), 0.0)
            assert_array_almost_equal(X_scaled.std(axis=0), 1.0)
        assert scaler.n_samples_seen_ == X.shape[0]

        # check inverse transform
        X_scaled_back = scaler.inverse_transform(X_scaled)
        assert_array_almost_equal(X_scaled_back, X)

    # Constant feature
    X = np.ones((5, 1))
    X_scaled = scaler.fit(X).transform(X, copy=True)
    assert_almost_equal(scaler.mean_, 1.0)
    assert_almost_equal(scaler.scale_, 1.0)
    assert_array_almost_equal(X_scaled.mean(axis=0), 0.0)
    assert_array_almost_equal(X_scaled.std(axis=0), 0.0)
    assert scaler.n_samples_seen_ == X.shape[0]

def test_standard_scaler_dtype(add_sample_weight, sparse_container):
    # Ensure scaling does not affect dtype
    if add_sample_weight:
    else:
    with_mean = True
        # scipy sparse containers do not support float16, see
        # https://github.com/scipy/scipy/issues/7408 for more details.
        supported_dtype = [np.float64, np.float32]
    else:
        supported_dtype = [np.float64, np.float32, np.float16]
    for dtype in supported_dtype:
        X = rng.randn(n_samples, n_features).astype(dtype)
            X = sparse_container(X)
            with_mean = False

        scaler = StandardScaler(with_mean=with_mean)
        X_scaled = scaler.fit(X, sample_weight=sample_weight).transform(X)
        assert X.dtype == X_scaled.dtype
        assert scaler.mean_.dtype == np.float64
        assert scaler.scale_.dtype == np.float64

def test_standard_scaler_constant_features(
    scaler, add_sample_weight, sparse_container, dtype, constant
):
    if isinstance(scaler, RobustScaler) and add_sample_weight:
        pytest.skip(f"{scaler.__class__.__name__} does not yet support sample_weight")

        fit_params = dict(sample_weight=rng.uniform(size=n_samples) * 2)
    else:
    X_array = np.full(shape=(n_samples, n_features), fill_value=constant, dtype=dtype)
    X = X_array if sparse_container is None else sparse_container(X_array)
    X_scaled = scaler.fit(X, **fit_params).transform(X)

    if isinstance(scaler, StandardScaler):
        # The variance info should be close to zero for constant features.
        assert_allclose(scaler.var_, np.zeros(X.shape[1]), atol=1e-7)

    # Constant features should not be scaled (scale of 1.):
    assert_allclose(scaler.scale_, np.ones(X.shape[1]))

    assert X_scaled is not X  # make sure we make a copy
    assert_allclose_dense_sparse(X_scaled, X)

    if isinstance(scaler, StandardScaler) and not add_sample_weight:
        # Also check consistency with the standard scale function.
        X_scaled_2 = scale(X, with_mean=scaler.with_mean)
        assert X_scaled_2 is not X  # make sure we did a copy
        assert_allclose_dense_sparse(X_scaled_2, X)

def test_standard_scaler_near_constant_features(
    n_samples, sparse_container, average, dtype
):
    # Check that when the variance is too small (var << mean**2) the feature
    # is considered constant and not scaled.

    scale_min, scale_max = -30, 19
    scales = np.array([10**i for i in range(scale_min, scale_max + 1)], dtype=dtype)

    n_features = scales.shape[0]
    X = np.empty((n_samples, n_features), dtype=dtype)
    # Make a dataset of known var = scales**2 and mean = average
    X[: n_samples // 2, :] = average + scales
    X[n_samples // 2 :, :] = average - scales
    X_array = X if sparse_container is None else sparse_container(X)

    scaler = StandardScaler(with_mean=False).fit(X_array)

    # StandardScaler uses float64 accumulators even if the data has a float32
    # dtype.

    # if var < bound = N.eps.var + N².eps².mean², the feature is considered
    # constant and the scale_ attribute is set to 1.
    bounds = n_samples * eps * scales**2 + n_samples**2 * eps**2 * average**2
    within_bounds = scales**2 <= bounds

    # Check that scale_min is small enough to have some scales below the
    # bound and therefore detected as constant:
    assert np.any(within_bounds)

    # Check that such features are actually treated as constant by the scaler:
    assert all(scaler.var_[within_bounds] <= bounds[within_bounds])
    assert_allclose(scaler.scale_[within_bounds], 1.0)

    # Depending the on the dtype of X, some features might not actually be
    # representable as non constant for small scales (even if above the
    # precision bound of the float64 variance estimate). Such feature should
    # be correctly detected as constants with 0 variance by StandardScaler.
    representable_diff = X[0, :] - X[-1, :] != 0
    assert_allclose(scaler.var_[np.logical_not(representable_diff)], 0)
    assert_allclose(scaler.scale_[np.logical_not(representable_diff)], 1)

    # The other features are scaled and scale_ is equal to sqrt(var_) assuming
    # that scales are large enough for average + scale and average - scale to
    # be distinct in X (depending on X's dtype).
    common_mask = np.logical_and(scales**2 > bounds, representable_diff)
    assert_allclose(scaler.scale_[common_mask], np.sqrt(scaler.var_)[common_mask])

def test_scale_1d():
    # 1-d inputs
    X_list = [1.0, 3.0, 5.0, 0.0]
    X_arr = np.array(X_list)

    for X in [X_list, X_arr]:
        X_scaled = scale(X)
        assert_array_almost_equal(X_scaled.mean(), 0.0)
        assert_array_almost_equal(X_scaled.std(), 1.0)
        assert_array_equal(scale(X, with_mean=False, with_std=False), X)

def test_standard_scaler_numerical_stability():
    # Test numerical stability of scaling
    # np.log(1e-5) is taken because of its floating point representation
    # was empirically found to cause numerical problems with np.mean & np.std.
    x = np.full(8, np.log(1e-5), dtype=np.float64)
    # This does not raise a warning as the number of samples is too low
    # to trigger the problem in recent numpy
        scale(x)
    assert_array_almost_equal(scale(x), np.zeros(8))

    # with 2 more samples, the std computation run into numerical issues:
    x = np.full(10, np.log(1e-5), dtype=np.float64)
    warning_message = "standard deviation of the data is probably very close to 0"
        x_scaled = scale(x)
    assert_array_almost_equal(x_scaled, np.zeros(10))

    x = np.full(10, 1e-100, dtype=np.float64)
        x_small_scaled = scale(x)
    assert_array_almost_equal(x_small_scaled, np.zeros(10))

    # Large values can cause (often recoverable) numerical stability issues:
    x_big = np.full(10, 1e100, dtype=np.float64)
    warning_message = "Dataset may contain too large values"
        x_big_scaled = scale(x_big)
    assert_array_almost_equal(x_big_scaled, np.zeros(10))
    assert_array_almost_equal(x_big_scaled, x_small_scaled)
        x_big_centered = scale(x_big, with_std=False)
    assert_array_almost_equal(x_big_centered, np.zeros(10))
    assert_array_almost_equal(x_big_centered, x_small_scaled)

def test_scaler_2d_arrays():
    # Test scaling of 2d array along first axis
    X[:, 0] = 0.0  # first feature is always of zero

    assert not np.any(np.isnan(X_scaled))
    assert scaler.n_samples_seen_ == n_samples

    assert_array_almost_equal(X_scaled.mean(axis=0), n_features * [0.0])
    assert_array_almost_equal(X_scaled.std(axis=0), [0.0, 1.0, 1.0, 1.0, 1.0])
    # Check that X has been copied
    assert X_scaled is not X

    # check inverse transform
    X_scaled_back = scaler.inverse_transform(X_scaled)
    assert X_scaled_back is not X
    assert X_scaled_back is not X_scaled
    assert_array_almost_equal(X_scaled_back, X)

    X_scaled = scale(X, axis=1, with_std=False)
    assert_array_almost_equal(X_scaled.mean(axis=1), n_samples * [0.0])
    X_scaled = scale(X, axis=1, with_std=True)
    assert_array_almost_equal(X_scaled.std(axis=1), n_samples * [1.0])
    # Check that the data hasn't been modified

    X_scaled = scaler.fit(X).transform(X, copy=False)
    # Check that X has not been copied
    assert X_scaled is X

    X = rng.randn(4, 5)
    X[:, 0] = 1.0  # first feature is a constant, non zero feature

def test_scaler_float16_overflow():
    # Test if the scaler will not overflow on float16 numpy arrays
    # float16 has a maximum of 65500.0. On the worst case 5 * 200000 is 100000
    # which is enough to overflow the data type
    X = rng.uniform(5, 10, [200000, 1]).astype(np.float16)

    with np.errstate(over="raise"):
        scaler = StandardScaler().fit(X)
        X_scaled = scaler.transform(X)

    # Calculate the float64 equivalent to verify result
    X_scaled_f64 = StandardScaler().fit_transform(X.astype(np.float64))

    # Overflow calculations may cause -inf, inf, or nan. Since there is no nan
    # input, all of the outputs should be finite. This may be redundant since a
    # FloatingPointError exception will be thrown on overflow above.
    assert np.all(np.isfinite(X_scaled))

    # The normal distribution is very unlikely to go above 4. At 4.0-8.0 the
    # float16 precision is 2^-8 which is around 0.004. Thus only 2 decimals are
    # checked to account for precision differences.
    assert_array_almost_equal(X_scaled, X_scaled_f64, decimal=2)

def test_handle_zeros_in_scale():
    s1 = np.array([0, 1e-16, 1, 2, 3])
    s2 = _handle_zeros_in_scale(s1, copy=True)

    assert_allclose(s1, np.array([0, 1e-16, 1, 2, 3]))
    assert_allclose(s2, np.array([1, 1, 1, 2, 3]))

def test_minmax_scaler_partial_fit():
    # Test if partial_fit run over many batches of size 1 and 50
    # gives the same results as fit
    X = X_2d
    n = X.shape[0]

    for chunk_size in [1, 2, 50, n, n + 42]:
        # Test mean at the end of the process
        scaler_batch = MinMaxScaler().fit(X)

        scaler_incr = MinMaxScaler()
        for batch in gen_batches(n_samples, chunk_size):
            scaler_incr = scaler_incr.partial_fit(X[batch])

        assert_array_almost_equal(scaler_batch.data_min_, scaler_incr.data_min_)
        assert_array_almost_equal(scaler_batch.data_max_, scaler_incr.data_max_)
        assert scaler_batch.n_samples_seen_ == scaler_incr.n_samples_seen_
        assert_array_almost_equal(scaler_batch.data_range_, scaler_incr.data_range_)
        assert_array_almost_equal(scaler_batch.scale_, scaler_incr.scale_)
        assert_array_almost_equal(scaler_batch.min_, scaler_incr.min_)

        # Test std after 1 step
        batch0 = slice(0, chunk_size)
        scaler_batch = MinMaxScaler().fit(X[batch0])
        scaler_incr = MinMaxScaler().partial_fit(X[batch0])

        # Test std until the end of partial fits, and
        scaler_incr = MinMaxScaler()  # Clean estimator
        for i, batch in enumerate(gen_batches(n_samples, chunk_size)):
            assert_correct_incr(
                i,
                batch_start=batch.start,
                batch_stop=batch.stop,
                n=n,
                n_samples_seen=scaler_incr.n_samples_seen_,
            )

def test_standard_scaler_partial_fit():
    X = X_2d

        scaler_batch = StandardScaler(with_std=False).fit(X)

        scaler_incr = StandardScaler(with_std=False)
        assert_array_almost_equal(scaler_batch.mean_, scaler_incr.mean_)
        assert scaler_batch.var_ == scaler_incr.var_  # Nones

        scaler_incr = StandardScaler().partial_fit(X[batch0])
        if chunk_size == 1:
                np.zeros(n_features, dtype=np.float64), scaler_incr.var_
            )
                np.ones(n_features, dtype=np.float64), scaler_incr.scale_
            )
        else:
            assert_array_almost_equal(np.var(X[batch0], axis=0), scaler_incr.var_)
                np.std(X[batch0], axis=0), scaler_incr.scale_
            )  # no constants

        scaler_batch = StandardScaler().fit(X)
        scaler_incr = StandardScaler()  # Clean estimator
                i,
                n=n,
            )

        assert_array_almost_equal(scaler_batch.var_, scaler_incr.var_)

def test_standard_scaler_partial_fit_numerical_stability(sparse_container):
    # Test if the incremental computation introduces significative errors
    # for large datasets with values of large magniture
    offsets = rng.uniform(-1e15, 1e15, size=n_features)
    scales = rng.uniform(1e3, 1e6, size=n_features)
    X = rng.randn(n_samples, n_features) * scales + offsets

    scaler_batch = StandardScaler().fit(X)
    scaler_incr = StandardScaler()
    for chunk in X:
        scaler_incr = scaler_incr.partial_fit(chunk.reshape(1, n_features))

    # Regardless of abs values, they must not be more diff 6 significant digits
    tol = 10 ** (-6)
    assert_allclose(scaler_incr.mean_, scaler_batch.mean_, rtol=tol)
    assert_allclose(scaler_incr.var_, scaler_batch.var_, rtol=tol)
    assert_allclose(scaler_incr.scale_, scaler_batch.scale_, rtol=tol)
    # NOTE Be aware that for much larger offsets std is very unstable (last
    # assert) while mean is OK.

    # Sparse input
    size = (100, 3)
    scale = 1e20
    X = sparse_container(rng.randint(0, 2, size).astype(np.float64) * scale)

    # with_mean=False is required with sparse input
    scaler = StandardScaler(with_mean=False).fit(X)
    scaler_incr = StandardScaler(with_mean=False)

        if chunk.ndim == 1:
            # Sparse arrays can be 1D (in scipy 1.14 and later) while old
            # sparse matrix instances are always 2D.
            chunk = chunk.reshape(1, -1)
        scaler_incr = scaler_incr.partial_fit(chunk)

    # Regardless of magnitude, they must not differ more than of 6 digits
    assert scaler.mean_ is not None
    assert_allclose(scaler_incr.var_, scaler.var_, rtol=tol)
    assert_allclose(scaler_incr.scale_, scaler.scale_, rtol=tol)

def test_partial_fit_sparse_input(sample_weight, sparse_container):
    # Check that sparsity is not destroyed
    X = sparse_container(np.array([[1.0], [0.0], [0.0], [5.0]]))

        sample_weight = rng.rand(X.shape[0])

    null_transform = StandardScaler(with_mean=False, with_std=False, copy=True)
    X_null = null_transform.partial_fit(X, sample_weight=sample_weight).transform(X)
    assert_array_equal(X_null.toarray(), X.toarray())
    X_orig = null_transform.inverse_transform(X_null)
    assert_array_equal(X_orig.toarray(), X_null.toarray())
    assert_array_equal(X_orig.toarray(), X.toarray())

def test_standard_scaler_trasform_with_partial_fit(sample_weight):
    # Check some postconditions after applying partial_fit and transform
    X = X_2d[:100, :]

    for i, batch in enumerate(gen_batches(X.shape[0], 1)):
        X_sofar = X[: (i + 1), :]
        chunks_copy = X_sofar.copy()
            scaled_batch = StandardScaler().fit_transform(X_sofar)
        else:
            scaled_batch = StandardScaler().fit_transform(
                X_sofar, sample_weight=sample_weight[: i + 1]
            )
            scaler_incr = scaler_incr.partial_fit(
                X[batch], sample_weight=sample_weight[batch]
            )
        scaled_incr = scaler_incr.transform(X_sofar)

        assert_array_almost_equal(scaled_batch, scaled_incr)
        assert_array_almost_equal(X_sofar, chunks_copy)  # No change
        right_input = scaler_incr.inverse_transform(scaled_incr)
        assert_array_almost_equal(X_sofar, right_input)

        zero = np.zeros(X.shape[1])
        epsilon = np.finfo(float).eps
        assert_array_less(zero, scaler_incr.var_ + epsilon)  # as less or equal
        assert_array_less(zero, scaler_incr.scale_ + epsilon)
            # (i+1) because the Scaler has been already fitted
            assert (i + 1) == scaler_incr.n_samples_seen_
        else:
            assert np.sum(sample_weight[: i + 1]) == pytest.approx(
                scaler_incr.n_samples_seen_
            )

def test_standard_check_array_of_inverse_transform():
    # Check if StandardScaler inverse_transform is
    # converting the integer array to float
        [
            [1, 1, 1, 0, 1, 0],
            [0, 8, 0, 1, 0, 0],
            [1, 4, 1, 1, 0, 0],
            [0, 1, 0, 0, 1, 0],
            [0, 4, 0, 1, 0, 1],
        ],
        dtype=np.int32,
    )

    scaler.fit(x)

    # The of inverse_transform should be converted
    # to a float array.
    # If not X *= self.scale_ will fail.
    scaler.inverse_transform(x)

def test_preprocessing_array_api_compliance(
):

def test_min_max_scaler_iris():
    scaler = MinMaxScaler()
    # default params
    X_trans = scaler.fit_transform(X)
    assert_array_almost_equal(X_trans.min(axis=0), 0)
    assert_array_almost_equal(X_trans.max(axis=0), 1)
    X_trans_inv = scaler.inverse_transform(X_trans)
    assert_array_almost_equal(X, X_trans_inv)

    # not default params: min=1, max=2
    scaler = MinMaxScaler(feature_range=(1, 2))
    assert_array_almost_equal(X_trans.min(axis=0), 1)
    assert_array_almost_equal(X_trans.max(axis=0), 2)

    # min=-.5, max=.6
    scaler = MinMaxScaler(feature_range=(-0.5, 0.6))
    assert_array_almost_equal(X_trans.min(axis=0), -0.5)
    assert_array_almost_equal(X_trans.max(axis=0), 0.6)

    # raises on invalid range
    scaler = MinMaxScaler(feature_range=(2, 1))
        scaler.fit(X)

def test_min_max_scaler_zero_variance_features():
    # Check min max scaler on toy data with zero variance features
    X = [[0.0, 1.0, +0.5], [0.0, 1.0, -0.1], [0.0, 1.0, +1.1]]

    X_new = [[+0.0, 2.0, 0.5], [-1.0, 1.0, 0.0], [+0.0, 1.0, 1.5]]

    X_expected_0_1 = [[0.0, 0.0, 0.5], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]
    assert_array_almost_equal(X_trans, X_expected_0_1)

    X_trans_new = scaler.transform(X_new)
    X_expected_0_1_new = [[+0.0, 1.0, 0.500], [-1.0, 0.0, 0.083], [+0.0, 0.0, 1.333]]
    assert_array_almost_equal(X_trans_new, X_expected_0_1_new, decimal=2)

    # not default params
    X_expected_1_2 = [[1.0, 1.0, 1.5], [1.0, 1.0, 1.0], [1.0, 1.0, 2.0]]
    assert_array_almost_equal(X_trans, X_expected_1_2)

    # function interface
    X_trans = minmax_scale(X)
    X_trans = minmax_scale(X, feature_range=(1, 2))

def test_minmax_scale_axis1():
    X_trans = minmax_scale(X, axis=1)
    assert_array_almost_equal(np.min(X_trans, axis=1), 0)
    assert_array_almost_equal(np.max(X_trans, axis=1), 1)

def test_min_max_scaler_1d():
        scaler = MinMaxScaler(copy=True)
        X_scaled = scaler.fit(X).transform(X)

            assert_array_almost_equal(X_scaled.min(axis=0), np.zeros(n_features))
            assert_array_almost_equal(X_scaled.max(axis=0), np.zeros(n_features))
        else:
            assert_array_almost_equal(X_scaled.min(axis=0), 0.0)
            assert_array_almost_equal(X_scaled.max(axis=0), 1.0)

    X_scaled = scaler.fit(X).transform(X)
    assert X_scaled.min() >= 0.0
    assert X_scaled.max() <= 1.0

    # Function interface
    X_1d = X_1row.ravel()
    min_ = X_1d.min()
    max_ = X_1d.max()
        (X_1d - min_) / (max_ - min_), minmax_scale(X_1d, copy=True)
    )

def test_scaler_without_centering(sample_weight, sparse_container):

        StandardScaler().fit(X_sparse)

    scaler = StandardScaler(with_mean=False).fit(X, sample_weight=sample_weight)
    X_scaled = scaler.transform(X, copy=True)

    scaler_sparse = StandardScaler(with_mean=False).fit(
        X_sparse, sample_weight=sample_weight
    )
    X_sparse_scaled = scaler_sparse.transform(X_sparse, copy=True)
    assert not np.any(np.isnan(X_sparse_scaled.data))

    assert_array_almost_equal(scaler.mean_, scaler_sparse.mean_)
    assert_array_almost_equal(scaler.var_, scaler_sparse.var_)
    assert_array_almost_equal(scaler.scale_, scaler_sparse.scale_)
    assert_array_almost_equal(scaler.n_samples_seen_, scaler_sparse.n_samples_seen_)

            X_scaled.mean(axis=0), [0.0, -0.01, 2.24, -0.35, -0.78], 2
        )
        assert_array_almost_equal(X_scaled.std(axis=0), [0.0, 1.0, 1.0, 1.0, 1.0])

    X_sparse_scaled_mean, X_sparse_scaled_var = mean_variance_axis(X_sparse_scaled, 0)
    assert_array_almost_equal(X_sparse_scaled_mean, X_scaled.mean(axis=0))
    assert_array_almost_equal(X_sparse_scaled_var, X_scaled.var(axis=0))

    # Check that X has not been modified (copy)
    assert X_sparse_scaled is not X_sparse

    X_sparse_scaled_back = scaler_sparse.inverse_transform(X_sparse_scaled)
    assert X_sparse_scaled_back is not X_sparse
    assert X_sparse_scaled_back is not X_sparse_scaled
    assert_array_almost_equal(X_sparse_scaled_back.toarray(), X)

    if sparse_container in CSR_CONTAINERS:
        null_transform = StandardScaler(with_mean=False, with_std=False, copy=True)
        X_null = null_transform.fit_transform(X_sparse)
        assert_array_equal(X_null.data, X_sparse.data)
        X_orig = null_transform.inverse_transform(X_null)
        assert_array_equal(X_orig.data, X_sparse.data)

def test_scaler_n_samples_seen_with_nan(with_mean, with_std, sparse_container):
        [[0, 1, 3], [np.nan, 6, 10], [5, 4, np.nan], [8, 0, np.nan]], dtype=np.float64
    )

    if sparse.issparse(X) and with_mean:
        pytest.skip("'with_mean=True' cannot be used with sparse matrix.")

    transformer = StandardScaler(with_mean=with_mean, with_std=with_std)

    assert_array_equal(transformer.n_samples_seen_, np.array([3, 4, 2]))

def _check_identity_scalers_attributes(scaler_1, scaler_2):
    assert scaler_1.mean_ is scaler_2.mean_ is None
    assert scaler_1.var_ is scaler_2.var_ is None
    assert scaler_1.scale_ is scaler_2.scale_ is None
    assert scaler_1.n_samples_seen_ == scaler_2.n_samples_seen_

def test_scaler_return_identity(sparse_container):
    # test that the scaler return identity when with_mean and with_std are
    # False
    X_dense = np.array([[0, 1, 3], [5, 6, 0], [8, 0, 10]], dtype=np.float64)
    X_sparse = sparse_container(X_dense)

    transformer_dense = StandardScaler(with_mean=False, with_std=False)
    X_trans_dense = transformer_dense.fit_transform(X_dense)
    assert_allclose(X_trans_dense, X_dense)

    transformer_sparse = clone(transformer_dense)
    X_trans_sparse = transformer_sparse.fit_transform(X_sparse)
    assert_allclose_dense_sparse(X_trans_sparse, X_sparse)

    _check_identity_scalers_attributes(transformer_dense, transformer_sparse)

    transformer_dense.partial_fit(X_dense)
    transformer_sparse.partial_fit(X_sparse)

    transformer_dense.fit(X_dense)
    transformer_sparse.fit(X_sparse)

def test_scaler_int(sparse_container):
    # test that scaler converts integer input to floating
    # for both sparse and dense matrices
    X = rng.randint(20, size=(4, 5))
    X[:, 0] = 0  # first feature is always of zero

        scaler = StandardScaler(with_mean=False).fit(X)
        X_scaled = scaler.transform(X, copy=True)

        scaler_sparse = StandardScaler(with_mean=False).fit(X_sparse)
        X_sparse_scaled = scaler_sparse.transform(X_sparse, copy=True)

        X_scaled.mean(axis=0), [0.0, 1.109, 1.856, 21.0, 1.559], 2
    )

    X_sparse_scaled_mean, X_sparse_scaled_std = mean_variance_axis(
        X_sparse_scaled.astype(float), 0
    )
    assert_array_almost_equal(X_sparse_scaled_std, X_scaled.std(axis=0))

            X_null = null_transform.fit_transform(X_sparse)

def test_scaler_without_copy(sparse_container):
    # Check that StandardScaler.fit does not change input

    StandardScaler(copy=False).fit(X)
    assert_array_equal(X, X_copy)

    X_sparse_copy = X_sparse.copy()
    StandardScaler(with_mean=False, copy=False).fit(X_sparse)
    assert_array_equal(X_sparse.toarray(), X_sparse_copy.toarray())

def test_scale_sparse_with_mean_raise_exception(sparse_container):

    # check scaling and fit with direct calls on sparse data
        scale(X_sparse, with_mean=True)
        StandardScaler(with_mean=True).fit(X_sparse)

    # check transform and inverse_transform after a fit on a dense array
    scaler = StandardScaler(with_mean=True).fit(X)
        scaler.transform(X_sparse)

    X_transformed_sparse = sparse_container(scaler.transform(X))
        scaler.inverse_transform(X_transformed_sparse)

def test_scale_input_finiteness_validation():
    # Check if non finite inputs raise ValueError
    X = [[np.inf, 5, 6, 7, 8]]
        ValueError, match="Input contains infinity or a value too large"
    ):
        scale(X)

def test_robust_scaler_error_sparse():
    X_sparse = sparse.rand(1000, 10)
    scaler = RobustScaler(with_centering=True)
    err_msg = "Cannot center sparse matrices"
        scaler.fit(X_sparse)

def test_robust_scaler_attributes(X, with_centering, with_scaling):
    # check consistent type of attributes
    if with_centering and sparse.issparse(X):
        pytest.skip("RobustScaler cannot center sparse matrix")

    scaler = RobustScaler(with_centering=with_centering, with_scaling=with_scaling)

    if with_centering:
        assert isinstance(scaler.center_, np.ndarray)
    else:
        assert scaler.center_ is None
    if with_scaling:
        assert isinstance(scaler.scale_, np.ndarray)
    else:
        assert scaler.scale_ is None

def test_robust_scaler_col_zero_sparse(csr_container):
    # check that the scaler is working when there is not data materialized in a
    # column of a sparse matrix
    X = np.random.randn(10, 5)
    X[:, 0] = 0

    scaler = RobustScaler(with_centering=False)
    assert scaler.scale_[0] == pytest.approx(1)

    X_trans = scaler.transform(X)
    assert_allclose(X[:, [0]].toarray(), X_trans[:, [0]].toarray())

def test_robust_scaler_2d_arrays():
    # Test robust scaling of 2d array along first axis

    scaler = RobustScaler()

    assert_array_almost_equal(np.median(X_scaled, axis=0), 5 * [0.0])
    assert_array_almost_equal(X_scaled.std(axis=0)[0], 0)

def test_robust_scaler_equivalence_dense_sparse(density, strictly_signed):
    # Check the equivalence of the fitting with dense and sparse matrices
    X_sparse = sparse.rand(1000, 5, density=density).tocsc()
    if strictly_signed == "positive":
        X_sparse.data = np.abs(X_sparse.data)
    elif strictly_signed == "negative":
        X_sparse.data = -np.abs(X_sparse.data)
    elif strictly_signed == "zeros":
        X_sparse.data = np.zeros(X_sparse.data.shape, dtype=np.float64)

    scaler_sparse = RobustScaler(with_centering=False)
    scaler_dense = RobustScaler(with_centering=False)

    scaler_sparse.fit(X_sparse)
    scaler_dense.fit(X_dense)

    assert_allclose(scaler_sparse.scale_, scaler_dense.scale_)

def test_robust_scaler_transform_one_row_csr(csr_container):
    # Check RobustScaler on transforming csr matrix with one row
    single_row = np.array([[0.1, 1.0, 2.0, 0.0, -1.0]])
    scaler = scaler.fit(X)
    row_trans = scaler.transform(csr_container(single_row))
    row_expected = single_row / scaler.scale_
    assert_array_almost_equal(row_trans.toarray(), row_expected)
    row_scaled_back = scaler.inverse_transform(row_trans)
    assert_array_almost_equal(single_row, row_scaled_back.toarray())

def test_robust_scaler_iris():
    assert_array_almost_equal(np.median(X_trans, axis=0), 0)
    q = np.percentile(X_trans, q=(25, 75), axis=0)
    iqr = q[1] - q[0]
    assert_array_almost_equal(iqr, 1)

def test_robust_scaler_iris_quantiles():
    scaler = RobustScaler(quantile_range=(10, 90))
    q = np.percentile(X_trans, q=(10, 90), axis=0)
    q_range = q[1] - q[0]
    assert_array_almost_equal(q_range, 1)

def test_quantile_transform_iris(csc_container):
    # uniform output distribution
    transformer = QuantileTransformer(n_quantiles=30)
    X_trans_inv = transformer.inverse_transform(X_trans)
    # normal output distribution
    transformer = QuantileTransformer(n_quantiles=30, output_distribution="normal")
    # make sure it is possible to take the inverse of a sparse matrix
    # which contain negative value; this is the case in the iris dataset
    X_sparse_tran = transformer.fit_transform(X_sparse)
    X_sparse_tran_inv = transformer.inverse_transform(X_sparse_tran)
    assert_array_almost_equal(X_sparse.toarray(), X_sparse_tran_inv.toarray())

def test_quantile_transform_check_error(csc_container):
        [
            [0, 25, 50, 0, 0, 0, 75, 0, 0, 100],
            [2, 4, 0, 0, 6, 8, 0, 10, 0, 0],
            [0, 0, 2.6, 4.1, 0, 0, 2.3, 0, 9.5, 0.1],
        ]
    )
    X_neg = np.transpose(
        [
            [-2, 4, 0, 0, 6, 8, 0, 10, 0, 0],
        ]
    )
    X_neg = csc_container(X_neg)

    err_msg = (
        "The number of quantiles cannot be greater than "
        "the number of samples used. Got 1000 quantiles "
        "and 10 samples."
    )
        QuantileTransformer(subsample=10).fit(X)

    transformer = QuantileTransformer(n_quantiles=10)
    err_msg = "QuantileTransformer only accepts non-negative sparse matrices."

    X_bad_feat = np.transpose(
        [[0, 25, 50, 0, 0, 0, 75, 0, 0, 100], [0, 0, 2.6, 4.1, 0, 0, 2.3, 0, 9.5, 0.1]]
    )
    err_msg = (
        "X has 2 features, but QuantileTransformer is expecting 3 features as input."
    )
        transformer.inverse_transform(X_bad_feat)

    transformer = QuantileTransformer(n_quantiles=10).fit(X)
    # check that an error is raised if input is scalar
        transformer.transform(10)
    # check that a warning is raised is n_quantiles > n_samples
    transformer = QuantileTransformer(n_quantiles=100)
    warn_msg = "n_quantiles is set to n_samples"
    with pytest.warns(UserWarning, match=warn_msg) as record:
    assert transformer.n_quantiles_ == X.shape[0]

def test_quantile_transform_sparse_ignore_zeros(csc_container):
    X = np.array([[0, 1], [0, 0], [0, 2], [0, 2], [0, 1]])
    transformer = QuantileTransformer(ignore_implicit_zeros=True, n_quantiles=5)

    # dense case -> warning raise
        "'ignore_implicit_zeros' takes effect"
        " only with sparse matrix. This parameter has no"
        " effect."
    )

    X_expected = np.array([[0, 0], [0, 0], [0, 1], [0, 1], [0, 0]])
    X_trans = transformer.fit_transform(X_sparse)
    assert_almost_equal(X_expected, X_trans.toarray())

    # consider the case where sparse entries are missing values and user-given
    # zeros are to be considered
    X_data = np.array([0, 0, 1, 0, 2, 2, 1, 0, 1, 2, 0])
    X_col = np.array([0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1])
    X_row = np.array([0, 4, 0, 1, 2, 3, 4, 5, 6, 7, 8])
    X_sparse = csc_container((X_data, (X_row, X_col)))
        [
            [0.0, 0.5],
            [0.0, 1.0],
        ]
    )

    X_data = np.array([-1, -1, 1, 0, 0, 0, 1, -1, 1])
    X_col = np.array([0, 0, 1, 1, 1, 1, 1, 1, 1])
    X_row = np.array([0, 4, 0, 1, 2, 3, 4, 5, 6])
        [[0, 1], [0, 0.375], [0, 0.375], [0, 0.375], [0, 1], [0, 0], [0, 1]]
    )
        X_sparse.toarray(), transformer.inverse_transform(X_trans).toarray()
    )

    # check in conjunction with subsampling
    transformer = QuantileTransformer(
        ignore_implicit_zeros=True, n_quantiles=5, subsample=8, random_state=0
    )
    )

def test_quantile_transform_dense_toy():
        [[0, 2, 2.6], [25, 4, 4.1], [50, 6, 2.3], [75, 8, 9.5], [100, 10, 0.1]]
    )

    transformer = QuantileTransformer(n_quantiles=5)

    # using a uniform output, each entry of X should be map between 0 and 1
    # and equally spaced
    X_expected = np.tile(np.linspace(0, 1, num=5), (3, 1)).T
    assert_almost_equal(np.sort(X_trans, axis=0), X_expected)

        [
            [-1, 1, 0],
            [101, 11, 10],
        ]
    )
        [
            [0, 0, 0],
            [1, 1, 1],
        ]
    )
    assert_array_almost_equal(transformer.transform(X_test), X_expected)

def test_quantile_transform_subsampling():
    # Test that subsampling the input yield to a consistent results We check
    # that the computed quantiles are almost mapped to a [0, 1] vector where
    # values are equally spaced. The infinite norm is checked to be smaller
    # than a given threshold. This is repeated 5 times.

    # dense support
    n_samples = 1000000
    n_quantiles = 1000
    X = np.sort(np.random.sample((n_samples, 1)), axis=0)
    ROUND = 5
    inf_norm_arr = []
    for random_state in range(ROUND):
        transformer = QuantileTransformer(
            n_quantiles=n_quantiles,
            subsample=n_samples // 10,
        )
        diff = np.linspace(0, 1, n_quantiles) - np.ravel(transformer.quantiles_)
        inf_norm = np.max(np.abs(diff))
        assert inf_norm < 1e-2
        inf_norm_arr.append(inf_norm)
    # each random subsampling yield a unique approximation to the expected
    # linspace CDF
    assert len(np.unique(inf_norm_arr)) == len(inf_norm_arr)

    # sparse support

    X = sparse.rand(n_samples, 1, density=0.99, format="csc", random_state=0)
        )
        assert inf_norm < 1e-1

def test_quantile_transform_subsampling_disabled():
    """Check the behaviour of `QuantileTransformer` when `subsample=None`."""
    X = np.random.RandomState(0).normal(size=(200, 1))

    n_quantiles = 5
    transformer = QuantileTransformer(n_quantiles=n_quantiles, subsample=None).fit(X)

    expected_references = np.linspace(0, 1, n_quantiles)
    assert_allclose(transformer.references_, expected_references)
    expected_quantiles = np.quantile(X.ravel(), expected_references)
    assert_allclose(transformer.quantiles_.ravel(), expected_quantiles)

def test_quantile_transform_sparse_toy(csc_container):
        [
            [0.0, 2.0, 0.0],
            [25.0, 4.0, 0.0],
            [50.0, 0.0, 2.6],
            [0.0, 0.0, 4.1],
            [0.0, 6.0, 0.0],
            [0.0, 8.0, 0.0],
            [75.0, 0.0, 2.3],
            [0.0, 10.0, 0.0],
            [0.0, 0.0, 9.5],
            [100.0, 0.0, 0.1],
        ]
    )

    assert_array_almost_equal(np.min(X_trans.toarray(), axis=0), 0.0)
    assert_array_almost_equal(np.max(X_trans.toarray(), axis=0), 1.0)

    assert_array_almost_equal(X.toarray(), X_trans_inv.toarray())

    transformer_dense = QuantileTransformer(n_quantiles=10).fit(X.toarray())

    X_trans = transformer_dense.transform(X)

    X_trans_inv = transformer_dense.inverse_transform(X_trans)

def test_quantile_transform_axis1():
    X = np.array([[0, 25, 50, 75, 100], [2, 4, 6, 8, 10], [2.6, 4.1, 2.3, 9.5, 0.1]])

    X_trans_a0 = quantile_transform(X.T, axis=0, n_quantiles=5)
    X_trans_a1 = quantile_transform(X, axis=1, n_quantiles=5)
    assert_array_almost_equal(X_trans_a0, X_trans_a1.T)

def test_quantile_transform_bounds(csc_container):
    # Lower and upper bounds are manually mapped. We checked that in the case
    # of a constant feature and binary feature, the bounds are properly mapped.
    X_dense = np.array([[0, 0], [0, 0], [1, 0]])
    X_sparse = csc_container(X_dense)

    # check sparse and dense are consistent
    X_trans = QuantileTransformer(n_quantiles=3, random_state=0).fit_transform(X_dense)
    assert_array_almost_equal(X_trans, X_dense)
    X_trans_sp = QuantileTransformer(n_quantiles=3, random_state=0).fit_transform(
        X_sparse
    )
    assert_array_almost_equal(X_trans_sp.toarray(), X_dense)
    assert_array_almost_equal(X_trans, X_trans_sp.toarray())

    # check the consistency of the bounds by learning on 1 matrix
    # and transforming another
    X = np.array([[0, 1], [0, 0.5], [1, 0]])
    X1 = np.array([[0, 0.1], [0, 0.5], [1, 0.1]])
    transformer = QuantileTransformer(n_quantiles=3).fit(X)
    X_trans = transformer.transform(X1)
    assert_array_almost_equal(X_trans, X1)

    # check that values outside of the range learned will be mapped properly.
    X = np.random.random((1000, 1))
    transformer = QuantileTransformer()
    assert transformer.transform([[-10]]) == transformer.transform([[np.min(X)]])
    assert transformer.transform([[10]]) == transformer.transform([[np.max(X)]])
    assert transformer.inverse_transform([[-10]]) == transformer.inverse_transform(
        [[np.min(transformer.references_)]]
    )
    assert transformer.inverse_transform([[10]]) == transformer.inverse_transform(
        [[np.max(transformer.references_)]]
    )

def test_quantile_transform_and_inverse():
    X_1 = iris.data
    X_2 = np.array([[0.0], [BOUNDS_THRESHOLD / 10], [1.5], [2], [3], [3], [4]])
        transformer = QuantileTransformer(n_quantiles=1000, random_state=0)
        X_trans = transformer.fit_transform(X)
        X_trans_inv = transformer.inverse_transform(X_trans)
        assert_array_almost_equal(X, X_trans_inv, decimal=9)

def test_quantile_transform_nan():
    X = np.array([[np.nan, 0, 0, 1], [np.nan, np.nan, 0, 0.5], [np.nan, 1, 1, 0]])

    transformer = QuantileTransformer(n_quantiles=10, random_state=42)

    # check that the quantile of the first column is all NaN
    assert np.isnan(transformer.quantiles_[:, 0]).all()
    # all other column should not contain NaN
    assert not np.isnan(transformer.quantiles_[:, 1:]).any()

def test_quantile_transformer_sorted_quantiles(array_type):
    # https://github.com/scikit-learn/scikit-learn/issues/15733
    # Taken from upstream bug report:
    # https://github.com/numpy/numpy/issues/14685
    X = np.array([0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 1, 1, 9, 9, 9, 8, 8, 7] * 10)
    X = 0.1 * X.reshape(-1, 1)

    n_quantiles = 100
    qt = QuantileTransformer(n_quantiles=n_quantiles).fit(X)

    # Check that the estimated quantile thresholds are monotically
    # increasing:
    quantiles = qt.quantiles_[:, 0]
    assert len(quantiles) == 100
    assert all(np.diff(quantiles) >= 0)

def test_robust_scaler_invalid_range():
    for range_ in [
        (-1, 90),
        (-2, -3),
        (10, 101),
        (100.5, 101),
        (90, 50),
    ]:
        scaler = RobustScaler(quantile_range=range_)

        with pytest.raises(ValueError, match=r"Invalid quantile range: \("):
            scaler.fit(iris.data)

def test_scale_function_without_centering(csr_container):

    X_scaled = scale(X, with_mean=False)

    X_csr_scaled = scale(X_csr, with_mean=False)
    assert not np.any(np.isnan(X_csr_scaled.data))

    # test csc has same outcome
    X_csc_scaled = scale(X_csr.tocsc(), with_mean=False)
    assert_array_almost_equal(X_scaled, X_csc_scaled.toarray())

    # raises value error on axis != 0
        scale(X_csr, with_mean=False, axis=1)

        X_scaled.mean(axis=0), [0.0, -0.01, 2.24, -0.35, -0.78], 2
    )

    X_csr_scaled_mean, X_csr_scaled_std = mean_variance_axis(X_csr_scaled, 0)
    assert_array_almost_equal(X_csr_scaled_mean, X_scaled.mean(axis=0))
    assert_array_almost_equal(X_csr_scaled_std, X_scaled.std(axis=0))

    # null scale
    X_csr_scaled = scale(X_csr, with_mean=False, with_std=False, copy=True)
    assert_array_almost_equal(X_csr.toarray(), X_csr_scaled.toarray())

def test_robust_scale_axis1():
    X_trans = robust_scale(X, axis=1)
    assert_array_almost_equal(np.median(X_trans, axis=1), 0)
    q = np.percentile(X_trans, q=(25, 75), axis=1)

def test_robust_scale_1d_array():
    X = iris.data[:, 1]
    X_trans = robust_scale(X)
    assert_array_almost_equal(np.median(X_trans), 0)
    q = np.percentile(X_trans, q=(25, 75))

def test_robust_scaler_zero_variance_features():
    # Check RobustScaler on toy data with zero variance features

    # NOTE: for such a small sample size, what we expect in the third column
    # depends HEAVILY on the method used to calculate quantiles. The values
    # here were calculated to fit the quantiles produces by np.percentile
    # using numpy 1.9 Calculating quantiles with
    # scipy.stats.mstats.scoreatquantile or scipy.stats.mstats.mquantiles
    # would yield very different results!
    X_expected = [[0.0, 0.0, +0.0], [0.0, 0.0, -1.0], [0.0, 0.0, +1.0]]
    assert_array_almost_equal(X_trans, X_expected)

    # make sure new data gets transformed correctly
    X_expected_new = [[+0.0, 1.0, +0.0], [-1.0, 0.0, -0.83333], [+0.0, 0.0, +1.66667]]
    assert_array_almost_equal(X_trans_new, X_expected_new, decimal=3)

def test_robust_scaler_unit_variance():
    # Check RobustScaler with unit_variance=True on standard normal data with
    # outliers
    X = rng.randn(1000000, 1)
    X_with_outliers = np.vstack([X, np.ones((100, 1)) * 100, np.ones((100, 1)) * -100])

    quantile_range = (1, 99)
    robust_scaler = RobustScaler(quantile_range=quantile_range, unit_variance=True).fit(
        X_with_outliers
    )
    X_trans = robust_scaler.transform(X)

    assert robust_scaler.center_ == pytest.approx(0, abs=1e-3)
    assert robust_scaler.scale_ == pytest.approx(1, abs=1e-2)
    assert X_trans.std() == pytest.approx(1, abs=1e-2)

def test_maxabs_scaler_zero_variance_features(sparse_container):
    # Check MaxAbsScaler on toy data with zero variance features
    X = [[0.0, 1.0, +0.5], [0.0, 1.0, -0.3], [0.0, 1.0, +1.5], [0.0, 0.0, +0.0]]

    scaler = MaxAbsScaler()
        [0.0, 1.0, 1.0 / 3.0],
        [0.0, 1.0, -0.2],
        [0.0, 1.0, 1.0],
        [0.0, 0.0, 0.0],
    ]

    X_expected_new = [[+0.0, 2.0, 1.0 / 3.0], [-1.0, 1.0, 0.0], [+0.0, 1.0, 1.0]]

    assert_array_almost_equal(X_trans_new, X_expected_new, decimal=2)

    X_trans = maxabs_scale(X)

    # sparse data
    X_trans_sparse = scaler.fit_transform(X_sparse)
    ]
    assert_array_almost_equal(X_trans_sparse.toarray(), X_expected)
    X_trans_sparse_inv = scaler.inverse_transform(X_trans_sparse)
    assert_array_almost_equal(X, X_trans_sparse_inv.toarray())

def test_maxabs_scaler_large_negative_value():
    # Check MaxAbsScaler on toy data with a large negative value
    X = [
        [0.0, 1.0, +0.5, -1.0],
        [0.0, 1.0, -0.3, -0.5],
        [0.0, 1.0, -100.0, 0.0],
        [0.0, 0.0, +0.0, -2.0],
    ]

        [0.0, 1.0, 0.005, -0.5],
        [0.0, 1.0, -0.003, -0.25],
        [0.0, 1.0, -1.0, 0.0],
        [0.0, 0.0, 0.0, -1.0],
    ]

def test_maxabs_scaler_transform_one_row_csr(csr_container):
    # Check MaxAbsScaler on transforming csr matrix with one row
    X = csr_container([[0.5, 1.0, 1.0]])
    X_expected = csr_container([[1.0, 1.0, 1.0]])
    assert_array_almost_equal(X_trans.toarray(), X_expected.toarray())
    X_scaled_back = scaler.inverse_transform(X_trans)
    assert_array_almost_equal(X.toarray(), X_scaled_back.toarray())

def test_maxabs_scaler_1d():
        scaler = MaxAbsScaler(copy=True)

            assert_array_almost_equal(np.abs(X_scaled.max(axis=0)), np.ones(n_features))
        else:
            assert_array_almost_equal(np.abs(X_scaled.max(axis=0)), 1.0)

    assert_array_almost_equal(np.abs(X_scaled.max(axis=0)), 1.0)

    max_abs = np.abs(X_1d).max()
    assert_array_almost_equal(X_1d / max_abs, maxabs_scale(X_1d, copy=True))

def test_maxabs_scaler_partial_fit(csr_container):

        scaler_batch = MaxAbsScaler().fit(X)

        scaler_incr = MaxAbsScaler()
        scaler_incr_csr = MaxAbsScaler()
        scaler_incr_csc = MaxAbsScaler()
        for batch in gen_batches(n, chunk_size):
            X_csr = csr_container(X[batch])
            scaler_incr_csr = scaler_incr_csr.partial_fit(X_csr)
            X_csc = csr_container(X[batch])
            scaler_incr_csc = scaler_incr_csc.partial_fit(X_csc)

        assert_array_almost_equal(scaler_batch.max_abs_, scaler_incr.max_abs_)
        assert_array_almost_equal(scaler_batch.max_abs_, scaler_incr_csr.max_abs_)
        assert_array_almost_equal(scaler_batch.max_abs_, scaler_incr_csc.max_abs_)
        assert scaler_batch.n_samples_seen_ == scaler_incr_csr.n_samples_seen_
        assert scaler_batch.n_samples_seen_ == scaler_incr_csc.n_samples_seen_
        assert_array_almost_equal(scaler_batch.scale_, scaler_incr_csr.scale_)
        assert_array_almost_equal(scaler_batch.scale_, scaler_incr_csc.scale_)
        assert_array_almost_equal(scaler_batch.transform(X), scaler_incr.transform(X))

        scaler_batch = MaxAbsScaler().fit(X[batch0])
        scaler_incr = MaxAbsScaler().partial_fit(X[batch0])

        scaler_incr = MaxAbsScaler()  # Clean estimator
        for i, batch in enumerate(gen_batches(n, chunk_size)):
                i,
                n=n,
            )

def check_normalizer(norm, X_norm):
    """
    Convenient checking function for `test_normalizer_l1_l2_max` and
    `test_normalizer_l1_l2_max_non_csr`
    """
    if norm == "l1":
        row_sums = np.abs(X_norm).sum(axis=1)
        for i in range(3):
            assert_almost_equal(row_sums[i], 1.0)
        assert_almost_equal(row_sums[3], 0.0)
    elif norm == "l2":
            assert_almost_equal(la.norm(X_norm[i]), 1.0)
        assert_almost_equal(la.norm(X_norm[3]), 0.0)
    elif norm == "max":
        row_maxs = abs(X_norm).max(axis=1)
            assert_almost_equal(row_maxs[i], 1.0)
        assert_almost_equal(row_maxs[3], 0.0)

def test_normalizer_l1_l2_max(norm, csr_container):
    X_dense = rng.randn(4, 5)
    X_sparse_unpruned = csr_container(X_dense)

    # set the row number 3 to zero
    X_dense[3, :] = 0.0

    # set the row number 3 to zero without pruning (can happen in real life)
    indptr_3 = X_sparse_unpruned.indptr[3]
    indptr_4 = X_sparse_unpruned.indptr[4]
    X_sparse_unpruned.data[indptr_3:indptr_4] = 0.0

    # build the pruned variant using the regular constructor
    X_sparse_pruned = csr_container(X_dense)

    # check inputs that support the no-copy optim
    for X in (X_dense, X_sparse_pruned, X_sparse_unpruned):
        normalizer = Normalizer(norm=norm, copy=True)
        X_norm1 = normalizer.transform(X)
        assert X_norm1 is not X
        X_norm1 = toarray(X_norm1)

        normalizer = Normalizer(norm=norm, copy=False)
        X_norm2 = normalizer.transform(X)
        assert X_norm2 is X
        X_norm2 = toarray(X_norm2)

        for X_norm in (X_norm1, X_norm2):
            check_normalizer(norm, X_norm)

def test_normalizer_l1_l2_max_non_csr(norm, sparse_container):

    X = sparse_container(X_dense)
    X_norm = Normalizer(norm=norm, copy=False).transform(X)

    assert X_norm is not X
    assert sparse.issparse(X_norm) and X_norm.format == "csr"

    X_norm = toarray(X_norm)
    check_normalizer(norm, X_norm)

def test_normalizer_max_sign(csr_container):
    # check that we normalize by a positive number even for negative data
    # check for mixed data where the value with
    # largest magnitude is negative
    X_dense[2, abs(X_dense[2, :]).argmax()] *= -1
    X_all_neg = -np.abs(X_dense)
    X_all_neg_sparse = csr_container(X_all_neg)

    for X in (X_dense, X_all_neg, X_all_neg_sparse):
        normalizer = Normalizer(norm="max")
        X_norm = normalizer.transform(X)
        assert X_norm is not X
        X_norm = toarray(X_norm)
        assert_array_equal(np.sign(X_norm), np.sign(toarray(X)))

def test_normalize(csr_container):
    # Test normalize function
    # Only tests functionality not used by the tests for Normalizer.
    X = np.random.RandomState(37).randn(3, 2)
    assert_array_equal(normalize(X, copy=False), normalize(X.T, axis=0, copy=False).T)

    rs = np.random.RandomState(0)
    X_dense = rs.randn(10, 5)
    X_sparse = csr_container(X_dense)
    ones = np.ones((10))
    for X in (X_dense, X_sparse):
        for dtype in (np.float32, np.float64):
            for norm in ("l1", "l2"):
                X = X.astype(dtype)
                X_norm = normalize(X, norm=norm)
                assert X_norm.dtype == dtype

                X_norm = toarray(X_norm)
                if norm == "l1":
                    row_sums = np.abs(X_norm).sum(axis=1)
                else:
                    X_norm_squared = X_norm**2
                    row_sums = X_norm_squared.sum(axis=1)

                assert_array_almost_equal(row_sums, ones)

    # Test return_norm
    X_dense = np.array([[3.0, 0, 4.0], [1.0, 0.0, 0.0], [2.0, 3.0, 0.0]])
    for norm in ("l1", "l2", "max"):
        _, norms = normalize(X_dense, norm=norm, return_norm=True)
            assert_array_almost_equal(norms, np.array([7.0, 1.0, 5.0]))
            assert_array_almost_equal(norms, np.array([5.0, 1.0, 3.60555127]))
        else:
            assert_array_almost_equal(norms, np.array([4.0, 1.0, 3.0]))

    for norm in ("l1", "l2"):
            normalize(X_sparse, norm=norm, return_norm=True)
    _, norms = normalize(X_sparse, norm="max", return_norm=True)
    assert_array_almost_equal(norms, np.array([4.0, 1.0, 3.0]))

def test_binarizer(constructor):
    X_ = np.array([[1, 0, 5], [2, 3, -1]])
    X = constructor(X_.copy())

    binarizer = Binarizer(threshold=2.0, copy=True)
    X_bin = toarray(binarizer.transform(X))
    assert np.sum(X_bin == 0) == 4
    assert np.sum(X_bin == 1) == 2
    X_bin = binarizer.transform(X)
    assert sparse.issparse(X) == sparse.issparse(X_bin)

    binarizer = Binarizer(copy=True).fit(X)
    assert X_bin is not X
    assert np.sum(X_bin == 0) == 2
    assert np.sum(X_bin == 1) == 4

    binarizer = Binarizer(copy=True)
    X_bin = toarray(X_bin)

    binarizer = Binarizer(copy=False)
    if constructor is not list:
        assert X_bin is X

    X_float = np.array([[1, 0, 5], [2, 3, -1]], dtype=np.float64)
    X_bin = binarizer.transform(X_float)
        assert X_bin is X_float

    binarizer = Binarizer(threshold=-0.5, copy=True)
    if constructor in (np.array, list):
        X = constructor(X_.copy())

        X_bin = toarray(binarizer.transform(X))
        assert np.sum(X_bin == 0) == 1
        assert np.sum(X_bin == 1) == 5
        X_bin = binarizer.transform(X)

    # Cannot use threshold < 0 for sparse
    if constructor in CSC_CONTAINERS:
            binarizer.transform(constructor(X))

def test_binarizer_array_api_int(array_namespace, device, dtype_name):
    # Checks that Binarizer works with integer elements and float threshold
    for dtype_name_ in [dtype_name, "int32", "int64"]:
        X_np = np.reshape(np.asarray([0, 1, 2, 3, 4], dtype=dtype_name_), (-1, 1))
        X_xp = xp.asarray(X_np, device=device)
        binarized_np = Binarizer(threshold=2.5).fit_transform(X_np)
            binarized_xp = Binarizer(threshold=2.5).fit_transform(X_xp)
        assert_array_equal(_convert_to_numpy(binarized_xp, xp), binarized_np)

def test_center_kernel():
    # Test that KernelCenterer is equivalent to StandardScaler
    # in feature space
    scaler = StandardScaler(with_std=False)
    scaler.fit(X_fit)
    X_fit_centered = scaler.transform(X_fit)
    K_fit = np.dot(X_fit, X_fit.T)

    # center fit time matrix
    centerer = KernelCenterer()
    K_fit_centered = np.dot(X_fit_centered, X_fit_centered.T)
    K_fit_centered2 = centerer.fit_transform(K_fit)
    assert_array_almost_equal(K_fit_centered, K_fit_centered2)

    # center predict time matrix
    K_pred = np.dot(X_pred, X_fit.T)
    X_pred_centered = scaler.transform(X_pred)
    K_pred_centered = np.dot(X_pred_centered, X_fit_centered.T)
    K_pred_centered2 = centerer.transform(K_pred)
    assert_array_almost_equal(K_pred_centered, K_pred_centered2)

    # check the results coherence with the method proposed in:
    # B. Schölkopf, A. Smola, and K.R. Müller,
    # "Nonlinear component analysis as a kernel eigenvalue problem"
    # equation (B.3)

    # K_centered3 = (I - 1_M) K (I - 1_M)
    #             =  K - 1_M K - K 1_M + 1_M K 1_M
    ones_M = np.ones_like(K_fit) / K_fit.shape[0]
    K_fit_centered3 = K_fit - ones_M @ K_fit - K_fit @ ones_M + ones_M @ K_fit @ ones_M
    assert_allclose(K_fit_centered, K_fit_centered3)

    # K_test_centered3 = (K_test - 1'_M K)(I - 1_M)
    #                  = K_test - 1'_M K - K_test 1_M + 1'_M K 1_M
    ones_prime_M = np.ones_like(K_pred) / K_fit.shape[0]
    K_pred_centered3 = (
        K_pred - ones_prime_M @ K_fit - K_pred @ ones_M + ones_prime_M @ K_fit @ ones_M
    )
    assert_allclose(K_pred_centered, K_pred_centered3)

def test_kernelcenterer_non_linear_kernel():
    """Check kernel centering for non-linear kernel."""
    X, X_test = rng.randn(100, 50), rng.randn(20, 50)

    def phi(X):
        """Our mapping function phi."""
            [
                np.clip(X, a_min=0, a_max=None),
                -np.clip(X, a_min=None, a_max=0),
            ]
        )

    phi_X = phi(X)
    phi_X_test = phi(X_test)

    # centered the projection
    phi_X_center = scaler.fit_transform(phi_X)
    phi_X_test_center = scaler.transform(phi_X_test)

    # create the different kernel
    K = phi_X @ phi_X.T
    K_test = phi_X_test @ phi_X.T
    K_center = phi_X_center @ phi_X_center.T
    K_test_center = phi_X_test_center @ phi_X_center.T

    kernel_centerer = KernelCenterer()
    kernel_centerer.fit(K)

    assert_allclose(kernel_centerer.transform(K), K_center)
    assert_allclose(kernel_centerer.transform(K_test), K_test_center)

    # K_centered = (I - 1_M) K (I - 1_M)
    #            =  K - 1_M K - K 1_M + 1_M K 1_M
    ones_M = np.ones_like(K) / K.shape[0]
    K_centered = K - ones_M @ K - K @ ones_M + ones_M @ K @ ones_M
    assert_allclose(kernel_centerer.transform(K), K_centered)

    # K_test_centered = (K_test - 1'_M K)(I - 1_M)
    #                 = K_test - 1'_M K - K_test 1_M + 1'_M K 1_M
    ones_prime_M = np.ones_like(K_test) / K.shape[0]
    K_test_centered = (
        K_test - ones_prime_M @ K - K_test @ ones_M + ones_prime_M @ K @ ones_M
    )
    assert_allclose(kernel_centerer.transform(K_test), K_test_centered)

def test_cv_pipeline_precomputed():
    # Cross-validate a regression on four coplanar points with the same
    # value. Use precomputed kernel to ensure Pipeline with KernelCenterer
    # is treated as a pairwise operation.
    X = np.array([[3, 0, 0], [0, 3, 0], [0, 0, 3], [1, 1, 1]])
    y_true = np.ones((4,))
    K = X.dot(X.T)
    kcent = KernelCenterer()
    pipeline = Pipeline([("kernel_centerer", kcent), ("svr", SVR())])

    # did the pipeline set the pairwise attribute?
    assert pipeline.__sklearn_tags__().input_tags.pairwise

    # test cross-validation, score should be almost perfect
    # NB: this test is pretty vacuous -- it's mainly to test integration
    #     of Pipeline and KernelCenterer
    y_pred = cross_val_predict(pipeline, K, y_true, cv=2)
    assert_array_almost_equal(y_true, y_pred)

def test_fit_transform():
    for obj in (StandardScaler(), Normalizer(), Binarizer()):
        X_transformed = obj.fit(X).transform(X)
        X_transformed2 = obj.fit_transform(X)
        assert_array_equal(X_transformed, X_transformed2)

def test_add_dummy_feature():
    X = [[1, 0], [0, 1], [0, 1]]
    X = add_dummy_feature(X)
    assert_array_equal(X, [[1, 1, 0], [1, 0, 1], [1, 0, 1]])

def test_add_dummy_feature_sparse(sparse_container):
    X = sparse_container([[1, 0], [0, 1], [0, 1]])
    desired_format = X.format
    assert sparse.issparse(X) and X.format == desired_format, X
    assert_array_equal(X.toarray(), [[1, 1, 0], [1, 0, 1], [1, 0, 1]])

def test_fit_cold_start():
    X_2d = X[:, :2]

    # Scalers that have a partial_fit method
    scalers = [
        MinMaxScaler(),
        MaxAbsScaler(),
    ]

    for scaler in scalers:
        scaler.fit_transform(X)
        # with a different shape, this may break the scaler unless the internal
        # state is reset
        scaler.fit_transform(X_2d)

def test_power_transformer_notfitted(method):
    pt = PowerTransformer(method=method)
    X = np.abs(X_1col)
        pt.transform(X)
        pt.inverse_transform(X)

def test_power_transformer_inverse(method, standardize, X):
    # Make sure we get the original input when applying transform and then
    # inverse transform
    X = np.abs(X) if method == "box-cox" else X
    pt = PowerTransformer(method=method, standardize=standardize)
    X_trans = pt.fit_transform(X)
    assert_almost_equal(X, pt.inverse_transform(X_trans))

def test_power_transformer_1d():

    for standardize in [True, False]:
        pt = PowerTransformer(method="box-cox", standardize=standardize)

        X_trans = pt.fit_transform(X)
        X_trans_func = power_transform(X, method="box-cox", standardize=standardize)

        X_expected, lambda_expected = stats.boxcox(X.flatten())

        if standardize:
            X_expected = scale(X_expected)

        assert_almost_equal(X_expected.reshape(-1, 1), X_trans)
        assert_almost_equal(X_expected.reshape(-1, 1), X_trans_func)

        assert_almost_equal(X, pt.inverse_transform(X_trans))
        assert_almost_equal(lambda_expected, pt.lambdas_[0])

        assert len(pt.lambdas_) == X.shape[1]
        assert isinstance(pt.lambdas_, np.ndarray)

def test_power_transformer_2d():
    X = np.abs(X_2d)

        X_trans_class = pt.fit_transform(X)

        for X_trans in [X_trans_class, X_trans_func]:
            for j in range(X_trans.shape[1]):
                X_expected, lmbda = stats.boxcox(X[:, j].flatten())

                if standardize:
                    X_expected = scale(X_expected)

                assert_almost_equal(X_trans[:, j], X_expected)
                assert_almost_equal(lmbda, pt.lambdas_[j])

            # Test inverse transformation
            X_inv = pt.inverse_transform(X_trans)
            assert_array_almost_equal(X_inv, X)

def test_power_transformer_boxcox_strictly_positive_exception():
    # Exceptions should be raised for negative arrays and zero arrays when
    # method is boxcox

    pt = PowerTransformer(method="box-cox")
    pt.fit(np.abs(X_2d))
    X_with_negatives = X_2d
    not_positive_message = "strictly positive"

    with pytest.raises(ValueError, match=not_positive_message):
        pt.transform(X_with_negatives)

        pt.fit(X_with_negatives)

        power_transform(X_with_negatives, method="box-cox")

        pt.transform(np.zeros(X_2d.shape))

        pt.fit(np.zeros(X_2d.shape))

        power_transform(np.zeros(X_2d.shape), method="box-cox")

def test_power_transformer_yeojohnson_any_input(X):
    # Yeo-Johnson method should support any kind of input
    power_transform(X, method="yeo-johnson")

def test_power_transformer_shape_exception(method):
    pt.fit(X)

    # Exceptions should be raised for arrays with different num_columns
    # than during fitting
    wrong_shape_message = (
        r"X has \d+ features, but PowerTransformer is expecting \d+ features"
    )

    with pytest.raises(ValueError, match=wrong_shape_message):
        pt.transform(X[:, 0:1])

        pt.inverse_transform(X[:, 0:1])

def test_power_transformer_lambda_zero():
    pt = PowerTransformer(method="box-cox", standardize=False)
    X = np.abs(X_2d)[:, 0:1]

    # Test the lambda = 0 case
    pt.lambdas_ = np.array([0])
    X_trans = pt.transform(X)
    assert_array_almost_equal(pt.inverse_transform(X_trans), X)

def test_power_transformer_lambda_one():
    # Make sure lambda = 1 corresponds to the identity for yeo-johnson
    pt = PowerTransformer(method="yeo-johnson", standardize=False)

    pt.lambdas_ = np.array([1])
    assert_array_almost_equal(X_trans, X)

def test_optimization_power_transformer(method, lmbda):
    # Test the optimization procedure:
    # - set a predefined value for lambda
    # - apply inverse_transform to a normal dist (we get X_inv)
    # - apply fit_transform to X_inv (we get X_inv_trans)
    # - check that X_inv_trans is roughly equal to X

    X = rng.normal(loc=0, scale=1, size=(n_samples, 1))

    if method == "box-cox":
        # For box-cox, means that lmbda * y + 1 > 0 or y > - 1 / lmbda
        # Clip the data here to make sure the inequality is valid.
        X = np.clip(X, -1 / lmbda + 1e-5, None)

    pt = PowerTransformer(method=method, standardize=False)
    pt.lambdas_ = [lmbda]
    X_inv = pt.inverse_transform(X)

    X_inv_trans = pt.fit_transform(X_inv)

    assert_almost_equal(0, np.linalg.norm(X - X_inv_trans) / n_samples, decimal=2)
    assert_almost_equal(0, X_inv_trans.mean(), decimal=1)
    assert_almost_equal(1, X_inv_trans.std(), decimal=1)

def test_invserse_box_cox():
    # output nan if the input is invalid
    pt.lambdas_ = [0.5]
    X_inv = pt.inverse_transform([[-2.1]])
    assert np.isnan(X_inv)

def test_yeo_johnson_darwin_example():
    # test from original paper "A new family of power transformations to
    # improve normality or symmetry" by Yeo and Johnson.
    X = [6.1, -8.4, 1.0, 2.0, 0.7, 2.9, 3.5, 5.1, 1.8, 3.6, 7.0, 3.0, 9.3, 7.5, -6.0]
    lmbda = PowerTransformer(method="yeo-johnson").fit(X).lambdas_
    assert np.allclose(lmbda, 1.305, atol=1e-3)

def test_power_transformer_nans(method):
    # Make sure lambda estimation is not influenced by NaN values
    # and that transform() supports NaN silently

    pt.fit(X)
    lmbda_no_nans = pt.lambdas_[0]

    # concat nans at the end and check lambda stays the same
    X = np.concatenate([X, np.full_like(X, np.nan)])
    X = shuffle(X, random_state=0)

    pt.fit(X)
    lmbda_nans = pt.lambdas_[0]

    assert_almost_equal(lmbda_no_nans, lmbda_nans, decimal=5)

    assert_array_equal(np.isnan(X_trans), np.isnan(X))

def test_power_transformer_fit_transform(method, standardize):
    # check that fit_transform() and fit().transform() return the same values
    X = X_1col
        X = np.abs(X)

    pt = PowerTransformer(method, standardize=standardize)
    assert_array_almost_equal(pt.fit(X).transform(X), pt.fit_transform(X))

def test_power_transformer_copy_True(method, standardize):
    # Check that neither fit, transform, fit_transform nor inverse_transform
    # modify X inplace when copy=True
    X = X_1col

    X_original = X.copy()
    assert X is not X_original  # sanity checks
    assert_array_almost_equal(X, X_original)

    pt = PowerTransformer(method, standardize=standardize, copy=True)

    pt.fit(X)
    assert X_trans is not X

    X_inv_trans = pt.inverse_transform(X_trans)
    assert X_trans is not X_inv_trans

def test_power_transformer_copy_False(method, standardize):
    # check that when copy=False fit doesn't change X inplace but transform,
    # fit_transform and inverse_transform do.
    X = X_1col

    pt = PowerTransformer(method, standardize=standardize, copy=False)

    pt.fit(X)
    assert_array_almost_equal(X, X_original)  # fit didn't change X

    assert X_trans is X

    assert X_trans is X_inv_trans

def test_power_transformer_box_cox_raise_all_nans_col():
    """Check that box-cox raises informative when a column contains all nans.

    Non-regression test for gh-26303
    """
    X = rng.random_sample((4, 5))

    err_msg = "Column must not be all nan."

        pt.fit_transform(X)

def test_standard_scaler_sparse_partial_fit_finite_variance(X_2):
    X_1 = sparse.random(5, 1, density=0.8)
    scaler = StandardScaler(with_mean=False)
    scaler.fit(X_1).partial_fit(X_2)
    assert np.isfinite(scaler.var_[0])

def test_minmax_scaler_clip(feature_range):
    # test behaviour of the parameter 'clip' in MinMaxScaler
    scaler = MinMaxScaler(feature_range=feature_range, clip=True).fit(X)
    X_min, X_max = np.min(X, axis=0), np.max(X, axis=0)
    X_test = [np.r_[X_min[:2] - 10, X_max[2:] + 10]]
    X_transformed = scaler.transform(X_test)
        X_transformed,
        [[feature_range[0], feature_range[0], feature_range[1], feature_range[1]]],
    )

def test_standard_scaler_raise_error_for_1d_input():
    """Check that `inverse_transform` from `StandardScaler` raises an error
    with 1D array.
    https://github.com/scikit-learn/scikit-learn/issues/19518
    """
    scaler = StandardScaler().fit(X_2d)
    err_msg = "Expected 2D array, got 1D array instead"
        scaler.inverse_transform(X_2d[:, 0])

def test_power_transformer_significantly_non_gaussian():
    """Check that significantly non-Gaussian data before transforms correctly.

    For some explored lambdas, the transformed data may be constant and will
    be rejected. Non-regression test for
    https://github.com/scikit-learn/scikit-learn/issues/14959
    """

    X_non_gaussian = 1e6 * np.array(
        [0.6, 2.0, 3.0, 4.0] * 4 + [11, 12, 12, 16, 17, 20, 85, 90], dtype=np.float64
    pt = PowerTransformer()

        X_trans = pt.fit_transform(X_non_gaussian)

    assert not np.any(np.isnan(X_trans))
    assert X_trans.mean() == pytest.approx(0.0)
    assert X_trans.std() == pytest.approx(1.0)
    assert X_trans.min() > -2
    assert X_trans.max() < 2

def test_one_to_one_features(Transformer):
    """Check one-to-one transformers give correct feature names."""
    tr = Transformer().fit(iris.data)
    names_out = tr.get_feature_names_out(iris.feature_names)
    assert_array_equal(names_out, iris.feature_names)

def test_one_to_one_features_pandas(Transformer):

    tr = Transformer().fit(df)

    names_out_df_default = tr.get_feature_names_out()
    assert_array_equal(names_out_df_default, iris.feature_names)

    names_out_df_valid_in = tr.get_feature_names_out(iris.feature_names)
    assert_array_equal(names_out_df_valid_in, iris.feature_names)

    msg = re.escape("input_features is not equal to feature_names_in_")
        invalid_names = list("abcd")
        tr.get_feature_names_out(invalid_names)

def test_kernel_centerer_feature_names_out():
    """Test that kernel centerer `feature_names_out`."""

    X = rng.random_sample((6, 4))
    X_pairwise = linear_kernel(X)
    centerer = KernelCenterer().fit(X_pairwise)

    names_out = centerer.get_feature_names_out()
    samples_out2 = X_pairwise.shape[1]
    assert_array_equal(names_out, [f"kernelcenterer{i}" for i in range(samples_out2)])

def test_power_transformer_constant_feature(standardize):
    """Check that PowerTransfomer leaves constant features unchanged."""
    X = [[-2, 0, 2], [-2, 0, 2], [-2, 0, 2]]

    pt = PowerTransformer(method="yeo-johnson", standardize=standardize).fit(X)

    assert_allclose(pt.lambdas_, [1, 1, 1])

    Xft = pt.fit_transform(X)
    Xt = pt.transform(X)

    for Xt_ in [Xft, Xt]:
            assert_allclose(Xt_, np.zeros_like(X))
        else:
            assert_allclose(Xt_, X)

            [
            ]
        )

def test_polynomial_and_spline_array_order(est):
    """Test that output array has the given order."""

    def is_c_contiguous(a):
        return np.isfortran(a.T)

    assert is_c_contiguous(est().fit_transform(X))
    assert is_c_contiguous(est(order="C").fit_transform(X))
    assert np.isfortran(est(order="F").fit_transform(X))

def test_spline_transformer_input_validation(params, err_msg):
    """Test that we raise errors for invalid input in SplineTransformer."""

        SplineTransformer(**params).fit(X)

def test_spline_transformer_integer_knots(extrapolation):
    """Test that SplineTransformer accepts integer value knot positions."""
    X = np.arange(20).reshape(10, 2)
    knots = [[0, 1], [1, 2], [5, 5], [11, 10], [12, 11]]
    _ = SplineTransformer(
        degree=3, knots=knots, extrapolation=extrapolation

def test_spline_transformer_feature_names():
    """Test that SplineTransformer generates correct features name."""
    splt = SplineTransformer(n_knots=3, degree=3, include_bias=True).fit(X)
    feature_names = splt.get_feature_names_out()
        [
            "x0_sp_0",
            "x0_sp_1",
            "x0_sp_2",
            "x0_sp_3",
            "x0_sp_4",
            "x1_sp_0",
            "x1_sp_1",
            "x1_sp_2",
            "x1_sp_3",
            "x1_sp_4",
        ],
    )

    splt = SplineTransformer(n_knots=3, degree=3, include_bias=False).fit(X)
    feature_names = splt.get_feature_names_out(["a", "b"])
        [
            "a_sp_0",
            "a_sp_1",
            "a_sp_2",
            "a_sp_3",
            "b_sp_0",
            "b_sp_1",
            "b_sp_2",
            "b_sp_3",
        ],
    )

def test_split_transform_feature_names_extrapolation_degree(extrapolation, degree):
    """Test feature names are correct for different extrapolations and degree.

    Non-regression test for gh-25292.
    """
    splt = SplineTransformer(degree=degree, extrapolation=extrapolation).fit(X)
    assert len(feature_names) == splt.n_features_out_

    X_trans = splt.transform(X)
    assert X_trans.shape[1] == len(feature_names)

def test_spline_transformer_unity_decomposition(degree, n_knots, knots, extrapolation):
    """Test that B-splines are indeed a decomposition of unity.

    Splines basis functions must sum up to 1 per row, if we stay in between boundaries.
    """
    X = np.linspace(0, 1, 100)[:, None]
    # make the boundaries 0 and 1 part of X_train, for sure.
    X_train = np.r_[[[0]], X[::2, :], [[1]]]
    X_test = X[1::2, :]

    if extrapolation == "periodic":
        n_knots = n_knots + degree  # periodic splines require degree < n_knots

    splt = SplineTransformer(
        n_knots=n_knots,
        knots=knots,
        extrapolation=extrapolation,
    )
    splt.fit(X_train)
    for X in [X_train, X_test]:
        assert_allclose(np.sum(splt.transform(X), axis=1), 1)

def test_spline_transformer_linear_regression(bias, intercept):
    """Test that B-splines fit a sinusodial curve pretty well."""
    X = np.linspace(0, 10, 100)[:, None]
    y = np.sin(X[:, 0]) + 2  # +2 to avoid the value 0 in assert_allclose
        steps=[
            (
                "spline",
                SplineTransformer(
                    n_knots=15,
                    degree=3,
                    include_bias=bias,
                    extrapolation="constant",
                ),
            ),
            ("ols", LinearRegression(fit_intercept=intercept)),
        ]
    )
    assert_allclose(pipe.predict(X), y, rtol=1e-3)

def test_spline_transformer_get_base_knot_positions(
    knots, n_knots, sample_weight, expected_knots
):
    """Check the behaviour to find knot positions with and without sample_weight."""
    X = np.array([[0, 2], [0, 2], [2, 2], [3, 3], [4, 6], [5, 8], [6, 14]])
    base_knots = SplineTransformer._get_base_knot_positions(
        X=X, knots=knots, n_knots=n_knots, sample_weight=sample_weight
    )
    assert_allclose(base_knots, expected_knots)

def test_spline_transformer_periodic_linear_regression(bias, intercept):
    """Test that B-splines fit a periodic curve pretty well."""

    # "+ 3" to avoid the value 0 in assert_allclose
    def f(x):
        return np.sin(2 * np.pi * x) - np.sin(8 * np.pi * x) + 3

    X = np.linspace(0, 1, 101)[:, None]
        steps=[
            (
                "spline",
                    n_knots=20,
                    degree=3,
                    extrapolation="periodic",
                ),
            ),
        ]
    )
    pipe.fit(X, f(X[:, 0]))

    # Generate larger array to check periodic extrapolation
    X_ = np.linspace(-1, 2, 301)[:, None]
    predictions = pipe.predict(X_)
    assert_allclose(predictions, f(X_[:, 0]), atol=0.01, rtol=0.01)
    assert_allclose(predictions[0:100], predictions[100:200], rtol=1e-3)

def test_spline_transformer_periodic_spline_backport():
    """Test that the backport of extrapolate="periodic" works correctly"""
    X = np.linspace(-2, 3.5, 10)[:, None]
    degree = 2

    # Use periodic extrapolation backport in SplineTransformer
    transformer = SplineTransformer(
        degree=degree, extrapolation="periodic", knots=[[-1.0], [0.0], [1.0]]
    )
    Xt = transformer.fit_transform(X)

    # Use periodic extrapolation in BSpline
    coef = np.array([[1.0, 0.0], [0.0, 1.0], [1.0, 0.0], [0.0, 1.0]])
    spl = BSpline(np.arange(-3, 4), coef, degree, "periodic")
    Xspl = spl(X[:, 0])
    assert_allclose(Xt, Xspl)

def test_spline_transformer_periodic_splines_periodicity():
    """Test if shifted knots result in the same transformation up to permutation."""
    X = np.linspace(0, 10, 101)[:, None]

    transformer_1 = SplineTransformer(
        degree=3,
        extrapolation="periodic",
        knots=[[0.0], [1.0], [3.0], [4.0], [5.0], [8.0]],
    )

    transformer_2 = SplineTransformer(
        degree=3,
        knots=[[1.0], [3.0], [4.0], [5.0], [8.0], [9.0]],
    )

    Xt_1 = transformer_1.fit_transform(X)
    Xt_2 = transformer_2.fit_transform(X)

    assert_allclose(Xt_1, Xt_2[:, [4, 0, 1, 2, 3]])

def test_spline_transformer_periodic_splines_smoothness(degree):
    """Test that spline transformation is smooth at first / last knot."""
    X = np.linspace(-2, 10, 10_000)[:, None]

    )

    delta = (X.max() - X.min()) / len(X)
    tol = 10 * delta

    dXt = Xt
    # We expect splines of degree `degree` to be (`degree`-1) times
    # continuously differentiable. I.e. for d = 0, ..., `degree` - 1 the d-th
    # derivative should be continuous. This is the case if the (d+1)-th
    # numerical derivative is reasonably small (smaller than `tol` in absolute
    # value). We thus compute d-th numeric derivatives for d = 1, ..., `degree`
    # and compare them to `tol`.
    #
    # Note that the 0-th derivative is the function itself, such that we are
    # also checking its continuity.
    for d in range(1, degree + 1):
        # Check continuity of the (d-1)-th derivative
        diff = np.diff(dXt, axis=0)
        assert np.abs(diff).max() < tol
        # Compute d-th numeric derivative
        dXt = diff / delta

    # As degree `degree` splines are not `degree` times continuously
    # differentiable at the knots, the `degree + 1`-th numeric derivative
    # should have spikes at the knots.
    diff = np.diff(dXt, axis=0)
    assert np.abs(diff).max() > 1

def test_spline_transformer_extrapolation(bias, intercept, degree):
    """Test that B-spline extrapolation works correctly."""
    # we use a straight line for that
    X = np.linspace(-1, 1, 100)[:, None]
    y = X.squeeze()

    # 'constant'
        [
            [
                "spline",
                    n_knots=4,
                    degree=degree,
                ),
            ],
            ["ols", LinearRegression(fit_intercept=intercept)],
        ]
    )
    assert_allclose(pipe.predict([[-10], [5]]), [-1, 1])

    # 'linear'
        [
            [
                "spline",
                    extrapolation="linear",
                ),
            ],
        ]
    )
    assert_allclose(pipe.predict([[-10], [5]]), [-10, 5])

    # 'error'
        n_knots=4, degree=degree, include_bias=bias, extrapolation="error"
    )
    splt.fit(X)
    msg = "X contains values beyond the limits of the knots"
        splt.transform([[-10]])
        splt.transform([[5]])

def test_spline_transformer_kbindiscretizer(global_random_seed):
    """Test that a B-spline of degree=0 is equivalent to KBinsDiscretizer."""
    X = rng.randn(200).reshape(200, 1)
    n_bins = 5
    n_knots = n_bins + 1

        n_knots=n_knots, degree=0, knots="quantile", include_bias=True
    )
    splines = splt.fit_transform(X)

    )
    kbins = kbd.fit_transform(X)

    # Though they should be exactly equal, we test approximately with high
    # accuracy.
    assert_allclose(splines, kbins, rtol=1e-13)

def test_spline_transformer_sparse_output(
    degree, knots, extrapolation, include_bias, global_random_seed
):
    X = rng.randn(200).reshape(40, 5)

    splt_dense = SplineTransformer(
        include_bias=include_bias,
    )
    splt_sparse = SplineTransformer(
    )

    splt_dense.fit(X)
    splt_sparse.fit(X)

    X_trans_sparse = splt_sparse.transform(X)
    X_trans_dense = splt_dense.transform(X)
    assert sparse.issparse(X_trans_sparse) and X_trans_sparse.format == "csr"
    assert_allclose(X_trans_dense, X_trans_sparse.toarray())

    # extrapolation regime
    X_min = np.amin(X, axis=0)
    X_max = np.amax(X, axis=0)
    X_extra = np.r_[
        np.linspace(X_min - 5, X_min, 10), np.linspace(X_max, X_max + 5, 10)
    ]
    if extrapolation == "error":
        msg = "X contains values beyond the limits of the knots"
            splt_dense.transform(X_extra)
        msg = "Out of bounds"
            splt_sparse.transform(X_extra)
    else:
            splt_dense.transform(X_extra), splt_sparse.transform(X_extra).toarray()
        )

def test_spline_transformer_n_features_out(
    n_knots, include_bias, degree, extrapolation, sparse_output
):
    """Test that transform results in n_features_out_ features."""
        sparse_output=sparse_output,
    )
    X = np.linspace(0, 1, 10)[:, None]

    assert splt.transform(X).shape[1] == splt.n_features_out_

def test_polynomial_features_input_validation(params, err_msg):
    """Test that we raise errors for invalid input in PolynomialFeatures."""

        PolynomialFeatures(**params).fit(X)

def single_feature_degree3():
    X = np.arange(6)[:, np.newaxis]
    P = np.hstack([np.ones_like(X), X, X**2, X**3])
    return X, P

def test_polynomial_features_one_feature(
    single_feature_degree3,
    degree,
    include_bias,
    interaction_only,
    indices,
    X_container,
):
    """Test PolynomialFeatures on single feature up to degree 3."""
    X, P = single_feature_degree3
    tf = PolynomialFeatures(
        degree=degree, include_bias=include_bias, interaction_only=interaction_only
    ).fit(X)
    out = tf.transform(X)
        out = out.toarray()
    assert_allclose(out, P[:, indices])
    if tf.n_output_features_ > 0:
        assert tf.powers_.shape == (tf.n_output_features_, tf.n_features_in_)

def two_features_degree3():
    X = np.arange(6).reshape((3, 2))
    x1 = X[:, :1]
    x2 = X[:, 1:]
    P = np.hstack(
        [
            x1**0 * x2**0,  # 0
            x1**1 * x2**0,  # 1
            x1**0 * x2**1,  # 2
            x1**2 * x2**0,  # 3
            x1**1 * x2**1,  # 4
            x1**0 * x2**2,  # 5
            x1**3 * x2**0,  # 6
            x1**2 * x2**1,  # 7
            x1**1 * x2**2,  # 8
            x1**0 * x2**3,  # 9
        ]
    )
    return X, P

def test_polynomial_features_two_features(
    two_features_degree3,
    degree,
    indices,
):
    """Test PolynomialFeatures on 2 features up to degree 3."""
    X, P = two_features_degree3
    ).fit(X)

def test_polynomial_feature_names():
    X = np.arange(30).reshape(10, 3)
    poly = PolynomialFeatures(degree=2, include_bias=True).fit(X)
    feature_names = poly.get_feature_names_out()
        ["1", "x0", "x1", "x2", "x0^2", "x0 x1", "x0 x2", "x1^2", "x1 x2", "x2^2"],
    )
    assert len(feature_names) == poly.transform(X).shape[1]

    poly = PolynomialFeatures(degree=3, include_bias=False).fit(X)
    feature_names = poly.get_feature_names_out(["a", "b", "c"])
        [
            "a",
            "b",
            "c",
            "a^2",
            "a b",
            "a c",
            "b^2",
            "b c",
            "c^2",
            "a^3",
            "a^2 b",
            "a^2 c",
            "a b^2",
            "a b c",
            "a c^2",
            "b^3",
            "b^2 c",
            "b c^2",
            "c^3",
        ],
    )

    poly = PolynomialFeatures(degree=(2, 3), include_bias=False).fit(X)
        [
            "a^2",
            "a b",
            "a c",
            "b^2",
            "b c",
            "c^2",
            "a^3",
            "a^2 b",
            "a^2 c",
            "a b^2",
            "a b c",
            "a c^2",
            "b^3",
            "b^2 c",
            "b c^2",
            "c^3",
        ],
    )

    poly = PolynomialFeatures(
        degree=(3, 3), include_bias=True, interaction_only=True
    ).fit(X)
    assert_array_equal(["1", "a b c"], feature_names)

    # test some unicode
    poly = PolynomialFeatures(degree=1, include_bias=True).fit(X)
    feature_names = poly.get_feature_names_out(["\u0001F40D", "\u262e", "\u05d0"])
    assert_array_equal(["1", "\u0001F40D", "\u262e", "\u05d0"], feature_names)

def test_polynomial_features_csc_X(
    deg, include_bias, interaction_only, dtype, csc_container, global_random_seed
):
    X = rng.randint(0, 2, (100, 2))

    est = PolynomialFeatures(
        deg, include_bias=include_bias, interaction_only=interaction_only
    )
    Xt_csc = est.fit_transform(X_csc.astype(dtype))
    Xt_dense = est.fit_transform(X.astype(dtype))

    assert sparse.issparse(Xt_csc) and Xt_csc.format == "csc"
    assert Xt_csc.dtype == Xt_dense.dtype
    assert_array_almost_equal(Xt_csc.toarray(), Xt_dense)

def test_polynomial_features_csr_X(
    deg, include_bias, interaction_only, dtype, csr_container, global_random_seed
):

    )
    Xt_csr = est.fit_transform(X_csr.astype(dtype))
    Xt_dense = est.fit_transform(X.astype(dtype, copy=False))

    assert sparse.issparse(Xt_csr) and Xt_csr.format == "csr"
    assert Xt_csr.dtype == Xt_dense.dtype
    assert_array_almost_equal(Xt_csr.toarray(), Xt_dense)

def test_num_combinations(
    n_features, min_degree, max_degree, interaction_only, include_bias, csr_container
):
    """
    Test that n_output_features_ is calculated correctly.
    """
    x = csr_container(([1], ([0], [n_features - 1])))
        degree=max_degree,
        interaction_only=interaction_only,
    )
    est.fit(x)
    num_combos = est.n_output_features_

    combos = PolynomialFeatures._combinations(
        min_degree=0,
        max_degree=max_degree,
    )
    assert num_combos == sum([1 for _ in combos])

def test_polynomial_features_csr_X_floats(
):
    X_csr = csr_container(sparse_random(1000, 10, 0.5, random_state=global_random_seed))
    X = X_csr.toarray()

    )

def test_polynomial_features_csr_X_zero_row(
    zero_row_index, deg, interaction_only, csr_container, global_random_seed
):
    X_csr = csr_container(sparse_random(3, 10, 1.0, random_state=global_random_seed))
    X_csr[zero_row_index, :] = 0.0

    est = PolynomialFeatures(deg, include_bias=False, interaction_only=interaction_only)
    Xt_csr = est.fit_transform(X_csr)
    Xt_dense = est.fit_transform(X)

def test_polynomial_features_csr_X_degree_4(
    include_bias, interaction_only, csr_container, global_random_seed
):

        4, include_bias=include_bias, interaction_only=interaction_only
    )

def test_polynomial_features_csr_X_dim_edges(
    deg, dim, interaction_only, csr_container, global_random_seed
):
    X_csr = csr_container(
        sparse_random(1000, dim, 0.5, random_state=global_random_seed)
    )

    est = PolynomialFeatures(deg, interaction_only=interaction_only)

def test_csr_polynomial_expansion_index_overflow_non_regression(
    interaction_only, include_bias, csr_container
):
    """Check the automatic index dtype promotion to `np.int64` when needed.

    This ensures that sufficiently large input configurations get
    properly promoted to use `np.int64` for index and indptr representation
    while preserving data integrity. Non-regression test for gh-16803.

    Note that this is only possible for Python runtimes with a 64 bit address
    space. On 32 bit platforms, a `ValueError` is raised instead.
    """

    def degree_2_calc(d, i, j):
            return d * i - (i**2 + 3 * i) // 2 - 1 + j
        else:
            return d * i - (i**2 + i) // 2 + j

    n_samples = 13
    n_features = 120001
    data_dtype = np.float32
    data = np.arange(1, 5, dtype=np.int64)
    row = np.array([n_samples - 2, n_samples - 2, n_samples - 1, n_samples - 1])
    # An int64 dtype is required to avoid overflow error on Windows within the
    # `degree_2_calc` function.
    col = np.array(
        [n_features - 2, n_features - 1, n_features - 2, n_features - 1], dtype=np.int64
    )
        (data, (row, col)),
        dtype=data_dtype,
    )
    pf = PolynomialFeatures(
        interaction_only=interaction_only, include_bias=include_bias, degree=2
    )

    # Calculate the number of combinations a-priori, and if needed check for
    # the correct ValueError and terminate the test early.
    num_combinations = pf._num_combinations(
        max_degree=2,
        interaction_only=pf.interaction_only,
        include_bias=pf.include_bias,
    )
    if num_combinations > np.iinfo(np.intp).max:
        msg = (
            r"The output that would result from the current configuration would have"
            r" \d* features which is too large to be indexed"
        )
            pf.fit(X)
        return
    X_trans = pf.fit_transform(X)
    row_nonzero, col_nonzero = X_trans.nonzero()
    n_degree_1_features_out = n_features + include_bias
    max_degree_2_idx = (
        degree_2_calc(n_features, col[int(not interaction_only)], col[1])
        + n_degree_1_features_out
    )

    # Account for bias of all samples except last one which will be handled
    # separately since there are distinct data values before it
    data_target = [1] * (n_samples - 2) if include_bias else []
    col_nonzero_target = [0] * (n_samples - 2) if include_bias else []

        x = data[2 * i]
        y = data[2 * i + 1]
        x_idx = col[2 * i]
        y_idx = col[2 * i + 1]
            data_target.append(1)
            col_nonzero_target.append(0)
        data_target.extend([x, y])
        col_nonzero_target.extend(
            [x_idx + int(include_bias), y_idx + int(include_bias)]
        )
        if not interaction_only:
            data_target.extend([x * x, x * y, y * y])
            col_nonzero_target.extend(
                [
                    degree_2_calc(n_features, x_idx, x_idx) + n_degree_1_features_out,
                    degree_2_calc(n_features, x_idx, y_idx) + n_degree_1_features_out,
                    degree_2_calc(n_features, y_idx, y_idx) + n_degree_1_features_out,
                ]
            )
        else:
            data_target.extend([x * y])
            col_nonzero_target.append(
                degree_2_calc(n_features, x_idx, y_idx) + n_degree_1_features_out
            )

    nnz_per_row = int(include_bias) + 3 + 2 * int(not interaction_only)

    assert pf.n_output_features_ == max_degree_2_idx + 1
    assert X_trans.dtype == data_dtype
    assert X_trans.shape == (n_samples, max_degree_2_idx + 1)
    assert X_trans.indptr.dtype == X_trans.indices.dtype == np.int64
    # Ensure that dtype promotion was actually required:
    assert X_trans.indices.max() > np.iinfo(np.int32).max

    row_nonzero_target = list(range(n_samples - 2)) if include_bias else []
    row_nonzero_target.extend(
        [n_samples - 2] * nnz_per_row + [n_samples - 1] * nnz_per_row
    )

    assert_allclose(X_trans.data, data_target)
    assert_array_equal(row_nonzero, row_nonzero_target)
    assert_array_equal(col_nonzero, col_nonzero_target)

def test_csr_polynomial_expansion_index_overflow(
    degree, n_features, interaction_only, include_bias, csr_container
):
    """Tests known edge-cases to the dtype promotion strategy and custom
    Cython code, including a current bug in the upstream
    `scipy.sparse.hstack`.
    """
    data = [1.0]
    # Use int32 indices as much as we can
    indices_dtype = np.int32 if n_features - 1 <= np.iinfo(np.int32).max else np.int64
    row = np.array([0], dtype=indices_dtype)
    col = np.array([n_features - 1], dtype=indices_dtype)

    # First degree index
        n_features - 1 + int(include_bias),
    ]
    # Second degree index
    expected_indices.append(n_features * (n_features + 1) // 2 + expected_indices[0])
    # Third degree index
    expected_indices.append(
        n_features * (n_features + 1) * (n_features + 2) // 6 + expected_indices[1]
    )

    X = csr_container((data, (row, col)))
        interaction_only=interaction_only, include_bias=include_bias, degree=degree
    )

        max_degree=degree,
    )
        msg = (
        )
            pf.fit(X)
        return

    # When `n_features>=65535`, `scipy.sparse.hstack` may not use the right
    # dtype for representing indices and indptr if `n_features` is still
    # small enough so that each block matrix's indices and indptr arrays
    # can be represented with `np.int32`. We test `n_features==65535`
    # since it is guaranteed to run into this bug.
    if (
        sp_version < parse_version("1.9.2")
        and n_features == 65535
        and degree == 2
        and not interaction_only
        msg = r"In scipy versions `<1.9.2`, the function `scipy.sparse.hstack`"
            X_trans = pf.fit_transform(X)
        return

    expected_dtype = np.int64 if num_combinations > np.iinfo(np.int32).max else np.int32
    # Terms higher than first degree
    non_bias_terms = 1 + (degree - 1) * int(not interaction_only)
    expected_nnz = int(include_bias) + non_bias_terms
    assert X_trans.shape == (1, pf.n_output_features_)
    assert X_trans.indptr.dtype == X_trans.indices.dtype == expected_dtype
    assert X_trans.nnz == expected_nnz

    if include_bias:
        assert X_trans[0, 0] == pytest.approx(1.0)
    for idx in range(non_bias_terms):
        assert X_trans[0, expected_indices[idx]] == pytest.approx(1.0)

    offset = interaction_only * n_features
    if degree == 3:
        offset *= 1 + n_features
    assert pf.n_output_features_ == expected_indices[degree - 1] + 1 - offset

def test_csr_polynomial_expansion_too_large_to_index(
):
    n_features = np.iinfo(np.int64).max // 2
    row = [0]
    col = [n_features - 1]
        interaction_only=interaction_only, include_bias=include_bias, degree=(2, 2)
    )
    msg = (
        r"The output that would result from the current configuration would have \d*"
        r" features which is too large to be indexed"
    )
        pf.fit(X)
        pf.fit_transform(X)

def test_polynomial_features_behaviour_on_zero_degree(sparse_container):
    """Check that PolynomialFeatures raises error when degree=0 and include_bias=False,
    and output a single constant column when include_bias=True
    """
    poly = PolynomialFeatures(degree=0, include_bias=False)
    err_msg = (
        "Setting degree to zero and include_bias to False would result in"
        " an empty output array."
    )
        poly.fit_transform(X)

    poly = PolynomialFeatures(degree=(0, 0), include_bias=False)
    err_msg = (
        "Setting both min_degree and max_degree to zero and include_bias to"
        " False would result in an empty output array."
    )

    for _X in [X, sparse_container(X)]:
        poly = PolynomialFeatures(degree=0, include_bias=True)
        output = poly.fit_transform(_X)
        # convert to dense array if needed
        if sparse.issparse(output):
            output = output.toarray()
        assert_array_equal(output, np.ones((X.shape[0], 1)))

def test_sizeof_LARGEST_INT_t():
    # On Windows, scikit-learn is typically compiled with MSVC that
    # does not support int128 arithmetic (at the time of writing):
    # https://stackoverflow.com/a/6761962/163740
    if sys.platform == "win32" or (
        sys.maxsize <= 2**32 and sys.platform != "emscripten"
    ):
        expected_size = 8
    else:
        expected_size = 16

    assert _get_sizeof_LARGEST_INT_t() == expected_size

def test_csr_polynomial_expansion_windows_fail(csr_container):
    # Minimum needed to ensure integer overflow occurs while guaranteeing an
    # int64-indexable output.
    n_features = int(np.iinfo(np.int64).max ** (1 / 3) + 3)
    row = [0]

        n_features - 1,
    ]
        int(n_features * (n_features + 1) // 2 + expected_indices[0])
    )
        int(n_features * (n_features + 1) * (n_features + 2) // 6 + expected_indices[1])
    )

    pf = PolynomialFeatures(interaction_only=False, include_bias=False, degree=3)
        msg = (
            r"The output that would result from the current configuration would"
            r" have \d*"
            r" features which is too large to be indexed"
        )
            pf.fit_transform(X)
    else:
        X_trans = pf.fit_transform(X)
        for idx in range(3):
            assert X_trans[0, expected_indices[idx]] == pytest.approx(1.0)

    def f(x):

        else: